From 9f914a790fd411d609cf811c407a6305f3357049 Mon Sep 17 00:00:00 2001
From: Samuel John <40059405+samueljd@users.noreply.github.com>
Date: Thu, 16 Nov 2023 08:45:10 +0200
Subject: [PATCH] Texteditor refactor (#190)
* directory change for editor
* editor-refactor
* popup-rework
* fixed insert issues
* custom paste function
* fixed-ui-bugs
* fixed undo redo and insert popup ui
---
.eslintrc.json | 3 +-
.vscode/settings.json | 10 +-
jsconfig.json | 6 +-
package.json | 2 +-
public/icons/Xelah/XMark.svg | 3 +
renderer/jsconfig.json | 11 +-
renderer/public/icons/Xelah/XMark.svg | 3 +
.../Navigation/reference/SelectVerse.js | 2 -
.../ReferenceBible/ReferenceBibleX.js | 2 +-
.../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 ----
.../components/EditorPage/Scribex/Scribex.jsx | 201 --------------
.../TextEditor/BibleNavigationX.jsx | 251 ++++++++++++++++++
.../{Scribex => TextEditor}/Buttons.jsx | 54 ++--
.../EditorPage/TextEditor/Editor.jsx | 163 ++++++++++++
.../EditorPage/TextEditor/EditorMenuBar.jsx | 87 ++++++
.../EditorPage/TextEditor/InsertMenu.jsx | 35 +++
.../EditorPage/TextEditor/Popup.jsx | 116 ++++++++
.../RecursiveBlock.jsx | 22 +-
.../ReferenceEditor.jsx | 10 +-
.../ReferenceRecursiveBlock.jsx | 0
.../ReferenceScribex.jsx | 16 +-
.../EditorPage/TextEditor/hooks/htmlmap.js | 55 ++++
.../EditorPage/TextEditor/hooks/saveToFile.js | 36 +++
.../EditorPage/TextEditor/hooks/usePerf.js | 123 +++++++++
.../TextEditor/hooks/useReadUsfmFile.js | 57 ++++
.../EditorPage/TextEditor/index.jsx | 117 ++++++++
.../TextEditor/utils/IntersectionObserver.js | 20 ++
.../utils}/getReferences.js | 0
.../TextEditor/utils/insertFunctionMap.js | 18 ++
renderer/src/components/Popup/Dialog.jsx | 132 +++++++++
.../src/components/Popup/PopupContext.jsx | 28 ++
renderer/src/components/Popup/index.jsx | 43 +++
.../src/components/Projects/CustomList.js | 4 +-
.../src/components/Projects/NewProject.js | 38 +--
.../CreateProject/AdvancedSettingsDropdown.js | 2 +-
.../src/components/context/ScribexContext.js | 30 +++
renderer/src/hooks/useAutoSaveIndication.js | 24 ++
.../src/layouts/editor/SectionContainer.js | 16 +-
.../src/layouts/editor/WebSectionContainer.js | 19 +-
renderer/src/modules/projects/CustomList.js | 4 +-
renderer/src/util/cursorUtils.js | 117 +++-----
styles/globals.css | 3 +
supabase.js | 5 +-
yarn.lock | 23 +-
48 files changed, 1498 insertions(+), 906 deletions(-)
create mode 100644 public/icons/Xelah/XMark.svg
create mode 100644 renderer/public/icons/Xelah/XMark.svg
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/Scribex.jsx
create mode 100644 renderer/src/components/EditorPage/TextEditor/BibleNavigationX.jsx
rename renderer/src/components/EditorPage/{Scribex => TextEditor}/Buttons.jsx (75%)
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
rename renderer/src/components/EditorPage/{Scribex => TextEditor}/RecursiveBlock.jsx (80%)
rename renderer/src/components/EditorPage/{Scribex => TextEditor}/ReferenceEditor.jsx (95%)
rename renderer/src/components/EditorPage/{Scribex => TextEditor}/ReferenceRecursiveBlock.jsx (100%)
rename renderer/src/components/EditorPage/{Scribex => TextEditor}/ReferenceScribex.jsx (85%)
create mode 100644 renderer/src/components/EditorPage/TextEditor/hooks/htmlmap.js
create mode 100644 renderer/src/components/EditorPage/TextEditor/hooks/saveToFile.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/components/EditorPage/TextEditor/index.jsx
create mode 100644 renderer/src/components/EditorPage/TextEditor/utils/IntersectionObserver.js
rename renderer/src/components/EditorPage/{Scribex => TextEditor/utils}/getReferences.js (100%)
create mode 100644 renderer/src/components/EditorPage/TextEditor/utils/insertFunctionMap.js
create mode 100644 renderer/src/components/Popup/Dialog.jsx
create mode 100644 renderer/src/components/Popup/PopupContext.jsx
create mode 100644 renderer/src/components/Popup/index.jsx
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/package.json b/package.json
index c56c1a7a4..be4f08ee8 100644
--- a/package.json
+++ b/package.json
@@ -143,7 +143,7 @@
"electron-log": "4.4.7",
"electron-next": "^3.1.5",
"electron-updater": "^5.0.1",
- "epitelete": "0.2.20-beta.1",
+ "epitelete": "^0.2.20",
"epitelete-html": "0.2.20-beta.2",
"eslint-import-resolver-alias": "^1.1.2",
"font-detect-rhl": "1.0.5-1",
diff --git a/public/icons/Xelah/XMark.svg b/public/icons/Xelah/XMark.svg
new file mode 100644
index 000000000..457d8e626
--- /dev/null
+++ b/public/icons/Xelah/XMark.svg
@@ -0,0 +1,3 @@
+
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/public/icons/Xelah/XMark.svg b/renderer/public/icons/Xelah/XMark.svg
new file mode 100644
index 000000000..457d8e626
--- /dev/null
+++ b/renderer/public/icons/Xelah/XMark.svg
@@ -0,0 +1,3 @@
+
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/Editor.jsx b/renderer/src/components/EditorPage/Scribex/Editor.jsx
deleted file mode 100644
index 5686132b6..000000000
--- a/renderer/src/components/EditorPage/Scribex/Editor.jsx
+++ /dev/null
@@ -1,206 +0,0 @@
-/* eslint-disable no-unused-vars */
-import { 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 {
- insertVerseNumber, insertChapterNumber, insertFootnote, insertXRef,
-} from '@/util/cursorUtils';
-import RecursiveBlock from './RecursiveBlock';
-
-export default function Editor(props) {
- const {
- chapterNumber,
- sequenceIds,
- isSaving,
- isLoading,
- htmlPerf,
- sectionable,
- blockable,
- editable,
- preview,
- verbose,
- bookName,
- bookChange,
- setBookChange,
- addSequenceId,
- saveHtmlPerf,
- setGraftSequenceId,
- bookAvailable,
- setChapterNumber,
- setVerseNumber,
- triggerVerseInsert,
- newVerChapNumber,
- insertVerseRChapter,
- reference,
- insertNewGraft,
- selectedText,
- setSelectedText,
- } = props;
-
- const [caretPosition, setCaretPosition] = useState();
- const [graftInsert, setGraftInsert] = useState(false);
- const {
- state: { chapter },
- } = useContext(ReferenceContext);
- const { t } = useTranslation();
-
- const {
- states: { openSideBar, scrollLock },
- actions: { setOpenSideBar, setSideBarTab },
- } = useContext(ProjectContext);
-
- const [chapters, setChapters] = useState();
- // const [selectedText, setSelectedText] = useState();
- const sequenceId = sequenceIds.at(-1);
- const style = isSaving ? { cursor: 'progress' } : {};
- const handlers = {
- onBlockClick: ({ content: _content, 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]);
-
- 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]);
-
- function onReferenceSelected({ bookId, chapter, verse }) {
- chapter && setChapterNumber(chapter);
- verse && setVerseNumber(verse);
- }
-
- function getSelectedText() {
- let selectedText = '';
- if (window.getSelection) {
- const selection = window.getSelection();
-
- selectedText = selection.toString();
- setSelectedText(selectedText);
- } else if (document.selection && document.selection.type !== 'Control') {
- selectedText = document.selection.createRange().text;
- setSelectedText(selectedText);
- }
- }
-
- useEffect(() => {
- if (insertVerseRChapter === 'Verse') {
- insertVerseNumber(caretPosition, newVerChapNumber);
- }
- if (insertVerseRChapter === 'Chapter') {
- insertChapterNumber(caretPosition, newVerChapNumber);
- }
- if (insertVerseRChapter === 'Footnote') {
- // setGraftInsert(true);
- insertFootnote(caretPosition, newVerChapNumber, selectedText);
- }
- if (insertVerseRChapter === 'Cross Reference') {
- insertXRef(caretPosition, newVerChapNumber, selectedText);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [triggerVerseInsert]);
-
- 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 onIntersection = (entries) => {
- // 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) : {};
- }
- }
- };
-
- const options = {
- root: document.querySelector('editor'),
- threshold: 0,
- rootMargin: '0% 0% -60% 0%',
- };
- const observer = new IntersectionObserver(onIntersection, 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); });
-
- const _props = {
- htmlPerf,
- // onHtmlPerf: graftInsert ? insertNewGraft : saveHtmlPerf,
- 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,
- autoSaveIndication,
- };
-
- return (
-
- {!bookAvailable && }
- {bookAvailable && (!sequenceId || bookChange) && }
- {bookAvailable && sequenceId && !bookChange && (
-
- )}
-
- );
-}
diff --git a/renderer/src/components/EditorPage/Scribex/GraftEditor.jsx b/renderer/src/components/EditorPage/Scribex/GraftEditor.jsx
deleted file mode 100644
index 385f5a58d..000000000
--- a/renderer/src/components/EditorPage/Scribex/GraftEditor.jsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/* eslint-disable no-unused-vars */
-import { HtmlPerfEditor } from '@xelah/type-perf-html';
-
-export default function GraftEditor(props) {
- const {
- sequenceIds,
- isLoading,
- htmlPerf,
- sectionable,
- blockable,
- editable,
- preview,
- verbose,
- graftSequenceId,
- addSequenceId,
- saveHtmlPerf,
- setGraftSequenceId,
- } = props;
-
- const sequenceId = sequenceIds.at(-1);
-
- const style = isLoading || !sequenceId ? { cursor: 'progress' } : {};
-
- const handlers = {
- onBlockClick: ({ content: _content, element }) => {
- const _sequenceId = element.dataset.target;
- const { tagName } = element;
- const isInline = tagName === 'SPAN';
- // if (_sequenceId && !isInline) addSequenceId(_sequenceId);
- if (_sequenceId) { setGraftSequenceId(_sequenceId); }
- },
- };
-
- const _props = {
- htmlPerf,
- onHtmlPerf: saveHtmlPerf,
- sequenceIds,
- sequenceId,
- addSequenceId,
- options: {
- sectionable,
- blockable,
- editable,
- preview,
- },
- decorators: {},
- verbose,
- handlers,
- };
-
- const graftProps = {
- ..._props,
- sequenceIds: [graftSequenceId],
- };
-
- const graftSequenceEditor = htmlPerf && (
-
- );
-
- return (
-
- {graftSequenceId ? graftSequenceEditor : ''}
-
- );
-}
diff --git a/renderer/src/components/EditorPage/Scribex/InsertMenu.jsx b/renderer/src/components/EditorPage/Scribex/InsertMenu.jsx
deleted file mode 100644
index aa7f518cd..000000000
--- a/renderer/src/components/EditorPage/Scribex/InsertMenu.jsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Fragment, useState } from 'react';
-import { Menu, Transition } from '@headlessui/react';
-import PopupButton from './PopupButton';
-import PlusIcon from '@/icons/Xelah/Plus.svg';
-
-export default function InsertMenu({ handleClick: handleButtonClick, selectedText }) {
- const [isOpen, setIsOpen] = useState(false);
- const handleClick = (number, title) => {
- handleButtonClick(number, title);
- setIsOpen(false);
- };
- return (
-
-
-
- );
-}
diff --git a/renderer/src/components/EditorPage/Scribex/Popup.jsx b/renderer/src/components/EditorPage/Scribex/Popup.jsx
deleted file mode 100644
index 7533e8466..000000000
--- a/renderer/src/components/EditorPage/Scribex/Popup.jsx
+++ /dev/null
@@ -1,119 +0,0 @@
-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('');
- 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/Scribex/PopupButton.jsx b/renderer/src/components/EditorPage/Scribex/PopupButton.jsx
deleted file mode 100644
index c0a6e24e5..000000000
--- a/renderer/src/components/EditorPage/Scribex/PopupButton.jsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, { useState } from 'react';
-import Popup from './Popup';
-
-const PopupButton = ({
- handleClick, title, roundedHover, selectedText,
-}) => {
- 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/Scribex/Scribex.jsx b/renderer/src/components/EditorPage/Scribex/Scribex.jsx
deleted file mode 100644
index e50fd5ee5..000000000
--- a/renderer/src/components/EditorPage/Scribex/Scribex.jsx
+++ /dev/null
@@ -1,201 +0,0 @@
-import {
- useEffect, useState, useContext, Fragment,
-} from 'react';
-import { useProskomma, useImport, useCatalog } from 'proskomma-react-hooks';
-import { useDeepCompareEffect } from 'use-deep-compare';
-import { LockClosedIcon, BookmarkIcon, LockOpenIcon } from '@heroicons/react/24/outline';
-import BibleNavigationX from '@/modules/biblenavigation/BibleNavigationX';
-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 MenuDropdown from '@/components/MenuDropdown/MenuDropdown';
-import Buttons from './Buttons';
-import Editor from './Editor';
-import InsertMenu from './InsertMenu';
-
-export default function Scribex(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 handleClick = (number, title) => {
- setInsertNumber(number);
- setInsertVerseRChapter(title);
- setTriggerVerseInsert(!triggerVerseInsert);
- };
-
- let selectedDocument;
-
- const { proskomma, stateId, newStateId } = useProskomma({ verbose });
- const { done } = useImport({
- proskomma,
- stateId,
- newStateId,
- documents: usfmData,
- });
-
- const {
- state: {
- bookId, selectedFont, fontSize, projectScriptureDir,
- },
- actions: { setSelectedFont },
- } = useContext(ReferenceContext);
-
- const {
- states: { scrollLock },
- actions: { setScrollLock },
- } = useContext(ProjectContext);
-
- const {
- states: { openSideBar },
- actions: { setOpenSideBar },
- } = useContext(ProjectContext);
-
- function closeSideBar(open) {
- setOpenSideBar(open);
- }
-
- 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,
- };
- return (
- <>
-
-
-
-
-
-
- Editor
-
-
-
- {scrollLock === true ? (
- setScrollLock(!scrollLock)}
- />
- ) : (
- setScrollLock(!scrollLock)}
- />
- )}
-
-
-
-
-
-
-
-
-
-
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"
- >
-
-
-
- >
- );
-}
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/Scribex/Buttons.jsx b/renderer/src/components/EditorPage/TextEditor/Buttons.jsx
similarity index 75%
rename from renderer/src/components/EditorPage/Scribex/Buttons.jsx
rename to renderer/src/components/EditorPage/TextEditor/Buttons.jsx
index 057559e40..e2fe18eb0 100644
--- a/renderer/src/components/EditorPage/Scribex/Buttons.jsx
+++ b/renderer/src/components/EditorPage/TextEditor/Buttons.jsx
@@ -20,6 +20,8 @@ export default function Buttons(props) {
const [preview, setPreviewState] = useState(false);
const {
bookCode,
+ canUndo,
+ canRedo,
undo,
redo,
setSectionable,
@@ -27,7 +29,6 @@ export default function Buttons(props) {
setEditable,
setPreview,
exportUsfm,
- setTriggerVerseInsert,
} = props;
const onSectionable = () => {
@@ -55,7 +56,6 @@ export default function Buttons(props) {
sectionable ? 'fill-current' : '',
'h-5 mr-2 w-5 text-white cursor-pointer',
)}
- // aria-hidden="true"
onClick={onSectionable}
title={sectionable ? 'Expand all Chapters' : 'Collapse Chapters'}
/>
@@ -66,7 +66,6 @@ export default function Buttons(props) {
editable ? 'fill-current' : '',
'h-5 mr-2 w-5 text-white cursor-pointer',
)}
- // aria-hidden="true"
onClick={onEditable}
title={editable ? 'Disable Edit' : 'Enable Edit'}
/>
@@ -74,7 +73,6 @@ export default function Buttons(props) {
@@ -83,48 +81,40 @@ export default function Buttons(props) {
)}
- undo()}
- title="Undo"
- />
-
+
+
+
+
exportUsfm(bookCode)}
title="Save"
/>
- {/* |
- copyText()}
- title="Save"
- />
- pasteText(true)}
- 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..61180ef17
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/Editor.jsx
@@ -0,0 +1,163 @@
+import React, { useContext, useEffect } 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 { ScribexContext } from '@/components/context/ScribexContext';
+import EmptyScreen from '@/components/Loading/EmptySrceen';
+// eslint-disable-next-line import/no-unresolved, import/extensions
+import { functionMapping } from './utils/insertFunctionMap';
+
+import RecursiveBlock from './RecursiveBlock';
+// eslint-disable-next-line import/no-unresolved, import/extensions
+import { useAutoSaveIndication } from '@/hooks2/useAutoSaveIndication';
+import { onIntersection } from './utils/IntersectionObserver';
+
+export default function Editor(props) {
+ const {
+ sequenceIds,
+ isSaving,
+ htmlPerf,
+ sectionable,
+ blockable,
+ editable,
+ preview,
+ verbose,
+ bookChange,
+ setBookChange,
+ addSequenceId,
+ saveHtmlPerf,
+ setGraftSequenceId,
+ bookAvailable,
+ setChapterNumber,
+ setVerseNumber,
+ triggerVerseInsert,
+ } = props;
+
+ const {
+ state: {
+ chapter, selectedFont, fontSize, projectScriptureDir,
+ },
+ } = useContext(ReferenceContext);
+
+ const {
+ states: { openSideBar, scrollLock },
+ actions: { setOpenSideBar, setSideBarTab },
+ } = useContext(ProjectContext);
+
+ const {
+ state: {
+ caretPosition, insertType, selectedText, numberToInsert, textToInsert,
+ },
+ actions: {
+ setCaretPosition, setSelectedText, setNumberToInsert, setTextToInsert, setInsertType,
+ },
+ } = useContext(ScribexContext);
+
+ console.log({ caretPosition });
+ 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]);
+
+ useEffect(() => { // temp fix to trigger rerender to cause onblcok trigger to save to file. Need to find a better way.
+ if (insertType !== '') {
+ console.log({
+ caretPosition, numberToInsert, textToInsert, selectedText, insertType,
+ });
+ insertType === 'insertVerseNumber' || insertType === 'insertChapterNumber'
+ ? functionMapping[insertType].function({ caretPosition, numberToInsert })
+ : functionMapping[insertType].function({ caretPosition, textToInsert, selectedText });
+ setNumberToInsert('');
+ setTextToInsert('');
+ setInsertType('');
+ setSelectedText(null);
+ setCaretPosition(0);
+ }
+ }, [triggerVerseInsert]);
+
+ useAutoSaveIndication(isSaving);
+
+ function onReferenceSelected({ chapter, verse }) {
+ chapter && setChapterNumber(chapter);
+ verse && setVerseNumber(verse);
+ }
+
+ const observer = new IntersectionObserver((entries) => onIntersection({ setChapterNumber, scrollLock, entries }), {
+ root: document.querySelector('editor'),
+ threshold: 0,
+ rootMargin: '0% 0% -60% 0%',
+ });
+
+ 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); });
+
+ 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..a6cb2478e
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx
@@ -0,0 +1,87 @@
+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,
+ chapterNumber,
+ setChapterNumber,
+ verseNumber,
+ setVerseNumber,
+ setSelectedFont,
+ setTriggerVerseInsert,
+ } = props;
+
+ const {
+ states: { scrollLock },
+ actions: { setScrollLock },
+ } = useContext(ProjectContext);
+
+ return (
+
+
+
+
+ Editor
+
+
+
+ {scrollLock === true ? (
+ setScrollLock(!scrollLock)}
+ />
+ ) : (
+ setScrollLock(!scrollLock)}
+ />
+ )}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/renderer/src/components/EditorPage/TextEditor/InsertMenu.jsx b/renderer/src/components/EditorPage/TextEditor/InsertMenu.jsx
new file mode 100644
index 000000000..d19c9ae03
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/InsertMenu.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { PopupContextProvider } from '../../Popup/PopupContext';
+import Popup from './Popup';
+
+export default function InsertMenu({ setTriggerVerseInsert }) {
+ 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..18698f9d4
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/Popup.jsx
@@ -0,0 +1,116 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { ScribexContext } from '@/components/context/ScribexContext';
+
+import XMarkIcon from '@/icons/Xelah/XMark.svg';
+import { usePopup } from '../../Popup/PopupContext';
+import PopUpTemplate from '../../Popup';
+import { functionMapping } from './utils/insertFunctionMap';
+
+const Popup = ({ action, setTriggerVerseInsert }) => {
+ const {
+ state: {
+ textToInsert, numberToInsert, selectedText,
+ },
+ actions: {
+ setTextToInsert,
+ setNumberToInsert,
+ setInsertType,
+ setSelectedText,
+ },
+ } = useContext(ScribexContext);
+ const { setIsOpen } = usePopup();
+
+ const handleInputChange = (event) => {
+ setTextToInsert(event.target.value);
+ };
+
+ const handleNumberInputChange = (e) => {
+ setNumberToInsert(e.target.value.replace(/[^0-9]/g, ''));
+ };
+ const handleSubmit = () => {
+ setInsertType(action);
+ setTriggerVerseInsert((prev) => !prev);
+
+ // calling funciton here does not trigger the perf check to safe. Hence the trigger to re-render. and the code is commented out.
+ // action === 'insertVerseNumber' || action === 'insertChapterNumber'
+ // ? functionMapping[action].function({ caretPosition, numberToInsert })
+ // : functionMapping[action].function({ caretPosition, textToInsert, selectedText });
+ setIsOpen(false);
+ setSelectedText('');
+ };
+
+ const handleClose = () => {
+ setIsOpen(false);
+ setSelectedText('');
+ };
+
+ return (
+
+
+
{functionMapping[action].title}
+
+
+
+
+ {selectedText && selectedText.length > 0
+ && (
+
+ Selected Text :
+ {' '}
+ {selectedText}
+ {' '}
+
+ )}
+
+
+
+ {action === 'insertVerseNumber' || action === 'insertChapterNumber'
+ ? (
+ e.stopPropagation()}
+ />
+ ) : (
+ e.stopPropagation()}
+ />
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+Popup.propTypes = {
+ action: PropTypes.string.isRequired,
+ // handleButtonClick: PropTypes.func.isRequired,
+};
+
+export default Popup;
diff --git a/renderer/src/components/EditorPage/Scribex/RecursiveBlock.jsx b/renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx
similarity index 80%
rename from renderer/src/components/EditorPage/Scribex/RecursiveBlock.jsx
rename to renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx
index 9a670a79b..d7eccde1e 100644
--- a/renderer/src/components/EditorPage/Scribex/RecursiveBlock.jsx
+++ b/renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx
@@ -2,8 +2,9 @@
/* 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 './getReferences';
+import { getCurrentCursorPosition, pasteTextAtCursorPosition } from '@/util/cursorUtils';
+import { getCurrentVerse, getCurrentChapter } from '@/components/EditorPage/TextEditor/utils/getReferences';
+import { on } from 'ws';
const getTarget = ({ content }) => {
const div = document.createElement('div');
@@ -35,11 +36,13 @@ export default function RecursiveBlock({
const [currentVerse, setCurrentVerse] = useState(null);
const updateCursorPosition = () => {
+ console.log('updateCursorPosition');
const cursorPosition = getCurrentCursorPosition('editor');
setCaretPosition(cursorPosition);
};
const checkReturnKeyPress = (event) => {
+ console.log('checkReturnKeyPress');
const activeTextArea = document.activeElement;
if (event.key === 'Enter') {
if (activeTextArea.children.length > 1) {
@@ -61,6 +64,7 @@ export default function RecursiveBlock({
};
function handleSelection() {
+ console.log('selectin');
let selectedText = '';
if (window.getSelection) {
selectedText = window.getSelection().toString();
@@ -68,24 +72,32 @@ export default function RecursiveBlock({
selectedText = document.selection.createRange().text;
}
if (selectedText) {
+ console.log('selectin', selectedText);
setSelectedText(selectedText);
}
}
const checkCurrentVerse = () => {
+ console.log('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);
- // if (onReferenceSelected) {
onReferenceSelected({ bookId, chapter, verse });
- // }
}
updateCursorPosition();
handleSelection();
};
+ function onPasteHandler(event) {
+ console.log('onPasteHandler');
+ const cursorPosition = getCurrentCursorPosition('editor');
+ const paste = (event.clipboardData || window.clipboardData).getData('text');
+ console.log({ paste, event });
+ pasteTextAtCursorPosition({ cursorPosition, textToInsert: paste });
+ }
+
let component;
const editable = !!content.match(/data-type="paragraph"/);
@@ -100,10 +112,10 @@ export default function RecursiveBlock({
onMouseUp={checkCurrentVerse}
onMouseDown={updateCursorPosition}
{...props}
+ onPaste={(event) => { event.preventDefault(); onPasteHandler(event); }}
/>
);
}
-
if (!editable) {
const sequenceId = getTarget({ content });
diff --git a/renderer/src/components/EditorPage/Scribex/ReferenceEditor.jsx b/renderer/src/components/EditorPage/TextEditor/ReferenceEditor.jsx
similarity index 95%
rename from renderer/src/components/EditorPage/Scribex/ReferenceEditor.jsx
rename to renderer/src/components/EditorPage/TextEditor/ReferenceEditor.jsx
index ba07cf85b..f1ef0fcc8 100644
--- a/renderer/src/components/EditorPage/Scribex/ReferenceEditor.jsx
+++ b/renderer/src/components/EditorPage/TextEditor/ReferenceEditor.jsx
@@ -1,5 +1,5 @@
/* eslint-disable no-unused-vars */
-import { useContext, useEffect, useState } from 'react';
+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';
@@ -93,10 +93,10 @@ export default function ReferenceEditor(props) {
},
components: {
block: (__props) => ReferenceRecursiveBlock({
- htmlPerf,
- sequenceIds,
- ...__props,
- }),
+ htmlPerf,
+ sequenceIds,
+ ...__props,
+ }),
},
decorators: {},
verbose,
diff --git a/renderer/src/components/EditorPage/Scribex/ReferenceRecursiveBlock.jsx b/renderer/src/components/EditorPage/TextEditor/ReferenceRecursiveBlock.jsx
similarity index 100%
rename from renderer/src/components/EditorPage/Scribex/ReferenceRecursiveBlock.jsx
rename to renderer/src/components/EditorPage/TextEditor/ReferenceRecursiveBlock.jsx
diff --git a/renderer/src/components/EditorPage/Scribex/ReferenceScribex.jsx b/renderer/src/components/EditorPage/TextEditor/ReferenceScribex.jsx
similarity index 85%
rename from renderer/src/components/EditorPage/Scribex/ReferenceScribex.jsx
rename to renderer/src/components/EditorPage/TextEditor/ReferenceScribex.jsx
index 3cdbe3656..9d128a8f9 100644
--- a/renderer/src/components/EditorPage/Scribex/ReferenceScribex.jsx
+++ b/renderer/src/components/EditorPage/TextEditor/ReferenceScribex.jsx
@@ -1,10 +1,10 @@
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 usePerf from './hooks/usePerf';
+import htmlMap from './hooks/htmlmap';
import ReferenceEditor from './ReferenceEditor';
export default function ReferenceScribex(props) {
@@ -27,6 +27,7 @@ export default function ReferenceScribex(props) {
const {
state: {
fontSize,
+ projectScriptureDir,
},
} = useContext(ReferenceContext);
@@ -58,7 +59,7 @@ export default function ReferenceScribex(props) {
useEffect(() => {
actions.setSequenceIds([]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [refName, bookId, scrollLock]);
const { htmlPerf } = perfState;
@@ -81,10 +82,11 @@ export default function ReferenceScribex(props) {
return (
1.3) ? 1.5 : '',
- }}
+ fontFamily: font || 'sans-serif',
+ fontSize: `${fontSize}rem`,
+ lineHeight: (fontSize > 1.3) ? 1.5 : '',
+ direction: `${projectScriptureDir === 'RTL' ? 'rtl' : 'auto'}`,
+ }}
>
diff --git a/renderer/src/components/EditorPage/TextEditor/hooks/htmlmap.js b/renderer/src/components/EditorPage/TextEditor/hooks/htmlmap.js
new file mode 100644
index 000000000..e9b9d9729
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/hooks/htmlmap.js
@@ -0,0 +1,55 @@
+const htmlMap = (context) => ({
+ '*': {
+ '*': {
+ tagName: 'span',
+ },
+ sequence: {
+ tagName: 'section',
+ },
+ },
+ wrapper: {
+ '*': {
+ tagName: 'wrapper',
+ },
+ sequence: {
+ tagName: 'section',
+ },
+ },
+ paragraph: {
+ '*': {
+ tagName: 'p',
+ },
+ },
+ mark: {
+ '*': {
+ tagName: 'span',
+ },
+ chapter: ({ atts }) => {
+ context.lastChapter = atts.number;
+ return ({
+ classList: ['mark', 'chapter', `chapter-${atts.number}`],
+ id: `ch-${atts.number}`,
+ tagName: 'span',
+ });
+ },
+ verses: ({ atts }) => ({
+ classList: ['mark', 'verse', `verse-${atts.number}`],
+ id: `ch${context.lastChapter}v${atts.number}`,
+ tagName: 'span',
+ attributes: { contenteditable: false },
+ }),
+ },
+ graft: {
+ heading: {
+ tagName: 'div',
+ },
+ title: {
+ tagName: 'div',
+ },
+ introduction: {
+ tagName: 'div',
+ },
+ },
+});
+
+export default htmlMap({});
diff --git a/renderer/src/components/EditorPage/TextEditor/hooks/saveToFile.js b/renderer/src/components/EditorPage/TextEditor/hooks/saveToFile.js
new file mode 100644
index 000000000..b5ef6c340
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/hooks/saveToFile.js
@@ -0,0 +1,36 @@
+import localforage from 'localforage';
+// import { readRefMeta } from '../../../core/reference/readRefMeta';
+import { isElectron } from '@/core/handleElectron';
+import writeToFile from '@/core/editor/writeToFile';
+import { readRefBurrito } from '@/core/reference/readRefBurrito';
+import packageInfo from '../../../../../../package.json';
+import { newPath } from '../../../../../../supabase';
+
+// function to save to file.
+export const saveToFile = async (usfmText, bookCode) => {
+ try {
+ const userProfile = await localforage.getItem('userProfile');
+ const userName = isElectron() ? userProfile?.username : userProfile?.user?.email;
+ const projectName = await localforage.getItem('currentProject');
+ const path = require('path');
+ const newpath = localStorage.getItem('userPath');
+ const metaPath = isElectron() ? path.join(newpath, packageInfo.name, 'users', userName, 'projects', projectName, 'metadata.json') : `${newPath}/${userName}/projects/${projectName}/metadata.json`;
+ const metaData = JSON.parse(await readRefBurrito({ metaPath }));
+ Object.entries(metaData.ingredients).forEach(async ([key, _ingredients]) => {
+ if (_ingredients.scope) {
+ const _bookID = Object.entries(_ingredients.scope)[0][0];
+ if (_bookID === bookCode) {
+ await writeToFile({
+ username: userName,
+ projectname: projectName,
+ filename: key,
+ data: usfmText,
+ });
+ }
+ }
+ });
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ return console.log(err);
+ }
+};
diff --git a/renderer/src/components/EditorPage/TextEditor/hooks/usePerf.js b/renderer/src/components/EditorPage/TextEditor/hooks/usePerf.js
new file mode 100644
index 000000000..18bdcccf8
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/hooks/usePerf.js
@@ -0,0 +1,123 @@
+/* eslint-disable no-unused-vars */
+import { useState, useTransition } from 'react';
+import {
+ useDeepCompareCallback,
+ useDeepCompareEffect,
+ useDeepCompareMemo,
+} from 'use-deep-compare';
+import isEqual from 'lodash.isequal';
+import EpiteleteHtml from 'epitelete-html';
+import { saveToFile } from './saveToFile';
+
+export default function usePerf({
+ proskomma,
+ ready,
+ docSetId,
+ bookCode,
+ verbose,
+ htmlMap,
+ refName,
+}) {
+ const [isSaving, startSaving] = useTransition();
+ const [htmlPerf, setHtmlPerf] = useState();
+ const [usfmText, setUsfmText] = useState();
+
+ const epiteleteHtml = useDeepCompareMemo(
+ () => ready
+ && new EpiteleteHtml({
+ proskomma,
+ docSetId,
+ htmlMap,
+ options: { historySize: 100 },
+ }),
+ [proskomma, ready, docSetId, refName],
+ );
+
+ useDeepCompareEffect(() => {
+ if (epiteleteHtml) {
+ epiteleteHtml.readHtml(bookCode, { cloning: false }, htmlMap).then((_htmlPerf) => {
+ // remove htmlMap for default classes
+ setHtmlPerf(_htmlPerf);
+ });
+ }
+ }, [epiteleteHtml, bookCode]);
+
+ // exports perf to usfm and writes to file.
+ const exportUsfm = async (bookCode) => {
+ const usfmString = await epiteleteHtml?.readUsfm(bookCode);
+ setUsfmText(usfmString);
+ saveToFile(usfmString, bookCode);
+ epiteleteHtml?.readHtml(bookCode, { cloning: false }, htmlMap).then((_htmlPerf) => { // remove htmlMap for default classes
+ setHtmlPerf(_htmlPerf);
+ });
+ };
+
+ const saveHtmlPerf = useDeepCompareCallback(
+ (_htmlPerf, { sequenceId }) => {
+ if (!isEqual(htmlPerf, _htmlPerf)) { setHtmlPerf(_htmlPerf); }
+
+ startSaving(async () => {
+ const newHtmlPerf = await epiteleteHtml?.writeHtml(
+ bookCode,
+ sequenceId,
+ _htmlPerf,
+ { insertSequences: true },
+ );
+ if (!isEqual(htmlPerf, newHtmlPerf)) { setHtmlPerf(newHtmlPerf); }
+ exportUsfm(bookCode);
+ });
+ },
+ [htmlPerf, bookCode],
+ );
+
+ const insertNewGraft = useDeepCompareCallback(
+ (_htmlPerf, { sequenceId }) => {
+ if (!isEqual(htmlPerf, _htmlPerf)) { setHtmlPerf(_htmlPerf); }
+
+ startSaving(async () => {
+ const newHtmlPerf = await epiteleteHtml?.writeHtml(
+ bookCode,
+ sequenceId,
+ _htmlPerf,
+ { insertSequences: true },
+ );
+
+ if (!isEqual(htmlPerf, newHtmlPerf)) { setHtmlPerf(newHtmlPerf); }
+ exportUsfm(bookCode);
+ });
+ },
+ [htmlPerf, bookCode],
+ );
+
+ const undo = async () => {
+ const newPerfHtml = await epiteleteHtml?.undoHtml(bookCode);
+ setHtmlPerf(newPerfHtml);
+ };
+
+ const redo = async () => {
+ const newPerfHtml = await epiteleteHtml?.redoHtml(bookCode);
+ setHtmlPerf(newPerfHtml);
+ };
+
+ const canUndo = (epiteleteHtml?.canUndo && epiteleteHtml?.canUndo(bookCode)) || false;
+ const canRedo = (epiteleteHtml?.canRedo && epiteleteHtml?.canRedo(bookCode)) || false;
+
+ const state = {
+ bookCode,
+ htmlPerf,
+ canUndo,
+ canRedo,
+ isSaving,
+ usfmText,
+ };
+
+ const actions = {
+ insertNewGraft,
+ saveHtmlPerf,
+ exportUsfm,
+ undo,
+ redo,
+ };
+
+ return { state, actions };
+}
diff --git a/renderer/src/components/EditorPage/TextEditor/hooks/useReadUsfmFile.js b/renderer/src/components/EditorPage/TextEditor/hooks/useReadUsfmFile.js
new file mode 100644
index 000000000..193486311
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/hooks/useReadUsfmFile.js
@@ -0,0 +1,57 @@
+import { useEffect, useState, useContext } from 'react';
+import localforage from 'localforage';
+import { ReferenceContext } from '@/components/context/ReferenceContext';
+import { readRefBurrito } from '../../../../core/reference/readRefBurrito';
+import { readFile } from '../../../../core/editor/readFile';
+import packageInfo from '../../../../../../package.json';
+// hook to fetch usfmfile from system drive
+export const useReadUsfmFile = () => {
+ const [usfmData, setUsfmData] = useState([]);
+ const [bookAvailable, setbookAvailable] = useState(false);
+ const {
+ state: {
+ bookId,
+ },
+ } = useContext(ReferenceContext);
+ useEffect(() => {
+ async function readLocalFile() {
+ try {
+ const userProfile = await localforage.getItem('userProfile');
+ const userName = userProfile?.username;
+ const projectName = await localforage.getItem('currentProject');
+ const path = require('path');
+ const newpath = localStorage.getItem('userPath');
+ const metaPath = path.join(newpath, packageInfo.name, 'users', userName, 'projects', projectName, 'metadata.json');
+ const metaData = JSON.parse(await readRefBurrito({ metaPath }));
+ const _books = [];
+ Object.entries(metaData.ingredients).forEach(async ([key, _ingredients]) => {
+ if (_ingredients.scope) {
+ const _bookID = Object.entries(_ingredients.scope)[0][0];
+ const bookObj = { bookId: _bookID, fileName: key };
+ _books.push(bookObj);
+ }
+ });
+ const [currentBook] = _books.filter((bookObj) => bookObj.bookId === bookId?.toUpperCase());
+ if (currentBook) {
+ const fileData = await readFile({ projectname: projectName, filename: currentBook.fileName, username: userName });
+ const books = [{
+ selectors: { org: 'unfoldingWord', lang: 'en', abbr: 'ult' },
+ bookCode: currentBook.bookId.toLowerCase(),
+ data: fileData,
+ }];
+ setUsfmData(books);
+ setbookAvailable(true);
+ } else {
+ setUsfmData([]);
+ setbookAvailable(false);
+ }
+ // setUsfmData(fileData);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ return console.log(err);
+ }
+ }
+ readLocalFile();
+ }, [bookId]);
+ return { usfmData, bookAvailable };
+};
diff --git a/renderer/src/components/EditorPage/TextEditor/index.jsx b/renderer/src/components/EditorPage/TextEditor/index.jsx
new file mode 100644
index 000000000..797c9d875
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/index.jsx
@@ -0,0 +1,117 @@
+import React, {
+ useEffect, useState, useContext, Fragment,
+} from 'react';
+import { useProskomma, useImport, useCatalog } from 'proskomma-react-hooks';
+import { useDeepCompareEffect } from 'use-deep-compare';
+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 { useReadUsfmFile } from './hooks/useReadUsfmFile';
+import htmlMap from './hooks/htmlmap';
+import usePerf from './hooks/usePerf';
+import EditorMenuBar from './EditorMenuBar';
+import Editor from './Editor';
+
+export default function TextEditor() {
+ 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 { usfmData, bookAvailable } = useReadUsfmFile();
+
+ 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,
+ chapterNumber,
+ verseNumber,
+ isLoading,
+ bookName,
+ bookChange,
+ bookAvailable,
+ setBookChange,
+ setChapterNumber,
+ setVerseNumber,
+ setSelectedFont,
+ triggerVerseInsert,
+ setTriggerVerseInsert,
+ };
+ 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..b6f806ad2
--- /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/Scribex/getReferences.js b/renderer/src/components/EditorPage/TextEditor/utils/getReferences.js
similarity index 100%
rename from renderer/src/components/EditorPage/Scribex/getReferences.js
rename to renderer/src/components/EditorPage/TextEditor/utils/getReferences.js
diff --git a/renderer/src/components/EditorPage/TextEditor/utils/insertFunctionMap.js b/renderer/src/components/EditorPage/TextEditor/utils/insertFunctionMap.js
new file mode 100644
index 000000000..a4fedbc64
--- /dev/null
+++ b/renderer/src/components/EditorPage/TextEditor/utils/insertFunctionMap.js
@@ -0,0 +1,18 @@
+import {
+ insertVerseNumber, insertChapterNumber, insertFootnote, insertXRef,
+} from '@/util/cursorUtils';
+
+export const functionMapping = {
+ insertVerseNumber: {
+ title: 'Insert Verse', function: insertVerseNumber, icon: 'V', pholder: 'Verse number',
+ },
+ insertChapterNumber: {
+ title: 'Insert Chapter', function: insertChapterNumber, icon: 'C', placeholder: 'Chapter number',
+ },
+ insertFootnote: {
+ title: 'Insert Footnote', function: insertFootnote, icon: 'FN', placeholder: 'Footnote',
+ },
+ insertXRef: {
+ title: 'Insert Cross Reference', function: insertXRef, icon: 'XR', placeholder: 'Cross Reference',
+ },
+};
diff --git a/renderer/src/components/Popup/Dialog.jsx b/renderer/src/components/Popup/Dialog.jsx
new file mode 100644
index 000000000..956a01557
--- /dev/null
+++ b/renderer/src/components/Popup/Dialog.jsx
@@ -0,0 +1,132 @@
+import * as React from 'react';
+
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+// import { IconX } from '@tabler/icons';
+
+import { classNames } from '@/util/classNames';
+
+const Dialog = DialogPrimitive.Root;
+
+const DialogTrigger = DialogPrimitive.Trigger;
+
+const DialogPortal = ({
+ className,
+ children,
+ ...props
+}) => (
+
+
+ {children}
+
+
+);
+DialogPortal.displayName = DialogPrimitive.Portal.displayName;
+
+const DialogOverlay = React.forwardRef(
+ ({
+ className,
+ ...props
+ }, ref) => (
+
+ ),
+);
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const DialogContent = React.forwardRef(
+ ({
+ className,
+ children,
+ ...props
+ }, ref) => (
+
+
+
+ {children}
+
+
+ ),
+);
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+const DialogHeader = ({
+ className,
+ ...props
+}) => (
+
+);
+DialogHeader.displayName = 'DialogHeader';
+
+const DialogFooter = ({
+ className,
+ ...props
+}) => (
+
+);
+DialogFooter.displayName = 'DialogFooter';
+
+const DialogTitle = React.forwardRef(
+ ({
+ className,
+ ...props
+ }, ref) => (
+
+ ),
+);
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+
+const DialogDescription = React.forwardRef(
+ ({
+ className,
+ ...props
+ }, ref) => (
+
+ ),
+);
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+ Dialog,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+};
diff --git a/renderer/src/components/Popup/PopupContext.jsx b/renderer/src/components/Popup/PopupContext.jsx
new file mode 100644
index 000000000..dfb6b4f28
--- /dev/null
+++ b/renderer/src/components/Popup/PopupContext.jsx
@@ -0,0 +1,28 @@
+/* eslint-disable react/jsx-no-constructed-context-values */
+import { createContext, useState, useContext } from 'react';
+import PropTypes from 'prop-types';
+
+export const PopupContext = createContext();
+
+export function usePopup() {
+ return useContext(PopupContext);
+}
+
+export const PopupContextProvider = ({ children }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const context = {
+ isOpen,
+ setIsOpen,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+PopupContextProvider.propTypes = {
+ children: PropTypes.node,
+};
diff --git a/renderer/src/components/Popup/index.jsx b/renderer/src/components/Popup/index.jsx
new file mode 100644
index 000000000..d6d769262
--- /dev/null
+++ b/renderer/src/components/Popup/index.jsx
@@ -0,0 +1,43 @@
+import { classNames } from '@/util/classNames';
+import { usePopup } from './PopupContext';
+import {
+ Dialog,
+ DialogContent,
+ DialogTrigger,
+} from './Dialog';
+
+export default function PopUp({
+ buttonStyle,
+ buttonText,
+ buttonIcon,
+ isLarge,
+ isSmall,
+ children,
+ maxWidth,
+}) {
+ const { isOpen, setIsOpen } = usePopup();
+ const openModal = (e) => {
+ e.stopPropagation();
+ setIsOpen(true);
+ };
+ return (
+
+
+
+ );
+}
diff --git a/renderer/src/components/Projects/CustomList.js b/renderer/src/components/Projects/CustomList.js
index 5a5ecf69e..c1b11e1c8 100644
--- a/renderer/src/components/Projects/CustomList.js
+++ b/renderer/src/components/Projects/CustomList.js
@@ -10,7 +10,7 @@ export default function CustomList({
const dropdownWidth = width ?? 40;
return (
-
+
{selected?.title}
@@ -26,7 +26,7 @@ export default function CustomList({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
-
+
{options.map((option) => (
{children}
+
{children}
);
}
@@ -317,10 +317,10 @@ export default function NewProject({ call, project, closeEdit }) {
)
: (
-
+
-
+
{t('label-project-name')}
*
@@ -334,7 +334,7 @@ export default function NewProject({ call, project, closeEdit }) {
handleProjectName(e);
}}
disabled={call !== 'new'}
- className={classNames(call !== 'new' ? 'bg-gray-200' : '', 'w-52 lg:w-80 block rounded shadow-sm sm:text-sm focus:border-primary border-gray-300')}
+ className={classNames(call !== 'new' ? 'bg-gray-200' : '', 'w-48 lg:w-full rounded shadow-sm sm:text-sm focus:border-primary border-gray-300')}
/>
{error.projectName[0]?.message || error.projectName[1]?.message}
{t('label-description')}
@@ -346,12 +346,12 @@ export default function NewProject({ call, project, closeEdit }) {
onChange={(e) => {
setNewProjectFields({ ...newProjectFields, description: e.target.value });
}}
- className="bg-white w-52 lg:w-80 h-28 block rounded shadow-sm sm:text-sm focus:border-primary border-gray-300"
+ className="bg-white w-48 lg:w-full h-28 block rounded shadow-sm sm:text-sm focus:border-primary border-gray-300"
/>
{error.description[0]?.message}
-
+
@@ -373,18 +373,20 @@ export default function NewProject({ call, project, closeEdit }) {
- {headerDropDown !== 'Audio'
- && (
-
-
- {language.ld ? language.ld : 'LTR'}
-
-
- )}
-
- {t('label-target-language')}
- *
-
+
+
+ {t('label-target-language')}
+ *
+
+ {headerDropDown !== 'Audio'
+ && (
+
+
+ {language.ld ? language.ld : 'LTR'}
+
+
+ )}
+
*/}
-
+
selectCanon(canonList[0])}
diff --git a/renderer/src/components/context/ScribexContext.js b/renderer/src/components/context/ScribexContext.js
index d03fc39cd..b608eec56 100644
--- a/renderer/src/components/context/ScribexContext.js
+++ b/renderer/src/components/context/ScribexContext.js
@@ -17,6 +17,11 @@ const ScribexContextProvider = ({
graftSequenceId: null,
reference,
font,
+ caretPosition: 0,
+ textToInsert: '',
+ numberToInsert: '',
+ selectedText: null,
+ insertType: '',
};
const [state, setState] = useState(initialState);
@@ -53,12 +58,32 @@ const ScribexContextProvider = ({
setState((prev) => ({ ...prev, graftSequenceId }));
}, []);
+ const setCaretPosition = useCallback((caretPosition) => {
+ setState((prev) => ({ ...prev, caretPosition }));
+ }, []);
+
+ const setTextToInsert = useCallback((textToInsert) => {
+ setState((prev) => ({ ...prev, textToInsert }));
+ }, []);
+
+ const setNumberToInsert = useCallback((numberToInsert) => {
+ setState((prev) => ({ ...prev, numberToInsert }));
+ }, []);
+
+ const setSelectedText = useCallback((selectedText) => {
+ setState((prev) => ({ ...prev, selectedText }));
+ }, []);
+
const addSequenceId = useCallback(
(_sequenceId) => {
setSequenceIds([...state.sequenceIds, _sequenceId]);
},
[state.sequenceIds, setSequenceIds],
);
+
+ const setInsertType = useCallback((insertType) => {
+ setState((prev) => ({ ...prev, insertType }));
+}, []);
const actions = {
setFont,
setSectionable,
@@ -69,6 +94,11 @@ const ScribexContextProvider = ({
setSequenceIds,
addSequenceId,
setGraftSequenceId,
+ setCaretPosition,
+ setTextToInsert,
+ setNumberToInsert,
+ setSelectedText,
+ setInsertType,
};
const context = {
diff --git a/renderer/src/hooks/useAutoSaveIndication.js b/renderer/src/hooks/useAutoSaveIndication.js
new file mode 100644
index 000000000..4b6530599
--- /dev/null
+++ b/renderer/src/hooks/useAutoSaveIndication.js
@@ -0,0 +1,24 @@
+import React, { useContext, useEffect } from 'react';
+import { ProjectContext } from '@/components/context/ProjectContext';
+import SaveIndicator from '@/components/Loading/SaveIndicator';
+import { useTranslation } from 'react-i18next';
+
+export const useAutoSaveIndication = (isSaving) => {
+const {
+ actions: { setEditorSave },
+} = useContext(ProjectContext);
+const { t } = useTranslation();
+
+const autoSaveIndication = () => {
+ setEditorSave(
);
+ setTimeout(() => {
+ setEditorSave(t('label-saved'));
+ }, 1000);
+};
+useEffect(() => {
+ if (isSaving) {
+ autoSaveIndication();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+}, [isSaving]);
+};
diff --git a/renderer/src/layouts/editor/SectionContainer.js b/renderer/src/layouts/editor/SectionContainer.js
index 5b2208cb6..72e5d0bb7 100644
--- a/renderer/src/layouts/editor/SectionContainer.js
+++ b/renderer/src/layouts/editor/SectionContainer.js
@@ -1,14 +1,12 @@
-import React, { useState, useEffect, useContext } from 'react';
+import React, { useState, useEffect } from 'react';
import dynamic from 'next/dynamic';
import localforage from 'localforage';
import ObsEditor from '@/components/EditorPage/ObsEditor/ObsEditor';
import AudioEditor from '@/components/EditorPage/AudioEditor/AudioEditor';
-import { ScribexContext } from '@/components/context/ScribexContext';
-import { useReadUsfmFile } from '@/components/hooks/scribex/useReadUsfmFile';
import packageInfo from '../../../../package.json';
import SectionPlaceholder1 from './SectionPlaceholder1';
import SectionPlaceholder2 from './SectionPlaceholder2';
-import Scribex from '@/components/EditorPage/Scribex/Scribex'; // eslint-disable-line
+import TextEditor from '@/components/EditorPage/TextEditor'; // eslint-disable-line
const MainPlayer = dynamic(
() => import('@/components/EditorPage/AudioEditor/MainPlayer'),
@@ -32,20 +30,12 @@ const SectionContainer = () => {
});
}, [editor]);
- const { usfmData, bookAvailable } = useReadUsfmFile();
- const { state, actions } = useContext(ScribexContext);
- const props = {
- usfmData,
- bookAvailable,
- state,
- actions,
- };
return (
<>
- {(editor === 'textTranslation' &&
)
+ {(editor === 'textTranslation' &&
)
|| (editor === 'textStories' &&
)
|| (editor === 'audioTranslation' &&
)}
diff --git a/renderer/src/layouts/editor/WebSectionContainer.js b/renderer/src/layouts/editor/WebSectionContainer.js
index 438b89738..14642663e 100644
--- a/renderer/src/layouts/editor/WebSectionContainer.js
+++ b/renderer/src/layouts/editor/WebSectionContainer.js
@@ -5,16 +5,10 @@ import dynamic from 'next/dynamic';
import localforage from 'localforage';
import ObsEditor from '@/components/EditorPage/ObsEditor/ObsEditor';
import AudioEditor from '@/components/EditorPage/AudioEditor/AudioEditor';
-import { ScribexContext } from '@/components/context/ScribexContext';
-import { useReadUsfmFile } from '@/components/hooks/scribex/useReadUsfmFile';
-import Scribex from '@/components/EditorPage/Scribex/Scribex'; // eslint-disable-line
+import TextEditor from '@/components/EditorPage/TextEditor'; // eslint-disable-line
import SectionPlaceholder1 from './WebSectionPlaceholder1';
import SectionPlaceholder2 from './WebSectionPlaceholder2';
import { newPath, sbStorageDownload } from '../../../../supabase';
-// if (!process.env.NEXT_PUBLIC_IS_ELECTRON) {
-// const supabaseStorage = require('../../../../../supabase').supabaseStorage
-// const newPath = require('../../../../supabase').newPath
-// }
const MainPlayer = dynamic(
() => import('@/components/EditorPage/AudioEditor/MainPlayer'),
@@ -36,21 +30,12 @@ const SectionContainer = () => {
setSupabaseEditor();
}, [editor]);
- const { usfmData, bookAvailable } = useReadUsfmFile();
- const { state, actions } = useContext(ScribexContext);
- const props = {
- usfmData,
- bookAvailable,
- state,
- actions,
- };
-
return (
<>
- {(editor === 'textTranslation' &&
)
+ {(editor === 'textTranslation' &&
)
|| (editor === 'textStories' &&
)
|| (editor === 'audioTranslation' &&
)}
diff --git a/renderer/src/modules/projects/CustomList.js b/renderer/src/modules/projects/CustomList.js
index a08a9c798..d51086530 100644
--- a/renderer/src/modules/projects/CustomList.js
+++ b/renderer/src/modules/projects/CustomList.js
@@ -10,7 +10,7 @@ export default function CustomList({
const dropdownWidth = width ?? 40;
return (
-
+
{selected?.title}
@@ -26,7 +26,7 @@ export default function CustomList({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
-
+
{options.map((option) => (
${verseNumber}`;
+// paste copied text from external source. This will paste the sanitized text.
+export function pasteTextAtCursorPosition({ cursorPosition, textToInsert }) {
+ setCurrentCursorPosition(cursorPosition);
+ const sel = window.getSelection();
+ let range;
+ if (sel.getRangeAt && sel.rangeCount) {
+ range = sel.getRangeAt(0);
+ range.deleteContents();
+ range.insertNode(document.createTextNode(textToInsert));
+ sel.collapseToEnd();
+ }
+}
+
+export function insertVerseNumber({ caretPosition, numberToInsert }) {
+ console.log({ caretPosition, numberToInsert });
+ if (numberToInsert && caretPosition) {
+ const verseTag = `${numberToInsert}`;
pasteHtmlAtCaret(verseTag, false, caretPosition);
}
}
-export function insertChapterNumber(caretPosition, chapterNumber) {
- if (chapterNumber && caretPosition) {
- const chapterTag = `"${chapterNumber}"`;
+export function insertChapterNumber({ caretPosition, numberToInsert }) {
+ console.log({ caretPosition, numberToInsert });
+ if (numberToInsert && caretPosition) {
+ const chapterTag = `${numberToInsert}`;
pasteHtmlAtCaret(chapterTag, false, caretPosition);
}
}
-export function insertFootnote(caretPosition, footNote, selectedText) {
- if (footNote && caretPosition) {
- const footnoteTag = ``;
- const footnoteTagWithSelection = ``;
+export function insertFootnote({ caretPosition, textToInsert, selectedText }) {
+ if (textToInsert && caretPosition) {
+ const footnoteTag = ``;
+ const footnoteTagWithSelection = ``;
selectedText && selectedText.length !== 0 ? pasteHtmlAtCaret(footnoteTagWithSelection, false, caretPosition) : pasteHtmlAtCaret(footnoteTag, false, caretPosition);
}
}
-export function insertXRef(caretPosition, references, selectedText) {
- if (insertXRef && caretPosition) {
- const xRefTag = `+${references}`;
- const xRefTagWithSelection = `+${selectedText}${references}`;
+export function insertXRef({ caretPosition, textToInsert, selectedText }) {
+ if (textToInsert && caretPosition) {
+ const xRefTag = `+${textToInsert}`;
+ const xRefTagWithSelection = `+${selectedText} ${textToInsert}`;
selectedText && selectedText.length !== 0
? pasteHtmlAtCaret(xRefTagWithSelection, false, caretPosition)
: pasteHtmlAtCaret(xRefTag, false, caretPosition);
}
}
+// export function insertHeading(caretPosition, heading) {
+// // if (heading && caretPosition) {
+// const headingTag = `${'test heading'}`;
+// // const headingTag = `${'subheading'}`;
+// console.log('inserting section heading');
+// pasteHtmlAtCaret(headingTag, false, caretPosition);
+// // }
+// }
// Helper function to get selected text within the editor
export function getSelectedText() {
@@ -165,61 +188,3 @@ export function getSelectedText() {
}
return selectedText;
}
-
-// Paste text from clipboard
-export function pasteText() {
- const editor = document.getElementById('editor');
-
- navigator.clipboard.readText()
- .then((text) => {
- editor.focus();
- document.execCommand('insertText', false, text);
- })
- .catch((error) => {
- console.error(`Unable to paste text: ${error}`);
- });
-}
-export class Clipboard {
- static copyText(text) {
- navigator.clipboard
- .writeText(text)
- .then(() => {
- // console.log('Text copied to clipboard');
- })
- .catch((error) => {
- console.error('Failed to copy text:', error);
- });
- }
-
- static pasteText(element) {
- navigator.clipboard
- .readText()
- .then((pastedText) => {
- element.focus();
- document.execCommand('insertText', false, pastedText);
- // console.log('Text pasted from clipboard');
- })
- .catch((error) => {
- console.error('Failed to paste text:', error);
- });
- }
-}
-
-// export function getWordAtCursor(parentElement) {
-// const cursorPosition = Cursor.getCurrentCursorPosition(parentElement);
-// const textContent = parentElement.textContent;
-// const words = textContent.trim().split(/\s+/);
-// let charCount = 0;
-
-// for (let i = 0; i < words.length; i++) {
-// const word = words[i];
-// const wordLength = word.length;
-// charCount += wordLength + 1; // +1 for the space after the word
-
-// if (charCount > cursorPosition) {
-// return word;
-// }
-// }
-
-// return null;
-// }
diff --git a/styles/globals.css b/styles/globals.css
index 83d9cdb90..c544797b6 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -261,3 +261,6 @@ p.paragraph:has(.chapter) {
.perf .PerfEditorWrapper {
margin: 0.1em;
} */
+.button {
+ @apply flex h-8 cursor-pointer items-center justify-center rounded-lg p-2 text-xs font-bold outline-none xl:px-4;
+}
\ No newline at end of file
diff --git a/supabase.js b/supabase.js
index 181f6b234..6a2a87768 100644
--- a/supabase.js
+++ b/supabase.js
@@ -97,7 +97,10 @@ const sbStorageDownload = async (path) => {
const sbStorageUpdate = async ({ path, payload, options = {} }) => {
await supabaseStorage().update(path, payload, options);
};
-const sbStorageRemove = async (path, payload, options) => {
+const sbStorageRemove = async (path, options) => {
+ if (options) {
+ await supabaseStorage().remove(path, options);
+ }
await supabaseStorage().remove(path);
};
diff --git a/yarn.lock b/yarn.lock
index 8a64f5a80..5547576c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8485,18 +8485,7 @@ epitelete-html@0.2.20-beta.2:
node-html-parser "^5.3.3"
pure-uuid "^1.6.3"
-epitelete@0.2.20-beta.1:
- version "0.2.20-beta.1"
- resolved "https://registry.yarnpkg.com/epitelete/-/epitelete-0.2.20-beta.1.tgz#e604ef358132dcb265ce3a967a6aec58162e1b13"
- integrity sha512-sJpjnj01UDTtO7glo6Tmyvl5WJCZSSyFzbbMWugXM7+LCDdZdIfzdvOWOL+4H68G4ZUjZF3VcA9/PsnE/sBgzA==
- dependencies:
- "@babel/core" "^7.19.0"
- base-64 "^1.0.0"
- proskomma-json-tools "^0.6.6"
- pure-uuid "^1.6.3"
- rfdc "^1.3.0"
-
-epitelete@^0.2.20-beta.1:
+epitelete@^0.2.20, epitelete@^0.2.20-beta.1:
version "0.2.20"
resolved "https://registry.yarnpkg.com/epitelete/-/epitelete-0.2.20.tgz#1a89703d5e3152461179bd87540a6f58905141cc"
integrity sha512-dag7scneBXNrR4LLSmeFWxb1ShYeWOxxRqyogJo9UEES2Nr8B5cNDfGqMqYOszCAMdmnGH1RoH1qydAk04JjoA==
@@ -16169,16 +16158,6 @@ proskomma-json-tools@^0.5.7:
ajv "^8.11.0"
deep-equal "^2.0.5"
-proskomma-json-tools@^0.6.6:
- version "0.6.6"
- resolved "https://registry.yarnpkg.com/proskomma-json-tools/-/proskomma-json-tools-0.6.6.tgz#a5df84641ff4c6239f94ab93e3dbf71e52333d88"
- integrity sha512-IEBJTyg6YDKr4E54CLDO1wDjcoiqGKjLxRDTnNSDRaHBG1TJyN1k94V6CHVrvgzKUtQFdWRH77mQ6hJjIUSLVg==
- dependencies:
- "@babel/core" "^7.17.10"
- "@babel/plugin-proposal-throw-expressions" "^7.18.6"
- ajv "^8.11.0"
- deep-equal "^2.0.5"
-
proskomma-json-tools@^0.7.1:
version "0.7.2"
resolved "https://registry.yarnpkg.com/proskomma-json-tools/-/proskomma-json-tools-0.7.2.tgz#317a0be61402811c5a5cea6fbef2468003e242d5"