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;