From 7d3ee9a6b7630819a50c7d0f9b97c75e46c34182 Mon Sep 17 00:00:00 2001 From: samueljd Date: Mon, 23 Oct 2023 17:22:22 +0530 Subject: [PATCH 1/7] directory change for editor --- .../TextEditor/BibleNavigationX.jsx | 251 ++++++++++++++++++ .../EditorPage/TextEditor/Buttons.jsx | 107 ++++++++ .../EditorPage/TextEditor/Editor.jsx | 153 +++++++++++ .../EditorPage/TextEditor/EditorMenuBar.jsx | 96 +++++++ .../EditorPage/TextEditor/InsertMenu.jsx | 39 +++ .../EditorPage/TextEditor/Popup.jsx | 120 +++++++++ .../EditorPage/TextEditor/PopupButton.jsx | 44 +++ .../EditorPage/TextEditor/RecursiveBlock.jsx | 123 +++++++++ .../EditorPage/TextEditor/ReferenceEditor.jsx | 115 ++++++++ .../TextEditor/ReferenceRecursiveBlock.jsx | 74 ++++++ .../TextEditor/ReferenceScribex.jsx | 94 +++++++ .../hooks/useIntersectionObserver.js | 46 ++++ .../EditorPage/TextEditor/index.jsx | 123 +++++++++ .../TextEditor/utils/IntersectionObserver.js | 20 ++ .../TextEditor/utils/getReferences.js | 32 +++ 15 files changed, 1437 insertions(+) create mode 100644 renderer/src/components/EditorPage/TextEditor/BibleNavigationX.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/Buttons.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/Editor.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/InsertMenu.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/Popup.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/PopupButton.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/ReferenceEditor.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/ReferenceRecursiveBlock.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/ReferenceScribex.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/hooks/useIntersectionObserver.js create mode 100644 renderer/src/components/EditorPage/TextEditor/index.jsx create mode 100644 renderer/src/components/EditorPage/TextEditor/utils/IntersectionObserver.js create mode 100644 renderer/src/components/EditorPage/TextEditor/utils/getReferences.js diff --git a/renderer/src/components/EditorPage/TextEditor/BibleNavigationX.jsx b/renderer/src/components/EditorPage/TextEditor/BibleNavigationX.jsx new file mode 100644 index 000000000..4be5cb2b0 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/BibleNavigationX.jsx @@ -0,0 +1,251 @@ +import PropTypes from 'prop-types'; +import { Dialog, Transition } from '@headlessui/react'; +import React, { + Fragment, useContext, useEffect, useRef, useState, +} from 'react'; +import { XMarkIcon, ChevronDownIcon } from '@heroicons/react/24/solid'; +import * as localforage from 'localforage'; +import SelectBook from '@/components/EditorPage/Navigation/reference/SelectBook'; +import SelectVerse from '@/components/EditorPage/Navigation/reference/SelectVerse'; + +import { ReferenceContext } from '@/components/context/ReferenceContext'; + +export default function BibleNavigationX(props) { + const { + showVerse, chapterNumber, setChapterNumber, verseNumber, setVerseNumber, + } = props; + const supportedBooks = null; // if empty array or null then all books available + + const { + state: { + bookId, + bookList, + bookName, + chapter, + verse, + chapterList, + verseList, + languageId, + // closeNavigation, + }, actions: { + onChangeBook, + onChangeChapter, + onChangeVerse, + applyBooksFilter, + setCloseNavigation, + }, + } = useContext(ReferenceContext); + + useEffect(() => { + applyBooksFilter(supportedBooks); + }, [applyBooksFilter, supportedBooks]); + + const [openBook, setOpenBook] = useState(false); + const [openVerse, setOpenVerse] = useState(false); + const cancelButtonRef = useRef(null); + + const [multiSelectVerse] = useState(false); + const [multiSelectBook] = useState(false); + const [selectedVerses, setSelectedVerses] = useState([]); + const [selectedBooks, setSelectedBooks] = useState([]); + const [verselectActive, setVerseSelectActive] = useState(false); + + function closeBooks() { + setOpenBook(false); + } + + function openBooks() { + setSelectedBooks([(bookId.toUpperCase())]); + setOpenBook(true); + } + + function closeVerses() { + setOpenVerse(false); + if (multiSelectVerse) { setVerseSelectActive(true); } + } + + function selectBook() { + setOpenBook(false); + setOpenVerse(true); + if (multiSelectVerse) { setSelectedVerses([]); } + } + + useEffect(() => { + const getSupportedBooks = async () => { + const refs = await localforage.getItem('refBibleBurrito'); + refs?.forEach((ref) => { + if (languageId !== null) { + if (ref.value.languages[0].tag === languageId) { + const supportedBooks = []; + Object.entries((ref.value.type.flavorType.currentScope)).forEach( + ([key]) => { + supportedBooks.push(key.toLowerCase()); + }, + ); + applyBooksFilter(supportedBooks); + } + } + }); + }; + getSupportedBooks(); + }, [languageId, applyBooksFilter]); + + useEffect(() => { + async function setReference() { + await localforage.setItem('navigationHistory', [bookId, chapter, verse]); + } + setReference(); + }, [bookId, chapter, verse]); + + useEffect(() => { + if (openBook === false && openVerse === false) { + setCloseNavigation(true); + } + if (openBook || openVerse) { + setCloseNavigation(false); + } + }, [openVerse, openBook, setCloseNavigation]); + + return ( + <> +
+
+ {bookName} + + + {chapterNumber} + + + {verseNumber} + {showVerse + && ( + + {multiSelectVerse + ? selectedVerses.join() + : verse} + + )} +
+
+ + + +
+
+ + + +
+
+ +
+
+ + + + + +
+ +
+ + + +
+
+ +
+
+ + ); +} + +BibleNavigationX.propTypes = { + showVerse: PropTypes.bool, +}; diff --git a/renderer/src/components/EditorPage/TextEditor/Buttons.jsx b/renderer/src/components/EditorPage/TextEditor/Buttons.jsx new file mode 100644 index 000000000..758a16b69 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/Buttons.jsx @@ -0,0 +1,107 @@ +/* eslint-disable no-unused-vars */ +import React, { useState } from 'react'; + +import RectangleStackIcon from '@/icons/Xelah/RectangleStack.svg'; +import ArrowDownOnSquareIcon from '@/icons/Xelah/ArrowDownOnSquare.svg'; +import Bars2Icon from '@/icons/Xelah/Bars2.svg'; +import Bars4Icon from '@/icons/Xelah/Bars4.svg'; +import ArrowUturnLeftIcon from '@/icons/Xelah/ArrowUturnLeft.svg'; +import ArrowUturnRightIcon from '@/icons/Xelah/ArrowUturnRight.svg'; +import PencilIcon from '@/icons/Common/Pencil.svg'; +import Copy from '@/icons/Xelah/Copy.svg'; +import Paste from '@/icons/Xelah/Paste.svg'; + +export const classNames = (...classes) => classes.filter(Boolean).join(' '); + +export default function Buttons(props) { + const [sectionable, setSectionableState] = useState(false); + const [blockable, setBlockableState] = useState(true); + const [editable, setEditableState] = useState(true); + const [preview, setPreviewState] = useState(false); + const { + bookCode, + undo, + redo, + setSectionable, + setBlockable, + setEditable, + setPreview, + exportUsfm, + } = props; + + const onSectionable = () => { + setSectionableState(!sectionable); + setSectionable(!sectionable); + }; + const onBlockable = () => { + setBlockableState(!blockable); + setBlockable(!blockable); + }; + const onEditable = () => { + setEditableState(!editable); + setEditable(!editable); + }; + const onPreview = () => { + setPreviewState(!preview); + setPreview(!preview); + }; + + return ( + <> + + + + {blockable ? ( + + ) + : ( + + )} + + undo()} + title="Undo" + /> + redo()} + title="Redo" + /> + exportUsfm(bookCode)} + title="Save" + /> + + ); +} diff --git a/renderer/src/components/EditorPage/TextEditor/Editor.jsx b/renderer/src/components/EditorPage/TextEditor/Editor.jsx new file mode 100644 index 000000000..c53aa4c09 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/Editor.jsx @@ -0,0 +1,153 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { HtmlPerfEditor } from '@xelah/type-perf-html'; + +import LoadingScreen from '@/components/Loading/LoadingScreen'; +import { ReferenceContext } from '@/components/context/ReferenceContext'; +import { ProjectContext } from '@/components/context/ProjectContext'; +import EmptyScreen from '@/components/Loading/EmptySrceen'; +import { + insertVerseNumber, insertChapterNumber, insertFootnote, insertXRef, insertHeading, +} from '@/util/cursorUtils'; +// eslint-disable-next-line import/no-unresolved, import/extensions +import { useAutoSaveIndication } from '@/hooks2/useAutoSaveIndication'; +import RecursiveBlock from './RecursiveBlock'; +// eslint-disable-next-line import/no-unresolved, import/extensions +import { useIntersectionObserver } from './hooks/useIntersectionObserver'; + +export default function Editor(props) { + const { + sequenceIds, + isSaving, + htmlPerf, + sectionable, + blockable, + editable, + preview, + verbose, + bookChange, + setBookChange, + addSequenceId, + saveHtmlPerf, + setGraftSequenceId, + bookAvailable, + setChapterNumber, + setVerseNumber, + triggerVerseInsert, + newVerChapNumber, + insertVerseRChapter, + selectedText, + setSelectedText, + } = props; + + const [caretPosition, setCaretPosition] = useState(); + const { + state: { + chapter, selectedFont, fontSize, projectScriptureDir, + }, + } = useContext(ReferenceContext); + + const { + states: { openSideBar, scrollLock }, + actions: { setOpenSideBar, setSideBarTab }, + } = useContext(ProjectContext); + + const sequenceId = sequenceIds.at(-1); + const style = isSaving ? { cursor: 'progress' } : {}; + + const handlers = { + onBlockClick: ({ element }) => { + const _sequenceId = element.dataset.target; + const { tagName } = element; + if (_sequenceId) { + if (tagName === 'SPAN' && element.dataset.subtype === 'footnote') { + setGraftSequenceId(_sequenceId); + setOpenSideBar(!openSideBar); + setSideBarTab('footnotes'); + } + if (tagName === 'SPAN' && element.dataset.subtype === 'xref') { + setGraftSequenceId(_sequenceId); + setOpenSideBar(!openSideBar); + setSideBarTab('xref'); + } + } else { + setSideBarTab(''); + setGraftSequenceId(null); + } + }, + }; + useEffect(() => { + setBookChange(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [htmlPerf]); + + useAutoSaveIndication(isSaving); + + function onReferenceSelected({ chapter, verse }) { + chapter && setChapterNumber(chapter); + verse && setVerseNumber(verse); + } + + useEffect(() => { + if (insertVerseRChapter === 'Verse') { + insertVerseNumber(caretPosition, newVerChapNumber); + } + if (insertVerseRChapter === 'Chapter') { + insertChapterNumber(caretPosition, newVerChapNumber); + } + if (insertVerseRChapter === 'Footnote') { + insertFootnote(caretPosition, newVerChapNumber, selectedText); + } + if (insertVerseRChapter === 'Cross Reference') { + insertXRef(caretPosition, newVerChapNumber, selectedText); + } + if (insertVerseRChapter === 'Section Heading') { + insertHeading(caretPosition, newVerChapNumber, selectedText); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [triggerVerseInsert]); + + // hook for autoscrolling to the current chapter + useIntersectionObserver(scrollLock, setChapterNumber); + + const _props = { + htmlPerf, + onHtmlPerf: saveHtmlPerf, + chapterIndex: chapter, + sequenceIds, + addSequenceId, + components: { + block: (__props) => RecursiveBlock({ + htmlPerf, onHtmlPerf: saveHtmlPerf, sequenceIds, addSequenceId, onReferenceSelected, setCaretPosition, setSelectedText, ...__props, + }), + }, + options: { + sectionable, + blockable, + editable, + preview, + }, + decorators: {}, + verbose, + handlers, + }; + + return ( +
1.3) ? 1.5 : '', + direction: `${projectScriptureDir === 'RTL' ? 'rtl' : 'auto'}`, + }} + className="border-l-2 border-r-2 border-secondary pb-16 overflow-auto h-full scrollbars-width leading-8" + > +
+ {!bookAvailable && } + {bookAvailable && (!sequenceId || bookChange) && } + {bookAvailable && sequenceId && !bookChange && ( + + )} +
+
+ ); +} diff --git a/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx b/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx new file mode 100644 index 000000000..20f1463dc --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx @@ -0,0 +1,96 @@ +import React, { useContext } from 'react'; + +import { ProjectContext } from '@/components/context/ProjectContext'; +import MenuDropdown from '@/components/MenuDropdown/MenuDropdown'; +import { LockClosedIcon, BookmarkIcon, LockOpenIcon } from '@heroicons/react/24/outline'; +// import BibleNavigationX from '@/components/EditorPage/TextEditor/BibleNavigationX'; +import BibleNavigationX from './BibleNavigationX'; +import Buttons from './Buttons'; +import InsertMenu from './InsertMenu'; + +export default function EditorMenuBar(props) { + const { + selectedFont, + setInsertNumber, + setInsertVerseRChapter, + setTriggerVerseInsert, + triggerVerseInsert, + chapterNumber, + setChapterNumber, + verseNumber, + setVerseNumber, + selectedText, + setSelectedFont, + } = props; + + const { + states: { scrollLock }, + actions: { setScrollLock }, + } = useContext(ProjectContext); + + const handleClick = (number, title) => { + setInsertNumber(number); + setInsertVerseRChapter(title); + setTriggerVerseInsert(!triggerVerseInsert); + }; + return ( +
+
+ +
+ Editor +
+
+
+ {scrollLock === true ? ( +
+
+
+
+
+
+
+ +
+
+ + +
+
+
+ ); +} diff --git a/renderer/src/components/EditorPage/TextEditor/InsertMenu.jsx b/renderer/src/components/EditorPage/TextEditor/InsertMenu.jsx new file mode 100644 index 000000000..081b93086 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/InsertMenu.jsx @@ -0,0 +1,39 @@ +import PopupButton from './PopupButton'; + +export default function InsertMenu({ handleClick: handleButtonClick, selectedText }) { + const handleClick = (number, title) => { + handleButtonClick(number, title); + }; + return ( +
+ + + + + +
+ ); +} diff --git a/renderer/src/components/EditorPage/TextEditor/Popup.jsx b/renderer/src/components/EditorPage/TextEditor/Popup.jsx new file mode 100644 index 000000000..498401eee --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/Popup.jsx @@ -0,0 +1,120 @@ +import React, { useState, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Dialog, Transition } from '@headlessui/react'; + +const Popup = ({ + handleClose, handleButtonClick, title, isPopupOpen, selectedText, +}) => { + const [number, setNumber] = useState(''); + // console.log({ title }, "title"); + const handleInputChange = (event) => { + setNumber(event.target.value); + }; + + const handleNumberInputChange = (e) => { + setNumber(e.target.value.replace(/[^0-9]/g, '')); + }; + const handleSubmit = () => { + handleButtonClick(number, title); + handleClose(); + }; + + return ( + + +
+ + + + +
+
+
+ ); +}; + +Popup.propTypes = { + handleClose: PropTypes.func.isRequired, + handleButtonClick: PropTypes.func.isRequired, +}; + +export default Popup; diff --git a/renderer/src/components/EditorPage/TextEditor/PopupButton.jsx b/renderer/src/components/EditorPage/TextEditor/PopupButton.jsx new file mode 100644 index 000000000..49615bed8 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/PopupButton.jsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import Popup from './Popup'; + +const PopupButton = ({ + handleClick, title, selectedText, icon, +}) => { + const [isPopupOpen, setIsPopupOpen] = useState(false); + + const handlePopupOpen = () => { + setIsPopupOpen(true); + }; + + const handlePopupClose = () => { + setIsPopupOpen(false); + }; + + const handleButtonClick = (number, title) => { + handleClick(number, title); + }; + + return ( +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + onFocus={(e) => e.stopPropagation()} + onMouseOver={(e) => e.stopPropagation()} + > + + {isPopupOpen && ( + + )} +
+ ); +}; + +export default PopupButton; diff --git a/renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx b/renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx new file mode 100644 index 000000000..5561aeef9 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx @@ -0,0 +1,123 @@ +/* eslint-disable react/prop-types */ +/* eslint-disable no-unused-vars */ +import React, { useEffect, useState } from 'react'; +import { HtmlPerfEditor } from '@xelah/type-perf-html'; +import { getCurrentCursorPosition } from '@/util/cursorUtils'; +import { getCurrentVerse, getCurrentChapter } from '@/components/EditorPage/TextEditor/utils/getReferences'; + +const getTarget = ({ content }) => { + const div = document.createElement('div'); + div.innerHTML = content; + + const { target } = div.firstChild?.dataset || {}; + + return target; +}; + +export default function RecursiveBlock({ + htmlPerf, + onHtmlPerf, + sequenceIds, + addSequenceId, + options, + content, + style, + contentEditable, + index, + verbose, + setFootNote, + bookId, + onReferenceSelected, + setCaretPosition, + setSelectedText, + ...props +}) { + const [currentVerse, setCurrentVerse] = useState(null); + + const updateCursorPosition = () => { + const cursorPosition = getCurrentCursorPosition('editor'); + setCaretPosition(cursorPosition); + }; + + const checkReturnKeyPress = (event) => { + const activeTextArea = document.activeElement; + if (event.key === 'Enter') { + if (activeTextArea.children.length > 1) { + const lineBreak = activeTextArea.children[1]?.outerHTML; + activeTextArea.children[1].outerHTML = lineBreak.replace(//gi, ' '); + } + } + // BACKSPACE DISABLE + if (event.keyCode === 8) { + const range = document.getSelection().getRangeAt(0); + const selectedNode = range.startContainer; + const prevNode = selectedNode.previousSibling; + if (prevNode && prevNode.dataset.attsNumber !== currentVerse) { + event.preventDefault(); + } + prevNode ? setCurrentVerse(prevNode.dataset.attsNumber) : {}; + } + updateCursorPosition(); + }; + + function handleSelection() { + let selectedText = ''; + if (window.getSelection) { + selectedText = window.getSelection().toString(); + } else if (document.selection && document.selection.type !== 'Control') { + selectedText = document.selection.createRange().text; + } + if (selectedText) { + setSelectedText(selectedText); + } + } + + const checkCurrentVerse = () => { + if (document.getSelection().rangeCount >= 1 && onReferenceSelected) { + const range = document.getSelection().getRangeAt(0); + const selectedNode = range.startContainer; + const verse = getCurrentVerse(selectedNode); + const chapter = getCurrentChapter(selectedNode); + onReferenceSelected({ bookId, chapter, verse }); + } + updateCursorPosition(); + handleSelection(); + }; + + let component; + + const editable = !!content.match(/data-type="paragraph"/); + + if (editable) { + component = ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
+ ); + } + if (!editable) { + const sequenceId = getTarget({ content }); + + if (sequenceId && !options.preview) { + const _props = { + sequenceIds: [...sequenceIds, sequenceId], + addSequenceId, + htmlPerf, + onHtmlPerf, + onInput: props?.onInput, + options, + }; + component = ; + } + component ||=
; + } + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{component}; +} diff --git a/renderer/src/components/EditorPage/TextEditor/ReferenceEditor.jsx b/renderer/src/components/EditorPage/TextEditor/ReferenceEditor.jsx new file mode 100644 index 000000000..f1ef0fcc8 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/ReferenceEditor.jsx @@ -0,0 +1,115 @@ +/* eslint-disable no-unused-vars */ +import React, { useContext, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { HtmlPerfEditor } from '@xelah/type-perf-html'; +import LoadingScreen from '@/components/Loading/LoadingScreen'; +import SaveIndicator from '@/components/Loading/SaveIndicator'; +import { ReferenceContext } from '@/components/context/ReferenceContext'; +import { ProjectContext } from '@/components/context/ProjectContext'; +import EmptyScreen from '@/components/Loading/EmptySrceen'; +import ReferenceRecursiveBlock from './ReferenceRecursiveBlock'; + +export default function ReferenceEditor(props) { + const { + sequenceIds, + isSaving, + isLoading, + htmlPerf, + sectionable, + blockable, + editable, + preview, + verbose, + bookName, + bookChange, + setBookChange, + addSequenceId, + saveHtmlPerf, + setGraftSequenceId, + bookAvailable, + } = props; + + const { + state: { chapter }, + } = useContext(ReferenceContext); + + const { t } = useTranslation(); + + const { + states: { openSideBar }, + actions: { setOpenSideBar, setSideBarTab }, + } = useContext(ProjectContext); + const sequenceId = sequenceIds.at(-1); + const style = isSaving ? { cursor: 'progress' } : {}; + // const bookChanged = sequenceId === htmlPerf?.mainSequenceId; + const handlers = { + onBlockClick: ({ content: _content, element }) => { + const _sequenceId = element.dataset.target; + const { tagName } = element; + const isInline = tagName === 'SPAN'; + if (_sequenceId) { + setGraftSequenceId(_sequenceId); + setOpenSideBar(!openSideBar); + setSideBarTab('footnotes'); + } else { + setSideBarTab(''); + setGraftSequenceId(null); + } + }, + }; + useEffect(() => { + setBookChange(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [htmlPerf]); + + const { + actions: { setEditorSave }, + } = useContext(ProjectContext); + + const autoSaveIndication = () => { + setEditorSave(); + setTimeout(() => { + setEditorSave(t('label-saved')); + }, 1000); + }; + useEffect(() => { + if (isSaving) { + autoSaveIndication(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSaving]); + + const _props = { + htmlPerf, + onHtmlPerf: saveHtmlPerf, + chapterIndex: chapter, + sequenceIds, + addSequenceId, + options: { + sectionable, + blockable, + editable, + preview, + }, + components: { + block: (__props) => ReferenceRecursiveBlock({ + htmlPerf, + sequenceIds, + ...__props, + }), + }, + decorators: {}, + verbose, + handlers, + autoSaveIndication, + }; + return ( +
+ {!bookAvailable && } + {bookAvailable && (!sequenceId || bookChange) && } + {bookAvailable && sequenceId && !bookChange && ( + + )} +
+ ); +} diff --git a/renderer/src/components/EditorPage/TextEditor/ReferenceRecursiveBlock.jsx b/renderer/src/components/EditorPage/TextEditor/ReferenceRecursiveBlock.jsx new file mode 100644 index 000000000..20225b684 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/ReferenceRecursiveBlock.jsx @@ -0,0 +1,74 @@ +/* eslint-disable react/prop-types */ +/* eslint-disable no-unused-vars */ +import React, { useEffect, useState } from 'react'; +import { HtmlPerfEditor } from '@xelah/type-perf-html'; + +const getTarget = ({ content }) => { + const div = document.createElement('div'); + div.innerHTML = content; + + const { target } = div.firstChild?.dataset || {}; + + return target; +}; + +export default function ReferenceRecursiveBlock({ + htmlPerf, + onHtmlPerf, + sequenceIds, + addSequenceId, + options, + content, + style, + contentEditable, + index, + verbose, + setFootNote, + ...props +}) { + const checkReturnKeyPress = (event) => { + if (event.key === 'Enter') { + const activeTextArea = document.activeElement; + if (activeTextArea.children.length > 1) { + const lineBreak = activeTextArea.children[1]?.outerHTML; + const newLine = lineBreak.replace(//gi, ' '); + activeTextArea.children[1].outerHTML = newLine; + } + } + }; + + let component; + + const editable = !!content.match(/data-type="paragraph"/); + + if (editable) { + component = ( +
+ ); + } + + if (!editable) { + const sequenceId = getTarget({ content }); + + if (sequenceId && !options.preview) { + const _props = { + sequenceIds: [...sequenceIds, sequenceId], + addSequenceId, + htmlPerf, + onHtmlPerf, + onInput: props?.onInput, + options, + }; + component = ; + } + component ||=
; + } + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{component}; +} diff --git a/renderer/src/components/EditorPage/TextEditor/ReferenceScribex.jsx b/renderer/src/components/EditorPage/TextEditor/ReferenceScribex.jsx new file mode 100644 index 000000000..e667abf02 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/ReferenceScribex.jsx @@ -0,0 +1,94 @@ +import { useEffect, useState, useContext } from 'react'; +import { useProskomma, useImport, useCatalog } from 'proskomma-react-hooks'; +import { useDeepCompareEffect } from 'use-deep-compare'; +import usePerf from '@/components/hooks/scribex/usePerf'; +import htmlMap from '@/components/hooks/scribex/htmlmap'; +import { ReferenceContext } from '@/components/context/ReferenceContext'; +import { ScribexContext } from '@/components/context/ScribexContext'; +import ReferenceEditor from './ReferenceEditor'; + +export default function ReferenceScribex(props) { + const { state, actions } = useContext(ScribexContext); + const { verbose } = state; + const { + usfmData, bookAvailable, refName, bookId, scrollLock, font, + } = props; + const [selectedBook, setSelectedBook] = useState(); + const [bookChange, setBookChange] = useState(false); + let selectedDocument; + + const { proskomma, stateId, newStateId } = useProskomma({ verbose }); + const { done } = useImport({ + proskomma, + stateId, + newStateId, + documents: usfmData, + }); + const { + state: { + fontSize, + projectScriptureDir, + }, + } = useContext(ReferenceContext); + + useEffect(() => { + setSelectedBook(bookId.toUpperCase()); + setBookChange(true); + }, [bookId]); + + const { catalog } = useCatalog({ proskomma, stateId, verbose }); + const { id: docSetId, documents } = (done && catalog.docSets[0]) || {}; + if (done) { + selectedDocument = documents?.find( + (doc) => doc.bookCode === selectedBook, + ); + } + + const { bookCode, h: bookName } = selectedDocument || {}; + const ready = (docSetId && bookCode) || false; + const isLoading = !done || !ready; + const { state: perfState, actions: perfActions } = usePerf({ + proskomma, + ready, + docSetId, + bookCode, + verbose, + htmlMap, + refName, + }); + + useEffect(() => { + actions.setSequenceIds([]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [refName, bookId, scrollLock]); + + const { htmlPerf } = perfState; + useDeepCompareEffect(() => { + if (htmlPerf && htmlPerf.mainSequenceId !== state.sequenceIds[0]) { + actions.setSequenceIds([htmlPerf?.mainSequenceId]); + } + }, [htmlPerf, state.sequenceIds, perfState, refName]); + const _props = { + ...state, + ...perfState, + ...actions, + ...perfActions, + isLoading, + bookName, + bookChange, + bookAvailable, + setBookChange, + }; + return ( +
1.3) ? 1.5 : '', + direction: `${projectScriptureDir === 'RTL' ? 'rtl' : 'auto'}`, + }} + > + +
+ ); +} diff --git a/renderer/src/components/EditorPage/TextEditor/hooks/useIntersectionObserver.js b/renderer/src/components/EditorPage/TextEditor/hooks/useIntersectionObserver.js new file mode 100644 index 000000000..b366f5ec2 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/hooks/useIntersectionObserver.js @@ -0,0 +1,46 @@ +import { useEffect } from 'react'; + +export const useIntersectionObserver = (scrollLock, setChapterNumber) => { + useEffect(() => { + console.log('scroll '); + const options = { + root: document.querySelector('editor'), + threshold: 0, + rootMargin: '0% 0% -60% 0%', + }; + const scrollReference = (chapterNumber) => { + const refEditors = document.getElementsByClassName('ref-editor'); + refEditors.length > 0 && Array.prototype.filter.call(refEditors, (refEditor) => { + const editorInView = refEditor.querySelector(`#ch-${chapterNumber}`); + if (editorInView) { + editorInView.scrollIntoView(); + editorInView.classList.add('scroll-mt-10'); + } + }); + }; + + const observer = new IntersectionObserver((entries) => { + // eslint-disable-next-line no-restricted-syntax + for (const entry of entries) { + if (entry.isIntersecting) { + setChapterNumber(entry.target.dataset.attsNumber); + if (!scrollLock) { + scrollReference(entry.target.dataset.attsNumber); + } + } + } + }, options); + + const watchNodes = document.querySelectorAll('.editor .chapter'); + const watchArr = Array.from(watchNodes); + const reverseArray = watchArr.length > 0 ? watchArr.slice().reverse() : []; + reverseArray.forEach((chapter) => { + observer.observe(chapter); + }); + + return () => { + // Cleanup: Disconnect the observer when the component unmounts + observer.disconnect(); + }; + }, []); +}; diff --git a/renderer/src/components/EditorPage/TextEditor/index.jsx b/renderer/src/components/EditorPage/TextEditor/index.jsx new file mode 100644 index 000000000..570f2c81d --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/index.jsx @@ -0,0 +1,123 @@ +import { + useEffect, useState, useContext, Fragment, +} from 'react'; +import { useProskomma, useImport, useCatalog } from 'proskomma-react-hooks'; +import { useDeepCompareEffect } from 'use-deep-compare'; +import usePerf from '@/components/hooks/scribex/usePerf'; +import htmlMap from '@/components/hooks/scribex/htmlmap'; +import { ScribexContext } from '@/components/context/ScribexContext'; +import { ReferenceContext } from '@/components/context/ReferenceContext'; +import { ProjectContext } from '@/components/context/ProjectContext'; +import EditorSideBar from '@/modules/editorsidebar/EditorSideBar'; +import EditorMenuBar from './EditorMenuBar'; +import Editor from './Editor'; + +export default function TextEditor(props) { + const { state, actions } = useContext(ScribexContext); + const { verbose } = state; + const { usfmData, bookAvailable } = props; + const [selectedBook, setSelectedBook] = useState(); + const [bookChange, setBookChange] = useState(false); + const [chapterNumber, setChapterNumber] = useState(1); + const [verseNumber, setVerseNumber] = useState(1); + const [triggerVerseInsert, setTriggerVerseInsert] = useState(false); + const [newVerChapNumber, setInsertNumber] = useState(''); + const [insertVerseRChapter, setInsertVerseRChapter] = useState(''); + const [selectedText, setSelectedText] = useState(); + + const { + state: { + bookId, + }, + actions: { setSelectedFont }, + } = useContext(ReferenceContext); + + const { + states: { openSideBar }, + actions: { setOpenSideBar }, + } = useContext(ProjectContext); + + let selectedDocument; + + const { proskomma, stateId, newStateId } = useProskomma({ verbose }); + const { done } = useImport({ + proskomma, + stateId, + newStateId, + documents: usfmData, + }); + + function closeSideBar(status) { + setOpenSideBar(status); + } + + useEffect(() => { + setSelectedBook(bookId.toUpperCase()); + setBookChange(true); + }, [bookId]); + + const { catalog } = useCatalog({ proskomma, stateId, verbose }); + const { id: docSetId, documents } = (done && catalog.docSets[0]) || {}; + if (done) { + selectedDocument = documents?.find( + (doc) => doc.bookCode === selectedBook, + ); + } + + const { bookCode, h: bookName } = selectedDocument || {}; + const ready = (docSetId && bookCode) || false; + const isLoading = !done || !ready; + const { state: perfState, actions: perfActions } = usePerf({ + proskomma, + ready, + docSetId, + bookCode, + verbose, + htmlMap, + }); + const { htmlPerf } = perfState; + + useDeepCompareEffect(() => { + if (htmlPerf && htmlPerf.mainSequenceId !== state.sequenceIds[0]) { + actions.setSequenceIds([htmlPerf?.mainSequenceId]); + } + }, [htmlPerf, state.sequenceIds, perfState]); + + const _props = { + ...state, + ...perfState, + ...actions, + ...perfActions, + triggerVerseInsert, + chapterNumber, + verseNumber, + isLoading, + bookName, + bookChange, + bookAvailable, + setBookChange, + setChapterNumber, + setVerseNumber, + newVerChapNumber, + insertVerseRChapter, + selectedText, + setSelectedText, + setTriggerVerseInsert, + setSelectedFont, + setInsertNumber, + setInsertVerseRChapter, + }; + return ( + <> + +
+ + +
+ + ); +} diff --git a/renderer/src/components/EditorPage/TextEditor/utils/IntersectionObserver.js b/renderer/src/components/EditorPage/TextEditor/utils/IntersectionObserver.js new file mode 100644 index 000000000..caafdf193 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/utils/IntersectionObserver.js @@ -0,0 +1,20 @@ +export const scrollReference = (chapterNumber) => { + const refEditors = document.getElementsByClassName('ref-editor'); + refEditors.length > 0 && Array.prototype.filter.call(refEditors, (refEditor) => { + const editorInView = refEditor.querySelector(`#ch-${chapterNumber}`); + if (editorInView) { + editorInView.scrollIntoView(); + editorInView.classList.add('scroll-mt-10'); + } + }); +}; + +export const onIntersection = (entries, setChapterNumber, scrollLock) => { + // eslint-disable-next-line no-restricted-syntax + for (const entry of entries) { + if (entry.isIntersecting) { + setChapterNumber(entry.target.dataset.attsNumber); + scrollLock === false ? scrollReference(entry.target.dataset.attsNumber) : {}; + } + } +}; diff --git a/renderer/src/components/EditorPage/TextEditor/utils/getReferences.js b/renderer/src/components/EditorPage/TextEditor/utils/getReferences.js new file mode 100644 index 000000000..fe6cb92f2 --- /dev/null +++ b/renderer/src/components/EditorPage/TextEditor/utils/getReferences.js @@ -0,0 +1,32 @@ +export const getCurrentVerse = (currentNode) => { + let currentVerse; + let prev = currentNode.previousElementSibling; + while (prev) { + if (prev.dataset.type === 'mark' && prev.dataset.subtype === 'verses') { + currentVerse = prev.dataset.attsNumber; + break; + } + // Get the previous sibling + prev = prev.previousElementSibling; + } + return currentVerse; +}; + +export const getCurrentChapter = (currentNode) => { + let currentChapter; + const closestParaDiv = currentNode.parentNode.parentNode; + if (closestParaDiv.firstElementChild?.firstElementChild?.classList.contains('chapter')) { + currentChapter = closestParaDiv.firstElementChild.firstElementChild.dataset.attsNumber; + return currentChapter; + } + + let prevParaDiv = closestParaDiv.previousElementSibling; + while (prevParaDiv) { + if (prevParaDiv.firstElementChild?.firstElementChild?.classList.contains('chapter')) { + currentChapter = prevParaDiv.firstElementChild.firstElementChild.dataset.attsNumber; + break; + } + prevParaDiv = prevParaDiv.previousElementSibling; + } + return currentChapter; +}; From 201ac740b5a8e82442f645d62ff3b576f88d7c53 Mon Sep 17 00:00:00 2001 From: samueljd Date: Mon, 23 Oct 2023 18:02:22 +0530 Subject: [PATCH 2/7] editor-refactor --- .eslintrc.json | 3 +- .vscode/settings.json | 10 +- jsconfig.json | 6 +- renderer/jsconfig.json | 11 +- .../Navigation/reference/SelectVerse.js | 2 - .../ReferenceBible/ReferenceBibleX.js | 2 +- .../components/EditorPage/Scribex/Buttons.jsx | 130 ----------- .../components/EditorPage/Scribex/Editor.jsx | 206 ------------------ .../EditorPage/Scribex/GraftEditor.jsx | 65 ------ .../EditorPage/Scribex/InsertMenu.jsx | 57 ----- .../components/EditorPage/Scribex/Popup.jsx | 119 ---------- .../EditorPage/Scribex/PopupButton.jsx | 46 ---- .../EditorPage/Scribex/RecursiveBlock.jsx | 126 ----------- .../EditorPage/Scribex/ReferenceEditor.jsx | 115 ---------- .../Scribex/ReferenceRecursiveBlock.jsx | 74 ------- .../EditorPage/Scribex/ReferenceScribex.jsx | 92 -------- .../components/EditorPage/Scribex/Scribex.jsx | 201 ----------------- .../EditorPage/Scribex/getReferences.js | 32 --- .../EditorPage/TextEditor/Editor.jsx | 22 +- .../EditorPage/TextEditor/InsertMenu.jsx | 4 +- .../TextEditor/ReferenceScribex.jsx | 16 +- .../EditorPage/TextEditor/hooks/htmlmap.js | 55 +++++ .../EditorPage/TextEditor/hooks/saveToFile.js | 36 +++ .../hooks/useIntersectionObserver.js | 46 ---- .../EditorPage/TextEditor/hooks/usePerf.js | 123 +++++++++++ .../TextEditor/hooks/useReadUsfmFile.js | 57 +++++ .../EditorPage/TextEditor/index.jsx | 11 +- .../TextEditor/utils/IntersectionObserver.js | 2 +- renderer/src/hooks/useAutoSaveIndication.js | 24 ++ .../src/layouts/editor/SectionContainer.js | 16 +- .../src/layouts/editor/WebSectionContainer.js | 19 +- renderer/src/util/cursorUtils.js | 8 + supabase.js | 5 +- yarn.lock | 82 +++++++ 34 files changed, 451 insertions(+), 1372 deletions(-) delete mode 100644 renderer/src/components/EditorPage/Scribex/Buttons.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/Editor.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/GraftEditor.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/InsertMenu.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/Popup.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/PopupButton.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/RecursiveBlock.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/ReferenceEditor.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/ReferenceRecursiveBlock.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/ReferenceScribex.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/Scribex.jsx delete mode 100644 renderer/src/components/EditorPage/Scribex/getReferences.js create mode 100644 renderer/src/components/EditorPage/TextEditor/hooks/htmlmap.js create mode 100644 renderer/src/components/EditorPage/TextEditor/hooks/saveToFile.js delete mode 100644 renderer/src/components/EditorPage/TextEditor/hooks/useIntersectionObserver.js create mode 100644 renderer/src/components/EditorPage/TextEditor/hooks/usePerf.js create mode 100644 renderer/src/components/EditorPage/TextEditor/hooks/useReadUsfmFile.js create mode 100644 renderer/src/hooks/useAutoSaveIndication.js diff --git a/.eslintrc.json b/.eslintrc.json index ce263e66f..673289bed 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -96,7 +96,8 @@ ["@/layouts", "./renderer/src/layouts/"], ["@/modules", "./renderer/src/modules/"], ["@/util", "./renderer/src/util/"], - ["@/core", "./renderer/src/core/"] + ["@/core", "./renderer/src/core/"], + ["@/hooks", "./renderer/src/hooks/"] ], "node": { "extensions": [".js", ".jsx", ".ts", ".tsx"] diff --git a/.vscode/settings.json b/.vscode/settings.json index e59d78323..0ecec27ec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,11 +2,15 @@ "[javascript]": { "editor.formatOnSave": false }, - "editor.rulers": [100], + "editor.rulers": [ + 100 + ], "editor.fontLigatures": true, "prettier.tabWidth": 4, "eslint.alwaysShowStatus": true, - "prettier.disableLanguages": ["js"], + "prettier.disableLanguages": [ + "js" + ], "prettier.useTabs": true, "editor.formatOnSave": true, "editor.multiCursorModifier": "alt", @@ -27,4 +31,4 @@ }, "eslint.workingDirectories": [], "editor.tabSize": 2 -} +} \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json index 4138fa416..cf2e97bcf 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -28,5 +28,9 @@ "app/*" ], } - } + }, + "exclude": [ + "node_modules", + "dist" + ] } \ No newline at end of file diff --git a/renderer/jsconfig.json b/renderer/jsconfig.json index 4f315116b..fb5f8086f 100644 --- a/renderer/jsconfig.json +++ b/renderer/jsconfig.json @@ -23,9 +23,16 @@ "@/illustrations/*": [ "public/illustrations/*" ], - "@/hooks/*":[ + "@/hooks/*": [ "src/components/hooks/*" + ], + "@/hooks2/*": [ + "src/hooks/*" ] } - } + }, + "exclude": [ + "node_modules", + "dist" + ] } \ No newline at end of file diff --git a/renderer/src/components/EditorPage/Navigation/reference/SelectVerse.js b/renderer/src/components/EditorPage/Navigation/reference/SelectVerse.js index 09646b4e0..40fa0165a 100644 --- a/renderer/src/components/EditorPage/Navigation/reference/SelectVerse.js +++ b/renderer/src/components/EditorPage/Navigation/reference/SelectVerse.js @@ -50,8 +50,6 @@ export default function SelectVerse({ document.getElementById('editor').querySelector(`#ch${chapter}v${verseNum}`)?.scrollIntoView(); setVerseNumber(verseNum); } - // window.location.href = `#ch${chapter}v${verseNum}`; - // document.getElementById(`ch${chapter}v${verseNum}`).scrollIntoView(); }; const onMultiSelectVerse = async (e, verses) => { diff --git a/renderer/src/components/EditorPage/Reference/ReferenceBible/ReferenceBibleX.js b/renderer/src/components/EditorPage/Reference/ReferenceBible/ReferenceBibleX.js index afc4519f3..472e791c1 100644 --- a/renderer/src/components/EditorPage/Reference/ReferenceBible/ReferenceBibleX.js +++ b/renderer/src/components/EditorPage/Reference/ReferenceBible/ReferenceBibleX.js @@ -6,7 +6,7 @@ import LoadingScreen from '@/components/Loading/LoadingScreen'; import { SnackBar } from '@/components/SnackBar'; import EmptyScreen from '@/components/Loading/EmptySrceen'; import { useReadReferenceUsfmFile } from '@/components/EditorPage/Reference/ReferenceBible/useReadReferenceUsfmFile'; -import ReferenceScribex from '../../Scribex/ReferenceScribex'; +import ReferenceScribex from '../../TextEditor/ReferenceScribex'; const ReferenceBibleX = ({ languageId, diff --git a/renderer/src/components/EditorPage/Scribex/Buttons.jsx b/renderer/src/components/EditorPage/Scribex/Buttons.jsx deleted file mode 100644 index 057559e40..000000000 --- a/renderer/src/components/EditorPage/Scribex/Buttons.jsx +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-disable no-unused-vars */ -import React, { useState } from 'react'; - -import RectangleStackIcon from '@/icons/Xelah/RectangleStack.svg'; -import ArrowDownOnSquareIcon from '@/icons/Xelah/ArrowDownOnSquare.svg'; -import Bars2Icon from '@/icons/Xelah/Bars2.svg'; -import Bars4Icon from '@/icons/Xelah/Bars4.svg'; -import ArrowUturnLeftIcon from '@/icons/Xelah/ArrowUturnLeft.svg'; -import ArrowUturnRightIcon from '@/icons/Xelah/ArrowUturnRight.svg'; -import PencilIcon from '@/icons/Common/Pencil.svg'; -import Copy from '@/icons/Xelah/Copy.svg'; -import Paste from '@/icons/Xelah/Paste.svg'; - -export const classNames = (...classes) => classes.filter(Boolean).join(' '); - -export default function Buttons(props) { - const [sectionable, setSectionableState] = useState(false); - const [blockable, setBlockableState] = useState(true); - const [editable, setEditableState] = useState(true); - const [preview, setPreviewState] = useState(false); - const { - bookCode, - undo, - redo, - setSectionable, - setBlockable, - setEditable, - setPreview, - exportUsfm, - setTriggerVerseInsert, - } = props; - - const onSectionable = () => { - setSectionableState(!sectionable); - setSectionable(!sectionable); - }; - const onBlockable = () => { - setBlockableState(!blockable); - setBlockable(!blockable); - }; - const onEditable = () => { - setEditableState(!editable); - setEditable(!editable); - }; - const onPreview = () => { - setPreviewState(!preview); - setPreview(!preview); - }; - - return ( - <> -