From 1a5742320439817190497d93e67192d4742bd77f Mon Sep 17 00:00:00 2001 From: braks <78412429+bcakmakoglu@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:41:59 +0100 Subject: [PATCH 1/5] refactor(system,panzoom): return promise from transitioned panzoom actions --- packages/system/src/xypanzoom/XYPanZoom.ts | 27 +++++++++++++++++++--- packages/system/src/xypanzoom/utils.ts | 2 ++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts index 372e6134..2d9e8a84 100644 --- a/packages/system/src/xypanzoom/XYPanZoom.ts +++ b/packages/system/src/xypanzoom/XYPanZoom.ts @@ -73,8 +73,15 @@ export function XYPanZoom({ function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) { if (d3Selection) { - d3ZoomInstance?.transform(getD3Transition(d3Selection, options?.duration), transform); + return new Promise((resolve) => { + d3ZoomInstance?.transform( + getD3Transition(d3Selection, options?.duration, () => resolve(true)), + transform + ); + }); } + + return Promise.resolve(false); } // public functions @@ -217,14 +224,28 @@ export function XYPanZoom({ function scaleTo(zoom: number, options?: PanZoomTransformOptions) { if (d3Selection) { - d3ZoomInstance?.scaleTo(getD3Transition(d3Selection, options?.duration), zoom); + return new Promise((resolve) => { + d3ZoomInstance?.scaleTo( + getD3Transition(d3Selection, options?.duration, () => resolve(true)), + zoom + ); + }); } + + return Promise.resolve(false); } function scaleBy(factor: number, options?: PanZoomTransformOptions) { if (d3Selection) { - d3ZoomInstance?.scaleBy(getD3Transition(d3Selection, options?.duration), factor); + return new Promise((resolve) => { + d3ZoomInstance?.scaleBy( + getD3Transition(d3Selection, options?.duration, () => resolve(true)), + factor + ); + }); } + + return Promise.resolve(false); } function setScaleExtent(scaleExtent: [number, number]) { diff --git a/packages/system/src/xypanzoom/utils.ts b/packages/system/src/xypanzoom/utils.ts index cad2b481..1a6f304f 100644 --- a/packages/system/src/xypanzoom/utils.ts +++ b/packages/system/src/xypanzoom/utils.ts @@ -22,6 +22,8 @@ export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: numbe export const getD3Transition = (selection: D3SelectionInstance, duration = 0) => typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration) : selection; +export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => + typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration).on('end', onEnd) : selection; export const wheelDelta = (event: any) => { const factor = event.ctrlKey && isMacOs() ? 10 : 1; From ba26ab62a9413d413e6b8ac0be184018adb631d8 Mon Sep 17 00:00:00 2001 From: braks <78412429+bcakmakoglu@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:42:53 +0100 Subject: [PATCH 2/5] chore(changeset): add --- .changeset/slimy-impalas-yell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slimy-impalas-yell.md diff --git a/.changeset/slimy-impalas-yell.md b/.changeset/slimy-impalas-yell.md new file mode 100644 index 00000000..e6aea28e --- /dev/null +++ b/.changeset/slimy-impalas-yell.md @@ -0,0 +1,5 @@ +--- +'@xyflow/system': major +--- + +Return Promise from transitionable panzoom actions if a transition is applied. Promise is resolved when transition ends. From 2503d32a88534bede9a5361f1bf7a26c9c2e2aef Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 8 Jul 2024 13:18:07 +0200 Subject: [PATCH 3/5] feat(viewport-helpers): return promises --- examples/react/src/examples/Stress/index.tsx | 1 + packages/react/src/hooks/useViewportHelper.ts | 76 +++++++++++++------ packages/react/src/store/index.ts | 6 +- packages/react/src/types/general.ts | 2 +- packages/react/src/types/store.ts | 2 +- .../svelte/src/lib/hooks/useSvelteFlow.ts | 55 ++++++++++---- packages/svelte/src/lib/store/index.ts | 17 +++-- packages/svelte/src/lib/store/types.ts | 8 +- packages/system/src/types/general.ts | 12 +-- packages/system/src/types/panzoom.ts | 8 +- packages/system/src/utils/graph.ts | 10 +-- packages/system/src/utils/store.ts | 10 +-- packages/system/src/xydrag/XYDrag.ts | 4 +- packages/system/src/xypanzoom/XYPanZoom.ts | 14 ++-- packages/system/src/xypanzoom/utils.ts | 2 - 15 files changed, 140 insertions(+), 87 deletions(-) diff --git a/examples/react/src/examples/Stress/index.tsx b/examples/react/src/examples/Stress/index.tsx index 9020ddd6..5fd6f9e9 100644 --- a/examples/react/src/examples/Stress/index.tsx +++ b/examples/react/src/examples/Stress/index.tsx @@ -224,6 +224,7 @@ const StressFlow = () => { onEdgesChange={onEdgeChange} minZoom={0.2} fitView + panOnDrag={false} > diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index 245fafd7..2bfea817 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -21,17 +21,33 @@ const useViewportHelper = (): ViewportHelperFunctions => { return useMemo(() => { return { - zoomIn: (options) => store.getState().panZoom?.scaleBy(1.2, { duration: options?.duration }), - zoomOut: (options) => store.getState().panZoom?.scaleBy(1 / 1.2, { duration: options?.duration }), - zoomTo: (zoomLevel, options) => store.getState().panZoom?.scaleTo(zoomLevel, { duration: options?.duration }), + zoomIn: (options) => { + const { panZoom } = store.getState(); + + return panZoom ? panZoom.scaleBy(1.2, { duration: options?.duration }) : Promise.resolve(false); + }, + zoomOut: (options) => { + const { panZoom } = store.getState(); + + return panZoom ? panZoom.scaleBy(1 / 1.2, { duration: options?.duration }) : Promise.resolve(false); + }, + zoomTo: (zoomLevel, options) => { + const { panZoom } = store.getState(); + + return panZoom ? panZoom.scaleTo(zoomLevel, { duration: options?.duration }) : Promise.resolve(false); + }, getZoom: () => store.getState().transform[2], - setViewport: (viewport, options) => { + setViewport: async (viewport, options) => { const { transform: [tX, tY, tZoom], panZoom, } = store.getState(); - panZoom?.setViewport( + if (!panZoom) { + return Promise.resolve(false); + } + + await panZoom.setViewport( { x: viewport.x ?? tX, y: viewport.y ?? tY, @@ -39,6 +55,8 @@ const useViewportHelper = (): ViewportHelperFunctions => { }, { duration: options?.duration } ); + + return Promise.resolve(true); }, getViewport: () => { const [x, y, zoom] = store.getState().transform; @@ -47,29 +65,31 @@ const useViewportHelper = (): ViewportHelperFunctions => { fitView: (options) => { const { nodeLookup, width, height, minZoom, maxZoom, panZoom } = store.getState(); - if (!panZoom) { - return false; - } - - return fitView( - { - nodeLookup, - width, - height, - minZoom, - maxZoom, - panZoom, - }, - options - ); + return panZoom + ? fitView( + { + nodeLookup, + width, + height, + minZoom, + maxZoom, + panZoom, + }, + options + ) + : Promise.resolve(false); }, - setCenter: (x, y, options) => { + setCenter: async (x, y, options) => { const { width, height, maxZoom, panZoom } = store.getState(); const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; const centerX = width / 2 - x * nextZoom; const centerY = height / 2 - y * nextZoom; - panZoom?.setViewport( + if (!panZoom) { + return Promise.resolve(false); + } + + await panZoom.setViewport( { x: centerX, y: centerY, @@ -77,12 +97,20 @@ const useViewportHelper = (): ViewportHelperFunctions => { }, { duration: options?.duration } ); + + return Promise.resolve(true); }, - fitBounds: (bounds, options) => { + fitBounds: async (bounds, options) => { const { width, height, minZoom, maxZoom, panZoom } = store.getState(); const viewport = getViewportForBounds(bounds, width, height, minZoom, maxZoom, options?.padding ?? 0.1); - panZoom?.setViewport(viewport, { duration: options?.duration }); + if (!panZoom) { + return Promise.resolve(false); + } + + await panZoom.setViewport(viewport, { duration: options?.duration }); + + return Promise.resolve(true); }, screenToFlowPosition: (clientPosition: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => { const { transform, snapGrid, domNode } = store.getState(); diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 57beb3f9..11245bb5 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -287,15 +287,15 @@ const createStore = ({ nodeExtent, }); }, - panBy: (delta): boolean => { + panBy: (delta): Promise => { const { transform, width, height, panZoom, translateExtent } = get(); return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); }, - fitView: (options?: FitViewOptions): boolean => { + fitView: (options?: FitViewOptions): Promise => { const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); if (!panZoom) { - return false; + return Promise.resolve(false); } return fitViewSystem( diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 2f518dca..48e5b2fe 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -65,7 +65,7 @@ export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void; export type FitViewParams = FitViewParamsBase; export type FitViewOptions = FitViewOptionsBase; -export type FitView = (fitViewOptions?: FitViewOptions) => boolean; +export type FitView = (fitViewOptions?: FitViewOptions) => Promise; export type OnInit = ( reactFlowInstance: ReactFlowInstance ) => void; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 2e86ade0..30da6e64 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -168,7 +168,7 @@ export type ReactFlowActions = { triggerNodeChanges: (changes: NodeChange[]) => void; triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; - fitView: (options?: FitViewOptions) => boolean; + fitView: (options?: FitViewOptions) => Promise; }; export type ReactFlowState = ReactFlowStore< diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.ts index be40af64..f46c1757 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.ts @@ -80,7 +80,7 @@ export function useSvelteFlow(): { * @param zoomLevel - the zoom level to set * @param options.duration - optional duration. If set, a transition will be applied */ - setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; + setZoom: (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise; /** * Returns the current zoom level. * @@ -94,14 +94,14 @@ export function useSvelteFlow(): { * @param y - y position * @param options.zoom - optional zoom */ - setCenter: (x: number, y: number, options?: SetCenterOptions) => void; + setCenter: (x: number, y: number, options?: SetCenterOptions) => Promise; /** * Sets the current viewport. * * @param viewport - the viewport to set * @param options.duration - optional duration. If set, a transition will be applied */ - setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; + setViewport: (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise; /** * Returns the current viewport. * @@ -118,7 +118,7 @@ export function useSvelteFlow(): { * @param options.duration - optional duration. If set, a transition will be applied * @param options.nodes - optional nodes to fit the view to */ - fitView: (options?: FitViewOptions) => void; + fitView: (options?: FitViewOptions) => Promise; /** * Returns all nodes that intersect with the given node or rect. * @@ -153,7 +153,7 @@ export function useSvelteFlow(): { * @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to * @param options.padding - optional padding */ - fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void; + fitBounds: (bounds: Rect, options?: FitBoundsOptions) => Promise; /** * Deletes nodes and edges. * @@ -298,26 +298,41 @@ export function useSvelteFlow(): { getEdge: (id) => get(edgeLookup).get(id), getEdges: (ids) => (ids === undefined ? get(edges) : getElements(get(edgeLookup), ids)), setZoom: (zoomLevel, options) => { - get(panZoom)?.scaleTo(zoomLevel, { duration: options?.duration }); + const currentPanZoom = get(panZoom); + return currentPanZoom + ? currentPanZoom.scaleTo(zoomLevel, { duration: options?.duration }) + : Promise.resolve(false); }, getZoom: () => get(viewport).zoom, - setViewport: (vieport, options) => { + setViewport: async (nextViewport, options) => { const currentViewport = get(viewport); + const currentPanZoom = get(panZoom); - get(panZoom)?.setViewport( + if (!currentPanZoom) { + return Promise.resolve(false); + } + + await currentPanZoom.setViewport( { - x: vieport.x ?? currentViewport.x, - y: vieport.y ?? currentViewport.y, - zoom: vieport.zoom ?? currentViewport.zoom + x: nextViewport.x ?? currentViewport.x, + y: nextViewport.y ?? currentViewport.y, + zoom: nextViewport.zoom ?? currentViewport.zoom }, { duration: options?.duration } ); + + return Promise.resolve(true); }, getViewport: () => get(viewport), - setCenter: (x, y, options) => { + setCenter: async (x, y, options) => { const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : get(maxZoom); + const currentPanZoom = get(panZoom); - get(panZoom)?.setViewport( + if (!currentPanZoom) { + return Promise.resolve(false); + } + + await currentPanZoom.setViewport( { x: get(width) / 2 - x * nextZoom, y: get(height) / 2 - y * nextZoom, @@ -325,9 +340,17 @@ export function useSvelteFlow(): { }, { duration: options?.duration } ); + + return Promise.resolve(true); }, fitView, - fitBounds: (bounds: Rect, options?: FitBoundsOptions) => { + fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => { + const currentPanZoom = get(panZoom); + + if (!currentPanZoom) { + return Promise.resolve(false); + } + const viewport = getViewportForBounds( bounds, get(width), @@ -337,7 +360,9 @@ export function useSvelteFlow(): { options?.padding ?? 0.1 ); - get(panZoom)?.setViewport(viewport, { duration: options?.duration }); + await currentPanZoom.setViewport(viewport, { duration: options?.duration }); + + return Promise.resolve(true); }, getIntersectingNodes: ( nodeOrRect: Node | { id: Node['id'] } | Rect, diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index e1eea73f..e52fc8a8 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -90,7 +90,7 @@ export function createStore({ store.nodes.update((nds) => nds); }; - function updateNodeInternals(updates: Map) { + async function updateNodeInternals(updates: Map) { const nodeLookup = get(store.nodeLookup); const { changes, updatedInternals } = updateNodeInternalsSystem( updates, @@ -106,7 +106,7 @@ export function createStore({ if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) { const fitViewOptions = get(store.fitViewOptions); - const fitViewOnInitDone = fitView({ + const fitViewOnInitDone = await fitView({ ...fitViewOptions, nodes: fitViewOptions?.nodes }); @@ -150,7 +150,7 @@ export function createStore({ const panZoom = get(store.panZoom); if (!panZoom) { - return false; + return Promise.resolve(false); } return fitViewSystem( @@ -168,18 +168,19 @@ export function createStore({ function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) { const panZoom = get(store.panZoom); - - if (panZoom) { - panZoom.scaleBy(factor, options); + if (!panZoom) { + return Promise.resolve(false); } + + return panZoom.scaleBy(factor, options); } function zoomIn(options?: ViewportHelperFunctionOptions) { - zoomBy(1.2, options); + return zoomBy(1.2, options); } function zoomOut(options?: ViewportHelperFunctionOptions) { - zoomBy(1 / 1.2, options); + return zoomBy(1 / 1.2, options); } function setMinZoom(minZoom: number) { diff --git a/packages/svelte/src/lib/store/types.ts b/packages/svelte/src/lib/store/types.ts index d87ca907..0d8c01bf 100644 --- a/packages/svelte/src/lib/store/types.ts +++ b/packages/svelte/src/lib/store/types.ts @@ -20,19 +20,19 @@ export type SvelteFlowStoreActions = { setNodeTypes: (nodeTypes: NodeTypes) => void; setEdgeTypes: (edgeTypes: EdgeTypes) => void; addEdge: (edge: Edge | Connection) => void; - zoomIn: (options?: ViewportHelperFunctionOptions) => void; - zoomOut: (options?: ViewportHelperFunctionOptions) => void; + zoomIn: (options?: ViewportHelperFunctionOptions) => Promise; + zoomOut: (options?: ViewportHelperFunctionOptions) => Promise; setMinZoom: (minZoom: number) => void; setMaxZoom: (maxZoom: number) => void; setTranslateExtent: (extent: CoordinateExtent) => void; - fitView: (options?: FitViewOptions) => boolean; + fitView: (options?: FitViewOptions) => Promise; updateNodePositions: UpdateNodePositions; updateNodeInternals: (updates: Map) => void; unselectNodesAndEdges: (params?: { nodes?: Node[]; edges?: Edge[] }) => void; addSelectedNodes: (ids: string[]) => void; addSelectedEdges: (ids: string[]) => void; handleNodeSelection: (id: string) => void; - panBy: (delta: XYPosition) => boolean; + panBy: (delta: XYPosition) => Promise; updateConnection: UpdateConnection; cancelConnection: () => void; reset(): void; diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 7e44be9b..f0a0a504 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -18,13 +18,13 @@ export type OnMove = (event: MouseEvent | TouchEvent | null, viewport: Viewport) export type OnMoveStart = OnMove; export type OnMoveEnd = OnMove; -export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => void; -export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => void; +export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => Promise; +export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise; export type GetZoom = () => number; export type GetViewport = () => Viewport; -export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => void; -export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void; -export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void; +export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise; +export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise; +export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise; export type Connection = { source: string; @@ -130,7 +130,7 @@ export type SelectionRect = Rect & { export type OnError = (id: string, message: string) => void; export type UpdateNodePositions = (dragItems: Map, dragging?: boolean) => void; -export type PanBy = (delta: XYPosition) => boolean; +export type PanBy = (delta: XYPosition) => Promise; export const initialConnection: NoConnection = { inProgress: false, diff --git a/packages/system/src/types/panzoom.ts b/packages/system/src/types/panzoom.ts index 81654bdb..4a126a98 100644 --- a/packages/system/src/types/panzoom.ts +++ b/packages/system/src/types/panzoom.ts @@ -45,15 +45,15 @@ export type PanZoomInstance = { update: (params: PanZoomUpdateOptions) => void; destroy: () => void; getViewport: () => Viewport; - setViewport: (viewport: Viewport, options?: PanZoomTransformOptions) => ZoomTransform | undefined; + setViewport: (viewport: Viewport, options?: PanZoomTransformOptions) => Promise; setViewportConstrained: ( viewport: Viewport, extent: CoordinateExtent, translateExtent: CoordinateExtent - ) => ZoomTransform | undefined; + ) => Promise; setScaleExtent: (scaleExtent: [number, number]) => void; setTranslateExtent: (translateExtent: CoordinateExtent) => void; - scaleTo: (scale: number, options?: PanZoomTransformOptions) => void; - scaleBy: (factor: number, options?: PanZoomTransformOptions) => void; + scaleTo: (scale: number, options?: PanZoomTransformOptions) => Promise; + scaleBy: (factor: number, options?: PanZoomTransformOptions) => Promise; syncViewport: (viewport: Viewport) => void; }; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index d09fe1ef..8d98fa40 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -233,10 +233,10 @@ export const getConnectedEdges = nodeIds.has(edge.source) || nodeIds.has(edge.target)); }; -export function fitView, Options extends FitViewOptionsBase>( +export async function fitView, Options extends FitViewOptionsBase>( { nodeLookup, width, height, panZoom, minZoom, maxZoom }: Params, options?: Options -) { +): Promise { const filteredNodes: Map = new Map(); const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null; @@ -260,12 +260,12 @@ export function fitView, Options exte options?.padding ?? 0.1 ); - panZoom.setViewport(viewport, { duration: options?.duration }); + await panZoom.setViewport(viewport, { duration: options?.duration }); - return true; + return Promise.resolve(true); } - return false; + return Promise.resolve(false); } /** diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 9c4904c7..5a977cc0 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -325,7 +325,7 @@ export function updateNodeInternals( return { changes, updatedInternals }; } -export function panBy({ +export async function panBy({ delta, panZoom, transform, @@ -339,12 +339,12 @@ export function panBy({ translateExtent: CoordinateExtent; width: number; height: number; -}) { +}): Promise { if (!panZoom || (!delta.x && !delta.y)) { - return false; + return Promise.resolve(false); } - const nextViewport = panZoom.setViewportConstrained( + const nextViewport = await panZoom.setViewportConstrained( { x: transform[0] + delta.x, y: transform[1] + delta.y, @@ -361,7 +361,7 @@ export function panBy({ !!nextViewport && (nextViewport.x !== transform[0] || nextViewport.y !== transform[1] || nextViewport.k !== transform[2]); - return transformChanged; + return Promise.resolve(transformChanged); } export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) { diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 675aef3a..27022760 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -195,7 +195,7 @@ export function XYDrag voi } } - function autoPan() { + async function autoPan() { if (!containerBounds) { return; } @@ -208,7 +208,7 @@ export function XYDrag voi lastPos.x = (lastPos.x ?? 0) - xMovement / transform[2]; lastPos.y = (lastPos.y ?? 0) - yMovement / transform[2]; - if (panBy({ x: xMovement, y: yMovement })) { + if (await panBy({ x: xMovement, y: yMovement })) { updateNodes(lastPos as XYPosition, null); } } diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts index a1402dee..2db56b10 100644 --- a/packages/system/src/xypanzoom/XYPanZoom.ts +++ b/packages/system/src/xypanzoom/XYPanZoom.ts @@ -188,27 +188,27 @@ export function XYPanZoom({ d3ZoomInstance.on('zoom', null); } - function setViewportConstrained( + async function setViewportConstrained( viewport: Viewport, extent: CoordinateExtent, translateExtent: CoordinateExtent - ): ZoomTransform | undefined { + ): Promise { const nextTransform = viewportToTransform(viewport); const contrainedTransform = d3ZoomInstance?.constrain()(nextTransform, extent, translateExtent); if (contrainedTransform) { - setTransform(contrainedTransform); + await setTransform(contrainedTransform); } - return contrainedTransform; + return new Promise((resolve) => resolve(contrainedTransform)); } - function setViewport(viewport: Viewport, options?: PanZoomTransformOptions) { + async function setViewport(viewport: Viewport, options?: PanZoomTransformOptions) { const nextTransform = viewportToTransform(viewport); - setTransform(nextTransform, options); + await setTransform(nextTransform, options); - return nextTransform; + return new Promise((resolve) => resolve(nextTransform)); } function syncViewport(viewport: Viewport) { diff --git a/packages/system/src/xypanzoom/utils.ts b/packages/system/src/xypanzoom/utils.ts index 55969f3e..5a3e687f 100644 --- a/packages/system/src/xypanzoom/utils.ts +++ b/packages/system/src/xypanzoom/utils.ts @@ -21,8 +21,6 @@ export const isWrappedWithClass = (event: any, className: string | undefined) => export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) => usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2); -export const getD3Transition = (selection: D3SelectionInstance, duration = 0) => - typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration) : selection; export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration).on('end', onEnd) : selection; From e5b8236a6066930da2d409f3968cc15b031a7a40 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 8 Jul 2024 15:34:41 +0200 Subject: [PATCH 4/5] refactor(panzoom): return promises for viewport helpers --- .changeset/slimy-impalas-yell.md | 5 -- examples/react/src/examples/Stress/index.tsx | 1 - .../react/src/examples/UseReactFlow/index.tsx | 18 +++++-- packages/react/src/hooks/useViewportHelper.ts | 4 +- packages/react/src/store/index.ts | 36 ++++++++++++-- packages/react/src/types/store.ts | 1 + packages/svelte/src/lib/store/index.ts | 35 +++++++++++-- packages/system/src/types/general.ts | 2 +- packages/system/src/utils/graph.ts | 49 +++++++++++-------- packages/system/src/xydrag/XYDrag.ts | 1 + packages/system/src/xypanzoom/utils.ts | 11 ++++- 11 files changed, 122 insertions(+), 41 deletions(-) delete mode 100644 .changeset/slimy-impalas-yell.md diff --git a/.changeset/slimy-impalas-yell.md b/.changeset/slimy-impalas-yell.md deleted file mode 100644 index e6aea28e..00000000 --- a/.changeset/slimy-impalas-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@xyflow/system': major ---- - -Return Promise from transitionable panzoom actions if a transition is applied. Promise is resolved when transition ends. diff --git a/examples/react/src/examples/Stress/index.tsx b/examples/react/src/examples/Stress/index.tsx index 5fd6f9e9..9020ddd6 100644 --- a/examples/react/src/examples/Stress/index.tsx +++ b/examples/react/src/examples/Stress/index.tsx @@ -224,7 +224,6 @@ const StressFlow = () => { onEdgesChange={onEdgeChange} minZoom={0.2} fitView - panOnDrag={false} > diff --git a/examples/react/src/examples/UseReactFlow/index.tsx b/examples/react/src/examples/UseReactFlow/index.tsx index 2177d0cc..cf6ae59f 100644 --- a/examples/react/src/examples/UseReactFlow/index.tsx +++ b/examples/react/src/examples/UseReactFlow/index.tsx @@ -96,9 +96,13 @@ const UseZoomPanHelperFlow = () => { ); const onNodeClick = useCallback( - (_: MouseEvent, node: Node) => { + async (_: MouseEvent, node: Node) => { + console.log('set center start'); + const { x, y } = node.position; - setCenter(x, y, { zoom: 1, duration: 1200 }); + await setCenter(x, y, { zoom: 1, duration: 1200 }); + + console.log('set center success'); }, [setCenter] ); @@ -172,7 +176,15 @@ const UseZoomPanHelperFlow = () => { - + diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index 2bfea817..a495ec89 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { pointToRendererPoint, getViewportForBounds, + getFitViewNodes, fitView, type XYPosition, rendererPointToPoint, @@ -64,11 +65,12 @@ const useViewportHelper = (): ViewportHelperFunctions => { }, fitView: (options) => { const { nodeLookup, width, height, minZoom, maxZoom, panZoom } = store.getState(); + const fitViewNodes = getFitViewNodes(nodeLookup, options); return panZoom ? fitView( { - nodeLookup, + nodes: fitViewNodes, width, height, minZoom, diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 11245bb5..062340b7 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -1,6 +1,7 @@ import { createWithEqualityFn } from 'zustand/traditional'; import { clampPosition, + getFitViewNodes, fitView as fitViewSystem, adoptUserNodes, updateAbsolutePositions, @@ -79,7 +80,6 @@ const createStore = ({ updateNodeInternals: (updates) => { const { triggerNodeChanges, - fitView, nodeLookup, parentLookup, fitViewOnInit, @@ -88,6 +88,7 @@ const createStore = ({ domNode, nodeOrigin, debug, + fitViewSync, } = get(); const { changes, updatedInternals } = updateNodeInternalsSystem( @@ -106,8 +107,9 @@ const createStore = ({ // we call fitView once initially after all dimensions are set let nextFitViewDone = fitViewDone; + if (!fitViewDone && fitViewOnInit) { - nextFitViewDone = fitView({ + nextFitViewDone = fitViewSync({ ...fitViewOnInitOptions, nodes: fitViewOnInitOptions?.nodes, }); @@ -289,6 +291,7 @@ const createStore = ({ }, panBy: (delta): Promise => { const { transform, width, height, panZoom, translateExtent } = get(); + return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); }, fitView: (options?: FitViewOptions): Promise => { @@ -298,9 +301,11 @@ const createStore = ({ return Promise.resolve(false); } + const fitViewNodes = getFitViewNodes(nodeLookup, options); + return fitViewSystem( { - nodeLookup, + nodes: fitViewNodes, width, height, panZoom, @@ -310,6 +315,31 @@ const createStore = ({ options ); }, + // we can't call an asnychronous function in updateNodeInternals + // for that we created this sync version of fitView + fitViewSync: (options?: FitViewOptions): boolean => { + const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); + + if (!panZoom) { + return false; + } + + const fitViewNodes = getFitViewNodes(nodeLookup, options); + + fitViewSystem( + { + nodes: fitViewNodes, + width, + height, + panZoom, + minZoom, + maxZoom, + }, + options + ); + + return fitViewNodes.size > 0; + }, cancelConnection: () => { set({ connection: { ...initialConnection }, diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 30da6e64..0e89d5c6 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -169,6 +169,7 @@ export type ReactFlowActions = { triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; fitView: (options?: FitViewOptions) => Promise; + fitViewSync: (options?: FitViewOptions) => boolean; }; export type ReactFlowState = ReactFlowStore< diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index e52fc8a8..bae08a24 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -18,7 +18,8 @@ import { type CoordinateExtent, type UpdateConnection, type ConnectionState, - type NodeOrigin + type NodeOrigin, + getFitViewNodes } from '@xyflow/system'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types'; @@ -90,7 +91,7 @@ export function createStore({ store.nodes.update((nds) => nds); }; - async function updateNodeInternals(updates: Map) { + function updateNodeInternals(updates: Map) { const nodeLookup = get(store.nodeLookup); const { changes, updatedInternals } = updateNodeInternalsSystem( updates, @@ -106,7 +107,7 @@ export function createStore({ if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) { const fitViewOptions = get(store.fitViewOptions); - const fitViewOnInitDone = await fitView({ + const fitViewOnInitDone = fitViewSync({ ...fitViewOptions, nodes: fitViewOptions?.nodes }); @@ -153,9 +154,11 @@ export function createStore({ return Promise.resolve(false); } + const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options); + return fitViewSystem( { - nodeLookup: get(store.nodeLookup), + nodes: fitViewNodes, width: get(store.width), height: get(store.height), minZoom: get(store.minZoom), @@ -166,6 +169,30 @@ export function createStore({ ); } + function fitViewSync(options?: FitViewOptions) { + const panZoom = get(store.panZoom); + + if (!panZoom) { + return false; + } + + const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options); + + fitViewSystem( + { + nodes: fitViewNodes, + width: get(store.width), + height: get(store.height), + minZoom: get(store.minZoom), + maxZoom: get(store.maxZoom), + panZoom + }, + options + ); + + return fitViewNodes.size > 0; + } + function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) { const panZoom = get(store.panZoom); if (!panZoom) { diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index f0a0a504..f712e1ea 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -55,7 +55,7 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void; export type IsValidConnection = (edge: EdgeBase | Connection) => boolean; export type FitViewParamsBase = { - nodeLookup: Map>; + nodes: Map>; width: number; height: number; panZoom: PanZoomInstance; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 8d98fa40..62c3d9da 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -233,39 +233,46 @@ export const getConnectedEdges = nodeIds.has(edge.source) || nodeIds.has(edge.target)); }; -export async function fitView, Options extends FitViewOptionsBase>( - { nodeLookup, width, height, panZoom, minZoom, maxZoom }: Params, - options?: Options -): Promise { - const filteredNodes: Map = new Map(); +export function getFitViewNodes< + Params extends NodeLookup>, + Options extends FitViewOptionsBase +>(nodeLookup: Params, options?: Pick) { + const fitViewNodes: NodeLookup = new Map(); const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null; nodeLookup.forEach((n) => { const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden); if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) { - filteredNodes.set(n.id, n); + fitViewNodes.set(n.id, n); } }); - if (filteredNodes.size > 0) { - const bounds = getInternalNodesBounds(filteredNodes); + return fitViewNodes; +} - const viewport = getViewportForBounds( - bounds, - width, - height, - options?.minZoom ?? minZoom, - options?.maxZoom ?? maxZoom, - options?.padding ?? 0.1 - ); - - await panZoom.setViewport(viewport, { duration: options?.duration }); - - return Promise.resolve(true); +export async function fitView, Options extends FitViewOptionsBase>( + { nodes, width, height, panZoom, minZoom, maxZoom }: Params, + options?: Omit +): Promise { + if (nodes.size === 0) { + return Promise.resolve(false); } - return Promise.resolve(false); + const bounds = getInternalNodesBounds(nodes); + + const viewport = getViewportForBounds( + bounds, + width, + height, + options?.minZoom ?? minZoom, + options?.maxZoom ?? maxZoom, + options?.padding ?? 0.1 + ); + + await panZoom.setViewport(viewport, { duration: options?.duration }); + + return Promise.resolve(true); } /** diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 27022760..54489a00 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -212,6 +212,7 @@ export function XYDrag voi updateNodes(lastPos as XYPosition, null); } } + autoPanId = requestAnimationFrame(autoPan); } diff --git a/packages/system/src/xypanzoom/utils.ts b/packages/system/src/xypanzoom/utils.ts index 5a3e687f..fbd0324d 100644 --- a/packages/system/src/xypanzoom/utils.ts +++ b/packages/system/src/xypanzoom/utils.ts @@ -21,8 +21,15 @@ export const isWrappedWithClass = (event: any, className: string | undefined) => export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) => usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2); -export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => - typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration).on('end', onEnd) : selection; +export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => { + const hasDuration = typeof duration === 'number' && duration > 0; + + if (!hasDuration) { + onEnd(); + } + + return hasDuration ? selection.transition().duration(duration).on('end', onEnd) : selection; +}; export const wheelDelta = (event: any) => { const factor = event.ctrlKey && isMacOs() ? 10 : 1; From e59bdc07ac9848ff17b53cfc710015bd8060e230 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 8 Jul 2024 15:51:38 +0200 Subject: [PATCH 5/5] chore(changelog): update --- packages/react/CHANGELOG.md | 4 ++++ packages/svelte/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 4f1bf0e8..eb0e3238 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,9 @@ # @xyflow/react +## 12.0.0-next.27 + +- return Promises for `setViewport`, `fitView`, `fitBounds` and `zoomTo` to be able to await viewport update + ## 12.0.0-next.26 - add `autoPanSpeed` prop diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 81de9ea1..169b8e7d 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,9 @@ # @xyflow/svelte +## 0.1.9 + +- return Promises for `setViewport`, `fitView`, `fitBounds` and `zoomTo` to be able to await viewport update + ## 0.1.8 - `useConnection` returns `ConnectionState` with `toNode` and `toHandle`