diff --git a/examples/react/cypress/components/hooks/useNodes.cy.tsx b/examples/react/cypress/components/hooks/useNodes.cy.tsx
index 9c10ad70..340b4b10 100644
--- a/examples/react/cypress/components/hooks/useNodes.cy.tsx
+++ b/examples/react/cypress/components/hooks/useNodes.cy.tsx
@@ -15,8 +15,7 @@ const initialNodes: Node[] = nodes.map((n) => ({
const expectedNodes: Node[] = initialNodes.map((n) => ({
...n,
- computed: {
- positionAbsolute: n.position,
+ measured: {
...nodeDimensions,
},
}));
diff --git a/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx b/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx
index a15fc67e..bceb4cbd 100644
--- a/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx
+++ b/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx
@@ -48,7 +48,7 @@ describe(': onNodesChange', () => {
id: '1',
item: {
...nodes[0],
- computed: { positionAbsolute: nodes[0].position, width: 200, height: 100 },
+ measured: { width: 200, height: 100 },
style: { width: 200, height: 100 },
},
},
diff --git a/examples/react/cypress/components/utils/apply-changes.cy.ts b/examples/react/cypress/components/utils/apply-changes.cy.ts
index 1548b7e9..0a75db7b 100644
--- a/examples/react/cypress/components/utils/apply-changes.cy.ts
+++ b/examples/react/cypress/components/utils/apply-changes.cy.ts
@@ -136,8 +136,8 @@ describe('applyChanges Testing', () => {
];
const nextNodes = applyNodeChanges(nodeChanges, nodes);
- expect(nodes[0].computed).to.be.undefined;
- expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight });
+ expect(nodes[0].measured).to.be.undefined;
+ expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
expect(nextNodes[0].width).to.be.undefined;
expect(nextNodes[0].height).to.be.undefined;
});
@@ -153,7 +153,7 @@ describe('applyChanges Testing', () => {
const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes[0].position).to.be.deep.equal(newPosition);
- expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight });
+ expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
});
it('replaces nodes/edges', () => {
diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx
index cafa9fa8..62232a35 100644
--- a/examples/react/src/examples/Basic/index.tsx
+++ b/examples/react/src/examples/Basic/index.tsx
@@ -56,18 +56,19 @@ const initialEdges: Edge[] = [
const defaultEdgeOptions = {};
const BasicFlow = () => {
- const { setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
+ const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
useReactFlow();
const updatePos = () => {
setNodes((nodes) =>
nodes.map((node) => {
- node.position = {
- x: Math.random() * 400,
- y: Math.random() * 400,
+ return {
+ ...node,
+ position: {
+ x: Math.random() * 400,
+ y: Math.random() * 400,
+ },
};
-
- return node;
})
);
};
@@ -78,9 +79,10 @@ const BasicFlow = () => {
const toggleClassnames = () => {
setNodes((nodes) =>
nodes.map((node) => {
- node.className = node.className === 'light' ? 'dark' : 'light';
-
- return node;
+ return {
+ ...node,
+ className: node.className === 'light' ? 'dark' : 'light',
+ };
})
);
};
@@ -108,6 +110,14 @@ const BasicFlow = () => {
updateNodeData('1', { label: 'update' });
updateNodeData('2', { label: 'update' });
};
+ const addNode = () => {
+ addNodes({
+ id: `${Math.random()}`,
+ data: { label: 'Node' },
+ position: { x: Math.random() * 300, y: Math.random() * 300 },
+ className: 'light',
+ });
+ };
return (
{
+
);
diff --git a/examples/react/src/examples/EasyConnect/utils.tsx b/examples/react/src/examples/EasyConnect/utils.tsx
index 6d87795b..c6693015 100644
--- a/examples/react/src/examples/EasyConnect/utils.tsx
+++ b/examples/react/src/examples/EasyConnect/utils.tsx
@@ -9,8 +9,8 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
width: intersectionNodeWidth,
height: intersectionNodeHeight,
positionAbsolute: intersectionNodePosition,
- } = intersectionNode.computed || {};
- const targetPosition = targetNode.computed?.positionAbsolute!;
+ } = intersectionNode.measured || {};
+ const targetPosition = targetNode.measured?.positionAbsolute!;
const w = intersectionNodeWidth! / 2;
const h = intersectionNodeHeight! / 2;
@@ -33,7 +33,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
// returns the position (top,right,bottom or right) passed node compared to the intersection point
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
- const n = { ...node.computed?.positionAbsolute, ...node };
+ const n = { ...node.measured?.positionAbsolute, ...node };
const nx = Math.round(n.x!);
const ny = Math.round(n.y!);
const px = Math.round(intersectionPoint.x);
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) {
return Position.Left;
}
- if (px >= nx + n.computed?.width! - 1) {
+ if (px >= nx + n.measured?.width! - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
- if (py >= n.y! + n.computed?.height! - 1) {
+ if (py >= n.y! + n.measured?.height! - 1) {
return Position.Bottom;
}
diff --git a/examples/react/src/examples/FloatingEdges/FloatingEdge.tsx b/examples/react/src/examples/FloatingEdges/FloatingEdge.tsx
index 6ee9e1e2..0856d012 100644
--- a/examples/react/src/examples/FloatingEdges/FloatingEdge.tsx
+++ b/examples/react/src/examples/FloatingEdges/FloatingEdge.tsx
@@ -5,8 +5,8 @@ import { getEdgeParams } from './utils';
const FloatingEdge: FC = ({ id, source, target, style }) => {
const { sourceNode, targetNode } = useStore((s) => {
- const sourceNode = s.nodes.find((n) => n.id === source);
- const targetNode = s.nodes.find((n) => n.id === target);
+ const sourceNode = s.nodeLookup.get(source);
+ const targetNode = s.nodeLookup.get(target);
return { sourceNode, targetNode };
});
diff --git a/examples/react/src/examples/FloatingEdges/utils.ts b/examples/react/src/examples/FloatingEdges/utils.ts
index c8a6e1c1..08593c5c 100644
--- a/examples/react/src/examples/FloatingEdges/utils.ts
+++ b/examples/react/src/examples/FloatingEdges/utils.ts
@@ -1,4 +1,4 @@
-import { Position, XYPosition, Node, Edge } from '@xyflow/react';
+import { Position, XYPosition, Node, Edge, InternalNode } from '@xyflow/react';
// this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node
@@ -6,7 +6,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPositi
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const { position: intersectionNodePosition } = intersectionNode;
- const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.computed ?? {
+ const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured ?? {
width: 0,
height: 0,
};
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) {
return Position.Left;
}
- if (px >= nx + (n.computed?.width ?? 0) - 1) {
+ if (px >= nx + (n.measured?.width ?? 0) - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
- if (py >= n.y + (n.computed?.height ?? 0) - 1) {
+ if (py >= n.y + (n.measured?.height ?? 0) - 1) {
return Position.Bottom;
}
@@ -56,7 +56,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
}
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
-export function getEdgeParams(source: Node, target: Node) {
+export function getEdgeParams(source: InternalNode, target: InternalNode) {
const sourceIntersectionPoint = getNodeIntersection(source, target);
const targetIntersectionPoint = getNodeIntersection(target, source);
diff --git a/examples/react/src/examples/NodeResizer/index.tsx b/examples/react/src/examples/NodeResizer/index.tsx
index b0a272f2..7afb411f 100644
--- a/examples/react/src/examples/NodeResizer/index.tsx
+++ b/examples/react/src/examples/NodeResizer/index.tsx
@@ -137,7 +137,7 @@ const initialNodes: Node[] = [
label: 'Child with extent: parent',
},
position: { x: 50, y: 50 },
- parentNode: '5',
+ parentId: '5',
extent: 'parent',
width: 50,
height: 100,
@@ -148,7 +148,7 @@ const initialNodes: Node[] = [
type: 'defaultResizer',
data: { label: 'Child with expandParent' },
position: { x: 150, y: 100 },
- parentNode: '5',
+ parentId: '5',
expandParent: true,
style: { ...nodeStyle },
},
@@ -157,7 +157,7 @@ const initialNodes: Node[] = [
type: 'defaultResizer',
data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true },
position: { x: 25, y: 200 },
- parentNode: '5',
+ parentId: '5',
expandParent: true,
style: { ...nodeStyle },
},
diff --git a/examples/react/src/examples/Subflow/index.tsx b/examples/react/src/examples/Subflow/index.tsx
index 8ea28e34..7f1b3f4b 100644
--- a/examples/react/src/examples/Subflow/index.tsx
+++ b/examples/react/src/examples/Subflow/index.tsx
@@ -50,7 +50,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 4a' },
position: { x: 15, y: 15 },
className: 'light',
- parentNode: '4',
+ parentId: '4',
origin: [0.5, 0.5],
extent: [
@@ -68,21 +68,21 @@ const initialNodes: Node[] = [
height: 200,
width: 300,
},
- parentNode: '4',
+ parentId: '4',
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 40, y: 20 },
className: 'light',
- parentNode: '4b',
+ parentId: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 20, y: 100 },
className: 'light',
- parentNode: '4b',
+ parentId: '4b',
},
{
id: '5',
@@ -98,7 +98,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 5a' },
position: { x: 0, y: 0 },
className: 'light',
- parentNode: '5',
+ parentId: '5',
extent: 'parent',
},
{
@@ -106,7 +106,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 5b' },
position: { x: 225, y: 50 },
className: 'light',
- parentNode: '5',
+ parentId: '5',
expandParent: true,
},
{
@@ -160,7 +160,7 @@ const Subflow = () => {
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
- if (!n.parentNode) {
+ if (!n.parentId) {
return {
...n,
position: {
@@ -194,7 +194,7 @@ const Subflow = () => {
return nds.map((n) => {
return {
...n,
- hidden: !!n.parentNode && !n.hidden,
+ hidden: !!n.parentId && !n.hidden,
};
});
});
diff --git a/examples/svelte/src/routes/examples/node-resizer/+page.svelte b/examples/svelte/src/routes/examples/node-resizer/+page.svelte
index b7479b03..8e08174a 100644
--- a/examples/svelte/src/routes/examples/node-resizer/+page.svelte
+++ b/examples/svelte/src/routes/examples/node-resizer/+page.svelte
@@ -113,7 +113,7 @@
type: 'defaultResizer',
data: { label: 'Child with extent parent' },
position: { x: 50, y: 50 },
- parentNode: '5',
+ parentId: '5',
extent: 'parent',
style: nodeStyle
},
@@ -122,7 +122,7 @@
type: 'defaultResizer',
data: { label: 'Child' },
position: { x: 100, y: 100 },
- parentNode: '5',
+ parentId: '5',
style: nodeStyle
}
]);
diff --git a/examples/svelte/src/routes/examples/subflows/+page.svelte b/examples/svelte/src/routes/examples/subflows/+page.svelte
index 4c57c171..a4574a6e 100644
--- a/examples/svelte/src/routes/examples/subflows/+page.svelte
+++ b/examples/svelte/src/routes/examples/subflows/+page.svelte
@@ -36,7 +36,7 @@
id: '4a',
data: { label: 'Node 4a' },
position: { x: 15, y: 15 },
- parentNode: '4',
+ parentId: '4',
extent: [
[0, 0],
[100, 100]
@@ -47,19 +47,19 @@
data: { label: 'Node 4b' },
position: { x: 100, y: 60 },
style: 'width: 300px; height: 200px;',
- parentNode: '4'
+ parentId: '4'
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 40, y: 20 },
- parentNode: '4b'
+ parentId: '4b'
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 20, y: 100 },
- parentNode: '4b'
+ parentId: '4b'
},
{
id: '5',
@@ -73,14 +73,14 @@
id: '5a',
data: { label: 'Node 5a' },
position: { x: 0, y: 0 },
- parentNode: '5',
+ parentId: '5',
extent: 'parent'
},
{
id: '5b',
data: { label: 'Node 5b' },
position: { x: 225, y: 50 },
- parentNode: '5',
+ parentId: '5',
expandParent: true
},
{
diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx
index 17a3f7f2..0a05890d 100644
--- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx
+++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx
@@ -3,7 +3,7 @@
import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
-import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
+import { getInternalNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { Panel } from '../../components/Panel';
@@ -26,7 +26,9 @@ const selector = (s: ReactFlowState) => {
return {
viewBB,
boundingRect:
- s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, { nodeOrigin: s.nodeOrigin }), viewBB) : viewBB,
+ s.nodeLookup.size > 0
+ ? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { nodeOrigin: s.nodeOrigin }), viewBB)
+ : viewBB,
rfId: s.rfId,
nodeOrigin: s.nodeOrigin,
panZoom: s.panZoom,
diff --git a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx
index 0abe3177..f2e7944c 100644
--- a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx
+++ b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx
@@ -6,7 +6,7 @@ import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore';
import { MiniMapNode } from './MiniMapNode';
-import type { ReactFlowState, Node } from '../../types';
+import type { ReactFlowState, Node, InternalNode } from '../../types';
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
declare const window: any;
@@ -85,7 +85,7 @@ function NodeComponentWrapperInner({
shapeRendering: string;
}) {
const { node, x, y } = useStore((s) => {
- const node = s.nodeLookup.get(id) as NodeType;
+ const node = s.nodeLookup.get(id) as InternalNode;
const { x, y } = getNodePositionWithOrigin(node, node?.origin || nodeOrigin).positionAbsolute;
return {
diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx
index 1a1107b1..2052c98b 100644
--- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx
+++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx
@@ -5,12 +5,14 @@ import {
ResizeControlVariant,
type XYResizerInstance,
type XYResizerChange,
- XYResizerChildChange,
+ type XYResizerChildChange,
+ type NodeChange,
+ type NodeDimensionChange,
+ type NodePositionChange,
} from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext';
-import type { NodeChange, NodeDimensionChange, NodePositionChange } from '../../types';
import type { ResizeControlProps, ResizeControlLineProps } from './types';
function ResizeControl({
diff --git a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx
index 0ecc8185..1fc6f25c 100644
--- a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx
+++ b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx
@@ -1,23 +1,23 @@
import { useCallback, CSSProperties } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
-import { getNodesBounds, Rect, Position, internalsSymbol, getNodeToolbarTransform } from '@xyflow/system';
+import { Rect, Position, getNodeToolbarTransform, getNodesBounds } from '@xyflow/system';
-import { Node, ReactFlowState } from '../../types';
+import { InternalNode, ReactFlowState } from '../../types';
import { useStore } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext';
import { NodeToolbarPortal } from './NodeToolbarPortal';
import type { NodeToolbarProps } from './types';
-const nodeEqualityFn = (a?: Node, b?: Node) =>
- a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x ||
- a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y ||
- a?.computed?.width !== b?.computed?.width ||
- a?.computed?.height !== b?.computed?.height ||
+const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) =>
+ a?.internals.positionAbsolute.x !== b?.internals.positionAbsolute.x ||
+ a?.internals.positionAbsolute.y !== b?.internals.positionAbsolute.y ||
+ a?.measured.width !== b?.measured.width ||
+ a?.measured.height !== b?.measured.height ||
a?.selected !== b?.selected ||
- a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z;
+ a?.internals.z !== b?.internals.z;
-const nodesEqualityFn = (a: Node[], b: Node[]) => {
+const nodesEqualityFn = (a: InternalNode[], b: InternalNode[]) => {
if (a.length !== b.length) {
return false;
}
@@ -49,10 +49,10 @@ export function NodeToolbar({
const contextNodeId = useNodeId();
const nodesSelector = useCallback(
- (state: ReactFlowState): Node[] => {
+ (state: ReactFlowState): InternalNode[] => {
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
- return nodeIds.reduce((acc, id) => {
+ return nodeIds.reduce((acc, id) => {
const node = state.nodeLookup.get(id);
if (node) {
acc.push(node);
@@ -74,7 +74,7 @@ export function NodeToolbar({
}
const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin });
- const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1));
+ const zIndex: number = Math.max(...nodes.map((node) => (node.internals?.z || 1) + 1));
const wrapperStyle: CSSProperties = {
position: 'absolute',
diff --git a/packages/react/src/components/ConnectionLine/index.tsx b/packages/react/src/components/ConnectionLine/index.tsx
index 4fe2ca76..54b3f24d 100644
--- a/packages/react/src/components/ConnectionLine/index.tsx
+++ b/packages/react/src/components/ConnectionLine/index.tsx
@@ -2,7 +2,6 @@ import { CSSProperties, useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import {
- internalsSymbol,
Position,
ConnectionLineType,
ConnectionMode,
@@ -53,7 +52,7 @@ const ConnectionLine = ({
),
shallow
);
- const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
+ const fromHandleBounds = fromNode?.internals?.handleBounds;
let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) {
@@ -65,10 +64,10 @@ const ConnectionLine = ({
}
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
- const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed?.width ?? 0) / 2;
- const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0;
- const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX;
- const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY;
+ const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2;
+ const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0;
+ const fromX = (fromNode.internals.positionAbsolute.x ?? 0) + fromHandleX;
+ const fromY = (fromNode.internals.positionAbsolute.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx
index 6061d8a7..e4810f11 100644
--- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx
+++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx
@@ -54,7 +54,7 @@ export function EdgeUpdateAnchors({
onConnectStart,
onConnectEnd,
cancelConnection,
- nodes,
+ nodeLookup,
rfId: flowId,
panBy,
updateConnection,
@@ -82,7 +82,7 @@ export function EdgeUpdateAnchors({
domNode,
handleId,
nodeId,
- nodes,
+ nodeLookup,
isTarget,
edgeUpdaterType: handleType,
lib,
diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx
index ee36d066..977a5c5b 100644
--- a/packages/react/src/components/Handle/index.tsx
+++ b/packages/react/src/components/Handle/index.tsx
@@ -126,7 +126,7 @@ function HandleComponent(
connectionMode: currentStore.connectionMode,
connectionRadius: currentStore.connectionRadius,
domNode: currentStore.domNode,
- nodes: currentStore.nodes,
+ nodeLookup: currentStore.nodeLookup,
lib: currentStore.lib,
isTarget,
handleId,
diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx
index 8c207cd9..e3fbf16c 100644
--- a/packages/react/src/components/NodeWrapper/index.tsx
+++ b/packages/react/src/components/NodeWrapper/index.tsx
@@ -7,7 +7,6 @@ import {
errorMessages,
getNodeDimensions,
getPositionWithOrigin,
- internalsSymbol,
isInputDOMNode,
nodeHasDimensions,
} from '@xyflow/system';
@@ -19,7 +18,7 @@ import { useDrag } from '../../hooks/useDrag';
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
import { handleNodeClick } from '../Nodes/utils';
import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils';
-import type { Node, NodeWrapperProps } from '../../types';
+import type { InternalNode, Node, NodeWrapperProps } from '../../types';
export function NodeWrapper({
id,
@@ -44,11 +43,11 @@ export function NodeWrapper({
onError,
}: NodeWrapperProps) {
const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => {
- const node = s.nodeLookup.get(id)! as NodeType;
+ const node = s.nodeLookup.get(id)! as InternalNode;
const positionAbsolute = nodeExtent
- ? clampPosition(node.computed?.positionAbsolute, nodeExtent)
- : node.computed?.positionAbsolute || { x: 0, y: 0 };
+ ? clampPosition(node.internals.positionAbsolute, nodeExtent)
+ : node.internals.positionAbsolute || { x: 0, y: 0 };
return {
node,
@@ -56,8 +55,8 @@ export function NodeWrapper({
// so we we need to force a re-render when some change
positionAbsoluteX: positionAbsolute.x,
positionAbsoluteY: positionAbsolute.y,
- zIndex: node[internalsSymbol]?.z ?? 0,
- isParent: !!node[internalsSymbol]?.isParent,
+ zIndex: node.internals.z,
+ isParent: node.internals.isParent,
};
}, shallow);
@@ -84,14 +83,16 @@ export function NodeWrapper({
const nodeDimensions = getNodeDimensions(node);
const inlineDimensions = getNodeInlineStyleDimensions(node);
const initialized = nodeHasDimensions(node);
- const hasHandleBounds = !!node[internalsSymbol]?.handleBounds;
+ const hasHandleBounds = !!node.internals.handleBounds;
const moveSelectedNodes = useMoveSelectedNodes();
useEffect(() => {
+ const currNode = nodeRef.current;
+
return () => {
- if (nodeRef.current) {
- resizeObserver?.unobserve(nodeRef.current);
+ if (currNode) {
+ resizeObserver?.unobserve(currNode);
}
};
}, []);
@@ -123,7 +124,7 @@ export function NodeWrapper({
if (targetPosChanged) {
prevTargetPosition.current = node.targetPosition;
}
- store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
+ store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, force: true }]]));
}
}, [id, nodeType, node.sourcePosition, node.targetPosition]);
diff --git a/packages/react/src/components/NodeWrapper/utils.tsx b/packages/react/src/components/NodeWrapper/utils.tsx
index d6f7cc1a..c8c7d5c7 100644
--- a/packages/react/src/components/NodeWrapper/utils.tsx
+++ b/packages/react/src/components/NodeWrapper/utils.tsx
@@ -4,7 +4,7 @@ import { InputNode } from '../Nodes/InputNode';
import { DefaultNode } from '../Nodes/DefaultNode';
import { GroupNode } from '../Nodes/GroupNode';
import { OutputNode } from '../Nodes/OutputNode';
-import type { Node, NodeTypes } from '../../types';
+import type { InternalNode, Node, NodeTypes } from '../../types';
export const arrowKeyDiffs: Record = {
ArrowUp: { x: 0, y: -1 },
@@ -21,12 +21,12 @@ export const builtinNodeTypes: NodeTypes = {
};
export function getNodeInlineStyleDimensions(
- node: NodeType
+ node: InternalNode
): {
width: number | string | undefined;
height: number | string | undefined;
} {
- if (!node.computed) {
+ if (node.internals.handleBounds === undefined) {
return {
width: node.width ?? node.initialWidth ?? node.style?.width,
height: node.height ?? node.initialHeight ?? node.style?.height,
diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx
index 6c037ba5..e87f23af 100644
--- a/packages/react/src/components/NodesSelection/index.tsx
+++ b/packages/react/src/components/NodesSelection/index.tsx
@@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
import { useDrag } from '../../hooks/useDrag';
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
import { arrowKeyDiffs } from '../NodeWrapper/utils';
-import type { Node, ReactFlowState } from '../../types';
+import type { InternalNode, Node, ReactFlowState } from '../../types';
export type NodesSelectionProps = {
onSelectionContextMenu?: (event: MouseEvent, nodes: NodeType[]) => void;
@@ -20,7 +20,13 @@ export type NodesSelectionProps = {
};
const selector = (s: ReactFlowState) => {
- const selectedNodes = s.nodes.filter((n) => n.selected);
+ const selectedNodes: InternalNode[] = [];
+ for (const [, node] of s.nodeLookup) {
+ if (node.selected) {
+ selectedNodes.push(node);
+ }
+ }
+
const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin });
return {
diff --git a/packages/react/src/components/SelectionListener/index.tsx b/packages/react/src/components/SelectionListener/index.tsx
index 0a3fa5a9..381a60cb 100644
--- a/packages/react/src/components/SelectionListener/index.tsx
+++ b/packages/react/src/components/SelectionListener/index.tsx
@@ -15,7 +15,7 @@ type SelectionListenerProps = {
};
const selector = (s: ReactFlowState) => ({
- selectedNodes: s.nodes.filter((n) => n.selected),
+ selectedNodes: Array.from(s.nodeLookup.values()).filter((n) => n.selected),
selectedEdges: s.edges.filter((e) => e.selected),
});
diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx
index 6d92e8b4..624d10a5 100644
--- a/packages/react/src/components/StoreUpdater/index.tsx
+++ b/packages/react/src/components/StoreUpdater/index.tsx
@@ -138,7 +138,6 @@ export function StoreUpdater s.updateNodeDimensions;
@@ -15,14 +16,13 @@ export function useResizeObserver() {
}
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
- const updates = new Map();
+ const updates = new Map();
entries.forEach((entry: ResizeObserverEntry) => {
const id = entry.target.getAttribute('data-id') as string;
updates.set(id, {
id,
nodeElement: entry.target as HTMLDivElement,
- forceUpdate: true,
});
});
diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx
index dfb240d0..0c0eb122 100644
--- a/packages/react/src/container/Pane/index.tsx
+++ b/packages/react/src/container/Pane/index.tsx
@@ -5,13 +5,13 @@
import { useRef, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
-import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system';
+import { getNodesInside, getEventPosition, SelectionMode, type NodeChange, type EdgeChange } from '@xyflow/system';
import { UserSelection } from '../../components/UserSelection';
import { containerStyle } from '../../styles/utils';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { getSelectionChanges } from '../../utils';
-import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
+import type { ReactFlowProps, ReactFlowState } from '../../types';
type PaneProps = {
isSelecting: boolean;
@@ -128,7 +128,7 @@ export function Pane({
};
const onMouseMove = (event: ReactMouseEvent): void => {
- const { userSelectionRect, edges, transform, nodeOrigin, nodes, triggerNodeChanges, triggerEdgeChanges } =
+ const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } =
store.getState();
if (!isSelecting || !containerBounds.current || !userSelectionRect) {
return;
@@ -149,7 +149,7 @@ export function Pane({
};
const selectedNodes = getNodesInside(
- nodes,
+ nodeLookup,
nextUserSelectRect,
transform,
selectionMode === SelectionMode.Partial,
@@ -163,22 +163,22 @@ export function Pane({
for (const selectedNode of selectedNodes) {
selectedNodeIds.add(selectedNode.id);
- for (const edge of edges) {
+ for (const [edgeId, edge] of edgeLookup) {
if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
- selectedEdgeIds.add(edge.id);
+ selectedEdgeIds.add(edgeId);
}
}
}
if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
prevSelectedNodesCount.current = selectedNodeIds.size;
- const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[];
+ const changes = getSelectionChanges(nodeLookup, selectedNodeIds, true) as NodeChange[];
triggerNodeChanges(changes);
}
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
prevSelectedEdgesCount.current = selectedEdgeIds.size;
- const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
+ const changes = getSelectionChanges(edgeLookup, selectedEdgeIds) as EdgeChange[];
triggerEdgeChanges(changes);
}
diff --git a/packages/react/src/hooks/useInternalNode.ts b/packages/react/src/hooks/useInternalNode.ts
new file mode 100644
index 00000000..d16d697b
--- /dev/null
+++ b/packages/react/src/hooks/useInternalNode.ts
@@ -0,0 +1,21 @@
+import { useCallback } from 'react';
+import { shallow } from 'zustand/shallow';
+
+import { useStore } from './useStore';
+import type { InternalNode, Node } from '../types';
+
+/**
+ * Hook for getting an internal node by id
+ *
+ * @public
+ * @param id - id of the node
+ * @returns array with visible node ids
+ */
+export function useInternalNode(id: string): InternalNode | undefined {
+ const node = useStore(
+ useCallback((s) => s.nodeLookup.get(id) as InternalNode | undefined, [id]),
+ shallow
+ );
+
+ return node;
+}
diff --git a/packages/react/src/hooks/useMoveSelectedNodes.ts b/packages/react/src/hooks/useMoveSelectedNodes.ts
index c627628a..d189642b 100644
--- a/packages/react/src/hooks/useMoveSelectedNodes.ts
+++ b/packages/react/src/hooks/useMoveSelectedNodes.ts
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { calculateNodePosition, snapPosition, type XYPosition } from '@xyflow/system';
-import { Node } from '../types';
+import { type Node } from '../types';
import { useStoreApi } from './useStore';
const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) =>
@@ -17,18 +17,11 @@ export function useMoveSelectedNodes() {
const store = useStoreApi();
const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => {
- const {
- nodeExtent,
- nodes,
- snapToGrid,
- snapGrid,
- nodesDraggable,
- onError,
- updateNodePositions,
- nodeLookup,
- nodeOrigin,
- } = store.getState();
- const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable));
+ const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } =
+ store.getState();
+ const nodeUpdates = [];
+ const isSelected = selectedAndDraggable(nodesDraggable);
+
// by default a node moves 5px on each key press
// if snap grid is enabled, we use that for the velocity
const xVelo = snapToGrid ? snapGrid[0] : 5;
@@ -37,32 +30,34 @@ export function useMoveSelectedNodes() {
const xDiff = params.direction.x * xVelo * params.factor;
const yDiff = params.direction.y * yVelo * params.factor;
- const nodeUpdates = selectedNodes.map((node) => {
- if (node.computed?.positionAbsolute) {
- let nextPosition = {
- x: node.computed.positionAbsolute.x + xDiff,
- y: node.computed.positionAbsolute.y + yDiff,
- };
-
- if (snapToGrid) {
- nextPosition = snapPosition(nextPosition, snapGrid);
- }
-
- const { position, positionAbsolute } = calculateNodePosition({
- nodeId: node.id,
- nextPosition,
- nodeLookup,
- nodeExtent,
- nodeOrigin,
- onError,
- });
-
- node.position = position;
- node.computed.positionAbsolute = positionAbsolute;
+ for (const [, node] of nodeLookup) {
+ if (!isSelected(node)) {
+ continue;
}
- return node;
- });
+ let nextPosition = {
+ x: node.internals.positionAbsolute.x + xDiff,
+ y: node.internals.positionAbsolute.y + yDiff,
+ };
+
+ if (snapToGrid) {
+ nextPosition = snapPosition(nextPosition, snapGrid);
+ }
+
+ const { position, positionAbsolute } = calculateNodePosition({
+ nodeId: node.id,
+ nextPosition,
+ nodeLookup,
+ nodeExtent,
+ nodeOrigin,
+ onError,
+ });
+
+ node.position = position;
+ node.internals.positionAbsolute = positionAbsolute;
+
+ nodeUpdates.push(node);
+ }
updateNodePositions(nodeUpdates);
}, []);
diff --git a/packages/react/src/hooks/useNodesInitialized.ts b/packages/react/src/hooks/useNodesInitialized.ts
index 3a278f6c..346e56d9 100644
--- a/packages/react/src/hooks/useNodesInitialized.ts
+++ b/packages/react/src/hooks/useNodesInitialized.ts
@@ -1,5 +1,3 @@
-import { internalsSymbol } from '@xyflow/system';
-
import { useStore } from './useStore';
import type { ReactFlowState } from '../types';
@@ -8,13 +6,13 @@ export type UseNodesInitializedOptions = {
};
const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => {
- if (s.nodes.length === 0) {
+ if (s.nodeLookup.size === 0) {
return false;
}
- for (const node of s.nodes) {
+ for (const [, node] of s.nodeLookup) {
if (options.includeHiddenNodes || !node.hidden) {
- if (node[internalsSymbol]?.handleBounds === undefined) {
+ if (node.internals.handleBounds === undefined) {
return false;
}
}
diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts
index 5c10ad6a..6be3f90b 100644
--- a/packages/react/src/hooks/useReactFlow.ts
+++ b/packages/react/src/hooks/useReactFlow.ts
@@ -1,16 +1,9 @@
import { useCallback, useMemo, useRef, useState } from 'react';
-import {
- getElementsToRemove,
- getOverlappingArea,
- isRectObject,
- nodeHasDimensions,
- nodeToRect,
- type Rect,
-} from '@xyflow/system';
+import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
import useViewportHelper from './useViewportHelper';
import { useStoreApi } from './useStore';
-import type { ReactFlowInstance, Instance, Node, Edge } from '../types';
+import type { ReactFlowInstance, Instance, Node, Edge, InternalNode } from '../types';
import { getElementsDiffChanges, isNode } from '../utils';
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
@@ -27,13 +20,20 @@ export function useReactFlow>(() => {
- return store.getState().nodes.map((n) => ({ ...n })) as NodeType[];
- }, []);
+ const getNodes = useCallback>(
+ () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[],
+ []
+ );
- const getNode = useCallback>((id) => {
- return store.getState().nodeLookup.get(id) as NodeType;
- }, []);
+ const getInternalNode = useCallback>(
+ (id) => store.getState().nodeLookup.get(id) as InternalNode,
+ []
+ );
+
+ const getNode = useCallback>(
+ (id) => getInternalNode(id)?.internals.userNode as NodeType,
+ [getInternalNode]
+ );
const getEdges = useCallback>(() => {
const { edges = [] } = store.getState();
@@ -223,13 +223,9 @@ export function useReactFlow {
- const node =
- isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect)
- ? nodeOrRect
- : (store.getState().nodeLookup.get(nodeOrRect.id) as NodeType);
-
- return node ? nodeToRect(node) : null;
+ const getNodeRect = useCallback(({ id }: { id: string }): Rect | null => {
+ const internalNode = store.getState().nodeLookup.get(id);
+ return internalNode ? nodeToRect(internalNode) : null;
}, []);
const getIntersectingNodes = useCallback>(
@@ -242,7 +238,9 @@ export function useReactFlow {
- if (!isRect && (n.id === nodeOrRect!.id || !n.computed?.positionAbsolute)) {
+ const internalNode = store.getState().nodeLookup.get(n.id);
+
+ if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) {
return false;
}
@@ -308,6 +306,7 @@ export function useReactFlow {
return { x, y, zoom };
},
fitView: (options) => {
- const { nodes, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState();
+ const { nodeLookup, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState();
return panZoom
? fitView(
{
- nodes,
+ nodeLookup,
width,
height,
nodeOrigin,
diff --git a/packages/react/src/hooks/useVisibleNodeIds.ts b/packages/react/src/hooks/useVisibleNodeIds.ts
index 309391dc..e8d14c8e 100644
--- a/packages/react/src/hooks/useVisibleNodeIds.ts
+++ b/packages/react/src/hooks/useVisibleNodeIds.ts
@@ -1,13 +1,13 @@
-import { getNodesInside } from '@xyflow/system';
+import { useCallback } from 'react';
import { shallow } from 'zustand/shallow';
+import { getNodesInside } from '@xyflow/system';
import { useStore } from './useStore';
import type { Node, ReactFlowState } from '../types';
-import { useCallback } from 'react';
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
return onlyRenderVisible
- ? getNodesInside(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
+ ? getNodesInside(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
(node) => node.id
)
: Array.from(s.nodeLookup.keys());
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 022f2626..e7e6b37f 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -26,9 +26,10 @@ export { useNodesInitialized, type UseNodesInitializedOptions } from './hooks/us
export { useHandleConnections } from './hooks/useHandleConnections';
export { useNodesData } from './hooks/useNodesData';
export { useConnection } from './hooks/useConnection';
+export { useInternalNode } from './hooks/useInternalNode';
export { useNodeId } from './contexts/NodeIdContext';
-export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes';
+export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
export { isNode, isEdge } from './utils/general';
export * from './additional-components';
@@ -103,5 +104,4 @@ export {
addEdge,
updateEdge,
getConnectedEdges,
- internalsSymbol,
} from '@xyflow/system';
diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts
index a2b98c1a..492d6186 100644
--- a/packages/react/src/store/index.ts
+++ b/packages/react/src/store/index.ts
@@ -2,27 +2,20 @@ import { createWithEqualityFn } from 'zustand/traditional';
import {
clampPosition,
fitView as fitViewSystem,
- adoptUserProvidedNodes,
+ adoptUserNodes,
updateAbsolutePositions,
panBy as panBySystem,
- Dimensions,
updateNodeDimensions as updateNodeDimensionsSystem,
updateConnectionLookup,
+ handleParentExpand,
+ NodeChange,
+ EdgeSelectionChange,
+ NodeSelectionChange,
} from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import getInitialState from './initialState';
-import type {
- ReactFlowState,
- Node,
- Edge,
- NodeDimensionChange,
- EdgeSelectionChange,
- NodeSelectionChange,
- NodePositionChange,
- UnselectNodesAndEdgesParams,
- FitViewOptions,
-} from '../types';
+import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions, InternalNode } from '../types';
const createRFStore = ({
nodes,
@@ -52,9 +45,9 @@ const createRFStore = ({
//
// When this happens, we take the note objects passed by the user and extend them with fields
// relevant for internal React Flow operations.
- const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
+ adoptUserNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
- set({ nodes: nodesWithInternalData });
+ set({ nodes });
},
setEdges: (edges: Edge[]) => {
const { connectionLookup, edgeLookup } = get();
@@ -82,7 +75,6 @@ const createRFStore = ({
const {
onNodesChange,
fitView,
- nodes,
nodeLookup,
fitViewOnInit,
fitViewDone,
@@ -91,35 +83,21 @@ const createRFStore = ({
nodeOrigin,
debug,
} = get();
- const changes: NodeDimensionChange[] = [];
- const updatedNodes = updateNodeDimensionsSystem(
- updates,
- nodes,
- nodeLookup,
- domNode,
- nodeOrigin,
- (id: string, dimensions: Dimensions) => {
- changes.push({
- id: id,
- type: 'dimensions',
- dimensions,
- });
- }
- );
+ const changes = updateNodeDimensionsSystem(updates, nodeLookup, domNode, nodeOrigin);
- if (!updatedNodes) {
+ if (changes.length === 0) {
return;
}
- const nextNodes = updateAbsolutePositions(updatedNodes, nodeLookup, nodeOrigin);
+ updateAbsolutePositions(nodeLookup, { nodeOrigin });
// we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone;
if (!fitViewDone && fitViewOnInit) {
- nextFitViewDone = fitView(nextNodes, {
+ nextFitViewDone = fitView({
...fitViewOnInitOptions,
- nodes: fitViewOnInitOptions?.nodes || nextNodes,
+ nodes: fitViewOnInitOptions?.nodes,
});
}
@@ -128,7 +106,7 @@ const createRFStore = ({
// 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({ nodes: nextNodes, fitViewDone: nextFitViewDone });
+ set({ fitViewDone: nextFitViewDone });
if (changes?.length > 0) {
if (debug) {
@@ -138,18 +116,41 @@ const createRFStore = ({
}
},
updateNodePositions: (nodeDragItems, dragging = false) => {
- const changes = nodeDragItems.map((node) => {
- const change: NodePositionChange = {
+ const { nodeLookup } = get();
+ const triggerChangeNodes: InternalNode[] = [];
+
+ const changes: NodeChange[] = nodeDragItems.map((node) => {
+ // @todo add expandParent to drag item so that we can get rid of the look up here
+ const internalNode = nodeLookup.get(node.id);
+ const change: NodeChange = {
id: node.id,
type: 'position',
position: node.position,
- positionAbsolute: node.computed?.positionAbsolute,
dragging,
};
+ if (internalNode?.expandParent && change.position) {
+ triggerChangeNodes.push({
+ ...internalNode,
+ position: change.position,
+ internals: {
+ ...internalNode.internals,
+ positionAbsolute: node.internals.positionAbsolute,
+ },
+ });
+
+ change.position.x = Math.max(0, change.position.x);
+ change.position.y = Math.max(0, change.position.y);
+ }
+
return change;
});
+ if (triggerChangeNodes.length > 0) {
+ const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup);
+ changes.push(...parentExpandChanges);
+ }
+
get().triggerNodeChanges(changes);
},
triggerNodeChanges: (changes) => {
@@ -185,7 +186,7 @@ const createRFStore = ({
}
},
addSelectedNodes: (selectedNodeIds) => {
- const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
+ const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
if (multiSelectionActive) {
const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
@@ -193,11 +194,11 @@ const createRFStore = ({
return;
}
- triggerNodeChanges(getSelectionChanges(nodes, new Set([...selectedNodeIds]), true));
- triggerEdgeChanges(getSelectionChanges(edges));
+ triggerNodeChanges(getSelectionChanges(nodeLookup, new Set([...selectedNodeIds]), true));
+ triggerEdgeChanges(getSelectionChanges(edgeLookup));
},
addSelectedEdges: (selectedEdgeIds) => {
- const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
+ const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
if (multiSelectionActive) {
const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
@@ -205,8 +206,8 @@ const createRFStore = ({
return;
}
- triggerEdgeChanges(getSelectionChanges(edges, new Set([...selectedEdgeIds])));
- triggerNodeChanges(getSelectionChanges(nodes, new Set(), true));
+ triggerEdgeChanges(getSelectionChanges(edgeLookup, new Set([...selectedEdgeIds])));
+ triggerNodeChanges(getSelectionChanges(nodeLookup, new Set(), true));
},
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get();
@@ -255,29 +256,30 @@ const createRFStore = ({
triggerEdgeChanges(edgeChanges);
},
setNodeExtent: (nodeExtent) => {
- const { nodes } = get();
+ const { nodeLookup } = get();
+
+ for (const [, node] of nodeLookup) {
+ const positionAbsolute = clampPosition(node.position, nodeExtent);
+
+ nodeLookup.set(node.id, {
+ ...node,
+ internals: {
+ ...node.internals,
+ positionAbsolute,
+ },
+ });
+ }
set({
nodeExtent,
- nodes: nodes.map((node) => {
- const positionAbsolute = clampPosition(node.position, nodeExtent);
-
- return {
- ...node,
- computed: {
- ...node.computed,
- positionAbsolute,
- },
- };
- }),
});
},
panBy: (delta): boolean => {
const { transform, width, height, panZoom, translateExtent } = get();
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
},
- fitView: (nodes: Node[], options?: FitViewOptions): boolean => {
- const { panZoom, width, height, minZoom, maxZoom, nodeOrigin } = get();
+ fitView: (options?: FitViewOptions): boolean => {
+ const { panZoom, width, height, minZoom, maxZoom, nodeOrigin, nodeLookup } = get();
if (!panZoom) {
return false;
@@ -285,7 +287,7 @@ const createRFStore = ({
return fitViewSystem(
{
- nodes,
+ nodeLookup,
width,
height,
panZoom,
diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts
index 0cf3c7f0..cc754976 100644
--- a/packages/react/src/store/initialState.ts
+++ b/packages/react/src/store/initialState.ts
@@ -1,7 +1,7 @@
import {
infiniteExtent,
ConnectionMode,
- adoptUserProvidedNodes,
+ adoptUserNodes,
getNodesBounds,
getViewportForBounds,
Transform,
@@ -35,7 +35,7 @@ const getInitialState = ({
const storeNodes = defaultNodes ?? nodes ?? [];
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
- const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, {
+ adoptUserNodes(storeNodes, nodeLookup, {
nodeOrigin: [0, 0],
elevateNodesOnSelect: false,
});
@@ -43,7 +43,7 @@ const getInitialState = ({
let transform: Transform = [0, 0, 1];
if (fitView && width && height) {
- const nodesWithDimensions = nextNodes.filter(
+ const nodesWithDimensions = storeNodes.filter(
(node) => (node.width || node.initialWidth) && (node.height || node.initialHeight)
);
// @todo users nodeOrigin should be used here
@@ -57,7 +57,7 @@ const getInitialState = ({
width: 0,
height: 0,
transform,
- nodes: nextNodes,
+ nodes: storeNodes,
nodeLookup,
edges: storeEdges,
edgeLookup,
diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts
index 302ffeee..8b55920f 100644
--- a/packages/react/src/types/general.ts
+++ b/packages/react/src/types/general.ts
@@ -12,9 +12,11 @@ import {
XYPosition,
OnBeforeDeleteBase,
Connection,
+ NodeChange,
+ EdgeChange,
} from '@xyflow/system';
-import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
+import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
export type OnNodesChange = (changes: NodeChange[]) => void;
export type OnEdgesChange = (changes: EdgeChange[]) => void;
diff --git a/packages/react/src/types/index.ts b/packages/react/src/types/index.ts
index 6ee8dada..f18123f2 100644
--- a/packages/react/src/types/index.ts
+++ b/packages/react/src/types/index.ts
@@ -1,6 +1,5 @@
export * from './nodes';
export * from './edges';
-export * from './changes';
export * from './component-props';
export * from './general';
export * from './store';
diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts
index 1991580c..67a6d928 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 { Rect, Viewport } from '@xyflow/system';
-import type { Node, Edge, ViewportHelperFunctions } from '.';
+import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.';
export type ReactFlowJsonObject = {
nodes: NodeType[];
@@ -20,6 +20,7 @@ export namespace Instance {
) => void;
export type AddNodes = (payload: NodeType[] | NodeType) => void;
export type GetNode = (id: string) => NodeType | undefined;
+ export type GetInternalNode = (id: string) => InternalNode | undefined;
export type GetEdges = () => EdgeType[];
export type SetEdges = (
payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])
@@ -83,6 +84,13 @@ export type ReactFlowInstance;
+ /**
+ * Returns an internal node by id.
+ *
+ * @param id - the node id
+ * @returns the internal node or undefined if no node was found
+ */
+ getInternalNode: Instance.GetInternalNode;
/**
* Returns edges.
*
diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts
index 52f5a321..89fec455 100644
--- a/packages/react/src/types/nodes.ts
+++ b/packages/react/src/types/nodes.ts
@@ -1,5 +1,12 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
-import type { CoordinateExtent, NodeBase, NodeOrigin, OnError, NodeProps as NodePropsBase } from '@xyflow/system';
+import type {
+ CoordinateExtent,
+ NodeBase,
+ NodeOrigin,
+ OnError,
+ NodeProps as NodePropsBase,
+ InternalNodeBase,
+} from '@xyflow/system';
import { NodeTypes } from './general';
@@ -17,6 +24,8 @@ export type Node<
focusable?: boolean;
};
+export type InternalNode = InternalNodeBase;
+
export type NodeMouseHandler = (event: ReactMouseEvent, node: NodeType) => void;
export type SelectionDragHandler = (event: ReactMouseEvent, nodes: NodeType[]) => void;
export type OnNodeDrag = (
diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts
index 6c1cf9b0..416f56f3 100644
--- a/packages/react/src/types/store.ts
+++ b/packages/react/src/types/store.ts
@@ -25,12 +25,13 @@ import {
type EdgeLookup,
type ConnectionLookup,
type NodeLookup,
+ NodeChange,
+ EdgeChange,
} from '@xyflow/system';
import type {
Edge,
Node,
- NodeChange,
OnNodesChange,
OnEdgesChange,
DefaultEdgeOptions,
@@ -43,7 +44,7 @@ import type {
OnNodeDrag,
OnBeforeDelete,
IsValidConnection,
- EdgeChange,
+ InternalNode,
} from '.';
export type ReactFlowStore = {
@@ -52,7 +53,7 @@ export type ReactFlowStore;
+ nodeLookup: NodeLookup>;
edges: Edge[];
edgeLookup: EdgeLookup;
connectionLookup: ConnectionLookup;
@@ -169,7 +170,7 @@ export type ReactFlowActions = {
triggerNodeChanges: (changes: NodeChange[]) => void;
triggerEdgeChanges: (changes: EdgeChange[]) => void;
panBy: PanBy;
- fitView: (nodes: NodeType[], options?: FitViewOptions) => boolean;
+ fitView: (options?: FitViewOptions) => boolean;
};
export type ReactFlowState = ReactFlowStore<
diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts
index fa6f6f30..44af2451 100644
--- a/packages/react/src/utils/changes.ts
+++ b/packages/react/src/utils/changes.ts
@@ -1,51 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { EdgeLookup, NodeLookup } from '@xyflow/system';
-import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types';
-
-export function handleParentExpand(updatedElements: any[], updateItem: any) {
- for (const [index, item] of updatedElements.entries()) {
- if (item.id === updateItem.parentNode) {
- const parent = { ...item };
- parent.computed ??= {};
-
- const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width;
- const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height;
-
- if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
- parent.width = parent.width ?? parent.computed.width;
- parent.height = parent.height ?? parent.computed.height;
-
- if (extendWidth > 0) {
- parent.width += extendWidth;
- }
-
- if (extendHeight > 0) {
- parent.height += extendHeight;
- }
-
- if (updateItem.position.x < 0) {
- const xDiff = Math.abs(updateItem.position.x);
- parent.position.x = parent.position.x - xDiff;
- parent.width += xDiff;
- updateItem.position.x = 0;
- }
-
- if (updateItem.position.y < 0) {
- const yDiff = Math.abs(updateItem.position.y);
- parent.position.y = parent.position.y - yDiff;
- parent.height += yDiff;
- updateItem.position.y = 0;
- }
-
- parent.computed.width = parent.width;
- parent.computed.height = parent.height;
-
- updatedElements[index] = parent;
- }
- break;
- }
- }
-}
+import {
+ EdgeLookup,
+ NodeLookup,
+ EdgeChange,
+ NodeChange,
+ NodeSelectionChange,
+ EdgeSelectionChange,
+} from '@xyflow/system';
+import type { Node, Edge, InternalNode } from '../types';
// This function applies changes to nodes or edges that are triggered by React Flow internally.
// When you drag a node for example, React Flow will send a position change update.
@@ -103,7 +65,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
const updatedElement = { ...element };
for (const change of changes) {
- applyChange(change, updatedElement, updatedElements);
+ applyChange(change, updatedElement);
}
updatedElements.push(updatedElement);
@@ -113,7 +75,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
}
// Applies a single change to an element. This is a *mutable* update.
-function applyChange(change: any, element: any, elements: any[] = []): any {
+function applyChange(change: any, element: any): any {
switch (change.type) {
case 'select': {
element.selected = change.selected;
@@ -125,26 +87,18 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
element.position = change.position;
}
- if (typeof change.positionAbsolute !== 'undefined') {
- element.computed ??= {};
- element.computed.positionAbsolute = change.positionAbsolute;
- }
-
if (typeof change.dragging !== 'undefined') {
element.dragging = change.dragging;
}
- if (element.expandParent) {
- handleParentExpand(elements, element);
- }
break;
}
case 'dimensions': {
if (typeof change.dimensions !== 'undefined') {
- element.computed ??= {};
- element.computed.width = change.dimensions.width;
- element.computed.height = change.dimensions.height;
+ element.measured ??= {};
+ element.measured.width = change.dimensions.width;
+ element.measured.height = change.dimensions.height;
if (change.resizing) {
element.width = change.dimensions.width;
@@ -156,10 +110,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
element.resizing = change.resizing;
}
- if (element.expandParent) {
- handleParentExpand(elements, element);
- }
-
break;
}
}
@@ -228,13 +178,13 @@ export function createSelectionChange(id: string, selected: boolean): NodeSelect
}
export function getSelectionChanges(
- items: any[],
+ items: Map,
selectedIds: Set = new Set(),
mutateItem = false
): NodeSelectionChange[] | EdgeSelectionChange[] {
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
- for (const item of items) {
+ for (const [, item] of items) {
const willBeSelected = selectedIds.has(item.id);
// we don't want to set all items to selected=false on the first selection
@@ -266,7 +216,7 @@ export function getElementsDiffChanges({
lookup,
}: {
items: Node[] | undefined;
- lookup: NodeLookup;
+ lookup: NodeLookup>;
}): NodeChange[];
export function getElementsDiffChanges({
items,
diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts
index 0f72d746..a6ba38de 100644
--- a/packages/svelte/src/lib/store/utils.ts
+++ b/packages/svelte/src/lib/store/utils.ts
@@ -7,7 +7,7 @@ import {
type Writable
} from 'svelte/store';
import {
- adoptUserProvidedNodes,
+ adoptUserNodes,
updateConnectionLookup,
type Viewport,
type PanZoomInstance,
@@ -141,7 +141,7 @@ export const createNodesStore = (
let elevateNodesOnSelect = true;
const _set = (nds: Node[]): Node[] => {
- const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, {
+ const nextNodes = adoptUserNodes(nds, nodeLookup, {
elevateNodesOnSelect,
defaults
});
diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts
index 21d21234..67b913a0 100644
--- a/packages/system/src/constants.ts
+++ b/packages/system/src/constants.ts
@@ -25,8 +25,6 @@ export const errorMessages = {
`Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,
};
-export const internalsSymbol = Symbol.for('internals');
-
export const infiniteExtent: CoordinateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
diff --git a/packages/react/src/types/changes.ts b/packages/system/src/types/changes.ts
similarity index 68%
rename from packages/react/src/types/changes.ts
rename to packages/system/src/types/changes.ts
index adcb484d..d2da81cf 100644
--- a/packages/react/src/types/changes.ts
+++ b/packages/system/src/types/changes.ts
@@ -1,6 +1,4 @@
-import type { XYPosition, Dimensions } from '@xyflow/system';
-
-import type { Node, Edge } from '.';
+import type { XYPosition, Dimensions, NodeBase, EdgeBase } from '.';
export type NodeDimensionChange = {
id: string;
@@ -28,12 +26,12 @@ export type NodeRemoveChange = {
type: 'remove';
};
-export type NodeAddChange = {
+export type NodeAddChange = {
item: NodeType;
type: 'add';
};
-export type NodeReplaceChange = {
+export type NodeReplaceChange = {
id: string;
item: NodeType;
type: 'replace';
@@ -43,7 +41,7 @@ export type NodeReplaceChange = {
* Union type of all possible node changes.
* @public
*/
-export type NodeChange =
+export type NodeChange =
| NodeDimensionChange
| NodePositionChange
| NodeSelectionChange
@@ -53,18 +51,18 @@ export type NodeChange =
export type EdgeSelectionChange = NodeSelectionChange;
export type EdgeRemoveChange = NodeRemoveChange;
-export type EdgeAddChange = {
+export type EdgeAddChange = {
item: EdgeType;
type: 'add';
};
-export type EdgeReplaceChange = {
+export type EdgeReplaceChange = {
id: string;
item: EdgeType;
type: 'replace';
};
-export type EdgeChange =
+export type EdgeChange =
| EdgeSelectionChange
| EdgeRemoveChange
| EdgeAddChange
diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts
index b7aa248e..2fd0e3ab 100644
--- a/packages/system/src/types/general.ts
+++ b/packages/system/src/types/general.ts
@@ -2,7 +2,7 @@
import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
import type { XYPosition, Rect } from './utils';
-import type { NodeBase, NodeDragItem, NodeOrigin } from './nodes';
+import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes';
import type { ConnectingHandle, HandleType } from './handles';
import { PanZoomInstance } from './panzoom';
import { EdgeBase } from '..';
@@ -52,7 +52,7 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
export type FitViewParamsBase = {
- nodes: NodeType[];
+ nodeLookup: Map>;
width: number;
height: number;
panZoom: PanZoomInstance;
@@ -127,7 +127,7 @@ export type SelectionRect = Rect & {
export type OnError = (id: string, message: string) => void;
-export type UpdateNodePositions = (dragItems: NodeDragItem[] | NodeBase[], dragging?: boolean) => void;
+export type UpdateNodePositions = (dragItems: NodeDragItem[] | InternalNodeBase[], dragging?: boolean) => void;
export type PanBy = (delta: XYPosition) => boolean;
export type UpdateConnection = (params: {
diff --git a/packages/system/src/types/index.ts b/packages/system/src/types/index.ts
index 421a8531..99653baf 100644
--- a/packages/system/src/types/index.ts
+++ b/packages/system/src/types/index.ts
@@ -1,3 +1,4 @@
+export * from './changes';
export * from './general';
export * from './nodes';
export * from './edges';
diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts
index c888d8a4..c8c85a55 100644
--- a/packages/system/src/types/nodes.ts
+++ b/packages/system/src/types/nodes.ts
@@ -1,4 +1,3 @@
-import { internalsSymbol } from '../constants';
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
import { Optional } from '../utils/types';
@@ -44,7 +43,7 @@ export type NodeBase<
initialWidth?: number;
initialHeight?: number;
/** Parent node id, used for creating sub-flows */
- parentNode?: string;
+ parentId?: string;
zIndex?: number;
/** Boundary a node can be moved in
* @example 'parent' or [[0, 0], [100, 100]]
@@ -60,21 +59,26 @@ export type NodeBase<
*/
origin?: NodeOrigin;
handles?: NodeHandle[];
- computed?: {
+ measured?: {
width?: number;
height?: number;
- positionAbsolute?: XYPosition;
};
+};
- // Only used internally
- [internalsSymbol]?: {
- z?: number;
+export type InternalNodeBase = NodeType & {
+ measured: {
+ width?: number;
+ height?: number;
+ };
+ internals: {
+ positionAbsolute: XYPosition;
+ z: number;
+ // @todo should we rename this to "handles" and use same type as node.handles?
+ isParent: boolean;
+ /** Holds a reference to the original node object provided by the user.
+ * Used as an optimization to avoid certain operations. */
+ userNode: NodeType;
handleBounds?: NodeHandleBounds;
- isParent?: boolean;
- /** Holds a reference to the original node object provided by the user
- * (which may lack some fields, like `computed` or `[internalSymbol]`. Used
- * as an optimization to avoid certain operations. */
- userProvidedNode: NodeBase;
};
};
@@ -104,7 +108,7 @@ export type NodeHandleBounds = {
export type NodeDimensionUpdate = {
id: string;
nodeElement: HTMLDivElement;
- forceUpdate?: boolean;
+ force?: boolean;
};
export type NodeBounds = XYPosition & {
@@ -117,13 +121,15 @@ export type NodeDragItem = {
position: XYPosition;
// distance from the mouse cursor to the node when start dragging
distance: XYPosition;
- computed: {
+ measured: {
width: number | null;
height: number | null;
+ };
+ internals: {
positionAbsolute: XYPosition;
};
extent?: 'parent' | CoordinateExtent;
- parentNode?: string;
+ parentId?: string;
dragging?: boolean;
origin?: NodeOrigin;
expandParent?: boolean;
@@ -137,4 +143,4 @@ export type NodeHandle = Optional;
export type Align = 'center' | 'start' | 'end';
-export type NodeLookup = Map;
+export type NodeLookup = Map;
diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts
index f53db23a..9e54e035 100644
--- a/packages/system/src/utils/edges/general.ts
+++ b/packages/system/src/utils/edges/general.ts
@@ -1,5 +1,4 @@
-import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
-import { EdgeBase, NodeBase } from '../../types';
+import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..';
import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
@@ -24,8 +23,8 @@ export function getEdgeCenter({
}
export type GetEdgeZIndexParams = {
- sourceNode: NodeBase;
- targetNode: NodeBase;
+ sourceNode: InternalNodeBase;
+ targetNode: InternalNodeBase;
selected?: boolean;
zIndex?: number;
elevateOnSelect?: boolean;
@@ -43,14 +42,14 @@ export function getElevatedEdgeZIndex({
}
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
- const selectedZIndex = Math.max(sourceNode[internalsSymbol]?.z || 0, targetNode[internalsSymbol]?.z || 0, 1000);
+ const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000);
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
}
type IsEdgeVisibleParams = {
- sourceNode: NodeBase;
- targetNode: NodeBase;
+ sourceNode: InternalNodeBase;
+ targetNode: InternalNodeBase;
width: number;
height: number;
transform: Transform;
diff --git a/packages/system/src/utils/edges/positions.ts b/packages/system/src/utils/edges/positions.ts
index 55a157ea..de8416db 100644
--- a/packages/system/src/utils/edges/positions.ts
+++ b/packages/system/src/utils/edges/positions.ts
@@ -1,25 +1,26 @@
import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general';
-import { NodeBase, NodeHandle } from '../../types/nodes';
+import { InternalNodeBase, NodeHandle } from '../../types/nodes';
import { Position } from '../../types/utils';
-import { errorMessages, internalsSymbol } from '../../constants';
+import { errorMessages } from '../../constants';
import { HandleElement } from '../../types';
import { getNodeDimensions } from '../general';
export type GetEdgePositionParams = {
id: string;
- sourceNode: NodeBase;
+ sourceNode: InternalNodeBase;
sourceHandle: string | null;
- targetNode: NodeBase;
+ targetNode: InternalNodeBase;
targetHandle: string | null;
connectionMode: ConnectionMode;
onError?: OnError;
};
-function isNodeInitialized(node: NodeBase): boolean {
+function isNodeInitialized(node: InternalNodeBase): boolean {
return (
- !!(node?.[internalsSymbol]?.handleBounds || node?.handles?.length) &&
- !!(node?.computed?.width || node?.width || node?.initialWidth)
+ node &&
+ !!(node.internals.handleBounds || node.handles?.length) &&
+ !!(node.measured.width || node.width || node.initialWidth)
);
}
@@ -30,8 +31,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
return null;
}
- const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
- const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles);
+ const sourceHandleBounds = sourceNode.internals.handleBounds || toHandleBounds(sourceNode.handles);
+ const targetHandleBounds = targetNode.internals.handleBounds || toHandleBounds(targetNode.handles);
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
const targetHandle = getHandle(
@@ -95,9 +96,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
};
}
-function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] {
- const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
- const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
+function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] {
+ const x = (handle?.x ?? 0) + (node.internals.positionAbsolute?.x ?? 0);
+ const y = (handle?.y ?? 0) + (node.internals.positionAbsolute?.y ?? 0);
const { width, height } = handle ?? getNodeDimensions(node);
switch (position) {
diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts
index 3a07e927..b8af19bc 100644
--- a/packages/system/src/utils/general.ts
+++ b/packages/system/src/utils/general.ts
@@ -8,6 +8,7 @@ import type {
NodeOrigin,
SnapGrid,
Transform,
+ InternalNodeBase,
} from '../types';
import { type Viewport } from '../types';
import { getNodePositionWithOrigin } from './graph';
@@ -65,23 +66,23 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
height: y2 - y,
});
-export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
+export const nodeToRect = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return {
...positionAbsolute,
- width: node.computed?.width ?? node.width ?? 0,
- height: node.computed?.height ?? node.height ?? 0,
+ width: node.measured?.width ?? node.width ?? 0,
+ height: node.measured?.height ?? node.height ?? 0,
};
};
-export const nodeToBox = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
+export const nodeToBox = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return {
...positionAbsolute,
- x2: positionAbsolute.x + (node.computed?.width ?? node.width ?? 0),
- y2: positionAbsolute.y + (node.computed?.height ?? node.height ?? 0),
+ x2: positionAbsolute.x + (node.measured?.width ?? node.width ?? 0),
+ y2: positionAbsolute.y + (node.measured?.height ?? node.height ?? 0),
};
};
@@ -207,14 +208,14 @@ export function getNodeDimensions(
node: NodeType
): { width: number; height: number } {
return {
- width: node.computed?.width ?? node.width ?? node.initialWidth ?? 0,
- height: node.computed?.height ?? node.height ?? node.initialHeight ?? 0,
+ width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
+ height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,
};
}
export function nodeHasDimensions(node: NodeType): boolean {
return (
- (node.computed?.width ?? node.width ?? node.initialWidth) !== undefined &&
- (node.computed?.height ?? node.height ?? node.initialHeight) !== undefined
+ (node.measured?.width ?? node.width ?? node.initialWidth) !== undefined &&
+ (node.measured?.height ?? node.height ?? node.initialHeight) !== undefined
);
}
diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts
index 06bf7f62..273a0057 100644
--- a/packages/system/src/utils/graph.ts
+++ b/packages/system/src/utils/graph.ts
@@ -24,6 +24,7 @@ import {
OnError,
OnBeforeDeleteBase,
NodeLookup,
+ InternalNodeBase,
} from '../types';
import { errorMessages } from '../constants';
@@ -47,6 +48,10 @@ export const isEdgeBase = (element: any):
export const isNodeBase = (element: any): element is NodeType =>
'id' in element && 'position' in element && !('source' in element) && !('target' in element);
+export const isInternalNodeBase = (
+ element: any
+): element is NodeType => 'id' in element && 'internals' in element && !('source' in element) && !('target' in element);
+
/**
* Pass in a node, and get connected nodes where edge.source === node.id
* @public
@@ -101,7 +106,7 @@ export const getIncomers = {
if (!node) {
@@ -128,12 +133,13 @@ export const getNodePositionWithOrigin = (
return {
position,
- positionAbsolute: node.computed?.positionAbsolute
- ? {
- x: node.computed.positionAbsolute.x - offsetX,
- y: node.computed.positionAbsolute.y - offsetY,
- }
- : position,
+ positionAbsolute:
+ 'internals' in node
+ ? {
+ x: node.internals.positionAbsolute.x - offsetX,
+ y: node.internals.positionAbsolute.y - offsetY,
+ }
+ : position,
};
};
@@ -151,6 +157,7 @@ export type GetNodesBoundsParams = {
* @param params.useRelativePosition - Whether to use the relative or absolute node positions
* @returns Bounding box enclosing all nodes
*/
+// @todo how to handle this if users do not have absolute positions?
export const getNodesBounds = (
nodes: NodeBase[],
params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false }
@@ -176,8 +183,47 @@ export const getNodesBounds = (
return boxToRect(box);
};
-export const getNodesInside = (
- nodes: NodeType[],
+export type GetInternalNodesBoundsParams = {
+ nodeOrigin?: NodeOrigin;
+ useRelativePosition?: boolean;
+ filter?: (node: NodeBase) => boolean;
+};
+
+/**
+ * Determines a bounding box that contains all given nodes in an array
+ * @internal
+ */
+export const getInternalNodesBounds = (
+ nodeLookup: NodeLookup,
+ params: GetInternalNodesBoundsParams = {
+ nodeOrigin: [0, 0],
+ useRelativePosition: false,
+ }
+): Rect => {
+ if (nodeLookup.size === 0) {
+ return { x: 0, y: 0, width: 0, height: 0 };
+ }
+
+ let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity };
+
+ nodeLookup.forEach((node) => {
+ if (params.filter == undefined || params.filter(node)) {
+ const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
+ box = getBoundsOfBoxes(
+ box,
+ rectToBox({
+ ...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'],
+ ...getNodeDimensions(node),
+ })
+ );
+ }
+ });
+
+ return boxToRect(box);
+};
+
+export const getNodesInside = (
+ nodeLookup: Map>,
rect: Rect,
[tx, ty, tScale]: Transform = [0, 0, 1],
partially = false,
@@ -191,13 +237,15 @@ export const getNodesInside = (
height: rect.height / tScale,
};
- const visibleNodes = nodes.reduce((res, node) => {
- const { computed, selectable = true, hidden = false } = node;
- const width = computed?.width ?? node.width ?? node.initialWidth ?? null;
- const height = computed?.height ?? node.height ?? node.initialHeight ?? null;
+ const visibleNodes: NodeType[] = [];
+
+ for (const [, node] of nodeLookup) {
+ const { measured, selectable = true, hidden = false } = node;
+ const width = measured.width ?? node.width ?? node.initialWidth ?? null;
+ const height = measured.height ?? node.height ?? node.initialHeight ?? null;
if ((excludeNonSelectableNodes && !selectable) || hidden) {
- return res;
+ continue;
}
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
@@ -208,11 +256,9 @@ export const getNodesInside = (
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
if (isVisible || node.dragging) {
- res.push(node);
+ visibleNodes.push(node);
}
-
- return res;
- }, []);
+ }
return visibleNodes;
};
@@ -236,17 +282,20 @@ export const getConnectedEdges = , Options extends FitViewOptionsBase>(
- { nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
+ { nodeLookup, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
options?: Options
) {
- const filteredNodes = nodes.filter((n) => {
- const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden);
+ const filteredNodes: InternalNodeBase[] = [];
- if (options?.nodes?.length) {
- return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id);
+ nodeLookup.forEach((n) => {
+ const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden);
+
+ if (
+ isVisible &&
+ (!options?.nodes || (options?.nodes.length && options?.nodes.some((optionNode) => optionNode.id === n.id)))
+ ) {
+ filteredNodes.push(n);
}
-
- return isVisible;
});
if (filteredNodes.length > 0) {
@@ -284,7 +333,7 @@ function clampNodeExtent(
if (!extent || extent === 'parent') {
return extent;
}
- return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]];
+ return [extent[0], [extent[1][0] - (node.measured?.width ?? 0), extent[1][1] - (node.measured?.height ?? 0)]];
}
/**
@@ -303,26 +352,27 @@ export function calculateNodePosition({
}: {
nodeId: string;
nextPosition: XYPosition;
- nodeLookup: NodeLookup;
+ nodeLookup: NodeLookup>;
nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent;
onError?: OnError;
}): { position: XYPosition; positionAbsolute: XYPosition } {
const node = nodeLookup.get(nodeId)!;
- const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : undefined;
+ const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined;
const { x: parentX, y: parentY } = parentNode
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
: { x: 0, y: 0 };
+
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
if (node.extent === 'parent' && !node.expandParent) {
if (!parentNode) {
onError?.('005', errorMessages['error005']());
} else {
- const nodeWidth = node.computed?.width;
- const nodeHeight = node.computed?.height;
- const parentWidth = parentNode?.computed?.width;
- const parentHeight = parentNode?.computed?.height;
+ const nodeWidth = node.measured.width;
+ const nodeHeight = node.measured.height;
+ const parentWidth = parentNode.measured.width;
+ const parentHeight = parentNode.measured.height;
if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
const currNodeOrigin = node.origin || nodeOrigin;
@@ -390,7 +440,7 @@ export async function getElementsToRemove n.id === node.parentNode);
+ const parentHit = !isIncluded && node.parentId && matchingNodes.find((n) => n.id === node.parentId);
if (isIncluded || parentHit) {
matchingNodes.push(node);
diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts
index c04c7e51..7ab5504a 100644
--- a/packages/system/src/utils/store.ts
+++ b/packages/system/src/utils/store.ts
@@ -1,8 +1,6 @@
-import { internalsSymbol } from '../constants';
import {
NodeBase,
CoordinateExtent,
- Dimensions,
NodeDimensionUpdate,
NodeOrigin,
PanZoomInstance,
@@ -12,54 +10,58 @@ import {
ConnectionLookup,
EdgeBase,
EdgeLookup,
+ InternalNodeBase,
+ NodeChange,
+ NodeLookup,
+ Rect,
} from '../types';
import { getDimensions, getHandleBounds } from './dom';
-import { isNumeric } from './general';
+import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general';
import { getNodePositionWithOrigin } from './graph';
-type ParentNodes = Record;
-
export function updateAbsolutePositions(
- nodes: NodeType[],
- nodeLookup: Map,
- nodeOrigin: NodeOrigin = [0, 0],
- parentNodes?: ParentNodes
+ nodeLookup: Map>,
+ options: UpdateNodesOptions = {
+ nodeOrigin: [0, 0] as NodeOrigin,
+ elevateNodesOnSelect: true,
+ defaults: {},
+ },
+ parentNodeIds?: Set
) {
- return nodes.map((node) => {
- if (node.parentNode && !nodeLookup.has(node.parentNode)) {
- throw new Error(`Parent node ${node.parentNode} not found`);
+ const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
+
+ for (const [id, node] of nodeLookup) {
+ const parentId = node.parentId;
+
+ if (parentId && !nodeLookup.has(parentId)) {
+ throw new Error(`Parent node ${parentId} not found`);
}
- if (node.parentNode || parentNodes?.[node.id]) {
- const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null;
+ if (parentId || node.internals.isParent || parentNodeIds?.has(id)) {
+ const parentNode = parentId ? nodeLookup.get(parentId) : null;
const { x, y, z } = calculateXYZPosition(
node,
- nodes,
nodeLookup,
{
...node.position,
- z: node[internalsSymbol]?.z ?? 0,
+ z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0),
},
- parentNode?.origin || nodeOrigin
+ parentNode?.origin || options.nodeOrigin
);
- const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
- node.computed!.positionAbsolute = positionChanged
- ? {
- x,
- y,
- }
- : node.computed?.positionAbsolute;
+ const currPosition = node.internals.positionAbsolute;
+ const positionChanged = x !== currPosition.x || y !== currPosition.y;
- node[internalsSymbol]!.z = z;
+ node.internals.positionAbsolute = positionChanged ? { x, y } : currPosition;
+ node.internals.z = z;
- if (parentNodes?.[node.id]) {
- node[internalsSymbol]!.isParent = true;
+ if (parentNodeIds !== undefined) {
+ node.internals.isParent = !!parentNodeIds?.has(id);
}
- }
- return node;
- });
+ nodeLookup.set(id, node);
+ }
+ }
}
type UpdateNodesOptions = {
@@ -68,128 +70,188 @@ type UpdateNodesOptions = {
defaults?: Partial;
};
-export function adoptUserProvidedNodes(
+export function adoptUserNodes(
nodes: NodeType[],
- nodeLookup: Map,
+ nodeLookup: Map>,
options: UpdateNodesOptions = {
nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true,
defaults: {},
}
-): NodeType[] {
+) {
const tmpLookup = new Map(nodeLookup);
nodeLookup.clear();
- const parentNodes: ParentNodes = {};
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
+ const parentNodeIds = new Set();
- const nextNodes = nodes.map((n) => {
- const currentStoreNode = tmpLookup.get(n.id);
- if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) {
- nodeLookup.set(n.id, currentStoreNode);
- return currentStoreNode;
+ nodes.forEach((userNode) => {
+ const currentStoreNode = tmpLookup.get(userNode.id);
+
+ if (userNode.parentId) {
+ parentNodeIds.add(userNode.parentId);
}
- const node: NodeType = {
- ...options.defaults,
- ...n,
- computed: {
- positionAbsolute: n.position,
- width: n.computed?.width,
- height: n.computed?.height,
- },
- };
- const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);
- const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol];
-
- if (node.parentNode) {
- parentNodes[node.parentNode] = true;
+ if (userNode === currentStoreNode?.internals.userNode) {
+ nodeLookup.set(userNode.id, currentStoreNode);
+ } else {
+ nodeLookup.set(userNode.id, {
+ ...options.defaults,
+ ...userNode,
+ measured: {
+ width: userNode.measured?.width,
+ height: userNode.measured?.height,
+ },
+ internals: {
+ positionAbsolute: userNode.position,
+ handleBounds: currentStoreNode?.internals?.handleBounds,
+ z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0),
+ userNode,
+ isParent: false,
+ },
+ });
}
-
- Object.defineProperty(node, internalsSymbol, {
- enumerable: false,
- value: {
- handleBounds: currInternals?.handleBounds,
- z,
- userProvidedNode: n,
- },
- });
-
- nodeLookup.set(node.id, node);
-
- return node;
});
- const nodesWithPositions = updateAbsolutePositions(nextNodes, nodeLookup, options.nodeOrigin, parentNodes);
-
- return nodesWithPositions;
+ if (parentNodeIds.size > 0) {
+ updateAbsolutePositions(nodeLookup, options, parentNodeIds);
+ }
}
function calculateXYZPosition(
node: NodeType,
- nodes: NodeType[],
- nodeLookup: Map,
+ nodeLookup: Map>,
result: XYZPosition,
- nodeOrigin: NodeOrigin
+ nodeOrigin: NodeOrigin = [0, 0]
): XYZPosition {
- if (!node.parentNode) {
+ if (!node.parentId) {
return result;
}
- const parentNode = nodeLookup.get(node.parentNode)!;
+ const parentNode = nodeLookup.get(node.parentId)!;
const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
return calculateXYZPosition(
parentNode,
- nodes,
nodeLookup,
{
x: (result.x ?? 0) + parentNodePosition.x,
y: (result.y ?? 0) + parentNodePosition.y,
- z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0,
+ z: (parentNode.internals.z ?? 0) > (result.z ?? 0) ? parentNode.internals.z ?? 0 : result.z ?? 0,
},
parentNode.origin || nodeOrigin
);
}
-export function updateNodeDimensions(
+export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLookup): NodeChange[] {
+ const changes: NodeChange[] = [];
+ const chilNodeRects = new Map();
+
+ nodes.forEach((node) => {
+ const parentId = node.parentId;
+ if (node.expandParent && parentId) {
+ const parentNode = nodeLookup.get(parentId);
+
+ if (parentNode) {
+ const parentRect = chilNodeRects.get(parentId) || nodeToRect(parentNode, node.origin);
+ const expandedRect = getBoundsOfRects(parentRect, nodeToRect(node, node.origin));
+ chilNodeRects.set(parentId, expandedRect);
+ }
+ }
+ });
+
+ if (chilNodeRects.size > 0) {
+ chilNodeRects.forEach((rect, id) => {
+ const origParent = nodeLookup.get(id)!;
+ const { position } = getNodePositionWithOrigin(origParent, origParent.origin);
+ const dimensions = getNodeDimensions(origParent);
+
+ if (rect.x < position.x || rect.y < position.y) {
+ const xChange = Math.round(Math.abs(position.x - rect.x));
+ const yChange = Math.round(Math.abs(position.y - rect.y));
+
+ changes.push({
+ id,
+ type: 'position',
+ position: {
+ x: position.x - xChange,
+ y: position.y - yChange,
+ },
+ });
+
+ changes.push({
+ id,
+ type: 'dimensions',
+ resizing: true,
+ dimensions: {
+ width: dimensions.width + xChange,
+ height: dimensions.height + yChange,
+ },
+ });
+
+ // @todo we need to reset child node positions if < 0
+ } else if (dimensions.width < rect.width || dimensions.height < rect.height) {
+ changes.push({
+ id,
+ type: 'dimensions',
+ resizing: true,
+ dimensions: {
+ width: Math.max(dimensions.width, rect.width),
+ height: Math.max(dimensions.height, rect.height),
+ },
+ });
+ }
+ });
+ }
+
+ return changes;
+}
+
+export function updateNodeDimensions(
updates: Map,
- nodes: NodeType[],
nodeLookup: Map,
domNode: HTMLElement | null,
- nodeOrigin?: NodeOrigin,
- onUpdate?: (id: string, dimensions: Dimensions) => void
-): NodeType[] | null {
+ nodeOrigin?: NodeOrigin
+): NodeChange[] {
const viewportNode = domNode?.querySelector('.xyflow__viewport');
if (!viewportNode) {
- return null;
+ return [];
}
+ const changes: NodeChange[] = [];
const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
+ // in this array we collect nodes, that might trigger changes (like expanding parent)
+ const triggerChangeNodes: NodeType[] = [];
- const nextNodes = nodes.map((node) => {
- const update = updates.get(node.id);
+ updates.forEach((update) => {
+ const node = nodeLookup.get(update.id);
- if (update) {
+ if (node?.hidden) {
+ nodeLookup.set(node.id, {
+ ...node,
+ internals: {
+ ...node.internals,
+ handleBounds: undefined,
+ },
+ });
+ } else if (node) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate = !!(
dimensions.width &&
dimensions.height &&
- (node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.forceUpdate)
+ (node.measured?.width !== dimensions.width || node.measured?.height !== dimensions.height || update.force)
);
if (doUpdate) {
- onUpdate?.(node.id, dimensions);
-
const newNode = {
...node,
- computed: {
- ...node.computed,
+ measured: {
+ ...node.measured,
...dimensions,
},
- [internalsSymbol]: {
- ...node[internalsSymbol],
+ internals: {
+ ...node.internals,
handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin),
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin),
@@ -199,14 +261,25 @@ export function updateNodeDimensions(
nodeLookup.set(node.id, newNode);
- return newNode;
+ changes.push({
+ id: newNode.id,
+ type: 'dimensions',
+ dimensions,
+ });
+
+ if (newNode.expandParent) {
+ triggerChangeNodes.push(newNode);
+ }
}
}
-
- return node;
});
- return nextNodes;
+ if (triggerChangeNodes.length > 0) {
+ const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup);
+ changes.push(...parentExpandChanges);
+ }
+
+ return changes;
}
export function panBy({
diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts
index cfed09f9..573b87af 100644
--- a/packages/system/src/xydrag/XYDrag.ts
+++ b/packages/system/src/xydrag/XYDrag.ts
@@ -26,13 +26,14 @@ import type {
OnSelectionDrag,
UpdateNodePositions,
Box,
+ InternalNodeBase,
} from '../types';
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
type StoreItems = {
nodes: NodeBase[];
- nodeLookup: Map;
+ nodeLookup: Map;
edges: EdgeBase[];
nodeExtent: CoordinateExtent;
snapGrid: SnapGrid;
@@ -130,19 +131,23 @@ export function XYDrag voi
// if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
// based on its position so that the node stays at it's position relative to the selection.
- const adjustedNodeExtent: CoordinateExtent = [
+ let adjustedNodeExtent: CoordinateExtent = [
[nodeExtent[0][0], nodeExtent[0][1]],
[nodeExtent[1][0], nodeExtent[1][1]],
];
if (dragItems.length > 1 && nodeExtent && !n.extent) {
- adjustedNodeExtent[0][0] = n.computed.positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
- adjustedNodeExtent[1][0] =
- n.computed.positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
+ const { positionAbsolute } = n.internals;
+ const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
+ const x2 = positionAbsolute.x + (n.measured?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
- adjustedNodeExtent[0][1] = n.computed.positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
- adjustedNodeExtent[1][1] =
- n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
+ const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
+ const y2 = positionAbsolute.y + (n.measured?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
+
+ adjustedNodeExtent = [
+ [x1, y1],
+ [x2, y2],
+ ];
}
const { position, positionAbsolute } = calculateNodePosition({
@@ -158,7 +163,7 @@ export function XYDrag voi
hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y;
n.position = position;
- n.computed.positionAbsolute = positionAbsolute;
+ n.internals.positionAbsolute = positionAbsolute;
return n;
});
@@ -208,7 +213,6 @@ export function XYDrag voi
function startDrag(event: UseDragEvent) {
const {
- nodes,
nodeLookup,
multiSelectionActive,
nodesDraggable,
@@ -236,7 +240,7 @@ export function XYDrag voi
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
lastPos = pointerPos;
- dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId);
+ dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId);
if (dragItems.length > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) {
const [currentNode, currentNodes] = getEventHandlerParams({
diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts
index 15c9af68..adead2b7 100644
--- a/packages/system/src/xydrag/utils.ts
+++ b/packages/system/src/xydrag/utils.ts
@@ -1,15 +1,15 @@
-import { type NodeDragItem, type XYPosition, NodeBase } from '../types';
+import { type NodeDragItem, type XYPosition, InternalNodeBase, NodeBase, NodeLookup } from '../types';
export function wrapSelectionDragFunc(selectionFunc?: (event: MouseEvent, nodes: NodeBase[]) => void) {
return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes);
}
-export function isParentSelected(node: NodeType, nodes: NodeType[]): boolean {
- if (!node.parentNode) {
+export function isParentSelected(node: NodeType, nodeLookup: NodeLookup): boolean {
+ if (!node.parentId) {
return false;
}
- const parentNode = nodes.find((node) => node.id === node.parentNode);
+ const parentNode = nodeLookup.get(node.parentId);
if (!parentNode) {
return false;
@@ -19,7 +19,7 @@ export function isParentSelected(node: NodeType, node
return true;
}
- return isParentSelected(parentNode, nodes);
+ return isParentSelected(parentNode, nodeLookup);
}
export function hasSelector(target: Element, selector: string, domNode: Element): boolean {
@@ -36,39 +36,43 @@ export function hasSelector(target: Element, selector: string, domNode: Element)
// looks for all selected nodes and created a NodeDragItem for each of them
export function getDragItems(
- nodes: NodeType[],
+ nodeLookup: Map>,
nodesDraggable: boolean,
mousePos: XYPosition,
nodeId?: string
): NodeDragItem[] {
- return nodes
- .filter(
- (n) =>
- (n.selected || n.id === nodeId) &&
- (!n.parentNode || !isParentSelected(n, nodes)) &&
- (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined'))
- )
- .map((n) => ({
- id: n.id,
- position: n.position || { x: 0, y: 0 },
- distance: {
- x: mousePos.x - (n.computed?.positionAbsolute?.x ?? 0),
- y: mousePos.y - (n.computed?.positionAbsolute?.y ?? 0),
- },
- delta: {
- x: 0,
- y: 0,
- },
- extent: n.extent,
- parentNode: n.parentNode,
- origin: n.origin,
- expandParent: n.expandParent,
- computed: {
- positionAbsolute: n.computed?.positionAbsolute || { x: 0, y: 0 },
- width: n.computed?.width || 0,
- height: n.computed?.height || 0,
- },
- }));
+ const dragItems: NodeDragItem[] = [];
+
+ for (const [id, node] of nodeLookup) {
+ if (
+ (node.selected || node.id === nodeId) &&
+ (!node.parentId || !isParentSelected(node, nodeLookup)) &&
+ (node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'))
+ ) {
+ const internalNode = nodeLookup.get(id)!;
+
+ dragItems.push({
+ id: internalNode.id,
+ position: internalNode.position || { x: 0, y: 0 },
+ distance: {
+ x: mousePos.x - (internalNode.internals.positionAbsolute?.x ?? 0),
+ y: mousePos.y - (internalNode.internals.positionAbsolute?.y ?? 0),
+ },
+ extent: internalNode.extent,
+ parentId: internalNode.parentId,
+ origin: internalNode.origin,
+ expandParent: internalNode.expandParent,
+ internals: {
+ positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
+ },
+ measured: {
+ width: internalNode.measured.width || 0,
+ height: internalNode.measured.height || 0,
+ },
+ });
+ }
+ }
+ return dragItems;
}
// returns two params:
@@ -89,9 +93,8 @@ export function getEventHandlerParams({
return {
...node,
position: n.position,
- computed: {
- ...n.computed,
- positionAbsolute: n.computed.positionAbsolute,
+ measured: {
+ ...n.measured,
},
};
});
diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts
index 638ec966..80b09e61 100644
--- a/packages/system/src/xyhandle/XYHandle.ts
+++ b/packages/system/src/xyhandle/XYHandle.ts
@@ -6,13 +6,13 @@ import {
type HandleType,
type Connection,
type PanBy,
- type NodeBase,
type Transform,
type ConnectingHandle,
type OnConnectEnd,
type UpdateConnection,
type IsValidConnection,
type ConnectionHandle,
+ NodeLookup,
} from '../types';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils';
@@ -25,7 +25,7 @@ export type OnPointerDownParams = {
handleId: string | null;
nodeId: string;
isTarget: boolean;
- nodes: NodeBase[];
+ nodeLookup: NodeLookup;
lib: string;
flowId: string | null;
edgeUpdaterType?: HandleType;
@@ -79,7 +79,7 @@ function onPointerDown(
edgeUpdaterType,
isTarget,
domNode,
- nodes,
+ nodeLookup,
lib,
autoPanOnConnect,
flowId,
@@ -116,7 +116,7 @@ function onPointerDown(
let handleDomNode: Element | null = null;
const handleLookup = getHandleLookup({
- nodes,
+ nodeLookup,
nodeId,
handleId,
handleType,
diff --git a/packages/system/src/xyhandle/utils.ts b/packages/system/src/xyhandle/utils.ts
index 8c82f6e3..346ebe45 100644
--- a/packages/system/src/xyhandle/utils.ts
+++ b/packages/system/src/xyhandle/utils.ts
@@ -3,15 +3,15 @@ import {
type HandleType,
type NodeHandleBounds,
type XYPosition,
- type NodeBase,
type ConnectionHandle,
+ InternalNodeBase,
+ NodeLookup,
} from '../types';
-import { internalsSymbol } from '../constants';
// this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position
export function getHandles(
- node: NodeBase,
+ node: InternalNodeBase,
handleBounds: NodeHandleBounds,
type: HandleType,
currentHandle: string
@@ -22,8 +22,8 @@ export function getHandles(
id: h.id || null,
type,
nodeId: node.id,
- x: (node.computed?.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
- y: (node.computed?.positionAbsolute?.y ?? 0) + h.y + h.height / 2,
+ x: (node.internals.positionAbsolute.x ?? 0) + h.x + h.width / 2,
+ y: (node.internals.positionAbsolute.y ?? 0) + h.y + h.height / 2,
});
}
return res;
@@ -62,28 +62,30 @@ export function getClosestHandle(
}
type GetHandleLookupParams = {
- nodes: NodeBase[];
+ nodeLookup: NodeLookup;
nodeId: string;
handleId: string | null;
handleType: string;
};
-export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
- return nodes.reduce((res, node) => {
- if (node[internalsSymbol]) {
- const { handleBounds } = node[internalsSymbol];
- let sourceHandles: ConnectionHandle[] = [];
- let targetHandles: ConnectionHandle[] = [];
+export function getHandleLookup({
+ nodeLookup,
+ nodeId,
+ handleId,
+ handleType,
+}: GetHandleLookupParams): ConnectionHandle[] {
+ const connectionHandles: ConnectionHandle[] = [];
- if (handleBounds) {
- sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`);
- targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`);
- }
-
- res.push(...sourceHandles, ...targetHandles);
+ for (const [, node] of nodeLookup) {
+ if (node.internals.handleBounds) {
+ const id = `${nodeId}-${handleId}-${handleType}`;
+ const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id);
+ const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id);
+ connectionHandles.push(...sourceHandles, ...targetHandles);
}
- return res;
- }, []);
+ }
+
+ return connectionHandles;
}
export function getHandleType(
diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts
index 1953b327..50a3e4aa 100644
--- a/packages/system/src/xyresizer/XYResizer.ts
+++ b/packages/system/src/xyresizer/XYResizer.ts
@@ -71,15 +71,15 @@ export type XYResizerInstance = {
function nodeToParentExtent(node: NodeBase): CoordinateExtent {
return [
[0, 0],
- [node.computed!.width!, node.computed!.height!],
+ [node.measured!.width!, node.measured!.height!],
];
}
function nodeToChildExtent(child: NodeBase, parent: NodeBase, nodeOrigin: NodeOrigin): CoordinateExtent {
const x = parent.position.x + child.position.x;
const y = parent.position.y + child.position.y;
- const width = child.computed!.width! ?? 0;
- const height = child.computed!.height! ?? 0;
+ const width = child.measured!.width! ?? 0;
+ const height = child.measured!.height! ?? 0;
const originOffsetX = nodeOrigin[0] * width;
const originOffsetY = nodeOrigin[1] * height;
@@ -121,8 +121,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
prevValues = {
- width: node.computed?.width ?? 0,
- height: node.computed?.height ?? 0,
+ width: node.measured?.width ?? 0,
+ height: node.measured?.height ?? 0,
x: node.position.x ?? 0,
y: node.position.y ?? 0,
};
@@ -136,7 +136,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
parentNode = undefined;
if (node.extent === 'parent' || node.expandParent) {
- parentNode = nodeLookup.get(node.parentNode!);
+ parentNode = nodeLookup.get(node.parentId!);
if (parentNode && node.extent === 'parent') {
parentExtent = nodeToParentExtent(parentNode);
}
@@ -148,7 +148,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
childExtent = undefined;
for (const [childId, child] of nodeLookup) {
- if (child.parentNode === nodeId) {
+ if (child.parentId === nodeId) {
childNodes.push({
id: childId,
position: { ...child.position },