diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index 0b5c574d..82683ecc 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -35,14 +35,14 @@ const initialNodes: Node[] = [ { id: '4a', data: { label: 'Node 4a' }, - position: { x: 115, y: 215 }, + position: { x: 15, y: 15 }, className: 'light', parentNode: '4', }, { id: '4b', data: { label: 'Node 4b' }, - position: { x: 250, y: 250 }, + position: { x: 80, y: 80 }, className: 'light', style: { backgroundColor: 'rgba(255, 0, 0, .2)' }, parentNode: '4', @@ -52,14 +52,14 @@ const initialNodes: Node[] = [ { id: '4b1', data: { label: 'Node 4b1' }, - position: { x: 270, y: 270 }, + position: { x: 20, y: 20 }, className: 'light', parentNode: '4b', }, { id: '4b2', data: { label: 'Node 4b2' }, - position: { x: 500, y: 400 }, + position: { x: 100, y: 100 }, className: 'light', parentNode: '4b', }, diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index aa195e18..f59d1eb6 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -211,7 +211,7 @@ export default (NodeComponent: ComponentType) => { if (nodeElement.current && (!isHidden || !isInitialized)) { updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); } - }, [id, isHidden, sourcePosition, targetPosition, isInitialized]); + }, [id, isHidden, sourcePosition, targetPosition, type, isInitialized]); useEffect(() => { if (nodeElement.current) { diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index dc8e960b..141b5342 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; import MarkerDefinitions from './MarkerDefinitions'; -import { getEdgePositions, getHandle, getSourceTargetNodes } from './utils'; +import { getEdgePositions, getHandle } from './utils'; import { Position, Edge, @@ -17,6 +17,7 @@ import { NodeHandleBounds, } from '../../types'; import useVisibleEdges from '../../hooks/useVisibleEdges'; +import useNodeLookup from '../../hooks/useNodeLookup'; interface EdgeRendererProps { edgeTypes: any; @@ -232,8 +233,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => { width, height, connectionMode, - nodes, } = useStore(selector, shallow); + const nodeLookup = useNodeLookup(); const edges = useVisibleEdges(props.onlyRenderVisibleElements); @@ -250,20 +251,22 @@ const EdgeRenderer = (props: EdgeRendererProps) => { {edges.map((edge: Edge) => { // @todo: getSourceTargetNodes is called many times during dragging/creating edges - const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); + const sourceNode = nodeLookup.current.get(edge.source); + const targetNode = nodeLookup.current.get(edge.target); + return ( ({ updateNodeDimensions: s.updateNodeDimensions, snapGrid: s.snapGrid, snapToGrid: s.snapToGrid, + nodeLookup: s.nodeLookup, }); interface NodesProps extends NodeRendererProps { - nodes: Node[]; + nodes: NodeRendererNode[]; + nodeLookup: NodeLookup; isDraggable?: boolean; resizeObserver: ResizeObserver | null; scale: number; @@ -43,13 +54,17 @@ interface NodesProps extends NodeRendererProps { parentId?: string; } -interface NodeProps extends Omit { +interface NodeProps extends Omit { node: Node; nodeType: string; + childNodes?: NodeRendererNode[]; + positionAbsoluteX?: number; + positionAbsoluteY?: number; } function Node({ node, + childNodes, nodeType, isDraggable, resizeObserver, @@ -60,6 +75,8 @@ function Node({ nodesConnectable, elementsSelectable, recursionDepth, + positionAbsoluteX, + positionAbsoluteY, ...props }: NodeProps) { // const onNodesChange = useStore((s) => s.onNodesChange); @@ -76,7 +93,7 @@ function Node({ typeof node.width !== 'undefined' && typeof node.height !== 'undefined'; - const isParentNode = !!node.childNodes?.length; + const isParentNode = !!childNodes?.length; return ( { + return nodes.map(({ node, childNodes }) => { const nodeType = node.type || 'default'; + const lookupNode = nodeLookup.get(node.id); if (!props.nodeTypes[nodeType]) { console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); @@ -143,6 +162,7 @@ function Nodes({ - {node.childNodes && node.childNodes.length > 0 && ( + {childNodes && childNodes.length > 0 && ( { snapGrid, snapToGrid, } = useStore(selector, shallow); + const nodeLookup = useNodeLookup(); + const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const transformStyle = useMemo( @@ -215,6 +240,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
{ - child.childNodes = getChildNodes(remaining, child); + child.childNodes = getChildNodes(remaining, child.node); return child; }); } diff --git a/src/store/index.ts b/src/store/index.ts index 2c5ab2db..9f6e7b2b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -26,6 +26,8 @@ import { OnEdgesChange, EdgeChange, NodeDimensionChange, + NodeLookup, + NodeLookupItem, } from '../types'; import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; @@ -39,36 +41,53 @@ const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | E }); // @todo needs refactoring / improvements -function findMatchingNodes(id: string | undefined, nodes: Node[]): Node[] { - if (!id) { - return nodes.filter((n) => !!n.isSelected); +// function findMatchingNodes(id: string | undefined, nodes: Node[]): Node[] { +// if (!id) { +// return nodes.filter((n) => !!n.isSelected); +// } + +// const result = []; +// const children = []; + +// for (let i = 0; i < nodes.length; i++) { +// const n = nodes[i]; + +// if (n.id === id) { +// result.push(n); +// } + +// if (n.parentNode === id) { +// children.push(n); +// } +// } + +// for (let i = 0; i < children.length; i++) { +// const n = children[i]; +// const matches = findMatchingNodes(n.id, nodes); + +// for (let j = 0; j < matches.length; j++) { +// result.push(matches[j]); +// } +// } + +// return result; +// } + +function addPositions(posA: XYPosition, posB: XYPosition): XYPosition { + return { + x: (posA.x ?? 0) + (posB.x ?? 0), + y: (posA.y ?? 0) + (posB.y ?? 0), + }; +} + +function getAbsolutePosition(node: NodeLookupItem, nodeLookup: NodeLookup, result: XYPosition): XYPosition { + const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : false; + + if (!parentNode) { + return result; } - const result = []; - const children = []; - - for (let i = 0; i < nodes.length; i++) { - const n = nodes[i]; - - if (n.id === id) { - result.push(n); - } - - if (n.parentNode === id) { - children.push(n); - } - } - - for (let i = 0; i < children.length; i++) { - const n = children[i]; - const matches = findMatchingNodes(n.id, nodes); - - for (let j = 0; j < matches.length; j++) { - result.push(matches[j]); - } - } - - return result; + return getAbsolutePosition(parentNode, nodeLookup, addPositions(result, parentNode.position || { x: 0, y: 0 })); } const createStore = () => @@ -127,36 +146,45 @@ const createStore = () => reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', - setNodes: (propNodes: Node[]) => { - const { nodes } = get(); + nodeLookup: new Map(), - const nextNodes = propNodes.map((propNode: Node) => { - const storeNode = nodes.find((node) => node.id === propNode.id); + setNodes: (nodes: Node[]) => { + const { nodeLookup } = get(); - if (storeNode) { - if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { - // we reset the elements dimensions here in order to force a re-calculation of the bounds. - // When the type of a node changes it is possible that the number or positions of handles changes too. - return { - ...propNode, - width: null, - height: null, - }; - } + nodes.forEach((node) => { + const lookupNode = { + ...nodeLookup.get(node.id), + width: node.width || null, + height: node.height || null, + position: node.position, + positionAbsolute: node.position, + }; + if (node.parentNode) { + lookupNode.parentNode = node.parentNode; } - - return propNode; + nodeLookup.set(node.id, lookupNode); }); - set({ - nodes: nextNodes, - }); + nodes + .filter((node) => node.parentNode) + .forEach((node) => { + const positionAbsolute = getAbsolutePosition(node, nodeLookup, node.position); + + if (positionAbsolute) { + nodeLookup.set(node.id, { + ...nodeLookup.get(node.id), + positionAbsolute, + }); + } + }); + + set({ nodes }); }, setEdges: (edges: Edge[]) => { set({ edges }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { - const { onNodesChange, nodes, transform } = get(); + const { onNodesChange, nodes, transform, nodeLookup } = get(); const nodesToChange: NodeChange[] = updates.reduce((res, update) => { const node = nodes.find((n) => n.id === update.id); @@ -170,6 +198,8 @@ const createStore = () => if (doUpdate) { const handleBounds = getHandleBounds(update.nodeElement, transform[2]); + nodeLookup.set(node.id, { ...nodeLookup.get(node.id), handleBounds }); + const change = { id: node.id, type: 'dimensions', @@ -189,13 +219,13 @@ const createStore = () => const { onNodesChange, nodes, nodeExtent } = get(); if (onNodesChange) { - const matchingNodes = findMatchingNodes(id, nodes); + const matchingNodes = nodes.filter((n) => !!n.isSelected || n.id === id); if (matchingNodes?.length) { onNodesChange( - matchingNodes.map((n) => { + matchingNodes.map((node) => { const change: NodeDimensionChange = { - id: n.id, + id: node.id, type: 'dimensions', isDragging: !!isDragging, }; @@ -204,12 +234,12 @@ const createStore = () => change.position = nodeExtent ? clampPosition( { - x: n.position.x + diff.x, - y: n.position.y + diff.y, + x: node.position.x + diff.x, + y: node.position.y + diff.y, }, nodeExtent ) - : { x: n.position.x + diff.x, y: n.position.y + diff.y }; + : { x: node.position.x + diff.x, y: node.position.y + diff.y }; } return change; diff --git a/src/types/index.ts b/src/types/index.ts index 9a84ae20..52d4e109 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -458,11 +458,23 @@ export type InitD3ZoomPayload = { export type OnNodesChange = (nodes: NodeChange[]) => void; export type OnEdgesChange = (nodes: EdgeChange[]) => void; +export type NodeLookupItem = { + width?: number | null; + height?: number | null; + parentNode?: ElementId; + position?: XYPosition; + positionAbsolute?: XYPosition; + handleBounds?: NodeHandleBounds; +}; + +export type NodeLookup = Map; + export interface ReactFlowState { width: number; height: number; transform: Transform; nodes: Node[]; + nodeLookup: NodeLookup; edges: Edge[]; selectedNodesBbox: Rect; onNodesChange: OnNodesChange | null; @@ -542,3 +554,8 @@ export interface ReactFlowState { export type UpdateNodeInternals = (nodeId: ElementId) => void; export type OnSelectionChangeFunc = (params: { nodes: Node[]; edges: Edge[] }) => void; + +export type NodeRendererNode = { + childNodes?: NodeRendererNode[]; + node: Node; +};