From 327a013bff1f6a82b988a33eac0ecb96a07e711e Mon Sep 17 00:00:00 2001 From: Samuel John <40059405+samueljd@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:02:53 +0530 Subject: [PATCH] versewise navigation with arrow keys. (#300) * versewise navigation with arrow keys. * highlight text, navigate on same verse multi click * fixed scroll lock issue * removed highlight text --- .../EditorPage/TextEditor/Editor.jsx | 6 +- .../EditorPage/TextEditor/RecursiveBlock.jsx | 59 +++++++++-------- .../TextEditor/utils/IntersectionObserver.js | 31 ++++++--- .../TextEditor/utils/getReferences.js | 64 ++++++++++++++----- styles/globals.css | 4 ++ 5 files changed, 109 insertions(+), 55 deletions(-) diff --git a/renderer/src/components/EditorPage/TextEditor/Editor.jsx b/renderer/src/components/EditorPage/TextEditor/Editor.jsx index 03c1ad86e..9423e062c 100644 --- a/renderer/src/components/EditorPage/TextEditor/Editor.jsx +++ b/renderer/src/components/EditorPage/TextEditor/Editor.jsx @@ -1,5 +1,5 @@ import React, { - useContext, useEffect, useLayoutEffect, useRef, + useContext, useEffect, useLayoutEffect, useRef, } from 'react'; import { HtmlPerfEditor } from '@xelah/type-perf-html'; @@ -123,7 +123,7 @@ export default function Editor(props) { function onReferenceSelected({ chapter, verse }) { chapter && setChapterNumber(chapter); verse && setVerseNumber(verse); - scrollReference(chapter); + !scrollLock && scrollReference(chapter, verse); } const observer = new IntersectionObserver((entries) => onIntersection({ @@ -147,7 +147,7 @@ export default function Editor(props) { addSequenceId, components: { block: (__props) => RecursiveBlock({ - htmlPerf, onHtmlPerf: saveHtmlPerf, sequenceIds, addSequenceId, onReferenceSelected, setCaretPosition, setSelectedText, ...__props, + htmlPerf, onHtmlPerf: saveHtmlPerf, sequenceIds, addSequenceId, onReferenceSelected, setCaretPosition, setSelectedText, scrollLock, ...__props, }), }, options: { diff --git a/renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx b/renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx index 17e7f36f3..ce97f24a2 100644 --- a/renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx +++ b/renderer/src/components/EditorPage/TextEditor/RecursiveBlock.jsx @@ -3,7 +3,9 @@ import React, { useEffect, useState } from 'react'; import { HtmlPerfEditor } from '@xelah/type-perf-html'; import { getCurrentCursorPosition, pasteTextAtCursorPosition } from '@/util/cursorUtils'; -import { getCurrentVerse, getCurrentChapter } from '@/components/EditorPage/TextEditor/utils/getReferences'; +import { + getCurrentVerse, getCurrentChapter, +} from '@/components/EditorPage/TextEditor/utils/getReferences'; import { on } from 'ws'; const getTarget = ({ content }) => { @@ -31,6 +33,7 @@ export default function RecursiveBlock({ onReferenceSelected, setCaretPosition, setSelectedText, + scrollLock, ...props }) { const [currentVerse, setCurrentVerse] = useState(null); @@ -40,27 +43,6 @@ export default function RecursiveBlock({ setCaretPosition(cursorPosition); }; - const checkReturnKeyPress = (event) => { - const activeTextArea = document.activeElement; - if (event.key === 'Enter') { - if (activeTextArea.children.length > 1) { - const lineBreak = activeTextArea.children[1]?.outerHTML; - activeTextArea.children[1].outerHTML = lineBreak.replace(//gi, ' '); - } - } - // BACKSPACE DISABLE - if (event.keyCode === 8) { - const range = document.getSelection().getRangeAt(0); - const selectedNode = range.startContainer; - const prevNode = selectedNode.previousSibling; - if (prevNode && prevNode.dataset.attsNumber !== currentVerse) { - event.preventDefault(); - } - prevNode ? setCurrentVerse(prevNode.dataset.attsNumber) : {}; - } - updateCursorPosition(); - }; - function handleSelection() { let selectedText = ''; if (window.getSelection) { @@ -72,19 +54,44 @@ export default function RecursiveBlock({ setSelectedText(selectedText); } } - const checkCurrentVerse = () => { if (document.getSelection().rangeCount >= 1 && onReferenceSelected) { const range = document.getSelection().getRangeAt(0); const selectedNode = range.startContainer; - const verse = getCurrentVerse(selectedNode); + const { verse } = getCurrentVerse(selectedNode); const chapter = getCurrentChapter(selectedNode); onReferenceSelected({ bookId, chapter, verse }); + // !scrollLock && hightlightRefVerse(chapter, verse); } updateCursorPosition(); handleSelection(); }; + const keyStrokeHandler = (event) => { + const activeTextArea = document.activeElement; + // Replace line break with space + if (event.key === 'Enter') { + if (activeTextArea.children.length > 1) { + const lineBreak = activeTextArea.children[1]?.outerHTML; + activeTextArea.children[1].outerHTML = lineBreak.replace(//gi, ' '); + } + } + // Disable backspace if the previous node is not the same verse + if (event.keyCode === 8) { + const range = document.getSelection().getRangeAt(0); + const selectedNode = range.startContainer; + const prevNode = selectedNode.previousSibling; + if (prevNode && prevNode.dataset.attsNumber !== currentVerse) { + event.preventDefault(); + } + prevNode ? setCurrentVerse(prevNode.dataset.attsNumber) : {}; + } + if ([37, 38, 39, 40].includes(event.keyCode)) { + checkCurrentVerse(); + updateCursorPosition(); + } + }; + function onPasteHandler(event) { const cursorPosition = getCurrentCursorPosition('editor'); const paste = (event.clipboardData || window.clipboardData).getData('text'); @@ -101,11 +108,11 @@ export default function RecursiveBlock({
{ event.preventDefault(); onPasteHandler(event); }} + {...props} /> ); } diff --git a/renderer/src/components/EditorPage/TextEditor/utils/IntersectionObserver.js b/renderer/src/components/EditorPage/TextEditor/utils/IntersectionObserver.js index dafa7eaf2..7ba0957aa 100644 --- a/renderer/src/components/EditorPage/TextEditor/utils/IntersectionObserver.js +++ b/renderer/src/components/EditorPage/TextEditor/utils/IntersectionObserver.js @@ -1,13 +1,24 @@ -export const scrollReference = (chapterNumber) => { - const refEditors = document.getElementsByClassName('ref-editor'); - refEditors.length > 0 && Array.prototype.filter.call(refEditors, (refEditor) => { - const editorInView = refEditor.querySelector(`#ch-${chapterNumber}`); - if (editorInView) { - editorInView.scrollIntoView(); - editorInView.classList.add('scroll-mt-10'); - } - }); -}; +export const scrollReference = (() => { + let prevCV; + return (c, v) => { + const refEditors = document.getElementsByClassName('ref-editor'); + refEditors.length > 0 && Array.prototype.filter.call(refEditors, (refEditor) => { + if (!prevCV || prevCV.c !== c) { + const chapterInView = refEditor.querySelector(`#ch-${c}`); + if (chapterInView) { + chapterInView.scrollIntoView(); + chapterInView.classList.add('scroll-mt-10'); + } + } else { + const verseInView = refEditor.querySelector(`#ch${c}v${v}`); + if (verseInView) { + verseInView.scrollIntoView(); + } + } + }); + prevCV = { c, v }; + }; +})(); export const onIntersection = ({ scroll, entries, setChapterNumber, scrollLock, setVerseNumber, diff --git a/renderer/src/components/EditorPage/TextEditor/utils/getReferences.js b/renderer/src/components/EditorPage/TextEditor/utils/getReferences.js index fe6cb92f2..b923fa370 100644 --- a/renderer/src/components/EditorPage/TextEditor/utils/getReferences.js +++ b/renderer/src/components/EditorPage/TextEditor/utils/getReferences.js @@ -1,17 +1,49 @@ export const getCurrentVerse = (currentNode) => { - let currentVerse; - let prev = currentNode.previousElementSibling; - while (prev) { - if (prev.dataset.type === 'mark' && prev.dataset.subtype === 'verses') { - currentVerse = prev.dataset.attsNumber; + let verse; + let previousElement = currentNode?.previousElementSibling; + const verseText = currentNode?.nextSibling; + while (previousElement) { + if (previousElement.dataset.type === 'mark' && previousElement.dataset.subtype === 'verses') { + verse = previousElement.dataset.attsNumber; break; } - // Get the previous sibling - prev = prev.previousElementSibling; + previousElement = previousElement?.previousElementSibling; } - return currentVerse; + return { verse, verseText }; }; +export const removeHighlightFromRefVerse = ({ c, v }) => { + const refEditors = document.getElementsByClassName('ref-editor'); + refEditors.length > 0 && Array.prototype.filter.call(refEditors, (refEditor) => { + const prevHighlight = refEditor.querySelector(`#ch${c}v${v}`)?.nextElementSibling; + const hightlightText = prevHighlight && prevHighlight.innerHTML; + prevHighlight && prevHighlight.replaceWith(hightlightText); + }); +}; + +export const hightlightRefVerse = (() => { + let prevCV; + return (c, v) => { + const refEditors = document.getElementsByClassName('ref-editor'); + refEditors.length > 0 && Array.prototype.filter.call(refEditors, (refEditor) => { + if (!(prevCV && prevCV.c !== c)) { + const verseInView = refEditor.querySelector(`#ch${c}v${v}`); + const { verseText } = getCurrentVerse(verseInView); + // highlight verse + const range = document.createRange(); + range.setStart(verseText, 0); + range.setEnd(verseText, verseText.textContent.length); + const newSpan = document.createElement('span'); + newSpan.classList.add('bg-primary-50'); + range.surroundContents(newSpan); + // remove highlight from previous verse + prevCV && removeHighlightFromRefVerse({ ...prevCV }); + } + }); + prevCV = { c, v }; + }; +})(); + export const getCurrentChapter = (currentNode) => { let currentChapter; const closestParaDiv = currentNode.parentNode.parentNode; @@ -20,13 +52,13 @@ export const getCurrentChapter = (currentNode) => { return currentChapter; } - let prevParaDiv = closestParaDiv.previousElementSibling; - while (prevParaDiv) { - if (prevParaDiv.firstElementChild?.firstElementChild?.classList.contains('chapter')) { - currentChapter = prevParaDiv.firstElementChild.firstElementChild.dataset.attsNumber; - break; - } - prevParaDiv = prevParaDiv.previousElementSibling; + let prevParaDiv = closestParaDiv.previousElementSibling; + while (prevParaDiv) { + if (prevParaDiv.firstElementChild?.firstElementChild?.classList.contains('chapter')) { + currentChapter = prevParaDiv.firstElementChild.firstElementChild.dataset.attsNumber; + break; } - return currentChapter; + prevParaDiv = prevParaDiv.previousElementSibling; + } + return currentChapter; }; diff --git a/styles/globals.css b/styles/globals.css index c544797b6..f716ddc74 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -103,6 +103,10 @@ p.paragraph:has(.chapter) { @apply text-primary text-sm; } +.ref-editor ::selection { + @apply bg-primary-50; +} + /* .perf .verse:after { visibility: visible; position: absolute;