import { useCallback, useEffect, useRef, useState } from 'react'; import { ZoomMode } from './panelcfg.gen'; const defaultOptions: Required = { stepUp: (s) => s * 1.5, stepDown: (s) => s / 1.5, min: 0.13, max: 2.25, zoomMode: ZoomMode.Cooperative, }; interface Options { /** * Allows you to specify how the step up will be handled so you can do fractional steps based on previous value. */ stepUp?: (scale: number) => number; stepDown?: (scale: number) => number; /** * Set max and min values. If stepUp/down overshoots these bounds this will return min or max but internal scale value * will still be what ever the step functions returned last. */ min?: number; max?: number; /** * Sets how to handle zoom events when user is interacting with the page */ zoomMode?: ZoomMode; } /** * Keeps state and returns handlers that can be used to implement zooming functionality ideally by using it with * 'transform: scale'. It returns handler for manual buttons with zoom in/zoom out function and a ref that can be * used to zoom in/out with mouse wheel. */ export function useZoom(options: Options = defaultOptions) { const { min, max, zoomMode } = { ...defaultOptions, ...options }; const stepUp = options.stepUp ?? defaultOptions.stepUp; const stepDown = options.stepDown ?? defaultOptions.stepDown; const ref = useRef(null); const [scale, setScale] = useState(1); const onStepUp = useCallback(() => { if (scale < (max ?? Infinity)) { setScale(stepUp(scale)); } }, [scale, stepUp, max]); const onStepDown = useCallback(() => { if (scale > (min ?? -Infinity)) { setScale(stepDown(scale)); } }, [scale, stepDown, min]); const onWheel = useCallback( function (wheelEvent: WheelEvent) { // Seems like typing for the addEventListener is lacking a bit // Only do this with special key pressed similar to how google maps work. // TODO: I would guess this won't work very well with touch right now if (wheelEvent.ctrlKey || wheelEvent.metaKey || zoomMode === ZoomMode.Greedy) { wheelEvent.preventDefault(); setScale(Math.min(Math.max(min ?? -Infinity, scale + Math.min(wheelEvent.deltaY, 2) * -0.01), max ?? Infinity)); if (wheelEvent.deltaY < 0) { const newScale = scale + Math.max(wheelEvent.deltaY, -4) * -0.015; setScale(Math.max(min ?? -Infinity, newScale)); } else if (wheelEvent.deltaY > 0) { const newScale = scale + Math.min(wheelEvent.deltaY, 4) * -0.015; setScale(Math.min(max ?? Infinity, newScale)); } } }, [min, max, scale, zoomMode] ); useEffect(() => { if (!ref.current) { return; } const zoomRef = ref.current; // Adds listener for wheel event, we need the passive: false to be able to prevent default otherwise that // cannot be used with passive listeners. zoomRef.addEventListener('wheel', onWheel, { passive: false }); return () => { if (zoomRef) { zoomRef.removeEventListener('wheel', onWheel); } }; }, [onWheel]); return { onStepUp, onStepDown, scale: Math.max(Math.min(scale, max ?? Infinity), min ?? -Infinity), isMax: scale >= (max ?? Infinity), isMin: scale <= (min ?? -Infinity), ref, }; }