@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
1 lines • 9.49 kB
Source Map (JSON)
{"version":3,"file":"useViewerGestures.mjs","names":[],"sources":["../../../src/Image/viewer/useViewerGestures.ts"],"sourcesContent":["import type { MouseEvent, PointerEvent } from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport type { Point } from './geometry';\nimport type { WheelLikeEvent } from './useZoomPan';\n\nconst DRAG_THRESHOLD = 4;\nconst DOUBLE_CLICK_WINDOW = 250;\n\nexport type ViewerCursor = 'grab' | 'grabbing' | 'zoom-out';\n\nexport interface UseViewerGesturesOptions {\n dragBy: (delta: Point) => void;\n dragEnd: () => void;\n handleDoubleClick: (point: Point) => void;\n handleWheel: (event: WheelLikeEvent) => void;\n isClean: boolean;\n isZoomed: boolean;\n onClose: () => void;\n reset: () => void;\n zoomIn: () => void;\n zoomOut: () => void;\n}\n\nexport interface UseViewerGesturesResult {\n cancelPendingClose: () => void;\n cursor: ViewerCursor;\n imageRef: (node: HTMLImageElement | null) => void;\n onImageClick: (event: MouseEvent<HTMLElement>) => void;\n onImageDoubleClick: (event: MouseEvent<HTMLElement>) => void;\n onPointerDown: (event: PointerEvent<HTMLElement>) => void;\n onPointerFinish: (event: PointerEvent<HTMLElement>) => void;\n onPointerMove: (event: PointerEvent<HTMLElement>) => void;\n onSurfaceClick: () => void;\n popupRef: (node: HTMLElement | null) => void;\n}\n\ninterface DragState {\n id: number;\n last: Point;\n panning: boolean;\n start: Point;\n}\n\nconst capturePointer = (node: Element, pointerId: number) => {\n if (typeof node.setPointerCapture === 'function') node.setPointerCapture(pointerId);\n};\n\nexport const isTypingTarget = (target: EventTarget | null) =>\n target instanceof HTMLElement &&\n (target.isContentEditable || ['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName));\n\nexport const useViewerGestures = ({\n dragBy,\n dragEnd,\n handleDoubleClick,\n handleWheel,\n isClean,\n isZoomed,\n onClose,\n reset,\n zoomIn,\n zoomOut,\n}: UseViewerGesturesOptions): UseViewerGesturesResult => {\n const [popup, setPopup] = useState<HTMLElement | null>(null);\n const [panning, setPanning] = useState(false);\n\n const imageNodeRef = useRef<HTMLImageElement | null>(null);\n const dragRef = useRef<DragState | null>(null);\n const movedRef = useRef(false);\n const pendingCloseRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const isCleanRef = useRef(isClean);\n isCleanRef.current = isClean;\n\n const cancelPendingClose = useCallback(() => {\n if (pendingCloseRef.current === null) return;\n clearTimeout(pendingCloseRef.current);\n pendingCloseRef.current = null;\n }, []);\n\n useEffect(() => cancelPendingClose, [cancelPendingClose]);\n\n const imageRef = useCallback((node: HTMLImageElement | null) => {\n imageNodeRef.current = node;\n }, []);\n\n useEffect(() => {\n if (!popup) return;\n const listener = (event: globalThis.WheelEvent) => handleWheel(event);\n popup.addEventListener('wheel', listener, { passive: false });\n return () => popup.removeEventListener('wheel', listener);\n }, [handleWheel, popup]);\n\n useEffect(() => {\n const listener = (event: KeyboardEvent) => {\n if (event.altKey || event.ctrlKey || event.metaKey || event.defaultPrevented) return;\n if (isTypingTarget(event.target)) return;\n switch (event.key) {\n case '+':\n case '=': {\n zoomIn();\n break;\n }\n case '-': {\n zoomOut();\n break;\n }\n case '0': {\n reset();\n break;\n }\n default: {\n return;\n }\n }\n event.preventDefault();\n };\n document.addEventListener('keydown', listener);\n return () => document.removeEventListener('keydown', listener);\n }, [reset, zoomIn, zoomOut]);\n\n const onPointerDown = useCallback(\n (event: PointerEvent<HTMLElement>) => {\n if (dragRef.current?.panning) return;\n if (event.button !== 0) return;\n cancelPendingClose();\n const start = { x: event.clientX, y: event.clientY };\n const panEligible = isZoomed && event.target === imageNodeRef.current;\n movedRef.current = false;\n dragRef.current = { id: event.pointerId, last: start, panning: panEligible, start };\n if (!panEligible) return;\n capturePointer(event.target as Element, event.pointerId);\n setPanning(true);\n },\n [cancelPendingClose, isZoomed],\n );\n\n const onPointerMove = useCallback(\n (event: PointerEvent<HTMLElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.id !== event.pointerId) return;\n const point = { x: event.clientX, y: event.clientY };\n if (Math.hypot(point.x - drag.start.x, point.y - drag.start.y) > DRAG_THRESHOLD) {\n movedRef.current = true;\n }\n if (drag.panning) dragBy({ x: point.x - drag.last.x, y: point.y - drag.last.y });\n drag.last = point;\n },\n [dragBy],\n );\n\n const onPointerFinish = useCallback(\n (event: PointerEvent<HTMLElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.id !== event.pointerId) return;\n dragRef.current = null;\n if (!drag.panning) return;\n setPanning(false);\n dragEnd();\n },\n [dragEnd],\n );\n\n const onSurfaceClick = useCallback(() => {\n if (movedRef.current) return;\n cancelPendingClose();\n onClose();\n }, [cancelPendingClose, onClose]);\n\n const onImageClick = useCallback(\n (event: MouseEvent<HTMLElement>) => {\n event.stopPropagation();\n cancelPendingClose();\n // Gated on cleanliness rather than zoom: an image that *opened* zoomed\n // (defaultZoom actual/auto) has been touched by nobody, so a plain click\n // should still dismiss it, exactly as Esc does.\n if (movedRef.current || !isCleanRef.current) return;\n pendingCloseRef.current = setTimeout(() => {\n pendingCloseRef.current = null;\n if (!isCleanRef.current) return;\n onClose();\n }, DOUBLE_CLICK_WINDOW);\n },\n [cancelPendingClose, onClose],\n );\n\n const onImageDoubleClick = useCallback(\n (event: MouseEvent<HTMLElement>) => {\n event.stopPropagation();\n cancelPendingClose();\n handleDoubleClick({ x: event.clientX, y: event.clientY });\n },\n [cancelPendingClose, handleDoubleClick],\n );\n\n return {\n cancelPendingClose,\n cursor: isZoomed ? (panning ? 'grabbing' : 'grab') : 'zoom-out',\n imageRef,\n onImageClick,\n onImageDoubleClick,\n onPointerDown,\n onPointerFinish,\n onPointerMove,\n onSurfaceClick,\n popupRef: setPopup,\n };\n};\n"],"mappings":"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"}