Merge pull request #4105 from xyflow/refactor/internal-nodes

React Flow 12: separate user nodes and internal nodes
This commit is contained in:
Moritz Klack
2024-04-09 13:33:25 +02:00
committed by GitHub
57 changed files with 722 additions and 582 deletions
@@ -15,8 +15,7 @@ const initialNodes: Node[] = nodes.map((n) => ({
const expectedNodes: Node[] = initialNodes.map((n) => ({ const expectedNodes: Node[] = initialNodes.map((n) => ({
...n, ...n,
computed: { measured: {
positionAbsolute: n.position,
...nodeDimensions, ...nodeDimensions,
}, },
})); }));
@@ -48,7 +48,7 @@ describe('<ReactFlow />: onNodesChange', () => {
id: '1', id: '1',
item: { item: {
...nodes[0], ...nodes[0],
computed: { positionAbsolute: nodes[0].position, width: 200, height: 100 }, measured: { width: 200, height: 100 },
style: { width: 200, height: 100 }, style: { width: 200, height: 100 },
}, },
}, },
@@ -136,8 +136,8 @@ describe('applyChanges Testing', () => {
]; ];
const nextNodes = applyNodeChanges(nodeChanges, nodes); const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nodes[0].computed).to.be.undefined; expect(nodes[0].measured).to.be.undefined;
expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight }); expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
expect(nextNodes[0].width).to.be.undefined; expect(nextNodes[0].width).to.be.undefined;
expect(nextNodes[0].height).to.be.undefined; expect(nextNodes[0].height).to.be.undefined;
}); });
@@ -153,7 +153,7 @@ describe('applyChanges Testing', () => {
const nextNodes = applyNodeChanges(nodeChanges, nodes); const nextNodes = applyNodeChanges(nodeChanges, nodes);
expect(nextNodes[0].position).to.be.deep.equal(newPosition); 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', () => { it('replaces nodes/edges', () => {
+20 -9
View File
@@ -56,18 +56,19 @@ const initialEdges: Edge[] = [
const defaultEdgeOptions = {}; const defaultEdgeOptions = {};
const BasicFlow = () => { const BasicFlow = () => {
const { setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } = const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
useReactFlow(); useReactFlow();
const updatePos = () => { const updatePos = () => {
setNodes((nodes) => setNodes((nodes) =>
nodes.map((node) => { nodes.map((node) => {
node.position = { return {
x: Math.random() * 400, ...node,
y: Math.random() * 400, position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
}; };
return node;
}) })
); );
}; };
@@ -78,9 +79,10 @@ const BasicFlow = () => {
const toggleClassnames = () => { const toggleClassnames = () => {
setNodes((nodes) => setNodes((nodes) =>
nodes.map((node) => { 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('1', { label: 'update' });
updateNodeData('2', { 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 ( return (
<ReactFlow <ReactFlow
@@ -144,6 +154,7 @@ const BasicFlow = () => {
<button onClick={deleteSomeElements}>deleteSomeElements</button> <button onClick={deleteSomeElements}>deleteSomeElements</button>
<button onClick={onSetNodes}>setNodes</button> <button onClick={onSetNodes}>setNodes</button>
<button onClick={onUpdateNode}>updateNode</button> <button onClick={onUpdateNode}>updateNode</button>
<button onClick={addNode}>addNode</button>
</Panel> </Panel>
</ReactFlow> </ReactFlow>
); );
@@ -9,8 +9,8 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
width: intersectionNodeWidth, width: intersectionNodeWidth,
height: intersectionNodeHeight, height: intersectionNodeHeight,
positionAbsolute: intersectionNodePosition, positionAbsolute: intersectionNodePosition,
} = intersectionNode.computed || {}; } = intersectionNode.measured || {};
const targetPosition = targetNode.computed?.positionAbsolute!; const targetPosition = targetNode.measured?.positionAbsolute!;
const w = intersectionNodeWidth! / 2; const w = intersectionNodeWidth! / 2;
const h = intersectionNodeHeight! / 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 // returns the position (top,right,bottom or right) passed node compared to the intersection point
function getEdgePosition(node: Node, intersectionPoint: XYPosition) { 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 nx = Math.round(n.x!);
const ny = Math.round(n.y!); const ny = Math.round(n.y!);
const px = Math.round(intersectionPoint.x); const px = Math.round(intersectionPoint.x);
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) { if (px <= nx + 1) {
return Position.Left; return Position.Left;
} }
if (px >= nx + n.computed?.width! - 1) { if (px >= nx + n.measured?.width! - 1) {
return Position.Right; return Position.Right;
} }
if (py <= ny + 1) { if (py <= ny + 1) {
return Position.Top; return Position.Top;
} }
if (py >= n.y! + n.computed?.height! - 1) { if (py >= n.y! + n.measured?.height! - 1) {
return Position.Bottom; return Position.Bottom;
} }
@@ -5,8 +5,8 @@ import { getEdgeParams } from './utils';
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => { const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const { sourceNode, targetNode } = useStore((s) => { const { sourceNode, targetNode } = useStore((s) => {
const sourceNode = s.nodes.find((n) => n.id === source); const sourceNode = s.nodeLookup.get(source);
const targetNode = s.nodes.find((n) => n.id === target); const targetNode = s.nodeLookup.get(target);
return { sourceNode, targetNode }; return { sourceNode, targetNode };
}); });
@@ -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 // this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node // 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 // 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 { position: intersectionNodePosition } = intersectionNode;
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.computed ?? { const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured ?? {
width: 0, width: 0,
height: 0, height: 0,
}; };
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) { if (px <= nx + 1) {
return Position.Left; return Position.Left;
} }
if (px >= nx + (n.computed?.width ?? 0) - 1) { if (px >= nx + (n.measured?.width ?? 0) - 1) {
return Position.Right; return Position.Right;
} }
if (py <= ny + 1) { if (py <= ny + 1) {
return Position.Top; return Position.Top;
} }
if (py >= n.y + (n.computed?.height ?? 0) - 1) { if (py >= n.y + (n.measured?.height ?? 0) - 1) {
return Position.Bottom; 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 // 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 sourceIntersectionPoint = getNodeIntersection(source, target);
const targetIntersectionPoint = getNodeIntersection(target, source); const targetIntersectionPoint = getNodeIntersection(target, source);
@@ -137,7 +137,7 @@ const initialNodes: Node[] = [
label: 'Child with extent: parent', label: 'Child with extent: parent',
}, },
position: { x: 50, y: 50 }, position: { x: 50, y: 50 },
parentNode: '5', parentId: '5',
extent: 'parent', extent: 'parent',
width: 50, width: 50,
height: 100, height: 100,
@@ -148,7 +148,7 @@ const initialNodes: Node[] = [
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'Child with expandParent' }, data: { label: 'Child with expandParent' },
position: { x: 150, y: 100 }, position: { x: 150, y: 100 },
parentNode: '5', parentId: '5',
expandParent: true, expandParent: true,
style: { ...nodeStyle }, style: { ...nodeStyle },
}, },
@@ -157,7 +157,7 @@ const initialNodes: Node[] = [
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true }, data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true },
position: { x: 25, y: 200 }, position: { x: 25, y: 200 },
parentNode: '5', parentId: '5',
expandParent: true, expandParent: true,
style: { ...nodeStyle }, style: { ...nodeStyle },
}, },
@@ -50,7 +50,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 4a' }, data: { label: 'Node 4a' },
position: { x: 15, y: 15 }, position: { x: 15, y: 15 },
className: 'light', className: 'light',
parentNode: '4', parentId: '4',
origin: [0.5, 0.5], origin: [0.5, 0.5],
extent: [ extent: [
@@ -68,21 +68,21 @@ const initialNodes: Node[] = [
height: 200, height: 200,
width: 300, width: 300,
}, },
parentNode: '4', parentId: '4',
}, },
{ {
id: '4b1', id: '4b1',
data: { label: 'Node 4b1' }, data: { label: 'Node 4b1' },
position: { x: 40, y: 20 }, position: { x: 40, y: 20 },
className: 'light', className: 'light',
parentNode: '4b', parentId: '4b',
}, },
{ {
id: '4b2', id: '4b2',
data: { label: 'Node 4b2' }, data: { label: 'Node 4b2' },
position: { x: 20, y: 100 }, position: { x: 20, y: 100 },
className: 'light', className: 'light',
parentNode: '4b', parentId: '4b',
}, },
{ {
id: '5', id: '5',
@@ -98,7 +98,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 5a' }, data: { label: 'Node 5a' },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
className: 'light', className: 'light',
parentNode: '5', parentId: '5',
extent: 'parent', extent: 'parent',
}, },
{ {
@@ -106,7 +106,7 @@ const initialNodes: Node[] = [
data: { label: 'Node 5b' }, data: { label: 'Node 5b' },
position: { x: 225, y: 50 }, position: { x: 225, y: 50 },
className: 'light', className: 'light',
parentNode: '5', parentId: '5',
expandParent: true, expandParent: true,
}, },
{ {
@@ -160,7 +160,7 @@ const Subflow = () => {
const updatePos = () => { const updatePos = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
if (!n.parentNode) { if (!n.parentId) {
return { return {
...n, ...n,
position: { position: {
@@ -194,7 +194,7 @@ const Subflow = () => {
return nds.map((n) => { return nds.map((n) => {
return { return {
...n, ...n,
hidden: !!n.parentNode && !n.hidden, hidden: !!n.parentId && !n.hidden,
}; };
}); });
}); });
@@ -113,7 +113,7 @@
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'Child with extent parent' }, data: { label: 'Child with extent parent' },
position: { x: 50, y: 50 }, position: { x: 50, y: 50 },
parentNode: '5', parentId: '5',
extent: 'parent', extent: 'parent',
style: nodeStyle style: nodeStyle
}, },
@@ -122,7 +122,7 @@
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'Child' }, data: { label: 'Child' },
position: { x: 100, y: 100 }, position: { x: 100, y: 100 },
parentNode: '5', parentId: '5',
style: nodeStyle style: nodeStyle
} }
]); ]);
@@ -36,7 +36,7 @@
id: '4a', id: '4a',
data: { label: 'Node 4a' }, data: { label: 'Node 4a' },
position: { x: 15, y: 15 }, position: { x: 15, y: 15 },
parentNode: '4', parentId: '4',
extent: [ extent: [
[0, 0], [0, 0],
[100, 100] [100, 100]
@@ -47,19 +47,19 @@
data: { label: 'Node 4b' }, data: { label: 'Node 4b' },
position: { x: 100, y: 60 }, position: { x: 100, y: 60 },
style: 'width: 300px; height: 200px;', style: 'width: 300px; height: 200px;',
parentNode: '4' parentId: '4'
}, },
{ {
id: '4b1', id: '4b1',
data: { label: 'Node 4b1' }, data: { label: 'Node 4b1' },
position: { x: 40, y: 20 }, position: { x: 40, y: 20 },
parentNode: '4b' parentId: '4b'
}, },
{ {
id: '4b2', id: '4b2',
data: { label: 'Node 4b2' }, data: { label: 'Node 4b2' },
position: { x: 20, y: 100 }, position: { x: 20, y: 100 },
parentNode: '4b' parentId: '4b'
}, },
{ {
id: '5', id: '5',
@@ -73,14 +73,14 @@
id: '5a', id: '5a',
data: { label: 'Node 5a' }, data: { label: 'Node 5a' },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
parentNode: '5', parentId: '5',
extent: 'parent' extent: 'parent'
}, },
{ {
id: '5b', id: '5b',
data: { label: 'Node 5b' }, data: { label: 'Node 5b' },
position: { x: 225, y: 50 }, position: { x: 225, y: 50 },
parentNode: '5', parentId: '5',
expandParent: true expandParent: true
}, },
{ {
@@ -3,7 +3,7 @@
import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react'; import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; 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 { useStore, useStoreApi } from '../../hooks/useStore';
import { Panel } from '../../components/Panel'; import { Panel } from '../../components/Panel';
@@ -26,7 +26,9 @@ const selector = (s: ReactFlowState) => {
return { return {
viewBB, viewBB,
boundingRect: 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, rfId: s.rfId,
nodeOrigin: s.nodeOrigin, nodeOrigin: s.nodeOrigin,
panZoom: s.panZoom, panZoom: s.panZoom,
@@ -6,7 +6,7 @@ import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { MiniMapNode } from './MiniMapNode'; 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'; import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
declare const window: any; declare const window: any;
@@ -85,7 +85,7 @@ function NodeComponentWrapperInner<NodeType extends Node>({
shapeRendering: string; shapeRendering: string;
}) { }) {
const { node, x, y } = useStore((s) => { const { node, x, y } = useStore((s) => {
const node = s.nodeLookup.get(id) as NodeType; const node = s.nodeLookup.get(id) as InternalNode<NodeType>;
const { x, y } = getNodePositionWithOrigin(node, node?.origin || nodeOrigin).positionAbsolute; const { x, y } = getNodePositionWithOrigin(node, node?.origin || nodeOrigin).positionAbsolute;
return { return {
@@ -5,12 +5,14 @@ import {
ResizeControlVariant, ResizeControlVariant,
type XYResizerInstance, type XYResizerInstance,
type XYResizerChange, type XYResizerChange,
XYResizerChildChange, type XYResizerChildChange,
type NodeChange,
type NodeDimensionChange,
type NodePositionChange,
} from '@xyflow/system'; } from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore'; import { useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext'; import { useNodeId } from '../../contexts/NodeIdContext';
import type { NodeChange, NodeDimensionChange, NodePositionChange } from '../../types';
import type { ResizeControlProps, ResizeControlLineProps } from './types'; import type { ResizeControlProps, ResizeControlLineProps } from './types';
function ResizeControl({ function ResizeControl({
@@ -1,23 +1,23 @@
import { useCallback, CSSProperties } from 'react'; import { useCallback, CSSProperties } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; 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 { useStore } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext'; import { useNodeId } from '../../contexts/NodeIdContext';
import { NodeToolbarPortal } from './NodeToolbarPortal'; import { NodeToolbarPortal } from './NodeToolbarPortal';
import type { NodeToolbarProps } from './types'; import type { NodeToolbarProps } from './types';
const nodeEqualityFn = (a?: Node, b?: Node) => const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) =>
a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x || a?.internals.positionAbsolute.x !== b?.internals.positionAbsolute.x ||
a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y || a?.internals.positionAbsolute.y !== b?.internals.positionAbsolute.y ||
a?.computed?.width !== b?.computed?.width || a?.measured.width !== b?.measured.width ||
a?.computed?.height !== b?.computed?.height || a?.measured.height !== b?.measured.height ||
a?.selected !== b?.selected || 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) { if (a.length !== b.length) {
return false; return false;
} }
@@ -49,10 +49,10 @@ export function NodeToolbar({
const contextNodeId = useNodeId(); const contextNodeId = useNodeId();
const nodesSelector = useCallback( const nodesSelector = useCallback(
(state: ReactFlowState): Node[] => { (state: ReactFlowState): InternalNode[] => {
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || '']; const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
return nodeIds.reduce<Node[]>((acc, id) => { return nodeIds.reduce<InternalNode[]>((acc, id) => {
const node = state.nodeLookup.get(id); const node = state.nodeLookup.get(id);
if (node) { if (node) {
acc.push(node); acc.push(node);
@@ -74,7 +74,7 @@ export function NodeToolbar({
} }
const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin }); 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 = { const wrapperStyle: CSSProperties = {
position: 'absolute', position: 'absolute',
@@ -2,7 +2,6 @@ import { CSSProperties, useCallback } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { import {
internalsSymbol,
Position, Position,
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
@@ -53,7 +52,7 @@ const ConnectionLine = ({
), ),
shallow shallow
); );
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; const fromHandleBounds = fromNode?.internals?.handleBounds;
let handleBounds = fromHandleBounds?.[handleType]; let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) { if (connectionMode === ConnectionMode.Loose) {
@@ -65,10 +64,10 @@ const ConnectionLine = ({
} }
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; 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 fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0;
const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX; const fromX = (fromNode.internals.positionAbsolute.x ?? 0) + fromHandleX;
const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY; const fromY = (fromNode.internals.positionAbsolute.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null; const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
@@ -54,7 +54,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
onConnectStart, onConnectStart,
onConnectEnd, onConnectEnd,
cancelConnection, cancelConnection,
nodes, nodeLookup,
rfId: flowId, rfId: flowId,
panBy, panBy,
updateConnection, updateConnection,
@@ -82,7 +82,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
domNode, domNode,
handleId, handleId,
nodeId, nodeId,
nodes, nodeLookup,
isTarget, isTarget,
edgeUpdaterType: handleType, edgeUpdaterType: handleType,
lib, lib,
@@ -126,7 +126,7 @@ function HandleComponent(
connectionMode: currentStore.connectionMode, connectionMode: currentStore.connectionMode,
connectionRadius: currentStore.connectionRadius, connectionRadius: currentStore.connectionRadius,
domNode: currentStore.domNode, domNode: currentStore.domNode,
nodes: currentStore.nodes, nodeLookup: currentStore.nodeLookup,
lib: currentStore.lib, lib: currentStore.lib,
isTarget, isTarget,
handleId, handleId,
@@ -7,7 +7,6 @@ import {
errorMessages, errorMessages,
getNodeDimensions, getNodeDimensions,
getPositionWithOrigin, getPositionWithOrigin,
internalsSymbol,
isInputDOMNode, isInputDOMNode,
nodeHasDimensions, nodeHasDimensions,
} from '@xyflow/system'; } from '@xyflow/system';
@@ -19,7 +18,7 @@ import { useDrag } from '../../hooks/useDrag';
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
import { handleNodeClick } from '../Nodes/utils'; import { handleNodeClick } from '../Nodes/utils';
import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils'; import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils';
import type { Node, NodeWrapperProps } from '../../types'; import type { InternalNode, Node, NodeWrapperProps } from '../../types';
export function NodeWrapper<NodeType extends Node>({ export function NodeWrapper<NodeType extends Node>({
id, id,
@@ -44,11 +43,11 @@ export function NodeWrapper<NodeType extends Node>({
onError, onError,
}: NodeWrapperProps<NodeType>) { }: NodeWrapperProps<NodeType>) {
const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => { const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => {
const node = s.nodeLookup.get(id)! as NodeType; const node = s.nodeLookup.get(id)! as InternalNode<NodeType>;
const positionAbsolute = nodeExtent const positionAbsolute = nodeExtent
? clampPosition(node.computed?.positionAbsolute, nodeExtent) ? clampPosition(node.internals.positionAbsolute, nodeExtent)
: node.computed?.positionAbsolute || { x: 0, y: 0 }; : node.internals.positionAbsolute || { x: 0, y: 0 };
return { return {
node, node,
@@ -56,8 +55,8 @@ export function NodeWrapper<NodeType extends Node>({
// so we we need to force a re-render when some change // so we we need to force a re-render when some change
positionAbsoluteX: positionAbsolute.x, positionAbsoluteX: positionAbsolute.x,
positionAbsoluteY: positionAbsolute.y, positionAbsoluteY: positionAbsolute.y,
zIndex: node[internalsSymbol]?.z ?? 0, zIndex: node.internals.z,
isParent: !!node[internalsSymbol]?.isParent, isParent: node.internals.isParent,
}; };
}, shallow); }, shallow);
@@ -84,14 +83,16 @@ export function NodeWrapper<NodeType extends Node>({
const nodeDimensions = getNodeDimensions(node); const nodeDimensions = getNodeDimensions(node);
const inlineDimensions = getNodeInlineStyleDimensions(node); const inlineDimensions = getNodeInlineStyleDimensions(node);
const initialized = nodeHasDimensions(node); const initialized = nodeHasDimensions(node);
const hasHandleBounds = !!node[internalsSymbol]?.handleBounds; const hasHandleBounds = !!node.internals.handleBounds;
const moveSelectedNodes = useMoveSelectedNodes(); const moveSelectedNodes = useMoveSelectedNodes();
useEffect(() => { useEffect(() => {
const currNode = nodeRef.current;
return () => { return () => {
if (nodeRef.current) { if (currNode) {
resizeObserver?.unobserve(nodeRef.current); resizeObserver?.unobserve(currNode);
} }
}; };
}, []); }, []);
@@ -123,7 +124,7 @@ export function NodeWrapper<NodeType extends Node>({
if (targetPosChanged) { if (targetPosChanged) {
prevTargetPosition.current = node.targetPosition; 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]); }, [id, nodeType, node.sourcePosition, node.targetPosition]);
@@ -4,7 +4,7 @@ import { InputNode } from '../Nodes/InputNode';
import { DefaultNode } from '../Nodes/DefaultNode'; import { DefaultNode } from '../Nodes/DefaultNode';
import { GroupNode } from '../Nodes/GroupNode'; import { GroupNode } from '../Nodes/GroupNode';
import { OutputNode } from '../Nodes/OutputNode'; import { OutputNode } from '../Nodes/OutputNode';
import type { Node, NodeTypes } from '../../types'; import type { InternalNode, Node, NodeTypes } from '../../types';
export const arrowKeyDiffs: Record<string, XYPosition> = { export const arrowKeyDiffs: Record<string, XYPosition> = {
ArrowUp: { x: 0, y: -1 }, ArrowUp: { x: 0, y: -1 },
@@ -21,12 +21,12 @@ export const builtinNodeTypes: NodeTypes = {
}; };
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>( export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
node: NodeType node: InternalNode<NodeType>
): { ): {
width: number | string | undefined; width: number | string | undefined;
height: number | string | undefined; height: number | string | undefined;
} { } {
if (!node.computed) { if (node.internals.handleBounds === undefined) {
return { return {
width: node.width ?? node.initialWidth ?? node.style?.width, width: node.width ?? node.initialWidth ?? node.style?.width,
height: node.height ?? node.initialHeight ?? node.style?.height, height: node.height ?? node.initialHeight ?? node.style?.height,
@@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
import { useDrag } from '../../hooks/useDrag'; import { useDrag } from '../../hooks/useDrag';
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
import { arrowKeyDiffs } from '../NodeWrapper/utils'; import { arrowKeyDiffs } from '../NodeWrapper/utils';
import type { Node, ReactFlowState } from '../../types'; import type { InternalNode, Node, ReactFlowState } from '../../types';
export type NodesSelectionProps<NodeType> = { export type NodesSelectionProps<NodeType> = {
onSelectionContextMenu?: (event: MouseEvent, nodes: NodeType[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: NodeType[]) => void;
@@ -20,7 +20,13 @@ export type NodesSelectionProps<NodeType> = {
}; };
const selector = (s: ReactFlowState) => { 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 }); const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin });
return { return {
@@ -15,7 +15,7 @@ type SelectionListenerProps = {
}; };
const selector = (s: ReactFlowState) => ({ 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), selectedEdges: s.edges.filter((e) => e.selected),
}); });
@@ -138,7 +138,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
if (fieldValue === previousFieldValue) continue; if (fieldValue === previousFieldValue) continue;
if (typeof props[fieldName] === 'undefined') continue; if (typeof props[fieldName] === 'undefined') continue;
// Custom handling with dedicated setters for some fields // Custom handling with dedicated setters for some fields
if (fieldName === 'nodes') setNodes(fieldValue as Node[]); if (fieldName === 'nodes') setNodes(fieldValue as Node[]);
else if (fieldName === 'edges') setEdges(fieldValue as Edge[]); else if (fieldName === 'edges') setEdges(fieldValue as Edge[]);
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef } from 'react';
import { ReactFlowState } from '../../types'; import { ReactFlowState } from '../../types';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { NodeDimensionUpdate } from '@xyflow/system';
const selector = (s: ReactFlowState) => s.updateNodeDimensions; const selector = (s: ReactFlowState) => s.updateNodeDimensions;
@@ -15,14 +16,13 @@ export function useResizeObserver() {
} }
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => { const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = new Map(); const updates = new Map<string, NodeDimensionUpdate>();
entries.forEach((entry: ResizeObserverEntry) => { entries.forEach((entry: ResizeObserverEntry) => {
const id = entry.target.getAttribute('data-id') as string; const id = entry.target.getAttribute('data-id') as string;
updates.set(id, { updates.set(id, {
id, id,
nodeElement: entry.target as HTMLDivElement, nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
}); });
}); });
+8 -8
View File
@@ -5,13 +5,13 @@
import { useRef, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react'; import { useRef, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; 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 { UserSelection } from '../../components/UserSelection';
import { containerStyle } from '../../styles/utils'; import { containerStyle } from '../../styles/utils';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { getSelectionChanges } from '../../utils'; import { getSelectionChanges } from '../../utils';
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types'; import type { ReactFlowProps, ReactFlowState } from '../../types';
type PaneProps = { type PaneProps = {
isSelecting: boolean; isSelecting: boolean;
@@ -128,7 +128,7 @@ export function Pane({
}; };
const onMouseMove = (event: ReactMouseEvent): void => { const onMouseMove = (event: ReactMouseEvent): void => {
const { userSelectionRect, edges, transform, nodeOrigin, nodes, triggerNodeChanges, triggerEdgeChanges } = const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } =
store.getState(); store.getState();
if (!isSelecting || !containerBounds.current || !userSelectionRect) { if (!isSelecting || !containerBounds.current || !userSelectionRect) {
return; return;
@@ -149,7 +149,7 @@ export function Pane({
}; };
const selectedNodes = getNodesInside( const selectedNodes = getNodesInside(
nodes, nodeLookup,
nextUserSelectRect, nextUserSelectRect,
transform, transform,
selectionMode === SelectionMode.Partial, selectionMode === SelectionMode.Partial,
@@ -163,22 +163,22 @@ export function Pane({
for (const selectedNode of selectedNodes) { for (const selectedNode of selectedNodes) {
selectedNodeIds.add(selectedNode.id); selectedNodeIds.add(selectedNode.id);
for (const edge of edges) { for (const [edgeId, edge] of edgeLookup) {
if (edge.source === selectedNode.id || edge.target === selectedNode.id) { if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
selectedEdgeIds.add(edge.id); selectedEdgeIds.add(edgeId);
} }
} }
} }
if (prevSelectedNodesCount.current !== selectedNodeIds.size) { if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
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); triggerNodeChanges(changes);
} }
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) { if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
prevSelectedEdgesCount.current = selectedEdgeIds.size; prevSelectedEdgesCount.current = selectedEdgeIds.size;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; const changes = getSelectionChanges(edgeLookup, selectedEdgeIds) as EdgeChange[];
triggerEdgeChanges(changes); triggerEdgeChanges(changes);
} }
@@ -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<NodeType extends Node = Node>(id: string): InternalNode<NodeType> | undefined {
const node = useStore(
useCallback((s) => s.nodeLookup.get(id) as InternalNode<NodeType> | undefined, [id]),
shallow
);
return node;
}
@@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { calculateNodePosition, snapPosition, type XYPosition } from '@xyflow/system'; import { calculateNodePosition, snapPosition, type XYPosition } from '@xyflow/system';
import { Node } from '../types'; import { type Node } from '../types';
import { useStoreApi } from './useStore'; import { useStoreApi } from './useStore';
const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) => const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) =>
@@ -17,18 +17,11 @@ export function useMoveSelectedNodes() {
const store = useStoreApi(); const store = useStoreApi();
const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => { const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => {
const { const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } =
nodeExtent, store.getState();
nodes, const nodeUpdates = [];
snapToGrid, const isSelected = selectedAndDraggable(nodesDraggable);
snapGrid,
nodesDraggable,
onError,
updateNodePositions,
nodeLookup,
nodeOrigin,
} = store.getState();
const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable));
// by default a node moves 5px on each key press // by default a node moves 5px on each key press
// if snap grid is enabled, we use that for the velocity // if snap grid is enabled, we use that for the velocity
const xVelo = snapToGrid ? snapGrid[0] : 5; const xVelo = snapToGrid ? snapGrid[0] : 5;
@@ -37,32 +30,34 @@ export function useMoveSelectedNodes() {
const xDiff = params.direction.x * xVelo * params.factor; const xDiff = params.direction.x * xVelo * params.factor;
const yDiff = params.direction.y * yVelo * params.factor; const yDiff = params.direction.y * yVelo * params.factor;
const nodeUpdates = selectedNodes.map((node) => { for (const [, node] of nodeLookup) {
if (node.computed?.positionAbsolute) { if (!isSelected(node)) {
let nextPosition = { continue;
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;
} }
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); updateNodePositions(nodeUpdates);
}, []); }, []);
@@ -1,5 +1,3 @@
import { internalsSymbol } from '@xyflow/system';
import { useStore } from './useStore'; import { useStore } from './useStore';
import type { ReactFlowState } from '../types'; import type { ReactFlowState } from '../types';
@@ -8,13 +6,13 @@ export type UseNodesInitializedOptions = {
}; };
const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => { const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => {
if (s.nodes.length === 0) { if (s.nodeLookup.size === 0) {
return false; return false;
} }
for (const node of s.nodes) { for (const [, node] of s.nodeLookup) {
if (options.includeHiddenNodes || !node.hidden) { if (options.includeHiddenNodes || !node.hidden) {
if (node[internalsSymbol]?.handleBounds === undefined) { if (node.internals.handleBounds === undefined) {
return false; return false;
} }
} }
+23 -23
View File
@@ -1,16 +1,9 @@
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
getElementsToRemove,
getOverlappingArea,
isRectObject,
nodeHasDimensions,
nodeToRect,
type Rect,
} from '@xyflow/system';
import useViewportHelper from './useViewportHelper'; import useViewportHelper from './useViewportHelper';
import { useStoreApi } from './useStore'; 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 { getElementsDiffChanges, isNode } from '../utils';
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
@@ -27,13 +20,20 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
const viewportHelper = useViewportHelper(); const viewportHelper = useViewportHelper();
const store = useStoreApi(); const store = useStoreApi();
const getNodes = useCallback<Instance.GetNodes<NodeType>>(() => { const getNodes = useCallback<Instance.GetNodes<NodeType>>(
return store.getState().nodes.map((n) => ({ ...n })) as NodeType[]; () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[],
}, []); []
);
const getNode = useCallback<Instance.GetNode<NodeType>>((id) => { const getInternalNode = useCallback<Instance.GetInternalNode<NodeType>>(
return store.getState().nodeLookup.get(id) as NodeType; (id) => store.getState().nodeLookup.get(id) as InternalNode<NodeType>,
}, []); []
);
const getNode = useCallback<Instance.GetNode<NodeType>>(
(id) => getInternalNode(id)?.internals.userNode as NodeType,
[getInternalNode]
);
const getEdges = useCallback<Instance.GetEdges<EdgeType>>(() => { const getEdges = useCallback<Instance.GetEdges<EdgeType>>(() => {
const { edges = [] } = store.getState(); const { edges = [] } = store.getState();
@@ -223,13 +223,9 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
[] []
); );
const getNodeRect = useCallback((nodeOrRect: NodeType | { id: NodeType['id'] }): Rect | null => { const getNodeRect = useCallback(({ id }: { id: string }): Rect | null => {
const node = const internalNode = store.getState().nodeLookup.get(id);
isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect) return internalNode ? nodeToRect(internalNode) : null;
? nodeOrRect
: (store.getState().nodeLookup.get(nodeOrRect.id) as NodeType);
return node ? nodeToRect(node) : null;
}, []); }, []);
const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeType>>( const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeType>>(
@@ -242,7 +238,9 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
} }
return (nodes || store.getState().nodes).filter((n) => { return (nodes || store.getState().nodes).filter((n) => {
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; return false;
} }
@@ -308,6 +306,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
...viewportHelper, ...viewportHelper,
getNodes, getNodes,
getNode, getNode,
getInternalNode,
getEdges, getEdges,
getEdge, getEdge,
setNodes, setNodes,
@@ -325,6 +324,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
viewportHelper, viewportHelper,
getNodes, getNodes,
getNode, getNode,
getInternalNode,
getEdges, getEdges,
getEdge, getEdge,
setNodes, setNodes,
@@ -21,7 +21,7 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement; const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement;
if (nodeElement) { if (nodeElement) {
updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true }); updates.set(updateId, { id: updateId, nodeElement, force: true });
} }
}); });
@@ -48,12 +48,12 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return { x, y, zoom }; return { x, y, zoom };
}, },
fitView: (options) => { fitView: (options) => {
const { nodes, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState(); const { nodeLookup, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState();
return panZoom return panZoom
? fitView( ? fitView(
{ {
nodes, nodeLookup,
width, width,
height, height,
nodeOrigin, nodeOrigin,
@@ -1,13 +1,13 @@
import { getNodesInside } from '@xyflow/system'; import { useCallback } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { getNodesInside } from '@xyflow/system';
import { useStore } from './useStore'; import { useStore } from './useStore';
import type { Node, ReactFlowState } from '../types'; import type { Node, ReactFlowState } from '../types';
import { useCallback } from 'react';
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => { const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
return onlyRenderVisible return onlyRenderVisible
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map( ? getNodesInside<Node>(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
(node) => node.id (node) => node.id
) )
: Array.from(s.nodeLookup.keys()); : Array.from(s.nodeLookup.keys());
+2 -2
View File
@@ -26,9 +26,10 @@ export { useNodesInitialized, type UseNodesInitializedOptions } from './hooks/us
export { useHandleConnections } from './hooks/useHandleConnections'; export { useHandleConnections } from './hooks/useHandleConnections';
export { useNodesData } from './hooks/useNodesData'; export { useNodesData } from './hooks/useNodesData';
export { useConnection } from './hooks/useConnection'; export { useConnection } from './hooks/useConnection';
export { useInternalNode } from './hooks/useInternalNode';
export { useNodeId } from './contexts/NodeIdContext'; 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 { isNode, isEdge } from './utils/general';
export * from './additional-components'; export * from './additional-components';
@@ -103,5 +104,4 @@ export {
addEdge, addEdge,
updateEdge, updateEdge,
getConnectedEdges, getConnectedEdges,
internalsSymbol,
} from '@xyflow/system'; } from '@xyflow/system';
+62 -60
View File
@@ -2,27 +2,20 @@ import { createWithEqualityFn } from 'zustand/traditional';
import { import {
clampPosition, clampPosition,
fitView as fitViewSystem, fitView as fitViewSystem,
adoptUserProvidedNodes, adoptUserNodes,
updateAbsolutePositions, updateAbsolutePositions,
panBy as panBySystem, panBy as panBySystem,
Dimensions,
updateNodeDimensions as updateNodeDimensionsSystem, updateNodeDimensions as updateNodeDimensionsSystem,
updateConnectionLookup, updateConnectionLookup,
handleParentExpand,
NodeChange,
EdgeSelectionChange,
NodeSelectionChange,
} from '@xyflow/system'; } from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import getInitialState from './initialState'; import getInitialState from './initialState';
import type { import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions, InternalNode } from '../types';
ReactFlowState,
Node,
Edge,
NodeDimensionChange,
EdgeSelectionChange,
NodeSelectionChange,
NodePositionChange,
UnselectNodesAndEdgesParams,
FitViewOptions,
} from '../types';
const createRFStore = ({ const createRFStore = ({
nodes, nodes,
@@ -52,9 +45,9 @@ const createRFStore = ({
// //
// When this happens, we take the note objects passed by the user and extend them with fields // When this happens, we take the note objects passed by the user and extend them with fields
// relevant for internal React Flow operations. // 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[]) => { setEdges: (edges: Edge[]) => {
const { connectionLookup, edgeLookup } = get(); const { connectionLookup, edgeLookup } = get();
@@ -82,7 +75,6 @@ const createRFStore = ({
const { const {
onNodesChange, onNodesChange,
fitView, fitView,
nodes,
nodeLookup, nodeLookup,
fitViewOnInit, fitViewOnInit,
fitViewDone, fitViewDone,
@@ -91,35 +83,21 @@ const createRFStore = ({
nodeOrigin, nodeOrigin,
debug, debug,
} = get(); } = get();
const changes: NodeDimensionChange[] = [];
const updatedNodes = updateNodeDimensionsSystem( const changes = updateNodeDimensionsSystem(updates, nodeLookup, domNode, nodeOrigin);
updates,
nodes,
nodeLookup,
domNode,
nodeOrigin,
(id: string, dimensions: Dimensions) => {
changes.push({
id: id,
type: 'dimensions',
dimensions,
});
}
);
if (!updatedNodes) { if (changes.length === 0) {
return; return;
} }
const nextNodes = updateAbsolutePositions(updatedNodes, nodeLookup, nodeOrigin); updateAbsolutePositions(nodeLookup, { nodeOrigin });
// we call fitView once initially after all dimensions are set // we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone; let nextFitViewDone = fitViewDone;
if (!fitViewDone && fitViewOnInit) { if (!fitViewDone && fitViewOnInit) {
nextFitViewDone = fitView(nextNodes, { nextFitViewDone = fitView({
...fitViewOnInitOptions, ...fitViewOnInitOptions,
nodes: fitViewOnInitOptions?.nodes || nextNodes, nodes: fitViewOnInitOptions?.nodes,
}); });
} }
@@ -128,7 +106,7 @@ const createRFStore = ({
// has not provided an onNodesChange handler. // has not provided an onNodesChange handler.
// Nodes are only rendered if they have a width and height // Nodes are only rendered if they have a width and height
// attribute which they get from this handler. // attribute which they get from this handler.
set({ nodes: nextNodes, fitViewDone: nextFitViewDone }); set({ fitViewDone: nextFitViewDone });
if (changes?.length > 0) { if (changes?.length > 0) {
if (debug) { if (debug) {
@@ -138,18 +116,41 @@ const createRFStore = ({
} }
}, },
updateNodePositions: (nodeDragItems, dragging = false) => { updateNodePositions: (nodeDragItems, dragging = false) => {
const changes = nodeDragItems.map((node) => { const { nodeLookup } = get();
const change: NodePositionChange = { 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, id: node.id,
type: 'position', type: 'position',
position: node.position, position: node.position,
positionAbsolute: node.computed?.positionAbsolute,
dragging, 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; return change;
}); });
if (triggerChangeNodes.length > 0) {
const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup);
changes.push(...parentExpandChanges);
}
get().triggerNodeChanges(changes); get().triggerNodeChanges(changes);
}, },
triggerNodeChanges: (changes) => { triggerNodeChanges: (changes) => {
@@ -185,7 +186,7 @@ const createRFStore = ({
} }
}, },
addSelectedNodes: (selectedNodeIds) => { addSelectedNodes: (selectedNodeIds) => {
const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
if (multiSelectionActive) { if (multiSelectionActive) {
const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)); const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
@@ -193,11 +194,11 @@ const createRFStore = ({
return; return;
} }
triggerNodeChanges(getSelectionChanges(nodes, new Set([...selectedNodeIds]), true)); triggerNodeChanges(getSelectionChanges(nodeLookup, new Set([...selectedNodeIds]), true));
triggerEdgeChanges(getSelectionChanges(edges)); triggerEdgeChanges(getSelectionChanges(edgeLookup));
}, },
addSelectedEdges: (selectedEdgeIds) => { addSelectedEdges: (selectedEdgeIds) => {
const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
if (multiSelectionActive) { if (multiSelectionActive) {
const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)); const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
@@ -205,8 +206,8 @@ const createRFStore = ({
return; return;
} }
triggerEdgeChanges(getSelectionChanges(edges, new Set([...selectedEdgeIds]))); triggerEdgeChanges(getSelectionChanges(edgeLookup, new Set([...selectedEdgeIds])));
triggerNodeChanges(getSelectionChanges(nodes, new Set(), true)); triggerNodeChanges(getSelectionChanges(nodeLookup, new Set(), true));
}, },
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => { unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get(); const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get();
@@ -255,29 +256,30 @@ const createRFStore = ({
triggerEdgeChanges(edgeChanges); triggerEdgeChanges(edgeChanges);
}, },
setNodeExtent: (nodeExtent) => { 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({ set({
nodeExtent, nodeExtent,
nodes: nodes.map((node) => {
const positionAbsolute = clampPosition(node.position, nodeExtent);
return {
...node,
computed: {
...node.computed,
positionAbsolute,
},
};
}),
}); });
}, },
panBy: (delta): boolean => { panBy: (delta): boolean => {
const { transform, width, height, panZoom, translateExtent } = get(); const { transform, width, height, panZoom, translateExtent } = get();
return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
}, },
fitView: (nodes: Node[], options?: FitViewOptions): boolean => { fitView: (options?: FitViewOptions): boolean => {
const { panZoom, width, height, minZoom, maxZoom, nodeOrigin } = get(); const { panZoom, width, height, minZoom, maxZoom, nodeOrigin, nodeLookup } = get();
if (!panZoom) { if (!panZoom) {
return false; return false;
@@ -285,7 +287,7 @@ const createRFStore = ({
return fitViewSystem( return fitViewSystem(
{ {
nodes, nodeLookup,
width, width,
height, height,
panZoom, panZoom,
+4 -4
View File
@@ -1,7 +1,7 @@
import { import {
infiniteExtent, infiniteExtent,
ConnectionMode, ConnectionMode,
adoptUserProvidedNodes, adoptUserNodes,
getNodesBounds, getNodesBounds,
getViewportForBounds, getViewportForBounds,
Transform, Transform,
@@ -35,7 +35,7 @@ const getInitialState = ({
const storeNodes = defaultNodes ?? nodes ?? []; const storeNodes = defaultNodes ?? nodes ?? [];
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, { adoptUserNodes(storeNodes, nodeLookup, {
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
elevateNodesOnSelect: false, elevateNodesOnSelect: false,
}); });
@@ -43,7 +43,7 @@ const getInitialState = ({
let transform: Transform = [0, 0, 1]; let transform: Transform = [0, 0, 1];
if (fitView && width && height) { if (fitView && width && height) {
const nodesWithDimensions = nextNodes.filter( const nodesWithDimensions = storeNodes.filter(
(node) => (node.width || node.initialWidth) && (node.height || node.initialHeight) (node) => (node.width || node.initialWidth) && (node.height || node.initialHeight)
); );
// @todo users nodeOrigin should be used here // @todo users nodeOrigin should be used here
@@ -57,7 +57,7 @@ const getInitialState = ({
width: 0, width: 0,
height: 0, height: 0,
transform, transform,
nodes: nextNodes, nodes: storeNodes,
nodeLookup, nodeLookup,
edges: storeEdges, edges: storeEdges,
edgeLookup, edgeLookup,
+3 -1
View File
@@ -12,9 +12,11 @@ import {
XYPosition, XYPosition,
OnBeforeDeleteBase, OnBeforeDeleteBase,
Connection, Connection,
NodeChange,
EdgeChange,
} from '@xyflow/system'; } from '@xyflow/system';
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.'; import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void; export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void; export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;
-1
View File
@@ -1,6 +1,5 @@
export * from './nodes'; export * from './nodes';
export * from './edges'; export * from './edges';
export * from './changes';
export * from './component-props'; export * from './component-props';
export * from './general'; export * from './general';
export * from './store'; export * from './store';
+9 -1
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-namespace */
import type { Rect, Viewport } from '@xyflow/system'; import type { Rect, Viewport } from '@xyflow/system';
import type { Node, Edge, ViewportHelperFunctions } from '.'; import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.';
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = { export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
nodes: NodeType[]; nodes: NodeType[];
@@ -20,6 +20,7 @@ export namespace Instance {
) => void; ) => void;
export type AddNodes<NodeType extends Node = Node> = (payload: NodeType[] | NodeType) => void; export type AddNodes<NodeType extends Node = Node> = (payload: NodeType[] | NodeType) => void;
export type GetNode<NodeType extends Node = Node> = (id: string) => NodeType | undefined; export type GetNode<NodeType extends Node = Node> = (id: string) => NodeType | undefined;
export type GetInternalNode<NodeType extends Node = Node> = (id: string) => InternalNode<NodeType> | undefined;
export type GetEdges<EdgeType extends Edge = Edge> = () => EdgeType[]; export type GetEdges<EdgeType extends Edge = Edge> = () => EdgeType[];
export type SetEdges<EdgeType extends Edge = Edge> = ( export type SetEdges<EdgeType extends Edge = Edge> = (
payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[]) payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])
@@ -83,6 +84,13 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
* @returns the node or undefined if no node was found * @returns the node or undefined if no node was found
*/ */
getNode: Instance.GetNode<NodeType>; getNode: Instance.GetNode<NodeType>;
/**
* 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<NodeType>;
/** /**
* Returns edges. * Returns edges.
* *
+10 -1
View File
@@ -1,5 +1,12 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; 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'; import { NodeTypes } from './general';
@@ -17,6 +24,8 @@ export type Node<
focusable?: boolean; focusable?: boolean;
}; };
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
export type NodeMouseHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, node: NodeType) => void; export type NodeMouseHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, node: NodeType) => void;
export type SelectionDragHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, nodes: NodeType[]) => void; export type SelectionDragHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, nodes: NodeType[]) => void;
export type OnNodeDrag<NodeType extends Node = Node> = ( export type OnNodeDrag<NodeType extends Node = Node> = (
+5 -4
View File
@@ -25,12 +25,13 @@ import {
type EdgeLookup, type EdgeLookup,
type ConnectionLookup, type ConnectionLookup,
type NodeLookup, type NodeLookup,
NodeChange,
EdgeChange,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
Edge, Edge,
Node, Node,
NodeChange,
OnNodesChange, OnNodesChange,
OnEdgesChange, OnEdgesChange,
DefaultEdgeOptions, DefaultEdgeOptions,
@@ -43,7 +44,7 @@ import type {
OnNodeDrag, OnNodeDrag,
OnBeforeDelete, OnBeforeDelete,
IsValidConnection, IsValidConnection,
EdgeChange, InternalNode,
} from '.'; } from '.';
export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge = Edge> = { export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
@@ -52,7 +53,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
height: number; height: number;
transform: Transform; transform: Transform;
nodes: NodeType[]; nodes: NodeType[];
nodeLookup: NodeLookup<NodeType>; nodeLookup: NodeLookup<InternalNode<NodeType>>;
edges: Edge[]; edges: Edge[];
edgeLookup: EdgeLookup<EdgeType>; edgeLookup: EdgeLookup<EdgeType>;
connectionLookup: ConnectionLookup; connectionLookup: ConnectionLookup;
@@ -169,7 +170,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void; triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void; triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy; panBy: PanBy;
fitView: (nodes: NodeType[], options?: FitViewOptions) => boolean; fitView: (options?: FitViewOptions) => boolean;
}; };
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore< export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<
+17 -67
View File
@@ -1,51 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { EdgeLookup, NodeLookup } from '@xyflow/system'; import {
import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types'; EdgeLookup,
NodeLookup,
export function handleParentExpand(updatedElements: any[], updateItem: any) { EdgeChange,
for (const [index, item] of updatedElements.entries()) { NodeChange,
if (item.id === updateItem.parentNode) { NodeSelectionChange,
const parent = { ...item }; EdgeSelectionChange,
parent.computed ??= {}; } from '@xyflow/system';
import type { Node, Edge, InternalNode } from '../types';
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;
}
}
}
// This function applies changes to nodes or edges that are triggered by React Flow internally. // 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. // 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 }; const updatedElement = { ...element };
for (const change of changes) { for (const change of changes) {
applyChange(change, updatedElement, updatedElements); applyChange(change, updatedElement);
} }
updatedElements.push(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. // 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) { switch (change.type) {
case 'select': { case 'select': {
element.selected = change.selected; element.selected = change.selected;
@@ -125,26 +87,18 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
element.position = change.position; element.position = change.position;
} }
if (typeof change.positionAbsolute !== 'undefined') {
element.computed ??= {};
element.computed.positionAbsolute = change.positionAbsolute;
}
if (typeof change.dragging !== 'undefined') { if (typeof change.dragging !== 'undefined') {
element.dragging = change.dragging; element.dragging = change.dragging;
} }
if (element.expandParent) {
handleParentExpand(elements, element);
}
break; break;
} }
case 'dimensions': { case 'dimensions': {
if (typeof change.dimensions !== 'undefined') { if (typeof change.dimensions !== 'undefined') {
element.computed ??= {}; element.measured ??= {};
element.computed.width = change.dimensions.width; element.measured.width = change.dimensions.width;
element.computed.height = change.dimensions.height; element.measured.height = change.dimensions.height;
if (change.resizing) { if (change.resizing) {
element.width = change.dimensions.width; element.width = change.dimensions.width;
@@ -156,10 +110,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
element.resizing = change.resizing; element.resizing = change.resizing;
} }
if (element.expandParent) {
handleParentExpand(elements, element);
}
break; break;
} }
} }
@@ -228,13 +178,13 @@ export function createSelectionChange(id: string, selected: boolean): NodeSelect
} }
export function getSelectionChanges( export function getSelectionChanges(
items: any[], items: Map<string, any>,
selectedIds: Set<string> = new Set(), selectedIds: Set<string> = new Set(),
mutateItem = false mutateItem = false
): NodeSelectionChange[] | EdgeSelectionChange[] { ): NodeSelectionChange[] | EdgeSelectionChange[] {
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = []; const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
for (const item of items) { for (const [, item] of items) {
const willBeSelected = selectedIds.has(item.id); const willBeSelected = selectedIds.has(item.id);
// we don't want to set all items to selected=false on the first selection // we don't want to set all items to selected=false on the first selection
@@ -266,7 +216,7 @@ export function getElementsDiffChanges({
lookup, lookup,
}: { }: {
items: Node[] | undefined; items: Node[] | undefined;
lookup: NodeLookup<Node>; lookup: NodeLookup<InternalNode<Node>>;
}): NodeChange[]; }): NodeChange[];
export function getElementsDiffChanges({ export function getElementsDiffChanges({
items, items,
+2 -2
View File
@@ -7,7 +7,7 @@ import {
type Writable type Writable
} from 'svelte/store'; } from 'svelte/store';
import { import {
adoptUserProvidedNodes, adoptUserNodes,
updateConnectionLookup, updateConnectionLookup,
type Viewport, type Viewport,
type PanZoomInstance, type PanZoomInstance,
@@ -141,7 +141,7 @@ export const createNodesStore = (
let elevateNodesOnSelect = true; let elevateNodesOnSelect = true;
const _set = (nds: Node[]): Node[] => { const _set = (nds: Node[]): Node[] => {
const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, { const nextNodes = adoptUserNodes(nds, nodeLookup, {
elevateNodesOnSelect, elevateNodesOnSelect,
defaults defaults
}); });
-2
View File
@@ -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.`, `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 = [ export const infiniteExtent: CoordinateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
@@ -1,6 +1,4 @@
import type { XYPosition, Dimensions } from '@xyflow/system'; import type { XYPosition, Dimensions, NodeBase, EdgeBase } from '.';
import type { Node, Edge } from '.';
export type NodeDimensionChange = { export type NodeDimensionChange = {
id: string; id: string;
@@ -28,12 +26,12 @@ export type NodeRemoveChange = {
type: 'remove'; type: 'remove';
}; };
export type NodeAddChange<NodeType extends Node = Node> = { export type NodeAddChange<NodeType extends NodeBase = NodeBase> = {
item: NodeType; item: NodeType;
type: 'add'; type: 'add';
}; };
export type NodeReplaceChange<NodeType extends Node = Node> = { export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
id: string; id: string;
item: NodeType; item: NodeType;
type: 'replace'; type: 'replace';
@@ -43,7 +41,7 @@ export type NodeReplaceChange<NodeType extends Node = Node> = {
* Union type of all possible node changes. * Union type of all possible node changes.
* @public * @public
*/ */
export type NodeChange<NodeType extends Node = Node> = export type NodeChange<NodeType extends NodeBase = NodeBase> =
| NodeDimensionChange | NodeDimensionChange
| NodePositionChange | NodePositionChange
| NodeSelectionChange | NodeSelectionChange
@@ -53,18 +51,18 @@ export type NodeChange<NodeType extends Node = Node> =
export type EdgeSelectionChange = NodeSelectionChange; export type EdgeSelectionChange = NodeSelectionChange;
export type EdgeRemoveChange = NodeRemoveChange; export type EdgeRemoveChange = NodeRemoveChange;
export type EdgeAddChange<EdgeType extends Edge = Edge> = { export type EdgeAddChange<EdgeType extends EdgeBase = EdgeBase> = {
item: EdgeType; item: EdgeType;
type: 'add'; type: 'add';
}; };
export type EdgeReplaceChange<EdgeType extends Edge = Edge> = { export type EdgeReplaceChange<EdgeType extends EdgeBase = EdgeBase> = {
id: string; id: string;
item: EdgeType; item: EdgeType;
type: 'replace'; type: 'replace';
}; };
export type EdgeChange<EdgeType extends Edge = Edge> = export type EdgeChange<EdgeType extends EdgeBase = EdgeBase> =
| EdgeSelectionChange | EdgeSelectionChange
| EdgeRemoveChange | EdgeRemoveChange
| EdgeAddChange<EdgeType> | EdgeAddChange<EdgeType>
+3 -3
View File
@@ -2,7 +2,7 @@
import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3'; import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
import type { XYPosition, Rect } from './utils'; 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 type { ConnectingHandle, HandleType } from './handles';
import { PanZoomInstance } from './panzoom'; import { PanZoomInstance } from './panzoom';
import { EdgeBase } from '..'; import { EdgeBase } from '..';
@@ -52,7 +52,7 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean; export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
export type FitViewParamsBase<NodeType extends NodeBase> = { export type FitViewParamsBase<NodeType extends NodeBase> = {
nodes: NodeType[]; nodeLookup: Map<string, InternalNodeBase<NodeType>>;
width: number; width: number;
height: number; height: number;
panZoom: PanZoomInstance; panZoom: PanZoomInstance;
@@ -127,7 +127,7 @@ export type SelectionRect = Rect & {
export type OnError = (id: string, message: string) => void; 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 PanBy = (delta: XYPosition) => boolean;
export type UpdateConnection = (params: { export type UpdateConnection = (params: {
+1
View File
@@ -1,3 +1,4 @@
export * from './changes';
export * from './general'; export * from './general';
export * from './nodes'; export * from './nodes';
export * from './edges'; export * from './edges';
+22 -16
View File
@@ -1,4 +1,3 @@
import { internalsSymbol } from '../constants';
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.'; import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
import { Optional } from '../utils/types'; import { Optional } from '../utils/types';
@@ -44,7 +43,7 @@ export type NodeBase<
initialWidth?: number; initialWidth?: number;
initialHeight?: number; initialHeight?: number;
/** Parent node id, used for creating sub-flows */ /** Parent node id, used for creating sub-flows */
parentNode?: string; parentId?: string;
zIndex?: number; zIndex?: number;
/** Boundary a node can be moved in /** Boundary a node can be moved in
* @example 'parent' or [[0, 0], [100, 100]] * @example 'parent' or [[0, 0], [100, 100]]
@@ -60,21 +59,26 @@ export type NodeBase<
*/ */
origin?: NodeOrigin; origin?: NodeOrigin;
handles?: NodeHandle[]; handles?: NodeHandle[];
computed?: { measured?: {
width?: number; width?: number;
height?: number; height?: number;
positionAbsolute?: XYPosition;
}; };
};
// Only used internally export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType & {
[internalsSymbol]?: { measured: {
z?: number; 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; 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<NodeData, NodeType>;
}; };
}; };
@@ -104,7 +108,7 @@ export type NodeHandleBounds = {
export type NodeDimensionUpdate = { export type NodeDimensionUpdate = {
id: string; id: string;
nodeElement: HTMLDivElement; nodeElement: HTMLDivElement;
forceUpdate?: boolean; force?: boolean;
}; };
export type NodeBounds = XYPosition & { export type NodeBounds = XYPosition & {
@@ -117,13 +121,15 @@ export type NodeDragItem = {
position: XYPosition; position: XYPosition;
// distance from the mouse cursor to the node when start dragging // distance from the mouse cursor to the node when start dragging
distance: XYPosition; distance: XYPosition;
computed: { measured: {
width: number | null; width: number | null;
height: number | null; height: number | null;
};
internals: {
positionAbsolute: XYPosition; positionAbsolute: XYPosition;
}; };
extent?: 'parent' | CoordinateExtent; extent?: 'parent' | CoordinateExtent;
parentNode?: string; parentId?: string;
dragging?: boolean; dragging?: boolean;
origin?: NodeOrigin; origin?: NodeOrigin;
expandParent?: boolean; expandParent?: boolean;
@@ -137,4 +143,4 @@ export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
export type Align = 'center' | 'start' | 'end'; export type Align = 'center' | 'start' | 'end';
export type NodeLookup<NodeType extends NodeBase = NodeBase> = Map<string, NodeType>; export type NodeLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType>;
+6 -7
View File
@@ -1,5 +1,4 @@
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..'; import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..';
import { EdgeBase, NodeBase } from '../../types';
import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general'; import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB) // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
@@ -24,8 +23,8 @@ export function getEdgeCenter({
} }
export type GetEdgeZIndexParams = { export type GetEdgeZIndexParams = {
sourceNode: NodeBase; sourceNode: InternalNodeBase;
targetNode: NodeBase; targetNode: InternalNodeBase;
selected?: boolean; selected?: boolean;
zIndex?: number; zIndex?: number;
elevateOnSelect?: boolean; elevateOnSelect?: boolean;
@@ -43,14 +42,14 @@ export function getElevatedEdgeZIndex({
} }
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected; 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); return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
} }
type IsEdgeVisibleParams = { type IsEdgeVisibleParams = {
sourceNode: NodeBase; sourceNode: InternalNodeBase;
targetNode: NodeBase; targetNode: InternalNodeBase;
width: number; width: number;
height: number; height: number;
transform: Transform; transform: Transform;
+13 -12
View File
@@ -1,25 +1,26 @@
import { EdgePosition } from '../../types/edges'; import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general'; import { ConnectionMode, OnError } from '../../types/general';
import { NodeBase, NodeHandle } from '../../types/nodes'; import { InternalNodeBase, NodeHandle } from '../../types/nodes';
import { Position } from '../../types/utils'; import { Position } from '../../types/utils';
import { errorMessages, internalsSymbol } from '../../constants'; import { errorMessages } from '../../constants';
import { HandleElement } from '../../types'; import { HandleElement } from '../../types';
import { getNodeDimensions } from '../general'; import { getNodeDimensions } from '../general';
export type GetEdgePositionParams = { export type GetEdgePositionParams = {
id: string; id: string;
sourceNode: NodeBase; sourceNode: InternalNodeBase;
sourceHandle: string | null; sourceHandle: string | null;
targetNode: NodeBase; targetNode: InternalNodeBase;
targetHandle: string | null; targetHandle: string | null;
connectionMode: ConnectionMode; connectionMode: ConnectionMode;
onError?: OnError; onError?: OnError;
}; };
function isNodeInitialized(node: NodeBase): boolean { function isNodeInitialized(node: InternalNodeBase): boolean {
return ( return (
!!(node?.[internalsSymbol]?.handleBounds || node?.handles?.length) && node &&
!!(node?.computed?.width || node?.width || node?.initialWidth) !!(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; return null;
} }
const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles); const sourceHandleBounds = sourceNode.internals.handleBounds || toHandleBounds(sourceNode.handles);
const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles); const targetHandleBounds = targetNode.internals.handleBounds || toHandleBounds(targetNode.handles);
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle); const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
const targetHandle = getHandle( const targetHandle = getHandle(
@@ -95,9 +96,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
}; };
} }
function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] { function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] {
const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0); const x = (handle?.x ?? 0) + (node.internals.positionAbsolute?.x ?? 0);
const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0); const y = (handle?.y ?? 0) + (node.internals.positionAbsolute?.y ?? 0);
const { width, height } = handle ?? getNodeDimensions(node); const { width, height } = handle ?? getNodeDimensions(node);
switch (position) { switch (position) {
+11 -10
View File
@@ -8,6 +8,7 @@ import type {
NodeOrigin, NodeOrigin,
SnapGrid, SnapGrid,
Transform, Transform,
InternalNodeBase,
} from '../types'; } from '../types';
import { type Viewport } from '../types'; import { type Viewport } from '../types';
import { getNodePositionWithOrigin } from './graph'; import { getNodePositionWithOrigin } from './graph';
@@ -65,23 +66,23 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
height: y2 - y, 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); const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return { return {
...positionAbsolute, ...positionAbsolute,
width: node.computed?.width ?? node.width ?? 0, width: node.measured?.width ?? node.width ?? 0,
height: node.computed?.height ?? node.height ?? 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); const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return { return {
...positionAbsolute, ...positionAbsolute,
x2: positionAbsolute.x + (node.computed?.width ?? node.width ?? 0), x2: positionAbsolute.x + (node.measured?.width ?? node.width ?? 0),
y2: positionAbsolute.y + (node.computed?.height ?? node.height ?? 0), y2: positionAbsolute.y + (node.measured?.height ?? node.height ?? 0),
}; };
}; };
@@ -207,14 +208,14 @@ export function getNodeDimensions<NodeType extends NodeBase = NodeBase>(
node: NodeType node: NodeType
): { width: number; height: number } { ): { width: number; height: number } {
return { return {
width: node.computed?.width ?? node.width ?? node.initialWidth ?? 0, width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
height: node.computed?.height ?? node.height ?? node.initialHeight ?? 0, height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,
}; };
} }
export function nodeHasDimensions<NodeType extends NodeBase = NodeBase>(node: NodeType): boolean { export function nodeHasDimensions<NodeType extends NodeBase = NodeBase>(node: NodeType): boolean {
return ( return (
(node.computed?.width ?? node.width ?? node.initialWidth) !== undefined && (node.measured?.width ?? node.width ?? node.initialWidth) !== undefined &&
(node.computed?.height ?? node.height ?? node.initialHeight) !== undefined (node.measured?.height ?? node.height ?? node.initialHeight) !== undefined
); );
} }
+83 -33
View File
@@ -24,6 +24,7 @@ import {
OnError, OnError,
OnBeforeDeleteBase, OnBeforeDeleteBase,
NodeLookup, NodeLookup,
InternalNodeBase,
} from '../types'; } from '../types';
import { errorMessages } from '../constants'; import { errorMessages } from '../constants';
@@ -47,6 +48,10 @@ export const isEdgeBase = <EdgeType extends EdgeBase = EdgeBase>(element: any):
export const isNodeBase = <NodeType extends NodeBase = NodeBase>(element: any): element is NodeType => export const isNodeBase = <NodeType extends NodeBase = NodeBase>(element: any): element is NodeType =>
'id' in element && 'position' in element && !('source' in element) && !('target' in element); 'id' in element && 'position' in element && !('source' in element) && !('target' in element);
export const isInternalNodeBase = <NodeType extends InternalNodeBase = InternalNodeBase>(
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 * Pass in a node, and get connected nodes where edge.source === node.id
* @public * @public
@@ -101,7 +106,7 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
}; };
export const getNodePositionWithOrigin = ( export const getNodePositionWithOrigin = (
node: NodeBase | undefined, node: InternalNodeBase | NodeBase | undefined,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0]
): { position: XYPosition; positionAbsolute: XYPosition } => { ): { position: XYPosition; positionAbsolute: XYPosition } => {
if (!node) { if (!node) {
@@ -128,12 +133,13 @@ export const getNodePositionWithOrigin = (
return { return {
position, position,
positionAbsolute: node.computed?.positionAbsolute positionAbsolute:
? { 'internals' in node
x: node.computed.positionAbsolute.x - offsetX, ? {
y: node.computed.positionAbsolute.y - offsetY, x: node.internals.positionAbsolute.x - offsetX,
} y: node.internals.positionAbsolute.y - offsetY,
: position, }
: position,
}; };
}; };
@@ -151,6 +157,7 @@ export type GetNodesBoundsParams = {
* @param params.useRelativePosition - Whether to use the relative or absolute node positions * @param params.useRelativePosition - Whether to use the relative or absolute node positions
* @returns Bounding box enclosing all nodes * @returns Bounding box enclosing all nodes
*/ */
// @todo how to handle this if users do not have absolute positions?
export const getNodesBounds = ( export const getNodesBounds = (
nodes: NodeBase[], nodes: NodeBase[],
params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false } params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false }
@@ -176,8 +183,47 @@ export const getNodesBounds = (
return boxToRect(box); return boxToRect(box);
}; };
export const getNodesInside = <NodeType extends NodeBase>( export type GetInternalNodesBoundsParams = {
nodes: NodeType[], 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 = <NodeType extends NodeBase = NodeBase>(
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
rect: Rect, rect: Rect,
[tx, ty, tScale]: Transform = [0, 0, 1], [tx, ty, tScale]: Transform = [0, 0, 1],
partially = false, partially = false,
@@ -191,13 +237,15 @@ export const getNodesInside = <NodeType extends NodeBase>(
height: rect.height / tScale, height: rect.height / tScale,
}; };
const visibleNodes = nodes.reduce<NodeType[]>((res, node) => { const visibleNodes: NodeType[] = [];
const { computed, selectable = true, hidden = false } = node;
const width = computed?.width ?? node.width ?? node.initialWidth ?? null; for (const [, node] of nodeLookup) {
const height = computed?.height ?? node.height ?? node.initialHeight ?? null; 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) { if ((excludeNonSelectableNodes && !selectable) || hidden) {
return res; continue;
} }
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin)); const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
@@ -208,11 +256,9 @@ export const getNodesInside = <NodeType extends NodeBase>(
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area; const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
if (isVisible || node.dragging) { if (isVisible || node.dragging) {
res.push(node); visibleNodes.push(node);
} }
}
return res;
}, []);
return visibleNodes; return visibleNodes;
}; };
@@ -236,17 +282,20 @@ export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType
}; };
export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>( export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
{ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params, { nodeLookup, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
options?: Options options?: Options
) { ) {
const filteredNodes = nodes.filter((n) => { const filteredNodes: InternalNodeBase[] = [];
const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden);
if (options?.nodes?.length) { nodeLookup.forEach((n) => {
return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id); 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) { if (filteredNodes.length > 0) {
@@ -284,7 +333,7 @@ function clampNodeExtent<NodeType extends NodeBase>(
if (!extent || extent === 'parent') { if (!extent || extent === 'parent') {
return extent; 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<NodeType extends NodeBase>({
}: { }: {
nodeId: string; nodeId: string;
nextPosition: XYPosition; nextPosition: XYPosition;
nodeLookup: NodeLookup<NodeType>; nodeLookup: NodeLookup<InternalNodeBase<NodeType>>;
nodeOrigin?: NodeOrigin; nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
onError?: OnError; onError?: OnError;
}): { position: XYPosition; positionAbsolute: XYPosition } { }): { position: XYPosition; positionAbsolute: XYPosition } {
const node = nodeLookup.get(nodeId)!; 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 const { x: parentX, y: parentY } = parentNode
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
: { x: 0, y: 0 }; : { x: 0, y: 0 };
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent); let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
if (node.extent === 'parent' && !node.expandParent) { if (node.extent === 'parent' && !node.expandParent) {
if (!parentNode) { if (!parentNode) {
onError?.('005', errorMessages['error005']()); onError?.('005', errorMessages['error005']());
} else { } else {
const nodeWidth = node.computed?.width; const nodeWidth = node.measured.width;
const nodeHeight = node.computed?.height; const nodeHeight = node.measured.height;
const parentWidth = parentNode?.computed?.width; const parentWidth = parentNode.measured.width;
const parentHeight = parentNode?.computed?.height; const parentHeight = parentNode.measured.height;
if (nodeWidth && nodeHeight && parentWidth && parentHeight) { if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
const currNodeOrigin = node.origin || nodeOrigin; const currNodeOrigin = node.origin || nodeOrigin;
@@ -390,7 +440,7 @@ export async function getElementsToRemove<NodeType extends NodeBase = NodeBase,
} }
const isIncluded = nodeIds.includes(node.id); const isIncluded = nodeIds.includes(node.id);
const parentHit = !isIncluded && node.parentNode && matchingNodes.find((n) => n.id === node.parentNode); const parentHit = !isIncluded && node.parentId && matchingNodes.find((n) => n.id === node.parentId);
if (isIncluded || parentHit) { if (isIncluded || parentHit) {
matchingNodes.push(node); matchingNodes.push(node);
+169 -96
View File
@@ -1,8 +1,6 @@
import { internalsSymbol } from '../constants';
import { import {
NodeBase, NodeBase,
CoordinateExtent, CoordinateExtent,
Dimensions,
NodeDimensionUpdate, NodeDimensionUpdate,
NodeOrigin, NodeOrigin,
PanZoomInstance, PanZoomInstance,
@@ -12,54 +10,58 @@ import {
ConnectionLookup, ConnectionLookup,
EdgeBase, EdgeBase,
EdgeLookup, EdgeLookup,
InternalNodeBase,
NodeChange,
NodeLookup,
Rect,
} from '../types'; } from '../types';
import { getDimensions, getHandleBounds } from './dom'; import { getDimensions, getHandleBounds } from './dom';
import { isNumeric } from './general'; import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general';
import { getNodePositionWithOrigin } from './graph'; import { getNodePositionWithOrigin } from './graph';
type ParentNodes = Record<string, boolean>;
export function updateAbsolutePositions<NodeType extends NodeBase>( export function updateAbsolutePositions<NodeType extends NodeBase>(
nodes: NodeType[], nodeLookup: Map<string, InternalNodeBase<NodeType>>,
nodeLookup: Map<string, NodeType>, options: UpdateNodesOptions<NodeType> = {
nodeOrigin: NodeOrigin = [0, 0], nodeOrigin: [0, 0] as NodeOrigin,
parentNodes?: ParentNodes elevateNodesOnSelect: true,
defaults: {},
},
parentNodeIds?: Set<string>
) { ) {
return nodes.map((node) => { const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
if (node.parentNode && !nodeLookup.has(node.parentNode)) {
throw new Error(`Parent node ${node.parentNode} not found`); 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]) { if (parentId || node.internals.isParent || parentNodeIds?.has(id)) {
const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null; const parentNode = parentId ? nodeLookup.get(parentId) : null;
const { x, y, z } = calculateXYZPosition( const { x, y, z } = calculateXYZPosition(
node, node,
nodes,
nodeLookup, nodeLookup,
{ {
...node.position, ...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; const currPosition = node.internals.positionAbsolute;
node.computed!.positionAbsolute = positionChanged const positionChanged = x !== currPosition.x || y !== currPosition.y;
? {
x,
y,
}
: node.computed?.positionAbsolute;
node[internalsSymbol]!.z = z; node.internals.positionAbsolute = positionChanged ? { x, y } : currPosition;
node.internals.z = z;
if (parentNodes?.[node.id]) { if (parentNodeIds !== undefined) {
node[internalsSymbol]!.isParent = true; node.internals.isParent = !!parentNodeIds?.has(id);
} }
}
return node; nodeLookup.set(id, node);
}); }
}
} }
type UpdateNodesOptions<NodeType extends NodeBase> = { type UpdateNodesOptions<NodeType extends NodeBase> = {
@@ -68,128 +70,188 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
defaults?: Partial<NodeType>; defaults?: Partial<NodeType>;
}; };
export function adoptUserProvidedNodes<NodeType extends NodeBase>( export function adoptUserNodes<NodeType extends NodeBase>(
nodes: NodeType[], nodes: NodeType[],
nodeLookup: Map<string, NodeType>, nodeLookup: Map<string, InternalNodeBase<NodeType>>,
options: UpdateNodesOptions<NodeType> = { options: UpdateNodesOptions<NodeType> = {
nodeOrigin: [0, 0] as NodeOrigin, nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true, elevateNodesOnSelect: true,
defaults: {}, defaults: {},
} }
): NodeType[] { ) {
const tmpLookup = new Map(nodeLookup); const tmpLookup = new Map(nodeLookup);
nodeLookup.clear(); nodeLookup.clear();
const parentNodes: ParentNodes = {};
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
const parentNodeIds = new Set<string>();
const nextNodes = nodes.map((n) => { nodes.forEach((userNode) => {
const currentStoreNode = tmpLookup.get(n.id); const currentStoreNode = tmpLookup.get(userNode.id);
if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) {
nodeLookup.set(n.id, currentStoreNode); if (userNode.parentId) {
return currentStoreNode; parentNodeIds.add(userNode.parentId);
} }
const node: NodeType = { if (userNode === currentStoreNode?.internals.userNode) {
...options.defaults, nodeLookup.set(userNode.id, currentStoreNode);
...n, } else {
computed: { nodeLookup.set(userNode.id, {
positionAbsolute: n.position, ...options.defaults,
width: n.computed?.width, ...userNode,
height: n.computed?.height, measured: {
}, width: userNode.measured?.width,
}; height: userNode.measured?.height,
const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0); },
const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol]; internals: {
positionAbsolute: userNode.position,
if (node.parentNode) { handleBounds: currentStoreNode?.internals?.handleBounds,
parentNodes[node.parentNode] = true; 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); if (parentNodeIds.size > 0) {
updateAbsolutePositions(nodeLookup, options, parentNodeIds);
return nodesWithPositions; }
} }
function calculateXYZPosition<NodeType extends NodeBase>( function calculateXYZPosition<NodeType extends NodeBase>(
node: NodeType, node: NodeType,
nodes: NodeType[], nodeLookup: Map<string, InternalNodeBase<NodeType>>,
nodeLookup: Map<string, NodeType>,
result: XYZPosition, result: XYZPosition,
nodeOrigin: NodeOrigin nodeOrigin: NodeOrigin = [0, 0]
): XYZPosition { ): XYZPosition {
if (!node.parentNode) { if (!node.parentId) {
return result; return result;
} }
const parentNode = nodeLookup.get(node.parentNode)!; const parentNode = nodeLookup.get(node.parentId)!;
const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin); const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
return calculateXYZPosition( return calculateXYZPosition(
parentNode, parentNode,
nodes,
nodeLookup, nodeLookup,
{ {
x: (result.x ?? 0) + parentNodePosition.x, x: (result.x ?? 0) + parentNodePosition.x,
y: (result.y ?? 0) + parentNodePosition.y, 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 parentNode.origin || nodeOrigin
); );
} }
export function updateNodeDimensions<NodeType extends NodeBase>( export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLookup): NodeChange[] {
const changes: NodeChange[] = [];
const chilNodeRects = new Map<string, Rect>();
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<NodeType extends InternalNodeBase>(
updates: Map<string, NodeDimensionUpdate>, updates: Map<string, NodeDimensionUpdate>,
nodes: NodeType[],
nodeLookup: Map<string, NodeType>, nodeLookup: Map<string, NodeType>,
domNode: HTMLElement | null, domNode: HTMLElement | null,
nodeOrigin?: NodeOrigin, nodeOrigin?: NodeOrigin
onUpdate?: (id: string, dimensions: Dimensions) => void ): NodeChange[] {
): NodeType[] | null {
const viewportNode = domNode?.querySelector('.xyflow__viewport'); const viewportNode = domNode?.querySelector('.xyflow__viewport');
if (!viewportNode) { if (!viewportNode) {
return null; return [];
} }
const changes: NodeChange[] = [];
const style = window.getComputedStyle(viewportNode); const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); 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) => { updates.forEach((update) => {
const update = updates.get(node.id); 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 dimensions = getDimensions(update.nodeElement);
const doUpdate = !!( const doUpdate = !!(
dimensions.width && dimensions.width &&
dimensions.height && 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) { if (doUpdate) {
onUpdate?.(node.id, dimensions);
const newNode = { const newNode = {
...node, ...node,
computed: { measured: {
...node.computed, ...node.measured,
...dimensions, ...dimensions,
}, },
[internalsSymbol]: { internals: {
...node[internalsSymbol], ...node.internals,
handleBounds: { handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin), source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin),
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin), target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin),
@@ -199,14 +261,25 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
nodeLookup.set(node.id, newNode); 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({ export function panBy({
+15 -11
View File
@@ -26,13 +26,14 @@ import type {
OnSelectionDrag, OnSelectionDrag,
UpdateNodePositions, UpdateNodePositions,
Box, Box,
InternalNodeBase,
} from '../types'; } from '../types';
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void; export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
type StoreItems<OnNodeDrag> = { type StoreItems<OnNodeDrag> = {
nodes: NodeBase[]; nodes: NodeBase[];
nodeLookup: Map<string, NodeBase>; nodeLookup: Map<string, InternalNodeBase>;
edges: EdgeBase[]; edges: EdgeBase[];
nodeExtent: CoordinateExtent; nodeExtent: CoordinateExtent;
snapGrid: SnapGrid; snapGrid: SnapGrid;
@@ -130,19 +131,23 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
// if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node // 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. // 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[0][0], nodeExtent[0][1]],
[nodeExtent[1][0], nodeExtent[1][1]], [nodeExtent[1][0], nodeExtent[1][1]],
]; ];
if (dragItems.length > 1 && nodeExtent && !n.extent) { if (dragItems.length > 1 && nodeExtent && !n.extent) {
adjustedNodeExtent[0][0] = n.computed.positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; const { positionAbsolute } = n.internals;
adjustedNodeExtent[1][0] = const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
n.computed.positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][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]; const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
adjustedNodeExtent[1][1] = const y2 = positionAbsolute.y + (n.measured?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
adjustedNodeExtent = [
[x1, y1],
[x2, y2],
];
} }
const { position, positionAbsolute } = calculateNodePosition({ const { position, positionAbsolute } = calculateNodePosition({
@@ -158,7 +163,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y; hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y;
n.position = position; n.position = position;
n.computed.positionAbsolute = positionAbsolute; n.internals.positionAbsolute = positionAbsolute;
return n; return n;
}); });
@@ -208,7 +213,6 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
function startDrag(event: UseDragEvent) { function startDrag(event: UseDragEvent) {
const { const {
nodes,
nodeLookup, nodeLookup,
multiSelectionActive, multiSelectionActive,
nodesDraggable, nodesDraggable,
@@ -236,7 +240,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
lastPos = pointerPos; lastPos = pointerPos;
dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId); dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId);
if (dragItems.length > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) { if (dragItems.length > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) {
const [currentNode, currentNodes] = getEventHandlerParams({ const [currentNode, currentNodes] = getEventHandlerParams({
+40 -37
View File
@@ -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) { export function wrapSelectionDragFunc(selectionFunc?: (event: MouseEvent, nodes: NodeBase[]) => void) {
return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes); return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes);
} }
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodes: NodeType[]): boolean { export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodeLookup: NodeLookup): boolean {
if (!node.parentNode) { if (!node.parentId) {
return false; return false;
} }
const parentNode = nodes.find((node) => node.id === node.parentNode); const parentNode = nodeLookup.get(node.parentId);
if (!parentNode) { if (!parentNode) {
return false; return false;
@@ -19,7 +19,7 @@ export function isParentSelected<NodeType extends NodeBase>(node: NodeType, node
return true; return true;
} }
return isParentSelected(parentNode, nodes); return isParentSelected(parentNode, nodeLookup);
} }
export function hasSelector(target: Element, selector: string, domNode: Element): boolean { 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 // looks for all selected nodes and created a NodeDragItem for each of them
export function getDragItems<NodeType extends NodeBase>( export function getDragItems<NodeType extends NodeBase>(
nodes: NodeType[], nodeLookup: Map<string, InternalNodeBase<NodeType>>,
nodesDraggable: boolean, nodesDraggable: boolean,
mousePos: XYPosition, mousePos: XYPosition,
nodeId?: string nodeId?: string
): NodeDragItem[] { ): NodeDragItem[] {
return nodes const dragItems: NodeDragItem[] = [];
.filter(
(n) => for (const [id, node] of nodeLookup) {
(n.selected || n.id === nodeId) && if (
(!n.parentNode || !isParentSelected(n, nodes)) && (node.selected || node.id === nodeId) &&
(n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')) (!node.parentId || !isParentSelected(node, nodeLookup)) &&
) (node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'))
.map((n) => ({ ) {
id: n.id, const internalNode = nodeLookup.get(id)!;
position: n.position || { x: 0, y: 0 },
distance: { dragItems.push({
x: mousePos.x - (n.computed?.positionAbsolute?.x ?? 0), id: internalNode.id,
y: mousePos.y - (n.computed?.positionAbsolute?.y ?? 0), position: internalNode.position || { x: 0, y: 0 },
}, distance: {
delta: { x: mousePos.x - (internalNode.internals.positionAbsolute?.x ?? 0),
x: 0, y: mousePos.y - (internalNode.internals.positionAbsolute?.y ?? 0),
y: 0, },
}, extent: internalNode.extent,
extent: n.extent, parentId: internalNode.parentId,
parentNode: n.parentNode, origin: internalNode.origin,
origin: n.origin, expandParent: internalNode.expandParent,
expandParent: n.expandParent, internals: {
computed: { positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
positionAbsolute: n.computed?.positionAbsolute || { x: 0, y: 0 }, },
width: n.computed?.width || 0, measured: {
height: n.computed?.height || 0, width: internalNode.measured.width || 0,
}, height: internalNode.measured.height || 0,
})); },
});
}
}
return dragItems;
} }
// returns two params: // returns two params:
@@ -89,9 +93,8 @@ export function getEventHandlerParams<NodeType extends NodeBase>({
return { return {
...node, ...node,
position: n.position, position: n.position,
computed: { measured: {
...n.computed, ...n.measured,
positionAbsolute: n.computed.positionAbsolute,
}, },
}; };
}); });
+4 -4
View File
@@ -6,13 +6,13 @@ import {
type HandleType, type HandleType,
type Connection, type Connection,
type PanBy, type PanBy,
type NodeBase,
type Transform, type Transform,
type ConnectingHandle, type ConnectingHandle,
type OnConnectEnd, type OnConnectEnd,
type UpdateConnection, type UpdateConnection,
type IsValidConnection, type IsValidConnection,
type ConnectionHandle, type ConnectionHandle,
NodeLookup,
} from '../types'; } from '../types';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils'; import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils';
@@ -25,7 +25,7 @@ export type OnPointerDownParams = {
handleId: string | null; handleId: string | null;
nodeId: string; nodeId: string;
isTarget: boolean; isTarget: boolean;
nodes: NodeBase[]; nodeLookup: NodeLookup;
lib: string; lib: string;
flowId: string | null; flowId: string | null;
edgeUpdaterType?: HandleType; edgeUpdaterType?: HandleType;
@@ -79,7 +79,7 @@ function onPointerDown(
edgeUpdaterType, edgeUpdaterType,
isTarget, isTarget,
domNode, domNode,
nodes, nodeLookup,
lib, lib,
autoPanOnConnect, autoPanOnConnect,
flowId, flowId,
@@ -116,7 +116,7 @@ function onPointerDown(
let handleDomNode: Element | null = null; let handleDomNode: Element | null = null;
const handleLookup = getHandleLookup({ const handleLookup = getHandleLookup({
nodes, nodeLookup,
nodeId, nodeId,
handleId, handleId,
handleType, handleType,
+22 -20
View File
@@ -3,15 +3,15 @@ import {
type HandleType, type HandleType,
type NodeHandleBounds, type NodeHandleBounds,
type XYPosition, type XYPosition,
type NodeBase,
type ConnectionHandle, type ConnectionHandle,
InternalNodeBase,
NodeLookup,
} from '../types'; } from '../types';
import { internalsSymbol } from '../constants';
// this functions collects all handles and adds an absolute position // this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position // so that we can later find the closest handle to the mouse position
export function getHandles( export function getHandles(
node: NodeBase, node: InternalNodeBase,
handleBounds: NodeHandleBounds, handleBounds: NodeHandleBounds,
type: HandleType, type: HandleType,
currentHandle: string currentHandle: string
@@ -22,8 +22,8 @@ export function getHandles(
id: h.id || null, id: h.id || null,
type, type,
nodeId: node.id, nodeId: node.id,
x: (node.computed?.positionAbsolute?.x ?? 0) + h.x + h.width / 2, x: (node.internals.positionAbsolute.x ?? 0) + h.x + h.width / 2,
y: (node.computed?.positionAbsolute?.y ?? 0) + h.y + h.height / 2, y: (node.internals.positionAbsolute.y ?? 0) + h.y + h.height / 2,
}); });
} }
return res; return res;
@@ -62,28 +62,30 @@ export function getClosestHandle(
} }
type GetHandleLookupParams = { type GetHandleLookupParams = {
nodes: NodeBase[]; nodeLookup: NodeLookup;
nodeId: string; nodeId: string;
handleId: string | null; handleId: string | null;
handleType: string; handleType: string;
}; };
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) { export function getHandleLookup({
return nodes.reduce<ConnectionHandle[]>((res, node) => { nodeLookup,
if (node[internalsSymbol]) { nodeId,
const { handleBounds } = node[internalsSymbol]; handleId,
let sourceHandles: ConnectionHandle[] = []; handleType,
let targetHandles: ConnectionHandle[] = []; }: GetHandleLookupParams): ConnectionHandle[] {
const connectionHandles: ConnectionHandle[] = [];
if (handleBounds) { for (const [, node] of nodeLookup) {
sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`); if (node.internals.handleBounds) {
targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`); const id = `${nodeId}-${handleId}-${handleType}`;
} const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id);
const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id);
res.push(...sourceHandles, ...targetHandles); connectionHandles.push(...sourceHandles, ...targetHandles);
} }
return res; }
}, []);
return connectionHandles;
} }
export function getHandleType( export function getHandleType(
+7 -7
View File
@@ -71,15 +71,15 @@ export type XYResizerInstance = {
function nodeToParentExtent(node: NodeBase): CoordinateExtent { function nodeToParentExtent(node: NodeBase): CoordinateExtent {
return [ return [
[0, 0], [0, 0],
[node.computed!.width!, node.computed!.height!], [node.measured!.width!, node.measured!.height!],
]; ];
} }
function nodeToChildExtent(child: NodeBase, parent: NodeBase, nodeOrigin: NodeOrigin): CoordinateExtent { function nodeToChildExtent(child: NodeBase, parent: NodeBase, nodeOrigin: NodeOrigin): CoordinateExtent {
const x = parent.position.x + child.position.x; const x = parent.position.x + child.position.x;
const y = parent.position.y + child.position.y; const y = parent.position.y + child.position.y;
const width = child.computed!.width! ?? 0; const width = child.measured!.width! ?? 0;
const height = child.computed!.height! ?? 0; const height = child.measured!.height! ?? 0;
const originOffsetX = nodeOrigin[0] * width; const originOffsetX = nodeOrigin[0] * width;
const originOffsetY = nodeOrigin[1] * height; 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 }); const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
prevValues = { prevValues = {
width: node.computed?.width ?? 0, width: node.measured?.width ?? 0,
height: node.computed?.height ?? 0, height: node.measured?.height ?? 0,
x: node.position.x ?? 0, x: node.position.x ?? 0,
y: node.position.y ?? 0, y: node.position.y ?? 0,
}; };
@@ -136,7 +136,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
parentNode = undefined; parentNode = undefined;
if (node.extent === 'parent' || node.expandParent) { if (node.extent === 'parent' || node.expandParent) {
parentNode = nodeLookup.get(node.parentNode!); parentNode = nodeLookup.get(node.parentId!);
if (parentNode && node.extent === 'parent') { if (parentNode && node.extent === 'parent') {
parentExtent = nodeToParentExtent(parentNode); parentExtent = nodeToParentExtent(parentNode);
} }
@@ -148,7 +148,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
childExtent = undefined; childExtent = undefined;
for (const [childId, child] of nodeLookup) { for (const [childId, child] of nodeLookup) {
if (child.parentNode === nodeId) { if (child.parentId === nodeId) {
childNodes.push({ childNodes.push({
id: childId, id: childId,
position: { ...child.position }, position: { ...child.position },