Tom Butcher cc1048fcaa
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Update App.css and PDFViewer.jsx for Enhanced Styling and Code Clarity
- 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.
2026-08-22 14:18:39 +01:00

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