From bdf0237055df49c05d733e5ea4a28563847de47e Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 23 Sep 2022 13:49:53 -0500 Subject: [PATCH 01/13] First pass on adding Slate Scripture Editing --- react-electron-poc/package-lock.json | 202 +++++++++++- react-electron-poc/package.json | 4 +- .../src/renderer/components/layout/Layout.tsx | 17 +- .../panels/TextPanels/ScriptureTextPanel.tsx | 303 ++++++++++++++++-- .../src/renderer/services/ScriptureService.ts | 16 +- .../src/shared/data/ScriptureTypes.ts | 7 +- 6 files changed, 511 insertions(+), 38 deletions(-) diff --git a/react-electron-poc/package-lock.json b/react-electron-poc/package-lock.json index 7012faa..c0d3eb1 100644 --- a/react-electron-poc/package-lock.json +++ b/react-electron-poc/package-lock.json @@ -14,7 +14,9 @@ "react": "^18.2.0", "react-contenteditable": "^3.3.6", "react-dom": "^18.2.0", - "react-router-dom": "^6.3.0" + "react-router-dom": "^6.3.0", + "slate": "^0.82.1", + "slate-react": "^0.82.2" }, "devDependencies": { "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", @@ -2076,6 +2078,11 @@ "@types/node": "*" } }, + "node_modules/@types/is-hotkey": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz", + "integrity": "sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ==" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -2181,6 +2188,11 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.185", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.185.tgz", + "integrity": "sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA==" + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -4648,6 +4660,11 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", + "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5497,6 +5514,18 @@ "node": ">=8" } }, + "node_modules/direction": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", + "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dmg-builder": { "version": "23.5.0", "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.5.0.tgz", @@ -9148,6 +9177,15 @@ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", "dev": true }, + "node_modules/immer": { + "version": "9.0.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", + "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", @@ -9446,6 +9484,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-hotkey": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz", + "integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==" + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -11347,8 +11390,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.difference": { "version": "4.5.0", @@ -14140,6 +14182,14 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/scroll-into-view-if-needed": { + "version": "2.2.29", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz", + "integrity": "sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==", + "dependencies": { + "compute-scroll-into-view": "^1.0.17" + } + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -14487,6 +14537,52 @@ "node": ">=8" } }, + "node_modules/slate": { + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.82.1.tgz", + "integrity": "sha512-3mdRdq7U3jSEoyFrGvbeb28hgrvrr4NdFCtJX+IjaNvSFozY0VZd/CGHF0zf/JDx7aEov864xd5uj0HQxxEWTQ==", + "dependencies": { + "immer": "^9.0.6", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/slate-react": { + "version": "0.82.2", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.82.2.tgz", + "integrity": "sha512-lNmSqqNOKQJG1i3Wx4MkggWr6dLhQ43n4exPf5NLmKVBHPBuQSRNaWhAAKm6HOEC36Q6xBfkVONIQWi3GSyIEQ==", + "dependencies": { + "@types/is-hotkey": "^0.1.1", + "@types/lodash": "^4.14.149", + "direction": "^1.0.3", + "is-hotkey": "^0.1.6", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.4", + "scroll-into-view-if-needed": "^2.2.20", + "tiny-invariant": "1.0.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.65.3" + } + }, + "node_modules/slate-react/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/slate/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -15192,6 +15288,16 @@ "globrex": "^0.1.2" } }, + "node_modules/tiny-invariant": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", + "integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -18170,6 +18276,11 @@ "@types/node": "*" } }, + "@types/is-hotkey": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz", + "integrity": "sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -18268,6 +18379,11 @@ "@types/node": "*" } }, + "@types/lodash": { + "version": "4.14.185", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.185.tgz", + "integrity": "sha512-evMDG1bC4rgQg4ku9tKpuMh5iBNEwNa3tf9zRHdP1qlv+1WUg44xat4IxCE14gIpZRGUUWAx2VhItCZc25NfMA==" + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -20196,6 +20312,11 @@ } } }, + "compute-scroll-into-view": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", + "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -20821,6 +20942,11 @@ "path-type": "^4.0.0" } }, + "direction": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", + "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==" + }, "dmg-builder": { "version": "23.5.0", "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.5.0.tgz", @@ -23557,6 +23683,11 @@ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", "dev": true }, + "immer": { + "version": "9.0.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz", + "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==" + }, "immutable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", @@ -23770,6 +23901,11 @@ "is-extglob": "^2.1.1" } }, + "is-hotkey": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz", + "integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==" + }, "is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -25186,8 +25322,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.difference": { "version": "4.5.0", @@ -27207,6 +27342,14 @@ "ajv-keywords": "^3.5.2" } }, + "scroll-into-view-if-needed": { + "version": "2.2.29", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz", + "integrity": "sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==", + "requires": { + "compute-scroll-into-view": "^1.0.17" + } + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -27497,6 +27640,45 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slate": { + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.82.1.tgz", + "integrity": "sha512-3mdRdq7U3jSEoyFrGvbeb28hgrvrr4NdFCtJX+IjaNvSFozY0VZd/CGHF0zf/JDx7aEov864xd5uj0HQxxEWTQ==", + "requires": { + "immer": "^9.0.6", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } + } + }, + "slate-react": { + "version": "0.82.2", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.82.2.tgz", + "integrity": "sha512-lNmSqqNOKQJG1i3Wx4MkggWr6dLhQ43n4exPf5NLmKVBHPBuQSRNaWhAAKm6HOEC36Q6xBfkVONIQWi3GSyIEQ==", + "requires": { + "@types/is-hotkey": "^0.1.1", + "@types/lodash": "^4.14.149", + "direction": "^1.0.3", + "is-hotkey": "^0.1.6", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.4", + "scroll-into-view-if-needed": "^2.2.20", + "tiny-invariant": "1.0.6" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } + } + }, "slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -28036,6 +28218,16 @@ "globrex": "^0.1.2" } }, + "tiny-invariant": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", + "integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==" + }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", diff --git a/react-electron-poc/package.json b/react-electron-poc/package.json index 92be70b..fd444d1 100644 --- a/react-electron-poc/package.json +++ b/react-electron-poc/package.json @@ -117,7 +117,9 @@ "react": "^18.2.0", "react-contenteditable": "^3.3.6", "react-dom": "^18.2.0", - "react-router-dom": "^6.3.0" + "react-router-dom": "^6.3.0", + "slate": "^0.82.1", + "slate-react": "^0.82.2" }, "devDependencies": { "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", diff --git a/react-electron-poc/src/renderer/components/layout/Layout.tsx b/react-electron-poc/src/renderer/components/layout/Layout.tsx index 3d49ff4..c2ad44d 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.tsx +++ b/react-electron-poc/src/renderer/components/layout/Layout.tsx @@ -118,7 +118,7 @@ const Layout = () => { }, }, ); - panelManager.current.addPanel( + const zzz1Panel = panelManager.current.addPanel( 'ScriptureTextPanel', { shortName: 'zzz1', @@ -132,6 +132,21 @@ const Layout = () => { }, }, ); + panelManager.current.addPanel( + 'ScriptureTextPanel', + { + shortName: 'zzz6', + editable: true, + isSlate: false, + ...scrRef, + } as ScriptureTextPanelProps, + { + position: { + direction: 'within', + referencePanel: zzz1Panel.id, + }, + }, + ); }, [scrRef], ); diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx index 8e26d51..8b1face 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx @@ -1,6 +1,7 @@ import { getScriptureStyle, getScriptureHtml, + getScripture, } from '@services/ScriptureService'; import { ResourceInfo, @@ -8,24 +9,207 @@ import { ScriptureReference, } from '@shared/data/ScriptureTypes'; import { getTextFromScrRef } from '@util/ScriptureUtil'; -import { isValidValue } from '@util/Util'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { isString, isValidValue } from '@util/Util'; +import { + createElement, + FunctionComponent, + PropsWithChildren, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; import usePromise from 'renderer/hooks/usePromise'; import useStyle from 'renderer/hooks/useStyle'; import './TextPanel.css'; +import { createEditor, BaseEditor, Descendant, NodeEntry, Node } from 'slate'; +import { + Slate, + Editable, + withReact, + ReactEditor, + RenderElementProps, +} from 'slate-react'; + +// Slate types +type CustomEditor = BaseEditor & ReactEditor; + +// Types of components: +// Element - contiguous, semantic elements in the document +// - Ex: Marker Element (in-line) that contains a line and is formatted appropriately +// Text - non-contiguous, character-level formatting +// Decoration - computed at render-time based on the content itself +// - helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting +// - Ex: The text inside markers is styled literally based on if there's a \ +// The notes are based on certain text in a verse +// Maybe we can make decorations that show the \markers before the lines? Otherwise need to be inline void elements +// Normalizing - enforce rules about your content like structure and such +// - Ex: Enforce that a marker element has the \marker at the beginning + +type MyRenderElementProps = PropsWithChildren< + { + element: T; + } & Omit +>; + +type CustomElementProps = { + style: string; + children: CustomElement[]; +}; + +export type VerseElementProps = { + type: 'verse'; +} & CustomElementProps; + +export type ParaElementProps = { + type: 'para'; +} & CustomElementProps; + +export type ChapterElementProps = { + type: 'chapter'; +} & CustomElementProps; + +export type EditorElementProps = { + type: 'editor'; + number: string; + children: CustomElement[]; +}; + +export type CustomElement = + | VerseElementProps + | ParaElementProps + | ChapterElementProps + | EditorElementProps; + +type FormattedText = { text: string; bold?: true }; + +type CustomText = FormattedText; + +type CustomDescendant = CustomElement | CustomText; + +declare module 'slate' { + interface CustomTypes { + Editor: CustomEditor; + Element: CustomElement; + Text: CustomText; + } +} + +// Slate components +/** Prefix added to every marker name for its css class name */ +const MARKER_CLASS_PREFIX = 'usfm_'; + +/** Renders a block-style element. Helper element - not actually rendered on its own */ +const BlockElement = ({ + element: { style }, + attributes, + children, +}: MyRenderElementProps) => ( +
+ {children} +
+); + +/** Renders an inline-style element. Helper element - not actually rendered on its own */ +const InlineElement = ({ + element: { style }, + attributes, + children, +}: MyRenderElementProps) => ( + + {children} + +); + +const VerseElement = (props: MyRenderElementProps) => ( + +); + +/* Renders a complete block of text */ +const ParaElement = (props: MyRenderElementProps) => ( + +); + +/* Renders a chapter number */ +const ChapterElement = (props: MyRenderElementProps) => ( + +); + +const EditorElement = ({ + element: { chapter }, + attributes, + children, +}: MyRenderElementProps) => ( +
+ {children} +
+); + +/** All available elements for use in slate editor */ +const EditorElements = { + verse: VerseElement, + para: ParaElement, + chapter: ChapterElement, + editor: EditorElement, +}; + +/** List of all inline elements */ +const InlineElements = ['verse']; + +/* Renders markers' \marker text with the marker style */ +const MarkerDecoration = ({ + attributes, + children, +}: MyRenderElementProps) => { + return ( + + {children} + + ); +}; + +const DefaultElement = ({ attributes, children }: RenderElementProps) => { + return

{children}

; +}; + +const withScrInlines = (editor: CustomEditor): CustomEditor => { + const { isInline } = editor; + + editor.isInline = (element: CustomElement): boolean => + InlineElements.includes(element.type) || isInline(element); + + return editor; +}; + +const withScrMarkers = (editor: CustomEditor): CustomEditor => { + const { normalizeNode } = editor; + + editor.normalizeNode = (entry: NodeEntry): void => { + const [node, path] = entry; + + // Make sure the marker-based elements all have markers + normalizeNode(entry); + }; + + return editor; +} export interface ScriptureTextPanelProps extends ScriptureReference, - ResourceInfo {} + ResourceInfo { + isSlate: boolean; +} export const ScriptureTextPanel = ({ shortName, editable, + isSlate = true, book, chapter, verse, }: ScriptureTextPanelProps) => { + // Pull in the project's stylesheet useStyle( useCallback(async () => { // TODO: Fix RTL scripture style sheets @@ -37,12 +221,15 @@ export const ScriptureTextPanel = ({ }, [shortName]), ); + // Get the project's contents const [scrChapters] = usePromise( useCallback(async () => { if (!shortName || !isValidValue(book) || !isValidValue(chapter)) return null; - return getScriptureHtml(shortName, book, chapter); - }, [shortName, book, chapter]), + return editable && isSlate + ? getScripture(shortName, book, chapter) + : getScriptureHtml(shortName, book, chapter); + }, [shortName, book, chapter, editable, isSlate]), useState([ { chapter: -1, @@ -55,6 +242,7 @@ export const ScriptureTextPanel = ({ ])[0], ); + // Make a ref for the Scripture that works with react-content-editable const editableScrChapters = useRef([ { chapter: -1, @@ -65,17 +253,20 @@ export const ScriptureTextPanel = ({ })}...`, }, ]); + // Need to force refresh with react-content-editable const [, setForceRefresh] = useState(0); const forceRefresh = useCallback( () => setForceRefresh((value) => value + 1), [setForceRefresh], ); + // When we get new Scripture project contents, update react-content-editable useEffect(() => { editableScrChapters.current = scrChapters; forceRefresh(); }, [scrChapters, forceRefresh]); + // Keep react-content-editable's ref data up-to-date const handleChange = (evt: ContentEditableEvent, editedChapter: number) => { const editedChapterInd = editableScrChapters.current.findIndex( (scrChapter) => scrChapter.chapter === editedChapter, @@ -86,25 +277,93 @@ export const ScriptureTextPanel = ({ }; }; + // Slate editor + const [editor] = useState(() => + withScrMarkers(withScrInlines(withReact(createEditor()))), + ); + + // Render our components for this project + const renderElement = useCallback( + (props: MyRenderElementProps): JSX.Element => { + return createElement( + EditorElements[props.element.type] as FunctionComponent, + props, + ); + /* switch (props.element.type) { + case 'para': + return ( + )} + /> + ); + case 'chapter': + return ( + )} + /> + ); + default: + return ( + + ); + } */ + }, + [], + ); + + // When we get new Scripture project contents, update slate + useEffect(() => { + if (scrChapters && scrChapters.length > 0) { + editor.children = scrChapters.map( + (scrChapter) => + ({ + type: 'editor', + number: scrChapter.chapter.toString(), + children: isString(scrChapter.contents) + ? [{ text: scrChapter.contents } as CustomText] + : (scrChapter.contents as CustomElement[]), + } as EditorElementProps), + ); + /* [ + { + type: 'chapter', + style: 'c', + children: [{ text: scrChapters[0].contents as string }], + }, + ]; */ + editor.onChange(); + } + }, [scrChapters, editor]); + return (
- {editable - ? editableScrChapters.current.map((scrChapter) => ( - handleChange(e, scrChapter.chapter)} - /> - )) - : scrChapters.map((scrChapter) => ( -
- ))} + {editable && + !isSlate && + editableScrChapters.current.map((scrChapter) => ( + handleChange(e, scrChapter.chapter)} + /> + ))} + {editable && isSlate && ( + + + + )} + {!editable && + scrChapters.map((scrChapter) => ( +
+ ))}
); }; diff --git a/react-electron-poc/src/renderer/services/ScriptureService.ts b/react-electron-poc/src/renderer/services/ScriptureService.ts index 95e0a4c..cba63c4 100644 --- a/react-electron-poc/src/renderer/services/ScriptureService.ts +++ b/react-electron-poc/src/renderer/services/ScriptureService.ts @@ -21,7 +21,9 @@ export const getScripture = async ( return chapter >= 0 ? await window.electronAPI.scripture .getScriptureChapter(shortName, bookNum, chapter) - .then((result) => [result]) + .then((result) => [ + { ...result, contents: JSON.parse(result.contents) }, + ]) : await window.electronAPI.scripture.getScriptureBook( shortName, bookNum, @@ -31,11 +33,13 @@ export const getScripture = async ( return [ { chapter, - contents: { - text: `Could not get contents of ${shortName} ${getTextFromScrRef( - { book: bookNum, chapter, verse: -1 }, - )}.`, - }, + contents: [ + { + text: `Could not get contents of ${shortName} ${getTextFromScrRef( + { book: bookNum, chapter, verse: -1 }, + )}.`, + }, + ], }, ]; } diff --git a/react-electron-poc/src/shared/data/ScriptureTypes.ts b/react-electron-poc/src/shared/data/ScriptureTypes.ts index 8b49e6b..2491f16 100644 --- a/react-electron-poc/src/shared/data/ScriptureTypes.ts +++ b/react-electron-poc/src/shared/data/ScriptureTypes.ts @@ -1,3 +1,4 @@ + export interface ScriptureReference { book: number; chapter: number; @@ -10,8 +11,8 @@ export interface ResourceInfo { } /** Slate object for Scripture editing */ -export interface ScriptureContent { - text: string; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ScriptureContent extends CustomDescendant { } /** Scipture chapter contents along with which chapter it is */ @@ -22,7 +23,7 @@ export interface ScriptureChapter { /** Scipture chapter contents Slate object along with which chapter it is */ export interface ScriptureChapterContent extends ScriptureChapter { - contents: ScriptureContent; + contents: ScriptureContent[]; } /** Scripture chapter string (usx, usfm, html, etc) along with which chapter it is */ From 2b7c991fab81d0b2d66fc91ee364ab2f02094083 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 26 Sep 2022 16:13:20 -0500 Subject: [PATCH 02/13] Displayed markers, added inline markers --- .../panels/TextPanels/ScriptureTextPanel.tsx | 95 +++++++++++++------ .../src/renderer/services/ScriptureService.ts | 7 +- 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx index 8b1face..96e3528 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx @@ -14,6 +14,7 @@ import { createElement, FunctionComponent, PropsWithChildren, + ReactNode, useCallback, useEffect, useRef, @@ -23,7 +24,14 @@ import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; import usePromise from 'renderer/hooks/usePromise'; import useStyle from 'renderer/hooks/useStyle'; import './TextPanel.css'; -import { createEditor, BaseEditor, Descendant, NodeEntry, Node } from 'slate'; +import { + createEditor, + BaseEditor, + NodeEntry, + Node, + Range, + Transforms, +} from 'slate'; import { Slate, Editable, @@ -53,10 +61,23 @@ type MyRenderElementProps = PropsWithChildren< } & Omit >; +/** Base element props. All elements should have a style */ +type StyleProps = { + style?: string; +}; + +type MarkerProps = { + closingMarker?: boolean; +} & StyleProps; + type CustomElementProps = { - style: string; children: CustomElement[]; -}; +} & StyleProps; + +export type InlineElementProps = { + endSpace?: boolean; + closingMarker?: boolean; +} & MyRenderElementProps; export type VerseElementProps = { type: 'verse'; @@ -66,6 +87,10 @@ export type ParaElementProps = { type: 'para'; } & CustomElementProps; +export type CharElementProps = { + type: 'char'; +} & CustomElementProps; + export type ChapterElementProps = { type: 'chapter'; } & CustomElementProps; @@ -79,15 +104,14 @@ export type EditorElementProps = { export type CustomElement = | VerseElementProps | ParaElementProps + | CharElementProps | ChapterElementProps | EditorElementProps; -type FormattedText = { text: string; bold?: true }; +type FormattedText = { text: string } & MarkerProps; type CustomText = FormattedText; -type CustomDescendant = CustomElement | CustomText; - declare module 'slate' { interface CustomTypes { Editor: CustomEditor; @@ -97,6 +121,16 @@ declare module 'slate' { } // Slate components + +/* Renders markers' \marker text with the marker style */ +const Marker = ({ style, closingMarker = false }: MarkerProps) => { + return ( + + {`\\${style}${closingMarker ? '' : ' '}`} + + ); +}; + /** Prefix added to every marker name for its css class name */ const MARKER_CLASS_PREFIX = 'usfm_'; @@ -107,6 +141,7 @@ const BlockElement = ({ children, }: MyRenderElementProps) => (
+ {children}
); @@ -116,32 +151,45 @@ const InlineElement = ({ element: { style }, attributes, children, -}: MyRenderElementProps) => ( + endSpace = false, + closingMarker = false, +}: InlineElementProps) => ( + {children} + {endSpace ? ' ' : undefined} + {closingMarker ? ( + + ) : undefined} ); const VerseElement = (props: MyRenderElementProps) => ( - + ); -/* Renders a complete block of text */ +/** Renders a complete block of text with an open marker at the start */ const ParaElement = (props: MyRenderElementProps) => ( ); -/* Renders a chapter number */ +/** Renders inline text with closed markers around it */ +const CharElement = (props: MyRenderElementProps) => ( + +); + +/** Renders a chapter number */ const ChapterElement = (props: MyRenderElementProps) => ( ); +/** Overall chapter editor element */ const EditorElement = ({ - element: { chapter }, + element: { number }, attributes, children, }: MyRenderElementProps) => ( -
+
{children}
); @@ -150,24 +198,13 @@ const EditorElement = ({ const EditorElements = { verse: VerseElement, para: ParaElement, + char: CharElement, chapter: ChapterElement, editor: EditorElement, }; /** List of all inline elements */ -const InlineElements = ['verse']; - -/* Renders markers' \marker text with the marker style */ -const MarkerDecoration = ({ - attributes, - children, -}: MyRenderElementProps) => { - return ( - - {children} - - ); -}; +const InlineElements = ['verse', 'char']; const DefaultElement = ({ attributes, children }: RenderElementProps) => { return

{children}

; @@ -193,7 +230,7 @@ const withScrMarkers = (editor: CustomEditor): CustomEditor => { }; return editor; -} +}; export interface ScriptureTextPanelProps extends ScriptureReference, @@ -286,8 +323,10 @@ export const ScriptureTextPanel = ({ const renderElement = useCallback( (props: MyRenderElementProps): JSX.Element => { return createElement( - EditorElements[props.element.type] as FunctionComponent, - props, + (EditorElements[props.element.type] || + DefaultElement) as FunctionComponent, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props as any, ); /* switch (props.element.type) { case 'para': diff --git a/react-electron-poc/src/renderer/services/ScriptureService.ts b/react-electron-poc/src/renderer/services/ScriptureService.ts index cba63c4..b6618ef 100644 --- a/react-electron-poc/src/renderer/services/ScriptureService.ts +++ b/react-electron-poc/src/renderer/services/ScriptureService.ts @@ -22,7 +22,12 @@ export const getScripture = async ( ? await window.electronAPI.scripture .getScriptureChapter(shortName, bookNum, chapter) .then((result) => [ - { ...result, contents: JSON.parse(result.contents) }, + { + ...result, + contents: JSON.parse( + result.contents as unknown as string, // Parsing from string, but it's nice to know getScripture intends to send json of known type + ), + }, ]) : await window.electronAPI.scripture.getScriptureBook( shortName, From 3e7f303957502cbe0167da4b2b40e48aac4f9456 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Tue, 27 Sep 2022 10:27:30 -0500 Subject: [PATCH 03/13] Split Slate editor into ScriptureTextPanelSlate, fixed double scroll bars, fixed error changing chapter on selection --- .../src/renderer/components/layout/Layout.css | 10 +- .../src/renderer/components/layout/Layout.tsx | 9 +- .../components/panels/PanelManager.ts | 6 +- .../src/renderer/components/panels/Panels.ts | 2 + .../panels/TextPanels/ScriptureTextPanel.tsx | 336 ++---------------- .../TextPanels/ScriptureTextPanelSlate.tsx | 332 +++++++++++++++++ 6 files changed, 373 insertions(+), 322 deletions(-) create mode 100644 react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx diff --git a/react-electron-poc/src/renderer/components/layout/Layout.css b/react-electron-poc/src/renderer/components/layout/Layout.css index 53ef03e..1ad62e8 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.css +++ b/react-electron-poc/src/renderer/components/layout/Layout.css @@ -3,12 +3,20 @@ body { } #root { - width: 100vw; height: 100vh; } .layout { height: 100%; + /* TODO: Figure out restoring after maximizing */ + /* display: flex; + flex-direction: column; */ +} + +.layout .layout-dock { + /* TODO: Figure out restoring after maximizing */ + /* flex-grow: 1; */ + height: calc(100% - 47px); } .groupview.active-group > .tabs-and-actions-container { diff --git a/react-electron-poc/src/renderer/components/layout/Layout.tsx b/react-electron-poc/src/renderer/components/layout/Layout.tsx index c2ad44d..6639faf 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.tsx +++ b/react-electron-poc/src/renderer/components/layout/Layout.tsx @@ -91,7 +91,7 @@ const Layout = () => { }, ); panelManager.current.addPanel( - 'ScriptureTextPanel', + 'ScriptureTextPanelSlate', { shortName: 'zzz6', editable: true, @@ -137,7 +137,6 @@ const Layout = () => { { shortName: 'zzz6', editable: true, - isSlate: false, ...scrRef, } as ScriptureTextPanelProps, { @@ -152,16 +151,16 @@ const Layout = () => { ); return ( - <> +
-
+
- +
); }; export default Layout; diff --git a/react-electron-poc/src/renderer/components/panels/PanelManager.ts b/react-electron-poc/src/renderer/components/panels/PanelManager.ts index 4485221..d03c2f0 100644 --- a/react-electron-poc/src/renderer/components/panels/PanelManager.ts +++ b/react-electron-poc/src/renderer/components/panels/PanelManager.ts @@ -30,13 +30,15 @@ export class PanelManager { throw new Error('generatePanelTitle: panelInfo undefined!'); if (panelInfo.title || panelInfo.title === '') return panelInfo.title; - if (panelInfo.type === 'ScriptureTextPanel') { + if (panelInfo.type.startsWith('ScriptureTextPanel')) { const scrPanelProps = panelProps as ScriptureTextPanelProps; return `${scrPanelProps.shortName}: ${getTextFromScrRef({ book: scrPanelProps.book, chapter: scrPanelProps.chapter, verse: -1, - })}${scrPanelProps.editable ? ' Editable' : ''}`; + })}${scrPanelProps.editable ? ' Editable' : ''}${ + panelInfo.type === 'ScriptureTextPanelSlate' ? ' Slate' : '' + }`; } return panelInfo.type; } diff --git a/react-electron-poc/src/renderer/components/panels/Panels.ts b/react-electron-poc/src/renderer/components/panels/Panels.ts index d1d2868..e1db2b0 100644 --- a/react-electron-poc/src/renderer/components/panels/Panels.ts +++ b/react-electron-poc/src/renderer/components/panels/Panels.ts @@ -5,6 +5,7 @@ import { TextPanel } from './TextPanels/TextPanel'; import { HtmlTextPanel } from './TextPanels/HtmlTextPanel'; import { EditableHtmlTextPanel } from './TextPanels/EditableHtmlTextPanel'; import { ScriptureTextPanel } from './TextPanels/ScriptureTextPanel'; +import { ScriptureTextPanelSlate } from './TextPanels/ScriptureTextPanelSlate'; // TODO: Consider doing something a bit different with this https://stackoverflow.com/questions/29722270/is-it-possible-to-import-modules-from-all-files-in-a-directory-using-a-wildcard /** All available panels for use in dockviews */ @@ -14,6 +15,7 @@ export const Panels = { HtmlTextPanel, EditableHtmlTextPanel, ScriptureTextPanel, + ScriptureTextPanelSlate, }; export default Panels; diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx index 96e3528..5c80b0f 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx @@ -1,7 +1,6 @@ import { getScriptureStyle, getScriptureHtml, - getScripture, } from '@services/ScriptureService'; import { ResourceInfo, @@ -9,239 +8,20 @@ import { ScriptureReference, } from '@shared/data/ScriptureTypes'; import { getTextFromScrRef } from '@util/ScriptureUtil'; -import { isString, isValidValue } from '@util/Util'; -import { - createElement, - FunctionComponent, - PropsWithChildren, - ReactNode, - useCallback, - useEffect, - useRef, - useState, -} from 'react'; +import { isValidValue } from '@util/Util'; +import { useCallback, useEffect, useRef, useState } from 'react'; import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; import usePromise from 'renderer/hooks/usePromise'; import useStyle from 'renderer/hooks/useStyle'; import './TextPanel.css'; -import { - createEditor, - BaseEditor, - NodeEntry, - Node, - Range, - Transforms, -} from 'slate'; -import { - Slate, - Editable, - withReact, - ReactEditor, - RenderElementProps, -} from 'slate-react'; - -// Slate types -type CustomEditor = BaseEditor & ReactEditor; - -// Types of components: -// Element - contiguous, semantic elements in the document -// - Ex: Marker Element (in-line) that contains a line and is formatted appropriately -// Text - non-contiguous, character-level formatting -// Decoration - computed at render-time based on the content itself -// - helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting -// - Ex: The text inside markers is styled literally based on if there's a \ -// The notes are based on certain text in a verse -// Maybe we can make decorations that show the \markers before the lines? Otherwise need to be inline void elements -// Normalizing - enforce rules about your content like structure and such -// - Ex: Enforce that a marker element has the \marker at the beginning - -type MyRenderElementProps = PropsWithChildren< - { - element: T; - } & Omit ->; - -/** Base element props. All elements should have a style */ -type StyleProps = { - style?: string; -}; - -type MarkerProps = { - closingMarker?: boolean; -} & StyleProps; - -type CustomElementProps = { - children: CustomElement[]; -} & StyleProps; - -export type InlineElementProps = { - endSpace?: boolean; - closingMarker?: boolean; -} & MyRenderElementProps; - -export type VerseElementProps = { - type: 'verse'; -} & CustomElementProps; - -export type ParaElementProps = { - type: 'para'; -} & CustomElementProps; - -export type CharElementProps = { - type: 'char'; -} & CustomElementProps; - -export type ChapterElementProps = { - type: 'chapter'; -} & CustomElementProps; - -export type EditorElementProps = { - type: 'editor'; - number: string; - children: CustomElement[]; -}; - -export type CustomElement = - | VerseElementProps - | ParaElementProps - | CharElementProps - | ChapterElementProps - | EditorElementProps; - -type FormattedText = { text: string } & MarkerProps; - -type CustomText = FormattedText; - -declare module 'slate' { - interface CustomTypes { - Editor: CustomEditor; - Element: CustomElement; - Text: CustomText; - } -} - -// Slate components - -/* Renders markers' \marker text with the marker style */ -const Marker = ({ style, closingMarker = false }: MarkerProps) => { - return ( - - {`\\${style}${closingMarker ? '' : ' '}`} - - ); -}; - -/** Prefix added to every marker name for its css class name */ -const MARKER_CLASS_PREFIX = 'usfm_'; - -/** Renders a block-style element. Helper element - not actually rendered on its own */ -const BlockElement = ({ - element: { style }, - attributes, - children, -}: MyRenderElementProps) => ( -
- - {children} -
-); - -/** Renders an inline-style element. Helper element - not actually rendered on its own */ -const InlineElement = ({ - element: { style }, - attributes, - children, - endSpace = false, - closingMarker = false, -}: InlineElementProps) => ( - - - {children} - {endSpace ? ' ' : undefined} - {closingMarker ? ( - - ) : undefined} - -); - -const VerseElement = (props: MyRenderElementProps) => ( - -); - -/** Renders a complete block of text with an open marker at the start */ -const ParaElement = (props: MyRenderElementProps) => ( - -); - -/** Renders inline text with closed markers around it */ -const CharElement = (props: MyRenderElementProps) => ( - -); - -/** Renders a chapter number */ -const ChapterElement = (props: MyRenderElementProps) => ( - -); - -/** Overall chapter editor element */ -const EditorElement = ({ - element: { number }, - attributes, - children, -}: MyRenderElementProps) => ( -
- {children} -
-); - -/** All available elements for use in slate editor */ -const EditorElements = { - verse: VerseElement, - para: ParaElement, - char: CharElement, - chapter: ChapterElement, - editor: EditorElement, -}; - -/** List of all inline elements */ -const InlineElements = ['verse', 'char']; - -const DefaultElement = ({ attributes, children }: RenderElementProps) => { - return

{children}

; -}; - -const withScrInlines = (editor: CustomEditor): CustomEditor => { - const { isInline } = editor; - - editor.isInline = (element: CustomElement): boolean => - InlineElements.includes(element.type) || isInline(element); - - return editor; -}; - -const withScrMarkers = (editor: CustomEditor): CustomEditor => { - const { normalizeNode } = editor; - - editor.normalizeNode = (entry: NodeEntry): void => { - const [node, path] = entry; - - // Make sure the marker-based elements all have markers - normalizeNode(entry); - }; - - return editor; -}; export interface ScriptureTextPanelProps extends ScriptureReference, - ResourceInfo { - isSlate: boolean; -} + ResourceInfo {} export const ScriptureTextPanel = ({ shortName, editable, - isSlate = true, book, chapter, verse, @@ -263,10 +43,8 @@ export const ScriptureTextPanel = ({ useCallback(async () => { if (!shortName || !isValidValue(book) || !isValidValue(chapter)) return null; - return editable && isSlate - ? getScripture(shortName, book, chapter) - : getScriptureHtml(shortName, book, chapter); - }, [shortName, book, chapter, editable, isSlate]), + return getScriptureHtml(shortName, book, chapter); + }, [shortName, book, chapter]), useState([ { chapter: -1, @@ -314,95 +92,25 @@ export const ScriptureTextPanel = ({ }; }; - // Slate editor - const [editor] = useState(() => - withScrMarkers(withScrInlines(withReact(createEditor()))), - ); - - // Render our components for this project - const renderElement = useCallback( - (props: MyRenderElementProps): JSX.Element => { - return createElement( - (EditorElements[props.element.type] || - DefaultElement) as FunctionComponent, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - props as any, - ); - /* switch (props.element.type) { - case 'para': - return ( - )} - /> - ); - case 'chapter': - return ( - )} - /> - ); - default: - return ( - - ); - } */ - }, - [], - ); - - // When we get new Scripture project contents, update slate - useEffect(() => { - if (scrChapters && scrChapters.length > 0) { - editor.children = scrChapters.map( - (scrChapter) => - ({ - type: 'editor', - number: scrChapter.chapter.toString(), - children: isString(scrChapter.contents) - ? [{ text: scrChapter.contents } as CustomText] - : (scrChapter.contents as CustomElement[]), - } as EditorElementProps), - ); - /* [ - { - type: 'chapter', - style: 'c', - children: [{ text: scrChapters[0].contents as string }], - }, - ]; */ - editor.onChange(); - } - }, [scrChapters, editor]); - return (
- {editable && - !isSlate && - editableScrChapters.current.map((scrChapter) => ( - handleChange(e, scrChapter.chapter)} - /> - ))} - {editable && isSlate && ( - - - - )} - {!editable && - scrChapters.map((scrChapter) => ( -
- ))} + {editable + ? editableScrChapters.current.map((scrChapter) => ( + handleChange(e, scrChapter.chapter)} + /> + )) + : scrChapters.map((scrChapter) => ( +
+ ))}
); }; diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx new file mode 100644 index 0000000..9e430a3 --- /dev/null +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -0,0 +1,332 @@ +import { getScriptureStyle, getScripture } from '@services/ScriptureService'; +import { + ResourceInfo, + ScriptureChapter, + ScriptureReference, +} from '@shared/data/ScriptureTypes'; +import { getTextFromScrRef } from '@util/ScriptureUtil'; +import { isString, isValidValue } from '@util/Util'; +import { + createElement, + FunctionComponent, + PropsWithChildren, + useCallback, + useEffect, + useState, +} from 'react'; +import usePromise from 'renderer/hooks/usePromise'; +import useStyle from 'renderer/hooks/useStyle'; +import './TextPanel.css'; +import { createEditor, BaseEditor, NodeEntry, Node, Transforms } from 'slate'; +import { + Slate, + Editable, + withReact, + ReactEditor, + RenderElementProps, +} from 'slate-react'; + +// Slate types +type CustomEditor = BaseEditor & ReactEditor; + +// Types of components: +// Element - contiguous, semantic elements in the document +// - Ex: Marker Element (in-line) that contains a line and is formatted appropriately +// Text - non-contiguous, character-level formatting +// Decoration - computed at render-time based on the content itself +// - helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting +// - Ex: The text inside markers is styled literally based on if there's a \ +// The notes are based on certain text in a verse +// Maybe we can make decorations that show the \markers before the lines? Otherwise need to be inline void elements +// Normalizing - enforce rules about your content like structure and such +// - Ex: Enforce that a marker element has the \marker at the beginning + +type MyRenderElementProps = PropsWithChildren< + { + element: T; + } & Omit +>; + +/** Base element props. All elements should have a style */ +type StyleProps = { + style?: string; +}; + +type MarkerProps = { + closingMarker?: boolean; +} & StyleProps; + +type CustomElementProps = { + children: CustomElement[]; +} & StyleProps; + +export type InlineElementProps = { + endSpace?: boolean; + closingMarker?: boolean; +} & MyRenderElementProps; + +export type VerseElementProps = { + type: 'verse'; +} & CustomElementProps; + +export type ParaElementProps = { + type: 'para'; +} & CustomElementProps; + +export type CharElementProps = { + type: 'char'; +} & CustomElementProps; + +export type ChapterElementProps = { + type: 'chapter'; +} & CustomElementProps; + +export type EditorElementProps = { + type: 'editor'; + number: string; + children: CustomElement[]; +}; + +export type CustomElement = + | VerseElementProps + | ParaElementProps + | CharElementProps + | ChapterElementProps + | EditorElementProps; + +type FormattedText = { text: string } & MarkerProps; + +type CustomText = FormattedText; + +declare module 'slate' { + interface CustomTypes { + Editor: CustomEditor; + Element: CustomElement; + Text: CustomText; + } +} + +// Slate components + +/* Renders markers' \marker text with the marker style */ +const Marker = ({ style, closingMarker = false }: MarkerProps) => { + return ( + + {`\\${style}${closingMarker ? '' : ' '}`} + + ); +}; + +/** Prefix added to every marker name for its css class name */ +const MARKER_CLASS_PREFIX = 'usfm_'; + +/** Renders a block-style element. Helper element - not actually rendered on its own */ +const BlockElement = ({ + element: { style }, + attributes, + children, +}: MyRenderElementProps) => ( +
+ + {children} +
+); + +/** Renders an inline-style element. Helper element - not actually rendered on its own */ +const InlineElement = ({ + element: { style }, + attributes, + children, + endSpace = false, + closingMarker = false, +}: InlineElementProps) => ( + + + {children} + {endSpace ? ' ' : undefined} + {closingMarker ? ( + + ) : undefined} + +); + +const VerseElement = (props: MyRenderElementProps) => ( + +); + +/** Renders a complete block of text with an open marker at the start */ +const ParaElement = (props: MyRenderElementProps) => ( + +); + +/** Renders inline text with closed markers around it */ +const CharElement = (props: MyRenderElementProps) => ( + +); + +/** Renders a chapter number */ +const ChapterElement = (props: MyRenderElementProps) => ( + +); + +/** Overall chapter editor element */ +const EditorElement = ({ + element: { number }, + attributes, + children, +}: MyRenderElementProps) => ( +
+ {children} +
+); + +/** All available elements for use in slate editor */ +const EditorElements = { + verse: VerseElement, + para: ParaElement, + char: CharElement, + chapter: ChapterElement, + editor: EditorElement, +}; + +/** List of all inline elements */ +const InlineElements = ['verse', 'char']; + +const DefaultElement = ({ attributes, children }: RenderElementProps) => { + return

{children}

; +}; + +const withScrInlines = (editor: CustomEditor): CustomEditor => { + const { isInline } = editor; + + editor.isInline = (element: CustomElement): boolean => + InlineElements.includes(element.type) || isInline(element); + + return editor; +}; + +const withScrMarkers = (editor: CustomEditor): CustomEditor => { + const { normalizeNode } = editor; + + editor.normalizeNode = (entry: NodeEntry): void => { + const [node, path] = entry; + + // Make sure the marker-based elements all have markers + normalizeNode(entry); + }; + + return editor; +}; + +export interface ScriptureTextPanelProps + extends ScriptureReference, + ResourceInfo {} + +export const ScriptureTextPanelSlate = ({ + shortName, + editable, + book, + chapter, + verse, +}: ScriptureTextPanelProps) => { + // Pull in the project's stylesheet + useStyle( + useCallback(async () => { + // TODO: Fix RTL scripture style sheets + if (!shortName) return undefined; + const style = await getScriptureStyle(shortName); + return shortName !== 'OHEB' && shortName !== 'zzz1' + ? style + : undefined; + }, [shortName]), + ); + + // Get the project's contents + const [scrChapters] = usePromise( + useCallback(async () => { + if (!shortName || !isValidValue(book) || !isValidValue(chapter)) + return null; + return getScripture(shortName, book, chapter); + }, [shortName, book, chapter]), + useState([ + { + chapter: -1, + contents: `Loading ${shortName} ${getTextFromScrRef({ + book, + chapter, + verse: -1, + })}...`, + }, + ])[0], + ); + + // Slate editor + const [editor] = useState(() => + withScrMarkers(withScrInlines(withReact(createEditor()))), + ); + + // Render our components for this project + const renderElement = useCallback( + (props: MyRenderElementProps): JSX.Element => { + return createElement( + (EditorElements[props.element.type] || + DefaultElement) as FunctionComponent, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props as any, + ); + /* switch (props.element.type) { + case 'para': + return ( + )} + /> + ); + case 'chapter': + return ( + )} + /> + ); + default: + return ( + + ); + } */ + }, + [], + ); + + // When we get new Scripture project contents, update slate + useEffect(() => { + if (scrChapters && scrChapters.length > 0) { + // TODO: Save the verse...? Maybe if book/chapter was not changed? + + // Unselect + Transforms.deselect(editor); + + // Replace the editor's contents + editor.children = scrChapters.map( + (scrChapter) => + ({ + type: 'editor', + number: scrChapter.chapter.toString(), + children: isString(scrChapter.contents) + ? [{ text: scrChapter.contents } as CustomText] + : (scrChapter.contents as CustomElement[]), + } as EditorElementProps), + ); + + // TODO: Update cursor to new ScrRef + + editor.onChange(); + } + }, [scrChapters, editor]); + + return ( +
+ + + +
+ ); +}; From 81cd2d6ff93b306075304f4022335a8c20b521b5 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Tue, 27 Sep 2022 16:43:21 -0500 Subject: [PATCH 04/13] Workaround for dockview not resizing on restore from maximize --- .../src/renderer/components/layout/Layout.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/react-electron-poc/src/renderer/components/layout/Layout.css b/react-electron-poc/src/renderer/components/layout/Layout.css index 1ad62e8..f52eb03 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.css +++ b/react-electron-poc/src/renderer/components/layout/Layout.css @@ -8,15 +8,15 @@ body { .layout { height: 100%; - /* TODO: Figure out restoring after maximizing */ - /* display: flex; - flex-direction: column; */ + display: flex; + flex-direction: column; } .layout .layout-dock { - /* TODO: Figure out restoring after maximizing */ - /* flex-grow: 1; */ - height: calc(100% - 47px); + flex-grow: 1; + /* Workaround for restoring from maximized leaving the dockview at maximized height. + See https://github.com/mathuo/dockview/issues/158#issuecomment-1260007967 for more info */ + overflow: hidden; } .groupview.active-group > .tabs-and-actions-container { From 56bdd06950fb5cd93955aadfed1fe887d5bf221e Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Tue, 27 Sep 2022 17:24:23 -0500 Subject: [PATCH 05/13] Refactored ScriptureTextPanels into HOC --- .../panels/TextPanels/ScriptureTextPanel.tsx | 162 +++++++-------- .../TextPanels/ScriptureTextPanelSlate.tsx | 191 ++++++++---------- 2 files changed, 153 insertions(+), 200 deletions(-) diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx index 5c80b0f..517d018 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx @@ -5,6 +5,7 @@ import { import { ResourceInfo, ScriptureChapter, + ScriptureChapterString, ScriptureReference, } from '@shared/data/ScriptureTypes'; import { getTextFromScrRef } from '@util/ScriptureUtil'; @@ -13,104 +14,85 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; import usePromise from 'renderer/hooks/usePromise'; import useStyle from 'renderer/hooks/useStyle'; +import { + ScriptureTextPanelHOC, + ScriptureTextPanelHOCProps, +} from './ScriptureTextPanelHOC'; import './TextPanel.css'; -export interface ScriptureTextPanelProps - extends ScriptureReference, - ResourceInfo {} +export interface ScriptureTextPanelProps extends ScriptureTextPanelHOCProps { + scrChapters: ScriptureChapterString[]; +} -export const ScriptureTextPanel = ({ - shortName, - editable, - book, - chapter, - verse, -}: ScriptureTextPanelProps) => { - // Pull in the project's stylesheet - useStyle( - useCallback(async () => { - // TODO: Fix RTL scripture style sheets - if (!shortName) return undefined; - const style = await getScriptureStyle(shortName); - return shortName !== 'OHEB' && shortName !== 'zzz1' - ? style - : undefined; - }, [shortName]), - ); +/** The function to use to get the Scripture chapter content to display */ +const getScrChapter = getScriptureHtml; - // Get the project's contents - const [scrChapters] = usePromise( - useCallback(async () => { - if (!shortName || !isValidValue(book) || !isValidValue(chapter)) - return null; - return getScriptureHtml(shortName, book, chapter); - }, [shortName, book, chapter]), - useState([ +export const ScriptureTextPanel = ScriptureTextPanelHOC( + ({ + shortName, + editable, + book, + chapter, + verse, + scrChapters, + }: ScriptureTextPanelProps) => { + // Make a ref for the Scripture that works with react-content-editable + const editableScrChapters = useRef([ { chapter: -1, - contents: `Loading ${shortName} ${getTextFromScrRef({ - book, - chapter, - verse: -1, - })}...`, + contents: 'Loading', }, - ])[0], - ); - - // Make a ref for the Scripture that works with react-content-editable - const editableScrChapters = useRef([ - { - chapter: -1, - contents: `Loading ${shortName} ${getTextFromScrRef({ - book, - chapter, - verse: -1, - })}...`, - }, - ]); - // Need to force refresh with react-content-editable - const [, setForceRefresh] = useState(0); - const forceRefresh = useCallback( - () => setForceRefresh((value) => value + 1), - [setForceRefresh], - ); + ]); + // Need to force refresh with react-content-editable + const [, setForceRefresh] = useState(0); + const forceRefresh = useCallback( + () => setForceRefresh((value) => value + 1), + [setForceRefresh], + ); - // When we get new Scripture project contents, update react-content-editable - useEffect(() => { - editableScrChapters.current = scrChapters; - forceRefresh(); - }, [scrChapters, forceRefresh]); + // When we get new Scripture project contents, update react-content-editable + useEffect(() => { + editableScrChapters.current = scrChapters; + forceRefresh(); + }, [scrChapters, forceRefresh]); - // Keep react-content-editable's ref data up-to-date - const handleChange = (evt: ContentEditableEvent, editedChapter: number) => { - const editedChapterInd = editableScrChapters.current.findIndex( - (scrChapter) => scrChapter.chapter === editedChapter, - ); - editableScrChapters.current[editedChapterInd] = { - ...editableScrChapters.current[editedChapterInd], - contents: evt.target.value, + // Keep react-content-editable's ref data up-to-date + const handleChange = ( + evt: ContentEditableEvent, + editedChapter: number, + ) => { + const editedChapterInd = editableScrChapters.current.findIndex( + (scrChapter) => scrChapter.chapter === editedChapter, + ); + editableScrChapters.current[editedChapterInd] = { + ...editableScrChapters.current[editedChapterInd], + contents: evt.target.value, + }; }; - }; - return ( -
- {editable - ? editableScrChapters.current.map((scrChapter) => ( - handleChange(e, scrChapter.chapter)} - /> - )) - : scrChapters.map((scrChapter) => ( -
- ))} -
- ); -}; + return ( +
+ {editable + ? editableScrChapters.current.map((scrChapter) => ( + + handleChange(e, scrChapter.chapter) + } + /> + )) + : scrChapters.map((scrChapter) => ( +
+ ))} +
+ ); + }, + getScrChapter, +); diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index 9e430a3..f5797a3 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -2,6 +2,7 @@ import { getScriptureStyle, getScripture } from '@services/ScriptureService'; import { ResourceInfo, ScriptureChapter, + ScriptureChapterContent, ScriptureReference, } from '@shared/data/ScriptureTypes'; import { getTextFromScrRef } from '@util/ScriptureUtil'; @@ -25,6 +26,10 @@ import { ReactEditor, RenderElementProps, } from 'slate-react'; +import { + ScriptureTextPanelHOC, + ScriptureTextPanelHOCProps, +} from './ScriptureTextPanelHOC'; // Slate types type CustomEditor = BaseEditor & ReactEditor; @@ -218,115 +223,81 @@ const withScrMarkers = (editor: CustomEditor): CustomEditor => { return editor; }; -export interface ScriptureTextPanelProps - extends ScriptureReference, - ResourceInfo {} - -export const ScriptureTextPanelSlate = ({ - shortName, - editable, - book, - chapter, - verse, -}: ScriptureTextPanelProps) => { - // Pull in the project's stylesheet - useStyle( - useCallback(async () => { - // TODO: Fix RTL scripture style sheets - if (!shortName) return undefined; - const style = await getScriptureStyle(shortName); - return shortName !== 'OHEB' && shortName !== 'zzz1' - ? style - : undefined; - }, [shortName]), - ); +export interface ScriptureTextPanelProps extends ScriptureTextPanelHOCProps { + scrChapters: ScriptureChapterContent[]; +} - // Get the project's contents - const [scrChapters] = usePromise( - useCallback(async () => { - if (!shortName || !isValidValue(book) || !isValidValue(chapter)) - return null; - return getScripture(shortName, book, chapter); - }, [shortName, book, chapter]), - useState([ - { - chapter: -1, - contents: `Loading ${shortName} ${getTextFromScrRef({ - book, - chapter, - verse: -1, - })}...`, +/** The function to use to get the Scripture chapter content to display */ +const getScrChapter = getScripture; + +export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( + ({ + shortName, + editable, + book, + chapter, + verse, + scrChapters, + }: ScriptureTextPanelProps) => { + // Slate editor + const [editor] = useState(() => + withScrMarkers(withScrInlines(withReact(createEditor()))), + ); + + // Render our components for this project + const renderElement = useCallback( + (props: MyRenderElementProps): JSX.Element => { + return createElement( + (EditorElements[props.element.type] || + DefaultElement) as FunctionComponent, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props as any, + ); }, - ])[0], - ); - - // Slate editor - const [editor] = useState(() => - withScrMarkers(withScrInlines(withReact(createEditor()))), - ); - - // Render our components for this project - const renderElement = useCallback( - (props: MyRenderElementProps): JSX.Element => { - return createElement( - (EditorElements[props.element.type] || - DefaultElement) as FunctionComponent, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - props as any, - ); - /* switch (props.element.type) { - case 'para': - return ( - )} - /> - ); - case 'chapter': - return ( - )} - /> - ); - default: - return ( - - ); - } */ - }, - [], - ); - - // When we get new Scripture project contents, update slate - useEffect(() => { - if (scrChapters && scrChapters.length > 0) { - // TODO: Save the verse...? Maybe if book/chapter was not changed? - - // Unselect - Transforms.deselect(editor); - - // Replace the editor's contents - editor.children = scrChapters.map( - (scrChapter) => - ({ - type: 'editor', - number: scrChapter.chapter.toString(), - children: isString(scrChapter.contents) - ? [{ text: scrChapter.contents } as CustomText] - : (scrChapter.contents as CustomElement[]), - } as EditorElementProps), - ); - - // TODO: Update cursor to new ScrRef - - editor.onChange(); - } - }, [scrChapters, editor]); - - return ( -
- - - -
- ); -}; + [], + ); + + // When we get new Scripture project contents, update slate + useEffect(() => { + if (scrChapters && scrChapters.length > 0) { + // TODO: Save the verse...? Maybe if book/chapter was not changed? + + // Unselect + Transforms.deselect(editor); + + // Replace the editor's contents + editor.children = scrChapters.map( + (scrChapter) => + ({ + type: 'editor', + number: scrChapter.chapter.toString(), + children: isString(scrChapter.contents) + ? [ + { + // TODO: When loading, the contents come as a string. Consider how to improve the loading value in ScriptureTextPanelHOC + text: scrChapter.contents as unknown as string, + } as CustomText, + ] + : (scrChapter.contents as CustomElement[]), + } as EditorElementProps), + ); + + // TODO: Update cursor to new ScrRef + + editor.onChange(); + } + }, [scrChapters, editor]); + + return ( +
+ + + +
+ ); + }, + getScrChapter, +); From 2eab0ee963801911510cf3541899a70bd8c65f2c Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Tue, 27 Sep 2022 17:28:56 -0500 Subject: [PATCH 06/13] Added ScriptureTetPanelHOC that I forgot with the last commit --- .../TextPanels/ScriptureTextPanelHOC.tsx | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx new file mode 100644 index 0000000..d8bb9e0 --- /dev/null +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx @@ -0,0 +1,70 @@ +import { getScriptureStyle } from '@services/ScriptureService'; +import { + ResourceInfo, + ScriptureChapter, + ScriptureReference, +} from '@shared/data/ScriptureTypes'; +import { getTextFromScrRef } from '@util/ScriptureUtil'; +import { isValidValue } from '@util/Util'; +import { ComponentType, PropsWithChildren, useCallback, useState } from 'react'; +import usePromise from 'renderer/hooks/usePromise'; +import useStyle from 'renderer/hooks/useStyle'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ScriptureTextPanelHOCProps + extends ScriptureReference, + ResourceInfo, + PropsWithChildren { + scrChapters: ScriptureChapter[]; +} + +export function ScriptureTextPanelHOC( + WrappedComponent: ComponentType, + getScrChapter: ( + shortName: string, + bookNum: number, + chapter?: number, + ) => Promise, +) { + return function ScriptureTextPanel(props: T) { + const { shortName, book, chapter, children } = props; + + // Pull in the project's stylesheet + useStyle( + useCallback(async () => { + // TODO: Fix RTL scripture style sheets + if (!shortName) return undefined; + const style = await getScriptureStyle(shortName); + return shortName !== 'OHEB' && shortName !== 'zzz1' + ? style + : undefined; + }, [shortName]), + ); + + // Get the project's contents + const [scrChapters] = usePromise( + useCallback(async () => { + if (!shortName || !isValidValue(book) || !isValidValue(chapter)) + return null; + return getScrChapter(shortName, book, chapter); + }, [shortName, book, chapter]), + useState([ + { + chapter: -1, + contents: `Loading ${shortName} ${getTextFromScrRef({ + book, + chapter, + verse: -1, + })}...`, + }, + ])[0], + ); + + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + {children} + + ); + }; +} From ea06cf2332fdc2c8c627863bce61fd1004dae046 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Tue, 27 Sep 2022 17:30:00 -0500 Subject: [PATCH 07/13] Renamed ScriptureTextPanel to ScriptureTextPanelHtml --- .../src/renderer/components/layout/Layout.tsx | 24 +++++++++---------- .../components/panels/PanelManager.ts | 4 ++-- .../src/renderer/components/panels/Panels.ts | 4 ++-- ...xtPanel.tsx => ScriptureTextPanelHtml.tsx} | 17 ++++--------- .../TextPanels/ScriptureTextPanelSlate.tsx | 14 +++-------- 5 files changed, 23 insertions(+), 40 deletions(-) rename react-electron-poc/src/renderer/components/panels/TextPanels/{ScriptureTextPanel.tsx => ScriptureTextPanelHtml.tsx} (85%) diff --git a/react-electron-poc/src/renderer/components/layout/Layout.tsx b/react-electron-poc/src/renderer/components/layout/Layout.tsx index 6639faf..6ad09c7 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.tsx +++ b/react-electron-poc/src/renderer/components/layout/Layout.tsx @@ -7,7 +7,7 @@ import { getAllResourceInfo, getResourceInfo, } from '@services/ScriptureService'; -import { ScriptureTextPanelProps } from '@components/panels/TextPanels/ScriptureTextPanel'; +import { ScriptureTextPanelHtmlProps } from '@components/panels/TextPanels/ScriptureTextPanelHtml'; import { ScriptureReference } from '@shared/data/ScriptureTypes'; import ScrRefSelector from '@components/ScrRefSelector'; import { PanelManager } from '@components/panels/PanelManager'; @@ -69,20 +69,20 @@ const Layout = () => { }, ); */ const csbPanel = panelManager.current.addPanel( - 'ScriptureTextPanel', + 'ScriptureTextPanelHtml', { shortName: 'CSB', editable: false, ...scrRef, - } as ScriptureTextPanelProps, + } as ScriptureTextPanelHtmlProps, ); const ohebPanel = panelManager.current.addPanel( - 'ScriptureTextPanel', + 'ScriptureTextPanelHtml', { shortName: 'OHEB', editable: false, ...scrRef, - } as ScriptureTextPanelProps, + } as ScriptureTextPanelHtmlProps, { position: { direction: 'right', @@ -96,7 +96,7 @@ const Layout = () => { shortName: 'zzz6', editable: true, ...scrRef, - } as ScriptureTextPanelProps, + } as ScriptureTextPanelHtmlProps, { position: { direction: 'below', @@ -105,12 +105,12 @@ const Layout = () => { }, ); panelManager.current.addPanel( - 'ScriptureTextPanel', + 'ScriptureTextPanelHtml', { shortName: 'NIV84', editable: false, ...scrRef, - } as ScriptureTextPanelProps, + } as ScriptureTextPanelHtmlProps, { position: { direction: 'below', @@ -119,12 +119,12 @@ const Layout = () => { }, ); const zzz1Panel = panelManager.current.addPanel( - 'ScriptureTextPanel', + 'ScriptureTextPanelHtml', { shortName: 'zzz1', editable: true, ...scrRef, - } as ScriptureTextPanelProps, + } as ScriptureTextPanelHtmlProps, { position: { direction: 'below', @@ -133,12 +133,12 @@ const Layout = () => { }, ); panelManager.current.addPanel( - 'ScriptureTextPanel', + 'ScriptureTextPanelHtml', { shortName: 'zzz6', editable: true, ...scrRef, - } as ScriptureTextPanelProps, + } as ScriptureTextPanelHtmlProps, { position: { direction: 'within', diff --git a/react-electron-poc/src/renderer/components/panels/PanelManager.ts b/react-electron-poc/src/renderer/components/panels/PanelManager.ts index d03c2f0..ebe3607 100644 --- a/react-electron-poc/src/renderer/components/panels/PanelManager.ts +++ b/react-electron-poc/src/renderer/components/panels/PanelManager.ts @@ -3,7 +3,7 @@ import { getTextFromScrRef } from '@util/ScriptureUtil'; import { newGuid } from '@util/Util'; import { DockviewReadyEvent, AddPanelOptions } from 'dockview'; import { PanelType } from './Panels'; -import { ScriptureTextPanelProps } from './TextPanels/ScriptureTextPanel'; +import { ScriptureTextPanelHOCProps } from './TextPanels/ScriptureTextPanelHOC'; export interface PanelInfo { id: string; @@ -31,7 +31,7 @@ export class PanelManager { if (panelInfo.title || panelInfo.title === '') return panelInfo.title; if (panelInfo.type.startsWith('ScriptureTextPanel')) { - const scrPanelProps = panelProps as ScriptureTextPanelProps; + const scrPanelProps = panelProps as ScriptureTextPanelHOCProps; return `${scrPanelProps.shortName}: ${getTextFromScrRef({ book: scrPanelProps.book, chapter: scrPanelProps.chapter, diff --git a/react-electron-poc/src/renderer/components/panels/Panels.ts b/react-electron-poc/src/renderer/components/panels/Panels.ts index e1db2b0..20484d4 100644 --- a/react-electron-poc/src/renderer/components/panels/Panels.ts +++ b/react-electron-poc/src/renderer/components/panels/Panels.ts @@ -4,7 +4,7 @@ import { Erb } from './Erb/Erb'; import { TextPanel } from './TextPanels/TextPanel'; import { HtmlTextPanel } from './TextPanels/HtmlTextPanel'; import { EditableHtmlTextPanel } from './TextPanels/EditableHtmlTextPanel'; -import { ScriptureTextPanel } from './TextPanels/ScriptureTextPanel'; +import { ScriptureTextPanelHtml } from './TextPanels/ScriptureTextPanelHtml'; import { ScriptureTextPanelSlate } from './TextPanels/ScriptureTextPanelSlate'; // TODO: Consider doing something a bit different with this https://stackoverflow.com/questions/29722270/is-it-possible-to-import-modules-from-all-files-in-a-directory-using-a-wildcard @@ -14,7 +14,7 @@ export const Panels = { TextPanel, HtmlTextPanel, EditableHtmlTextPanel, - ScriptureTextPanel, + ScriptureTextPanelHtml, ScriptureTextPanelSlate, }; export default Panels; diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx similarity index 85% rename from react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx rename to react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx index 517d018..f98f1fd 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx @@ -1,33 +1,24 @@ +import { getScriptureHtml } from '@services/ScriptureService'; import { - getScriptureStyle, - getScriptureHtml, -} from '@services/ScriptureService'; -import { - ResourceInfo, ScriptureChapter, ScriptureChapterString, - ScriptureReference, } from '@shared/data/ScriptureTypes'; -import { getTextFromScrRef } from '@util/ScriptureUtil'; -import { isValidValue } from '@util/Util'; import { useCallback, useEffect, useRef, useState } from 'react'; import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; -import usePromise from 'renderer/hooks/usePromise'; -import useStyle from 'renderer/hooks/useStyle'; import { ScriptureTextPanelHOC, ScriptureTextPanelHOCProps, } from './ScriptureTextPanelHOC'; import './TextPanel.css'; -export interface ScriptureTextPanelProps extends ScriptureTextPanelHOCProps { +export interface ScriptureTextPanelHtmlProps extends ScriptureTextPanelHOCProps { scrChapters: ScriptureChapterString[]; } /** The function to use to get the Scripture chapter content to display */ const getScrChapter = getScriptureHtml; -export const ScriptureTextPanel = ScriptureTextPanelHOC( +export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( ({ shortName, editable, @@ -35,7 +26,7 @@ export const ScriptureTextPanel = ScriptureTextPanelHOC( chapter, verse, scrChapters, - }: ScriptureTextPanelProps) => { + }: ScriptureTextPanelHtmlProps) => { // Make a ref for the Scripture that works with react-content-editable const editableScrChapters = useRef([ { diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index f5797a3..3a537c4 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -1,12 +1,6 @@ -import { getScriptureStyle, getScripture } from '@services/ScriptureService'; -import { - ResourceInfo, - ScriptureChapter, - ScriptureChapterContent, - ScriptureReference, -} from '@shared/data/ScriptureTypes'; -import { getTextFromScrRef } from '@util/ScriptureUtil'; -import { isString, isValidValue } from '@util/Util'; +import { getScripture } from '@services/ScriptureService'; +import { ScriptureChapterContent } from '@shared/data/ScriptureTypes'; +import { isString } from '@util/Util'; import { createElement, FunctionComponent, @@ -15,8 +9,6 @@ import { useEffect, useState, } from 'react'; -import usePromise from 'renderer/hooks/usePromise'; -import useStyle from 'renderer/hooks/useStyle'; import './TextPanel.css'; import { createEditor, BaseEditor, NodeEntry, Node, Transforms } from 'slate'; import { From 4c46f0b0e2fba965c967e4fb16b5634967712573 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Wed, 28 Sep 2022 09:00:40 -0500 Subject: [PATCH 08/13] Fixed Slate error from contentEditable nonbreaking space --- .../components/panels/TextPanels/ScriptureTextPanelSlate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index 3a537c4..5a2cd0e 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -140,7 +140,7 @@ const InlineElement = ({ {children} - {endSpace ? ' ' : undefined} + {endSpace ?   : undefined} {closingMarker ? ( ) : undefined} From 7a82327a1660590fb144048a4d5f971cfad5ee19 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Wed, 28 Sep 2022 13:36:10 -0500 Subject: [PATCH 09/13] Can now delete inline markers, tweaked getting full book to test with --- react-electron-poc/src/main/main.ts | 57 +++++++++++++ .../TextPanels/ScriptureTextPanelHOC.tsx | 2 +- .../TextPanels/ScriptureTextPanelHtml.tsx | 4 +- .../TextPanels/ScriptureTextPanelSlate.tsx | 80 ++++++++++++++++++- .../src/renderer/services/ScriptureService.ts | 30 +++---- 5 files changed, 152 insertions(+), 21 deletions(-) diff --git a/react-electron-poc/src/main/main.ts b/react-electron-poc/src/main/main.ts index d6bd7cb..349a91f 100644 --- a/react-electron-poc/src/main/main.ts +++ b/react-electron-poc/src/main/main.ts @@ -169,6 +169,9 @@ const getScriptureDelay = 75; /** Simulating how long it may take Paratext to serve the resource info */ const getResourceInfoDelay = 20; +/** Regex for test Scripture file name bookNum-chapterNum.fileExtension */ +const regexpScrFileName = /(\d+)-(\d+)\.(.+)/; + /** * Get the Scripture for a certain project for a whole book. * The json files are Slate JSON files @@ -189,6 +192,60 @@ async function handleGetScriptureBook( bookNum: number, ): Promise { // TODO: If we want to implement this, parse file and split out into actual chapters + return delayPromise((resolve, reject) => { + fs.readdir( + getAssetPath(`testScripture/${shortName}`), + { + withFileTypes: true, + }, + async (err, dirents) => { + if (err) reject(`No path data for ${shortName} ${bookNum}`); + else { + try { + // Get all Scripture files + const scrFilePaths = dirents + .filter((dirent) => { + if (dirent.isDirectory()) return false; + + const scrFileNameMatch = + dirent.name.match(regexpScrFileName); + return ( + scrFileNameMatch && + scrFileNameMatch.length >= 4 && + scrFileNameMatch[3] === fileExtension + ); + }) + .map( + (dirent) => + `testScripture/${shortName}/${dirent.name}`, + ); + const filesContents = await getFilesText( + scrFilePaths, + 0, + ); + resolve( + filesContents.map( + (fileContents, i) => + ({ + chapter: parseInt( + scrFilePaths[i].match( + regexpScrFileName, + )[2], + 10, + ), + contents: fileContents, + } as ScriptureChapter), + ), + ); + } catch (e) { + console.log(e); + reject(`No data for ${shortName} ${bookNum}`); + } + } + }, + ); + }, getScriptureDelay); + try { return await getFilesText( [`testScripture/${shortName}/${bookNum}.${fileExtension}`], diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx index d8bb9e0..61933c6 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx @@ -46,7 +46,7 @@ export function ScriptureTextPanelHOC( useCallback(async () => { if (!shortName || !isValidValue(book) || !isValidValue(chapter)) return null; - return getScrChapter(shortName, book, chapter); + return getScrChapter(shortName, book, -1); }, [shortName, book, chapter]), useState([ { diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx index f98f1fd..7a71dff 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx @@ -11,7 +11,8 @@ import { } from './ScriptureTextPanelHOC'; import './TextPanel.css'; -export interface ScriptureTextPanelHtmlProps extends ScriptureTextPanelHOCProps { +export interface ScriptureTextPanelHtmlProps + extends ScriptureTextPanelHOCProps { scrChapters: ScriptureChapterString[]; } @@ -66,7 +67,6 @@ export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( {editable ? editableScrChapters.current.map((scrChapter) => ( handleChange(e, scrChapter.chapter) diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index 5a2cd0e..1b66439 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -10,7 +10,18 @@ import { useState, } from 'react'; import './TextPanel.css'; -import { createEditor, BaseEditor, NodeEntry, Node, Transforms } from 'slate'; +import { + createEditor, + BaseEditor, + NodeEntry, + Node, + Transforms, + Element, + Text, + Editor, + Point, + Range, +} from 'slate'; import { Slate, Editable, @@ -203,15 +214,78 @@ const withScrInlines = (editor: CustomEditor): CustomEditor => { }; const withScrMarkers = (editor: CustomEditor): CustomEditor => { - const { normalizeNode } = editor; + const { normalizeNode, deleteBackward } = editor; editor.normalizeNode = (entry: NodeEntry): void => { const [node, path] = entry; - // Make sure the marker-based elements all have markers + // TODO: Figure out how to make sure there is a spot to navigate between markers like \q \v + /* if (Element.isElement(node)) { + const firstChild = Node.child(node, 0); + if (firstChild && Element.isElement(firstChild)) { + Transforms.insertText(editor, '', { at: path.concat(0) }); + } + } */ + normalizeNode(entry); }; + editor.deleteBackward = (...args) => { + const { selection } = editor; + + // Delete in-line markers + if (selection && Range.isCollapsed(selection)) { + // Get the selection if it is an inline element + const [match] = Editor.nodes(editor, { + match: (n) => + !Editor.isEditor(n) && + Element.isElement(n) && + editor.isInline(n), + }); + + if (match) { + const [, path] = match; + const start = Editor.start(editor, path); + + // If the cursor is at the start of the inline element, remove the element + if (Point.equals(selection.anchor, start)) { + Transforms.unwrapNodes(editor, { at: path }); + return; + } + } + } + + deleteBackward(...args); + }; + + editor.deleteForward = (...args) => { + const { selection } = editor; + + // Delete in-line markers + if (selection && Range.isCollapsed(selection)) { + // Get the selection if it is an inline element + const [match] = Editor.nodes(editor, { + match: (n) => + !Editor.isEditor(n) && + Element.isElement(n) && + editor.isInline(n), + }); + + if (match) { + const [, path] = match; + const end = Editor.end(editor, path); + + // If the cursor is at the end of the inline element, remove the element + if (Point.equals(selection.anchor, end)) { + Transforms.unwrapNodes(editor, { at: path }); + return; + } + } + } + + deleteBackward(...args); + }; + return editor; }; diff --git a/react-electron-poc/src/renderer/services/ScriptureService.ts b/react-electron-poc/src/renderer/services/ScriptureService.ts index b6618ef..529cf23 100644 --- a/react-electron-poc/src/renderer/services/ScriptureService.ts +++ b/react-electron-poc/src/renderer/services/ScriptureService.ts @@ -18,21 +18,21 @@ export const getScripture = async ( chapter = -1, ): Promise => { try { - return chapter >= 0 - ? await window.electronAPI.scripture - .getScriptureChapter(shortName, bookNum, chapter) - .then((result) => [ - { - ...result, - contents: JSON.parse( - result.contents as unknown as string, // Parsing from string, but it's nice to know getScripture intends to send json of known type - ), - }, - ]) - : await window.electronAPI.scripture.getScriptureBook( - shortName, - bookNum, - ); + const scrChapterContents = + chapter >= 0 + ? await window.electronAPI.scripture + .getScriptureChapter(shortName, bookNum, chapter) + .then((result) => [result]) + : await window.electronAPI.scripture.getScriptureBook( + shortName, + bookNum, + ); + return scrChapterContents.map((scrChapterContent) => ({ + ...scrChapterContent, + contents: JSON.parse( + scrChapterContent.contents as unknown as string, // Parsing from string, but it's nice to know getScripture intends to send json of known type + ), + })); } catch (e) { console.log(e); return [ From b5234cb9ed9221845c995d6470e5c1e32fc602dd Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 3 Oct 2022 13:47:20 -0500 Subject: [PATCH 10/13] Added browse by book option, update scripture reference and scrolling on other windows when changing selected verse, optimizations --- react-electron-poc/.eslintrc.js | 1 + react-electron-poc/src/main/main.ts | 15 -- .../renderer/components/ScrRefSelector.tsx | 2 +- .../src/renderer/components/layout/Layout.css | 18 ++ .../src/renderer/components/layout/Layout.tsx | 30 ++- .../components/panels/PanelManager.ts | 18 ++ .../TextPanels/ScriptureTextPanelHOC.tsx | 32 ++- .../TextPanels/ScriptureTextPanelHtml.tsx | 179 ++++++++++++++- .../TextPanels/ScriptureTextPanelSlate.tsx | 205 ++++++++++++++++-- .../src/renderer/util/ScriptureUtil.ts | 14 +- react-electron-poc/src/renderer/util/Util.ts | 2 +- 11 files changed, 464 insertions(+), 52 deletions(-) diff --git a/react-electron-poc/.eslintrc.js b/react-electron-poc/.eslintrc.js index 2eed088..50b023f 100644 --- a/react-electron-poc/.eslintrc.js +++ b/react-electron-poc/.eslintrc.js @@ -14,6 +14,7 @@ module.exports = { 'prettier/prettier': ['warn', { tabWidth: 4, trailingComma: 'all' }], 'no-console': 'off', 'react/require-default-props': 'off', + 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], }, parserOptions: { ecmaVersion: 2020, diff --git a/react-electron-poc/src/main/main.ts b/react-electron-poc/src/main/main.ts index 349a91f..f859451 100644 --- a/react-electron-poc/src/main/main.ts +++ b/react-electron-poc/src/main/main.ts @@ -245,21 +245,6 @@ async function handleGetScriptureBook( }, ); }, getScriptureDelay); - - try { - return await getFilesText( - [`testScripture/${shortName}/${bookNum}.${fileExtension}`], - getScriptureDelay, - ).then((filesContents) => - filesContents.map((fileContents, ind) => ({ - chapter: ind, - contents: fileContents, - })), - ); - } catch (e) { - console.log(e); - throw new Error(`No data for ${shortName} ${bookNum}`); - } } /** diff --git a/react-electron-poc/src/renderer/components/ScrRefSelector.tsx b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx index dd39949..cc6d581 100644 --- a/react-electron-poc/src/renderer/components/ScrRefSelector.tsx +++ b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx @@ -24,7 +24,7 @@ export default ({ scrRef, handleSubmit }: ScrRefSelectorProps) => { const handleChange = useCallback( (e: React.ChangeEvent) => { - setCurrentRefText(e.currentTarget.value); + setCurrentRefText(e.target.value); }, [], ); diff --git a/react-electron-poc/src/renderer/components/layout/Layout.css b/react-electron-poc/src/renderer/components/layout/Layout.css index f52eb03..7b836a7 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.css +++ b/react-electron-poc/src/renderer/components/layout/Layout.css @@ -10,6 +10,24 @@ body { height: 100%; display: flex; flex-direction: column; + background-color: #1c1c2a; +} + +.layout .layout-bar { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.layout .layout-bar .layout-checkbox { + background-color: #1c1c2a; + color: #505070; + border-radius: 5px; + border: 1px solid #505070; + padding: 3px; + margin: 4px; + flex-shrink: 0; } .layout .layout-dock { diff --git a/react-electron-poc/src/renderer/components/layout/Layout.tsx b/react-electron-poc/src/renderer/components/layout/Layout.tsx index 6ad09c7..a7f0ff9 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.tsx +++ b/react-electron-poc/src/renderer/components/layout/Layout.tsx @@ -20,13 +20,19 @@ const Layout = () => { chapter: 119, verse: 1, }); - const updateScrRef = useCallback((newScrRef: ScriptureReference) => { setScrRef(newScrRef); panelManager.current?.updateScrRef(newScrRef); }, []); + const [browseBook, setBrowseBook] = useState(false); + const updateBrowseBook = useCallback((newBrowseBook: boolean) => { + setBrowseBook(newBrowseBook); + + panelManager.current?.updateBrowseBook(newBrowseBook); + }, []); + const onReady = useCallback( (event: DockviewReadyEvent) => { // Test resource info api @@ -74,6 +80,7 @@ const Layout = () => { shortName: 'CSB', editable: false, ...scrRef, + updateScrRef, } as ScriptureTextPanelHtmlProps, ); const ohebPanel = panelManager.current.addPanel( @@ -82,6 +89,7 @@ const Layout = () => { shortName: 'OHEB', editable: false, ...scrRef, + updateScrRef, } as ScriptureTextPanelHtmlProps, { position: { @@ -96,6 +104,7 @@ const Layout = () => { shortName: 'zzz6', editable: true, ...scrRef, + updateScrRef, } as ScriptureTextPanelHtmlProps, { position: { @@ -110,6 +119,7 @@ const Layout = () => { shortName: 'NIV84', editable: false, ...scrRef, + updateScrRef, } as ScriptureTextPanelHtmlProps, { position: { @@ -124,6 +134,7 @@ const Layout = () => { shortName: 'zzz1', editable: true, ...scrRef, + updateScrRef, } as ScriptureTextPanelHtmlProps, { position: { @@ -138,6 +149,7 @@ const Layout = () => { shortName: 'zzz6', editable: true, ...scrRef, + updateScrRef, } as ScriptureTextPanelHtmlProps, { position: { @@ -147,12 +159,24 @@ const Layout = () => { }, ); }, - [scrRef], + [scrRef, updateScrRef], ); return (
- +
+ + + Edit whole book + + updateBrowseBook(event.target.checked) + } + /> + +
{ + const panelPropsUpdated = { + ...panel.params, + browseBook: newBrowseBook, + }; + panel.update({ + params: { + params: panelPropsUpdated, + title: PanelManager.generatePanelTitle( + this.panelsInfo.get(panel.id), + panelPropsUpdated, + ), + }, + }); + }); + } } diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx index 61933c6..22e4e1d 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx @@ -6,7 +6,13 @@ import { } from '@shared/data/ScriptureTypes'; import { getTextFromScrRef } from '@util/ScriptureUtil'; import { isValidValue } from '@util/Util'; -import { ComponentType, PropsWithChildren, useCallback, useState } from 'react'; +import { + ComponentType, + memo, + PropsWithChildren, + useCallback, + useState, +} from 'react'; import usePromise from 'renderer/hooks/usePromise'; import useStyle from 'renderer/hooks/useStyle'; @@ -16,6 +22,8 @@ export interface ScriptureTextPanelHOCProps ResourceInfo, PropsWithChildren { scrChapters: ScriptureChapter[]; + updateScrRef: (newScrRef: ScriptureReference) => void; + browseBook?: boolean; } export function ScriptureTextPanelHOC( @@ -26,8 +34,8 @@ export function ScriptureTextPanelHOC( chapter?: number, ) => Promise, ) { - return function ScriptureTextPanel(props: T) { - const { shortName, book, chapter, children } = props; + return memo(function ScriptureTextPanel(props: T) { + const { shortName, book, chapter, children, browseBook } = props; // Pull in the project's stylesheet useStyle( @@ -41,13 +49,19 @@ export function ScriptureTextPanelHOC( }, [shortName]), ); + const getMyScrBook = useCallback(async () => { + if (!shortName || !isValidValue(book)) return null; + return getScrChapter(shortName, book, -1); + }, [shortName, book]); + const getMyScrChapter = useCallback(async () => { + if (!shortName || !isValidValue(book) || !isValidValue(chapter)) + return null; + return getScrChapter(shortName, book, chapter); + }, [shortName, book, chapter]); + // Get the project's contents const [scrChapters] = usePromise( - useCallback(async () => { - if (!shortName || !isValidValue(book) || !isValidValue(chapter)) - return null; - return getScrChapter(shortName, book, -1); - }, [shortName, book, chapter]), + browseBook ? getMyScrBook : getMyScrChapter, useState([ { chapter: -1, @@ -66,5 +80,5 @@ export function ScriptureTextPanelHOC( {children} ); - }; + }); } diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx index 7a71dff..81c4287 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx @@ -3,7 +3,9 @@ import { ScriptureChapter, ScriptureChapterString, } from '@shared/data/ScriptureTypes'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { parseChapter, parseVerse } from '@util/ScriptureUtil'; +import { isValidValue } from '@util/Util'; +import { EventHandler, useCallback, useEffect, useRef, useState } from 'react'; import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; import { ScriptureTextPanelHOC, @@ -11,6 +13,9 @@ import { } from './ScriptureTextPanelHOC'; import './TextPanel.css'; +/** Regex for parsing cvCHAPTER_VERSE ids in the html supplied by Paratext */ +const regexpChapterVerseId = /cv(\d+)_(\d+)/; + export interface ScriptureTextPanelHtmlProps extends ScriptureTextPanelHOCProps { scrChapters: ScriptureChapterString[]; @@ -27,7 +32,11 @@ export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( chapter, verse, scrChapters, + updateScrRef, }: ScriptureTextPanelHtmlProps) => { + /** Ref for the top-level editor div */ + const editorRef = useRef(null); + // Make a ref for the Scripture that works with react-content-editable const editableScrChapters = useRef([ { @@ -62,8 +71,171 @@ export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( }; }; + /** + * Whether or not the upcoming scrRef update is form this text panel. + * TODO: Not a great way to determine this - should be improved in the future + * */ + const didIUpdateScrRef = useRef(false); + + const tryUpdateScrRef = () => { + const windowSel = window.getSelection(); + if (windowSel && windowSel.rangeCount > 0) { + // Set reference to the current verse + // We must be in a chapter + let selectedChapter = -1; + // Intro material should be shown as verse 0, so allow 0 + let selectedVerse = 0; + + // Get the selected node + let node = windowSel.getRangeAt(0).startContainer; + + // Step up the node tree via previous siblings then parents all the way up until we find a chapter and verse + while (node && node !== editorRef.current) { + if (node instanceof Element) { + if (selectedVerse <= 0 && node.className === 'usfm_v') { + // It's a verse, so try to parse its text and use that as the verse + const textParts = node.textContent?.split(' '); + if (textParts) { + const verseText = + textParts[textParts.length - 1]; + const verseNum = parseVerse(verseText); + if (isValidValue(verseNum)) { + selectedVerse = verseNum; + } + } + } else if ( + selectedChapter < 0 && + node.className === 'usfm_c' + ) { + // It's a chapter, so try to parse its text and use that as the chapter + const textParts = node.textContent?.split(' '); + if (textParts) { + const chapterText = + textParts[textParts.length - 1]; + const chapterNum = parseChapter(chapterText); + if (isValidValue(chapterNum)) { + selectedChapter = chapterNum; + } + } + } + } + + if (selectedChapter >= 0) { + // We got our results! Done + break; + } else if (node.previousSibling) { + // This node has a previous sibling. Get the lowest node of the previous sibling and try again + node = node.previousSibling; + while (node.hasChildNodes()) { + node = node.lastChild as ChildNode; + } + } else { + // This is the first node of its siblings, so get the parent and try again + node = node.parentNode as ParentNode; + } + } + + // If we found verse info, set the selection + if (selectedChapter > 0) { + updateScrRef({ + book, + chapter: selectedChapter, + verse: selectedVerse, + }); + didIUpdateScrRef.current = true; + } + } + }; + + const onKeyDown = (event: React.KeyboardEvent) => { + if (!event.altKey) + switch (event.key) { + case 'ArrowDown': + case 'ArrowUp': + case 'ArrowLeft': + case 'ArrowRight': + // TODO: For some reason, the onKeyDown callback doesn't always have the most up-to-date editor.selection. + // As such, I added a setTimeout, which is gross. Please fix this hacky setTimeout + setTimeout(() => { + tryUpdateScrRef(); + }, 1); + break; + default: + break; + } + }; + + // When the scrRef changes, scroll to view + useEffect(() => { + // TODO: Determine if this window should scroll by computing if the verse element is visible instead of using hacky didIUpdateScrRef + if (!didIUpdateScrRef.current && editorRef.current) { + // Get the node for the specified chapter editor + let editorElement: Element | undefined; + let chapterElement: Element | undefined; + // Get all the usfm elements (full chapters) + const usfmElements = + editorRef.current.getElementsByClassName('usfm'); + for (let i = 0; i < usfmElements.length; i++) { + // If we already found our chapter, we're done + if (editorElement) break; + + // Get the chapter element within the current usfm element + const chapterElements = + usfmElements[i].getElementsByClassName('usfm_c'); + for (let j = 0; j < chapterElements.length; j++) { + // Check if this usfm element is the right one for this chapter + const idMatch = + chapterElements[j].id.match(regexpChapterVerseId); + if ( + idMatch && + idMatch.length >= 2 && + parseChapter(idMatch[1]) === chapter + ) { + editorElement = usfmElements[i]; + chapterElement = chapterElements[j]; + break; + } + } + } + + if (editorElement) { + // Get the element for the specified verse + let verseElement: Element | undefined; + + // If the verse we're trying to scroll to is 0, scroll to the chapter + if (verse <= 0) { + verseElement = chapterElement; + } else { + // Get all the verse elements for this chapter + const verseElements = + editorElement.getElementsByClassName('usfm_v'); + for (let i = 0; i < verseElements.length; i++) { + // Check if this verse element is the right one for this verse + const idMatch = + verseElements[i].id.match(regexpChapterVerseId); + if ( + idMatch && + idMatch.length >= 3 && + parseVerse(idMatch[2]) === verse + ) { + verseElement = verseElements[i]; + break; + } + } + } + + if (verseElement) + verseElement.scrollIntoView({ + block: 'center', + behavior: 'smooth', + }); + } + } + didIUpdateScrRef.current = false; + }, [book, chapter, verse]); + return ( -
+
{editable ? editableScrChapters.current.map((scrChapter) => ( handleChange(e, scrChapter.chapter) } + // Couldn't use onSelect here because of what is likely a bug in ContentEditable - onSelect fired multiple times, sometimes going to the end of the div before restoring to the correct location + onClick={tryUpdateScrRef} + onKeyDown={onKeyDown} /> )) : scrChapters.map((scrChapter) => ( diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index 1b66439..4971b82 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -1,12 +1,13 @@ import { getScripture } from '@services/ScriptureService'; import { ScriptureChapterContent } from '@shared/data/ScriptureTypes'; -import { isString } from '@util/Util'; +import { isString, isValidValue } from '@util/Util'; import { createElement, FunctionComponent, PropsWithChildren, useCallback, useEffect, + useRef, useState, } from 'react'; import './TextPanel.css'; @@ -21,6 +22,7 @@ import { Editor, Point, Range, + Path, } from 'slate'; import { Slate, @@ -33,6 +35,11 @@ import { ScriptureTextPanelHOC, ScriptureTextPanelHOCProps, } from './ScriptureTextPanelHOC'; +import { + getTextFromScrRef, + parseChapter, + parseVerse, +} from '@util/ScriptureUtil'; // Slate types type CustomEditor = BaseEditor & ReactEditor; @@ -40,6 +47,8 @@ type CustomEditor = BaseEditor & ReactEditor; // Types of components: // Element - contiguous, semantic elements in the document // - Ex: Marker Element (in-line) that contains a line and is formatted appropriately +// - Note: You cannot have block elements as siblings of inline elements or text. +// - Note: You cannot have children of inline elements other than text, it seems. // Text - non-contiguous, character-level formatting // Decoration - computed at render-time based on the content itself // - helpful for dynamic formatting like syntax highlighting or search keywords, where changes to the content (or some external data) has the potential to change the formatting @@ -188,18 +197,22 @@ const EditorElement = ({
); +interface ElementInfo { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component: (props: MyRenderElementProps) => JSX.Element; + inline?: boolean; + validStyles?: string[]; +} + /** All available elements for use in slate editor */ -const EditorElements = { - verse: VerseElement, - para: ParaElement, - char: CharElement, - chapter: ChapterElement, - editor: EditorElement, +const EditorElements: { [type: string]: ElementInfo } = { + verse: { component: VerseElement, inline: true, validStyles: ['v'] }, + para: { component: ParaElement, validStyles: ['p', 'q', 'q2'] }, + char: { component: CharElement, inline: true, validStyles: ['nd'] }, + chapter: { component: ChapterElement, validStyles: ['c'] }, + editor: { component: EditorElement }, }; -/** List of all inline elements */ -const InlineElements = ['verse', 'char']; - const DefaultElement = ({ attributes, children }: RenderElementProps) => { return

{children}

; }; @@ -208,16 +221,16 @@ const withScrInlines = (editor: CustomEditor): CustomEditor => { const { isInline } = editor; editor.isInline = (element: CustomElement): boolean => - InlineElements.includes(element.type) || isInline(element); + EditorElements[element.type]?.inline || isInline(element); return editor; }; const withScrMarkers = (editor: CustomEditor): CustomEditor => { - const { normalizeNode, deleteBackward } = editor; + const { normalizeNode, deleteBackward, deleteForward, onChange } = editor; editor.normalizeNode = (entry: NodeEntry): void => { - const [node, path] = entry; + // const [node, path] = entry; // TODO: Figure out how to make sure there is a spot to navigate between markers like \q \v /* if (Element.isElement(node)) { @@ -235,7 +248,7 @@ const withScrMarkers = (editor: CustomEditor): CustomEditor => { // Delete in-line markers if (selection && Range.isCollapsed(selection)) { - // Get the selection if it is an inline element + // Get the inline element in the path of the selection const [match] = Editor.nodes(editor, { match: (n) => !Editor.isEditor(n) && @@ -263,7 +276,7 @@ const withScrMarkers = (editor: CustomEditor): CustomEditor => { // Delete in-line markers if (selection && Range.isCollapsed(selection)) { - // Get the selection if it is an inline element + // Get the inline element in the path of the selection const [match] = Editor.nodes(editor, { match: (n) => !Editor.isEditor(n) && @@ -283,7 +296,7 @@ const withScrMarkers = (editor: CustomEditor): CustomEditor => { } } - deleteBackward(...args); + deleteForward(...args); }; return editor; @@ -304,8 +317,10 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( chapter, verse, scrChapters, + updateScrRef, }: ScriptureTextPanelProps) => { // Slate editor + // TODO: Put in a useEffect listening for scrChapters and create editors for the number of chapters const [editor] = useState(() => withScrMarkers(withScrInlines(withReact(createEditor()))), ); @@ -314,7 +329,7 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( const renderElement = useCallback( (props: MyRenderElementProps): JSX.Element => { return createElement( - (EditorElements[props.element.type] || + (EditorElements[props.element.type].component || DefaultElement) as FunctionComponent, // eslint-disable-next-line @typescript-eslint/no-explicit-any props as any, @@ -323,10 +338,88 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( [], ); + /** + * Whether or not the upcoming scrRef update is form this text panel. + * TODO: Not a great way to determine this - should be improved in the future + * */ + const didIUpdateScrRef = useRef(false); + + const onSelect = useCallback(() => { + // TODO: For some reason, the onSelect callback doesn't always have the most up-to-date editor.selection. + // As such, I added a setTimeout, which is gross. Please fix this hacky setTimeout + // One possible solution would be to listen for mouse clicks and arrow key events and see if editor.selection is updated by then. + // Or use onChange and keep track of selection vs previous selection if selection is updated. + // Or try useSlateSelection hook again + setTimeout(() => { + if (editor.selection) { + // Set reference to the current verse + // We must be in a chapter + let selectedChapter = -1; + // Intro material should show as verse 0, so allow 0 + let selectedVerse = 0; + + // Get the selected node + let nodeEntry: NodeEntry | undefined = Editor.node( + editor, + editor.selection.anchor, + ); + + // Step up the node tree via previous siblings then parents all the way up until we find a chapter and verse + while (nodeEntry && !Editor.isEditor(nodeEntry[0])) { + const [node, path] = nodeEntry as NodeEntry; + if (Element.isElement(node)) { + if (selectedVerse <= 0 && node.type === 'verse') { + // It's a verse, so try to parse its text and use that as the verse + const verseText = Node.string(node); + const verseNum = parseVerse(verseText); + if (isValidValue(verseNum)) { + selectedVerse = verseNum; + } + } else if ( + selectedChapter < 0 && + node.type === 'chapter' + ) { + // It's a chapter, so try to parse its text and use that as the chapter + const chapterText = Node.string(node); + const chapterNum = parseChapter(chapterText); + if (isValidValue(chapterNum)) { + selectedChapter = chapterNum; + } + } + } + + if (selectedChapter >= 0) { + // We got our results! Done + break; + } else if (Path.hasPrevious(path)) { + // This node has a previous sibling. Get the lowest node of the previous sibling and try again + nodeEntry = Editor.last( + editor, + Path.previous(path), + ); + } else { + // This is the first node of its siblings, so get the parent and try again + nodeEntry = Editor.parent(editor, path); + } + } + + // If we found verse info, set the selection + if (selectedChapter > 0) { + updateScrRef({ + book, + chapter: selectedChapter, + verse: selectedVerse, + }); + didIUpdateScrRef.current = true; + } + } + }, 1); + }, [editor, updateScrRef, book]); + // When we get new Scripture project contents, update slate useEffect(() => { if (scrChapters && scrChapters.length > 0) { - // TODO: Save the verse...? Maybe if book/chapter was not changed? + // TODO: Save the selection // Unselect Transforms.deselect(editor); @@ -348,11 +441,82 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( } as EditorElementProps), ); - // TODO: Update cursor to new ScrRef + // TODO: May need to call Editor.normalize, potentially with option { force: true } + // Editor.normalize(editor); + + // TODO: Restore cursor to new ScrRef editor.onChange(); } - }, [scrChapters, editor]); + }, [editor, scrChapters]); + + // When the scrRef changes, scroll to view + useEffect(() => { + // TODO: Determine if this window should scroll by computing if the verse element is visible instead of using hacky didIUpdateScrRef + if (!didIUpdateScrRef.current) { + // Get the node for the specified chapter editor + const [editorNodeEntry] = Editor.nodes(editor, { + at: [], + match: (n) => + !Editor.isEditor(n) && + Element.isElement(n) && + n.type === 'editor' && + parseChapter(n.number) === chapter, + /* n.type === 'chapter' && + parseChapter(Node.string(n)) === chapter, */ + }); + if (editorNodeEntry) { + const [, editorNodePath] = editorNodeEntry; + + // Make a match function that matches on the chapter node if verse 0 or the verse node otherwise + const matchVerseNode = + verse > 0 + ? (n: Node) => + Element.isElement(n) && + n.type === 'verse' && + parseVerse(Node.string(n)) === verse + : (n: Node) => + Element.isElement(n) && + n.type === 'chapter' && + parseChapter(Node.string(n)) === chapter; + + // Get the node for the specified verse + const [verseNodeEntry] = Editor.nodes(editor, { + at: [ + editorNodePath, + Editor.last(editor, editorNodePath)[1], + ], + match: matchVerseNode, + }); + if (verseNodeEntry) { + const [verseNode] = verseNodeEntry; + + try { + // Get the dom element for this verse marker and scroll to it + const verseDomElement = ReactEditor.toDOMNode( + editor, + verseNode, + ); + + verseDomElement.scrollIntoView({ + block: 'center', + behavior: 'smooth', + }); + } catch (e) { + console.warn( + `Not able to scroll to ${getTextFromScrRef({ + book, + chapter, + verse, + })}`, + ); + console.warn(e); + } + } + } + } + didIUpdateScrRef.current = false; + }, [editor, book, chapter, verse]); return (
@@ -360,6 +524,7 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC(
diff --git a/react-electron-poc/src/renderer/util/ScriptureUtil.ts b/react-electron-poc/src/renderer/util/ScriptureUtil.ts index bed7904..6251121 100644 --- a/react-electron-poc/src/renderer/util/ScriptureUtil.ts +++ b/react-electron-poc/src/renderer/util/ScriptureUtil.ts @@ -1,5 +1,5 @@ import { ScriptureReference } from '@shared/data/ScriptureTypes'; -import { isString } from './Util'; +import { isString, isValidValue } from './Util'; const scrBookNames: string[][] = [ ['ERR', 'ERROR'], @@ -123,6 +123,18 @@ export const offsetVerse = ( offset: number, ): ScriptureReference => ({ ...scrRef, verse: scrRef.verse + offset }); +/** Parse a verse number from a string */ +export const parseVerse = (verseText: string): number | undefined => { + const verseNum = parseInt(verseText, 10); + return isValidValue(verseNum) ? verseNum : undefined; +}; + +/** Parse a chapter number from a string */ +export const parseChapter = (chapterText: string): number | undefined => { + // For now, this is the same as parseVerse. Maybe there could be other constraints in the future + return parseVerse(chapterText); +}; + const regexpScrRef = /([^ ]+) ([^:]+):(.+)/; export const getScrRefFromText = (refText: string): ScriptureReference => { if (!refText) return { book: -1, chapter: -1, verse: -1 }; diff --git a/react-electron-poc/src/renderer/util/Util.ts b/react-electron-poc/src/renderer/util/Util.ts index 04e06d7..857d4d4 100644 --- a/react-electron-poc/src/renderer/util/Util.ts +++ b/react-electron-poc/src/renderer/util/Util.ts @@ -21,7 +21,7 @@ export function isString(o: unknown) { * @param val value to evaluate * @returns whether the value is truthy, false, or 0 */ -export function isValidValue(val: unknown): boolean { +export function isValidValue(val: unknown): val is NonNullable { return !!val || val === false || val === 0; } From 6f8dd94c100532504bb8541429b1e683510dcffe Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 3 Oct 2022 14:25:20 -0500 Subject: [PATCH 11/13] Fixed scrolling to ref not working with finished resources, fixed only one project scrolling at a time --- .../TextPanels/ScriptureTextPanelHtml.tsx | 84 ++++++++----------- .../TextPanels/ScriptureTextPanelSlate.tsx | 1 - 2 files changed, 35 insertions(+), 50 deletions(-) diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx index 81c4287..9d6b84d 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx @@ -3,7 +3,11 @@ import { ScriptureChapter, ScriptureChapterString, } from '@shared/data/ScriptureTypes'; -import { parseChapter, parseVerse } from '@util/ScriptureUtil'; +import { + getTextFromScrRef, + parseChapter, + parseVerse, +} from '@util/ScriptureUtil'; import { isValidValue } from '@util/Util'; import { EventHandler, useCallback, useEffect, useRef, useState } from 'react'; import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; @@ -91,31 +95,17 @@ export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( // Step up the node tree via previous siblings then parents all the way up until we find a chapter and verse while (node && node !== editorRef.current) { - if (node instanceof Element) { - if (selectedVerse <= 0 && node.className === 'usfm_v') { - // It's a verse, so try to parse its text and use that as the verse - const textParts = node.textContent?.split(' '); - if (textParts) { - const verseText = - textParts[textParts.length - 1]; - const verseNum = parseVerse(verseText); - if (isValidValue(verseNum)) { - selectedVerse = verseNum; - } - } - } else if ( - selectedChapter < 0 && - node.className === 'usfm_c' - ) { - // It's a chapter, so try to parse its text and use that as the chapter - const textParts = node.textContent?.split(' '); - if (textParts) { - const chapterText = - textParts[textParts.length - 1]; - const chapterNum = parseChapter(chapterText); - if (isValidValue(chapterNum)) { - selectedChapter = chapterNum; - } + if (node instanceof Element && node.id) { + const idMatch = node.id.match(regexpChapterVerseId); + if (idMatch && idMatch.length >= 2) { + const verseNum = parseVerse(idMatch[2]); + const chapterNum = parseChapter(idMatch[1]); + if ( + isValidValue(verseNum) && + isValidValue(chapterNum) + ) { + selectedVerse = verseNum; + selectedChapter = chapterNum; } } } @@ -180,19 +170,19 @@ export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( if (editorElement) break; // Get the chapter element within the current usfm element - const chapterElements = - usfmElements[i].getElementsByClassName('usfm_c'); - for (let j = 0; j < chapterElements.length; j++) { + const currChapterElement = + usfmElements[i].querySelector('.usfm_c'); + if (currChapterElement) { // Check if this usfm element is the right one for this chapter const idMatch = - chapterElements[j].id.match(regexpChapterVerseId); + currChapterElement.id.match(regexpChapterVerseId); if ( idMatch && idMatch.length >= 2 && parseChapter(idMatch[1]) === chapter ) { editorElement = usfmElements[i]; - chapterElement = chapterElements[j]; + chapterElement = currChapterElement; break; } } @@ -200,34 +190,20 @@ export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( if (editorElement) { // Get the element for the specified verse - let verseElement: Element | undefined; + let verseElement: Element | undefined | null; // If the verse we're trying to scroll to is 0, scroll to the chapter if (verse <= 0) { verseElement = chapterElement; } else { - // Get all the verse elements for this chapter - const verseElements = - editorElement.getElementsByClassName('usfm_v'); - for (let i = 0; i < verseElements.length; i++) { - // Check if this verse element is the right one for this verse - const idMatch = - verseElements[i].id.match(regexpChapterVerseId); - if ( - idMatch && - idMatch.length >= 3 && - parseVerse(idMatch[2]) === verse - ) { - verseElement = verseElements[i]; - break; - } - } + verseElement = editorElement.querySelector( + `#cv${chapter}_${verse}`, + ); } if (verseElement) verseElement.scrollIntoView({ block: 'center', - behavior: 'smooth', }); } } @@ -250,11 +226,21 @@ export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( )) : scrChapters.map((scrChapter) => (
e.preventDefault()} /> ))}
diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index 4971b82..8260a38 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -500,7 +500,6 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( verseDomElement.scrollIntoView({ block: 'center', - behavior: 'smooth', }); } catch (e) { console.warn( From 2b0e68e86a076bf7849c73e8eefd1269429e5ee6 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 3 Oct 2022 16:54:55 -0500 Subject: [PATCH 12/13] Fixed problem where scrolling to ref wouldn't always work --- .../TextPanels/ScriptureTextPanelHtml.tsx | 96 +++++++------- .../TextPanels/ScriptureTextPanelSlate.tsx | 117 +++++++++--------- 2 files changed, 112 insertions(+), 101 deletions(-) diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx index 9d6b84d..d54205c 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHtml.tsx @@ -81,6 +81,7 @@ export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( * */ const didIUpdateScrRef = useRef(false); + /** Look for the selection's current BCV and update the ScrRef to it */ const tryUpdateScrRef = () => { const windowSel = window.getSelection(); if (windowSel && windowSel.rangeCount > 0) { @@ -159,56 +160,63 @@ export const ScriptureTextPanelHtml = ScriptureTextPanelHOC( useEffect(() => { // TODO: Determine if this window should scroll by computing if the verse element is visible instead of using hacky didIUpdateScrRef if (!didIUpdateScrRef.current && editorRef.current) { - // Get the node for the specified chapter editor - let editorElement: Element | undefined; - let chapterElement: Element | undefined; - // Get all the usfm elements (full chapters) - const usfmElements = - editorRef.current.getElementsByClassName('usfm'); - for (let i = 0; i < usfmElements.length; i++) { - // If we already found our chapter, we're done - if (editorElement) break; - - // Get the chapter element within the current usfm element - const currChapterElement = - usfmElements[i].querySelector('.usfm_c'); - if (currChapterElement) { - // Check if this usfm element is the right one for this chapter - const idMatch = - currChapterElement.id.match(regexpChapterVerseId); - if ( - idMatch && - idMatch.length >= 2 && - parseChapter(idMatch[1]) === chapter - ) { - editorElement = usfmElements[i]; - chapterElement = currChapterElement; - break; + // TODO: Find a better way to wait for the DOM to load before scrolling and hopefully remove scrChapters from dependencies. Ex: Go between Psalm 118:15 and Psalm 119:150 + setTimeout(() => { + // Get the node for the specified chapter editor + let editorElement: Element | undefined; + let chapterElement: Element | undefined; + // Get all the usfm elements (full chapters) + if (editorRef.current) { + const usfmElements = + editorRef.current.getElementsByClassName('usfm'); + for (let i = 0; i < usfmElements.length; i++) { + // If we already found our chapter, we're done + if (editorElement) break; + + // Get the chapter element within the current usfm element + const currChapterElement = + usfmElements[i].querySelector('.usfm_c'); + if (currChapterElement) { + // Check if this usfm element is the right one for this chapter + const idMatch = + currChapterElement.id.match( + regexpChapterVerseId, + ); + if ( + idMatch && + idMatch.length >= 2 && + parseChapter(idMatch[1]) === chapter + ) { + editorElement = usfmElements[i]; + chapterElement = currChapterElement; + break; + } + } } - } - } - if (editorElement) { - // Get the element for the specified verse - let verseElement: Element | undefined | null; + if (editorElement) { + // Get the element for the specified verse + let verseElement: Element | undefined | null; - // If the verse we're trying to scroll to is 0, scroll to the chapter - if (verse <= 0) { - verseElement = chapterElement; - } else { - verseElement = editorElement.querySelector( - `#cv${chapter}_${verse}`, - ); - } + // If the verse we're trying to scroll to is 0, scroll to the chapter + if (verse <= 0) { + verseElement = chapterElement; + } else { + verseElement = editorElement.querySelector( + `#cv${chapter}_${verse}`, + ); + } - if (verseElement) - verseElement.scrollIntoView({ - block: 'center', - }); - } + if (verseElement) + verseElement.scrollIntoView({ + block: 'center', + }); + } + } + }, 1); } didIUpdateScrRef.current = false; - }, [book, chapter, verse]); + }, [scrChapters, book, chapter, verse]); return (
diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index 8260a38..fbfe29d 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -454,68 +454,71 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( useEffect(() => { // TODO: Determine if this window should scroll by computing if the verse element is visible instead of using hacky didIUpdateScrRef if (!didIUpdateScrRef.current) { - // Get the node for the specified chapter editor - const [editorNodeEntry] = Editor.nodes(editor, { - at: [], - match: (n) => - !Editor.isEditor(n) && - Element.isElement(n) && - n.type === 'editor' && - parseChapter(n.number) === chapter, - /* n.type === 'chapter' && - parseChapter(Node.string(n)) === chapter, */ - }); - if (editorNodeEntry) { - const [, editorNodePath] = editorNodeEntry; - - // Make a match function that matches on the chapter node if verse 0 or the verse node otherwise - const matchVerseNode = - verse > 0 - ? (n: Node) => - Element.isElement(n) && - n.type === 'verse' && - parseVerse(Node.string(n)) === verse - : (n: Node) => - Element.isElement(n) && - n.type === 'chapter' && - parseChapter(Node.string(n)) === chapter; - - // Get the node for the specified verse - const [verseNodeEntry] = Editor.nodes(editor, { - at: [ - editorNodePath, - Editor.last(editor, editorNodePath)[1], - ], - match: matchVerseNode, + // TODO: Find a better way to wait for the DOM to load before scrolling and hopefully remove scrChapters from dependencies. Ex: Go between Psalm 118:15 and Psalm 119:150 + setTimeout(() => { + // Get the node for the specified chapter editor + const [editorNodeEntry] = Editor.nodes(editor, { + at: [], + match: (n) => + !Editor.isEditor(n) && + Element.isElement(n) && + n.type === 'editor' && + parseChapter(n.number) === chapter, + /* n.type === 'chapter' && + parseChapter(Node.string(n)) === chapter, */ }); - if (verseNodeEntry) { - const [verseNode] = verseNodeEntry; - - try { - // Get the dom element for this verse marker and scroll to it - const verseDomElement = ReactEditor.toDOMNode( - editor, - verseNode, - ); - - verseDomElement.scrollIntoView({ - block: 'center', - }); - } catch (e) { - console.warn( - `Not able to scroll to ${getTextFromScrRef({ - book, - chapter, - verse, - })}`, - ); - console.warn(e); + if (editorNodeEntry) { + const [, editorNodePath] = editorNodeEntry; + + // Make a match function that matches on the chapter node if verse 0 or the verse node otherwise + const matchVerseNode = + verse > 0 + ? (n: Node) => + Element.isElement(n) && + n.type === 'verse' && + parseVerse(Node.string(n)) === verse + : (n: Node) => + Element.isElement(n) && + n.type === 'chapter' && + parseChapter(Node.string(n)) === chapter; + + // Get the node for the specified verse + const [verseNodeEntry] = Editor.nodes(editor, { + at: [ + editorNodePath, + Editor.last(editor, editorNodePath)[1], + ], + match: matchVerseNode, + }); + if (verseNodeEntry) { + const [verseNode] = verseNodeEntry; + + try { + // Get the dom element for this verse marker and scroll to it + const verseDomElement = ReactEditor.toDOMNode( + editor, + verseNode, + ); + + verseDomElement.scrollIntoView({ + block: 'center', + }); + } catch (e) { + console.warn( + `Not able to scroll to ${getTextFromScrRef({ + book, + chapter, + verse, + })}`, + ); + console.warn(e); + } } } - } + }, 1); } didIUpdateScrRef.current = false; - }, [editor, book, chapter, verse]); + }, [editor, scrChapters, book, chapter, verse]); return (
From ccc8e498131aa5c35e3288f34c01886c7c454479 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 3 Oct 2022 16:55:23 -0500 Subject: [PATCH 13/13] Persisted settings --- .../src/renderer/components/layout/Layout.tsx | 30 +++++++++++++++---- .../src/renderer/services/SettingsService.ts | 9 ++++++ 2 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 react-electron-poc/src/renderer/services/SettingsService.ts diff --git a/react-electron-poc/src/renderer/components/layout/Layout.tsx b/react-electron-poc/src/renderer/components/layout/Layout.tsx index a7f0ff9..cc3f279 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.tsx +++ b/react-electron-poc/src/renderer/components/layout/Layout.tsx @@ -11,24 +11,36 @@ import { ScriptureTextPanelHtmlProps } from '@components/panels/TextPanels/Scrip import { ScriptureReference } from '@shared/data/ScriptureTypes'; import ScrRefSelector from '@components/ScrRefSelector'; import { PanelManager } from '@components/panels/PanelManager'; +import { getSetting, setSetting } from '@services/SettingsService'; + +/** Key for saving scrRef setting */ +const scrRefSettingKey = 'scrRef'; +/** Key for saving browseBook setting */ +const browseBookSettingKey = 'browseBook'; const Layout = () => { const panelManager = useRef(undefined); - const [scrRef, setScrRef] = useState({ - book: 19, - chapter: 119, - verse: 1, - }); + const [scrRef, setScrRef] = useState( + getSetting(scrRefSettingKey) || { + book: 19, + chapter: 119, + verse: 1, + }, + ); const updateScrRef = useCallback((newScrRef: ScriptureReference) => { setScrRef(newScrRef); + setSetting(scrRefSettingKey, newScrRef); panelManager.current?.updateScrRef(newScrRef); }, []); - const [browseBook, setBrowseBook] = useState(false); + const [browseBook, setBrowseBook] = useState( + getSetting(browseBookSettingKey) || false, + ); const updateBrowseBook = useCallback((newBrowseBook: boolean) => { setBrowseBook(newBrowseBook); + setSetting(browseBookSettingKey, newBrowseBook); panelManager.current?.updateBrowseBook(newBrowseBook); }, []); @@ -81,6 +93,7 @@ const Layout = () => { editable: false, ...scrRef, updateScrRef, + browseBook, } as ScriptureTextPanelHtmlProps, ); const ohebPanel = panelManager.current.addPanel( @@ -90,6 +103,7 @@ const Layout = () => { editable: false, ...scrRef, updateScrRef, + browseBook, } as ScriptureTextPanelHtmlProps, { position: { @@ -105,6 +119,7 @@ const Layout = () => { editable: true, ...scrRef, updateScrRef, + browseBook, } as ScriptureTextPanelHtmlProps, { position: { @@ -120,6 +135,7 @@ const Layout = () => { editable: false, ...scrRef, updateScrRef, + browseBook, } as ScriptureTextPanelHtmlProps, { position: { @@ -135,6 +151,7 @@ const Layout = () => { editable: true, ...scrRef, updateScrRef, + browseBook, } as ScriptureTextPanelHtmlProps, { position: { @@ -150,6 +167,7 @@ const Layout = () => { editable: true, ...scrRef, updateScrRef, + browseBook, } as ScriptureTextPanelHtmlProps, { position: { diff --git a/react-electron-poc/src/renderer/services/SettingsService.ts b/react-electron-poc/src/renderer/services/SettingsService.ts new file mode 100644 index 0000000..63f7ef9 --- /dev/null +++ b/react-electron-poc/src/renderer/services/SettingsService.ts @@ -0,0 +1,9 @@ +export const getSetting = (key: string): T | undefined => { + const setting = localStorage.getItem(key); + return setting ? JSON.parse(setting) : setting; +}; + +export const setSetting = (key: string, value: T | undefined) => { + if (value) localStorage.setItem(key, JSON.stringify(value)); + else localStorage.removeItem(key); +};