- Added usePinchZoom and usePreserveZoomScroll hooks to enable pinch zoom functionality and maintain scroll position during zooming in PDFViewer and TemplatePreview components. - Introduced a new clampPreviewScale function to ensure zoom levels remain within defined limits. - Enhanced TemplatePreview with a zoom slider and overlay for better user interaction. - Updated PDFViewer to handle scale changes and improve rendering performance. - Refactored related components to support the new zooming features, enhancing overall user experience.
194 lines
5.1 KiB
JavaScript
194 lines
5.1 KiB
JavaScript
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
|