From 258244f368923bfe972db74eaaf155ebbe02f101 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 16 Aug 2023 10:51:55 +0200 Subject: [PATCH] fix(move-events): fire for panOnScroll --- examples/vite-app/package.json | 2 +- .../vite-app/src/examples/Figma/index.tsx | 14 ++++- packages/system/src/xypanzoom/XYPanZoom.ts | 8 +++ packages/system/src/xypanzoom/eventhandler.ts | 57 ++++++++++++++---- pnpm-lock.yaml | 60 +++++++++---------- 5 files changed, 93 insertions(+), 48 deletions(-) diff --git a/examples/vite-app/package.json b/examples/vite-app/package.json index 3e2e707a..6d7c5987 100644 --- a/examples/vite-app/package.json +++ b/examples/vite-app/package.json @@ -21,7 +21,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.3.0", - "zustand": "^4.3.1" + "zustand": "^4.4.1" }, "devDependencies": { "@cypress/skip-test": "^2.6.1", diff --git a/examples/vite-app/src/examples/Figma/index.tsx b/examples/vite-app/src/examples/Figma/index.tsx index bccafa96..6780b6cb 100644 --- a/examples/vite-app/src/examples/Figma/index.tsx +++ b/examples/vite-app/src/examples/Figma/index.tsx @@ -1,4 +1,4 @@ -import { ReactFlow, Background, BackgroundVariant, Node, Edge, SelectionMode } from '@xyflow/react'; +import { ReactFlow, Background, BackgroundVariant, Node, Edge, SelectionMode, Controls, Panel } from '@xyflow/react'; const MULTI_SELECT_KEY = ['Meta', 'Shift']; @@ -21,6 +21,10 @@ const onPaneContextMenu = (e: any) => { const panOnDrag = [1, 2]; +const onMoveStart = (e: any) => console.log('move start', e); +const onMove = (e: any) => console.log('move', e); +const onMoveEnd = (e: any) => console.log('move end', e); + const BasicFlow = () => { return ( { fitView selectNodesOnDrag={false} onSelectionContextMenu={onPaneContextMenu} + onMoveStart={onMoveStart} + onMove={onMove} + onMoveEnd={onMoveEnd} > -
+ + -
+
); }; diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts index 1faded97..f5a3e5fe 100644 --- a/packages/system/src/xypanzoom/XYPanZoom.ts +++ b/packages/system/src/xypanzoom/XYPanZoom.ts @@ -26,6 +26,8 @@ export type ZoomPanValues = { prevViewport: Viewport; mouseButton: number; timerId: ReturnType | undefined; + panScrollTimeout: ReturnType | undefined; + isPanScrolling: boolean; }; export function XYPanZoom({ @@ -46,6 +48,8 @@ export function XYPanZoom({ prevViewport: { x: 0, y: 0, zoom: 0 }, mouseButton: 0, timerId: undefined, + panScrollTimeout: undefined, + isPanScrolling: false, }; const bbox = domNode.getBoundingClientRect(); const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent); @@ -98,12 +102,16 @@ export function XYPanZoom({ const wheelHandler = isPanOnScroll ? createPanOnScrollHandler({ + zoomPanValues, noWheelClassName, d3Selection, d3Zoom: d3ZoomInstance, panOnScrollMode, panOnScrollSpeed, zoomOnPinch, + onPanZoomStart, + onPanZoom, + onPanZoomEnd, }) : createZoomOnScrollHandler({ noWheelClassName, diff --git a/packages/system/src/xypanzoom/eventhandler.ts b/packages/system/src/xypanzoom/eventhandler.ts index b2b46b48..456e2332 100644 --- a/packages/system/src/xypanzoom/eventhandler.ts +++ b/packages/system/src/xypanzoom/eventhandler.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-ts-comment */ import type { D3ZoomEvent } from 'd3-zoom'; import { pointer } from 'd3-selection'; @@ -8,28 +9,24 @@ import { type D3ZoomHandler, type D3ZoomInstance, type OnPanZoom, - type Viewport, type OnDraggingChange, type OnTransformChange, } from '../types'; import { isRightClickPan, isWrappedWithClass, transformToViewport, viewChanged, wheelDelta } from './utils'; -import { clamp, isMacOs } from '../utils'; - -export type ZoomPanValues = { - isZoomingOrPanning: boolean; - usedRightMouseButton: boolean; - prevViewport: Viewport; - mouseButton: number; - timerId: ReturnType | undefined; -}; +import { isMacOs } from '../utils'; +import { ZoomPanValues } from './XYPanZoom'; export type PanOnScrollParams = { + zoomPanValues: ZoomPanValues; noWheelClassName: string; d3Selection: D3SelectionInstance; d3Zoom: D3ZoomInstance; panOnScrollMode: PanOnScrollMode; panOnScrollSpeed: number; zoomOnPinch: boolean; + onPanZoomStart?: OnPanZoom; + onPanZoom?: OnPanZoom; + onPanZoomEnd?: OnPanZoom; }; export type ZoomOnScrollParams = { @@ -62,12 +59,16 @@ export type PanZoomEndParams = { }; export function createPanOnScrollHandler({ + zoomPanValues, noWheelClassName, d3Selection, d3Zoom, panOnScrollMode, panOnScrollSpeed, zoomOnPinch, + onPanZoomStart, + onPanZoom, + onPanZoomEnd, }: PanOnScrollParams) { return (event: any) => { if (isWrappedWithClass(event, noWheelClassName)) { @@ -105,8 +106,33 @@ export function createPanOnScrollHandler({ d3Zoom.translateBy( d3Selection, -(deltaX / currentZoom) * panOnScrollSpeed, - -(deltaY / currentZoom) * panOnScrollSpeed + -(deltaY / currentZoom) * panOnScrollSpeed, + // @ts-ignore + { internal: true } ); + + const nextViewport = transformToViewport(d3Selection.property('__zoom')); + + clearTimeout(zoomPanValues.panScrollTimeout); + + // for pan on scroll we need to handle the event calls on our own + // we can't use the start, zoom and end events from d3-zoom + // because start and move gets called on every scroll event and not once at the beginning + if (!zoomPanValues.isPanScrolling) { + zoomPanValues.isPanScrolling = true; + + onPanZoomStart?.(event, nextViewport); + } + + if (zoomPanValues.isPanScrolling) { + onPanZoom?.(event, nextViewport); + + zoomPanValues.panScrollTimeout = setTimeout(() => { + onPanZoomEnd?.(event, nextViewport); + + zoomPanValues.isPanScrolling = false; + }, 150); + } }; } @@ -124,6 +150,10 @@ export function createZoomOnScrollHandler({ noWheelClassName, preventScrolling, export function createPanZoomStartHandler({ zoomPanValues, onDraggingChange, onPanZoomStart }: PanZoomStartParams) { return (event: D3ZoomEvent) => { + if (event.sourceEvent?.internal) { + return; + } + const viewport = transformToViewport(event.transform); // we need to remember it here, because it's always 0 in the "zoom" event @@ -155,7 +185,7 @@ export function createPanZoomHandler({ onTransformChange([event.transform.x, event.transform.y, event.transform.k]); - if (onPanZoom) { + if (onPanZoom && !event.sourceEvent?.internal) { onPanZoom?.(event.sourceEvent as MouseEvent | TouchEvent, transformToViewport(event.transform)); } }; @@ -170,6 +200,9 @@ export function createPanZoomEndHandler({ onPaneContextMenu, }: PanZoomEndParams) { return (event: D3ZoomEvent) => { + if (event.sourceEvent?.internal) { + return; + } zoomPanValues.isZoomingOrPanning = false; if ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1b4d1c3..d63f897b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,8 +99,8 @@ importers: specifier: ^6.3.0 version: registry.npmjs.org/react-router-dom@6.12.1(react-dom@18.2.0)(react@18.2.0) zustand: - specifier: ^4.3.1 - version: registry.npmjs.org/zustand@4.3.8(react@18.2.0) + specifier: ^4.4.1 + version: registry.npmjs.org/zustand@4.4.1(@types/react@18.2.12)(react@18.2.0) devDependencies: '@cypress/skip-test': specifier: ^2.6.1 @@ -2421,7 +2421,7 @@ packages: '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree@5.60.0(typescript@5.1.3) eslint: registry.npmjs.org/eslint@8.43.0 eslint-scope: registry.npmjs.org/eslint-scope@5.1.1 - semver: registry.npmjs.org/semver@7.5.1 + semver: registry.npmjs.org/semver@7.5.4 transitivePeerDependencies: - supports-color - typescript @@ -4140,7 +4140,7 @@ packages: dependencies: acorn: registry.npmjs.org/acorn@8.8.2 acorn-jsx: registry.npmjs.org/acorn-jsx@5.3.2(acorn@8.8.2) - eslint-visitor-keys: registry.npmjs.org/eslint-visitor-keys@3.4.1 + eslint-visitor-keys: registry.npmjs.org/eslint-visitor-keys@3.4.3 dev: true registry.npmjs.org/esprima@4.0.1: @@ -5076,14 +5076,6 @@ packages: ci-info: registry.npmjs.org/ci-info@3.8.0 dev: true - registry.npmjs.org/is-core-module@2.12.1: - resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz} - name: is-core-module - version: 2.12.1 - dependencies: - has: registry.npmjs.org/has@1.0.3 - dev: true - registry.npmjs.org/is-core-module@2.13.0: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz} name: is-core-module @@ -6772,7 +6764,7 @@ packages: version: 1.22.2 hasBin: true dependencies: - is-core-module: registry.npmjs.org/is-core-module@2.12.1 + is-core-module: registry.npmjs.org/is-core-module@2.13.0 path-parse: registry.npmjs.org/path-parse@1.0.7 supports-preserve-symlinks-flag: registry.npmjs.org/supports-preserve-symlinks-flag@1.0.0 dev: true @@ -8338,25 +8330,6 @@ packages: engines: {node: '>=10'} dev: true - registry.npmjs.org/zustand@4.3.8(react@18.2.0): - resolution: {integrity: sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz} - id: registry.npmjs.org/zustand/4.3.8 - name: zustand - version: 4.3.8 - engines: {node: '>=12.7.0'} - peerDependencies: - immer: '>=9.0' - react: '>=16.8' - peerDependenciesMeta: - immer: - optional: true - react: - optional: true - dependencies: - react: registry.npmjs.org/react@18.2.0 - use-sync-external-store: registry.npmjs.org/use-sync-external-store@1.2.0(react@18.2.0) - dev: false - registry.npmjs.org/zustand@4.4.0(@types/react@18.2.12)(react@18.2.0): resolution: {integrity: sha512-2dq6wq4dSxbiPTamGar0NlIG/av0wpyWZJGeQYtUOLegIUvhM2Bf86ekPlmgpUtS5uR7HyetSiktYrGsdsyZgQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/zustand/-/zustand-4.4.0.tgz} id: registry.npmjs.org/zustand/4.4.0 @@ -8379,3 +8352,26 @@ packages: react: registry.npmjs.org/react@18.2.0 use-sync-external-store: registry.npmjs.org/use-sync-external-store@1.2.0(react@18.2.0) dev: false + + registry.npmjs.org/zustand@4.4.1(@types/react@18.2.12)(react@18.2.0): + resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz} + id: registry.npmjs.org/zustand/4.4.1 + name: zustand + version: 4.4.1 + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + dependencies: + '@types/react': registry.npmjs.org/@types/react@18.2.12 + react: registry.npmjs.org/react@18.2.0 + use-sync-external-store: registry.npmjs.org/use-sync-external-store@1.2.0(react@18.2.0) + dev: false