Просмотр исходного кода

Merge pull request #19239 from overleaf/dp-scroll-wheel-zoom

Implement pdf mouse wheel zooming

GitOrigin-RevId: d69570bb5a54970072d6f602792ddb1159343423
David 2 лет назад
Родитель
Сommit
4fe66c0189

+ 3 - 0
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,

+ 89 - 0
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])
+}