diff --git a/src/anchor/pdf-viewer-adapter-spike.tsx b/src/anchor/pdf-viewer-adapter-spike.tsx index 5f9b967..8578289 100644 --- a/src/anchor/pdf-viewer-adapter-spike.tsx +++ b/src/anchor/pdf-viewer-adapter-spike.tsx @@ -14,7 +14,15 @@ * will build the real PDFViewerAdapter on top of this lessons-learned. */ -import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + type ReactNode, +} from "react"; import { PdfHighlighter, PdfLoader, @@ -116,39 +124,38 @@ function captureFromPdfSelection(sel: PdfSelection): PdfSelectionCapture { }; } -/** - * Trivial container that renders every stored highlight as a TextHighlight. - * For the spike, no editing tooling — just visual proof of "did the saved - * coordinates land on the right passage on the right page after reload?" - */ -interface HighlightContainerProps { - readonly activeAnnotationId: string | null | undefined; - readonly onHighlightClicked: ((annotationId: string) => void) | undefined; -} +const ActiveAnnotationContext = createContext( + undefined, +); +const HighlightClickContext = createContext<((annotationId: string) => void) | undefined>( + undefined, +); -function makeSpikeHighlightContainer(props: HighlightContainerProps) { - return function SpikeHighlightContainer(): ReactNode { - const { highlight, isScrolledTo } = useHighlightContainerContext(); - const isActive = props.activeAnnotationId === highlight.id; - return ( -
{ - // Click-capture so the click registers before the library's - // built-in selection-clearing logic eats it. Stop propagation - // so the highlight click doesn't also start a new selection. - e.stopPropagation(); - props.onHighlightClicked?.(highlight.id); - }} - > - - - -
- ); - }; +/** + * Stable highlight row — component type never changes so PdfHighlighter does + * not remount highlight layers on activation changes (which disturbs scroll). + * Active/focus styling reads from context instead. + */ +function SpikeHighlightContainer(): ReactNode { + const activeAnnotationId = useContext(ActiveAnnotationContext); + const onHighlightClicked = useContext(HighlightClickContext); + const { highlight, isScrolledTo } = useHighlightContainerContext(); + const isActive = activeAnnotationId === highlight.id; + return ( +
{ + e.stopPropagation(); + onHighlightClicked?.(highlight.id); + }} + > + + + +
+ ); } /** @@ -279,9 +286,21 @@ export function PdfSpikeViewer(props: PdfSpikeViewerProps) { hideAnnotationLayer, hideXfaLayer, } = props; - const HighlightContainer = useMemo( - () => makeSpikeHighlightContainer({ activeAnnotationId, onHighlightClicked }), - [activeAnnotationId, onHighlightClicked], + const onHighlightClickedRef = useRef(onHighlightClicked); + onHighlightClickedRef.current = onHighlightClicked; + const handleHighlightClicked = useCallback((annotationId: string) => { + onHighlightClickedRef.current?.(annotationId); + }, []); + const pdfLoaderDocument = useMemo( + () => ({ + url: pdfUrl, + // PdfLoader's effect depends on `document` by reference — must be + // stable across re-renders or the PDF reloads and scroll resets to top. + cMapUrl: "/cmaps/", + cMapPacked: true, + standardFontDataUrl: "/standard_fonts/", + }), + [pdfUrl], ); const wrapperClasses = [ debugTextLayer ? "ce-debug-textlayer" : null, @@ -322,6 +341,17 @@ export function PdfSpikeViewer(props: PdfSpikeViewerProps) { const highlightsRef = useRef(highlights); highlightsRef.current = highlights; + const highlightsSignature = useMemo( + () => highlights.map((h) => h.id).join(","), + [highlights], + ); + + // Re-render highlight layers when focus moves so `data-ce-active` updates. + const highlightsForViewer = useMemo( + () => highlights, + [highlights, activeAnnotationId], + ); + useEffect(() => { const requestKey = scrollRequestKey ?? scrollToAnnotationId ?? null; if (!requestKey || !scrollToAnnotationId) return; @@ -348,51 +378,42 @@ export function PdfSpikeViewer(props: PdfSpikeViewerProps) { }, scrollStateRef.current, ); - }, [scrollToAnnotationId, scrollRequestKey, highlights, debugTextLayer]); + }, [scrollToAnnotationId, scrollRequestKey, highlightsSignature, debugTextLayer]); return (
0 ? wrapperClasses : undefined} style={{ height: "100%" }} > - + {(pdfDocument) => ( - { - utilsRef.current = u; - }} - onSelection={(selection) => { - const capture = captureFromPdfSelection(selection); - const selectors = selectorsFromPdfCapture(capture); - if (debugTextLayer) { - console.log("[ce] onSelection", { - text: capture.text, - page: capture.page, - rects: capture.rects, - selectorTypes: selectors.map((s) => s.type), - raw: selection, - }); - } - onSelectionCaptured(capture, selectors); - }} - > - - + + + { + utilsRef.current = u; + }} + onSelection={(selection) => { + const capture = captureFromPdfSelection(selection); + const selectors = selectorsFromPdfCapture(capture); + if (debugTextLayer) { + console.log("[ce] onSelection", { + text: capture.text, + page: capture.page, + rects: capture.rects, + selectorTypes: selectors.map((s) => s.type), + raw: selection, + }); + } + onSelectionCaptured(capture, selectors); + }} + > + + + + )}