diff --git a/extensions/src/resource-viewer/src/resource-viewer.web-view.scss b/extensions/src/resource-viewer/src/resource-viewer.web-view.scss index 51fa97c439..b4ce92f303 100644 --- a/extensions/src/resource-viewer/src/resource-viewer.web-view.scss +++ b/extensions/src/resource-viewer/src/resource-viewer.web-view.scss @@ -5,3 +5,31 @@ body { background-color: #eee; } + +// #region verse number highlight + +// Highlight keyframes thanks to chazsolo at https://stackoverflow.com/a/55835473 +@keyframes highlight { + from { + background-color: yellow; + } +} + +.editor-container .highlighted { + position: relative; +} + +.editor-container .highlighted::before { + content: ''; + position: absolute; + width: 25px; + height: 25px; + border-radius: 50%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 0.8; + animation: highlight 2s; +} + +// #endregion diff --git a/extensions/src/resource-viewer/src/resource-viewer.web-view.tsx b/extensions/src/resource-viewer/src/resource-viewer.web-view.tsx index aac2843cd4..7f4e7362a6 100644 --- a/extensions/src/resource-viewer/src/resource-viewer.web-view.tsx +++ b/extensions/src/resource-viewer/src/resource-viewer.web-view.tsx @@ -19,16 +19,51 @@ const defaultScrRef: ScriptureReference = { verseNum: 1, }; +function scrollToScrRef(scrRef: ScriptureReference) { + // We are querying for a span, so this Element will be an HTMLElement + // eslint-disable-next-line no-type-assertion/no-type-assertion + const verseElement = document.querySelector( + `.editor-container span[data-marker="v"][data-number="${scrRef.verseNum}"]`, + ) as HTMLElement | undefined; + if (verseElement) { + window.scrollTo({ + top: verseElement.getBoundingClientRect().top + window.scrollY - 55, + behavior: 'smooth', + }); + } + return verseElement; +} + globalThis.webViewComponent = function ResourceViewer({ useWebViewState, }: WebViewProps): JSX.Element { const [projectId] = useWebViewState('projectId', ''); logger.debug(`Resource Viewer project ID: ${projectId}`); - // This ref becomes defined when passed to the editor. - // eslint-disable-next-line no-type-assertion/no-type-assertion, no-null/no-null - const editorRef = useRef(null!); - const [scrRef, setScrRef] = useSetting('platform.verseRef', defaultScrRef); + // Using react's ref api which uses null, so we must use null + // eslint-disable-next-line no-null/no-null + const editorRef = useRef(null); + const [scrRef, setScrRefInternal] = useSetting('platform.verseRef', defaultScrRef); + + /** + * Scripture reference we set most recently. Used so we don't scroll on updates to scrRef that + * come from us + */ + const internallySetScrRefRef = useRef(undefined); + + const setScrRef = useCallback( + (newScrRef: ScriptureReference) => { + internallySetScrRefRef.current = newScrRef; + return setScrRefInternal(newScrRef); + }, + [setScrRefInternal], + ); + + /** + * Whether we have gotten the Scripture data for the very first time. Used to scroll to the + * current scrRef on startup + */ + const hasFirstRetrievedScripture = useRef(false); const [usx, setUsx] = useProjectData('ParatextStandard', projectId).ChapterUSX( useMemo(() => new VerseRef(scrRef.bookNum, scrRef.chapterNum, scrRef.verseNum), [scrRef]), @@ -47,8 +82,42 @@ globalThis.webViewComponent = function ResourceViewer({ if (usx) editorRef.current?.setUsj(usxStringToUsj(usx)); }, [usx]); + useEffect(() => { + if (usx && !hasFirstRetrievedScripture.current) { + hasFirstRetrievedScripture.current = true; + // Wait 100 ms before scrolling to make sure there is plenty of time for the editor to load + // TODO: hook into the editor and detect when it has loaded somehow + setTimeout(() => scrollToScrRef(scrRef), 100); + } + }, [usx, scrRef]); + const viewOptions = useMemo(() => getViewOptions('formatted'), []); + // Scroll the selected verse into view + useEffect(() => { + // If we made this latest scrRef change, don't scroll + if ( + internallySetScrRefRef.current && + internallySetScrRefRef.current.bookNum === scrRef.bookNum && + internallySetScrRefRef.current.chapterNum === scrRef.chapterNum && + internallySetScrRefRef.current.verseNum === scrRef.verseNum + ) { + internallySetScrRefRef.current = undefined; + return () => {}; + } + + // Add a highlight to the current verse element + const highlightedVerseElement = scrollToScrRef(scrRef); + if (highlightedVerseElement) highlightedVerseElement.classList.add('highlighted'); + + internallySetScrRefRef.current = undefined; + + return () => { + // Remove highlight from the current verse element + if (highlightedVerseElement) highlightedVerseElement.classList.remove('highlighted'); + }; + }, [scrRef]); + return (