import { useCallback, useEffect, useRef } from 'react' export const MIN_PREVIEW_SCALE = 0.1 export const MAX_PREVIEW_SCALE = 5 const isPinchZoomEvent = (event) => event.ctrlKey || event.metaKey export const clampPreviewScale = (value) => { if (!Number.isFinite(value)) { return 1 } const clamped = Math.min(MAX_PREVIEW_SCALE, Math.max(MIN_PREVIEW_SCALE, value)) return Math.round(clamped * 10000) / 10000 } const nextScaleFromWheel = (scale, event) => { const intensity = event.deltaMode === 1 ? 0.12 : 0.0045 return clampPreviewScale(scale * Math.exp(-event.deltaY * intensity)) } const eventClientPoint = (event, iframeRef, scale) => { const iframe = iframeRef?.current const eventDoc = event.target?.ownerDocument if ( iframe && eventDoc && eventDoc !== document && eventDoc === iframe.contentDocument ) { const rect = iframe.getBoundingClientRect() return { clientX: rect.left + event.clientX * scale, clientY: rect.top + event.clientY * scale } } return { clientX: event.clientX, clientY: event.clientY } } const usePinchZoom = ({ containerRef, iframeRef, iframeBindKey, scale = 1, onScaleChange, onBeforeScaleChange, enabled = true }) => { const onScaleChangeRef = useRef(onScaleChange) onScaleChangeRef.current = onScaleChange const onBeforeScaleChangeRef = useRef(onBeforeScaleChange) onBeforeScaleChangeRef.current = onBeforeScaleChange const scaleRef = useRef(scale) scaleRef.current = scale const gestureStartScaleRef = useRef(scale) const enabledRef = useRef(enabled) enabledRef.current = enabled const handleWheel = useCallback( (event) => { if (!enabledRef.current || !isPinchZoomEvent(event)) { return } if (event.target?.closest?.('.previewZoomOverlay')) { return } event.preventDefault() event.stopPropagation() const current = scaleRef.current const next = nextScaleFromWheel(current, event) if (next === current) { return } const point = eventClientPoint(event, iframeRef, current) onBeforeScaleChangeRef.current?.(point.clientX, point.clientY) onScaleChangeRef.current?.(next) }, [iframeRef] ) const handleGestureStart = useCallback((event) => { if (!enabledRef.current) { return } event.preventDefault() gestureStartScaleRef.current = scaleRef.current }, []) const handleGestureChange = useCallback( (event) => { if (!enabledRef.current) { return } event.preventDefault() const startScale = gestureStartScaleRef.current if (!Number.isFinite(startScale)) { return } const next = clampPreviewScale(startScale * event.scale) if (next === scaleRef.current) { return } const point = eventClientPoint(event, iframeRef, scaleRef.current) onBeforeScaleChangeRef.current?.(point.clientX, point.clientY) onScaleChangeRef.current?.(next) }, [iframeRef] ) useEffect(() => { const container = containerRef?.current if (!container || !enabled || typeof onScaleChange !== 'function') { return undefined } const options = { capture: true, passive: false } container.addEventListener('wheel', handleWheel, options) container.addEventListener('gesturestart', handleGestureStart, options) container.addEventListener('gesturechange', handleGestureChange, options) return () => { container.removeEventListener('wheel', handleWheel, options) container.removeEventListener('gesturestart', handleGestureStart, options) container.removeEventListener( 'gesturechange', handleGestureChange, options ) } }, [ containerRef, enabled, handleGestureChange, handleGestureStart, handleWheel, onScaleChange ]) useEffect(() => { const iframe = iframeRef?.current if (!iframe || !enabled || typeof onScaleChange !== 'function') { return undefined } const options = { capture: true, passive: false } let boundDoc = null const unbind = () => { if (!boundDoc) { return } boundDoc.removeEventListener('wheel', handleWheel, options) boundDoc.removeEventListener('gesturestart', handleGestureStart, options) boundDoc.removeEventListener( 'gesturechange', handleGestureChange, options ) boundDoc = null } const bind = () => { const doc = iframe.contentDocument if (!doc || boundDoc === doc) { return } unbind() doc.addEventListener('wheel', handleWheel, options) doc.addEventListener('gesturestart', handleGestureStart, options) doc.addEventListener('gesturechange', handleGestureChange, options) boundDoc = doc } iframe.addEventListener('load', bind) bind() return () => { iframe.removeEventListener('load', bind) unbind() } }, [ enabled, handleGestureChange, handleGestureStart, handleWheel, iframeBindKey, iframeRef, onScaleChange ]) } export default usePinchZoom