All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Modified background color in App.css to use color-mix for improved visual effects. - Added border and box-shadow to .react-pdf__Page class for better PDF presentation. - Simplified conditional styling in PDFViewer.jsx for cleaner code structure.
240 lines
6.6 KiB
JavaScript
240 lines
6.6 KiB
JavaScript
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 (
|
|
<div
|
|
style={{
|
|
height: '100%',
|
|
minHeight: 0,
|
|
overflow: 'visible',
|
|
position: 'relative'
|
|
}}
|
|
>
|
|
{showLoadingPlaceholder ? (
|
|
<div
|
|
style={{
|
|
height: '100%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
...(activeFile ? { position: 'absolute', inset: 0, zIndex: 1 } : {})
|
|
}}
|
|
>
|
|
<LoadingPlaceholder message={'Loading PDF...'} />
|
|
</div>
|
|
) : null}
|
|
{activeFile ? (
|
|
<ScrollBox
|
|
scrollableNodeProps={{ ref: scrollElementRef }}
|
|
style={{
|
|
cursor: panMode ? (isPanning ? 'grabbing' : 'grab') : undefined,
|
|
touchAction: panMode ? 'none' : undefined
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
padding: 60,
|
|
width: 'max-content',
|
|
minWidth: '100%',
|
|
cursor: panMode ? (isPanning ? 'grabbing' : 'grab') : undefined,
|
|
pointerEvents: panMode ? 'none' : undefined
|
|
}}
|
|
>
|
|
<Document
|
|
key={documentKeyRef.current}
|
|
file={activeFile}
|
|
loading={null}
|
|
error={
|
|
<div style={{ color: 'var(--color-error, #ff4d4f)' }}>
|
|
Failed to load PDF.
|
|
</div>
|
|
}
|
|
onLoadSuccess={handleDocumentLoadSuccess}
|
|
onLoadError={handleDocumentLoadError}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
justifyContent: 'center',
|
|
gap: 32,
|
|
width: 'max-content',
|
|
minWidth: '100%'
|
|
}}
|
|
>
|
|
{Array.from(new Array(numPages), (_el, index) => (
|
|
<Page
|
|
key={`page_${index + 1}`}
|
|
pageNumber={index + 1}
|
|
scale={scale}
|
|
renderTextLayer={panMode != true}
|
|
renderAnnotationLayer={panMode != true}
|
|
/>
|
|
))}
|
|
</div>
|
|
</Document>
|
|
</div>
|
|
</ScrollBox>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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
|