import { useCallback, useEffect, useRef, useState } from 'react' import PropTypes from 'prop-types' import { Document, Page, pdfjs } from 'react-pdf' import pdfWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url' import 'react-pdf/dist/Page/AnnotationLayer.css' import 'react-pdf/dist/Page/TextLayer.css' import LoadingPlaceholder from './LoadingPlaceholder.jsx' import ScrollBox from './ScrollBox.jsx' pdfjs.GlobalWorkerOptions.workerSrc = pdfWorker const noop = () => {} const PDFViewer = ({ file, scale = 1, panMode = false, loading = false, onLoadSuccess = noop, onLoadError = noop }) => { const scrollElementRef = useRef(null) const panStartRef = useRef({ x: 0, y: 0, scrollLeft: 0, scrollTop: 0 }) const pdfDocumentRef = useRef(null) const prevFileRef = useRef(file) const documentKeyRef = useRef(0) const [numPages, setNumPages] = useState(0) const [isPanning, setIsPanning] = useState(false) const [hasLoadError, setHasLoadError] = useState(false) const activeFile = loading == true ? null : file if (prevFileRef.current !== activeFile) { prevFileRef.current = activeFile documentKeyRef.current += 1 } const destroyPdfDocument = useCallback(() => { const pdf = pdfDocumentRef.current pdfDocumentRef.current = null if (pdf && typeof pdf.destroy === 'function') { pdf.destroy() } }, []) const handleDocumentLoadSuccess = useCallback( (pdf) => { pdfDocumentRef.current = pdf setHasLoadError(false) setNumPages(pdf.numPages) onLoadSuccess(pdf) }, [onLoadSuccess] ) const handleDocumentLoadError = useCallback( (error) => { destroyPdfDocument() setHasLoadError(true) setNumPages(0) onLoadError(error) }, [destroyPdfDocument, onLoadError] ) useEffect(() => { setNumPages(0) setHasLoadError(false) return () => { destroyPdfDocument() } }, [activeFile, destroyPdfDocument]) useEffect(() => { if (!panMode) { setIsPanning(false) } }, [panMode]) useEffect(() => { const scrollEl = scrollElementRef.current if (!panMode || !scrollEl) { return } const handlePointerDown = (event) => { if (event.pointerType === 'mouse' && event.button !== 0) { return } event.preventDefault() panStartRef.current = { x: event.clientX, y: event.clientY, scrollLeft: scrollEl.scrollLeft, scrollTop: scrollEl.scrollTop } setIsPanning(true) } scrollEl.addEventListener('pointerdown', handlePointerDown) return () => { scrollEl.removeEventListener('pointerdown', handlePointerDown) } }, [panMode, activeFile]) useEffect(() => { if (!isPanning) { return } const previousCursor = document.body.style.cursor const previousUserSelect = document.body.style.userSelect document.body.style.cursor = 'grabbing' document.body.style.userSelect = 'none' const handlePointerMove = (event) => { const scrollEl = scrollElementRef.current if (!scrollEl) { return } const { x, y, scrollLeft, scrollTop } = panStartRef.current scrollEl.scrollLeft = scrollLeft - (event.clientX - x) scrollEl.scrollTop = scrollTop - (event.clientY - y) } const handlePointerUp = () => { setIsPanning(false) } window.addEventListener('pointermove', handlePointerMove) window.addEventListener('pointerup', handlePointerUp) window.addEventListener('pointercancel', handlePointerUp) return () => { document.body.style.cursor = previousCursor document.body.style.userSelect = previousUserSelect window.removeEventListener('pointermove', handlePointerMove) window.removeEventListener('pointerup', handlePointerUp) window.removeEventListener('pointercancel', handlePointerUp) } }, [isPanning]) const isDocumentLoading = Boolean(activeFile) && numPages === 0 && hasLoadError != true const showLoadingPlaceholder = loading == true || isDocumentLoading return (
{showLoadingPlaceholder ? (
) : null} {activeFile ? (
Failed to load PDF.
} onLoadSuccess={handleDocumentLoadSuccess} onLoadError={handleDocumentLoadError} >
{Array.from(new Array(numPages), (_el, index) => ( ))}
) : null} ) } PDFViewer.propTypes = { file: PropTypes.oneOfType([ PropTypes.string, PropTypes.instanceOf(Blob), PropTypes.instanceOf(ArrayBuffer), PropTypes.object ]), scale: PropTypes.number, panMode: PropTypes.bool, loading: PropTypes.bool, onLoadSuccess: PropTypes.func, onLoadError: PropTypes.func } export default PDFViewer