useZoom.ts 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { useCallback, useEffect, useRef, useState } from 'react';
  2. const defaultOptions: Options = {
  3. stepDown: (s) => s / 1.5,
  4. stepUp: (s) => s * 1.5,
  5. min: 0.13,
  6. max: 2.25,
  7. };
  8. interface Options {
  9. /**
  10. * Allows you to specify how the step up will be handled so you can do fractional steps based on previous value.
  11. */
  12. stepUp: (scale: number) => number;
  13. stepDown: (scale: number) => number;
  14. /**
  15. * Set max and min values. If stepUp/down overshoots these bounds this will return min or max but internal scale value
  16. * will still be what ever the step functions returned last.
  17. */
  18. min?: number;
  19. max?: number;
  20. }
  21. /**
  22. * Keeps state and returns handlers that can be used to implement zooming functionality ideally by using it with
  23. * 'transform: scale'. It returns handler for manual buttons with zoom in/zoom out function and a ref that can be
  24. * used to zoom in/out with mouse wheel.
  25. */
  26. export function useZoom({ stepUp, stepDown, min, max } = defaultOptions) {
  27. const ref = useRef<HTMLElement>(null);
  28. const [scale, setScale] = useState(1);
  29. const onStepUp = useCallback(() => {
  30. if (scale < (max ?? Infinity)) {
  31. setScale(stepUp(scale));
  32. }
  33. }, [scale, stepUp, max]);
  34. const onStepDown = useCallback(() => {
  35. if (scale > (min ?? -Infinity)) {
  36. setScale(stepDown(scale));
  37. }
  38. }, [scale, stepDown, min]);
  39. const onWheel = useCallback(
  40. function (event: Event) {
  41. // Seems like typing for the addEventListener is lacking a bit
  42. const wheelEvent = event as WheelEvent;
  43. // Only do this with special key pressed similar to how google maps work.
  44. // TODO: I would guess this won't work very well with touch right now
  45. if (wheelEvent.ctrlKey || wheelEvent.metaKey) {
  46. event.preventDefault();
  47. setScale(Math.min(Math.max(min ?? -Infinity, scale + Math.min(wheelEvent.deltaY, 2) * -0.01), max ?? Infinity));
  48. if (wheelEvent.deltaY < 0) {
  49. const newScale = scale + Math.max(wheelEvent.deltaY, -4) * -0.015;
  50. setScale(Math.max(min ?? -Infinity, newScale));
  51. } else if (wheelEvent.deltaY > 0) {
  52. const newScale = scale + Math.min(wheelEvent.deltaY, 4) * -0.015;
  53. setScale(Math.min(max ?? Infinity, newScale));
  54. }
  55. }
  56. },
  57. [min, max, scale]
  58. );
  59. useEffect(() => {
  60. if (!ref.current) {
  61. return;
  62. }
  63. const zoomRef = ref.current;
  64. // Adds listener for wheel event, we need the passive: false to be able to prevent default otherwise that
  65. // cannot be used with passive listeners.
  66. zoomRef.addEventListener('wheel', onWheel, { passive: false });
  67. return () => {
  68. if (zoomRef) {
  69. zoomRef.removeEventListener('wheel', onWheel);
  70. }
  71. };
  72. }, [onWheel]);
  73. return {
  74. onStepUp,
  75. onStepDown,
  76. scale: Math.max(Math.min(scale, max ?? Infinity), min ?? -Infinity),
  77. isMax: scale >= (max ?? Infinity),
  78. isMin: scale <= (min ?? -Infinity),
  79. ref,
  80. };
  81. }