From ee1ef205ebdc01d357465afacbda5988400f4daf Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 5 Mar 2025 13:45:28 +0100 Subject: [PATCH 01/21] Refactor fitView --- examples/react/src/examples/Basic/index.tsx | 16 ++- .../src/components/StoreUpdater/index.tsx | 4 +- packages/react/src/hooks/useReactFlow.ts | 14 +- packages/react/src/hooks/useViewportHelper.ts | 25 ---- packages/react/src/store/index.ts | 121 +++++------------- packages/react/src/store/initialState.ts | 6 +- packages/react/src/types/general.ts | 13 +- packages/react/src/types/instance.ts | 13 +- packages/react/src/types/store.ts | 7 +- packages/system/src/utils/graph.ts | 22 ++-- packages/system/src/utils/store.ts | 9 +- 11 files changed, 99 insertions(+), 151 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index 62232a35..bb53b309 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -56,8 +56,18 @@ const initialEdges: Edge[] = [ const defaultEdgeOptions = {}; const BasicFlow = () => { - const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } = - useReactFlow(); + const { + addNodes, + setNodes, + getNodes, + setEdges, + getEdges, + deleteElements, + updateNodeData, + toObject, + setViewport, + fitView, + } = useReactFlow(); const updatePos = () => { setNodes((nodes) => @@ -104,6 +114,7 @@ const BasicFlow = () => { ]); setEdges([{ id: 'a-b', source: 'a', target: 'b' }]); + fitView(); }; const onUpdateNode = () => { @@ -117,6 +128,7 @@ const BasicFlow = () => { position: { x: Math.random() * 300, y: Math.random() * 300 }, className: 'light', }); + fitView(); }; return ( diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index e3a61ab2..ac9122ba 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -154,8 +154,8 @@ export function StoreUpdater !!s.panZoom; @@ -271,6 +279,10 @@ export function useReactFlow | undefined) => { + store.setState({ fitViewQueued: true, fitViewOptions: options }); + batchContext.nodeQueue.push((nodes) => [...nodes]); + }, }; }, []); diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index 9a773d14..a88bd3fc 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -2,11 +2,8 @@ import { useMemo } from 'react'; import { pointToRendererPoint, getViewportForBounds, - getFitViewNodes, - fitView, type XYPosition, rendererPointToPoint, - getDimensions, SnapGrid, } from '@xyflow/system'; @@ -65,28 +62,6 @@ const useViewportHelper = (): ViewportHelperFunctions => { const [x, y, zoom] = store.getState().transform; return { x, y, zoom }; }, - fitView: (options) => { - const { nodeLookup, minZoom, maxZoom, panZoom, domNode } = store.getState(); - - if (!panZoom || !domNode) { - return Promise.resolve(false); - } - - const fitViewNodes = getFitViewNodes(nodeLookup, options); - const { width, height } = getDimensions(domNode); - - return fitView( - { - nodes: fitViewNodes, - width, - height, - minZoom, - maxZoom, - panZoom, - }, - options - ); - }, setCenter: async (x, y, options) => { const { width, height, maxZoom, panZoom } = store.getState(); const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index bb05ae22..64d1b405 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -1,7 +1,5 @@ import { createWithEqualityFn } from 'zustand/traditional'; import { - getFitViewNodes, - fitView as fitViewSystem, adoptUserNodes, updateAbsolutePositions, panBy as panBySystem, @@ -15,11 +13,12 @@ import { initialConnection, NodeOrigin, CoordinateExtent, + fitViewport, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import getInitialState from './initialState'; -import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions } from '../types'; +import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams } from '../types'; const createStore = ({ nodes, @@ -46,7 +45,7 @@ const createStore = ({ (set, get) => ({ ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { - const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); + const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued, panZoom } = get(); /* * setNodes() is called exclusively in response to user actions: * - either when the `` prop is updated in the controlled ReactFlow setup, @@ -55,14 +54,35 @@ const createStore = ({ * When this happens, we take the note objects passed by the user and extend them with fields * relevant for internal React Flow operations. */ - adoptUserNodes(nodes, nodeLookup, parentLookup, { + + const nodesInitialized = adoptUserNodes(nodes, nodeLookup, parentLookup, { nodeOrigin, nodeExtent, elevateNodesOnSelect, checkEquality: true, }); - set({ nodes }); + let viewportFitted = false; + if (fitViewQueued && nodesInitialized && panZoom) { + const { fitViewOptions, width, height, minZoom, maxZoom } = get(); + viewportFitted = fitViewport( + { + nodes: nodeLookup, + width, + height, + panZoom, + minZoom, + maxZoom, + }, + fitViewOptions + ); + } + + if (viewportFitted) { + set({ nodes, fitViewQueued: false, fitViewOptions: undefined }); + } else { + set({ nodes }); + } }, setEdges: (edges: Edge[]) => { const { connectionLookup, edgeLookup } = get(); @@ -88,20 +108,8 @@ const createStore = ({ * changes its dimensions, this function is called to measure the * new dimensions and update the nodes. */ - updateNodeInternals: (updates, params = { triggerFitView: true }) => { - const { - triggerNodeChanges, - nodeLookup, - parentLookup, - fitViewOnInit, - fitViewDone, - fitViewOnInitOptions, - domNode, - nodeOrigin, - nodeExtent, - debug, - fitViewSync, - } = get(); + updateNodeInternals: (updates) => { + const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug } = get(); const { changes, updatedInternals } = updateNodeInternalsSystem( updates, @@ -118,29 +126,8 @@ const createStore = ({ updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent }); - if (params.triggerFitView) { - // we call fitView once initially after all dimensions are set - let nextFitViewDone = fitViewDone; - - if (!fitViewDone && fitViewOnInit) { - nextFitViewDone = fitViewSync({ - ...fitViewOnInitOptions, - nodes: fitViewOnInitOptions?.nodes, - }); - } - - /* - * here we are cirmumventing the onNodesChange handler - * in order to be able to display nodes even if the user - * has not provided an onNodesChange handler. - * Nodes are only rendered if they have a width and height - * attribute which they get from this handler. - */ - set({ fitViewDone: nextFitViewDone }); - } else { - // we always want to trigger useStore calls whenever updateNodeInternals is called - set({}); - } + // we always want to trigger useStore calls whenever updateNodeInternals is called + set({}); if (changes?.length > 0) { if (debug) { @@ -332,54 +319,6 @@ const createStore = ({ return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); }, - fitView: (options?: FitViewOptions): Promise => { - const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); - - if (!panZoom) { - return Promise.resolve(false); - } - - const fitViewNodes = getFitViewNodes(nodeLookup, options); - - return fitViewSystem( - { - nodes: fitViewNodes, - width, - height, - panZoom, - minZoom, - maxZoom, - }, - 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/store/initialState.ts b/packages/react/src/store/initialState.ts index af52f2a2..9005c99f 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -104,13 +104,13 @@ const getInitialState = ({ elementsSelectable: true, elevateNodesOnSelect: true, elevateEdgesOnSelect: false, - fitViewOnInit: false, - fitViewDone: false, - fitViewOnInitOptions: undefined, selectNodesOnDrag: true, multiSelectionActive: false, + fitViewQueued: false, + fitViewOptions: undefined, + connection: { ...initialConnection }, connectionClickStartHandle: null, connectOnClick: true, diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 8b78e77b..45dc93c1 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -109,7 +109,7 @@ export type FitViewParams = FitViewParamsBase = FitViewOptionsBase; -export type FitView = (fitViewOptions?: FitViewOptions) => Promise; +export type FitView = (fitViewOptions?: FitViewOptions) => void; export type OnInit = ( reactFlowInstance: ReactFlowInstance ) => void; @@ -156,17 +156,6 @@ export type ViewportHelperFunctions = { * @returns Viewport */ getViewport: GetViewport; - /** - * Fits the view. - * - * @param options.padding - optional padding - * @param options.includeHiddenNodes - optional includeHiddenNodes - * @param options.minZoom - optional minZoom - * @param options.maxZoom - optional maxZoom - * @param options.duration - optional duration. If set, a transition will be applied - * @param options.nodes - optional nodes to fit the view to - */ - fitView: FitView; /** * Sets the center of the view to the given position. * diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index fce67ec5..7aa615f5 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-namespace */ import type { HandleConnection, HandleType, NodeConnection, Rect, Viewport } from '@xyflow/system'; -import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.'; +import type { Node, Edge, ViewportHelperFunctions, InternalNode, FitView } from '.'; export type ReactFlowJsonObject = { nodes: NodeType[]; @@ -217,6 +217,17 @@ export type GeneralHelpers NodeConnection[]; + // /** + // * Fits the view. + // * + // * @param options.padding - optional padding + // * @param options.includeHiddenNodes - optional includeHiddenNodes + // * @param options.minZoom - optional minZoom + // * @param options.maxZoom - optional maxZoom + // * @param options.duration - optional duration. If set, a transition will be applied + // * @param options.nodes - optional nodes to fit the view to + // */ + fitView: FitView; }; /** * The `ReactFlowInstance` provides a collection of methods to query and manipulate diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index e0ec76fa..10814ecf 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -119,9 +119,8 @@ export type ReactFlowStore; onEdgesDelete?: OnEdgesDelete; @@ -168,8 +167,6 @@ export type ReactFlowActions = { triggerNodeChanges: (changes: NodeChange[]) => void; triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; - fitView: (options?: FitViewOptions) => Promise; - fitViewSync: (options?: FitViewOptions) => boolean; setPaneClickDistance: (distance: number) => void; }; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 9720e8c5..84738faf 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -333,10 +333,10 @@ export const getConnectedEdges = nodeIds.has(edge.source) || nodeIds.has(edge.target)); }; -export function getFitViewNodes< +function getFitViewNodes< Params extends NodeLookup>, Options extends FitViewOptionsBase ->(nodeLookup: Params, options?: Pick) { +>(nodeLookup: Params, options?: Options) { const fitViewNodes: NodeLookup = new Map(); const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null; @@ -351,15 +351,21 @@ export function getFitViewNodes< return fitViewNodes; } -export async function fitView, Options extends FitViewOptionsBase>( +export function fitViewport, Options extends FitViewOptionsBase>( { nodes, width, height, panZoom, minZoom, maxZoom }: Params, options?: Omit -): Promise { +): boolean { if (nodes.size === 0) { - return Promise.resolve(false); + return true; } - const bounds = getInternalNodesBounds(nodes); + if (!panZoom) { + return false; + } + + const nodesToFit = getFitViewNodes(nodes, options); + + const bounds = getInternalNodesBounds(nodesToFit); const viewport = getViewportForBounds( bounds, @@ -370,9 +376,9 @@ export async function fitView, Option options?.padding ?? 0.1 ); - await panZoom.setViewport(viewport, { duration: options?.duration }); + panZoom.setViewport(viewport, { duration: options?.duration }); - return Promise.resolve(true); + return true; } /** diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index a9d13316..03b4249c 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -85,9 +85,10 @@ export function adoptUserNodes( nodeLookup: NodeLookup>, parentLookup: ParentLookup>, options?: UpdateNodesOptions -) { +): boolean { const _options = mergeObjects(adoptUserNodesDefaultOptions, options); + let nodesInitialized = true; const tmpLookup = new Map(nodeLookup); const selectedNodeZ: number = _options?.elevateNodesOnSelect ? 1000 : 0; @@ -123,10 +124,16 @@ export function adoptUserNodes( nodeLookup.set(userNode.id, internalNode); } + if (!internalNode.measured || !internalNode.measured.width || !internalNode.measured.height) { + nodesInitialized = false; + } + if (userNode.parentId) { updateChildNode(internalNode, nodeLookup, parentLookup, options); } } + + return nodesInitialized; } function updateParentLookup( From f208223400f725dcd1dfac98dc9800e8a9bd38bd Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 5 Mar 2025 15:00:44 +0100 Subject: [PATCH 02/21] Add back promise resolution for transitioning on fitView --- packages/react/src/hooks/useReactFlow.ts | 6 ++++-- packages/react/src/store/index.ts | 12 ++++++------ packages/react/src/store/initialState.ts | 1 + packages/react/src/types/general.ts | 2 +- packages/react/src/types/store.ts | 1 + packages/system/src/utils/graph.ts | 17 ++++++++--------- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 63b9df86..b67ef0d3 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -279,9 +279,11 @@ export function useReactFlow | undefined) => { - store.setState({ fitViewQueued: true, fitViewOptions: options }); + fitView: async (options: FitViewOptions | undefined) => { + const fitViewResolver = store.getState().fitViewResolver ?? Promise.withResolvers(); + store.setState({ fitViewQueued: true, fitViewOptions: options, fitViewResolver }); batchContext.nodeQueue.push((nodes) => [...nodes]); + return fitViewResolver.promise; }, }; }, []); diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 64d1b405..909d8572 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -62,10 +62,9 @@ const createStore = ({ checkEquality: true, }); - let viewportFitted = false; if (fitViewQueued && nodesInitialized && panZoom) { - const { fitViewOptions, width, height, minZoom, maxZoom } = get(); - viewportFitted = fitViewport( + const { fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get(); + const fitViewPromise = fitViewport( { nodes: nodeLookup, width, @@ -76,9 +75,10 @@ const createStore = ({ }, fitViewOptions ); - } - - if (viewportFitted) { + fitViewPromise.then((value) => { + fitViewResolver?.resolve(value); + set({ fitViewResolver: null }); + }); set({ nodes, fitViewQueued: false, fitViewOptions: undefined }); } else { set({ nodes }); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 9005c99f..c63c9e9c 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -110,6 +110,7 @@ const getInitialState = ({ fitViewQueued: false, fitViewOptions: undefined, + fitViewResolver: null, connection: { ...initialConnection }, connectionClickStartHandle: null, diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 45dc93c1..11021b3a 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -109,7 +109,7 @@ export type FitViewParams = FitViewParamsBase = FitViewOptionsBase; -export type FitView = (fitViewOptions?: FitViewOptions) => void; +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 10814ecf..efd9d957 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -121,6 +121,7 @@ export type ReactFlowStore | null; onNodesDelete?: OnNodesDelete; onEdgesDelete?: OnEdgesDelete; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 84738faf..e14f81cd 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -351,16 +351,15 @@ function getFitViewNodes< return fitViewNodes; } -export function fitViewport, Options extends FitViewOptionsBase>( +export async function fitViewport< + Params extends FitViewParamsBase, + Options extends FitViewOptionsBase +>( { nodes, width, height, panZoom, minZoom, maxZoom }: Params, options?: Omit -): boolean { +): Promise { if (nodes.size === 0) { - return true; - } - - if (!panZoom) { - return false; + return Promise.resolve(true); } const nodesToFit = getFitViewNodes(nodes, options); @@ -376,9 +375,9 @@ export function fitViewport, Options options?.padding ?? 0.1 ); - panZoom.setViewport(viewport, { duration: options?.duration }); + await panZoom.setViewport(viewport, { duration: options?.duration }); - return true; + return Promise.resolve(true); } /** From c98b6c01f918a8ad63dee790e172a40951a56dd6 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 5 Mar 2025 15:16:12 +0100 Subject: [PATCH 03/21] Add comments --- packages/react/src/hooks/useReactFlow.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index b67ef0d3..a02cbb55 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -280,9 +280,14 @@ export function useReactFlow | undefined) => { + // We either create a new Promise or reuse the existing one + // Even if fitView is called multiple times in a row, we only end up with a single Promise const fitViewResolver = store.getState().fitViewResolver ?? Promise.withResolvers(); + + // We schedule a fitView by setting fitViewQueued and triggering a setNodes store.setState({ fitViewQueued: true, fitViewOptions: options, fitViewResolver }); batchContext.nodeQueue.push((nodes) => [...nodes]); + return fitViewResolver.promise; }, }; From 502a2405601284871667c2f84142fa3ace40253e Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 5 Mar 2025 16:20:05 +0100 Subject: [PATCH 04/21] updated typescript, implemented for svelte flow --- examples/react/package.json | 2 +- examples/svelte/package.json | 2 +- .../src/routes/examples/overview/+page.svelte | 2 +- package.json | 2 +- packages/react/package.json | 2 +- packages/svelte/package.json | 2 +- .../container/SvelteFlow/SvelteFlow.svelte | 2 +- packages/svelte/src/lib/store/index.ts | 69 ++----- .../svelte/src/lib/store/initial-store.ts | 41 ++++- packages/svelte/src/lib/store/utils.ts | 48 ++++- packages/system/package.json | 2 +- packages/system/src/utils/store.ts | 5 +- pnpm-lock.yaml | 173 +++++++----------- tooling/rollup-config/package.json | 2 +- 14 files changed, 167 insertions(+), 187 deletions(-) diff --git a/examples/react/package.json b/examples/react/package.json index ecbcb09f..31099a74 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -37,7 +37,7 @@ "cypress": "13.6.6", "cypress-real-events": "1.12.0", "start-server-and-test": "^2.0.2", - "typescript": "5.2.2", + "typescript": "5.4.5", "vite": "4.5.0" } } diff --git a/examples/svelte/package.json b/examples/svelte/package.json index e8c221ae..cfd2ecea 100644 --- a/examples/svelte/package.json +++ b/examples/svelte/package.json @@ -24,7 +24,7 @@ "svelte": "^4.2.12", "svelte-check": "^3.6.6", "tslib": "^2.6.2", - "typescript": "^5.2.2", + "typescript": "^5.4.5", "vite": "^5.2.12" }, "type": "module", diff --git a/examples/svelte/src/routes/examples/overview/+page.svelte b/examples/svelte/src/routes/examples/overview/+page.svelte index 2dd57734..21ad6f89 100644 --- a/examples/svelte/src/routes/examples/overview/+page.svelte +++ b/examples/svelte/src/routes/examples/overview/+page.svelte @@ -198,7 +198,7 @@ attributionPosition={'top-center'} deleteKey={['Backspace', 'd']} > - + xy console.log('control button')} >log(); - if (!panZoom || !domNode) { - return Promise.resolve(false); - } + // We schedule a fitView by setting fitViewQueued and triggering a setNodes + store.fitViewQueued.set(true); + store.fitViewOptions.set(options); + store.fitViewResolver.set(fitViewResolver); + store.nodes.set(get(store.nodes)); - const { width, height } = getDimensions(domNode); - - const fitViewNodes = getFitViewNodes(get(store.nodeLookup), options); - - return fitViewSystem( - { - nodes: fitViewNodes, - width, - height, - minZoom: get(store.minZoom), - maxZoom: get(store.maxZoom), - panZoom - }, - options - ); - } - - 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; + return fitViewResolver.promise; } function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) { @@ -395,7 +349,6 @@ export function createStore({ } function reset() { - store.fitViewOnInitDone.set(false); store.selectionRect.set(null); store.selectionRectMode.set(null); store.snapGrid.set(null); diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 142460a7..3b0064a8 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -112,9 +112,32 @@ export const getInitialStore = ({ viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); } + const fitViewQueued = writable(false); + const fitViewOptions = writable(undefined); + const fitViewResolver = writable | null>(null); + const panZoom = writable(null); + const widthStore = writable(500); + const heightStore = writable(500); + const minZoom = writable(0.5); + const maxZoom = writable(2); + return { flowId: writable(null), - nodes: createNodesStore(nodes, nodeLookup, parentLookup, storeNodeOrigin, storeNodeExtent), + nodes: createNodesStore( + nodes, + nodeLookup, + parentLookup, + storeNodeOrigin, + storeNodeExtent, + fitViewQueued, + fitViewOptions, + fitViewResolver, + panZoom, + widthStore, + heightStore, + minZoom, + maxZoom + ), nodeLookup: readable>(nodeLookup), parentLookup: readable>(parentLookup), edgeLookup: readable>(edgeLookup), @@ -122,20 +145,20 @@ export const getInitialStore = ({ edges: createEdgesStore(edges, connectionLookup, edgeLookup), visibleEdges: readable([]), connectionLookup: readable(connectionLookup), - height: writable(500), - width: writable(500), - minZoom: writable(0.5), - maxZoom: writable(2), + width: widthStore, + height: heightStore, + minZoom, + maxZoom, nodeOrigin: writable(storeNodeOrigin), nodeDragThreshold: writable(1), nodeExtent: writable(storeNodeExtent), translateExtent: writable(infiniteExtent), autoPanOnNodeDrag: writable(true), autoPanOnConnect: writable(true), - fitViewOnInit: writable(false), - fitViewOnInitDone: writable(false), - fitViewOptions: writable(undefined), - panZoom: writable(null), + fitViewQueued, + fitViewOptions, + fitViewResolver, + panZoom, snapGrid: writable(null), dragging: writable(false), selectionRect: writable(null), diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index 30af8f73..0d453c45 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -17,10 +17,18 @@ import { type ParentLookup, type NodeOrigin, infiniteExtent, - type CoordinateExtent + type CoordinateExtent, + fitViewport } from '@xyflow/system'; -import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, InternalNode, Node } from '$lib/types'; +import type { + DefaultEdgeOptions, + DefaultNodeOptions, + Edge, + FitViewOptions, + InternalNode, + Node +} from '$lib/types'; // we need to sync the user nodes and the internal nodes so that the user can receive the updates // made by Svelte Flow (like dragging or selecting a node). @@ -134,7 +142,15 @@ export const createNodesStore = ( nodeLookup: NodeLookup, parentLookup: ParentLookup, nodeOrigin: NodeOrigin = [0, 0], - nodeExtent: CoordinateExtent = infiniteExtent + nodeExtent: CoordinateExtent = infiniteExtent, + fitViewQueued: Writable, + fitViewOptions: Writable, + fitViewResolver: Writable | null>, + panZoom: Writable, + width: Writable, + height: Writable, + minZoom: Writable, + maxZoom: Writable ): { subscribe: (this: void, run: Subscriber) => Unsubscriber; update: (this: void, updater: Updater) => void; @@ -148,7 +164,7 @@ export const createNodesStore = ( let elevateNodesOnSelect = true; const _set = (nds: Node[]): Node[] => { - adoptUserNodes(nds, nodeLookup, parentLookup, { + const nodesInitialized = adoptUserNodes(nds, nodeLookup, parentLookup, { elevateNodesOnSelect, nodeOrigin, nodeExtent, @@ -156,6 +172,30 @@ export const createNodesStore = ( checkEquality: false }); + console.log(nodesInitialized); + + if (get(fitViewQueued) && nodesInitialized && get(panZoom)) { + console.log('trying'); + const fitViewPromise = fitViewport( + { + nodes: nodeLookup, + width: get(width), + height: get(height), + panZoom: get(panZoom)!, + minZoom: get(minZoom), + maxZoom: get(maxZoom) + }, + get(fitViewOptions) + ); + fitViewPromise.then((value) => { + get(fitViewResolver)?.resolve(value); + fitViewResolver.set(null); + }); + + fitViewQueued.set(false); + fitViewOptions.set(undefined); + } + value = nds; set(value); diff --git a/packages/system/package.json b/packages/system/package.json index 9f45b7ef..f23d4c73 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -63,7 +63,7 @@ "@xyflow/eslint-config": "workspace:*", "@xyflow/rollup-config": "workspace:*", "@xyflow/tsconfig": "workspace:*", - "typescript": "5.1.3" + "typescript": "5.4.5" }, "rollup": { "globals": { diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 03b4249c..14fc710a 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -124,7 +124,10 @@ export function adoptUserNodes( nodeLookup.set(userNode.id, internalNode); } - if (!internalNode.measured || !internalNode.measured.width || !internalNode.measured.height) { + if ( + (!internalNode.measured || !internalNode.measured.width || !internalNode.measured.height) && + !internalNode.hidden + ) { nodesInitialized = false; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22541966..bb3a0689 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: ^2.0.3 version: 2.0.3 typescript: - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.4.5 + version: 5.4.5 examples/astro-xyflow: dependencies: @@ -145,8 +145,8 @@ importers: specifier: ^2.0.2 version: 2.0.2 typescript: - specifier: 5.2.2 - version: 5.2.2 + specifier: 5.4.5 + version: 5.4.5 vite: specifier: 4.5.0 version: 4.5.0(@types/node@20.14.6)(terser@5.31.0) @@ -171,10 +171,10 @@ importers: version: 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.12)(vite@5.2.12(@types/node@20.14.6)(terser@5.31.0)))(svelte@4.2.12)(vite@5.2.12(@types/node@20.14.6)(terser@5.31.0)) '@typescript-eslint/eslint-plugin': specifier: ^6.10.0 - version: 6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2))(eslint@8.53.0)(typescript@5.2.2) + version: 6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.4.5))(eslint@8.53.0)(typescript@5.4.5) '@typescript-eslint/parser': specifier: ^6.10.0 - version: 6.10.0(eslint@8.53.0)(typescript@5.2.2) + version: 6.10.0(eslint@8.53.0)(typescript@5.4.5) eslint: specifier: ^8.53.0 version: 8.53.0 @@ -200,8 +200,8 @@ importers: specifier: ^2.6.2 version: 2.6.2 typescript: - specifier: ^5.2.2 - version: 5.2.2 + specifier: ^5.4.5 + version: 5.4.5 vite: specifier: ^5.2.12 version: 5.2.12(@types/node@20.14.6)(terser@5.31.0) @@ -267,8 +267,8 @@ importers: specifier: ^18.2.0 version: 18.2.0 typescript: - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.4.5 + version: 5.4.5 packages/svelte: dependencies: @@ -290,13 +290,13 @@ importers: version: 2.5.4(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.12)(vite@5.3.2(@types/node@20.14.6)(terser@5.31.0)))(svelte@4.2.12)(vite@5.3.2(@types/node@20.14.6)(terser@5.31.0)) '@sveltejs/package': specifier: ^2.3.0 - version: 2.3.0(svelte@4.2.12)(typescript@5.4.2) + version: 2.3.0(svelte@4.2.12)(typescript@5.4.5) '@typescript-eslint/eslint-plugin': specifier: ^7.2.0 - version: 7.2.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2) + version: 7.2.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/parser': specifier: ^7.2.0 - version: 7.2.0(eslint@8.57.0)(typescript@5.4.2) + version: 7.2.0(eslint@8.57.0)(typescript@5.4.5) autoprefixer: specifier: ^10.4.18 version: 10.4.18(postcss@8.4.35) @@ -350,13 +350,13 @@ importers: version: 0.33.1(svelte@4.2.12) svelte-preprocess: specifier: ^5.1.3 - version: 5.1.3(@babel/core@7.24.7)(postcss-load-config@5.0.2(postcss@8.4.35))(postcss@8.4.35)(svelte@4.2.12)(typescript@5.4.2) + version: 5.1.3(@babel/core@7.24.7)(postcss-load-config@5.0.2(postcss@8.4.35))(postcss@8.4.35)(svelte@4.2.12)(typescript@5.4.5) tslib: specifier: ^2.6.2 version: 2.6.2 typescript: - specifier: 5.4.2 - version: 5.4.2 + specifier: 5.4.5 + version: 5.4.5 packages/system: dependencies: @@ -395,8 +395,8 @@ importers: specifier: workspace:* version: link:../../tooling/tsconfig typescript: - specifier: 5.1.3 - version: 5.1.3 + specifier: 5.4.5 + version: 5.4.5 tests/playwright: dependencies: @@ -463,7 +463,7 @@ importers: version: 0.4.4(rollup@4.18.0) '@rollup/plugin-typescript': specifier: 11.1.6 - version: 11.1.6(rollup@4.18.0)(tslib@2.6.2)(typescript@5.4.2) + version: 11.1.6(rollup@4.18.0)(tslib@2.6.2)(typescript@5.4.5) rollup: specifier: ^4.18.0 version: 4.18.0 @@ -471,8 +471,8 @@ importers: specifier: ^2.2.4 version: 2.2.4(rollup@4.18.0) typescript: - specifier: ^5.1.3 - version: 5.4.2 + specifier: ^5.4.5 + version: 5.4.5 tooling/tsconfig: {} @@ -6300,21 +6300,6 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript@5.1.3: - resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} - engines: {node: '>=14.17'} - hasBin: true - - typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} - engines: {node: '>=14.17'} - hasBin: true - - typescript@5.4.2: - resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} - engines: {node: '>=14.17'} - hasBin: true - typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} @@ -7884,11 +7869,11 @@ snapshots: optionalDependencies: rollup: 4.18.0 - '@rollup/plugin-typescript@11.1.6(rollup@4.18.0)(tslib@2.6.2)(typescript@5.4.2)': + '@rollup/plugin-typescript@11.1.6(rollup@4.18.0)(tslib@2.6.2)(typescript@5.4.5)': dependencies: '@rollup/pluginutils': 5.1.0(rollup@4.18.0) resolve: 1.22.8 - typescript: 5.4.2 + typescript: 5.4.5 optionalDependencies: rollup: 4.18.0 tslib: 2.6.2 @@ -8009,14 +7994,14 @@ snapshots: tiny-glob: 0.2.9 vite: 5.3.2(@types/node@20.14.6)(terser@5.31.0) - '@sveltejs/package@2.3.0(svelte@4.2.12)(typescript@5.4.2)': + '@sveltejs/package@2.3.0(svelte@4.2.12)(typescript@5.4.5)': dependencies: chokidar: 3.6.0 kleur: 4.1.5 sade: 1.8.1 semver: 7.6.0 svelte: 4.2.12 - svelte2tsx: 0.7.3(svelte@4.2.12)(typescript@5.4.2) + svelte2tsx: 0.7.3(svelte@4.2.12)(typescript@5.4.5) transitivePeerDependencies: - typescript @@ -8319,13 +8304,13 @@ snapshots: '@types/node': 18.7.16 optional: true - '@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2))(eslint@8.53.0)(typescript@5.2.2)': + '@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.4.5))(eslint@8.53.0)(typescript@5.4.5)': dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.4.5) '@typescript-eslint/scope-manager': 6.10.0 - '@typescript-eslint/type-utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/type-utils': 6.10.0(eslint@8.53.0)(typescript@5.4.5) + '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.4.5) '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4(supports-color@8.1.1) eslint: 8.53.0 @@ -8333,19 +8318,19 @@ snapshots: ignore: 5.2.4 natural-compare: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.2.2) + ts-api-utils: 1.0.3(typescript@5.4.5) optionalDependencies: - typescript: 5.2.2 + typescript: 5.4.5 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2)': + '@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/scope-manager': 7.2.0 - '@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) - '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 @@ -8353,9 +8338,9 @@ snapshots: ignore: 5.3.0 natural-compare: 1.4.0 semver: 7.6.0 - ts-api-utils: 1.0.3(typescript@5.4.2) + ts-api-utils: 1.0.3(typescript@5.4.5) optionalDependencies: - typescript: 5.4.2 + typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -8376,29 +8361,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2)': + '@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.4.5)': dependencies: '@typescript-eslint/scope-manager': 6.10.0 '@typescript-eslint/types': 6.10.0 - '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4(supports-color@8.1.1) eslint: 8.53.0 optionalDependencies: - typescript: 5.2.2 + typescript: 5.4.5 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2)': + '@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5)': dependencies: '@typescript-eslint/scope-manager': 7.2.0 '@typescript-eslint/types': 7.2.0 - '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 7.2.0 debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 optionalDependencies: - typescript: 5.4.2 + typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -8429,27 +8414,27 @@ snapshots: '@typescript-eslint/types': 8.23.0 '@typescript-eslint/visitor-keys': 8.23.0 - '@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.2.2)': + '@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.4.5)': dependencies: - '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) - '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.4.5) + '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) eslint: 8.53.0 - ts-api-utils: 1.0.3(typescript@5.2.2) + ts-api-utils: 1.0.3(typescript@5.4.5) optionalDependencies: - typescript: 5.2.2 + typescript: 5.4.5 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.2.0(eslint@8.57.0)(typescript@5.4.2)': + '@typescript-eslint/type-utils@7.2.0(eslint@8.57.0)(typescript@5.4.5)': dependencies: - '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) - '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.5) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.5) debug: 4.3.4(supports-color@8.1.1) eslint: 8.57.0 - ts-api-utils: 1.0.3(typescript@5.4.2) + ts-api-utils: 1.0.3(typescript@5.4.5) optionalDependencies: - typescript: 5.4.2 + typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -8470,7 +8455,7 @@ snapshots: '@typescript-eslint/types@8.23.0': {} - '@typescript-eslint/typescript-estree@6.10.0(typescript@5.2.2)': + '@typescript-eslint/typescript-estree@6.10.0(typescript@5.4.5)': dependencies: '@typescript-eslint/types': 6.10.0 '@typescript-eslint/visitor-keys': 6.10.0 @@ -8478,13 +8463,13 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.2.2) + ts-api-utils: 1.0.3(typescript@5.4.5) optionalDependencies: - typescript: 5.2.2 + typescript: 5.4.5 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@7.2.0(typescript@5.4.2)': + '@typescript-eslint/typescript-estree@7.2.0(typescript@5.4.5)': dependencies: '@typescript-eslint/types': 7.2.0 '@typescript-eslint/visitor-keys': 7.2.0 @@ -8493,9 +8478,9 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.3 semver: 7.6.0 - ts-api-utils: 1.0.3(typescript@5.4.2) + ts-api-utils: 1.0.3(typescript@5.4.5) optionalDependencies: - typescript: 5.4.2 + typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -8513,28 +8498,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.2.2)': + '@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.4.5)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.6 '@typescript-eslint/scope-manager': 6.10.0 '@typescript-eslint/types': 6.10.0 - '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.4.5) eslint: 8.53.0 semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/utils@7.2.0(eslint@8.57.0)(typescript@5.4.2)': + '@typescript-eslint/utils@7.2.0(eslint@8.57.0)(typescript@5.4.5)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.6 '@typescript-eslint/scope-manager': 7.2.0 '@typescript-eslint/types': 7.2.0 - '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2) + '@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.5) eslint: 8.57.0 semver: 7.6.0 transitivePeerDependencies: @@ -13430,20 +13415,6 @@ snapshots: dependencies: svelte: 4.2.12 - svelte-preprocess@5.1.3(@babel/core@7.24.7)(postcss-load-config@5.0.2(postcss@8.4.35))(postcss@8.4.35)(svelte@4.2.12)(typescript@5.4.2): - dependencies: - '@types/pug': 2.0.10 - detect-indent: 6.1.0 - magic-string: 0.30.8 - sorcery: 0.11.0 - strip-indent: 3.0.0 - svelte: 4.2.12 - optionalDependencies: - '@babel/core': 7.24.7 - postcss: 8.4.35 - postcss-load-config: 5.0.2(postcss@8.4.35) - typescript: 5.4.2 - svelte-preprocess@5.1.3(@babel/core@7.24.7)(postcss-load-config@5.0.2(postcss@8.4.35))(postcss@8.4.35)(svelte@4.2.12)(typescript@5.4.5): dependencies: '@types/pug': 2.0.10 @@ -13465,12 +13436,12 @@ snapshots: svelte: 4.2.1 typescript: 5.4.5 - svelte2tsx@0.7.3(svelte@4.2.12)(typescript@5.4.2): + svelte2tsx@0.7.3(svelte@4.2.12)(typescript@5.4.5): dependencies: dedent-js: 1.0.1 pascal-case: 3.1.2 svelte: 4.2.12 - typescript: 5.4.2 + typescript: 5.4.5 svelte@4.2.1: dependencies: @@ -13608,13 +13579,9 @@ snapshots: trough@2.1.0: {} - ts-api-utils@1.0.3(typescript@5.2.2): + ts-api-utils@1.0.3(typescript@5.4.5): dependencies: - typescript: 5.2.2 - - ts-api-utils@1.0.3(typescript@5.4.2): - dependencies: - typescript: 5.4.2 + typescript: 5.4.5 ts-api-utils@2.0.1(typescript@5.4.5): dependencies: @@ -13782,12 +13749,6 @@ snapshots: possible-typed-array-names: 1.0.0 reflect.getprototypeof: 1.0.10 - typescript@5.1.3: {} - - typescript@5.2.2: {} - - typescript@5.4.2: {} - typescript@5.4.5: {} ultrahtml@1.5.2: {} diff --git a/tooling/rollup-config/package.json b/tooling/rollup-config/package.json index c8871f61..d04d3d93 100644 --- a/tooling/rollup-config/package.json +++ b/tooling/rollup-config/package.json @@ -14,6 +14,6 @@ "@rollup/plugin-typescript": "11.1.6", "rollup": "^4.18.0", "rollup-plugin-peer-deps-external": "^2.2.4", - "typescript": "^5.1.3" + "typescript": "^5.4.5" } } From cb685281d0eaf03e9833271c31f92b1d143af2fe Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 5 Mar 2025 16:22:43 +0100 Subject: [PATCH 05/21] add changeset --- .changeset/poor-poems-visit.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/poor-poems-visit.md diff --git a/.changeset/poor-poems-visit.md b/.changeset/poor-poems-visit.md new file mode 100644 index 00000000..4aeb827d --- /dev/null +++ b/.changeset/poor-poems-visit.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': minor +'@xyflow/svelte': patch +'@xyflow/system': patch +--- + +Fix fitView not working when adding new nodes From cd03c4619610c9426ce46b373975cfddc5e54417 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 5 Mar 2025 17:38:30 +0100 Subject: [PATCH 06/21] Add pixel units and left and right padding options for fitView --- packages/system/src/types/general.ts | 3 ++- packages/system/src/utils/general.ts | 12 +++++++++--- packages/system/src/utils/graph.ts | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 8ac3f9e9..1f6b2ecf 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -95,7 +95,8 @@ export type FitViewParamsBase = { * @inline */ export type FitViewOptionsBase = { - padding?: number; + padding?: number | [number, number]; + paddingUnit?: 'px' | '%'; includeHiddenNodes?: boolean; minZoom?: number; maxZoom?: number; diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 32c339e5..90ff64fa 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -195,10 +195,16 @@ export const getViewportForBounds = ( height: number, minZoom: number, maxZoom: number, - padding: number + padding: number | [number, number], + paddingUnit: 'px' | '%' ): Viewport => { - const xZoom = width / (bounds.width * (1 + padding)); - const yZoom = height / (bounds.height * (1 + padding)); + const [paddingX, paddingY] = Array.isArray(padding) ? [padding[0], padding[1]] : [padding, padding]; + + const isPixelPadding = paddingUnit === 'px'; + + const xZoom = width / (isPixelPadding ? bounds.width + paddingX : bounds.width * (1 + paddingX)); + const yZoom = height / (isPixelPadding ? bounds.height + paddingY : bounds.height * (1 + paddingY)); + const zoom = Math.min(xZoom, yZoom); const clampedZoom = clamp(zoom, minZoom, maxZoom); const boundsCenterX = bounds.x + bounds.width / 2; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index e14f81cd..930c4adb 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -372,7 +372,8 @@ export async function fitViewport< height, options?.minZoom ?? minZoom, options?.maxZoom ?? maxZoom, - options?.padding ?? 0.1 + options?.padding ?? 0.1, + options?.paddingUnit ?? '%' ); await panZoom.setViewport(viewport, { duration: options?.duration }); From db4c019fe42d1528f0fd6d816202281603c079fe Mon Sep 17 00:00:00 2001 From: peterkogo Date: Mon, 17 Mar 2025 16:01:22 +0100 Subject: [PATCH 07/21] either trigger fitView after adoptUserNodes or after updateNodeInternals --- examples/react/src/examples/Stress/index.tsx | 16 +++++++-- packages/react/src/store/index.ts | 36 ++++++++++++++++++-- packages/react/src/store/initialState.ts | 2 +- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/examples/react/src/examples/Stress/index.tsx b/examples/react/src/examples/Stress/index.tsx index 9020ddd6..496fd500 100644 --- a/examples/react/src/examples/Stress/index.tsx +++ b/examples/react/src/examples/Stress/index.tsx @@ -12,6 +12,8 @@ import { Controls, Background, Panel, + ReactFlowProvider, + useReactFlow, } from '@xyflow/react'; import { getNodesAndEdges } from './utils'; @@ -22,6 +24,7 @@ const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25); const StressFlow = () => { const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(initialEdges); + const { fitView } = useReactFlow(); const onConnect = useCallback((connection: Connection) => { setEdges((eds) => addEdge(connection, eds)); }, []); @@ -191,12 +194,13 @@ const StressFlow = () => { return { ...n, position: { - x: Math.random() * window.innerWidth, - y: Math.random() * window.innerHeight, + x: Math.random() * window.innerWidth * 4, + y: Math.random() * window.innerHeight * 4, }, }; }); }); + fitView(); }; const updateElements = () => { @@ -240,4 +244,10 @@ const StressFlow = () => { ); }; -export default StressFlow; +export default function StressFlowProvider() { + return ( + + + + ); +} diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 909d8572..68d1dd24 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -109,7 +109,17 @@ const createStore = ({ * new dimensions and update the nodes. */ updateNodeInternals: (updates) => { - const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug } = get(); + const { + triggerNodeChanges, + nodeLookup, + parentLookup, + domNode, + nodeOrigin, + nodeExtent, + debug, + panZoom, + fitViewQueued, + } = get(); const { changes, updatedInternals } = updateNodeInternalsSystem( updates, @@ -126,8 +136,28 @@ const createStore = ({ updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent }); - // we always want to trigger useStore calls whenever updateNodeInternals is called - set({}); + if (fitViewQueued && panZoom) { + const { fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get(); + const fitViewPromise = fitViewport( + { + nodes: nodeLookup, + width, + height, + panZoom, + minZoom, + maxZoom, + }, + fitViewOptions + ); + fitViewPromise.then((value) => { + fitViewResolver?.resolve(value); + set({ fitViewResolver: null }); + }); + set({ nodes, fitViewQueued: false, fitViewOptions: undefined }); + } else { + // we always want to trigger useStore calls whenever updateNodeInternals is called + set({}); + } if (changes?.length > 0) { if (debug) { diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index c63c9e9c..8be408b8 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -108,7 +108,7 @@ const getInitialState = ({ multiSelectionActive: false, - fitViewQueued: false, + fitViewQueued: fitView ?? false, fitViewOptions: undefined, fitViewResolver: null, From 6acb709bef20c9663e453956567f6e08a553b289 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 18 Mar 2025 09:30:29 +0100 Subject: [PATCH 08/21] extract fitView into action --- packages/react/src/store/index.ts | 89 +++++++++++++------------------ 1 file changed, 36 insertions(+), 53 deletions(-) diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 68d1dd24..8b219d27 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -41,11 +41,36 @@ const createStore = ({ nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; }) => - createWithEqualityFn( - (set, get) => ({ + createWithEqualityFn((set, get) => { + function resolveFitView() { + const { nodeLookup, panZoom, fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get(); + + if (!panZoom) { + return; + } + + const fitViewPromise = fitViewport( + { + nodes: nodeLookup, + width, + height, + panZoom, + minZoom, + maxZoom, + }, + fitViewOptions + ); + fitViewPromise.then((value) => { + fitViewResolver?.resolve(value); + set({ fitViewResolver: null }); + }); + set({ nodes, fitViewQueued: false, fitViewOptions: undefined }); + } + + return { ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { - const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued, panZoom } = get(); + const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued } = get(); /* * setNodes() is called exclusively in response to user actions: * - either when the `` prop is updated in the controlled ReactFlow setup, @@ -62,24 +87,8 @@ const createStore = ({ checkEquality: true, }); - if (fitViewQueued && nodesInitialized && panZoom) { - const { fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get(); - const fitViewPromise = fitViewport( - { - nodes: nodeLookup, - width, - height, - panZoom, - minZoom, - maxZoom, - }, - fitViewOptions - ); - fitViewPromise.then((value) => { - fitViewResolver?.resolve(value); - set({ fitViewResolver: null }); - }); - set({ nodes, fitViewQueued: false, fitViewOptions: undefined }); + if (fitViewQueued && nodesInitialized) { + resolveFitView(); } else { set({ nodes }); } @@ -109,17 +118,8 @@ const createStore = ({ * new dimensions and update the nodes. */ updateNodeInternals: (updates) => { - const { - triggerNodeChanges, - nodeLookup, - parentLookup, - domNode, - nodeOrigin, - nodeExtent, - debug, - panZoom, - fitViewQueued, - } = get(); + const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug, fitViewQueued } = + get(); const { changes, updatedInternals } = updateNodeInternalsSystem( updates, @@ -136,24 +136,8 @@ const createStore = ({ updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent }); - if (fitViewQueued && panZoom) { - const { fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get(); - const fitViewPromise = fitViewport( - { - nodes: nodeLookup, - width, - height, - panZoom, - minZoom, - maxZoom, - }, - fitViewOptions - ); - fitViewPromise.then((value) => { - fitViewResolver?.resolve(value); - set({ fitViewResolver: null }); - }); - set({ nodes, fitViewQueued: false, fitViewOptions: undefined }); + if (fitViewQueued) { + resolveFitView(); } else { // we always want to trigger useStore calls whenever updateNodeInternals is called set({}); @@ -359,8 +343,7 @@ const createStore = ({ }, reset: () => set({ ...getInitialState() }), - }), - Object.is - ); + }; + }, Object.is); export { createStore }; From 1202c9a68772776a9259c11b8f2ae404b6f16acf Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 25 Mar 2025 13:10:13 +0100 Subject: [PATCH 09/21] paddings with units --- packages/system/src/types/general.ts | 18 ++++++- packages/system/src/utils/general.ts | 80 +++++++++++++++++++++++++--- packages/system/src/utils/graph.ts | 3 +- 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 1f6b2ecf..9acb3316 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -91,12 +91,26 @@ export type FitViewParamsBase = { maxZoom: number; }; +export type PaddingUnit = 'px' | '%'; +export type PaddingWithUnit = `${number}${PaddingUnit}` | number; + +export type Padding = + | PaddingWithUnit + | [padding: PaddingWithUnit] + | [paddingY: PaddingWithUnit, paddingX: PaddingWithUnit] + | [paddingTop: PaddingWithUnit, paddingX: PaddingWithUnit, paddingBottom: PaddingWithUnit] + | [ + paddingTop: PaddingWithUnit, + paddingRight: PaddingWithUnit, + paddingBottom: PaddingWithUnit, + paddingLeft: PaddingWithUnit + ]; + /** * @inline */ export type FitViewOptionsBase = { - padding?: number | [number, number]; - paddingUnit?: 'px' | '%'; + padding?: Padding; includeHiddenNodes?: boolean; minZoom?: number; maxZoom?: number; diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 90ff64fa..a21908bb 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -10,6 +10,8 @@ import type { Transform, InternalNodeBase, NodeLookup, + Padding, + PaddingWithUnit, } from '../types'; import { type Viewport } from '../types'; import { getNodePositionWithOrigin, isInternalNodeBase } from './graph'; @@ -173,6 +175,71 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra }; }; +function parsePadding(padding: PaddingWithUnit, viewportDimension: number, bound: number): number { + if (typeof padding === 'number') { + return bound * padding; + } + + if (typeof padding === 'string' && padding.endsWith('px')) { + return parseFloat(padding.slice(0, -2)); + } + + if (typeof padding === 'string' && padding.endsWith('%')) { + return viewportDimension * parseFloat(padding.slice(0, -1)); + } + + return 0; +} + +function parsePaddings( + padding: Padding, + viewportDiemsions: Dimensions, + bounds: Dimensions +): [number, number, number, number] { + if (typeof padding === 'number') { + return [padding, padding, padding, padding]; + } + + if (typeof padding === 'string') { + const paddingX = parsePadding(padding, viewportDiemsions.width, bounds.width); + const paddingY = parsePadding(padding, viewportDiemsions.height, bounds.height); + return [paddingY, paddingX, paddingY, paddingX]; + } + + if (Array.isArray(padding)) { + switch (padding.length) { + case 1: { + const paddingX = parsePadding(padding[0], viewportDiemsions.width, bounds.width); + const paddingY = parsePadding(padding[0], viewportDiemsions.height, bounds.height); + return [paddingY, paddingX, paddingY, paddingX]; + } + case 2: { + const [pY, pX] = padding; + const paddingY = parsePadding(pY, viewportDiemsions.height, bounds.height); + const paddingX = parsePadding(pX, viewportDiemsions.width, bounds.width); + return [paddingY, paddingX, paddingY, paddingX]; + } + case 3: { + const [pTop, pX, pBottom] = padding; + const paddingTop = parsePadding(pTop, viewportDiemsions.height, bounds.height); + const padddingX = parsePadding(pX, viewportDiemsions.width, bounds.width); + const paddingBottom = parsePadding(pBottom, viewportDiemsions.height, bounds.height); + return [paddingTop, padddingX, paddingBottom, padddingX]; + } + case 4: { + const [pTop, pRight, pBottom, pLeft] = padding; + const paddingTop = parsePadding(pTop, viewportDiemsions.height, bounds.height); + const paddingRight = parsePadding(pRight, viewportDiemsions.width, bounds.width); + const paddingBottom = parsePadding(pBottom, viewportDiemsions.height, bounds.height); + const paddingLeft = parsePadding(pLeft, viewportDiemsions.width, bounds.width); + return [paddingTop, paddingRight, paddingBottom, paddingLeft]; + } + } + } + + return [0, 0, 0, 0]; +} + /** * Returns a viewport that encloses the given bounds with optional padding. * @public @@ -195,15 +262,16 @@ export const getViewportForBounds = ( height: number, minZoom: number, maxZoom: number, - padding: number | [number, number], - paddingUnit: 'px' | '%' + padding: Padding = 0 ): Viewport => { - const [paddingX, paddingY] = Array.isArray(padding) ? [padding[0], padding[1]] : [padding, padding]; + // const [paddingX, paddingY] = Array.isArray(padding) ? [padding[0], padding[1]] : [padding, padding]; - const isPixelPadding = paddingUnit === 'px'; + // const isPixelPadding = paddingUnit === 'px'; - const xZoom = width / (isPixelPadding ? bounds.width + paddingX : bounds.width * (1 + paddingX)); - const yZoom = height / (isPixelPadding ? bounds.height + paddingY : bounds.height * (1 + paddingY)); + const [paddingTop, paddingRight, paddingBottom, paddingLeft] = parsePaddings(padding, { width, height }, bounds); + + const xZoom = (width - paddingLeft - paddingRight) / bounds.width; + const yZoom = (height - paddingTop - paddingBottom) / bounds.height; const zoom = Math.min(xZoom, yZoom); const clampedZoom = clamp(zoom, minZoom, maxZoom); diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 930c4adb..e14f81cd 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -372,8 +372,7 @@ export async function fitViewport< height, options?.minZoom ?? minZoom, options?.maxZoom ?? maxZoom, - options?.padding ?? 0.1, - options?.paddingUnit ?? '%' + options?.padding ?? 0.1 ); await panZoom.setViewport(viewport, { duration: options?.duration }); From bb1a97d7a8cb7375c18c48567fa544520f5e5bd7 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 25 Mar 2025 13:43:30 +0100 Subject: [PATCH 10/21] fix uncontrolled flows --- packages/react/src/store/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 8b219d27..e099ca95 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -64,7 +64,6 @@ const createStore = ({ fitViewResolver?.resolve(value); set({ fitViewResolver: null }); }); - set({ nodes, fitViewQueued: false, fitViewOptions: undefined }); } return { @@ -89,6 +88,7 @@ const createStore = ({ if (fitViewQueued && nodesInitialized) { resolveFitView(); + set({ nodes, fitViewQueued: false, fitViewOptions: undefined }); } else { set({ nodes }); } @@ -138,6 +138,7 @@ const createStore = ({ if (fitViewQueued) { resolveFitView(); + set({ fitViewQueued: false, fitViewOptions: undefined }); } else { // we always want to trigger useStore calls whenever updateNodeInternals is called set({}); From 137b23c6a7bb01cc76f09b3ecb74b928d15998cc Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 25 Mar 2025 17:13:21 +0100 Subject: [PATCH 11/21] add initial support for asymmetric paddings --- examples/react/src/examples/Basic/index.tsx | 3 + packages/system/src/utils/general.ts | 78 +++++++++++---------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index bb53b309..e399ab88 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -146,6 +146,9 @@ const BasicFlow = () => { minZoom={0.2} maxZoom={4} fitView + fitViewOptions={{ + padding: ['30%', '10%', '10%', '20%'], + }} defaultEdgeOptions={defaultEdgeOptions} selectNodesOnDrag={false} elevateEdgesOnSelect diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index a21908bb..0d47025b 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -175,63 +175,68 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra }; }; -function parsePadding(padding: PaddingWithUnit, viewportDimension: number, bound: number): number { +function paddingError(padding: PaddingWithUnit) { + console.error( + `[React Flow] The padding value "${padding}" is invalid. Please provide a number or a string with a valid unit (px or %).` + ); +} + +function parsePadding(padding: PaddingWithUnit, viewport: number): number { if (typeof padding === 'number') { - return bound * padding; + return viewport - viewport / (1 + padding * 0.5); } if (typeof padding === 'string' && padding.endsWith('px')) { - return parseFloat(padding.slice(0, -2)); + const paddingValue = parseFloat(padding); + if (!Number.isNaN(paddingValue)) { + return paddingValue; + } } if (typeof padding === 'string' && padding.endsWith('%')) { - return viewportDimension * parseFloat(padding.slice(0, -1)); + const paddingValue = parseFloat(padding); + if (!Number.isNaN(paddingValue)) { + return viewport * paddingValue * 0.01; + } } + paddingError(padding); return 0; } -function parsePaddings( - padding: Padding, - viewportDiemsions: Dimensions, - bounds: Dimensions -): [number, number, number, number] { - if (typeof padding === 'number') { - return [padding, padding, padding, padding]; - } - - if (typeof padding === 'string') { - const paddingX = parsePadding(padding, viewportDiemsions.width, bounds.width); - const paddingY = parsePadding(padding, viewportDiemsions.height, bounds.height); +function parsePaddings(padding: Padding, width: number, height: number): [number, number, number, number] { + if (typeof padding === 'string' || typeof padding === 'number') { + const paddingY = parsePadding(padding, height); + const paddingX = parsePadding(padding, width); return [paddingY, paddingX, paddingY, paddingX]; } if (Array.isArray(padding)) { switch (padding.length) { case 1: { - const paddingX = parsePadding(padding[0], viewportDiemsions.width, bounds.width); - const paddingY = parsePadding(padding[0], viewportDiemsions.height, bounds.height); + const paddingY = parsePadding(padding[0], height); + const paddingX = parsePadding(padding[0], width); return [paddingY, paddingX, paddingY, paddingX]; } case 2: { const [pY, pX] = padding; - const paddingY = parsePadding(pY, viewportDiemsions.height, bounds.height); - const paddingX = parsePadding(pX, viewportDiemsions.width, bounds.width); + const paddingY = parsePadding(pY, height); + const paddingX = parsePadding(pX, width); return [paddingY, paddingX, paddingY, paddingX]; } case 3: { const [pTop, pX, pBottom] = padding; - const paddingTop = parsePadding(pTop, viewportDiemsions.height, bounds.height); - const padddingX = parsePadding(pX, viewportDiemsions.width, bounds.width); - const paddingBottom = parsePadding(pBottom, viewportDiemsions.height, bounds.height); + const paddingTop = parsePadding(pTop, height); + const padddingX = parsePadding(pX, width); + const paddingBottom = parsePadding(pBottom, height); return [paddingTop, padddingX, paddingBottom, padddingX]; } case 4: { const [pTop, pRight, pBottom, pLeft] = padding; - const paddingTop = parsePadding(pTop, viewportDiemsions.height, bounds.height); - const paddingRight = parsePadding(pRight, viewportDiemsions.width, bounds.width); - const paddingBottom = parsePadding(pBottom, viewportDiemsions.height, bounds.height); - const paddingLeft = parsePadding(pLeft, viewportDiemsions.width, bounds.width); + const paddingTop = parsePadding(pTop, height); + const paddingRight = parsePadding(pRight, width); + const paddingBottom = parsePadding(pBottom, height); + const paddingLeft = parsePadding(pLeft, width); return [paddingTop, paddingRight, paddingBottom, paddingLeft]; } } @@ -262,23 +267,24 @@ export const getViewportForBounds = ( height: number, minZoom: number, maxZoom: number, - padding: Padding = 0 + padding: Padding ): Viewport => { - // const [paddingX, paddingY] = Array.isArray(padding) ? [padding[0], padding[1]] : [padding, padding]; + const [paddingTop, paddingRight, paddingBottom, paddingLeft] = parsePaddings(padding, width, height); + const paddingX = paddingLeft + paddingRight; + const paddingY = paddingTop + paddingBottom; - // const isPixelPadding = paddingUnit === 'px'; + console.log({ paddingTop, paddingRight, paddingBottom, paddingLeft }); + console.log(paddingX / paddingLeft); - const [paddingTop, paddingRight, paddingBottom, paddingLeft] = parsePaddings(padding, { width, height }, bounds); - - const xZoom = (width - paddingLeft - paddingRight) / bounds.width; - const yZoom = (height - paddingTop - paddingBottom) / bounds.height; + const xZoom = (width - paddingX) / bounds.width; + const yZoom = (height - paddingY) / bounds.height; const zoom = Math.min(xZoom, yZoom); const clampedZoom = clamp(zoom, minZoom, maxZoom); const boundsCenterX = bounds.x + bounds.width / 2; const boundsCenterY = bounds.y + bounds.height / 2; - const x = width / 2 - boundsCenterX * clampedZoom; - const y = height / 2 - boundsCenterY * clampedZoom; + const x = width / 2 - boundsCenterX * clampedZoom - paddingX * 0.5 + paddingLeft; + const y = height / 2 - boundsCenterY * clampedZoom - paddingY * 0.5 + paddingTop; return { x, y, zoom: clampedZoom }; }; From e3ecf999aad558b2bc493648f6049da7a1fc87d2 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 25 Mar 2025 18:05:58 +0100 Subject: [PATCH 12/21] correctly apply asymetric paddings --- examples/react/src/examples/Basic/index.tsx | 3 +- packages/system/src/utils/general.ts | 35 +++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index e399ab88..6b538593 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -147,7 +147,8 @@ const BasicFlow = () => { maxZoom={4} fitView fitViewOptions={{ - padding: ['30%', '10%', '10%', '20%'], + // top, right, bottom, left + padding: ['100px', '0%', '0%', '50px'], }} defaultEdgeOptions={defaultEdgeOptions} selectNodesOnDrag={false} diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 0d47025b..f6b796a7 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -273,9 +273,6 @@ export const getViewportForBounds = ( const paddingX = paddingLeft + paddingRight; const paddingY = paddingTop + paddingBottom; - console.log({ paddingTop, paddingRight, paddingBottom, paddingLeft }); - console.log(paddingX / paddingLeft); - const xZoom = (width - paddingX) / bounds.width; const yZoom = (height - paddingY) / bounds.height; @@ -283,12 +280,38 @@ export const getViewportForBounds = ( const clampedZoom = clamp(zoom, minZoom, maxZoom); const boundsCenterX = bounds.x + bounds.width / 2; const boundsCenterY = bounds.y + bounds.height / 2; - const x = width / 2 - boundsCenterX * clampedZoom - paddingX * 0.5 + paddingLeft; - const y = height / 2 - boundsCenterY * clampedZoom - paddingY * 0.5 + paddingTop; + const x = width / 2 - boundsCenterX * clampedZoom; + const y = height / 2 - boundsCenterY * clampedZoom; - return { x, y, zoom: clampedZoom }; + const realPadding = calculatePadding(bounds, x, y, clampedZoom, width, height); + + const paddingDifferences = { + left: Math.min(Math.floor(realPadding.left) - Math.floor(paddingLeft), 0), + top: Math.min(Math.floor(realPadding.top) - Math.floor(paddingTop), 0), + right: Math.min(Math.floor(realPadding.right) - Math.floor(paddingRight), 0), + bottom: Math.min(Math.floor(realPadding.bottom) - Math.floor(paddingBottom), 0), + }; + return { + x: x - paddingDifferences.left + paddingDifferences.right, + y: y - paddingDifferences.top + paddingDifferences.bottom, + zoom: clampedZoom, + }; }; +function calculatePadding(bounds: Rect, x: number, y: number, zoom: number, width: number, height: number) { + const { x: paddingLeft, y: paddingTop } = rendererPointToPoint(bounds, [x, y, zoom]); + + const { x: boundRight, y: boundBottom } = rendererPointToPoint( + { x: bounds.x + bounds.width, y: bounds.y + bounds.height }, + [x, y, zoom] + ); + + const paddingRight = width - boundRight; + const paddingBottom = height - boundBottom; + + return { left: paddingLeft, top: paddingTop, right: paddingRight, bottom: paddingBottom }; +} + export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0; export function isCoordinateExtent(extent?: CoordinateExtent | 'parent'): extent is CoordinateExtent { From 48d08379392816dcc00c312c5c534d21ef1ee38c Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 26 Mar 2025 12:49:43 +0100 Subject: [PATCH 13/21] use object instead of array for paddings & clean up fitView code --- examples/react/src/examples/Basic/index.tsx | 3 +- packages/system/src/types/general.ts | 17 +-- packages/system/src/utils/general.ts | 161 +++++++++++--------- 3 files changed, 99 insertions(+), 82 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index 6b538593..5754fd62 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -147,8 +147,7 @@ const BasicFlow = () => { maxZoom={4} fitView fitViewOptions={{ - // top, right, bottom, left - padding: ['100px', '0%', '0%', '50px'], + padding: { top: '100px', left: '0%', right: '10%', bottom: 0.1 }, }} defaultEdgeOptions={defaultEdgeOptions} selectNodesOnDrag={false} diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 806b354e..2a34e9ff 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -96,15 +96,14 @@ export type PaddingWithUnit = `${number}${PaddingUnit}` | number; export type Padding = | PaddingWithUnit - | [padding: PaddingWithUnit] - | [paddingY: PaddingWithUnit, paddingX: PaddingWithUnit] - | [paddingTop: PaddingWithUnit, paddingX: PaddingWithUnit, paddingBottom: PaddingWithUnit] - | [ - paddingTop: PaddingWithUnit, - paddingRight: PaddingWithUnit, - paddingBottom: PaddingWithUnit, - paddingLeft: PaddingWithUnit - ]; + | { + top?: PaddingWithUnit; + right?: PaddingWithUnit; + bottom?: PaddingWithUnit; + left?: PaddingWithUnit; + x?: PaddingWithUnit; + y?: PaddingWithUnit; + }; /** * @inline diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index f6b796a7..5b4c6995 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -175,74 +175,103 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra }; }; -function paddingError(padding: PaddingWithUnit) { - console.error( - `[React Flow] The padding value "${padding}" is invalid. Please provide a number or a string with a valid unit (px or %).` - ); -} - +/** + * Parses a single padding value to a number + * @internal + * @param padding - Padding to parse + * @param viewport - Width or height of the viewport + * @returns The padding in pixels + */ function parsePadding(padding: PaddingWithUnit, viewport: number): number { if (typeof padding === 'number') { - return viewport - viewport / (1 + padding * 0.5); + return Math.floor(viewport - viewport / (1 + padding)); } if (typeof padding === 'string' && padding.endsWith('px')) { const paddingValue = parseFloat(padding); if (!Number.isNaN(paddingValue)) { - return paddingValue; + return Math.floor(paddingValue); } } if (typeof padding === 'string' && padding.endsWith('%')) { const paddingValue = parseFloat(padding); if (!Number.isNaN(paddingValue)) { - return viewport * paddingValue * 0.01; + return Math.floor(viewport * paddingValue * 0.01); } } - paddingError(padding); + console.error( + `[React Flow] The padding value "${padding}" is invalid. Please provide a number or a string with a valid unit (px or %).` + ); return 0; } -function parsePaddings(padding: Padding, width: number, height: number): [number, number, number, number] { +/** + * Parses the paddings to an object with top, right, bottom, left, x and y paddings + * @internal + * @param padding - Padding to parse + * @param width - Width of the viewport + * @param height - Height of the viewport + * @returns An object with the paddings in pixels + */ +function parsePaddings( + padding: Padding, + width: number, + height: number +): { top: number; bottom: number; left: number; right: number; x: number; y: number } { if (typeof padding === 'string' || typeof padding === 'number') { const paddingY = parsePadding(padding, height); const paddingX = parsePadding(padding, width); - return [paddingY, paddingX, paddingY, paddingX]; + return { + top: paddingY, + right: paddingX, + bottom: paddingY, + left: paddingX, + x: paddingX * 2, + y: paddingY * 2, + }; } - if (Array.isArray(padding)) { - switch (padding.length) { - case 1: { - const paddingY = parsePadding(padding[0], height); - const paddingX = parsePadding(padding[0], width); - return [paddingY, paddingX, paddingY, paddingX]; - } - case 2: { - const [pY, pX] = padding; - const paddingY = parsePadding(pY, height); - const paddingX = parsePadding(pX, width); - return [paddingY, paddingX, paddingY, paddingX]; - } - case 3: { - const [pTop, pX, pBottom] = padding; - const paddingTop = parsePadding(pTop, height); - const padddingX = parsePadding(pX, width); - const paddingBottom = parsePadding(pBottom, height); - return [paddingTop, padddingX, paddingBottom, padddingX]; - } - case 4: { - const [pTop, pRight, pBottom, pLeft] = padding; - const paddingTop = parsePadding(pTop, height); - const paddingRight = parsePadding(pRight, width); - const paddingBottom = parsePadding(pBottom, height); - const paddingLeft = parsePadding(pLeft, width); - return [paddingTop, paddingRight, paddingBottom, paddingLeft]; - } - } + if (typeof padding === 'object') { + const top = parsePadding(padding.top ?? padding.y ?? 0, height); + const bottom = parsePadding(padding.bottom ?? padding.y ?? 0, height); + const left = parsePadding(padding.left ?? padding.x ?? 0, width); + const right = parsePadding(padding.right ?? padding.x ?? 0, width); + return { top, right, bottom, left, x: left + right, y: top + bottom }; } - return [0, 0, 0, 0]; + return { top: 0, right: 0, bottom: 0, left: 0, x: 0, y: 0 }; +} + +/** + * Calculates the resulting paddings if the new viewport is applied + * @internal + * @param bounds - Bounds to fit inside viewport + * @param x - X position of the viewport + * @param y - Y position of the viewport + * @param zoom - Zoom level of the viewport + * @param width - Width of the viewport + * @param height - Height of the viewport + * @returns An object with the minimum padding required to fit the bounds inside the viewport + */ +function calculateAppliedPaddings(bounds: Rect, x: number, y: number, zoom: number, width: number, height: number) { + const { x: left, y: top } = rendererPointToPoint(bounds, [x, y, zoom]); + + const { x: boundRight, y: boundBottom } = rendererPointToPoint( + { x: bounds.x + bounds.width, y: bounds.y + bounds.height }, + [x, y, zoom] + ); + + const right = width - boundRight; + const bottom = height - boundBottom; + + return { + left: Math.floor(left), + top: Math.floor(top), + right: Math.floor(right), + bottom: Math.floor(bottom), + }; } /** @@ -258,8 +287,8 @@ function parsePaddings(padding: Padding, width: number, height: number): [number * @returns A transforned {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport} * @example * const { x, y, zoom } = getViewportForBounds( - *{ x: 0, y: 0, width: 100, height: 100}, - *1200, 800, 0.5, 2); + * { x: 0, y: 0, width: 100, height: 100}, + * 1200, 800, 0.5, 2); */ export const getViewportForBounds = ( bounds: Rect, @@ -269,49 +298,39 @@ export const getViewportForBounds = ( maxZoom: number, padding: Padding ): Viewport => { - const [paddingTop, paddingRight, paddingBottom, paddingLeft] = parsePaddings(padding, width, height); - const paddingX = paddingLeft + paddingRight; - const paddingY = paddingTop + paddingBottom; + // First we resolve all the paddings to actual pixel values + const p = parsePaddings(padding, width, height); - const xZoom = (width - paddingX) / bounds.width; - const yZoom = (height - paddingY) / bounds.height; + const xZoom = (width - p.x) / bounds.width; + const yZoom = (height - p.y) / bounds.height; + // We calculate the new x, y, zoom for a centered view const zoom = Math.min(xZoom, yZoom); const clampedZoom = clamp(zoom, minZoom, maxZoom); + const boundsCenterX = bounds.x + bounds.width / 2; const boundsCenterY = bounds.y + bounds.height / 2; const x = width / 2 - boundsCenterX * clampedZoom; const y = height / 2 - boundsCenterY * clampedZoom; - const realPadding = calculatePadding(bounds, x, y, clampedZoom, width, height); + // Then we calculate the minimum padding, to respect asymmetric paddings + const newPadding = calculateAppliedPaddings(bounds, x, y, clampedZoom, width, height); - const paddingDifferences = { - left: Math.min(Math.floor(realPadding.left) - Math.floor(paddingLeft), 0), - top: Math.min(Math.floor(realPadding.top) - Math.floor(paddingTop), 0), - right: Math.min(Math.floor(realPadding.right) - Math.floor(paddingRight), 0), - bottom: Math.min(Math.floor(realPadding.bottom) - Math.floor(paddingBottom), 0), + // We only want to have an offset if the newPadding is smaller than the required padding + const offset = { + left: Math.min(newPadding.left - p.left, 0), + top: Math.min(newPadding.top - p.top, 0), + right: Math.min(newPadding.right - p.right, 0), + bottom: Math.min(newPadding.bottom - p.bottom, 0), }; + return { - x: x - paddingDifferences.left + paddingDifferences.right, - y: y - paddingDifferences.top + paddingDifferences.bottom, + x: x - offset.left + offset.right, + y: y - offset.top + offset.bottom, zoom: clampedZoom, }; }; -function calculatePadding(bounds: Rect, x: number, y: number, zoom: number, width: number, height: number) { - const { x: paddingLeft, y: paddingTop } = rendererPointToPoint(bounds, [x, y, zoom]); - - const { x: boundRight, y: boundBottom } = rendererPointToPoint( - { x: bounds.x + bounds.width, y: bounds.y + bounds.height }, - [x, y, zoom] - ); - - const paddingRight = width - boundRight; - const paddingBottom = height - boundBottom; - - return { left: paddingLeft, top: paddingTop, right: paddingRight, bottom: paddingBottom }; -} - export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0; export function isCoordinateExtent(extent?: CoordinateExtent | 'parent'): extent is CoordinateExtent { From acba901d861aa84cb5beba60b24fff4cfde7ada6 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 26 Mar 2025 12:57:16 +0100 Subject: [PATCH 14/21] make two seperate changesets for fix and the new features --- .changeset/hungry-shrimps-remain.md | 5 +++++ .changeset/poor-poems-visit.md | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/hungry-shrimps-remain.md diff --git a/.changeset/hungry-shrimps-remain.md b/.changeset/hungry-shrimps-remain.md new file mode 100644 index 00000000..abf4ef75 --- /dev/null +++ b/.changeset/hungry-shrimps-remain.md @@ -0,0 +1,5 @@ +--- +'@xyflow/react': minor +--- + +You can now express paddings in fitViewOptions as pixels ('30px'), as viewport percentages ('20%') and define different paddings for each side. diff --git a/.changeset/poor-poems-visit.md b/.changeset/poor-poems-visit.md index 4aeb827d..8a258582 100644 --- a/.changeset/poor-poems-visit.md +++ b/.changeset/poor-poems-visit.md @@ -1,7 +1,7 @@ --- -'@xyflow/react': minor +'@xyflow/react': patch '@xyflow/svelte': patch '@xyflow/system': patch --- -Fix fitView not working when adding new nodes +Fix fitView not working immediately after adding new nodes From 85e212a5dc8e2ada03b97f2053b2bf40768557f2 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 26 Mar 2025 14:02:37 +0100 Subject: [PATCH 15/21] fix rounding error in pane test --- tests/playwright/e2e/pane.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/playwright/e2e/pane.spec.ts b/tests/playwright/e2e/pane.spec.ts index fade5e0c..802e0139 100644 --- a/tests/playwright/e2e/pane.spec.ts +++ b/tests/playwright/e2e/pane.spec.ts @@ -30,8 +30,8 @@ test.describe('Pane default', () => { const transformsAfter = await getTransform(viewport); - expect(transformsAfter.translateX - transformsBefore.translateX).toBe(100); - expect(transformsAfter.translateY - transformsBefore.translateY).toBe(100); + expect(Math.floor(transformsAfter.translateX - transformsBefore.translateX)).toBe(100); + expect(Math.floor(transformsAfter.translateY - transformsBefore.translateY)).toBe(100); }); test('scrolling the default pane zooms it', async ({ page }) => { From 5e810bc01c1e2c71687819460a0d9e6a5217db55 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 27 Mar 2025 11:01:01 +0100 Subject: [PATCH 16/21] remove console.log --- packages/svelte/src/lib/store/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index 0d453c45..76b100b6 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -175,7 +175,6 @@ export const createNodesStore = ( console.log(nodesInitialized); if (get(fitViewQueued) && nodesInitialized && get(panZoom)) { - console.log('trying'); const fitViewPromise = fitViewport( { nodes: nodeLookup, From e078c55442bf363922aa70ecb6d1a812cf47435f Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 27 Mar 2025 11:01:28 +0100 Subject: [PATCH 17/21] remove console.log --- packages/svelte/src/lib/store/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index b0d70a5e..83c6c623 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -144,7 +144,6 @@ export function createStore({ } function fitView(options?: FitViewOptions) { - console.log('fitView Store'); // We either create a new Promise or reuse the existing one // Even if fitView is called multiple times in a row, we only end up with a single Promise const fitViewResolver = get(store.fitViewResolver) ?? Promise.withResolvers(); From b0dc7baa802a8566cc2a0ff06703073894c4d2be Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 27 Mar 2025 11:02:21 +0100 Subject: [PATCH 18/21] remove console.log --- packages/svelte/src/lib/store/utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index 76b100b6..e924d832 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -172,8 +172,6 @@ export const createNodesStore = ( checkEquality: false }); - console.log(nodesInitialized); - if (get(fitViewQueued) && nodesInitialized && get(panZoom)) { const fitViewPromise = fitViewport( { From ed1cb4da31481c4e556cad93ef1f88ecf6437507 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 27 Mar 2025 11:06:57 +0100 Subject: [PATCH 19/21] use async function instead of .then --- packages/react/src/store/index.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index e099ca95..deb30c29 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -42,14 +42,14 @@ const createStore = ({ nodeExtent?: CoordinateExtent; }) => createWithEqualityFn((set, get) => { - function resolveFitView() { + async function resolveFitView() { const { nodeLookup, panZoom, fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get(); if (!panZoom) { return; } - const fitViewPromise = fitViewport( + const viewFitted = await fitViewport( { nodes: nodeLookup, width, @@ -60,10 +60,9 @@ const createStore = ({ }, fitViewOptions ); - fitViewPromise.then((value) => { - fitViewResolver?.resolve(value); - set({ fitViewResolver: null }); - }); + + fitViewResolver?.resolve(viewFitted); + set({ fitViewResolver: null }); } return { From 9be527606989152a66d9f3427487bb8ee6b4d92f Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 27 Mar 2025 11:12:18 +0100 Subject: [PATCH 20/21] simplify resolveFitView --- packages/react/src/store/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index deb30c29..8ca0b050 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -49,7 +49,7 @@ const createStore = ({ return; } - const viewFitted = await fitViewport( + await fitViewport( { nodes: nodeLookup, width, @@ -61,7 +61,7 @@ const createStore = ({ fitViewOptions ); - fitViewResolver?.resolve(viewFitted); + fitViewResolver?.resolve(true); set({ fitViewResolver: null }); } From 3ac2846d5e359d18a2d9ccf6ae9c24c2a2e39b6d Mon Sep 17 00:00:00 2001 From: peterkogo Date: Thu, 27 Mar 2025 12:03:47 +0100 Subject: [PATCH 21/21] add comment --- packages/react/src/store/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 8ca0b050..8e64b51c 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -45,7 +45,7 @@ const createStore = ({ async function resolveFitView() { const { nodeLookup, panZoom, fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get(); - if (!panZoom) { + if (!panZoom || !fitViewResolver) { return; } @@ -61,7 +61,11 @@ const createStore = ({ fitViewOptions ); - fitViewResolver?.resolve(true); + fitViewResolver.resolve(true); + /** + * wait for the fitViewport to resolve before deleting the resolver, + * we want to reuse the old resolver if the user calls fitView again in the mean time + */ set({ fitViewResolver: null }); }