From 844d574c4ffabe353bd6271d6195955e884decde Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 27 Mar 2024 11:22:43 +0100 Subject: [PATCH 1/9] refactor(react): separate user nodes and indernal nodes --- .../examples/FloatingEdges/FloatingEdge.tsx | 4 +- .../react/src/examples/FloatingEdges/utils.ts | 4 +- .../additional-components/MiniMap/MiniMap.tsx | 6 +- .../NodeToolbar/NodeToolbar.tsx | 24 ++-- .../src/components/ConnectionLine/index.tsx | 11 +- .../EdgeWrapper/EdgeUpdateAnchors.tsx | 4 +- .../react/src/components/Handle/index.tsx | 2 +- .../src/components/NodeWrapper/index.tsx | 15 ++- .../src/components/NodeWrapper/utils.tsx | 6 +- .../src/components/NodesSelection/index.tsx | 10 +- .../components/SelectionListener/index.tsx | 2 +- packages/react/src/container/Pane/index.tsx | 12 +- .../react/src/hooks/useMoveSelectedNodes.ts | 69 ++++++------ .../react/src/hooks/useNodesInitialized.ts | 8 +- packages/react/src/hooks/useReactFlow.ts | 8 +- packages/react/src/index.ts | 1 - packages/react/src/store/index.ts | 34 +++--- packages/react/src/store/initialState.ts | 6 +- packages/react/src/types/nodes.ts | 11 +- packages/react/src/types/store.ts | 5 +- packages/react/src/utils/changes.ts | 21 ++-- packages/system/src/constants.ts | 2 - packages/system/src/types/general.ts | 4 +- packages/system/src/types/nodes.ts | 24 ++-- packages/system/src/utils/edges/general.ts | 13 +-- packages/system/src/utils/edges/positions.ts | 25 +++-- packages/system/src/utils/general.ts | 5 +- packages/system/src/utils/graph.ts | 100 ++++++++++++----- packages/system/src/utils/store.ts | 106 ++++++++---------- packages/system/src/xydrag/XYDrag.ts | 26 +++-- packages/system/src/xydrag/utils.ts | 71 ++++++------ packages/system/src/xyhandle/XYHandle.ts | 8 +- packages/system/src/xyhandle/utils.ts | 42 +++---- 33 files changed, 374 insertions(+), 315 deletions(-) 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..a022207c 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 @@ -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/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/NodeToolbar/NodeToolbar.tsx b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx index 0ecc8185..6c0236d8 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?.computed.width !== b?.computed.width || + a?.computed.height !== b?.computed.height || a?.selected !== b?.selected || - a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z; + a?.internals.z !== b?.internals.z; -const nodesEqualityFn = (a: Node[], b: Node[]) => { +const nodesEqualityFn = (a: InternalNode[], b: InternalNode[]) => { if (a.length !== b.length) { return false; } @@ -49,10 +49,10 @@ export function NodeToolbar({ const contextNodeId = useNodeId(); const nodesSelector = useCallback( - (state: ReactFlowState): Node[] => { + (state: ReactFlowState): InternalNode[] => { const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || '']; - return nodeIds.reduce((acc, id) => { + return nodeIds.reduce((acc, id) => { const node = state.nodeLookup.get(id); if (node) { acc.push(node); @@ -74,7 +74,7 @@ export function NodeToolbar({ } const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin }); - const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1)); + const zIndex: number = Math.max(...nodes.map((node) => (node.internals?.z || 1) + 1)); const wrapperStyle: CSSProperties = { position: 'absolute', diff --git a/packages/react/src/components/ConnectionLine/index.tsx b/packages/react/src/components/ConnectionLine/index.tsx index 4fe2ca76..86655232 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.computed.width ?? 0) / 2; + const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed.height ?? 0; + const fromX = (fromNode.internals.positionAbsolute.x ?? 0) + fromHandleX; + const fromY = (fromNode.internals.positionAbsolute.y ?? 0) + fromHandleY; const fromPosition = fromHandle?.position; const toPosition = fromPosition ? oppositePosition[fromPosition] : null; diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index 6061d8a7..e4810f11 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -54,7 +54,7 @@ export function EdgeUpdateAnchors({ onConnectStart, onConnectEnd, cancelConnection, - nodes, + nodeLookup, rfId: flowId, panBy, updateConnection, @@ -82,7 +82,7 @@ export function EdgeUpdateAnchors({ domNode, handleId, nodeId, - nodes, + nodeLookup, isTarget, edgeUpdaterType: handleType, lib, diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index ee36d066..977a5c5b 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -126,7 +126,7 @@ function HandleComponent( connectionMode: currentStore.connectionMode, connectionRadius: currentStore.connectionRadius, domNode: currentStore.domNode, - nodes: currentStore.nodes, + nodeLookup: currentStore.nodeLookup, lib: currentStore.lib, isTarget, handleId, diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 8c207cd9..5e2b91e2 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,7 +83,7 @@ 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(); 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/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index dfb240d0..ad2b01ed 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -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/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..4e83448a 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -32,7 +32,7 @@ export function useReactFlow>((id) => { - return store.getState().nodeLookup.get(id) as NodeType; + return store.getState().nodeLookup.get(id)?.internals.userProvidedNode as NodeType; }, []); const getEdges = useCallback>(() => { @@ -227,7 +227,7 @@ 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; } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 022f2626..800ebd94 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -103,5 +103,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..3eddf5bc 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -52,9 +52,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 }); + adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); - set({ nodes: nodesWithInternalData }); + set({ nodes }); }, setEdges: (edges: Edge[]) => { const { connectionLookup, edgeLookup } = get(); @@ -80,9 +80,9 @@ const createRFStore = ({ // new dimensions and update the nodes. updateNodeDimensions: (updates) => { const { + nodes, onNodesChange, fitView, - nodes, nodeLookup, fitViewOnInit, fitViewDone, @@ -95,7 +95,6 @@ const createRFStore = ({ const updatedNodes = updateNodeDimensionsSystem( updates, - nodes, nodeLookup, domNode, nodeOrigin, @@ -112,14 +111,14 @@ const createRFStore = ({ 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 || nodes, }); } @@ -128,7 +127,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({ nodes, fitViewDone: nextFitViewDone }); if (changes?.length > 0) { if (debug) { @@ -143,7 +142,6 @@ const createRFStore = ({ id: node.id, type: 'position', position: node.position, - positionAbsolute: node.computed?.positionAbsolute, dragging, }; @@ -185,7 +183,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 +191,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 +203,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(); @@ -276,8 +274,8 @@ const createRFStore = ({ 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 +283,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..9f373719 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -35,7 +35,7 @@ const getInitialState = ({ const storeNodes = defaultNodes ?? nodes ?? []; updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); - const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, { + adoptUserProvidedNodes(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/nodes.ts b/packages/react/src/types/nodes.ts index 52f5a321..89fec455 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -1,5 +1,12 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; -import type { CoordinateExtent, NodeBase, NodeOrigin, OnError, NodeProps as NodePropsBase } from '@xyflow/system'; +import type { + CoordinateExtent, + NodeBase, + NodeOrigin, + OnError, + NodeProps as NodePropsBase, + InternalNodeBase, +} from '@xyflow/system'; import { NodeTypes } from './general'; @@ -17,6 +24,8 @@ export type Node< focusable?: boolean; }; +export type InternalNode = InternalNodeBase; + export type NodeMouseHandler = (event: ReactMouseEvent, node: NodeType) => void; export type SelectionDragHandler = (event: ReactMouseEvent, nodes: NodeType[]) => void; export type OnNodeDrag = ( diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 6c1cf9b0..eed7b9a3 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -44,6 +44,7 @@ import type { OnBeforeDelete, IsValidConnection, EdgeChange, + InternalNode, } from '.'; export type ReactFlowStore = { @@ -52,7 +53,7 @@ export type ReactFlowStore; + nodeLookup: NodeLookup>; edges: Edge[]; edgeLookup: EdgeLookup; connectionLookup: ConnectionLookup; @@ -169,7 +170,7 @@ export type ReactFlowActions = { triggerNodeChanges: (changes: NodeChange[]) => void; triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; - fitView: (nodes: NodeType[], options?: FitViewOptions) => boolean; + fitView: (options?: FitViewOptions) => boolean; }; export type ReactFlowState = ReactFlowStore< diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index fa6f6f30..437c80dc 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -1,6 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { EdgeLookup, NodeLookup } from '@xyflow/system'; -import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types'; +import type { + Node, + Edge, + EdgeChange, + NodeChange, + NodeSelectionChange, + EdgeSelectionChange, + InternalNode, +} from '../types'; export function handleParentExpand(updatedElements: any[], updateItem: any) { for (const [index, item] of updatedElements.entries()) { @@ -125,11 +133,6 @@ 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; } @@ -228,13 +231,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 +269,7 @@ export function getElementsDiffChanges({ lookup, }: { items: Node[] | undefined; - lookup: NodeLookup; + lookup: NodeLookup>; }): NodeChange[]; export function getElementsDiffChanges({ items, diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index 21d21234..67b913a0 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -25,8 +25,6 @@ export const errorMessages = { `Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`, }; -export const internalsSymbol = Symbol.for('internals'); - export const infiniteExtent: CoordinateExtent = [ [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index b7aa248e..a7c6665b 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -2,7 +2,7 @@ import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3'; import type { XYPosition, Rect } from './utils'; -import type { NodeBase, NodeDragItem, NodeOrigin } from './nodes'; +import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes'; import type { ConnectingHandle, HandleType } from './handles'; import { PanZoomInstance } from './panzoom'; import { EdgeBase } from '..'; @@ -52,7 +52,7 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void; export type IsValidConnection = (edge: EdgeBase | Connection) => boolean; export type FitViewParamsBase = { - nodes: NodeType[]; + nodeLookup: Map>; width: number; height: number; panZoom: PanZoomInstance; diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index c888d8a4..6929321b 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -1,4 +1,3 @@ -import { internalsSymbol } from '../constants'; import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.'; import { Optional } from '../utils/types'; @@ -63,18 +62,25 @@ export type NodeBase< computed?: { width?: number; height?: number; - positionAbsolute?: XYPosition; }; +}; +export type InternalNodeBase = NodeType & { + computed: { + width?: number; + height?: number; + }; // Only used internally - [internalsSymbol]?: { - z?: number; - handleBounds?: NodeHandleBounds; - isParent?: boolean; + 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 * (which may lack some fields, like `computed` or `[internalSymbol]`. Used * as an optimization to avoid certain operations. */ - userProvidedNode: NodeBase; + userProvidedNode: NodeType; + handleBounds?: NodeHandleBounds; }; }; @@ -120,6 +126,8 @@ export type NodeDragItem = { computed: { width: number | null; height: number | null; + }; + internals: { positionAbsolute: XYPosition; }; extent?: 'parent' | CoordinateExtent; @@ -137,4 +145,4 @@ export type NodeHandle = Optional; export type Align = 'center' | 'start' | 'end'; -export type NodeLookup = Map; +export type NodeLookup = Map; diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index f53db23a..9e54e035 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -1,5 +1,4 @@ -import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..'; -import { EdgeBase, NodeBase } from '../../types'; +import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..'; import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general'; // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB) @@ -24,8 +23,8 @@ export function getEdgeCenter({ } export type GetEdgeZIndexParams = { - sourceNode: NodeBase; - targetNode: NodeBase; + sourceNode: InternalNodeBase; + targetNode: InternalNodeBase; selected?: boolean; zIndex?: number; elevateOnSelect?: boolean; @@ -43,14 +42,14 @@ export function getElevatedEdgeZIndex({ } const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected; - const selectedZIndex = Math.max(sourceNode[internalsSymbol]?.z || 0, targetNode[internalsSymbol]?.z || 0, 1000); + const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000); return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0); } type IsEdgeVisibleParams = { - sourceNode: NodeBase; - targetNode: NodeBase; + sourceNode: InternalNodeBase; + targetNode: InternalNodeBase; width: number; height: number; transform: Transform; diff --git a/packages/system/src/utils/edges/positions.ts b/packages/system/src/utils/edges/positions.ts index 55a157ea..33fa59ee 100644 --- a/packages/system/src/utils/edges/positions.ts +++ b/packages/system/src/utils/edges/positions.ts @@ -1,25 +1,26 @@ import { EdgePosition } from '../../types/edges'; import { ConnectionMode, OnError } from '../../types/general'; -import { NodeBase, NodeHandle } from '../../types/nodes'; +import { InternalNodeBase, NodeHandle } from '../../types/nodes'; import { Position } from '../../types/utils'; -import { errorMessages, internalsSymbol } from '../../constants'; +import { errorMessages } from '../../constants'; import { HandleElement } from '../../types'; import { getNodeDimensions } from '../general'; export type GetEdgePositionParams = { id: string; - sourceNode: NodeBase; + sourceNode: InternalNodeBase; sourceHandle: string | null; - targetNode: NodeBase; + targetNode: InternalNodeBase; targetHandle: string | null; connectionMode: ConnectionMode; onError?: OnError; }; -function isNodeInitialized(node: NodeBase): boolean { +function isNodeInitialized(node: InternalNodeBase): boolean { return ( - !!(node?.[internalsSymbol]?.handleBounds || node?.handles?.length) && - !!(node?.computed?.width || node?.width || node?.initialWidth) + node && + !!(node.internals.handleBounds || node.handles?.length) && + !!(node.computed.width || node.width || node.initialWidth) ); } @@ -30,8 +31,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n return null; } - const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles); - const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles); + const sourceHandleBounds = sourceNode.internals.handleBounds || toHandleBounds(sourceNode.handles); + const targetHandleBounds = targetNode.internals.handleBounds || toHandleBounds(targetNode.handles); const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle); const targetHandle = getHandle( @@ -95,9 +96,9 @@ function toHandleBounds(handles?: NodeHandle[]) { }; } -function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] { - const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0); - const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0); +function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] { + const x = (handle?.x ?? 0) + (node.internals.positionAbsolute?.x ?? 0); + const y = (handle?.y ?? 0) + (node.internals.positionAbsolute?.y ?? 0); const { width, height } = handle ?? getNodeDimensions(node); switch (position) { diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 3a07e927..5fab4f49 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -8,6 +8,7 @@ import type { NodeOrigin, SnapGrid, Transform, + InternalNodeBase, } from '../types'; import { type Viewport } from '../types'; import { getNodePositionWithOrigin } from './graph'; @@ -65,7 +66,7 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({ height: y2 - y, }); -export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => { +export const nodeToRect = (node: InternalNodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => { const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); return { @@ -75,7 +76,7 @@ export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rec }; }; -export const nodeToBox = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => { +export const nodeToBox = (node: InternalNodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => { const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); return { diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 06bf7f62..9df39893 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -24,6 +24,7 @@ import { OnError, OnBeforeDeleteBase, NodeLookup, + InternalNodeBase, } from '../types'; import { errorMessages } from '../constants'; @@ -47,6 +48,10 @@ export const isEdgeBase = (element: any): export const isNodeBase = (element: any): element is NodeType => 'id' in element && 'position' in element && !('source' in element) && !('target' in element); +export const isInternalNodeBase = ( + element: any +): element is NodeType => 'id' in element && 'computed' in element && !('source' in element) && !('target' in element); + /** * Pass in a node, and get connected nodes where edge.source === node.id * @public @@ -101,7 +106,7 @@ export const getIncomers = { if (!node) { @@ -128,12 +133,10 @@ export const getNodePositionWithOrigin = ( return { position, - positionAbsolute: node.computed?.positionAbsolute - ? { - x: node.computed.positionAbsolute.x - offsetX, - y: node.computed.positionAbsolute.y - offsetY, - } - : position, + positionAbsolute: { + x: node.internals.positionAbsolute.x - offsetX, + y: node.internals.positionAbsolute.y - offsetY, + }, }; }; @@ -151,6 +154,7 @@ export type GetNodesBoundsParams = { * @param params.useRelativePosition - Whether to use the relative or absolute node positions * @returns Bounding box enclosing all nodes */ +// @todo how to handle this if users do not have absolute positions? export const getNodesBounds = ( nodes: NodeBase[], params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false } @@ -161,6 +165,7 @@ export const getNodesBounds = ( const box = nodes.reduce( (currBox, node) => { + // @ts-expect-error const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin); return getBoundsOfBoxes( currBox, @@ -176,8 +181,47 @@ export const getNodesBounds = ( return boxToRect(box); }; -export const getNodesInside = ( - nodes: NodeType[], +export type GetInternalNodesBoundsParams = { + nodeOrigin?: NodeOrigin; + useRelativePosition?: boolean; + filter?: (node: NodeBase) => boolean; +}; + +/** + * Determines a bounding box that contains all given nodes in an array + * @internal + */ +export const getInternalNodesBounds = ( + nodeLookup: NodeLookup, + params: GetInternalNodesBoundsParams = { + nodeOrigin: [0, 0], + useRelativePosition: false, + } +): Rect => { + if (nodeLookup.size === 0) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + + let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }; + + nodeLookup.forEach((node) => { + if (params.filter == undefined || params.filter(node)) { + const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin); + box = getBoundsOfBoxes( + box, + rectToBox({ + ...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'], + ...getNodeDimensions(node), + }) + ); + } + }); + + return boxToRect(box); +}; + +export const getNodesInside = ( + nodeLookup: Map>, rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially = false, @@ -191,13 +235,15 @@ export const getNodesInside = ( height: rect.height / tScale, }; - const visibleNodes = nodes.reduce((res, node) => { + const visibleNodes: NodeType[] = []; + + for (const [, node] of nodeLookup) { const { computed, selectable = true, hidden = false } = node; const width = computed?.width ?? node.width ?? node.initialWidth ?? null; const height = computed?.height ?? node.height ?? node.initialHeight ?? null; if ((excludeNonSelectableNodes && !selectable) || hidden) { - return res; + continue; } const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin)); @@ -208,11 +254,9 @@ export const getNodesInside = ( const isVisible = notInitialized || partiallyVisible || overlappingArea >= area; if (isVisible || node.dragging) { - res.push(node); + visibleNodes.push(node); } - - return res; - }, []); + } return visibleNodes; }; @@ -236,17 +280,20 @@ export const getConnectedEdges = , Options extends FitViewOptionsBase>( - { nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params, + { nodeLookup, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params, options?: Options ) { - const filteredNodes = nodes.filter((n) => { + const filteredNodes: InternalNodeBase[] = []; + + nodeLookup.forEach((n) => { const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden); - if (options?.nodes?.length) { - return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id); + if ( + isVisible && + (!options?.nodes || (options?.nodes.length && options?.nodes.some((optionNode) => optionNode.id === n.id))) + ) { + filteredNodes.push(n); } - - return isVisible; }); if (filteredNodes.length > 0) { @@ -303,7 +350,7 @@ export function calculateNodePosition({ }: { nodeId: string; nextPosition: XYPosition; - nodeLookup: NodeLookup; + nodeLookup: NodeLookup>; nodeOrigin?: NodeOrigin; nodeExtent?: CoordinateExtent; onError?: OnError; @@ -313,16 +360,17 @@ export function calculateNodePosition({ const { x: parentX, y: parentY } = parentNode ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute : { x: 0, y: 0 }; + let currentExtent = clampNodeExtent(node, node.extent || nodeExtent); if (node.extent === 'parent' && !node.expandParent) { if (!parentNode) { onError?.('005', errorMessages['error005']()); } else { - const nodeWidth = node.computed?.width; - const nodeHeight = node.computed?.height; - const parentWidth = parentNode?.computed?.width; - const parentHeight = parentNode?.computed?.height; + const nodeWidth = node.computed.width; + const nodeHeight = node.computed.height; + const parentWidth = parentNode.computed.width; + const parentHeight = parentNode.computed.height; if (nodeWidth && nodeHeight && parentWidth && parentHeight) { const currNodeOrigin = node.origin || nodeOrigin; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index c04c7e51..21d5f9cf 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -1,4 +1,3 @@ -import { internalsSymbol } from '../constants'; import { NodeBase, CoordinateExtent, @@ -12,54 +11,53 @@ import { ConnectionLookup, EdgeBase, EdgeLookup, + InternalNodeBase, } from '../types'; import { getDimensions, getHandleBounds } from './dom'; import { isNumeric } from './general'; import { getNodePositionWithOrigin } from './graph'; -type ParentNodes = Record; +type ParentNodes = Set; export function updateAbsolutePositions( - nodes: NodeType[], - nodeLookup: Map, + nodeLookup: Map>, nodeOrigin: NodeOrigin = [0, 0], parentNodes?: ParentNodes ) { - return nodes.map((node) => { + for (const [, node] of nodeLookup) { if (node.parentNode && !nodeLookup.has(node.parentNode)) { throw new Error(`Parent node ${node.parentNode} not found`); } - if (node.parentNode || parentNodes?.[node.id]) { + if (node.parentNode || parentNodes?.has(node.id)) { const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null; const { x, y, z } = calculateXYZPosition( node, - nodes, nodeLookup, { ...node.position, - z: node[internalsSymbol]?.z ?? 0, + z: node.internals.z ?? 0, }, parentNode?.origin || nodeOrigin ); - const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y; - node.computed!.positionAbsolute = positionChanged + const positionChanged = x !== node.internals.positionAbsolute?.x || y !== node.internals.positionAbsolute?.y; + node.internals.positionAbsolute = positionChanged ? { x, y, } - : node.computed?.positionAbsolute; + : node.internals.positionAbsolute; - node[internalsSymbol]!.z = z; + node.internals.z = z; - if (parentNodes?.[node.id]) { - node[internalsSymbol]!.isParent = true; + if (parentNodes?.has(node.id)) { + node.internals.isParent = true; } } - return node; - }); + nodeLookup.set(node.id, node); + } } type UpdateNodesOptions = { @@ -70,64 +68,55 @@ type UpdateNodesOptions = { export function adoptUserProvidedNodes( nodes: NodeType[], - nodeLookup: Map, + nodeLookup: Map>, options: UpdateNodesOptions = { nodeOrigin: [0, 0] as NodeOrigin, elevateNodesOnSelect: true, defaults: {}, } -): NodeType[] { +) { const tmpLookup = new Map(nodeLookup); nodeLookup.clear(); - const parentNodes: ParentNodes = {}; + const parentNodes: ParentNodes = new Set(); const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; - const nextNodes = nodes.map((n) => { + nodes.forEach((n) => { const currentStoreNode = tmpLookup.get(n.id); - if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) { + if (n.parentNode) { + parentNodes.add(n.parentNode); + } + if (n === currentStoreNode?.internals?.userProvidedNode) { nodeLookup.set(n.id, currentStoreNode); return currentStoreNode; } - const node: NodeType = { + const node: InternalNodeBase = { ...options.defaults, ...n, computed: { - positionAbsolute: n.position, width: n.computed?.width, height: n.computed?.height, }, - }; - const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0); - const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol]; - - if (node.parentNode) { - parentNodes[node.parentNode] = true; - } - - Object.defineProperty(node, internalsSymbol, { - enumerable: false, - value: { - handleBounds: currInternals?.handleBounds, - z, + internals: { + positionAbsolute: n.position, + handleBounds: currentStoreNode?.internals?.handleBounds, + z: (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0), userProvidedNode: n, + isParent: false, }, - }); + }; nodeLookup.set(node.id, node); - - return node; }); - const nodesWithPositions = updateAbsolutePositions(nextNodes, nodeLookup, options.nodeOrigin, parentNodes); - - return nodesWithPositions; + if (parentNodes.size > 0) { + updateAbsolutePositions(nodeLookup, options.nodeOrigin, parentNodes); + } } function calculateXYZPosition( node: NodeType, - nodes: NodeType[], - nodeLookup: Map, + nodeLookup: Map>, result: XYZPosition, nodeOrigin: NodeOrigin ): XYZPosition { @@ -140,12 +129,11 @@ function calculateXYZPosition( return calculateXYZPosition( parentNode, - nodes, nodeLookup, { x: (result.x ?? 0) + parentNodePosition.x, y: (result.y ?? 0) + parentNodePosition.y, - z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0, + z: (parentNode.internals.z ?? 0) > (result.z ?? 0) ? parentNode.internals.z ?? 0 : result.z ?? 0, }, parentNode.origin || nodeOrigin ); @@ -153,25 +141,25 @@ function calculateXYZPosition( export function updateNodeDimensions( updates: Map, - nodes: NodeType[], - nodeLookup: Map, + nodeLookup: Map>, domNode: HTMLElement | null, nodeOrigin?: NodeOrigin, onUpdate?: (id: string, dimensions: Dimensions) => void -): NodeType[] | null { +): { hasUpdate: boolean } { const viewportNode = domNode?.querySelector('.xyflow__viewport'); + let hasUpdate = false; if (!viewportNode) { - return null; + return { hasUpdate }; } const style = window.getComputedStyle(viewportNode); const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); - const nextNodes = nodes.map((node) => { - const update = updates.get(node.id); + updates.forEach((update) => { + const node = nodeLookup.get(update.id); - if (update) { + if (node) { const dimensions = getDimensions(update.nodeElement); const doUpdate = !!( dimensions.width && @@ -180,6 +168,7 @@ export function updateNodeDimensions( ); if (doUpdate) { + hasUpdate = true; onUpdate?.(node.id, dimensions); const newNode = { @@ -188,25 +177,20 @@ export function updateNodeDimensions( ...node.computed, ...dimensions, }, - [internalsSymbol]: { - ...node[internalsSymbol], + internals: { + ...node.internals, handleBounds: { source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin), target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin), }, }, }; - nodeLookup.set(node.id, newNode); - - return newNode; } } - - return node; }); - return nextNodes; + return { hasUpdate }; } export function panBy({ diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index cfed09f9..7d2fbaef 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -26,13 +26,14 @@ import type { OnSelectionDrag, UpdateNodePositions, Box, + InternalNodeBase, } from '../types'; export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void; type StoreItems = { nodes: NodeBase[]; - nodeLookup: Map; + nodeLookup: Map; edges: EdgeBase[]; nodeExtent: CoordinateExtent; snapGrid: SnapGrid; @@ -130,19 +131,23 @@ export function XYDrag voi // if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node // based on its position so that the node stays at it's position relative to the selection. - const adjustedNodeExtent: CoordinateExtent = [ + let adjustedNodeExtent: CoordinateExtent = [ [nodeExtent[0][0], nodeExtent[0][1]], [nodeExtent[1][0], nodeExtent[1][1]], ]; if (dragItems.length > 1 && nodeExtent && !n.extent) { - adjustedNodeExtent[0][0] = n.computed.positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; - adjustedNodeExtent[1][0] = - n.computed.positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0]; + const { positionAbsolute } = n.internals; + const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; + const x2 = positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0]; - adjustedNodeExtent[0][1] = n.computed.positionAbsolute.y - nodesBox.y + nodeExtent[0][1]; - adjustedNodeExtent[1][1] = - n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1]; + const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1]; + const y2 = positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1]; + + adjustedNodeExtent = [ + [x1, y1], + [x2, y2], + ]; } const { position, positionAbsolute } = calculateNodePosition({ @@ -158,7 +163,7 @@ export function XYDrag voi hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y; n.position = position; - n.computed.positionAbsolute = positionAbsolute; + n.internals.positionAbsolute = positionAbsolute; return n; }); @@ -208,7 +213,6 @@ export function XYDrag voi function startDrag(event: UseDragEvent) { const { - nodes, nodeLookup, multiSelectionActive, nodesDraggable, @@ -236,7 +240,7 @@ export function XYDrag voi const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); lastPos = pointerPos; - dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId); + dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId); if (dragItems.length > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) { const [currentNode, currentNodes] = getEventHandlerParams({ diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts index 15c9af68..e958cf64 100644 --- a/packages/system/src/xydrag/utils.ts +++ b/packages/system/src/xydrag/utils.ts @@ -1,15 +1,15 @@ -import { type NodeDragItem, type XYPosition, NodeBase } from '../types'; +import { type NodeDragItem, type XYPosition, InternalNodeBase, NodeBase, NodeLookup } from '../types'; export function wrapSelectionDragFunc(selectionFunc?: (event: MouseEvent, nodes: NodeBase[]) => void) { return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes); } -export function isParentSelected(node: NodeType, nodes: NodeType[]): boolean { +export function isParentSelected(node: NodeType, nodeLookup: NodeLookup): boolean { if (!node.parentNode) { return false; } - const parentNode = nodes.find((node) => node.id === node.parentNode); + const parentNode = nodeLookup.get(node.parentNode); if (!parentNode) { return false; @@ -19,7 +19,7 @@ export function isParentSelected(node: NodeType, node return true; } - return isParentSelected(parentNode, nodes); + return isParentSelected(parentNode, nodeLookup); } export function hasSelector(target: Element, selector: string, domNode: Element): boolean { @@ -36,39 +36,43 @@ export function hasSelector(target: Element, selector: string, domNode: Element) // looks for all selected nodes and created a NodeDragItem for each of them export function getDragItems( - nodes: NodeType[], + nodeLookup: Map>, nodesDraggable: boolean, mousePos: XYPosition, nodeId?: string ): NodeDragItem[] { - return nodes - .filter( - (n) => - (n.selected || n.id === nodeId) && - (!n.parentNode || !isParentSelected(n, nodes)) && - (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')) - ) - .map((n) => ({ - id: n.id, - position: n.position || { x: 0, y: 0 }, - distance: { - x: mousePos.x - (n.computed?.positionAbsolute?.x ?? 0), - y: mousePos.y - (n.computed?.positionAbsolute?.y ?? 0), - }, - delta: { - x: 0, - y: 0, - }, - extent: n.extent, - parentNode: n.parentNode, - origin: n.origin, - expandParent: n.expandParent, - computed: { - positionAbsolute: n.computed?.positionAbsolute || { x: 0, y: 0 }, - width: n.computed?.width || 0, - height: n.computed?.height || 0, - }, - })); + const dragItems: NodeDragItem[] = []; + + for (const [id, node] of nodeLookup) { + if ( + (node.selected || node.id === nodeId) && + (!node.parentNode || !isParentSelected(node, nodeLookup)) && + (node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')) + ) { + const internalNode = nodeLookup.get(id)!; + + dragItems.push({ + id: internalNode.id, + position: internalNode.position || { x: 0, y: 0 }, + distance: { + x: mousePos.x - (internalNode.internals.positionAbsolute?.x ?? 0), + y: mousePos.y - (internalNode.internals.positionAbsolute?.y ?? 0), + }, + extent: internalNode.extent, + parentNode: internalNode.parentNode, + origin: internalNode.origin, + expandParent: internalNode.expandParent, + internals: { + positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 }, + }, + computed: { + width: internalNode.computed.width || 0, + height: internalNode.computed.height || 0, + }, + }); + } + } + return dragItems; } // returns two params: @@ -91,7 +95,6 @@ export function getEventHandlerParams({ position: n.position, computed: { ...n.computed, - positionAbsolute: n.computed.positionAbsolute, }, }; }); diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index 638ec966..80b09e61 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -6,13 +6,13 @@ import { type HandleType, type Connection, type PanBy, - type NodeBase, type Transform, type ConnectingHandle, type OnConnectEnd, type UpdateConnection, type IsValidConnection, type ConnectionHandle, + NodeLookup, } from '../types'; import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils'; @@ -25,7 +25,7 @@ export type OnPointerDownParams = { handleId: string | null; nodeId: string; isTarget: boolean; - nodes: NodeBase[]; + nodeLookup: NodeLookup; lib: string; flowId: string | null; edgeUpdaterType?: HandleType; @@ -79,7 +79,7 @@ function onPointerDown( edgeUpdaterType, isTarget, domNode, - nodes, + nodeLookup, lib, autoPanOnConnect, flowId, @@ -116,7 +116,7 @@ function onPointerDown( let handleDomNode: Element | null = null; const handleLookup = getHandleLookup({ - nodes, + nodeLookup, nodeId, handleId, handleType, diff --git a/packages/system/src/xyhandle/utils.ts b/packages/system/src/xyhandle/utils.ts index 8c82f6e3..346ebe45 100644 --- a/packages/system/src/xyhandle/utils.ts +++ b/packages/system/src/xyhandle/utils.ts @@ -3,15 +3,15 @@ import { type HandleType, type NodeHandleBounds, type XYPosition, - type NodeBase, type ConnectionHandle, + InternalNodeBase, + NodeLookup, } from '../types'; -import { internalsSymbol } from '../constants'; // this functions collects all handles and adds an absolute position // so that we can later find the closest handle to the mouse position export function getHandles( - node: NodeBase, + node: InternalNodeBase, handleBounds: NodeHandleBounds, type: HandleType, currentHandle: string @@ -22,8 +22,8 @@ export function getHandles( id: h.id || null, type, nodeId: node.id, - x: (node.computed?.positionAbsolute?.x ?? 0) + h.x + h.width / 2, - y: (node.computed?.positionAbsolute?.y ?? 0) + h.y + h.height / 2, + x: (node.internals.positionAbsolute.x ?? 0) + h.x + h.width / 2, + y: (node.internals.positionAbsolute.y ?? 0) + h.y + h.height / 2, }); } return res; @@ -62,28 +62,30 @@ export function getClosestHandle( } type GetHandleLookupParams = { - nodes: NodeBase[]; + nodeLookup: NodeLookup; nodeId: string; handleId: string | null; handleType: string; }; -export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) { - return nodes.reduce((res, node) => { - if (node[internalsSymbol]) { - const { handleBounds } = node[internalsSymbol]; - let sourceHandles: ConnectionHandle[] = []; - let targetHandles: ConnectionHandle[] = []; +export function getHandleLookup({ + nodeLookup, + nodeId, + handleId, + handleType, +}: GetHandleLookupParams): ConnectionHandle[] { + const connectionHandles: ConnectionHandle[] = []; - if (handleBounds) { - sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`); - targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`); - } - - res.push(...sourceHandles, ...targetHandles); + for (const [, node] of nodeLookup) { + if (node.internals.handleBounds) { + const id = `${nodeId}-${handleId}-${handleType}`; + const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id); + const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id); + connectionHandles.push(...sourceHandles, ...targetHandles); } - return res; - }, []); + } + + return connectionHandles; } export function getHandleType( From 67d979985de52b9466c80df2967160e69aec47f2 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 28 Mar 2024 17:08:05 +0100 Subject: [PATCH 2/9] refactor(react): handle parent expand in store instead of applyChanges --- .../src/components/NodeWrapper/index.tsx | 8 +- .../NodeRenderer/useResizeObserver.ts | 4 +- .../react/src/hooks/useUpdateNodeInternals.ts | 2 +- packages/react/src/index.ts | 2 +- packages/react/src/store/index.ts | 39 +++--- packages/react/src/utils/changes.ts | 56 +------- packages/system/src/types/changes.ts | 69 ++++++++++ packages/system/src/types/index.ts | 1 + packages/system/src/types/nodes.ts | 2 +- packages/system/src/utils/store.ts | 120 +++++++++++++++--- 10 files changed, 203 insertions(+), 100 deletions(-) create mode 100644 packages/system/src/types/changes.ts diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 5e2b91e2..e3fbf16c 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -88,9 +88,11 @@ export function NodeWrapper({ const moveSelectedNodes = useMoveSelectedNodes(); useEffect(() => { + const currNode = nodeRef.current; + return () => { - if (nodeRef.current) { - resizeObserver?.unobserve(nodeRef.current); + if (currNode) { + resizeObserver?.unobserve(currNode); } }; }, []); @@ -122,7 +124,7 @@ export function NodeWrapper({ if (targetPosChanged) { prevTargetPosition.current = node.targetPosition; } - store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]])); + store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, force: true }]])); } }, [id, nodeType, node.sourcePosition, node.targetPosition]); diff --git a/packages/react/src/container/NodeRenderer/useResizeObserver.ts b/packages/react/src/container/NodeRenderer/useResizeObserver.ts index f0d0a22e..82c079bd 100644 --- a/packages/react/src/container/NodeRenderer/useResizeObserver.ts +++ b/packages/react/src/container/NodeRenderer/useResizeObserver.ts @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef } from 'react'; import { ReactFlowState } from '../../types'; import { useStore } from '../../hooks/useStore'; +import { NodeDimensionUpdate } from '@xyflow/system'; const selector = (s: ReactFlowState) => s.updateNodeDimensions; @@ -15,14 +16,13 @@ export function useResizeObserver() { } const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => { - const updates = new Map(); + const updates = new Map(); entries.forEach((entry: ResizeObserverEntry) => { const id = entry.target.getAttribute('data-id') as string; updates.set(id, { id, nodeElement: entry.target as HTMLDivElement, - forceUpdate: true, }); }); diff --git a/packages/react/src/hooks/useUpdateNodeInternals.ts b/packages/react/src/hooks/useUpdateNodeInternals.ts index 1d7cc681..b156d506 100644 --- a/packages/react/src/hooks/useUpdateNodeInternals.ts +++ b/packages/react/src/hooks/useUpdateNodeInternals.ts @@ -21,7 +21,7 @@ export function useUpdateNodeInternals(): UpdateNodeInternals { 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 }); } }); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 800ebd94..91be19eb 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -28,7 +28,7 @@ export { useNodesData } from './hooks/useNodesData'; export { useConnection } from './hooks/useConnection'; 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'; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 3eddf5bc..b5a5effa 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -5,9 +5,10 @@ import { adoptUserProvidedNodes, updateAbsolutePositions, panBy as panBySystem, - Dimensions, updateNodeDimensions as updateNodeDimensionsSystem, updateConnectionLookup, + handleParentExpand, + NodeChange, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -16,12 +17,12 @@ import type { ReactFlowState, Node, Edge, - NodeDimensionChange, EdgeSelectionChange, NodeSelectionChange, NodePositionChange, UnselectNodesAndEdgesParams, FitViewOptions, + InternalNode, } from '../types'; const createRFStore = ({ @@ -91,23 +92,10 @@ const createRFStore = ({ nodeOrigin, debug, } = get(); - const changes: NodeDimensionChange[] = []; - const updatedNodes = updateNodeDimensionsSystem( - updates, - nodeLookup, - domNode, - nodeOrigin, - (id: string, dimensions: Dimensions) => { - changes.push({ - id: id, - type: 'dimensions', - dimensions, - }); - } - ); + const changes = updateNodeDimensionsSystem(updates, nodeLookup, domNode, nodeOrigin); - if (!updatedNodes) { + if (changes.length === 0) { return; } @@ -137,17 +125,30 @@ 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) => { + const internalNode = nodeLookup.get(node.id); + const change: NodeChange = { id: node.id, type: 'position', position: node.position, dragging, }; + if (internalNode?.expandParent) { + triggerChangeNodes.push(internalNode); + } + return change; }); + if (triggerChangeNodes.length > 0) { + const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup); + changes.push(...parentExpandChanges); + } + get().triggerNodeChanges(changes); }, triggerNodeChanges: (changes) => { diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 437c80dc..e4d0464d 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -10,51 +10,6 @@ import type { InternalNode, } 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; - } - } -} - // 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. // This function then applies the changes and returns the updated elements. @@ -111,7 +66,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); @@ -121,7 +76,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; @@ -137,9 +92,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any { element.dragging = change.dragging; } - if (element.expandParent) { - handleParentExpand(elements, element); - } break; } @@ -159,10 +111,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any { element.resizing = change.resizing; } - if (element.expandParent) { - handleParentExpand(elements, element); - } - break; } } diff --git a/packages/system/src/types/changes.ts b/packages/system/src/types/changes.ts new file mode 100644 index 00000000..d2da81cf --- /dev/null +++ b/packages/system/src/types/changes.ts @@ -0,0 +1,69 @@ +import type { XYPosition, Dimensions, NodeBase, EdgeBase } from '.'; + +export type NodeDimensionChange = { + id: string; + type: 'dimensions'; + dimensions?: Dimensions; + resizing?: boolean; +}; + +export type NodePositionChange = { + id: string; + type: 'position'; + position?: XYPosition; + positionAbsolute?: XYPosition; + dragging?: boolean; +}; + +export type NodeSelectionChange = { + id: string; + type: 'select'; + selected: boolean; +}; + +export type NodeRemoveChange = { + id: string; + type: 'remove'; +}; + +export type NodeAddChange = { + item: NodeType; + type: 'add'; +}; + +export type NodeReplaceChange = { + id: string; + item: NodeType; + type: 'replace'; +}; + +/** + * Union type of all possible node changes. + * @public + */ +export type NodeChange = + | NodeDimensionChange + | NodePositionChange + | NodeSelectionChange + | NodeRemoveChange + | NodeAddChange + | NodeReplaceChange; + +export type EdgeSelectionChange = NodeSelectionChange; +export type EdgeRemoveChange = NodeRemoveChange; +export type EdgeAddChange = { + item: EdgeType; + type: 'add'; +}; + +export type EdgeReplaceChange = { + id: string; + item: EdgeType; + type: 'replace'; +}; + +export type EdgeChange = + | EdgeSelectionChange + | EdgeRemoveChange + | EdgeAddChange + | EdgeReplaceChange; diff --git a/packages/system/src/types/index.ts b/packages/system/src/types/index.ts index 421a8531..99653baf 100644 --- a/packages/system/src/types/index.ts +++ b/packages/system/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './changes'; export * from './general'; export * from './nodes'; export * from './edges'; diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 6929321b..2d9dfd5d 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -110,7 +110,7 @@ export type NodeHandleBounds = { export type NodeDimensionUpdate = { id: string; nodeElement: HTMLDivElement; - forceUpdate?: boolean; + force?: boolean; }; export type NodeBounds = XYPosition & { diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 21d5f9cf..1c14d5f5 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -1,7 +1,6 @@ import { NodeBase, CoordinateExtent, - Dimensions, NodeDimensionUpdate, NodeOrigin, PanZoomInstance, @@ -12,9 +11,12 @@ import { EdgeBase, EdgeLookup, InternalNodeBase, + NodeChange, + NodeLookup, + Rect, } from '../types'; import { getDimensions, getHandleBounds } from './dom'; -import { isNumeric } from './general'; +import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general'; import { getNodePositionWithOrigin } from './graph'; type ParentNodes = Set; @@ -139,38 +141,102 @@ function calculateXYZPosition( ); } -export function updateNodeDimensions( - updates: Map, - nodeLookup: Map>, - domNode: HTMLElement | null, - nodeOrigin?: NodeOrigin, - onUpdate?: (id: string, dimensions: Dimensions) => void -): { hasUpdate: boolean } { - const viewportNode = domNode?.querySelector('.xyflow__viewport'); - let hasUpdate = false; +export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLookup): NodeChange[] { + const changes: NodeChange[] = []; + const chilNodeRects = new Map(); - if (!viewportNode) { - return { hasUpdate }; + nodes.forEach((node) => { + if (node.expandParent && node.parentNode) { + const parentNode = nodeLookup.get(node.parentNode); + + if (parentNode) { + const parentRect = chilNodeRects.get(node.parentNode) || nodeToRect(parentNode, node.origin); + const expandedRect = getBoundsOfRects(parentRect, nodeToRect(node, node.origin)); + chilNodeRects.set(node.parentNode, expandedRect); + } + } + }); + + if (chilNodeRects.size > 0) { + chilNodeRects.forEach((rect, id) => { + const origParent = nodeLookup.get(id)!; + const { position } = getNodePositionWithOrigin(origParent, origParent.origin); + const dimensions = getNodeDimensions(origParent); + + let xChange = null; + let yChange = null; + + if (rect.x < position.x || rect.y < position.y) { + xChange = Math.abs(position.x - rect.x); + yChange = Math.abs(position.y - rect.y); + + changes.push({ + id, + type: 'position', + position: { + x: position.x - xChange, + y: position.y - yChange, + }, + }); + + // @todo we need to reset child node positions if < 0 + } + + if (dimensions.width < rect.width || dimensions.height < rect.height) { + changes.push({ + id, + type: 'dimensions', + resizing: true, + dimensions: { + width: Math.max(dimensions.width, rect.width), + height: Math.max(dimensions.height, rect.height), + }, + }); + } + }); } + return changes; +} + +export function updateNodeDimensions( + updates: Map, + nodeLookup: Map, + domNode: HTMLElement | null, + nodeOrigin?: NodeOrigin +): NodeChange[] { + const viewportNode = domNode?.querySelector('.xyflow__viewport'); + + if (!viewportNode) { + return []; + } + + const changes: NodeChange[] = []; const style = window.getComputedStyle(viewportNode); const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); + // in this array we collect nodes, that might trigger changes (like expanding parent) + const triggerChangeNodes: NodeType[] = []; updates.forEach((update) => { const node = nodeLookup.get(update.id); - if (node) { + if (node?.hidden) { + nodeLookup.set(node.id, { + ...node, + internals: { + ...node.internals, + handleBounds: undefined, + }, + }); + } else if (node) { const dimensions = getDimensions(update.nodeElement); const doUpdate = !!( dimensions.width && dimensions.height && - (node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.forceUpdate) + (node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.force) ); if (doUpdate) { - hasUpdate = true; - onUpdate?.(node.id, dimensions); - const newNode = { ...node, computed: { @@ -185,12 +251,28 @@ export function updateNodeDimensions( }, }, }; + nodeLookup.set(node.id, newNode); + + changes.push({ + id: newNode.id, + type: 'dimensions', + dimensions, + }); + + if (newNode.expandParent) { + triggerChangeNodes.push(newNode); + } } } }); - return { hasUpdate }; + if (triggerChangeNodes.length > 0) { + const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup); + changes.push(...parentExpandChanges); + } + + return changes; } export function panBy({ From e409939234aaaa950a06064bbebe84a78fb6fdbb Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 2 Apr 2024 20:15:38 +0200 Subject: [PATCH 3/9] refactor(node/internals): cleanup --- packages/react/src/store/index.ts | 3 +- packages/system/src/utils/store.ts | 57 +++++++++++++++--------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index b5a5effa..9da8f142 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -19,7 +19,6 @@ import type { Edge, EdgeSelectionChange, NodeSelectionChange, - NodePositionChange, UnselectNodesAndEdgesParams, FitViewOptions, InternalNode, @@ -99,7 +98,7 @@ const createRFStore = ({ return; } - updateAbsolutePositions(nodeLookup, nodeOrigin); + updateAbsolutePositions(nodeLookup, { nodeOrigin }); // we call fitView once initially after all dimensions are set let nextFitViewDone = fitViewDone; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 1c14d5f5..c404c0f9 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -19,46 +19,45 @@ import { getDimensions, getHandleBounds } from './dom'; import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general'; import { getNodePositionWithOrigin } from './graph'; -type ParentNodes = Set; - export function updateAbsolutePositions( nodeLookup: Map>, - nodeOrigin: NodeOrigin = [0, 0], - parentNodes?: ParentNodes + options: UpdateNodesOptions = { + nodeOrigin: [0, 0] as NodeOrigin, + elevateNodesOnSelect: true, + defaults: {}, + }, + parentNodeIds?: Set ) { - for (const [, node] of nodeLookup) { - if (node.parentNode && !nodeLookup.has(node.parentNode)) { - throw new Error(`Parent node ${node.parentNode} not found`); + const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; + + for (const [id, node] of nodeLookup) { + const parentId = node.parentNode; + + if (parentId && !nodeLookup.has(parentId)) { + throw new Error(`Parent node ${parentId} not found`); } - if (node.parentNode || parentNodes?.has(node.id)) { - const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null; + if (node.parentNode || node.internals.isParent || parentNodeIds?.has(id)) { + const parentNode = parentId ? nodeLookup.get(parentId) : null; const { x, y, z } = calculateXYZPosition( node, nodeLookup, { ...node.position, - z: node.internals.z ?? 0, + z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0), }, - parentNode?.origin || nodeOrigin + parentNode?.origin || options.nodeOrigin ); - const positionChanged = x !== node.internals.positionAbsolute?.x || y !== node.internals.positionAbsolute?.y; - node.internals.positionAbsolute = positionChanged - ? { - x, - y, - } - : node.internals.positionAbsolute; + const currPosition = node.internals.positionAbsolute; + const positionChanged = x !== currPosition.x || y !== currPosition.y; + node.internals.positionAbsolute = positionChanged ? { x, y } : currPosition; node.internals.z = z; + node.internals.isParent = !!parentNodeIds?.has(id); - if (parentNodes?.has(node.id)) { - node.internals.isParent = true; - } + nodeLookup.set(id, node); } - - nodeLookup.set(node.id, node); } } @@ -79,14 +78,16 @@ export function adoptUserProvidedNodes( ) { const tmpLookup = new Map(nodeLookup); nodeLookup.clear(); - const parentNodes: ParentNodes = new Set(); const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; + const parentNodeIds = new Set(); nodes.forEach((n) => { const currentStoreNode = tmpLookup.get(n.id); + if (n.parentNode) { - parentNodes.add(n.parentNode); + parentNodeIds.add(n.parentNode); } + if (n === currentStoreNode?.internals?.userProvidedNode) { nodeLookup.set(n.id, currentStoreNode); return currentStoreNode; @@ -111,8 +112,8 @@ export function adoptUserProvidedNodes( nodeLookup.set(node.id, node); }); - if (parentNodes.size > 0) { - updateAbsolutePositions(nodeLookup, options.nodeOrigin, parentNodes); + if (parentNodeIds.size > 0) { + updateAbsolutePositions(nodeLookup, options, parentNodeIds); } } @@ -120,7 +121,7 @@ function calculateXYZPosition( node: NodeType, nodeLookup: Map>, result: XYZPosition, - nodeOrigin: NodeOrigin + nodeOrigin: NodeOrigin = [0, 0] ): XYZPosition { if (!node.parentNode) { return result; From 6753c19188251df201affa7934e5aa0fcfc431b5 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 3 Apr 2024 10:16:47 +0200 Subject: [PATCH 4/9] fix(parentExpand): prevent jumping --- packages/system/src/utils/store.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index c404c0f9..47b4f7f5 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -54,7 +54,10 @@ export function updateAbsolutePositions( node.internals.positionAbsolute = positionChanged ? { x, y } : currPosition; node.internals.z = z; - node.internals.isParent = !!parentNodeIds?.has(id); + + if (parentNodeIds !== undefined) { + node.internals.isParent = !!parentNodeIds?.has(id); + } nodeLookup.set(id, node); } @@ -164,12 +167,9 @@ export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLo const { position } = getNodePositionWithOrigin(origParent, origParent.origin); const dimensions = getNodeDimensions(origParent); - let xChange = null; - let yChange = null; - if (rect.x < position.x || rect.y < position.y) { - xChange = Math.abs(position.x - rect.x); - yChange = Math.abs(position.y - rect.y); + const xChange = Math.round(Math.abs(position.x - rect.x)); + const yChange = Math.round(Math.abs(position.y - rect.y)); changes.push({ id, @@ -180,10 +180,18 @@ export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLo }, }); - // @todo we need to reset child node positions if < 0 - } + changes.push({ + id, + type: 'dimensions', + resizing: true, + dimensions: { + width: dimensions.width + xChange, + height: dimensions.height + yChange, + }, + }); - if (dimensions.width < rect.width || dimensions.height < rect.height) { + // @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', From f45f50671d21b4c3804f9cb7cfcbb3134fbf763a Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 3 Apr 2024 12:54:55 +0200 Subject: [PATCH 5/9] refactor(react/internal-nodes): cleanup --- packages/react/src/store/index.ts | 14 +++++++++-- packages/system/src/utils/store.ts | 39 +++++++++++++++--------------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 9da8f142..843a2500 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -136,8 +136,18 @@ const createRFStore = ({ dragging, }; - if (internalNode?.expandParent) { - triggerChangeNodes.push(internalNode); + 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; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 47b4f7f5..b43534eb 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -91,28 +91,27 @@ export function adoptUserProvidedNodes( parentNodeIds.add(n.parentNode); } - if (n === currentStoreNode?.internals?.userProvidedNode) { + if (n === currentStoreNode?.internals.userProvidedNode) { nodeLookup.set(n.id, currentStoreNode); - return currentStoreNode; + } else { + const node: InternalNodeBase = { + ...options.defaults, + ...n, + computed: { + width: n.computed?.width, + height: n.computed?.height, + }, + internals: { + positionAbsolute: n.position, + handleBounds: currentStoreNode?.internals?.handleBounds, + z: (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0), + userProvidedNode: n, + isParent: false, + }, + }; + + nodeLookup.set(node.id, node); } - - const node: InternalNodeBase = { - ...options.defaults, - ...n, - computed: { - width: n.computed?.width, - height: n.computed?.height, - }, - internals: { - positionAbsolute: n.position, - handleBounds: currentStoreNode?.internals?.handleBounds, - z: (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0), - userProvidedNode: n, - isParent: false, - }, - }; - - nodeLookup.set(node.id, node); }); if (parentNodeIds.size > 0) { From ea18e46a1a0923318b27b3f2a2311c345cee399a Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 3 Apr 2024 14:54:24 +0200 Subject: [PATCH 6/9] refactor(nodes): rename computed to measured, add helpers --- .../cypress/components/hooks/useNodes.cy.tsx | 3 +- .../reactflow/on-nodes-change.cy.tsx | 2 +- .../components/utils/apply-changes.cy.ts | 6 +- .../react/src/examples/EasyConnect/utils.tsx | 10 +-- .../react/src/examples/FloatingEdges/utils.ts | 6 +- .../MiniMap/MiniMapNodes.tsx | 4 +- .../NodeResizer/NodeResizeControl.tsx | 6 +- .../NodeToolbar/NodeToolbar.tsx | 4 +- .../src/components/ConnectionLine/index.tsx | 4 +- packages/react/src/container/Pane/index.tsx | 4 +- packages/react/src/hooks/useInternalNode.ts | 21 ++++++ packages/react/src/hooks/useReactFlow.ts | 21 ++---- packages/react/src/hooks/useViewportHelper.ts | 4 +- packages/react/src/hooks/useVisibleNodeIds.ts | 6 +- packages/react/src/index.ts | 1 + packages/react/src/store/index.ts | 42 +++++------ packages/react/src/store/initialState.ts | 4 +- packages/react/src/types/changes.ts | 71 ------------------- packages/react/src/types/general.ts | 4 +- packages/react/src/types/index.ts | 1 - packages/react/src/types/store.ts | 4 +- packages/react/src/utils/changes.ts | 17 +++-- packages/svelte/src/lib/store/utils.ts | 4 +- packages/system/src/types/general.ts | 2 +- packages/system/src/types/nodes.ts | 14 ++-- packages/system/src/utils/edges/positions.ts | 2 +- packages/system/src/utils/general.ts | 20 +++--- packages/system/src/utils/graph.ts | 34 ++++----- packages/system/src/utils/store.ts | 40 +++++------ packages/system/src/xydrag/XYDrag.ts | 4 +- packages/system/src/xydrag/utils.ts | 10 +-- packages/system/src/xyresizer/XYResizer.ts | 10 +-- 32 files changed, 159 insertions(+), 226 deletions(-) create mode 100644 packages/react/src/hooks/useInternalNode.ts delete mode 100644 packages/react/src/types/changes.ts diff --git a/examples/react/cypress/components/hooks/useNodes.cy.tsx b/examples/react/cypress/components/hooks/useNodes.cy.tsx index 9c10ad70..340b4b10 100644 --- a/examples/react/cypress/components/hooks/useNodes.cy.tsx +++ b/examples/react/cypress/components/hooks/useNodes.cy.tsx @@ -15,8 +15,7 @@ const initialNodes: Node[] = nodes.map((n) => ({ const expectedNodes: Node[] = initialNodes.map((n) => ({ ...n, - computed: { - positionAbsolute: n.position, + measured: { ...nodeDimensions, }, })); diff --git a/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx b/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx index a15fc67e..bceb4cbd 100644 --- a/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx +++ b/examples/react/cypress/components/reactflow/on-nodes-change.cy.tsx @@ -48,7 +48,7 @@ describe(': onNodesChange', () => { id: '1', item: { ...nodes[0], - computed: { positionAbsolute: nodes[0].position, width: 200, height: 100 }, + measured: { width: 200, height: 100 }, style: { width: 200, height: 100 }, }, }, diff --git a/examples/react/cypress/components/utils/apply-changes.cy.ts b/examples/react/cypress/components/utils/apply-changes.cy.ts index 1548b7e9..0a75db7b 100644 --- a/examples/react/cypress/components/utils/apply-changes.cy.ts +++ b/examples/react/cypress/components/utils/apply-changes.cy.ts @@ -136,8 +136,8 @@ describe('applyChanges Testing', () => { ]; const nextNodes = applyNodeChanges(nodeChanges, nodes); - expect(nodes[0].computed).to.be.undefined; - expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight }); + expect(nodes[0].measured).to.be.undefined; + expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight }); expect(nextNodes[0].width).to.be.undefined; expect(nextNodes[0].height).to.be.undefined; }); @@ -153,7 +153,7 @@ describe('applyChanges Testing', () => { const nextNodes = applyNodeChanges(nodeChanges, nodes); expect(nextNodes[0].position).to.be.deep.equal(newPosition); - expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight }); + expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight }); }); it('replaces nodes/edges', () => { diff --git a/examples/react/src/examples/EasyConnect/utils.tsx b/examples/react/src/examples/EasyConnect/utils.tsx index 6d87795b..c6693015 100644 --- a/examples/react/src/examples/EasyConnect/utils.tsx +++ b/examples/react/src/examples/EasyConnect/utils.tsx @@ -9,8 +9,8 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) { width: intersectionNodeWidth, height: intersectionNodeHeight, positionAbsolute: intersectionNodePosition, - } = intersectionNode.computed || {}; - const targetPosition = targetNode.computed?.positionAbsolute!; + } = intersectionNode.measured || {}; + const targetPosition = targetNode.measured?.positionAbsolute!; const w = intersectionNodeWidth! / 2; const h = intersectionNodeHeight! / 2; @@ -33,7 +33,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) { // returns the position (top,right,bottom or right) passed node compared to the intersection point function getEdgePosition(node: Node, intersectionPoint: XYPosition) { - const n = { ...node.computed?.positionAbsolute, ...node }; + const n = { ...node.measured?.positionAbsolute, ...node }; const nx = Math.round(n.x!); const ny = Math.round(n.y!); const px = Math.round(intersectionPoint.x); @@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) { if (px <= nx + 1) { return Position.Left; } - if (px >= nx + n.computed?.width! - 1) { + if (px >= nx + n.measured?.width! - 1) { return Position.Right; } if (py <= ny + 1) { return Position.Top; } - if (py >= n.y! + n.computed?.height! - 1) { + if (py >= n.y! + n.measured?.height! - 1) { return Position.Bottom; } diff --git a/examples/react/src/examples/FloatingEdges/utils.ts b/examples/react/src/examples/FloatingEdges/utils.ts index a022207c..08593c5c 100644 --- a/examples/react/src/examples/FloatingEdges/utils.ts +++ b/examples/react/src/examples/FloatingEdges/utils.ts @@ -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; } 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 6c0236d8..1fc6f25c 100644 --- a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx +++ b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx @@ -12,8 +12,8 @@ import type { NodeToolbarProps } from './types'; const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) => a?.internals.positionAbsolute.x !== b?.internals.positionAbsolute.x || a?.internals.positionAbsolute.y !== b?.internals.positionAbsolute.y || - a?.computed.width !== b?.computed.width || - a?.computed.height !== b?.computed.height || + a?.measured.width !== b?.measured.width || + a?.measured.height !== b?.measured.height || a?.selected !== b?.selected || a?.internals.z !== b?.internals.z; diff --git a/packages/react/src/components/ConnectionLine/index.tsx b/packages/react/src/components/ConnectionLine/index.tsx index 86655232..54b3f24d 100644 --- a/packages/react/src/components/ConnectionLine/index.tsx +++ b/packages/react/src/components/ConnectionLine/index.tsx @@ -64,8 +64,8 @@ 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 fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2; + const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0; const fromX = (fromNode.internals.positionAbsolute.x ?? 0) + fromHandleX; const fromY = (fromNode.internals.positionAbsolute.y ?? 0) + fromHandleY; const fromPosition = fromHandle?.position; diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index ad2b01ed..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; 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/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 4e83448a..3a022b15 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -1,12 +1,5 @@ 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'; @@ -32,7 +25,7 @@ export function useReactFlow>((id) => { - return store.getState().nodeLookup.get(id)?.internals.userProvidedNode as NodeType; + return store.getState().nodeLookup.get(id)?.internals.userNode as NodeType; }, []); const getEdges = useCallback>(() => { @@ -223,13 +216,9 @@ export function useReactFlow { - const node = - isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect) - ? nodeOrRect - : (store.getState().nodeLookup.get(nodeOrRect.id)?.internals.userProvidedNode 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>( 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 91be19eb..e7e6b37f 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -26,6 +26,7 @@ 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 } from './utils/changes'; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 843a2500..3a5686b2 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, updateNodeDimensions as updateNodeDimensionsSystem, updateConnectionLookup, handleParentExpand, NodeChange, + EdgeSelectionChange, + NodeSelectionChange, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import getInitialState from './initialState'; -import type { - ReactFlowState, - Node, - Edge, - EdgeSelectionChange, - NodeSelectionChange, - UnselectNodesAndEdgesParams, - FitViewOptions, - InternalNode, -} from '../types'; +import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions, InternalNode } from '../types'; const createRFStore = ({ nodes, @@ -52,7 +45,7 @@ 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. - adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); + adoptUserNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); set({ nodes }); }, @@ -263,21 +256,22 @@ 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 => { diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 9f373719..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); - adoptUserProvidedNodes(storeNodes, nodeLookup, { + adoptUserNodes(storeNodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false, }); diff --git a/packages/react/src/types/changes.ts b/packages/react/src/types/changes.ts deleted file mode 100644 index adcb484d..00000000 --- a/packages/react/src/types/changes.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { XYPosition, Dimensions } from '@xyflow/system'; - -import type { Node, Edge } from '.'; - -export type NodeDimensionChange = { - id: string; - type: 'dimensions'; - dimensions?: Dimensions; - resizing?: boolean; -}; - -export type NodePositionChange = { - id: string; - type: 'position'; - position?: XYPosition; - positionAbsolute?: XYPosition; - dragging?: boolean; -}; - -export type NodeSelectionChange = { - id: string; - type: 'select'; - selected: boolean; -}; - -export type NodeRemoveChange = { - id: string; - type: 'remove'; -}; - -export type NodeAddChange = { - item: NodeType; - type: 'add'; -}; - -export type NodeReplaceChange = { - id: string; - item: NodeType; - type: 'replace'; -}; - -/** - * Union type of all possible node changes. - * @public - */ -export type NodeChange = - | NodeDimensionChange - | NodePositionChange - | NodeSelectionChange - | NodeRemoveChange - | NodeAddChange - | NodeReplaceChange; - -export type EdgeSelectionChange = NodeSelectionChange; -export type EdgeRemoveChange = NodeRemoveChange; -export type EdgeAddChange = { - item: EdgeType; - type: 'add'; -}; - -export type EdgeReplaceChange = { - id: string; - item: EdgeType; - type: 'replace'; -}; - -export type EdgeChange = - | EdgeSelectionChange - | EdgeRemoveChange - | EdgeAddChange - | EdgeReplaceChange; 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/store.ts b/packages/react/src/types/store.ts index eed7b9a3..416f56f3 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -25,12 +25,13 @@ import { type EdgeLookup, type ConnectionLookup, type NodeLookup, + NodeChange, + EdgeChange, } from '@xyflow/system'; import type { Edge, Node, - NodeChange, OnNodesChange, OnEdgesChange, DefaultEdgeOptions, @@ -43,7 +44,6 @@ import type { OnNodeDrag, OnBeforeDelete, IsValidConnection, - EdgeChange, InternalNode, } from '.'; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index e4d0464d..44af2451 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -1,14 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { EdgeLookup, NodeLookup } from '@xyflow/system'; -import type { - Node, - Edge, +import { + EdgeLookup, + NodeLookup, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange, - InternalNode, -} from '../types'; +} 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. @@ -97,9 +96,9 @@ function applyChange(change: any, element: any): any { 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; diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index 0f72d746..a6ba38de 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -7,7 +7,7 @@ import { type Writable } from 'svelte/store'; import { - adoptUserProvidedNodes, + adoptUserNodes, updateConnectionLookup, type Viewport, type PanZoomInstance, @@ -141,7 +141,7 @@ export const createNodesStore = ( let elevateNodesOnSelect = true; const _set = (nds: Node[]): Node[] => { - const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, { + const nextNodes = adoptUserNodes(nds, nodeLookup, { elevateNodesOnSelect, defaults }); diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index a7c6665b..2fd0e3ab 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -127,7 +127,7 @@ export type SelectionRect = Rect & { export type OnError = (id: string, message: string) => void; -export type UpdateNodePositions = (dragItems: NodeDragItem[] | NodeBase[], dragging?: boolean) => void; +export type UpdateNodePositions = (dragItems: NodeDragItem[] | InternalNodeBase[], dragging?: boolean) => void; export type PanBy = (delta: XYPosition) => boolean; export type UpdateConnection = (params: { diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 2d9dfd5d..7b85fef8 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -59,27 +59,25 @@ export type NodeBase< */ origin?: NodeOrigin; handles?: NodeHandle[]; - computed?: { + measured?: { width?: number; height?: number; }; }; export type InternalNodeBase = NodeType & { - computed: { + measured: { width?: number; height?: number; }; - // Only used internally 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 - * (which may lack some fields, like `computed` or `[internalSymbol]`. Used - * as an optimization to avoid certain operations. */ - userProvidedNode: NodeType; + /** Holds a reference to the original node object provided by the user. + * Used as an optimization to avoid certain operations. */ + userNode: NodeType; handleBounds?: NodeHandleBounds; }; }; @@ -123,7 +121,7 @@ export type NodeDragItem = { position: XYPosition; // distance from the mouse cursor to the node when start dragging distance: XYPosition; - computed: { + measured: { width: number | null; height: number | null; }; diff --git a/packages/system/src/utils/edges/positions.ts b/packages/system/src/utils/edges/positions.ts index 33fa59ee..de8416db 100644 --- a/packages/system/src/utils/edges/positions.ts +++ b/packages/system/src/utils/edges/positions.ts @@ -20,7 +20,7 @@ function isNodeInitialized(node: InternalNodeBase): boolean { return ( node && !!(node.internals.handleBounds || node.handles?.length) && - !!(node.computed.width || node.width || node.initialWidth) + !!(node.measured.width || node.width || node.initialWidth) ); } diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 5fab4f49..b8af19bc 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -66,23 +66,23 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({ height: y2 - y, }); -export const nodeToRect = (node: InternalNodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => { +export const nodeToRect = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => { const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); return { ...positionAbsolute, - width: node.computed?.width ?? node.width ?? 0, - height: node.computed?.height ?? node.height ?? 0, + width: node.measured?.width ?? node.width ?? 0, + height: node.measured?.height ?? node.height ?? 0, }; }; -export const nodeToBox = (node: InternalNodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => { +export const nodeToBox = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => { const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin); return { ...positionAbsolute, - x2: positionAbsolute.x + (node.computed?.width ?? node.width ?? 0), - y2: positionAbsolute.y + (node.computed?.height ?? node.height ?? 0), + x2: positionAbsolute.x + (node.measured?.width ?? node.width ?? 0), + y2: positionAbsolute.y + (node.measured?.height ?? node.height ?? 0), }; }; @@ -208,14 +208,14 @@ export function getNodeDimensions( node: NodeType ): { width: number; height: number } { return { - width: node.computed?.width ?? node.width ?? node.initialWidth ?? 0, - height: node.computed?.height ?? node.height ?? node.initialHeight ?? 0, + width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0, + height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0, }; } export function nodeHasDimensions(node: NodeType): boolean { return ( - (node.computed?.width ?? node.width ?? node.initialWidth) !== undefined && - (node.computed?.height ?? node.height ?? node.initialHeight) !== undefined + (node.measured?.width ?? node.width ?? node.initialWidth) !== undefined && + (node.measured?.height ?? node.height ?? node.initialHeight) !== undefined ); } diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 9df39893..c53add40 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -50,7 +50,7 @@ export const isNodeBase = (element: any): export const isInternalNodeBase = ( element: any -): element is NodeType => 'id' in element && 'computed' in element && !('source' in element) && !('target' in element); +): 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 @@ -106,7 +106,7 @@ export const getIncomers = { if (!node) { @@ -133,10 +133,13 @@ export const getNodePositionWithOrigin = ( return { position, - positionAbsolute: { - x: node.internals.positionAbsolute.x - offsetX, - y: node.internals.positionAbsolute.y - offsetY, - }, + positionAbsolute: + 'internals' in node + ? { + x: node.internals.positionAbsolute.x - offsetX, + y: node.internals.positionAbsolute.y - offsetY, + } + : position, }; }; @@ -165,7 +168,6 @@ export const getNodesBounds = ( const box = nodes.reduce( (currBox, node) => { - // @ts-expect-error const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin); return getBoundsOfBoxes( currBox, @@ -238,9 +240,9 @@ export const getNodesInside = ( const visibleNodes: NodeType[] = []; for (const [, node] of nodeLookup) { - const { computed, selectable = true, hidden = false } = node; - const width = computed?.width ?? node.width ?? node.initialWidth ?? null; - const height = computed?.height ?? node.height ?? node.initialHeight ?? null; + const { 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) { continue; @@ -286,7 +288,7 @@ export function fitView, Options exte const filteredNodes: InternalNodeBase[] = []; nodeLookup.forEach((n) => { - const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden); + const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden); if ( isVisible && @@ -331,7 +333,7 @@ function clampNodeExtent( if (!extent || extent === 'parent') { return extent; } - return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]]; + return [extent[0], [extent[1][0] - (node.measured?.width ?? 0), extent[1][1] - (node.measured?.height ?? 0)]]; } /** @@ -367,10 +369,10 @@ export function calculateNodePosition({ if (!parentNode) { onError?.('005', errorMessages['error005']()); } else { - const nodeWidth = node.computed.width; - const nodeHeight = node.computed.height; - const parentWidth = parentNode.computed.width; - const parentHeight = parentNode.computed.height; + const nodeWidth = node.measured.width; + const nodeHeight = node.measured.height; + const parentWidth = parentNode.measured.width; + const parentHeight = parentNode.measured.height; if (nodeWidth && nodeHeight && parentWidth && parentHeight) { const currNodeOrigin = node.origin || nodeOrigin; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index b43534eb..e82e2dea 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -70,7 +70,7 @@ type UpdateNodesOptions = { defaults?: Partial; }; -export function adoptUserProvidedNodes( +export function adoptUserNodes( nodes: NodeType[], nodeLookup: Map>, options: UpdateNodesOptions = { @@ -84,33 +84,31 @@ export function adoptUserProvidedNodes( const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; const parentNodeIds = new Set(); - nodes.forEach((n) => { - const currentStoreNode = tmpLookup.get(n.id); + nodes.forEach((userNode) => { + const currentStoreNode = tmpLookup.get(userNode.id); - if (n.parentNode) { - parentNodeIds.add(n.parentNode); + if (userNode.parentNode) { + parentNodeIds.add(userNode.parentNode); } - if (n === currentStoreNode?.internals.userProvidedNode) { - nodeLookup.set(n.id, currentStoreNode); + if (userNode === currentStoreNode?.internals.userNode) { + nodeLookup.set(userNode.id, currentStoreNode); } else { - const node: InternalNodeBase = { + nodeLookup.set(userNode.id, { ...options.defaults, - ...n, - computed: { - width: n.computed?.width, - height: n.computed?.height, + ...userNode, + measured: { + width: userNode.measured?.width, + height: userNode.measured?.height, }, internals: { - positionAbsolute: n.position, + positionAbsolute: userNode.position, handleBounds: currentStoreNode?.internals?.handleBounds, - z: (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0), - userProvidedNode: n, + z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0), + userNode, isParent: false, }, - }; - - nodeLookup.set(node.id, node); + }); } }); @@ -241,14 +239,14 @@ export function updateNodeDimensions( const doUpdate = !!( dimensions.width && dimensions.height && - (node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.force) + (node.measured?.width !== dimensions.width || node.measured?.height !== dimensions.height || update.force) ); if (doUpdate) { const newNode = { ...node, - computed: { - ...node.computed, + measured: { + ...node.measured, ...dimensions, }, internals: { diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 7d2fbaef..573b87af 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -139,10 +139,10 @@ export function XYDrag voi if (dragItems.length > 1 && nodeExtent && !n.extent) { const { positionAbsolute } = n.internals; const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; - const x2 = positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0]; + const x2 = positionAbsolute.x + (n.measured?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0]; const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1]; - const y2 = positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1]; + const y2 = positionAbsolute.y + (n.measured?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1]; adjustedNodeExtent = [ [x1, y1], diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts index e958cf64..e4570b6e 100644 --- a/packages/system/src/xydrag/utils.ts +++ b/packages/system/src/xydrag/utils.ts @@ -65,9 +65,9 @@ export function getDragItems( internals: { positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 }, }, - computed: { - width: internalNode.computed.width || 0, - height: internalNode.computed.height || 0, + measured: { + width: internalNode.measured.width || 0, + height: internalNode.measured.height || 0, }, }); } @@ -93,8 +93,8 @@ export function getEventHandlerParams({ return { ...node, position: n.position, - computed: { - ...n.computed, + measured: { + ...n.measured, }, }; }); diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index 1953b327..ce1bbd12 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -71,15 +71,15 @@ export type XYResizerInstance = { function nodeToParentExtent(node: NodeBase): CoordinateExtent { return [ [0, 0], - [node.computed!.width!, node.computed!.height!], + [node.measured!.width!, node.measured!.height!], ]; } function nodeToChildExtent(child: NodeBase, parent: NodeBase, nodeOrigin: NodeOrigin): CoordinateExtent { const x = parent.position.x + child.position.x; const y = parent.position.y + child.position.y; - const width = child.computed!.width! ?? 0; - const height = child.computed!.height! ?? 0; + const width = child.measured!.width! ?? 0; + const height = child.measured!.height! ?? 0; const originOffsetX = nodeOrigin[0] * width; const originOffsetY = nodeOrigin[1] * height; @@ -121,8 +121,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); prevValues = { - width: node.computed?.width ?? 0, - height: node.computed?.height ?? 0, + width: node.measured?.width ?? 0, + height: node.measured?.height ?? 0, x: node.position.x ?? 0, y: node.position.y ?? 0, }; From 48ad9d3ccf67132101655302a90b92a1c3f55488 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 3 Apr 2024 15:33:52 +0200 Subject: [PATCH 7/9] feat(react): add internal node helper --- packages/react/src/hooks/useReactFlow.ts | 23 ++++++++++++++++------- packages/react/src/types/instance.ts | 10 +++++++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 3a022b15..6be3f90b 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -3,7 +3,7 @@ import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type 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'; @@ -20,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)?.internals.userNode 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(); @@ -299,6 +306,7 @@ export function useReactFlow = { 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. * From 4570b2494bcbeffc8278bdf93d21fe5419a855e6 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 8 Apr 2024 17:32:49 +0200 Subject: [PATCH 8/9] chore(react/store): cleanup --- examples/react/src/examples/Basic/index.tsx | 29 +++++++++++++------ .../src/components/StoreUpdater/index.tsx | 1 - packages/react/src/store/index.ts | 5 ++-- 3 files changed, 22 insertions(+), 13 deletions(-) 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/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 { const { - nodes, onNodesChange, fitView, nodeLookup, @@ -98,7 +97,7 @@ const createRFStore = ({ if (!fitViewDone && fitViewOnInit) { nextFitViewDone = fitView({ ...fitViewOnInitOptions, - nodes: fitViewOnInitOptions?.nodes || nodes, + nodes: fitViewOnInitOptions?.nodes, }); } @@ -107,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, fitViewDone: nextFitViewDone }); + set({ fitViewDone: nextFitViewDone }); if (changes?.length > 0) { if (debug) { From 3e59ab94f008e2d82dbb910836a37d6a50b010f8 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 9 Apr 2024 13:26:54 +0200 Subject: [PATCH 9/9] refactor(nodes): rename parentNode to parentId --- .../react/src/examples/NodeResizer/index.tsx | 6 +++--- examples/react/src/examples/Subflow/index.tsx | 16 +++++++------- .../routes/examples/node-resizer/+page.svelte | 4 ++-- .../src/routes/examples/subflows/+page.svelte | 12 +++++------ packages/react/src/store/index.ts | 1 + packages/system/src/types/nodes.ts | 4 ++-- packages/system/src/utils/graph.ts | 4 ++-- packages/system/src/utils/store.ts | 21 ++++++++++--------- packages/system/src/xydrag/utils.ts | 8 +++---- packages/system/src/xyresizer/XYResizer.ts | 4 ++-- 10 files changed, 41 insertions(+), 39 deletions(-) diff --git a/examples/react/src/examples/NodeResizer/index.tsx b/examples/react/src/examples/NodeResizer/index.tsx index b0a272f2..7afb411f 100644 --- a/examples/react/src/examples/NodeResizer/index.tsx +++ b/examples/react/src/examples/NodeResizer/index.tsx @@ -137,7 +137,7 @@ const initialNodes: Node[] = [ label: 'Child with extent: parent', }, position: { x: 50, y: 50 }, - parentNode: '5', + parentId: '5', extent: 'parent', width: 50, height: 100, @@ -148,7 +148,7 @@ const initialNodes: Node[] = [ type: 'defaultResizer', data: { label: 'Child with expandParent' }, position: { x: 150, y: 100 }, - parentNode: '5', + parentId: '5', expandParent: true, style: { ...nodeStyle }, }, @@ -157,7 +157,7 @@ const initialNodes: Node[] = [ type: 'defaultResizer', data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true }, position: { x: 25, y: 200 }, - parentNode: '5', + parentId: '5', expandParent: true, style: { ...nodeStyle }, }, diff --git a/examples/react/src/examples/Subflow/index.tsx b/examples/react/src/examples/Subflow/index.tsx index 8ea28e34..7f1b3f4b 100644 --- a/examples/react/src/examples/Subflow/index.tsx +++ b/examples/react/src/examples/Subflow/index.tsx @@ -50,7 +50,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 4a' }, position: { x: 15, y: 15 }, className: 'light', - parentNode: '4', + parentId: '4', origin: [0.5, 0.5], extent: [ @@ -68,21 +68,21 @@ const initialNodes: Node[] = [ height: 200, width: 300, }, - parentNode: '4', + parentId: '4', }, { id: '4b1', data: { label: 'Node 4b1' }, position: { x: 40, y: 20 }, className: 'light', - parentNode: '4b', + parentId: '4b', }, { id: '4b2', data: { label: 'Node 4b2' }, position: { x: 20, y: 100 }, className: 'light', - parentNode: '4b', + parentId: '4b', }, { id: '5', @@ -98,7 +98,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 5a' }, position: { x: 0, y: 0 }, className: 'light', - parentNode: '5', + parentId: '5', extent: 'parent', }, { @@ -106,7 +106,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 5b' }, position: { x: 225, y: 50 }, className: 'light', - parentNode: '5', + parentId: '5', expandParent: true, }, { @@ -160,7 +160,7 @@ const Subflow = () => { const updatePos = () => { setNodes((nds) => { return nds.map((n) => { - if (!n.parentNode) { + if (!n.parentId) { return { ...n, position: { @@ -194,7 +194,7 @@ const Subflow = () => { return nds.map((n) => { return { ...n, - hidden: !!n.parentNode && !n.hidden, + hidden: !!n.parentId && !n.hidden, }; }); }); diff --git a/examples/svelte/src/routes/examples/node-resizer/+page.svelte b/examples/svelte/src/routes/examples/node-resizer/+page.svelte index b7479b03..8e08174a 100644 --- a/examples/svelte/src/routes/examples/node-resizer/+page.svelte +++ b/examples/svelte/src/routes/examples/node-resizer/+page.svelte @@ -113,7 +113,7 @@ type: 'defaultResizer', data: { label: 'Child with extent parent' }, position: { x: 50, y: 50 }, - parentNode: '5', + parentId: '5', extent: 'parent', style: nodeStyle }, @@ -122,7 +122,7 @@ type: 'defaultResizer', data: { label: 'Child' }, position: { x: 100, y: 100 }, - parentNode: '5', + parentId: '5', style: nodeStyle } ]); diff --git a/examples/svelte/src/routes/examples/subflows/+page.svelte b/examples/svelte/src/routes/examples/subflows/+page.svelte index 4c57c171..a4574a6e 100644 --- a/examples/svelte/src/routes/examples/subflows/+page.svelte +++ b/examples/svelte/src/routes/examples/subflows/+page.svelte @@ -36,7 +36,7 @@ id: '4a', data: { label: 'Node 4a' }, position: { x: 15, y: 15 }, - parentNode: '4', + parentId: '4', extent: [ [0, 0], [100, 100] @@ -47,19 +47,19 @@ data: { label: 'Node 4b' }, position: { x: 100, y: 60 }, style: 'width: 300px; height: 200px;', - parentNode: '4' + parentId: '4' }, { id: '4b1', data: { label: 'Node 4b1' }, position: { x: 40, y: 20 }, - parentNode: '4b' + parentId: '4b' }, { id: '4b2', data: { label: 'Node 4b2' }, position: { x: 20, y: 100 }, - parentNode: '4b' + parentId: '4b' }, { id: '5', @@ -73,14 +73,14 @@ id: '5a', data: { label: 'Node 5a' }, position: { x: 0, y: 0 }, - parentNode: '5', + parentId: '5', extent: 'parent' }, { id: '5b', data: { label: 'Node 5b' }, position: { x: 225, y: 50 }, - parentNode: '5', + parentId: '5', expandParent: true }, { diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 4b2c0e95..492d6186 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -120,6 +120,7 @@ const createRFStore = ({ 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, diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 7b85fef8..c8c85a55 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -43,7 +43,7 @@ export type NodeBase< initialWidth?: number; initialHeight?: number; /** Parent node id, used for creating sub-flows */ - parentNode?: string; + parentId?: string; zIndex?: number; /** Boundary a node can be moved in * @example 'parent' or [[0, 0], [100, 100]] @@ -129,7 +129,7 @@ export type NodeDragItem = { positionAbsolute: XYPosition; }; extent?: 'parent' | CoordinateExtent; - parentNode?: string; + parentId?: string; dragging?: boolean; origin?: NodeOrigin; expandParent?: boolean; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index c53add40..273a0057 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -358,7 +358,7 @@ export function calculateNodePosition({ onError?: OnError; }): { position: XYPosition; positionAbsolute: XYPosition } { const node = nodeLookup.get(nodeId)!; - const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : undefined; + const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined; const { x: parentX, y: parentY } = parentNode ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute : { x: 0, y: 0 }; @@ -440,7 +440,7 @@ export async function getElementsToRemove n.id === node.parentNode); + const parentHit = !isIncluded && node.parentId && matchingNodes.find((n) => n.id === node.parentId); if (isIncluded || parentHit) { matchingNodes.push(node); diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index e82e2dea..7ab5504a 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -31,13 +31,13 @@ export function updateAbsolutePositions( const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; for (const [id, node] of nodeLookup) { - const parentId = node.parentNode; + const parentId = node.parentId; if (parentId && !nodeLookup.has(parentId)) { throw new Error(`Parent node ${parentId} not found`); } - if (node.parentNode || node.internals.isParent || parentNodeIds?.has(id)) { + if (parentId || node.internals.isParent || parentNodeIds?.has(id)) { const parentNode = parentId ? nodeLookup.get(parentId) : null; const { x, y, z } = calculateXYZPosition( node, @@ -87,8 +87,8 @@ export function adoptUserNodes( nodes.forEach((userNode) => { const currentStoreNode = tmpLookup.get(userNode.id); - if (userNode.parentNode) { - parentNodeIds.add(userNode.parentNode); + if (userNode.parentId) { + parentNodeIds.add(userNode.parentId); } if (userNode === currentStoreNode?.internals.userNode) { @@ -123,11 +123,11 @@ function calculateXYZPosition( result: XYZPosition, nodeOrigin: NodeOrigin = [0, 0] ): XYZPosition { - if (!node.parentNode) { + if (!node.parentId) { return result; } - const parentNode = nodeLookup.get(node.parentNode)!; + const parentNode = nodeLookup.get(node.parentId)!; const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin); return calculateXYZPosition( @@ -147,13 +147,14 @@ export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLo const chilNodeRects = new Map(); nodes.forEach((node) => { - if (node.expandParent && node.parentNode) { - const parentNode = nodeLookup.get(node.parentNode); + const parentId = node.parentId; + if (node.expandParent && parentId) { + const parentNode = nodeLookup.get(parentId); if (parentNode) { - const parentRect = chilNodeRects.get(node.parentNode) || nodeToRect(parentNode, node.origin); + const parentRect = chilNodeRects.get(parentId) || nodeToRect(parentNode, node.origin); const expandedRect = getBoundsOfRects(parentRect, nodeToRect(node, node.origin)); - chilNodeRects.set(node.parentNode, expandedRect); + chilNodeRects.set(parentId, expandedRect); } } }); diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts index e4570b6e..adead2b7 100644 --- a/packages/system/src/xydrag/utils.ts +++ b/packages/system/src/xydrag/utils.ts @@ -5,11 +5,11 @@ export function wrapSelectionDragFunc(selectionFunc?: (event: MouseEvent, nodes: } export function isParentSelected(node: NodeType, nodeLookup: NodeLookup): boolean { - if (!node.parentNode) { + if (!node.parentId) { return false; } - const parentNode = nodeLookup.get(node.parentNode); + const parentNode = nodeLookup.get(node.parentId); if (!parentNode) { return false; @@ -46,7 +46,7 @@ export function getDragItems( for (const [id, node] of nodeLookup) { if ( (node.selected || node.id === nodeId) && - (!node.parentNode || !isParentSelected(node, nodeLookup)) && + (!node.parentId || !isParentSelected(node, nodeLookup)) && (node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')) ) { const internalNode = nodeLookup.get(id)!; @@ -59,7 +59,7 @@ export function getDragItems( y: mousePos.y - (internalNode.internals.positionAbsolute?.y ?? 0), }, extent: internalNode.extent, - parentNode: internalNode.parentNode, + parentId: internalNode.parentId, origin: internalNode.origin, expandParent: internalNode.expandParent, internals: { diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index ce1bbd12..50a3e4aa 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -136,7 +136,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize parentNode = undefined; if (node.extent === 'parent' || node.expandParent) { - parentNode = nodeLookup.get(node.parentNode!); + parentNode = nodeLookup.get(node.parentId!); if (parentNode && node.extent === 'parent') { parentExtent = nodeToParentExtent(parentNode); } @@ -148,7 +148,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize childExtent = undefined; for (const [childId, child] of nodeLookup) { - if (child.parentNode === nodeId) { + if (child.parentId === nodeId) { childNodes.push({ id: childId, position: { ...child.position },