diff --git a/examples/react/cypress/components/hooks/useNodes.cy.tsx b/examples/react/cypress/components/hooks/useNodes.cy.tsx index 9c10ad70..340b4b10 100644 --- a/examples/react/cypress/components/hooks/useNodes.cy.tsx +++ b/examples/react/cypress/components/hooks/useNodes.cy.tsx @@ -15,8 +15,7 @@ const initialNodes: Node[] = nodes.map((n) => ({ const expectedNodes: Node[] = initialNodes.map((n) => ({ ...n, - computed: { - positionAbsolute: n.position, + measured: { ...nodeDimensions, }, })); diff --git a/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx b/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx index a15fc67e..bceb4cbd 100644 --- a/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx +++ b/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx @@ -48,7 +48,7 @@ describe(': onNodesChange', () => { id: '1', item: { ...nodes[0], - computed: { positionAbsolute: nodes[0].position, width: 200, height: 100 }, + measured: { width: 200, height: 100 }, style: { width: 200, height: 100 }, }, }, diff --git a/examples/react/cypress/components/utils/apply-changes.cy.ts b/examples/react/cypress/components/utils/apply-changes.cy.ts index 1548b7e9..0a75db7b 100644 --- a/examples/react/cypress/components/utils/apply-changes.cy.ts +++ b/examples/react/cypress/components/utils/apply-changes.cy.ts @@ -136,8 +136,8 @@ describe('applyChanges Testing', () => { ]; const nextNodes = applyNodeChanges(nodeChanges, nodes); - expect(nodes[0].computed).to.be.undefined; - expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight }); + expect(nodes[0].measured).to.be.undefined; + expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight }); expect(nextNodes[0].width).to.be.undefined; expect(nextNodes[0].height).to.be.undefined; }); @@ -153,7 +153,7 @@ describe('applyChanges Testing', () => { const nextNodes = applyNodeChanges(nodeChanges, nodes); expect(nextNodes[0].position).to.be.deep.equal(newPosition); - expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight }); + expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight }); }); it('replaces nodes/edges', () => { diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 7cd3eb13..1d68c858 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -50,6 +50,7 @@ import UseNodesInitialized from '../examples/UseNodesInit'; import UseNodesData from '../examples/UseNodesData'; import UseHandleConnections from '../examples/UseHandleConnections'; import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop'; +import DevTools from '../examples/DevTools'; export interface IRoute { name: string; @@ -113,6 +114,11 @@ const routes: IRoute[] = [ path: 'default-nodes', component: DefaultNodes, }, + { + name: 'DevTools', + path: 'devtools', + component: DevTools, + }, { name: 'Drag Handle', path: 'draghandle', diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index cafa9fa8..62232a35 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -56,18 +56,19 @@ const initialEdges: Edge[] = [ const defaultEdgeOptions = {}; const BasicFlow = () => { - const { setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } = + const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } = useReactFlow(); const updatePos = () => { setNodes((nodes) => nodes.map((node) => { - node.position = { - x: Math.random() * 400, - y: Math.random() * 400, + return { + ...node, + position: { + x: Math.random() * 400, + y: Math.random() * 400, + }, }; - - return node; }) ); }; @@ -78,9 +79,10 @@ const BasicFlow = () => { const toggleClassnames = () => { setNodes((nodes) => nodes.map((node) => { - node.className = node.className === 'light' ? 'dark' : 'light'; - - return node; + return { + ...node, + className: node.className === 'light' ? 'dark' : 'light', + }; }) ); }; @@ -108,6 +110,14 @@ const BasicFlow = () => { updateNodeData('1', { label: 'update' }); updateNodeData('2', { label: 'update' }); }; + const addNode = () => { + addNodes({ + id: `${Math.random()}`, + data: { label: 'Node' }, + position: { x: Math.random() * 300, y: Math.random() * 300 }, + className: 'light', + }); + }; return ( { + ); diff --git a/examples/react/src/examples/CustomMiniMapNode/index.tsx b/examples/react/src/examples/CustomMiniMapNode/index.tsx index 7ecf1c54..21b1d073 100644 --- a/examples/react/src/examples/CustomMiniMapNode/index.tsx +++ b/examples/react/src/examples/CustomMiniMapNode/index.tsx @@ -59,7 +59,7 @@ const CustomMiniMapNodeFlow = () => { onNodeClick={onNodeClick} onConnect={(p) => onConnect(p)} onNodeDragStop={onNodeDragStop} - onlyRenderVisibleElements={false} + onlyRenderVisibleElements={true} > diff --git a/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx b/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx new file mode 100644 index 00000000..d4fffcda --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx @@ -0,0 +1,74 @@ +import { useEffect, useRef, useState } from 'react'; +import { NodeChange, OnNodesChange, useStore, useStoreApi } from '@xyflow/react'; + +type ChangeLoggerProps = { + color?: string; + limit?: number; +}; + +type ChangeInfoProps = { + change: NodeChange; +}; + +function ChangeInfo({ change }: ChangeInfoProps) { + const id = 'id' in change ? change.id : '-'; + const { type } = change; + + return ( +
+
node id: {id}
+
+ {type === 'add' ? JSON.stringify(change.item, null, 2) : null} + {type === 'dimensions' ? `${change.dimensions?.width} × ${change.dimensions?.height}` : null} + {type === 'position' ? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}` : null} + {type === 'remove' ? 'remove' : null} + {type === 'replace' ? JSON.stringify(change.item, null, 2) : null} + {type === 'select' ? (change.selected ? 'select' : 'unselect') : null} +
+
+ ); +} + +export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) { + const [changes, setChanges] = useState([]); + const onNodesChangeIntercepted = useRef(false); + const onNodesChange = useStore((s) => s.onNodesChange); + const store = useStoreApi(); + + useEffect(() => { + if (!onNodesChange || onNodesChangeIntercepted.current) { + return; + } + + onNodesChangeIntercepted.current = true; + const userOnNodesChange = onNodesChange; + + const onNodesChangeLogger: OnNodesChange = (changes) => { + userOnNodesChange(changes); + + setChanges((c) => { + changes.forEach((change) => { + if (c.length >= limit) { + c.pop(); + } + + c = [change, ...c]; + }); + return c; + }); + }; + + store.setState({ onNodesChange: onNodesChangeLogger }); + }, [onNodesChange]); + + return ( +
+
Change Logger
+ {changes.length === 0 ? ( + <>no changes triggered + ) : ( + changes.map((change, index) => ) + )} +
+ ); +} diff --git a/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx new file mode 100644 index 00000000..5d467514 --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx @@ -0,0 +1,68 @@ +import { useNodes, ViewportPortal } from '@xyflow/react'; + +type NodeInfoProps = { + id: string; + type: string; + x: number; + y: number; + width?: number; + height?: number; + data: any; +}; + +function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) { + if (!width || !height) { + return null; + } + + return ( +
+
id: {id}
+
type: {type}
+
+ position: {x.toFixed(1)}, {y.toFixed(1)} +
+
+ dimensions: {width} × {height} +
+
data: {JSON.stringify(data, null, 2)}
+
+ ); +} + +export default function NodeInspector() { + const nodes = useNodes(); + + return ( + +
+ {nodes.map((node) => { + const x = node.computed?.positionAbsolute?.x || 0; + const y = node.computed?.positionAbsolute?.y || 0; + const width = node.computed?.width || 0; + const height = node.computed?.height || 0; + + return ( + + ); + })} +
+
+ ); +} diff --git a/examples/react/src/examples/DevTools/DevTools/index.tsx b/examples/react/src/examples/DevTools/DevTools/index.tsx new file mode 100644 index 00000000..a00a6f2e --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/index.tsx @@ -0,0 +1,44 @@ +import { useState, type Dispatch, type SetStateAction, type ReactNode, HTMLAttributes } from 'react'; +import { Panel, PanelPosition } from '@xyflow/react'; + +import NodeInspector from './NodeInspector'; +import ChangeLogger from './ChangeLogger'; + +import './style.css'; + +export default function ReactFlowDevTools({ position = 'top-left' }: { position?: PanelPosition }) { + const [nodeInspectorActive, setNodeInspectorActive] = useState(false); + const [changeLoggerActive, setChangeLoggerActive] = useState(false); + + return ( +
+ + + Node Inspector + + + Change Logger + + + {changeLoggerActive && } + {nodeInspectorActive && } +
+ ); +} + +function DevToolButton({ + active, + setActive, + children, + ...rest +}: { + active: boolean; + setActive: Dispatch>; + children: ReactNode; +} & HTMLAttributes) { + return ( + + ); +} diff --git a/examples/react/src/examples/DevTools/DevTools/style.css b/examples/react/src/examples/DevTools/DevTools/style.css new file mode 100644 index 00000000..39c65969 --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/style.css @@ -0,0 +1,63 @@ +.react-flow__devtools { + --border-radius: 4px; + --highlight-color: rgba(238, 58, 115, 1); + --font: monospace, sans-serif; + + border-radius: var(--border-radius); + font-size: 11px; + font-family: var(--font); +} + +.react-flow__devtools button { + background: white; + border: none; + padding: 5px 15px; + color: #222; + font-weight: bold; + font-size: 12px; + cursor: pointer; + font-family: var(--font); + background-color: #f4f4f4; +} + +.react-flow__devtools button:hover { + background: var(--highlight-color); + color: white; +} + +.react-flow__devtools button.active { + background: var(--highlight-color); + color: white; +} + +.react-flow__devtools button:first-child { + border-radius: var(--border-radius) 0 0 var(--border-radius); + border-right: 1px solid #ddd; +} + +.react-flow__devtools button:last-child { + border-radius: 0 var(--border-radius) var(--border-radius) 0; +} + +.react-flow__devtools-changelogger { + pointer-events: none; + position: relative; + top: 50px; + left: 20px; + font-family: var(--font); +} + +.react-flow__devtools-title { + font-weight: bold; + margin-bottom: 5px; +} + +.react-flow__devtools-nodeinspector { + pointer-events: none; + font-family: monospace, sans-serif; + font-size: 10px; +} + +.react-flow__devtools-nodeinfo { + top: 5px; +} diff --git a/examples/react/src/examples/DevTools/index.tsx b/examples/react/src/examples/DevTools/index.tsx new file mode 100644 index 00000000..ff75ec9d --- /dev/null +++ b/examples/react/src/examples/DevTools/index.tsx @@ -0,0 +1,55 @@ +import { useCallback } from 'react'; +import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react'; + +import DevTools from './DevTools'; + +const initNodes: Node[] = [ + { + id: '1a', + type: 'input', + data: { label: 'Node 1' }, + position: { x: 250, y: 5 }, + }, + { + id: '2a', + data: { label: 'Node 2' }, + position: { x: 100, y: 100 }, + }, + { + id: '3a', + data: { label: 'Node 3' }, + position: { x: 400, y: 100 }, + }, + { + id: '4a', + data: { label: 'Node 4' }, + position: { x: 400, y: 200 }, + }, +]; + +const initEdges: Edge[] = [ + { id: 'e1-2', source: '1a', target: '2a' }, + { id: 'e1-3', source: '1a', target: '3a' }, +]; + +const BasicFlow = () => { + const [nodes, , onNodesChange] = useNodesState(initNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges); + + const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + + return ( + + + + ); +}; + +export default BasicFlow; diff --git a/examples/react/src/examples/EasyConnect/FloatingEdge.tsx b/examples/react/src/examples/EasyConnect/FloatingEdge.tsx index 7709cb42..48d621e7 100644 --- a/examples/react/src/examples/EasyConnect/FloatingEdge.tsx +++ b/examples/react/src/examples/EasyConnect/FloatingEdge.tsx @@ -1,11 +1,11 @@ import { useCallback } from 'react'; -import { useStore, getStraightPath, EdgeProps } from '@xyflow/react'; +import { useStore, getStraightPath, EdgeProps, useInternalNode } from '@xyflow/react'; import { getEdgeParams } from './utils.js'; function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) { - const sourceNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === source), [source])); - const targetNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === target), [target])); + const sourceNode = useInternalNode(source); + const targetNode = useInternalNode(target); if (!sourceNode || !targetNode) { return null; diff --git a/examples/react/src/examples/EasyConnect/utils.tsx b/examples/react/src/examples/EasyConnect/utils.tsx index 6d87795b..f9667f23 100644 --- a/examples/react/src/examples/EasyConnect/utils.tsx +++ b/examples/react/src/examples/EasyConnect/utils.tsx @@ -1,22 +1,19 @@ -import { Node, Position, MarkerType, XYPosition } from '@xyflow/react'; +import { Node, Position, MarkerType, XYPosition, InternalNode } from '@xyflow/react'; // this helper function returns the intersection point // of the line between the center of the intersectionNode and the target node -function getNodeIntersection(intersectionNode: Node, targetNode: Node) { +function getNodeIntersection(intersectionNode: InternalNode, targetNode: InternalNode) { // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a - const { - width: intersectionNodeWidth, - height: intersectionNodeHeight, - positionAbsolute: intersectionNodePosition, - } = intersectionNode.computed || {}; - const targetPosition = targetNode.computed?.positionAbsolute!; + const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured; + const intersectionNodePosition = intersectionNode.internals.positionAbsolute; + const targetPosition = targetNode.internals.positionAbsolute!; const w = intersectionNodeWidth! / 2; const h = intersectionNodeHeight! / 2; - const x2 = intersectionNodePosition!.x + w; - const y2 = intersectionNodePosition!.y + h; + const x2 = intersectionNodePosition.x + w; + const y2 = intersectionNodePosition.y + h; const x1 = targetPosition.x + w; const y1 = targetPosition.y + h; @@ -32,8 +29,8 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) { } // returns the position (top,right,bottom or right) passed node compared to the intersection point -function getEdgePosition(node: Node, intersectionPoint: XYPosition) { - const n = { ...node.computed?.positionAbsolute, ...node }; +function getEdgePosition(node: InternalNode, intersectionPoint: XYPosition) { + const n = { ...node.internals.positionAbsolute, ...node }; const nx = Math.round(n.x!); const ny = Math.round(n.y!); const px = Math.round(intersectionPoint.x); @@ -42,13 +39,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) { if (px <= nx + 1) { return Position.Left; } - if (px >= nx + n.computed?.width! - 1) { + if (px >= nx + n.measured?.width! - 1) { return Position.Right; } if (py <= ny + 1) { return Position.Top; } - if (py >= n.y! + n.computed?.height! - 1) { + if (py >= n.y! + n.measured?.height! - 1) { return Position.Bottom; } @@ -56,7 +53,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) { } // returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge -export function getEdgeParams(source: Node, target: Node) { +export function getEdgeParams(source: InternalNode, target: InternalNode) { const sourceIntersectionPoint = getNodeIntersection(source, target); const targetIntersectionPoint = getNodeIntersection(target, source); diff --git a/examples/react/src/examples/FloatingEdges/FloatingEdge.tsx b/examples/react/src/examples/FloatingEdges/FloatingEdge.tsx index 6ee9e1e2..0856d012 100644 --- a/examples/react/src/examples/FloatingEdges/FloatingEdge.tsx +++ b/examples/react/src/examples/FloatingEdges/FloatingEdge.tsx @@ -5,8 +5,8 @@ import { getEdgeParams } from './utils'; const FloatingEdge: FC = ({ id, source, target, style }) => { const { sourceNode, targetNode } = useStore((s) => { - const sourceNode = s.nodes.find((n) => n.id === source); - const targetNode = s.nodes.find((n) => n.id === target); + const sourceNode = s.nodeLookup.get(source); + const targetNode = s.nodeLookup.get(target); return { sourceNode, targetNode }; }); diff --git a/examples/react/src/examples/FloatingEdges/utils.ts b/examples/react/src/examples/FloatingEdges/utils.ts index c8a6e1c1..08593c5c 100644 --- a/examples/react/src/examples/FloatingEdges/utils.ts +++ b/examples/react/src/examples/FloatingEdges/utils.ts @@ -1,4 +1,4 @@ -import { Position, XYPosition, Node, Edge } from '@xyflow/react'; +import { Position, XYPosition, Node, Edge, InternalNode } from '@xyflow/react'; // this helper function returns the intersection point // of the line between the center of the intersectionNode and the target node @@ -6,7 +6,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPositi // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a const { position: intersectionNodePosition } = intersectionNode; - const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.computed ?? { + const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured ?? { width: 0, height: 0, }; @@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) { if (px <= nx + 1) { return Position.Left; } - if (px >= nx + (n.computed?.width ?? 0) - 1) { + if (px >= nx + (n.measured?.width ?? 0) - 1) { return Position.Right; } if (py <= ny + 1) { return Position.Top; } - if (py >= n.y + (n.computed?.height ?? 0) - 1) { + if (py >= n.y + (n.measured?.height ?? 0) - 1) { return Position.Bottom; } @@ -56,7 +56,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) { } // returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge -export function getEdgeParams(source: Node, target: Node) { +export function getEdgeParams(source: InternalNode, target: InternalNode) { const sourceIntersectionPoint = getNodeIntersection(source, target); const targetIntersectionPoint = getNodeIntersection(target, source); diff --git a/examples/react/src/examples/Hidden/index.tsx b/examples/react/src/examples/Hidden/index.tsx index f00d8bae..97bb6fcb 100644 --- a/examples/react/src/examples/Hidden/index.tsx +++ b/examples/react/src/examples/Hidden/index.tsx @@ -11,6 +11,7 @@ import { MiniMap, Controls, } from '@xyflow/react'; +import ReactFlowDevTools from '../DevTools/DevTools'; const initialNodes: Node[] = [ { @@ -82,6 +83,7 @@ const HiddenFlow = () => { > +
diff --git a/examples/react/src/examples/Layouting/index.tsx b/examples/react/src/examples/Layouting/index.tsx index 7383e41e..2371b503 100644 --- a/examples/react/src/examples/Layouting/index.tsx +++ b/examples/react/src/examples/Layouting/index.tsx @@ -19,6 +19,7 @@ import { import initialItems from './initial-elements'; import styles from './layouting.module.css'; +import ReactFlowDevTools from '../DevTools/DevTools'; const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); @@ -98,6 +99,7 @@ const LayoutFlow = () => { onEdgesChange={onEdgesChange} > + diff --git a/examples/react/src/examples/NodeResizer/index.tsx b/examples/react/src/examples/NodeResizer/index.tsx index b0a272f2..7afb411f 100644 --- a/examples/react/src/examples/NodeResizer/index.tsx +++ b/examples/react/src/examples/NodeResizer/index.tsx @@ -137,7 +137,7 @@ const initialNodes: Node[] = [ label: 'Child with extent: parent', }, position: { x: 50, y: 50 }, - parentNode: '5', + parentId: '5', extent: 'parent', width: 50, height: 100, @@ -148,7 +148,7 @@ const initialNodes: Node[] = [ type: 'defaultResizer', data: { label: 'Child with expandParent' }, position: { x: 150, y: 100 }, - parentNode: '5', + parentId: '5', expandParent: true, style: { ...nodeStyle }, }, @@ -157,7 +157,7 @@ const initialNodes: Node[] = [ type: 'defaultResizer', data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true }, position: { x: 25, y: 200 }, - parentNode: '5', + parentId: '5', expandParent: true, style: { ...nodeStyle }, }, diff --git a/examples/react/src/examples/Subflow/index.tsx b/examples/react/src/examples/Subflow/index.tsx index 8ea28e34..7f1b3f4b 100644 --- a/examples/react/src/examples/Subflow/index.tsx +++ b/examples/react/src/examples/Subflow/index.tsx @@ -50,7 +50,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4a' }, position: { x: 15, y: 15 }, className: 'light', - parentNode: '4', + parentId: '4', origin: [0.5, 0.5], extent: [ @@ -68,21 +68,21 @@ const initialNodes: Node[] = [ height: 200, width: 300, }, - parentNode: '4', + parentId: '4', }, { id: '4b1', data: { label: 'Node 4b1' }, position: { x: 40, y: 20 }, className: 'light', - parentNode: '4b', + parentId: '4b', }, { id: '4b2', data: { label: 'Node 4b2' }, position: { x: 20, y: 100 }, className: 'light', - parentNode: '4b', + parentId: '4b', }, { id: '5', @@ -98,7 +98,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 5a' }, position: { x: 0, y: 0 }, className: 'light', - parentNode: '5', + parentId: '5', extent: 'parent', }, { @@ -106,7 +106,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 5b' }, position: { x: 225, y: 50 }, className: 'light', - parentNode: '5', + parentId: '5', expandParent: true, }, { @@ -160,7 +160,7 @@ const Subflow = () => { const updatePos = () => { setNodes((nds) => { return nds.map((n) => { - if (!n.parentNode) { + if (!n.parentId) { return { ...n, position: { @@ -194,7 +194,7 @@ const Subflow = () => { return nds.map((n) => { return { ...n, - hidden: !!n.parentNode && !n.hidden, + hidden: !!n.parentId && !n.hidden, }; }); }); diff --git a/examples/svelte/src/routes/examples/drag-n-drop/Flow.svelte b/examples/svelte/src/routes/examples/drag-n-drop/Flow.svelte index 2826a8ea..b72a4b17 100644 --- a/examples/svelte/src/routes/examples/drag-n-drop/Flow.svelte +++ b/examples/svelte/src/routes/examples/drag-n-drop/Flow.svelte @@ -13,7 +13,7 @@ import '@xyflow/svelte/dist/style.css'; - const nodes = writable([ + const nodes = writable([ { id: '1', type: 'input', @@ -55,8 +55,6 @@ const onDragOver = (event: DragEvent) => { event.preventDefault(); - console.log(event); - if (event.dataTransfer) { event.dataTransfer.dropEffect = 'move'; } @@ -81,7 +79,8 @@ data: { label: `${type} node` } }; - nodes.update((nds) => nds.concat(newNode)); + $nodes.push(newNode); + $nodes = $nodes; }; $: { diff --git a/examples/svelte/src/routes/examples/intersections/Flow.svelte b/examples/svelte/src/routes/examples/intersections/Flow.svelte index f3f80335..12c98e00 100644 --- a/examples/svelte/src/routes/examples/intersections/Flow.svelte +++ b/examples/svelte/src/routes/examples/intersections/Flow.svelte @@ -12,8 +12,8 @@ const { getIntersectingNodes } = useSvelteFlow(); - function onNodeDrag({ detail: { node } }) { - const intersections = getIntersectingNodes(node).map((n) => n.id); + function onNodeDrag({ detail: { targetNode } }) { + const intersections = getIntersectingNodes(targetNode).map((n) => n.id); $nodes.forEach((n) => { n.class = intersections.includes(n.id) ? 'highlight' : ''; diff --git a/examples/svelte/src/routes/examples/intersections/nodes-and-edges.ts b/examples/svelte/src/routes/examples/intersections/nodes-and-edges.ts index fffb59b8..7b094ce2 100644 --- a/examples/svelte/src/routes/examples/intersections/nodes-and-edges.ts +++ b/examples/svelte/src/routes/examples/intersections/nodes-and-edges.ts @@ -1,28 +1,28 @@ import type { Node, Edge } from '@xyflow/svelte'; export const initialNodes: Node[] = [ - { - id: '1', - data: { label: 'Node 1' }, - position: { x: 0, y: 0 }, - style: 'width: 200px; height: 100px;' - }, - { - id: '2', - data: { label: 'Node 2' }, - position: { x: 0, y: 150 } - }, - { - id: '3', - data: { label: 'Node 3' }, - position: { x: 250, y: 0 } - }, - { - id: '4', - data: { label: 'Node' }, - position: { x: 350, y: 150 }, - style: 'width: 50px; height: 50px;' - } + { + id: '1', + data: { label: 'Node 1' }, + position: { x: 0, y: 0 }, + style: 'width: 200px; height: 100px;' + }, + { + id: '2', + data: { label: 'Node 2' }, + position: { x: 0, y: 150 } + }, + { + id: '3', + data: { label: 'Node 3' }, + position: { x: 250, y: 0 } + }, + { + id: '4', + data: { label: 'Node' }, + position: { x: 350, y: 150 }, + style: 'width: 50px; height: 50px;' + } ]; export const initialEdges: Edge[] = []; diff --git a/examples/svelte/src/routes/examples/node-resizer/+page.svelte b/examples/svelte/src/routes/examples/node-resizer/+page.svelte index b7479b03..8e08174a 100644 --- a/examples/svelte/src/routes/examples/node-resizer/+page.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/+page.svelte @@ -113,7 +113,7 @@ type: 'defaultResizer', data: { label: 'Child with extent parent' }, position: { x: 50, y: 50 }, - parentNode: '5', + parentId: '5', extent: 'parent', style: nodeStyle }, @@ -122,7 +122,7 @@ type: 'defaultResizer', data: { label: 'Child' }, position: { x: 100, y: 100 }, - parentNode: '5', + parentId: '5', style: nodeStyle } ]); diff --git a/examples/svelte/src/routes/examples/subflows/+page.svelte b/examples/svelte/src/routes/examples/subflows/+page.svelte index 4c57c171..a4574a6e 100644 --- a/examples/svelte/src/routes/examples/subflows/+page.svelte +++ b/examples/svelte/src/routes/examples/subflows/+page.svelte @@ -36,7 +36,7 @@ id: '4a', data: { label: 'Node 4a' }, position: { x: 15, y: 15 }, - parentNode: '4', + parentId: '4', extent: [ [0, 0], [100, 100] @@ -47,19 +47,19 @@ data: { label: 'Node 4b' }, position: { x: 100, y: 60 }, style: 'width: 300px; height: 200px;', - parentNode: '4' + parentId: '4' }, { id: '4b1', data: { label: 'Node 4b1' }, position: { x: 40, y: 20 }, - parentNode: '4b' + parentId: '4b' }, { id: '4b2', data: { label: 'Node 4b2' }, position: { x: 20, y: 100 }, - parentNode: '4b' + parentId: '4b' }, { id: '5', @@ -73,14 +73,14 @@ id: '5a', data: { label: 'Node 5a' }, position: { x: 0, y: 0 }, - parentNode: '5', + parentId: '5', extent: 'parent' }, { id: '5b', data: { label: 'Node 5b' }, position: { x: 225, y: 50 }, - parentNode: '5', + parentId: '5', expandParent: true }, { diff --git a/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte index d5955501..436d2109 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte @@ -11,6 +11,7 @@ type $$Props = NodeProps; export let id: $$Props['id']; + $$restProps; const connections = useHandleConnections({ nodeId: id, diff --git a/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte index c1642da2..1fe5ff70 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/TextNode.svelte @@ -7,6 +7,7 @@ export let data: $$Props['data']; const { updateNodeData } = useSvelteFlow(); + $$restProps;
diff --git a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte index be15ac9a..55960cb5 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte @@ -12,6 +12,8 @@ type $$Props = NodeProps; export let id: $$Props['id']; + export let data: $$Props['data']; + $$restProps; const { updateNodeData } = useSvelteFlow(); const connections = useHandleConnections({ @@ -22,8 +24,12 @@ $: nodeData = useNodesData($connections[0]?.source); $: textNode = isTextNode($nodeData) ? $nodeData : null; + $: console.log(textNode?.data, data); + $: { - updateNodeData(id, { text: textNode?.data.text.toUpperCase() || '' }); + const input = textNode?.data.text.toUpperCase() ?? ''; + updateNodeData(id, { text: input }); + console.log('updatedNodeData with', input); } diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 1124d817..b210cd0c 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,33 @@ # @xyflow/react +## 12.0.0-next.14 + +## Patch changes + +- fix hidden nodes +- use `direction=ltr` for outer wrapper to support rtl sites +- allow pinch zoom even if `preventScrolling=false` +- export node and edge change related types +- only trigger dimensions updates when changes detected + +## 12.0.0-next.13 + +## ⚠️ Breaking changes + +- rename `node.parentNode` to `node.parentId` +- rename node.computed to node.measured +- remove positionAbsolute from `node.computed` + +## Minor Changes + +- new helpers: `useInternalNode` hook, `getInternalNode` function + +## Patch changes + +- remove `internalsSymbol` (now called internals and only available for internal nodes) +- handle parentExpand on library side instead of applyChanges +- new type `InternalNode` + ## 12.0.0-next.12 ## Patch changes diff --git a/packages/react/package.json b/packages/react/package.json index 3a47cbb2..b9beea2f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/react", - "version": "12.0.0-next.12", + "version": "12.0.0-next.14", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "keywords": [ "react", diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index 17a3f7f2..0a05890d 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -3,7 +3,7 @@ import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; -import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system'; +import { getInternalNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; import { Panel } from '../../components/Panel'; @@ -26,7 +26,9 @@ const selector = (s: ReactFlowState) => { return { viewBB, boundingRect: - s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, { nodeOrigin: s.nodeOrigin }), viewBB) : viewBB, + s.nodeLookup.size > 0 + ? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { nodeOrigin: s.nodeOrigin }), viewBB) + : viewBB, rfId: s.rfId, nodeOrigin: s.nodeOrigin, panZoom: s.panZoom, diff --git a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx index 0abe3177..f2e7944c 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx @@ -6,7 +6,7 @@ import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; import { MiniMapNode } from './MiniMapNode'; -import type { ReactFlowState, Node } from '../../types'; +import type { ReactFlowState, Node, InternalNode } from '../../types'; import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types'; declare const window: any; @@ -85,7 +85,7 @@ function NodeComponentWrapperInner({ shapeRendering: string; }) { const { node, x, y } = useStore((s) => { - const node = s.nodeLookup.get(id) as NodeType; + const node = s.nodeLookup.get(id) as InternalNode; const { x, y } = getNodePositionWithOrigin(node, node?.origin || nodeOrigin).positionAbsolute; return { diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index 1a1107b1..2052c98b 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -5,12 +5,14 @@ import { ResizeControlVariant, type XYResizerInstance, type XYResizerChange, - XYResizerChildChange, + type XYResizerChildChange, + type NodeChange, + type NodeDimensionChange, + type NodePositionChange, } from '@xyflow/system'; import { useStoreApi } from '../../hooks/useStore'; import { useNodeId } from '../../contexts/NodeIdContext'; -import type { NodeChange, NodeDimensionChange, NodePositionChange } from '../../types'; import type { ResizeControlProps, ResizeControlLineProps } from './types'; function ResizeControl({ diff --git a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx index 0ecc8185..f14f822e 100644 --- a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx +++ b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx @@ -1,23 +1,23 @@ import { useCallback, CSSProperties } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; -import { getNodesBounds, Rect, Position, internalsSymbol, getNodeToolbarTransform } from '@xyflow/system'; +import { Rect, Position, getNodeToolbarTransform, getNodesBounds } from '@xyflow/system'; -import { Node, ReactFlowState } from '../../types'; +import { InternalNode, ReactFlowState } from '../../types'; import { useStore } from '../../hooks/useStore'; import { useNodeId } from '../../contexts/NodeIdContext'; import { NodeToolbarPortal } from './NodeToolbarPortal'; import type { NodeToolbarProps } from './types'; -const nodeEqualityFn = (a?: Node, b?: Node) => - a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x || - a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y || - a?.computed?.width !== b?.computed?.width || - a?.computed?.height !== b?.computed?.height || +const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) => + a?.internals.positionAbsolute.x !== b?.internals.positionAbsolute.x || + a?.internals.positionAbsolute.y !== b?.internals.positionAbsolute.y || + a?.measured.width !== b?.measured.width || + a?.measured.height !== b?.measured.height || a?.selected !== b?.selected || - a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z; + a?.internals.z !== b?.internals.z; -const nodesEqualityFn = (a: Node[], b: Node[]) => { +const nodesEqualityFn = (a: InternalNode[], b: InternalNode[]) => { if (a.length !== b.length) { return false; } @@ -49,10 +49,10 @@ export function NodeToolbar({ const contextNodeId = useNodeId(); const nodesSelector = useCallback( - (state: ReactFlowState): Node[] => { + (state: ReactFlowState): InternalNode[] => { const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || '']; - return nodeIds.reduce((acc, id) => { + return nodeIds.reduce((acc, id) => { const node = state.nodeLookup.get(id); if (node) { acc.push(node); @@ -74,7 +74,7 @@ export function NodeToolbar({ } const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin }); - const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1)); + const zIndex: number = Math.max(...nodes.map((node) => node.internals.z + 1)); const wrapperStyle: CSSProperties = { position: 'absolute', diff --git a/packages/react/src/components/ConnectionLine/index.tsx b/packages/react/src/components/ConnectionLine/index.tsx index 4fe2ca76..4ca5c106 100644 --- a/packages/react/src/components/ConnectionLine/index.tsx +++ b/packages/react/src/components/ConnectionLine/index.tsx @@ -2,7 +2,6 @@ import { CSSProperties, useCallback } from 'react'; import { shallow } from 'zustand/shallow'; import cc from 'classcat'; import { - internalsSymbol, Position, ConnectionLineType, ConnectionMode, @@ -53,7 +52,7 @@ const ConnectionLine = ({ ), shallow ); - const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; + const fromHandleBounds = fromNode?.internals.handleBounds; let handleBounds = fromHandleBounds?.[handleType]; if (connectionMode === ConnectionMode.Loose) { @@ -65,10 +64,10 @@ const ConnectionLine = ({ } const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; - const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed?.width ?? 0) / 2; - const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0; - const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX; - const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY; + const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2; + const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0; + const fromX = fromNode.internals.positionAbsolute.x + fromHandleX; + const fromY = fromNode.internals.positionAbsolute.y + fromHandleY; const fromPosition = fromHandle?.position; const toPosition = fromPosition ? oppositePosition[fromPosition] : null; diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index 6061d8a7..e4810f11 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -54,7 +54,7 @@ export function EdgeUpdateAnchors({ onConnectStart, onConnectEnd, cancelConnection, - nodes, + nodeLookup, rfId: flowId, panBy, updateConnection, @@ -82,7 +82,7 @@ export function EdgeUpdateAnchors({ domNode, handleId, nodeId, - nodes, + nodeLookup, isTarget, edgeUpdaterType: handleType, lib, diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index ee36d066..977a5c5b 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -126,7 +126,7 @@ function HandleComponent( connectionMode: currentStore.connectionMode, connectionRadius: currentStore.connectionRadius, domNode: currentStore.domNode, - nodes: currentStore.nodes, + nodeLookup: currentStore.nodeLookup, lib: currentStore.lib, isTarget, handleId, diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 8c207cd9..18c2d314 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -7,7 +7,6 @@ import { errorMessages, getNodeDimensions, getPositionWithOrigin, - internalsSymbol, isInputDOMNode, nodeHasDimensions, } from '@xyflow/system'; @@ -19,7 +18,7 @@ import { useDrag } from '../../hooks/useDrag'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { handleNodeClick } from '../Nodes/utils'; import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils'; -import type { Node, NodeWrapperProps } from '../../types'; +import type { InternalNode, Node, NodeWrapperProps } from '../../types'; export function NodeWrapper({ id, @@ -44,11 +43,11 @@ export function NodeWrapper({ onError, }: NodeWrapperProps) { const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => { - const node = s.nodeLookup.get(id)! as NodeType; + const node = s.nodeLookup.get(id)! as InternalNode; const positionAbsolute = nodeExtent - ? clampPosition(node.computed?.positionAbsolute, nodeExtent) - : node.computed?.positionAbsolute || { x: 0, y: 0 }; + ? clampPosition(node.internals.positionAbsolute, nodeExtent) + : node.internals.positionAbsolute || { x: 0, y: 0 }; return { node, @@ -56,8 +55,8 @@ export function NodeWrapper({ // so we we need to force a re-render when some change positionAbsoluteX: positionAbsolute.x, positionAbsoluteY: positionAbsolute.y, - zIndex: node[internalsSymbol]?.z ?? 0, - isParent: !!node[internalsSymbol]?.isParent, + zIndex: node.internals.z, + isParent: node.internals.isParent, }; }, shallow); @@ -84,14 +83,16 @@ export function NodeWrapper({ const nodeDimensions = getNodeDimensions(node); const inlineDimensions = getNodeInlineStyleDimensions(node); const initialized = nodeHasDimensions(node); - const hasHandleBounds = !!node[internalsSymbol]?.handleBounds; + const hasHandleBounds = !!node.internals.handleBounds; const moveSelectedNodes = useMoveSelectedNodes(); useEffect(() => { + const currNode = nodeRef.current; + return () => { - if (nodeRef.current) { - resizeObserver?.unobserve(nodeRef.current); + if (currNode) { + resizeObserver?.unobserve(currNode); } }; }, []); @@ -99,7 +100,6 @@ export function NodeWrapper({ useEffect(() => { if (nodeRef.current && !node.hidden) { const currNode = nodeRef.current; - if (!initialized || !hasHandleBounds) { resizeObserver?.unobserve(currNode); resizeObserver?.observe(currNode); @@ -123,7 +123,7 @@ export function NodeWrapper({ if (targetPosChanged) { prevTargetPosition.current = node.targetPosition; } - store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]])); + store.getState().updateNodeInternals(new Map([[id, { id, nodeElement: nodeRef.current, force: true }]])); } }, [id, nodeType, node.sourcePosition, node.targetPosition]); diff --git a/packages/react/src/components/NodeWrapper/utils.tsx b/packages/react/src/components/NodeWrapper/utils.tsx index d6f7cc1a..c8c7d5c7 100644 --- a/packages/react/src/components/NodeWrapper/utils.tsx +++ b/packages/react/src/components/NodeWrapper/utils.tsx @@ -4,7 +4,7 @@ import { InputNode } from '../Nodes/InputNode'; import { DefaultNode } from '../Nodes/DefaultNode'; import { GroupNode } from '../Nodes/GroupNode'; import { OutputNode } from '../Nodes/OutputNode'; -import type { Node, NodeTypes } from '../../types'; +import type { InternalNode, Node, NodeTypes } from '../../types'; export const arrowKeyDiffs: Record = { ArrowUp: { x: 0, y: -1 }, @@ -21,12 +21,12 @@ export const builtinNodeTypes: NodeTypes = { }; export function getNodeInlineStyleDimensions( - node: NodeType + node: InternalNode ): { width: number | string | undefined; height: number | string | undefined; } { - if (!node.computed) { + if (node.internals.handleBounds === undefined) { return { width: node.width ?? node.initialWidth ?? node.style?.width, height: node.height ?? node.initialHeight ?? node.style?.height, diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx index 6c037ba5..e87f23af 100644 --- a/packages/react/src/components/NodesSelection/index.tsx +++ b/packages/react/src/components/NodesSelection/index.tsx @@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore'; import { useDrag } from '../../hooks/useDrag'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { arrowKeyDiffs } from '../NodeWrapper/utils'; -import type { Node, ReactFlowState } from '../../types'; +import type { InternalNode, Node, ReactFlowState } from '../../types'; export type NodesSelectionProps = { onSelectionContextMenu?: (event: MouseEvent, nodes: NodeType[]) => void; @@ -20,7 +20,13 @@ export type NodesSelectionProps = { }; const selector = (s: ReactFlowState) => { - const selectedNodes = s.nodes.filter((n) => n.selected); + const selectedNodes: InternalNode[] = []; + for (const [, node] of s.nodeLookup) { + if (node.selected) { + selectedNodes.push(node); + } + } + const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin }); return { diff --git a/packages/react/src/components/SelectionListener/index.tsx b/packages/react/src/components/SelectionListener/index.tsx index 0a3fa5a9..381a60cb 100644 --- a/packages/react/src/components/SelectionListener/index.tsx +++ b/packages/react/src/components/SelectionListener/index.tsx @@ -15,7 +15,7 @@ type SelectionListenerProps = { }; const selector = (s: ReactFlowState) => ({ - selectedNodes: s.nodes.filter((n) => n.selected), + selectedNodes: Array.from(s.nodeLookup.values()).filter((n) => n.selected), selectedEdges: s.edges.filter((e) => e.selected), }); diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 6d92e8b4..624d10a5 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -138,7 +138,6 @@ export function StoreUpdater s.updateNodeDimensions; +const selector = (s: ReactFlowState) => s.updateNodeInternals; export function useResizeObserver() { - const updateNodeDimensions = useStore(selector); + const updateNodeInternals = useStore(selector); const resizeObserverRef = useRef(); const resizeObserver = useMemo(() => { @@ -15,18 +16,17 @@ export function useResizeObserver() { } const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => { - const updates = new Map(); + const updates = new Map(); entries.forEach((entry: ResizeObserverEntry) => { const id = entry.target.getAttribute('data-id') as string; updates.set(id, { id, nodeElement: entry.target as HTMLDivElement, - forceUpdate: true, }); }); - updateNodeDimensions(updates); + updateNodeInternals(updates); }); resizeObserverRef.current = observer; diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index dfb240d0..0c0eb122 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -5,13 +5,13 @@ import { useRef, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react'; import { shallow } from 'zustand/shallow'; import cc from 'classcat'; -import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system'; +import { getNodesInside, getEventPosition, SelectionMode, type NodeChange, type EdgeChange } from '@xyflow/system'; import { UserSelection } from '../../components/UserSelection'; import { containerStyle } from '../../styles/utils'; import { useStore, useStoreApi } from '../../hooks/useStore'; import { getSelectionChanges } from '../../utils'; -import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types'; +import type { ReactFlowProps, ReactFlowState } from '../../types'; type PaneProps = { isSelecting: boolean; @@ -128,7 +128,7 @@ export function Pane({ }; const onMouseMove = (event: ReactMouseEvent): void => { - const { userSelectionRect, edges, transform, nodeOrigin, nodes, triggerNodeChanges, triggerEdgeChanges } = + const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = store.getState(); if (!isSelecting || !containerBounds.current || !userSelectionRect) { return; @@ -149,7 +149,7 @@ export function Pane({ }; const selectedNodes = getNodesInside( - nodes, + nodeLookup, nextUserSelectRect, transform, selectionMode === SelectionMode.Partial, @@ -163,22 +163,22 @@ export function Pane({ for (const selectedNode of selectedNodes) { selectedNodeIds.add(selectedNode.id); - for (const edge of edges) { + for (const [edgeId, edge] of edgeLookup) { if (edge.source === selectedNode.id || edge.target === selectedNode.id) { - selectedEdgeIds.add(edge.id); + selectedEdgeIds.add(edgeId); } } } if (prevSelectedNodesCount.current !== selectedNodeIds.size) { prevSelectedNodesCount.current = selectedNodeIds.size; - const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[]; + const changes = getSelectionChanges(nodeLookup, selectedNodeIds, true) as NodeChange[]; triggerNodeChanges(changes); } if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) { prevSelectedEdgesCount.current = selectedEdgeIds.size; - const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; + const changes = getSelectionChanges(edgeLookup, selectedEdgeIds) as EdgeChange[]; triggerEdgeChanges(changes); } diff --git a/packages/react/src/hooks/useInternalNode.ts b/packages/react/src/hooks/useInternalNode.ts new file mode 100644 index 00000000..d16d697b --- /dev/null +++ b/packages/react/src/hooks/useInternalNode.ts @@ -0,0 +1,21 @@ +import { useCallback } from 'react'; +import { shallow } from 'zustand/shallow'; + +import { useStore } from './useStore'; +import type { InternalNode, Node } from '../types'; + +/** + * Hook for getting an internal node by id + * + * @public + * @param id - id of the node + * @returns array with visible node ids + */ +export function useInternalNode(id: string): InternalNode | undefined { + const node = useStore( + useCallback((s) => s.nodeLookup.get(id) as InternalNode | undefined, [id]), + shallow + ); + + return node; +} diff --git a/packages/react/src/hooks/useMoveSelectedNodes.ts b/packages/react/src/hooks/useMoveSelectedNodes.ts index c627628a..d189642b 100644 --- a/packages/react/src/hooks/useMoveSelectedNodes.ts +++ b/packages/react/src/hooks/useMoveSelectedNodes.ts @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { calculateNodePosition, snapPosition, type XYPosition } from '@xyflow/system'; -import { Node } from '../types'; +import { type Node } from '../types'; import { useStoreApi } from './useStore'; const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) => @@ -17,18 +17,11 @@ export function useMoveSelectedNodes() { const store = useStoreApi(); const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => { - const { - nodeExtent, - nodes, - snapToGrid, - snapGrid, - nodesDraggable, - onError, - updateNodePositions, - nodeLookup, - nodeOrigin, - } = store.getState(); - const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable)); + const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } = + store.getState(); + const nodeUpdates = []; + const isSelected = selectedAndDraggable(nodesDraggable); + // by default a node moves 5px on each key press // if snap grid is enabled, we use that for the velocity const xVelo = snapToGrid ? snapGrid[0] : 5; @@ -37,32 +30,34 @@ export function useMoveSelectedNodes() { const xDiff = params.direction.x * xVelo * params.factor; const yDiff = params.direction.y * yVelo * params.factor; - const nodeUpdates = selectedNodes.map((node) => { - if (node.computed?.positionAbsolute) { - let nextPosition = { - x: node.computed.positionAbsolute.x + xDiff, - y: node.computed.positionAbsolute.y + yDiff, - }; - - if (snapToGrid) { - nextPosition = snapPosition(nextPosition, snapGrid); - } - - const { position, positionAbsolute } = calculateNodePosition({ - nodeId: node.id, - nextPosition, - nodeLookup, - nodeExtent, - nodeOrigin, - onError, - }); - - node.position = position; - node.computed.positionAbsolute = positionAbsolute; + for (const [, node] of nodeLookup) { + if (!isSelected(node)) { + continue; } - return node; - }); + let nextPosition = { + x: node.internals.positionAbsolute.x + xDiff, + y: node.internals.positionAbsolute.y + yDiff, + }; + + if (snapToGrid) { + nextPosition = snapPosition(nextPosition, snapGrid); + } + + const { position, positionAbsolute } = calculateNodePosition({ + nodeId: node.id, + nextPosition, + nodeLookup, + nodeExtent, + nodeOrigin, + onError, + }); + + node.position = position; + node.internals.positionAbsolute = positionAbsolute; + + nodeUpdates.push(node); + } updateNodePositions(nodeUpdates); }, []); diff --git a/packages/react/src/hooks/useNodesInitialized.ts b/packages/react/src/hooks/useNodesInitialized.ts index 3a278f6c..346e56d9 100644 --- a/packages/react/src/hooks/useNodesInitialized.ts +++ b/packages/react/src/hooks/useNodesInitialized.ts @@ -1,5 +1,3 @@ -import { internalsSymbol } from '@xyflow/system'; - import { useStore } from './useStore'; import type { ReactFlowState } from '../types'; @@ -8,13 +6,13 @@ export type UseNodesInitializedOptions = { }; const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => { - if (s.nodes.length === 0) { + if (s.nodeLookup.size === 0) { return false; } - for (const node of s.nodes) { + for (const [, node] of s.nodeLookup) { if (options.includeHiddenNodes || !node.hidden) { - if (node[internalsSymbol]?.handleBounds === undefined) { + if (node.internals.handleBounds === undefined) { return false; } } diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 5c10ad6a..6be3f90b 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -1,16 +1,9 @@ import { useCallback, useMemo, useRef, useState } from 'react'; -import { - getElementsToRemove, - getOverlappingArea, - isRectObject, - nodeHasDimensions, - nodeToRect, - type Rect, -} from '@xyflow/system'; +import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system'; import useViewportHelper from './useViewportHelper'; import { useStoreApi } from './useStore'; -import type { ReactFlowInstance, Instance, Node, Edge } from '../types'; +import type { ReactFlowInstance, Instance, Node, Edge, InternalNode } from '../types'; import { getElementsDiffChanges, isNode } from '../utils'; import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; @@ -27,13 +20,20 @@ export function useReactFlow>(() => { - return store.getState().nodes.map((n) => ({ ...n })) as NodeType[]; - }, []); + const getNodes = useCallback>( + () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[], + [] + ); - const getNode = useCallback>((id) => { - return store.getState().nodeLookup.get(id) as NodeType; - }, []); + const getInternalNode = useCallback>( + (id) => store.getState().nodeLookup.get(id) as InternalNode, + [] + ); + + const getNode = useCallback>( + (id) => getInternalNode(id)?.internals.userNode as NodeType, + [getInternalNode] + ); const getEdges = useCallback>(() => { const { edges = [] } = store.getState(); @@ -223,13 +223,9 @@ export function useReactFlow { - const node = - isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect) - ? nodeOrRect - : (store.getState().nodeLookup.get(nodeOrRect.id) as NodeType); - - return node ? nodeToRect(node) : null; + const getNodeRect = useCallback(({ id }: { id: string }): Rect | null => { + const internalNode = store.getState().nodeLookup.get(id); + return internalNode ? nodeToRect(internalNode) : null; }, []); const getIntersectingNodes = useCallback>( @@ -242,7 +238,9 @@ export function useReactFlow { - if (!isRect && (n.id === nodeOrRect!.id || !n.computed?.positionAbsolute)) { + const internalNode = store.getState().nodeLookup.get(n.id); + + if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) { return false; } @@ -308,6 +306,7 @@ export function useReactFlow((id: string | string[]) => { - const { domNode, updateNodeDimensions } = store.getState(); + const { domNode, updateNodeInternals } = store.getState(); const updateIds = Array.isArray(id) ? id : [id]; - const updates = new Map(); + const updates = new Map(); updateIds.forEach((updateId) => { const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement; if (nodeElement) { - updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true }); + updates.set(updateId, { id: updateId, nodeElement, force: true }); } }); - requestAnimationFrame(() => updateNodeDimensions(updates)); + requestAnimationFrame(() => updateNodeInternals(updates)); }, []); } diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index 0e27d933..eb92b310 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -48,12 +48,12 @@ const useViewportHelper = (): ViewportHelperFunctions => { return { x, y, zoom }; }, fitView: (options) => { - const { nodes, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState(); + const { nodeLookup, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState(); return panZoom ? fitView( { - nodes, + nodeLookup, width, height, nodeOrigin, diff --git a/packages/react/src/hooks/useVisibleNodeIds.ts b/packages/react/src/hooks/useVisibleNodeIds.ts index 309391dc..e8d14c8e 100644 --- a/packages/react/src/hooks/useVisibleNodeIds.ts +++ b/packages/react/src/hooks/useVisibleNodeIds.ts @@ -1,13 +1,13 @@ -import { getNodesInside } from '@xyflow/system'; +import { useCallback } from 'react'; import { shallow } from 'zustand/shallow'; +import { getNodesInside } from '@xyflow/system'; import { useStore } from './useStore'; import type { Node, ReactFlowState } from '../types'; -import { useCallback } from 'react'; const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => { return onlyRenderVisible - ? getNodesInside(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map( + ? getNodesInside(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map( (node) => node.id ) : Array.from(s.nodeLookup.keys()); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 022f2626..1c5794f4 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -26,9 +26,10 @@ export { useNodesInitialized, type UseNodesInitializedOptions } from './hooks/us export { useHandleConnections } from './hooks/useHandleConnections'; export { useNodesData } from './hooks/useNodesData'; export { useConnection } from './hooks/useConnection'; +export { useInternalNode } from './hooks/useInternalNode'; export { useNodeId } from './contexts/NodeIdContext'; -export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes'; +export { applyNodeChanges, applyEdgeChanges } from './utils/changes'; export { isNode, isEdge } from './utils/general'; export * from './additional-components'; @@ -84,6 +85,18 @@ export { type ControlPosition, type ControlLinePosition, type ResizeControlVariant, + type NodeChange, + type NodeDimensionChange, + type NodePositionChange, + type NodeSelectionChange, + type NodeRemoveChange, + type NodeAddChange, + type NodeReplaceChange, + type EdgeChange, + type EdgeSelectionChange, + type EdgeRemoveChange, + type EdgeAddChange, + type EdgeReplaceChange, } from '@xyflow/system'; // system utils @@ -103,5 +116,4 @@ export { addEdge, updateEdge, getConnectedEdges, - internalsSymbol, } from '@xyflow/system'; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index a2b98c1a..7ce263c9 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -2,27 +2,20 @@ import { createWithEqualityFn } from 'zustand/traditional'; import { clampPosition, fitView as fitViewSystem, - adoptUserProvidedNodes, + adoptUserNodes, updateAbsolutePositions, panBy as panBySystem, - Dimensions, - updateNodeDimensions as updateNodeDimensionsSystem, + updateNodeInternals as updateNodeInternalsSystem, updateConnectionLookup, + handleParentExpand, + NodeChange, + EdgeSelectionChange, + NodeSelectionChange, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import getInitialState from './initialState'; -import type { - ReactFlowState, - Node, - Edge, - NodeDimensionChange, - EdgeSelectionChange, - NodeSelectionChange, - NodePositionChange, - UnselectNodesAndEdgesParams, - FitViewOptions, -} from '../types'; +import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions, InternalNode } from '../types'; const createRFStore = ({ nodes, @@ -52,9 +45,9 @@ const createRFStore = ({ // // When this happens, we take the note objects passed by the user and extend them with fields // relevant for internal React Flow operations. - const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); + adoptUserNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); - set({ nodes: nodesWithInternalData }); + set({ nodes }); }, setEdges: (edges: Edge[]) => { const { connectionLookup, edgeLookup } = get(); @@ -78,11 +71,10 @@ const createRFStore = ({ // Every node gets registerd at a ResizeObserver. Whenever a node // changes its dimensions, this function is called to measure the // new dimensions and update the nodes. - updateNodeDimensions: (updates) => { + updateNodeInternals: (updates) => { const { onNodesChange, fitView, - nodes, nodeLookup, fitViewOnInit, fitViewDone, @@ -91,35 +83,21 @@ const createRFStore = ({ nodeOrigin, debug, } = get(); - const changes: NodeDimensionChange[] = []; - const updatedNodes = updateNodeDimensionsSystem( - updates, - nodes, - nodeLookup, - domNode, - nodeOrigin, - (id: string, dimensions: Dimensions) => { - changes.push({ - id: id, - type: 'dimensions', - dimensions, - }); - } - ); + const { changes, updatedInternals } = updateNodeInternalsSystem(updates, nodeLookup, domNode, nodeOrigin); - if (!updatedNodes) { + if (!updatedInternals) { return; } - const nextNodes = updateAbsolutePositions(updatedNodes, nodeLookup, nodeOrigin); + updateAbsolutePositions(nodeLookup, { nodeOrigin }); // we call fitView once initially after all dimensions are set let nextFitViewDone = fitViewDone; if (!fitViewDone && fitViewOnInit) { - nextFitViewDone = fitView(nextNodes, { + nextFitViewDone = fitView({ ...fitViewOnInitOptions, - nodes: fitViewOnInitOptions?.nodes || nextNodes, + nodes: fitViewOnInitOptions?.nodes, }); } @@ -128,7 +106,7 @@ const createRFStore = ({ // has not provided an onNodesChange handler. // Nodes are only rendered if they have a width and height // attribute which they get from this handler. - set({ nodes: nextNodes, fitViewDone: nextFitViewDone }); + set({ fitViewDone: nextFitViewDone }); if (changes?.length > 0) { if (debug) { @@ -138,18 +116,41 @@ const createRFStore = ({ } }, updateNodePositions: (nodeDragItems, dragging = false) => { - const changes = nodeDragItems.map((node) => { - const change: NodePositionChange = { + const { nodeLookup } = get(); + const triggerChangeNodes: InternalNode[] = []; + + const changes: NodeChange[] = nodeDragItems.map((node) => { + // @todo add expandParent to drag item so that we can get rid of the look up here + const internalNode = nodeLookup.get(node.id); + const change: NodeChange = { id: node.id, type: 'position', position: node.position, - positionAbsolute: node.computed?.positionAbsolute, dragging, }; + if (internalNode?.expandParent && change.position) { + triggerChangeNodes.push({ + ...internalNode, + position: change.position, + internals: { + ...internalNode.internals, + positionAbsolute: node.internals.positionAbsolute, + }, + }); + + change.position.x = Math.max(0, change.position.x); + change.position.y = Math.max(0, change.position.y); + } + return change; }); + if (triggerChangeNodes.length > 0) { + const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup); + changes.push(...parentExpandChanges); + } + get().triggerNodeChanges(changes); }, triggerNodeChanges: (changes) => { @@ -185,7 +186,7 @@ const createRFStore = ({ } }, addSelectedNodes: (selectedNodeIds) => { - const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); + const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get(); if (multiSelectionActive) { const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)); @@ -193,11 +194,11 @@ const createRFStore = ({ return; } - triggerNodeChanges(getSelectionChanges(nodes, new Set([...selectedNodeIds]), true)); - triggerEdgeChanges(getSelectionChanges(edges)); + triggerNodeChanges(getSelectionChanges(nodeLookup, new Set([...selectedNodeIds]), true)); + triggerEdgeChanges(getSelectionChanges(edgeLookup)); }, addSelectedEdges: (selectedEdgeIds) => { - const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); + const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get(); if (multiSelectionActive) { const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)); @@ -205,8 +206,8 @@ const createRFStore = ({ return; } - triggerEdgeChanges(getSelectionChanges(edges, new Set([...selectedEdgeIds]))); - triggerNodeChanges(getSelectionChanges(nodes, new Set(), true)); + triggerEdgeChanges(getSelectionChanges(edgeLookup, new Set([...selectedEdgeIds]))); + triggerNodeChanges(getSelectionChanges(nodeLookup, new Set(), true)); }, unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => { const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get(); @@ -255,29 +256,30 @@ const createRFStore = ({ triggerEdgeChanges(edgeChanges); }, setNodeExtent: (nodeExtent) => { - const { nodes } = get(); + const { nodeLookup } = get(); + + for (const [, node] of nodeLookup) { + const positionAbsolute = clampPosition(node.position, nodeExtent); + + nodeLookup.set(node.id, { + ...node, + internals: { + ...node.internals, + positionAbsolute, + }, + }); + } set({ nodeExtent, - nodes: nodes.map((node) => { - const positionAbsolute = clampPosition(node.position, nodeExtent); - - return { - ...node, - computed: { - ...node.computed, - positionAbsolute, - }, - }; - }), }); }, panBy: (delta): boolean => { const { transform, width, height, panZoom, translateExtent } = get(); return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); }, - fitView: (nodes: Node[], options?: FitViewOptions): boolean => { - const { panZoom, width, height, minZoom, maxZoom, nodeOrigin } = get(); + fitView: (options?: FitViewOptions): boolean => { + const { panZoom, width, height, minZoom, maxZoom, nodeOrigin, nodeLookup } = get(); if (!panZoom) { return false; @@ -285,7 +287,7 @@ const createRFStore = ({ return fitViewSystem( { - nodes, + nodeLookup, width, height, panZoom, diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 0cf3c7f0..cc754976 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -1,7 +1,7 @@ import { infiniteExtent, ConnectionMode, - adoptUserProvidedNodes, + adoptUserNodes, getNodesBounds, getViewportForBounds, Transform, @@ -35,7 +35,7 @@ const getInitialState = ({ const storeNodes = defaultNodes ?? nodes ?? []; updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); - const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, { + adoptUserNodes(storeNodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false, }); @@ -43,7 +43,7 @@ const getInitialState = ({ let transform: Transform = [0, 0, 1]; if (fitView && width && height) { - const nodesWithDimensions = nextNodes.filter( + const nodesWithDimensions = storeNodes.filter( (node) => (node.width || node.initialWidth) && (node.height || node.initialHeight) ); // @todo users nodeOrigin should be used here @@ -57,7 +57,7 @@ const getInitialState = ({ width: 0, height: 0, transform, - nodes: nextNodes, + nodes: storeNodes, nodeLookup, edges: storeEdges, edgeLookup, diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 302ffeee..8b55920f 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -12,9 +12,11 @@ import { XYPosition, OnBeforeDeleteBase, Connection, + NodeChange, + EdgeChange, } from '@xyflow/system'; -import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.'; +import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.'; export type OnNodesChange = (changes: NodeChange[]) => void; export type OnEdgesChange = (changes: EdgeChange[]) => void; diff --git a/packages/react/src/types/index.ts b/packages/react/src/types/index.ts index 6ee8dada..f18123f2 100644 --- a/packages/react/src/types/index.ts +++ b/packages/react/src/types/index.ts @@ -1,6 +1,5 @@ export * from './nodes'; export * from './edges'; -export * from './changes'; export * from './component-props'; export * from './general'; export * from './store'; diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index 1991580c..67a6d928 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-namespace */ import type { Rect, Viewport } from '@xyflow/system'; -import type { Node, Edge, ViewportHelperFunctions } from '.'; +import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.'; export type ReactFlowJsonObject = { nodes: NodeType[]; @@ -20,6 +20,7 @@ export namespace Instance { ) => void; export type AddNodes = (payload: NodeType[] | NodeType) => void; export type GetNode = (id: string) => NodeType | undefined; + export type GetInternalNode = (id: string) => InternalNode | undefined; export type GetEdges = () => EdgeType[]; export type SetEdges = ( payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[]) @@ -83,6 +84,13 @@ export type ReactFlowInstance; + /** + * Returns an internal node by id. + * + * @param id - the node id + * @returns the internal node or undefined if no node was found + */ + getInternalNode: Instance.GetInternalNode; /** * Returns edges. * diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index 52f5a321..a01a894a 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -1,5 +1,12 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; -import type { CoordinateExtent, NodeBase, NodeOrigin, OnError, NodeProps as NodePropsBase } from '@xyflow/system'; +import type { + CoordinateExtent, + NodeBase, + NodeOrigin, + OnError, + NodeProps as NodePropsBase, + InternalNodeBase, +} from '@xyflow/system'; import { NodeTypes } from './general'; @@ -17,6 +24,14 @@ export type Node< focusable?: boolean; }; +/** + * The node data structure that gets used for internal nodes. + * There are some data structures added under node.internal + * that are needed for tracking some properties + * @public + */ +export type InternalNode = InternalNodeBase; + export type NodeMouseHandler = (event: ReactMouseEvent, node: NodeType) => void; export type SelectionDragHandler = (event: ReactMouseEvent, nodes: NodeType[]) => void; export type OnNodeDrag = ( diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 6c1cf9b0..397ca6f1 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -2,7 +2,7 @@ import { ConnectionMode, type ConnectionStatus, type CoordinateExtent, - type NodeDimensionUpdate, + type InternalNodeUpdate, type UpdateNodePositions, type NodeOrigin, type OnConnect, @@ -25,12 +25,13 @@ import { type EdgeLookup, type ConnectionLookup, type NodeLookup, + NodeChange, + EdgeChange, } from '@xyflow/system'; import type { Edge, Node, - NodeChange, OnNodesChange, OnEdgesChange, DefaultEdgeOptions, @@ -43,7 +44,7 @@ import type { OnNodeDrag, OnBeforeDelete, IsValidConnection, - EdgeChange, + InternalNode, } from '.'; export type ReactFlowStore = { @@ -52,7 +53,7 @@ export type ReactFlowStore; + nodeLookup: NodeLookup>; edges: Edge[]; edgeLookup: EdgeLookup; connectionLookup: ConnectionLookup; @@ -153,7 +154,7 @@ export type ReactFlowActions = { setNodes: (nodes: NodeType[]) => void; setEdges: (edges: EdgeType[]) => void; setDefaultNodesAndEdges: (nodes?: NodeType[], edges?: EdgeType[]) => void; - updateNodeDimensions: (updates: Map) => void; + updateNodeInternals: (updates: Map) => void; updateNodePositions: UpdateNodePositions; resetSelectedElements: () => void; unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void; @@ -169,7 +170,7 @@ export type ReactFlowActions = { triggerNodeChanges: (changes: NodeChange[]) => void; triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; - fitView: (nodes: NodeType[], options?: FitViewOptions) => boolean; + fitView: (options?: FitViewOptions) => boolean; }; export type ReactFlowState = ReactFlowStore< diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index fa6f6f30..44af2451 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -1,51 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { EdgeLookup, NodeLookup } from '@xyflow/system'; -import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types'; - -export function handleParentExpand(updatedElements: any[], updateItem: any) { - for (const [index, item] of updatedElements.entries()) { - if (item.id === updateItem.parentNode) { - const parent = { ...item }; - parent.computed ??= {}; - - const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width; - const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height; - - if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) { - parent.width = parent.width ?? parent.computed.width; - parent.height = parent.height ?? parent.computed.height; - - if (extendWidth > 0) { - parent.width += extendWidth; - } - - if (extendHeight > 0) { - parent.height += extendHeight; - } - - if (updateItem.position.x < 0) { - const xDiff = Math.abs(updateItem.position.x); - parent.position.x = parent.position.x - xDiff; - parent.width += xDiff; - updateItem.position.x = 0; - } - - if (updateItem.position.y < 0) { - const yDiff = Math.abs(updateItem.position.y); - parent.position.y = parent.position.y - yDiff; - parent.height += yDiff; - updateItem.position.y = 0; - } - - parent.computed.width = parent.width; - parent.computed.height = parent.height; - - updatedElements[index] = parent; - } - break; - } - } -} +import { + EdgeLookup, + NodeLookup, + EdgeChange, + NodeChange, + NodeSelectionChange, + EdgeSelectionChange, +} from '@xyflow/system'; +import type { Node, Edge, InternalNode } from '../types'; // This function applies changes to nodes or edges that are triggered by React Flow internally. // When you drag a node for example, React Flow will send a position change update. @@ -103,7 +65,7 @@ function applyChanges(changes: any[], elements: any[]): any[] { const updatedElement = { ...element }; for (const change of changes) { - applyChange(change, updatedElement, updatedElements); + applyChange(change, updatedElement); } updatedElements.push(updatedElement); @@ -113,7 +75,7 @@ function applyChanges(changes: any[], elements: any[]): any[] { } // Applies a single change to an element. This is a *mutable* update. -function applyChange(change: any, element: any, elements: any[] = []): any { +function applyChange(change: any, element: any): any { switch (change.type) { case 'select': { element.selected = change.selected; @@ -125,26 +87,18 @@ function applyChange(change: any, element: any, elements: any[] = []): any { element.position = change.position; } - if (typeof change.positionAbsolute !== 'undefined') { - element.computed ??= {}; - element.computed.positionAbsolute = change.positionAbsolute; - } - if (typeof change.dragging !== 'undefined') { element.dragging = change.dragging; } - if (element.expandParent) { - handleParentExpand(elements, element); - } break; } case 'dimensions': { if (typeof change.dimensions !== 'undefined') { - element.computed ??= {}; - element.computed.width = change.dimensions.width; - element.computed.height = change.dimensions.height; + element.measured ??= {}; + element.measured.width = change.dimensions.width; + element.measured.height = change.dimensions.height; if (change.resizing) { element.width = change.dimensions.width; @@ -156,10 +110,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any { element.resizing = change.resizing; } - if (element.expandParent) { - handleParentExpand(elements, element); - } - break; } } @@ -228,13 +178,13 @@ export function createSelectionChange(id: string, selected: boolean): NodeSelect } export function getSelectionChanges( - items: any[], + items: Map, selectedIds: Set = new Set(), mutateItem = false ): NodeSelectionChange[] | EdgeSelectionChange[] { const changes: NodeSelectionChange[] | EdgeSelectionChange[] = []; - for (const item of items) { + for (const [, item] of items) { const willBeSelected = selectedIds.has(item.id); // we don't want to set all items to selected=false on the first selection @@ -266,7 +216,7 @@ export function getElementsDiffChanges({ lookup, }: { items: Node[] | undefined; - lookup: NodeLookup; + lookup: NodeLookup>; }): NodeChange[]; export function getElementsDiffChanges({ items, diff --git a/packages/svelte/.prettierignore b/packages/svelte/.prettierignore index 38972655..33affffb 100644 --- a/packages/svelte/.prettierignore +++ b/packages/svelte/.prettierignore @@ -1,6 +1,7 @@ .DS_Store node_modules /build +/dist /.svelte-kit /package .env diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 5217686c..f93688fc 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -42,7 +42,7 @@ const { connectionMode, domNode, - nodes, + nodeLookup, connectionRadius, viewport, isValidConnection, @@ -72,7 +72,7 @@ isTarget, connectionRadius: $connectionRadius, domNode: $domNode, - nodes: $nodes, + nodeLookup: $nodeLookup, connectionMode: $connectionMode, lib: $lib, autoPanOnConnect: $autoPanOnConnect, diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index e502cd50..384b3369 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -35,8 +35,8 @@ export let sourcePosition: $$Props['sourcePosition'] = undefined; export let targetPosition: $$Props['targetPosition'] = undefined; export let zIndex: $$Props['zIndex']; - export let computedWidth: $$Props['computedWidth'] = undefined; - export let computedHeight: $$Props['computedHeight'] = undefined; + export let measuredWidth: $$Props['measuredWidth'] = undefined; + export let measuredHeight: $$Props['measuredHeight'] = undefined; export let initialWidth: $$Props['initialWidth'] = undefined; export let initialHeight: $$Props['initialHeight'] = undefined; export let width: $$Props['width'] = undefined; @@ -52,7 +52,7 @@ nodeDragThreshold, selectNodesOnDrag, handleNodeSelection, - updateNodeDimensions + updateNodeInternals } = store; let nodeRef: HTMLDivElement; @@ -79,8 +79,8 @@ height, initialWidth, initialHeight, - computedWidth, - computedHeight + measuredWidth, + measuredHeight }); $: { @@ -97,7 +97,7 @@ if (doUpdate) { requestAnimationFrame(() => - updateNodeDimensions( + updateNodeInternals( new Map([ [ id, diff --git a/packages/svelte/src/lib/components/NodeWrapper/types.ts b/packages/svelte/src/lib/components/NodeWrapper/types.ts index a290ec08..7277be5f 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/types.ts +++ b/packages/svelte/src/lib/components/NodeWrapper/types.ts @@ -21,8 +21,8 @@ export type NodeWrapperProps = Pick< | 'initialWidth' | 'initialHeight' > & { - computedWidth?: number; - computedHeight?: number; + measuredWidth?: number; + measuredHeight?: number; type: string; positionX: number; positionY: number; diff --git a/packages/svelte/src/lib/components/NodeWrapper/utils.ts b/packages/svelte/src/lib/components/NodeWrapper/utils.ts index b2609587..f795e31d 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/utils.ts +++ b/packages/svelte/src/lib/components/NodeWrapper/utils.ts @@ -3,20 +3,20 @@ export function getNodeInlineStyleDimensions({ height, initialWidth, initialHeight, - computedWidth, - computedHeight + measuredWidth, + measuredHeight }: { width?: number; height?: number; initialWidth?: number; initialHeight?: number; - computedWidth?: number; - computedHeight?: number; + measuredWidth?: number; + measuredHeight?: number; }): { width: string | undefined; height: string | undefined; } { - if (computedWidth === undefined && computedHeight === undefined) { + if (measuredWidth === undefined && measuredHeight === undefined) { const styleWidth = width ?? initialWidth; const styleHeight = height ?? initialHeight; diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index 8761942d..c0b8165e 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -1,11 +1,6 @@