export function PagePreview()

in frontend/src/js/components/viewer/PageViewer/PagePreview.tsx [51:145]


export function PagePreview({ uri, page, onHighlightMount, currentHighlightId, q }: Props) {
    const [pdfPage, setPdfPage] = useState<PDFPageProxy | undefined>(undefined);
    const [text, setText] = useState<PDFText[]>([]);
    const {width: pageWidth, height: pageHeight} = page.dimensions;

    const canvasRef = useCallback(async canvasEl => {
        if (canvasEl && pdfPage) {
            // If pdf.js gets called twice with the same canvas element, it throws an error:
            // https://github.com/mozilla/pdf.js/blob/d3f7959689b9ccd9b292ccebf73377370c64268a/src/display/api.js#L2867-L2871
            //
            // here's the PR where that logic was introduced (2017):
            // mozilla/pdf.js#8519
            //
            // here are some issues related to it:
            // mozilla/pdf.js#9456 (opened Feb 2018, closed Oct 2018)
            // mozilla/pdf.js#10018 (opened Aug 2018, still open)
            //
            // So, force a fresh canvas before calling rasterisePage.
            // Use cloneNode() to preserve the width & height attributes set on the canvas by React.
            const newCanvasEl = canvasEl.cloneNode();
            canvasEl.replaceWith(newCanvasEl);
            await rasterisePage(newCanvasEl, pdfPage, pageWidth, pageHeight);
        }
    }, [pdfPage, pageWidth, pageHeight]);

    // I suspect the fact that this useEffect has so many deps is a sign that
    // this data fetching should be done in the parent component.
    // PagePreview could just accept the pdfPage.
    //
    // Interesting prior art for data fetching with hooks:
    // https://www.robinwieruch.de/react-hooks-fetch-data
    // https://codesandbox.io/s/jvvkoo8pq3?file=/src/index.js:513-518
    //
    // And someday soon this might make it all saner:
    // https://reactjs.org/docs/concurrent-mode-suspense.html
    useEffect(() => {
        // TODO MRB: handle errors
        (async function () {
            const pdfPage = await fetchPagePreview(page.currentLanguage, uri, page.page, q).then(parsePDF);
            setPdfPage(pdfPage);

            const text = await renderPDFText(pdfPage, pageWidth, pageHeight);
            setText(text);
        })();
    }, [page.currentLanguage, uri, page.page, pageWidth, pageHeight, q]);

    const style = { width: pageWidth, height: pageHeight, top: page.dimensions.top };

    // TODO MRB: just use the text divs created by PDF.js directly via dangerouslySetInnerHTML?
    const textElements = text.map(({ value, left, top, fontSize, fontFamily, transform }, ix) => {
        return <div
            key={ix}
            className='pfi-page__pdf-text'
            style={{left, top, fontSize, fontFamily, transform}}
        >{value}</div>;
    });

    const highlightElements = page.highlights.flatMap(highlight => {
        switch(highlight.type) {
            case 'SearchHighlight':
                return highlight.data.map(highlightSpan => (
                    <PageHighlightWrapper
                        key={highlight.id}
                        highlightId={highlight.id}
                        text={''}
                        focused={highlight.id === currentHighlightId}
                        onHighlightMount={onHighlightMount}
                        style={{
                            display: 'block',
                            position: 'absolute',
                            left: highlightSpan.x,
                            top: highlightSpan.y,
                            width: highlightSpan.width,
                            height: highlightSpan.height,
                            transformOrigin: 'top left',
                            transform: `rotate(${highlightSpan.rotation}rad)`,
                            pointerEvents: 'none'
                        }}
                    />
                ));

            // Without this ESLint complains about not returning a value from the arrow function,
            // presumably because it doesn't understand my switch is exhaustive (does TS even do that?)
            default:
                return [];
        }
    });

    return <div className='pfi-page' style={style}>
        <canvas ref={canvasRef} width={pageWidth} height={pageHeight}/>
        {textElements}
        {highlightElements}
    </div>;

}