Download static/reader/ui/reader-navigation.mjs from VoiceOfML/Search: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/ui/reader-navigation.mjs
- Command line
-
hf download hf://spaces/VoiceOfML/Search/static/reader/ui/reader-navigation.mjs
-
curl -L -o reader-navigation.mjs https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/ui/reader-navigation.mjs
11.1 kB
| export function createReaderNavigation(readerContext) { | |
| function beginReaderNavigation() { | |
| if (readerContext.epubSeekFrame) cancelAnimationFrame(readerContext.epubSeekFrame); | |
| readerContext.epubSeekFrame = 0; | |
| readerContext.foliateScrollAnchors.invalidate(); | |
| return readerContext.nextReaderGeneration("navigation"); | |
| } | |
| function clearReturnNavigation() { | |
| try { | |
| if (readerContext.returnHistoryKey) sessionStorage.removeItem(readerContext.returnHistoryKey); | |
| const saved = JSON.parse(sessionStorage.getItem("reader-navigation-current") || "null"); | |
| if (saved && (saved.readerUrl === location.href || saved.shareUrl === location.href)) | |
| sessionStorage.removeItem("reader-navigation-current"); | |
| } catch (_) {} | |
| } | |
| // Only the search page's own Reader overlay handles these messages. Any other | |
| // frame host (or a cross-origin one) would drop them, so navigate in place. | |
| function isReaderOverlayFrame() { | |
| try { | |
| return ( | |
| window.parent !== window && | |
| window.parent.location.origin === location.origin && | |
| !!window.frameElement?.matches("iframe.reader-overlay") | |
| ); | |
| } catch (_) { | |
| return false; | |
| } | |
| } | |
| function navigateReaderFolder() { | |
| if (!readerContext.folderNavigationTarget) return; | |
| if (isReaderOverlayFrame()) | |
| window.parent.postMessage( | |
| { type: "voice-reader-navigate", url: readerContext.folderNavigationTarget.href }, | |
| location.origin | |
| ); | |
| else { | |
| readerContext.clearReturnNavigation(); | |
| location.assign(readerContext.folderNavigationTarget.href); | |
| } | |
| } | |
| function turnViewport(direction) { | |
| readerContext.beginReaderNavigation(); | |
| const target = readerContext.htmlFrame?.contentWindow || readerContext.viewport; | |
| target.scrollBy({ | |
| top: | |
| Math.max(160, (readerContext.htmlFrame?.contentWindow?.innerHeight || readerContext.viewport.clientHeight) * 0.86) * | |
| direction, | |
| behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth" | |
| }); | |
| } | |
| function handleReaderKeydown(event) { | |
| if ( | |
| event.defaultPrevented || | |
| event.isComposing || | |
| event.altKey || | |
| event.ctrlKey || | |
| event.metaKey | |
| ) | |
| return; | |
| if (event.key === "Escape" && readerContext.panelState.open && readerContext.bookmarkPopover.hidden) { | |
| event.preventDefault(); | |
| if (readerContext.readerUiHistoryState === "full-search") readerContext.closeFullSearchView(); | |
| else readerContext.setReaderPanelOpen(false, true); | |
| return; | |
| } | |
| // Panel lists scroll independently. Inspect the composed path so a control | |
| // inside a chapter Shadow DOM is not mistaken for the reading surface. | |
| if ((event.composedPath?.() || [event.target]).some(node => node.closest?.( | |
| '#history-panel,input,textarea,select,button,a,audio,video,[role="button"],[role="link"],[contenteditable]:not([contenteditable=false])' | |
| ))) | |
| return; | |
| if (["PageDown", " ", "ArrowDown", "PageUp", "ArrowUp"].includes(event.key)) { | |
| event.preventDefault(); | |
| readerContext.turnViewport( | |
| ["PageUp", "ArrowUp"].includes(event.key) || (event.key === " " && event.shiftKey) ? -1 : 1 | |
| ); | |
| } | |
| } | |
| function handleReaderOutsideClick(event) { | |
| if (readerContext.readerLifecycle.disposed || readerContext.readerAbortController.signal.aborted || | |
| !readerContext.panelState.open || !readerContext.bookmarkPopover.hidden) return; | |
| const path = event.composedPath(); | |
| if (path.includes(document.querySelector("#history-panel")) || path.includes(document.querySelector("#history")) || | |
| path.includes(readerContext.bookmarkPopover) || path.includes(readerContext.bookmarkRibbon)) return; | |
| // Do not consume the content click or move focus: text selection and book | |
| // links keep their ordinary behavior, including inside the HTML iframe. | |
| readerContext.setReaderPanelOpen(false); | |
| } | |
| function readerPageScrollTop(shell) { | |
| return readerContext.viewport.scrollTop + shell.getBoundingClientRect().top - | |
| readerContext.viewport.getBoundingClientRect().top - readerContext.viewport.clientTop; | |
| } | |
| async function goToPage(value, generation = readerContext.beginReaderNavigation()) { | |
| try { | |
| if (!readerContext.documentState.pageCount || !readerContext.isReaderGenerationCurrent("navigation", generation)) | |
| return false; | |
| const page = VoiceOfMLReader.clampNumber(value, 1, readerContext.documentState.pageCount, 1); | |
| readerContext.pdfShellWindow?.ensure(page); | |
| let shell = readerContext.content.querySelector( | |
| `.reader-page[data-page="${page}"]` | |
| ); | |
| if (!shell && ["pdf-pages", "image-pages"].includes(readerContext.capability.mode)) { | |
| await readerContext.pdfShellsReady; | |
| shell = readerContext.content.querySelector(`.reader-page[data-page="${page}"]`); | |
| } | |
| if (!readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| if (!shell) return false; | |
| const imagePage = ["pdf-pages", "image-pages"].includes(readerContext.capability.mode); | |
| if (imagePage && shell.dataset.renderState !== "rendered") | |
| readerContext.prioritizePdfManifestPrefetch(readerContext.pdfPageManifest?.pageUrl(page)); | |
| const pendingPdf = | |
| !imagePage && shell.classList.contains("reader-page") | |
| ? readerContext.renderPdfInBackground(shell, false, true) | |
| : null; | |
| if (!readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| readerContext.viewport.scrollTop = readerContext.readerPageScrollTop(shell); | |
| if (imagePage && shell.dataset.renderState === "rendered") readerContext.renderPdfOcrText(shell); | |
| readerContext.pageNavigationLockUntil = performance.now() + 500; | |
| if (pendingPdf) readerContext.cancelSpeculativePdfRenders(shell); | |
| const pendingImage = imagePage ? readerContext.renderPdfInBackground(shell, false, true) : null; | |
| if (imagePage && shell.dataset.renderState !== "rendered") readerContext.cancelSpeculativePdfRenders(shell); | |
| readerContext.updateDocumentState({ page }); | |
| readerContext.pageInput.value = String(page); | |
| readerContext.updateTocCurrentMark(); | |
| readerContext.updateProgressTools(); | |
| pendingPdf?.catch(() => {}); | |
| pendingImage?.catch(() => {}); | |
| if (!readerContext.isReaderGenerationCurrent("navigation", generation)) return false; | |
| readerContext.scheduleSave(); | |
| return true; | |
| } catch (error) { | |
| readerContext.handlePageNavigationFailure(error, generation); | |
| return false; | |
| } | |
| } | |
| function handleReaderPositionChange() { | |
| if (readerContext.readerAbortController.signal.aborted) return; | |
| readerContext.notePdfViewportMotion?.(); | |
| if (readerContext.viewport.scrollTop !== readerContext.pdfLastScrollTop) readerContext.pdfScrollDirection = readerContext.viewport.scrollTop > readerContext.pdfLastScrollTop ? 1 : -1; | |
| readerContext.pdfLastScrollTop = readerContext.viewport.scrollTop; | |
| readerContext.pdfShellWindow?.schedule(); | |
| readerContext.scheduleVisiblePdfRenderReconciliation?.(true); | |
| readerContext.scheduleMarkerSync(); | |
| readerContext.scheduleFoliateScrollSync(); | |
| readerContext.foliateScrollAnchors.remember(); | |
| } | |
| async function navigateReader(rawUrl, bookmark = null) { | |
| let target; | |
| try { | |
| target = new URL(rawUrl, location.origin); | |
| if ( | |
| target.origin !== location.origin || | |
| target.pathname !== "/static/reader.html" || | |
| target.username || | |
| target.password | |
| ) | |
| return; | |
| target.searchParams.delete("bookmark"); | |
| target.searchParams.delete("bookmark_source"); | |
| if (bookmark) { | |
| if ( | |
| typeof bookmark.id !== "string" || | |
| !bookmark.id || | |
| typeof bookmark.url !== "string" || | |
| !bookmark.url | |
| ) | |
| return; | |
| target.searchParams.set("bookmark", bookmark.id); | |
| target.searchParams.set("bookmark_source", bookmark.url); | |
| } | |
| } catch (_) { | |
| return; | |
| } | |
| const generation = readerContext.beginReaderNavigation(); | |
| await readerContext.saveProgress(); | |
| if (!readerContext.isReaderGenerationCurrent("navigation", generation)) return; | |
| if (isReaderOverlayFrame()) | |
| window.parent.postMessage({ type: "voice-reader-open", url: target.href }, location.origin); | |
| else location.assign(target.href); | |
| } | |
| function notePdfScrollIntent() { | |
| readerContext.pdfUserHasScrolled = true; | |
| readerContext.beginReaderNavigation(); | |
| } | |
| function installReaderTapSelectionClear(region, eventRoot = region.ownerDocument) { | |
| let tap = null; | |
| const down = (event) => { | |
| tap = null; | |
| if (event.pointerType !== "touch" || !event.isPrimary) return; | |
| const target = event.composedPath()[0]; | |
| if (target.closest?.("a,button,input,textarea,select,[contenteditable]")) return; | |
| const root = target.getRootNode(); | |
| const selection = root.getSelection?.() || eventRoot.getSelection(); | |
| if (!selection || selection.isCollapsed || !selection.rangeCount) return; | |
| tap = { id: event.pointerId, x: event.clientX, y: event.clientY, time: performance.now(), | |
| selection, anchor: selection.anchorNode, anchorOffset: selection.anchorOffset, | |
| focus: selection.focusNode, focusOffset: selection.focusOffset }; | |
| }; | |
| const move = (event) => { | |
| if (tap && event.pointerId === tap.id && Math.hypot(event.clientX - tap.x, event.clientY - tap.y) > 8) | |
| tap = null; | |
| }; | |
| const up = (event) => { | |
| const start = tap; | |
| tap = null; | |
| if (!start || event.pointerId !== start.id || performance.now() - start.time > 300 || | |
| Math.hypot(event.clientX - start.x, event.clientY - start.y) > 8) return; | |
| const selection = start.selection; | |
| if (selection.anchorNode === start.anchor && selection.anchorOffset === start.anchorOffset && | |
| selection.focusNode === start.focus && selection.focusOffset === start.focusOffset) | |
| selection.removeAllRanges(); | |
| }; | |
| const cancel = () => { tap = null; }; | |
| region.addEventListener("pointerdown", down, { passive: true }); | |
| eventRoot.addEventListener("pointermove", move, { passive: true }); | |
| eventRoot.addEventListener("pointerup", up, { passive: true }); | |
| eventRoot.addEventListener("pointercancel", cancel, { passive: true }); | |
| readerContext.trackReaderResource(() => { | |
| region.removeEventListener("pointerdown", down); | |
| eventRoot.removeEventListener("pointermove", move); | |
| eventRoot.removeEventListener("pointerup", up); | |
| eventRoot.removeEventListener("pointercancel", cancel); | |
| tap = null; | |
| }); | |
| } | |
| return { | |
| beginReaderNavigation, | |
| clearReturnNavigation, | |
| isReaderOverlayFrame, | |
| navigateReaderFolder, | |
| turnViewport, | |
| handleReaderKeydown, | |
| handleReaderOutsideClick, | |
| readerPageScrollTop, | |
| goToPage, | |
| handleReaderPositionChange, | |
| navigateReader, | |
| notePdfScrollIntent, | |
| installReaderTapSelectionClear | |
| }; | |
| } | |