Download static/reader/ui/reader-scroll-anchor.js from VoiceOfML/Search: direct link, hf CLI and curl.
- Browser
- Download file 5.56 kB
-
https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/ui/reader-scroll-anchor.js
- Command line
-
hf download hf://spaces/VoiceOfML/Search/static/reader/ui/reader-scroll-anchor.js
-
curl -L -o reader-scroll-anchor.js https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/static/reader/ui/reader-scroll-anchor.js
5.56 kB
| (function (root) { | |
| "use strict"; | |
| function createScrollAnchorManager({ | |
| viewport, | |
| candidates, | |
| ResizeObserverImpl = root.ResizeObserver, | |
| requestFrame = root.requestAnimationFrame.bind(root), | |
| cancelFrame = root.cancelAnimationFrame.bind(root), | |
| setTimer = root.setTimeout.bind(root), | |
| clearTimer = root.clearTimeout.bind(root), | |
| scrollIdleDelay = 140 | |
| }) { | |
| let anchor = null; | |
| let captureFrame = 0; | |
| let restoreFrame = 0; | |
| let disposed = false; | |
| let generation = 0; | |
| let scrolling = false; | |
| let scrollTimer = 0; | |
| const idleTasks = new Set(); | |
| function measure(node) { | |
| if (!node?.isConnected) return null; | |
| const viewportRect = viewport.getBoundingClientRect(); | |
| const rect = node.getBoundingClientRect(); | |
| return { node, offset: rect.top - viewportRect.top, generation }; | |
| } | |
| function capture() { | |
| if (disposed) return null; | |
| const viewportRect = viewport.getBoundingClientRect(); | |
| const marker = viewportRect.top + 8; | |
| const measured = candidates() | |
| .filter((node) => node?.isConnected) | |
| .map((node) => ({ node, rect: node.getBoundingClientRect() })) | |
| .filter( | |
| ({ rect }) => | |
| rect.height > 0 && rect.bottom > viewportRect.top && rect.top < viewportRect.bottom | |
| ); | |
| const selected = measured | |
| .filter(({ rect }) => rect.bottom > marker) | |
| .sort( | |
| (left, right) => | |
| Math.abs(left.rect.top - marker) - Math.abs(right.rect.top - marker) || | |
| left.rect.height - right.rect.height | |
| )[0]; | |
| anchor = measure(selected?.node) || anchor; | |
| return anchor; | |
| } | |
| function restore(snapshot = anchor) { | |
| if (disposed || snapshot?.generation !== generation || !snapshot.node?.isConnected) | |
| return false; | |
| if (scrolling) { | |
| capture(); | |
| return false; | |
| } | |
| const current = measure(snapshot.node); | |
| if (!current) return false; | |
| const delta = current.offset - snapshot.offset; | |
| if (Math.abs(delta) > 0.5) viewport.scrollTop += delta; | |
| anchor = measure(snapshot.node); | |
| return true; | |
| } | |
| function scheduleRestore(snapshot = anchor) { | |
| if (disposed || !snapshot || restoreFrame) return; | |
| restoreFrame = requestFrame(() => { | |
| restoreFrame = 0; | |
| restore(anchor); | |
| }); | |
| } | |
| function remember() { | |
| if (disposed) return; | |
| if (restoreFrame) { | |
| cancelFrame(restoreFrame); | |
| restoreFrame = 0; | |
| } | |
| if (captureFrame) cancelFrame(captureFrame); | |
| captureFrame = 0; | |
| capture(); | |
| } | |
| function preserve(change) { | |
| const snapshot = capture(); | |
| const result = change(); | |
| if (scrolling && snapshot?.node?.isConnected) { | |
| // This synchronous layout change must not move the line under a moving finger. | |
| const current = measure(snapshot.node); | |
| if (current && snapshot.generation === generation) | |
| viewport.scrollTop += current.offset - snapshot.offset; | |
| capture(); | |
| return result; | |
| } | |
| restore(snapshot); | |
| scheduleRestore(snapshot); | |
| return result; | |
| } | |
| function finishScroll() { | |
| scrollTimer = 0; | |
| scrolling = false; | |
| const tasks = [...idleTasks]; | |
| idleTasks.clear(); | |
| for (const task of tasks) task(); | |
| capture(); | |
| } | |
| function scheduleScrollEnd() { | |
| if (disposed) return; | |
| if (scrollTimer) clearTimer(scrollTimer); | |
| scrollTimer = setTimer(finishScroll, scrollIdleDelay); | |
| } | |
| function handleScrollIntent() { | |
| scrolling = true; | |
| scheduleScrollEnd(); | |
| } | |
| function handleScroll() { | |
| if (scrolling) scheduleScrollEnd(); | |
| } | |
| function whenIdle(task) { | |
| if (disposed || typeof task !== "function") return; | |
| if (!scrolling) task(); | |
| else idleTasks.add(task); | |
| } | |
| viewport.addEventListener?.("scroll", handleScroll, { passive: true }); | |
| for (const name of ["wheel", "touchstart", "pointerdown"]) | |
| viewport.addEventListener?.(name, handleScrollIntent, { passive: true }); | |
| const resizeObserver = ResizeObserverImpl | |
| ? new ResizeObserverImpl(() => (scrolling ? capture() : restore(anchor))) | |
| : null; | |
| function observe(node) { | |
| resizeObserver?.observe(node); | |
| if (!anchor) capture(); | |
| } | |
| function unobserve(node) { | |
| resizeObserver?.unobserve(node); | |
| if (anchor?.node === node || node?.contains?.(anchor?.node)) anchor = null; | |
| } | |
| function invalidate() { | |
| generation += 1; | |
| if (captureFrame) cancelFrame(captureFrame); | |
| if (restoreFrame) cancelFrame(restoreFrame); | |
| captureFrame = 0; | |
| restoreFrame = 0; | |
| anchor = null; | |
| } | |
| function dispose() { | |
| if (disposed) return; | |
| disposed = true; | |
| viewport.removeEventListener?.("scroll", handleScroll); | |
| for (const name of ["wheel", "touchstart", "pointerdown"]) | |
| viewport.removeEventListener?.(name, handleScrollIntent); | |
| if (scrollTimer) clearTimer(scrollTimer); | |
| scrollTimer = 0; | |
| scrolling = false; | |
| idleTasks.clear(); | |
| resizeObserver?.disconnect(); | |
| invalidate(); | |
| } | |
| return Object.freeze({ | |
| capture, | |
| restore, | |
| remember, | |
| preserve, | |
| observe, | |
| unobserve, | |
| invalidate, | |
| dispose, | |
| whenIdle, | |
| get scrolling() { | |
| return scrolling; | |
| } | |
| }); | |
| } | |
| root.VoiceOfMLReaderScroll = Object.freeze({ createScrollAnchorManager }); | |
| })(typeof self !== "undefined" ? self : globalThis); | |