implemented new fitView logic

This commit is contained in:
peterkogo
2025-04-09 10:36:16 +02:00
parent 8b5284697c
commit 9383ba5554
132 changed files with 5506 additions and 2297 deletions
+81 -112
View File
@@ -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,
@@ -42,25 +41,60 @@ const createStore = ({
nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent;
}) =>
createWithEqualityFn<ReactFlowState>(
(set, get) => ({
createWithEqualityFn<ReactFlowState>((set, get) => {
async function resolveFitView() {
const { nodeLookup, panZoom, fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get();
if (!panZoom) {
return;
}
await fitViewport(
{
nodes: nodeLookup,
width,
height,
panZoom,
minZoom,
maxZoom,
},
fitViewOptions
);
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 });
}
return {
...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }),
setNodes: (nodes: Node[]) => {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get();
// setNodes() is called exclusively in response to user actions:
// - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
// - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
//
// 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 { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued } = get();
/*
* setNodes() is called exclusively in response to user actions:
* - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
* - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
*
* When this happens, we take the note objects passed by the user and extend them with fields
* relevant for internal React Flow operations.
*/
const nodesInitialized = adoptUserNodes(nodes, nodeLookup, parentLookup, {
nodeOrigin,
nodeExtent,
elevateNodesOnSelect,
checkEquality: true,
});
set({ nodes });
if (fitViewQueued && nodesInitialized) {
resolveFitView();
set({ nodes, fitViewQueued: false, fitViewOptions: undefined });
} else {
set({ nodes });
}
},
setEdges: (edges: Edge[]) => {
const { connectionLookup, edgeLookup } = get();
@@ -81,23 +115,14 @@ const createStore = ({
set({ hasDefaultEdges: true });
}
},
// Every node gets registerd at a ResizeObserver. Whenever a node
// 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();
/*
* Every node gets registerd at a ResizeObserver. Whenever a node
* changes its dimensions, this function is called to measure the
* new dimensions and update the nodes.
*/
updateNodeInternals: (updates) => {
const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug, fitViewQueued } =
get();
const { changes, updatedInternals } = updateNodeInternalsSystem(
updates,
@@ -114,23 +139,9 @@ 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 });
if (fitViewQueued) {
resolveFitView();
set({ fitViewQueued: false, fitViewOptions: undefined });
} else {
// we always want to trigger useStore calls whenever updateNodeInternals is called
set({});
@@ -146,9 +157,12 @@ const createStore = ({
updateNodePositions: (nodeDragItems, dragging = false) => {
const parentExpandChildren: ParentExpandChild[] = [];
const changes = [];
const { nodeLookup, triggerNodeChanges } = get();
for (const [id, dragItem] of nodeDragItems) {
const expandParent = !!(dragItem?.expandParent && dragItem?.parentId && dragItem?.position);
// we are using the nodelookup to be sure to use the current expandParent and parentId value
const node = nodeLookup.get(id);
const expandParent = !!(node?.expandParent && node?.parentId && dragItem?.position);
const change: NodeChange = {
id,
@@ -162,14 +176,14 @@ const createStore = ({
dragging,
};
if (expandParent) {
if (expandParent && node.parentId) {
parentExpandChildren.push({
id,
parentId: dragItem.parentId!,
parentId: node.parentId,
rect: {
...dragItem.internals.positionAbsolute,
width: dragItem.measured.width!,
height: dragItem.measured.height!,
width: dragItem.measured.width ?? 0,
height: dragItem.measured.height ?? 0,
},
});
}
@@ -178,12 +192,12 @@ const createStore = ({
}
if (parentExpandChildren.length > 0) {
const { nodeLookup, parentLookup, nodeOrigin } = get();
const { parentLookup, nodeOrigin } = get();
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges);
}
get().triggerNodeChanges(changes);
triggerNodeChanges(changes);
},
triggerNodeChanges: (changes) => {
const { onNodesChange, setNodes, nodes, hasDefaultNodes, debug } = get();
@@ -222,7 +236,7 @@ const createStore = ({
if (multiSelectionActive) {
const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
triggerNodeChanges(nodeChanges as NodeSelectionChange[]);
triggerNodeChanges(nodeChanges);
return;
}
@@ -234,7 +248,7 @@ const createStore = ({
if (multiSelectionActive) {
const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
triggerEdgeChanges(changedEdges as EdgeSelectionChange[]);
triggerEdgeChanges(changedEdges);
return;
}
@@ -248,8 +262,10 @@ const createStore = ({
const nodeChanges = nodesToUnselect.map((n) => {
const internalNode = nodeLookup.get(n.id);
if (internalNode) {
// we need to unselect the internal node that was selected previously before we
// send the change to the user to prevent it to be selected while dragging the new node
/*
* we need to unselect the internal node that was selected previously before we
* send the change to the user to prevent it to be selected while dragging the new node
*/
internalNode.selected = false;
}
@@ -257,8 +273,8 @@ const createStore = ({
});
const edgeChanges = edgesToUnselect.map((edge) => createSelectionChange(edge.id, false));
triggerNodeChanges(nodeChanges as NodeSelectionChange[]);
triggerEdgeChanges(edgeChanges as EdgeSelectionChange[]);
triggerNodeChanges(nodeChanges);
triggerEdgeChanges(edgeChanges);
},
setMinZoom: (minZoom) => {
const { panZoom, maxZoom } = get();
@@ -284,11 +300,11 @@ const createStore = ({
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
const nodeChanges = nodes.reduce<NodeSelectionChange[]>(
(res, node) => (node.selected ? [...res, createSelectionChange(node.id, false) as NodeSelectionChange] : res),
(res, node) => (node.selected ? [...res, createSelectionChange(node.id, false)] : res),
[]
);
const edgeChanges = edges.reduce<EdgeSelectionChange[]>(
(res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false) as EdgeSelectionChange] : res),
(res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false)] : res),
[]
);
@@ -321,52 +337,6 @@ const createStore = ({
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
},
fitView: (options?: FitViewOptions): Promise<boolean> => {
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 },
@@ -377,8 +347,7 @@ const createStore = ({
},
reset: () => set({ ...getInitialState() }),
}),
Object.is
);
};
}, Object.is);
export { createStore };
+4 -3
View File
@@ -104,13 +104,14 @@ const getInitialState = ({
elementsSelectable: true,
elevateNodesOnSelect: true,
elevateEdgesOnSelect: false,
fitViewOnInit: false,
fitViewDone: false,
fitViewOnInitOptions: undefined,
selectNodesOnDrag: true,
multiSelectionActive: false,
fitViewQueued: fitView ?? false,
fitViewOptions: undefined,
fitViewResolver: null,
connection: { ...initialConnection },
connectionClickStartHandle: null,
connectOnClick: true,