diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx index 01650346bb..bc34e66678 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx @@ -16,6 +16,7 @@ import { debugConsole } from '@/utils/debugging' import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider' import { useFeatureFlag } from '@/shared/context/split-test-context' import usePresentationMode from '../hooks/use-presentation-mode' +import useMouseWheelZoom from '../hooks/use-mouse-wheel-zoom' type PdfJsViewerProps = { url: string @@ -467,6 +468,8 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) { [initialised, setZoom] ) + useMouseWheelZoom(pdfJsWrapper, setScale) + const requestPresentationMode = usePresentationMode( pdfJsWrapper, page, diff --git a/services/web/frontend/js/features/pdf-preview/hooks/use-mouse-wheel-zoom.ts b/services/web/frontend/js/features/pdf-preview/hooks/use-mouse-wheel-zoom.ts new file mode 100644 index 0000000000..4fe09b711b --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/hooks/use-mouse-wheel-zoom.ts @@ -0,0 +1,89 @@ +import { useCallback, useEffect, useRef } from 'react' +import PDFJSWrapper from '../util/pdf-js-wrapper' +import { useFeatureFlag } from '@/shared/context/split-test-context' + +// We need this to work for both a traditional mouse wheel and a touchpad "pinch to zoom". +// From experimentation, trackpads tend to fire a lot of events with small deltaY's where +// as a mouse wheel will fire fewer events but sometimes with a very high deltaY if you +// move the wheel quickly. +// The divisor is set to a value that works for the trackpad with the maximum value ensuring +// that the scale doesn't suddenly change drastically from moving the mouse wheel quickly. +const MAX_SCALE_FACTOR = 1.2 +const SCALE_FACTOR_DIVISOR = 20 + +export default function useMouseWheelZoom( + pdfJsWrapper: PDFJSWrapper | null | undefined, + setScale: (scale: string) => void +) { + const isEnabled = useFeatureFlag('pdf-controls') + + const isZoomingRef = useRef(false) + + const performZoom = useCallback( + (event: WheelEvent, pdfJsWrapper: PDFJSWrapper) => { + // First, we calculate and set the new scale + const scrollMagnitude = Math.abs(event.deltaY) + const scaleFactorMagnitude = Math.min( + 1 + scrollMagnitude / SCALE_FACTOR_DIVISOR, + MAX_SCALE_FACTOR + ) + const previousScale = pdfJsWrapper.viewer.currentScale + const scaleChangeDirection = Math.sign(event.deltaY) + + const approximateScaleFactor = + scaleChangeDirection < 0 + ? scaleFactorMagnitude + : 1 / scaleFactorMagnitude + + const newScale = + Math.round(previousScale * approximateScaleFactor * 100) / 100 + const exactScaleFactor = newScale / previousScale + + // Set the scale directly to ensure it is set before we do the scrolling below + pdfJsWrapper.viewer.currentScale = newScale + setScale(`${newScale}`) + + // Then we need to ensure we are centering the zoom on the mouse position + const containerRect = pdfJsWrapper.container.getBoundingClientRect() + const top = containerRect.top + const left = containerRect.left + + // Positions relative to pdf viewer + const currentMouseX = event.clientX - left + const currentMouseY = event.clientY - top + + pdfJsWrapper.container.scrollBy({ + left: currentMouseX * exactScaleFactor - currentMouseX, + top: currentMouseY * exactScaleFactor - currentMouseY, + behavior: 'instant', + }) + }, + [setScale] + ) + + useEffect(() => { + if (pdfJsWrapper && isEnabled) { + const wheelListener = (event: WheelEvent) => { + if (event.metaKey || event.ctrlKey) { + event.preventDefault() + + if (!isZoomingRef.current) { + isZoomingRef.current = true + + performZoom(event, pdfJsWrapper) + + setTimeout(() => { + isZoomingRef.current = false + }, 5) + } + } + } + + pdfJsWrapper.container.addEventListener('wheel', wheelListener) + + return () => { + pdfJsWrapper.container.removeEventListener('wheel', wheelListener) + } + } + }, [pdfJsWrapper, setScale, isEnabled, performZoom]) +}