From 410b12946d10a157189bb0b7c242487831b6b6e8 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 19 Oct 2021 11:09:49 +0200 Subject: [PATCH] refactor(state): separate nodes and edges --- example/src/Basic/index.tsx | 14 +- example/src/Stress/index.tsx | 4 +- example/src/UpdateNode/index.tsx | 4 +- src/components/ConnectionLine/index.tsx | 8 +- src/components/Edges/wrapEdge.tsx | 6 +- src/components/ElementUpdater/index.tsx | 31 --- src/components/Nodes/wrapNode.tsx | 58 +++--- src/components/NodesSelection/index.tsx | 12 +- src/components/SelectionListener/index.tsx | 4 +- src/components/StoreUpdater/index.tsx | 211 +++++++++++++++++++++ src/container/EdgeRenderer/index.tsx | 138 +++++++++----- src/container/EdgeRenderer/utils.ts | 32 +--- src/container/FlowRenderer/index.tsx | 4 +- src/container/GraphView/index.tsx | 175 ++--------------- src/container/NodeRenderer/index.tsx | 40 ++-- src/container/ReactFlow/Wrapper.tsx | 27 ++- src/container/ReactFlow/index.tsx | 62 +++--- src/container/ZoomPane/index.tsx | 15 +- src/hooks/useGlobalKeyHandler.ts | 13 +- src/store/index.ts | 168 ++++++++-------- src/types/index.ts | 69 +++++-- src/utils/graph.ts | 76 ++++---- 22 files changed, 634 insertions(+), 537 deletions(-) delete mode 100644 src/components/ElementUpdater/index.tsx create mode 100644 src/components/StoreUpdater/index.tsx diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index fad5f17d..f47d372a 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -7,16 +7,17 @@ import ReactFlow, { applyEdgeChanges, MiniMap, Controls, - FlowElement, Node, Edge, - ElementChange, + NodeChange, + EdgeChange, OnLoadParams, Connection, } from 'react-flow-renderer'; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); -const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); +const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); +const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const initialNodes: Node[] = [ { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, @@ -66,11 +67,11 @@ const BasicFlow = () => { }); }; - const onNodesChange = useCallback((changes: ElementChange[]) => { + const onNodesChange = useCallback((changes: NodeChange[]) => { setNodes((ns) => applyNodeChanges(changes, ns)); }, []); - const onEdgesChange = useCallback((changes: ElementChange[]) => { + const onEdgesChange = useCallback((changes: EdgeChange[]) => { setEdges((es) => applyEdgeChanges(changes, es)); }, []); @@ -81,7 +82,8 @@ const BasicFlow = () => { onLoad={onLoad} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onElementClick={onElementClick} + onNodeClick={onNodeClick} + onEdgeClick={onEdgeClick} onConnect={onConnect} onNodeDragStop={onNodeDragStop} className="react-flow-basic-example" diff --git a/example/src/Stress/index.tsx b/example/src/Stress/index.tsx index 2b27158b..f3cf1df2 100644 --- a/example/src/Stress/index.tsx +++ b/example/src/Stress/index.tsx @@ -6,7 +6,7 @@ import ReactFlow, { OnLoadParams, Edge, Node, - ElementChange, + NodeChange, applyNodeChanges, } from 'react-flow-renderer'; @@ -48,7 +48,7 @@ const StressFlow = () => { setEdges(initialElements.edges); }; - const onNodesChange = useCallback((changes: ElementChange[]) => { + const onNodesChange = useCallback((changes: NodeChange[]) => { setNodes((ns) => applyNodeChanges(changes, ns)); }, []); diff --git a/example/src/UpdateNode/index.tsx b/example/src/UpdateNode/index.tsx index a4c10abe..8a4269aa 100644 --- a/example/src/UpdateNode/index.tsx +++ b/example/src/UpdateNode/index.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, useCallback } from 'react'; -import ReactFlow, { Node, Edge, applyNodeChanges, ElementChange } from 'react-flow-renderer'; +import ReactFlow, { Node, Edge, applyNodeChanges, NodeChange } from 'react-flow-renderer'; import './updatenode.css'; @@ -60,7 +60,7 @@ const UpdateNode = () => { ); }, [nodeHidden]); - const onNodesChange = useCallback((changes: ElementChange[]) => { + const onNodesChange = useCallback((changes: NodeChange[]) => { setNodes((ns) => applyNodeChanges(changes, ns)); }, []); diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index da1ed174..926ed692 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -52,15 +52,15 @@ export default ({ setSourceNode(nextSourceNode); }, []); - if (!sourceNode || !isConnectable) { + if (!sourceNode || !isConnectable || !sourceNode.handleBounds?.[connectionHandleType]) { return null; } const sourceHandle = handleId - ? sourceNode.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId) - : sourceNode.handleBounds[connectionHandleType][0]; + ? sourceNode.handleBounds[connectionHandleType]!.find((d: HandleElement) => d.id === handleId) + : sourceNode.handleBounds[connectionHandleType]![0]; const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.width! / 2; - const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height; + const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.height!; const sourceX = sourceNode.position.x + sourceHandleX; const sourceY = sourceNode.position.y + sourceHandleY; diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index ef934e01..8bb9caae 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -23,7 +23,7 @@ export default (EdgeComponent: ComponentType) => { data, onClick, onEdgeDoubleClick, - selected, + isSelected, animated, label, labelStyle, @@ -69,7 +69,7 @@ export default (EdgeComponent: ComponentType) => { 'react-flow__edge', `react-flow__edge-${type}`, className, - { selected, animated, inactive, updating }, + { selected: isSelected, animated, inactive, updating }, ]); const edgeElement = useMemo(() => { @@ -219,7 +219,7 @@ export default (EdgeComponent: ComponentType) => { id={id} source={source} target={target} - selected={selected} + isSelected={isSelected} animated={animated} label={label} labelStyle={labelStyle} diff --git a/src/components/ElementUpdater/index.tsx b/src/components/ElementUpdater/index.tsx deleted file mode 100644 index df6f3745..00000000 --- a/src/components/ElementUpdater/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect } from 'react'; -import shallow from 'zustand/shallow'; - -import { useStore } from '../../store'; -import { Node, Edge, ReactFlowState } from '../../types'; - -interface ElementUpdaterProps { - nodes: Node[]; - edges: Edge[]; -} - -const selector = (s: ReactFlowState) => ({ - setNodes: s.setNodes, - setEdges: s.setEdges, -}); - -const ElementUpdater = ({ nodes, edges }: ElementUpdaterProps) => { - const { setNodes, setEdges } = useStore(selector, shallow); - - useEffect(() => { - setNodes(nodes); - }, [nodes]); - - useEffect(() => { - setEdges(edges); - }, [edges]); - - return null; -}; - -export default ElementUpdater; diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index d1b9be3c..ebf1106b 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -9,9 +9,9 @@ import { NodeComponentProps, WrapNodeProps, ReactFlowState } from '../../types'; const selector = (s: ReactFlowState) => ({ addSelectedElements: s.addSelectedElements, - onNodesChange: s.onNodesChange, unsetNodesSelection: s.unsetNodesSelection, - updateNodePosDiff: s.updateNodePosDiff, + updateNodePosition: s.updateNodePosition, + updateNodeDimensions: s.updateNodeDimensions, }); export default (NodeComponent: ComponentType) => { @@ -22,7 +22,7 @@ export default (NodeComponent: ComponentType) => { scale, xPos, yPos, - selected, + isSelected, onClick, onMouseEnter, onMouseMove, @@ -48,8 +48,10 @@ export default (NodeComponent: ComponentType) => { resizeObserver, dragHandle, }: WrapNodeProps) => { - // const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); - const { addSelectedElements, onNodesChange, unsetNodesSelection, updateNodePosDiff } = useStore(selector, shallow); + const { addSelectedElements, unsetNodesSelection, updateNodePosition, updateNodeDimensions } = useStore( + selector, + shallow + ); const nodeElement = useRef(null); const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); @@ -57,7 +59,7 @@ export default (NodeComponent: ComponentType) => { const nodeStyle: CSSProperties = useMemo( () => ({ - zIndex: selected ? 10 : 3, + zIndex: isSelected ? 10 : 3, transform: `translate(${xPos}px,${yPos}px)`, pointerEvents: isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none', @@ -66,7 +68,7 @@ export default (NodeComponent: ComponentType) => { ...style, }), [ - selected, + isSelected, xPos, yPos, isSelectable, @@ -118,7 +120,7 @@ export default (NodeComponent: ComponentType) => { if (isSelectable) { unsetNodesSelection(); - if (!selected) { + if (!isSelected) { addSelectedElements([node]); } } @@ -126,7 +128,7 @@ export default (NodeComponent: ComponentType) => { onClick?.(event, node); } }, - [isSelectable, selected, isDraggable, onClick, node] + [isSelectable, isSelected, isDraggable, onClick, node] ); const onDragStart = useCallback( @@ -136,15 +138,15 @@ export default (NodeComponent: ComponentType) => { if (selectNodesOnDrag && isSelectable) { unsetNodesSelection(); - if (!selected) { + if (!isSelected) { addSelectedElements([node]); } - } else if (!selectNodesOnDrag && !selected && isSelectable) { + } else if (!selectNodesOnDrag && !isSelected && isSelectable) { unsetNodesSelection(); addSelectedElements([]); } }, - [node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart] + [node, isSelected, selectNodesOnDrag, isSelectable, onNodeDragStart] ); const onDrag = useCallback( @@ -156,7 +158,7 @@ export default (NodeComponent: ComponentType) => { onNodeDrag(event as MouseEvent, node); } - updateNodePosDiff({ id, isDragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } }); + updateNodePosition({ id, isDragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } }); }, [id, node, onNodeDrag] ); @@ -166,7 +168,7 @@ export default (NodeComponent: ComponentType) => { // onDragStop also gets called when user just clicks on a node. // Because of that we set dragging to true inside the onDrag handler and handle the click here if (!isDragging) { - if (isSelectable && !selectNodesOnDrag && !selected) { + if (isSelectable && !selectNodesOnDrag && !isSelected) { addSelectedElements([node]); } @@ -175,18 +177,14 @@ export default (NodeComponent: ComponentType) => { return; } - onNodesChange?.([ - { - id: node.id, - change: { - isDragging: true, - }, - }, - ]); + updateNodePosition({ + id: node.id, + isDragging: false, + }); onNodeDragStop?.(event as MouseEvent, node); }, - [node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected] + [node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, isSelected] ); const onNodeDoubleClickHandler = useCallback( @@ -196,11 +194,11 @@ export default (NodeComponent: ComponentType) => { [node, onNodeDoubleClick] ); - // useEffect(() => { - // if (nodeElement.current && !isHidden) { - // updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); - // } - // }, [id, isHidden, sourcePosition, targetPosition]); + useEffect(() => { + if (nodeElement.current && !isHidden && !isInitialized) { + updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); + } + }, [id, isHidden, sourcePosition, targetPosition, isInitialized]); useEffect(() => { if (nodeElement.current) { @@ -220,7 +218,7 @@ export default (NodeComponent: ComponentType) => { `react-flow__node-${type}`, className, { - selected, + selected: isSelected, selectable: isSelectable, }, ]); @@ -257,7 +255,7 @@ export default (NodeComponent: ComponentType) => { type={type} xPos={xPos} yPos={yPos} - selected={selected} + isSelected={isSelected} isConnectable={isConnectable} sourcePosition={sourcePosition} targetPosition={targetPosition} diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 4518be9e..5cc8e7ac 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -20,10 +20,10 @@ const selector = (s: ReactFlowState) => ({ transform: s.transform, selectedNodesBbox: s.selectedNodesBbox, selectionActive: s.selectionActive, - selectedNodes: s.nodes.filter((n) => n.selected), + selectedNodes: s.nodes.filter((n) => n.isSelected), snapToGrid: s.snapToGrid, snapGrid: s.snapGrid, - updateNodePosDiff: s.updateNodePosDiff, + updateNodePosition: s.updateNodePosition, }); export default ({ @@ -32,7 +32,7 @@ export default ({ onSelectionDragStop, onSelectionContextMenu, }: NodesSelectionProps) => { - const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosDiff } = + const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(selector); const [tX, tY, tScale] = transform; const nodeRef = useRef(null); @@ -69,7 +69,7 @@ export default ({ onSelectionDrag(event, selectedNodes); } - updateNodePosDiff({ + updateNodePosition({ diff: { x: data.deltaX, y: data.deltaY, @@ -77,12 +77,12 @@ export default ({ isDragging: true, }); }, - [onSelectionDrag, selectedNodes, updateNodePosDiff] + [onSelectionDrag, selectedNodes, updateNodePosition] ); const onStop = useCallback( (event: MouseEvent) => { - updateNodePosDiff({ + updateNodePosition({ isDragging: false, }); diff --git a/src/components/SelectionListener/index.tsx b/src/components/SelectionListener/index.tsx index 250f9ecb..a99a95a0 100644 --- a/src/components/SelectionListener/index.tsx +++ b/src/components/SelectionListener/index.tsx @@ -9,8 +9,8 @@ interface SelectionListenerProps { } const selectedElementsSelector = (s: ReactFlowState) => [ - ...s.nodes.filter((n) => n.selected), - ...s.edges.filter((e) => e.selected), + ...s.nodes.filter((n) => n.isSelected), + ...s.edges.filter((e) => e.isSelected), ]; // This is just a helper component for calling the onSelectionChange listener. diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx new file mode 100644 index 00000000..235b7b08 --- /dev/null +++ b/src/components/StoreUpdater/index.tsx @@ -0,0 +1,211 @@ +import { useEffect } from 'react'; +import shallow from 'zustand/shallow'; + +import { useStore } from '../../store'; +import { + Node, + Edge, + ReactFlowState, + OnConnectFunc, + OnConnectStartFunc, + OnConnectStopFunc, + OnConnectEndFunc, + NodeExtent, + OnNodesChange, + OnEdgesChange, + ConnectionMode, + SnapGrid, + TranslateExtent, +} from '../../types'; + +interface StoreUpdaterProps { + nodes: Node[]; + edges: Edge[]; + onConnect?: OnConnectFunc; + onConnectStart?: OnConnectStartFunc; + onConnectStop?: OnConnectStopFunc; + onConnectEnd?: OnConnectEndFunc; + nodesDraggable?: boolean; + nodesConnectable?: boolean; + minZoom?: number; + maxZoom?: number; + nodeExtent?: NodeExtent; + onNodesChange?: OnNodesChange; + onEdgesChange?: OnEdgesChange; + elementsSelectable?: boolean; + connectionMode?: ConnectionMode; + snapToGrid?: boolean; + snapGrid?: SnapGrid; + translateExtent?: TranslateExtent; +} + +const selector = (s: ReactFlowState) => ({ + setNodes: s.setNodes, + setEdges: s.setEdges, + setOnConnect: s.setOnConnect, + setOnConnectStart: s.setOnConnectStart, + setOnConnectStop: s.setOnConnectStop, + setOnConnectEnd: s.setOnConnectEnd, + setSnapGrid: s.setSnapGrid, + setSnapToGrid: s.setSnapToGrid, + setNodesDraggable: s.setNodesDraggable, + setNodesConnectable: s.setNodesConnectable, + setElementsSelectable: s.setElementsSelectable, + setMinZoom: s.setMinZoom, + setMaxZoom: s.setMaxZoom, + setTranslateExtent: s.setTranslateExtent, + setNodeExtent: s.setNodeExtent, + setConnectionMode: s.setConnectionMode, + setOnNodesChange: s.setOnNodesChange, + setOnEdgesChange: s.setOnEdgesChange, +}); + +const StoreUpdater = ({ + nodes, + edges, + onConnect, + onConnectStart, + onConnectStop, + onConnectEnd, + nodesDraggable, + nodesConnectable, + minZoom, + maxZoom, + nodeExtent, + onNodesChange, + onEdgesChange, + elementsSelectable, + connectionMode, + snapGrid, + snapToGrid, + translateExtent, +}: StoreUpdaterProps) => { + const { + setNodes, + setEdges, + setOnConnect, + setOnConnectStart, + setOnConnectStop, + setOnConnectEnd, + setNodesDraggable, + setSnapGrid, + setSnapToGrid, + setNodesConnectable, + setElementsSelectable, + setMinZoom, + setMaxZoom, + setTranslateExtent, + setNodeExtent, + setOnNodesChange, + setOnEdgesChange, + setConnectionMode, + } = useStore(selector, shallow); + + useEffect(() => { + setNodes(nodes); + }, [nodes]); + + useEffect(() => { + setEdges(edges, nodes); + }, [edges, nodes]); + + useEffect(() => { + if (onConnect) { + setOnConnect(onConnect); + } + }, [onConnect]); + + useEffect(() => { + if (onConnectStart) { + setOnConnectStart(onConnectStart); + } + }, [onConnectStart]); + + useEffect(() => { + if (onConnectStop) { + setOnConnectStop(onConnectStop); + } + }, [onConnectStop]); + + useEffect(() => { + if (onConnectEnd) { + setOnConnectEnd(onConnectEnd); + } + }, [onConnectEnd]); + + useEffect(() => { + if (typeof snapToGrid !== 'undefined') { + setSnapToGrid(snapToGrid); + } + }, [snapToGrid]); + + useEffect(() => { + if (typeof snapGrid !== 'undefined') { + setSnapGrid(snapGrid); + } + }, [snapGrid]); + + useEffect(() => { + if (typeof nodesDraggable !== 'undefined') { + setNodesDraggable(nodesDraggable); + } + }, [nodesDraggable]); + + useEffect(() => { + if (typeof nodesConnectable !== 'undefined') { + setNodesConnectable(nodesConnectable); + } + }, [nodesConnectable]); + + useEffect(() => { + if (typeof elementsSelectable !== 'undefined') { + setElementsSelectable(elementsSelectable); + } + }, [elementsSelectable]); + + useEffect(() => { + if (typeof minZoom !== 'undefined') { + setMinZoom(minZoom); + } + }, [minZoom]); + + useEffect(() => { + if (typeof maxZoom !== 'undefined') { + setMaxZoom(maxZoom); + } + }, [maxZoom]); + + useEffect(() => { + if (typeof translateExtent !== 'undefined') { + setTranslateExtent(translateExtent); + } + }, [translateExtent]); + + useEffect(() => { + if (typeof nodeExtent !== 'undefined') { + setNodeExtent(nodeExtent); + } + }, [nodeExtent]); + + useEffect(() => { + if (typeof connectionMode !== 'undefined') { + setConnectionMode(connectionMode); + } + }, [connectionMode]); + + useEffect(() => { + if (typeof onNodesChange !== 'undefined') { + setOnNodesChange(onNodesChange); + } + }, [onNodesChange]); + + useEffect(() => { + if (typeof onEdgesChange !== 'undefined') { + setOnEdgesChange(onEdgesChange); + } + }, [onEdgesChange]); + + return null; +}; + +export default StoreUpdater; diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 1157df2b..739aa4a6 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 } from './utils'; +import { getEdgePositions, getHandle, getSourceTargetNodes } from './utils'; import { Position, Edge, @@ -15,15 +15,17 @@ import { ConnectionMode, OnEdgeUpdateFunc, ReactFlowState, + NodeHandleBounds, } from '../../types'; interface EdgeRendererProps { + nodes: Node[]; + edges: Edge[]; edgeTypes: any; connectionLineType: ConnectionLineType; connectionLineStyle?: CSSProperties; connectionLineComponent?: ConnectionLineComponent; - connectionMode?: ConnectionMode; - onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void; + onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; arrowHeadColor: string; markerEndId?: string; @@ -42,7 +44,7 @@ interface EdgeWrapperProps { edge: Edge; edgeTypes: any; markerEndId?: string; - onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void; + onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void; onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void; onEdgeMouseMove?: (event: React.MouseEvent, edge: Edge) => void; @@ -52,10 +54,18 @@ interface EdgeWrapperProps { onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; onEdgeUpdate?: OnEdgeUpdateFunc; - targetNode?: Node; - sourceNode?: Node; elementsSelectable: boolean; connectionMode?: ConnectionMode; + sourceNodeWidth?: number | null; + sourceNodeHeight?: number | null; + sourceNodeX?: number; + sourceNodeY?: number; + sourceNodeHandleBounds?: NodeHandleBounds; + targetNodeWidth?: number | null; + targetNodeHeight?: number | null; + targetNodeX?: number; + targetNodeY?: number; + targetNodeHandleBounds?: NodeHandleBounds; } const Edge = memo( @@ -63,7 +73,7 @@ const Edge = memo( edge, edgeTypes, markerEndId, - onElementClick, + onEdgeClick, onEdgeContextMenu, onEdgeMouseEnter, onEdgeMouseMove, @@ -73,10 +83,18 @@ const Edge = memo( onEdgeUpdateStart, onEdgeUpdateEnd, onEdgeUpdate, - targetNode, - sourceNode, - elementsSelectable, connectionMode, + elementsSelectable, + sourceNodeWidth, + sourceNodeHeight, + sourceNodeX, + sourceNodeY, + sourceNodeHandleBounds, + targetNodeWidth, + targetNodeHeight, + targetNodeX, + targetNodeY, + targetNodeHandleBounds, }: EdgeWrapperProps) => { const sourceHandleId = edge.sourceHandle || null; const targetHandleId = edge.targetHandle || null; @@ -88,31 +106,40 @@ const Edge = memo( [edge, onEdgeUpdate] ); - if (!sourceNode) { + // source and target node need to be initialized + if (!sourceNodeHandleBounds || !targetNodeHandleBounds) { + return null; + } + + if ( + !sourceNodeWidth || + !sourceNodeHeight || + typeof sourceNodeX === 'undefined' || + typeof sourceNodeY === 'undefined' + ) { console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`); return null; } - if (!targetNode) { + if ( + !targetNodeWidth || + !targetNodeHeight || + typeof targetNodeX === 'undefined' || + typeof targetNodeY === 'undefined' + ) { console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`); return null; } - // source and target node need to be initialized - if (!sourceNode.width || !targetNode.width) { - return null; - } - const edgeType = edge.type || 'default'; const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default; - const targetNodeBounds = targetNode.handleBounds; // when connection type is loose we can define all handles as sources const targetNodeHandles = connectionMode === ConnectionMode.Strict - ? targetNodeBounds.target - : targetNodeBounds.target || targetNodeBounds.source; - const sourceHandle = getHandle(sourceNode.handleBounds.source, sourceHandleId); - const targetHandle = getHandle(targetNodeHandles, targetHandleId); + ? targetNodeHandleBounds.target + : targetNodeHandleBounds.target || targetNodeHandleBounds.source; + const sourceHandle = getHandle(sourceNodeHandleBounds.source!, sourceHandleId); + const targetHandle = getHandle(targetNodeHandles!, targetHandleId); const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom; const targetPosition = targetHandle ? targetHandle.position : Position.Top; @@ -127,10 +154,10 @@ const Edge = memo( } const { sourceX, sourceY, targetX, targetY } = getEdgePositions( - sourceNode, + { x: sourceNodeX, y: sourceNodeY, width: sourceNodeWidth, height: sourceNodeHeight }, sourceHandle, sourcePosition, - targetNode, + { x: targetNodeX, y: targetNodeY, width: targetNodeWidth, height: targetNodeHeight }, targetHandle, targetPosition ); @@ -156,8 +183,8 @@ const Edge = memo( className={edge.className} type={edge.type} data={edge.data} - onClick={onElementClick} - selected={!!edge.selected} + onClick={onEdgeClick} + isSelected={!!edge.isSelected} animated={edge.animated} label={edge.label} labelStyle={edge.labelStyle} @@ -197,7 +224,6 @@ const Edge = memo( const selector = (s: ReactFlowState) => ({ transform: s.transform, - edges: s.edges, connectionNodeId: s.connectionNodeId, connectionHandleId: s.connectionHandleId, connectionHandleType: s.connectionHandleType, @@ -206,12 +232,12 @@ const selector = (s: ReactFlowState) => ({ elementsSelectable: s.elementsSelectable, width: s.width, height: s.height, + connectionMode: s.connectionMode, }); const EdgeRenderer = (props: EdgeRendererProps) => { const { transform, - edges, connectionNodeId, connectionHandleId, connectionHandleType, @@ -220,6 +246,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { elementsSelectable, width, height, + connectionMode, } = useStore(selector, shallow); if (!width) { @@ -227,33 +254,44 @@ const EdgeRenderer = (props: EdgeRendererProps) => { } const { connectionLineType, arrowHeadColor, connectionLineStyle, connectionLineComponent } = props; - const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`; const renderConnectionLine = connectionNodeId && connectionHandleType; return ( - - {edges.map((edge: Edge) => ( - - ))} + + {props.edges.map((edge: Edge) => { + const { sourceNode, targetNode } = getSourceTargetNodes(edge, props.nodes); + return ( + + ); + })} {renderConnectionLine && ( { - const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle); - const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle); + const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle); + const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle); return { sourceX: sourceHandlePos.x, @@ -169,16 +170,3 @@ export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNod { sourceNode: null, targetNode: null } as SourceTargetNode ); }; - -export const extendEdgeWithSourceAndTarget = (edge: Edge, nodes: Node[]): Edge => { - const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes); - - if (sourceNode) { - edge.sourceNode = sourceNode; - } - if (targetNode) { - edge.targetNode = targetNode; - } - - return edge; -}; diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index fc105de2..2db1ab80 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -22,6 +22,8 @@ interface FlowRendererProps | 'arrowHeadColor' | 'onlyRenderVisibleElements' | 'selectNodesOnDrag' + | 'nodes' + | 'edges' > { children: ReactNode; } @@ -54,7 +56,6 @@ const FlowRenderer = ({ paneMoveable, defaultPosition, defaultZoom, - translateExtent, preventScrolling, onSelectionDragStart, onSelectionDrag, @@ -106,7 +107,6 @@ const FlowRenderer = ({ paneMoveable={paneMoveable} defaultPosition={defaultPosition} defaultZoom={defaultZoom} - translateExtent={translateExtent} zoomActivationKeyCode={zoomActivationKeyCode} preventScrolling={preventScrolling} > diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 20e28dc3..8fddc4f3 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, memo } from 'react'; -import shallow from 'zustand/shallow'; -import { useStore, useStoreApi } from '../../store'; +import { useStoreApi } from '../../store'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; @@ -10,17 +9,15 @@ import useZoomPanHelper from '../../hooks/useZoomPanHelper'; import { ReactFlowProps } from '../ReactFlow'; -import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode, ReactFlowState } from '../../types'; +import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; -export interface GraphViewProps extends Omit { +export interface GraphViewProps extends Omit { nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; selectionKeyCode: KeyCode; deleteKeyCode: KeyCode; multiSelectionKeyCode: KeyCode; connectionLineType: ConnectionLineType; - snapToGrid: boolean; - snapGrid: [number, number]; onlyRenderVisibleElements: boolean; defaultZoom: number; defaultPosition: [number, number]; @@ -28,33 +25,17 @@ export interface GraphViewProps extends Omit ({ - setOnConnect: s.setOnConnect, - setOnConnectStart: s.setOnConnectStart, - setOnConnectStop: s.setOnConnectStop, - setOnConnectEnd: s.setOnConnectEnd, - setSnapGrid: s.setSnapGrid, - setSnapToGrid: s.setSnapToGrid, - setNodesDraggable: s.setNodesDraggable, - setNodesConnectable: s.setNodesConnectable, - setElementsSelectable: s.setElementsSelectable, - setMinZoom: s.setMinZoom, - setMaxZoom: s.setMaxZoom, - setTranslateExtent: s.setTranslateExtent, - setNodeExtent: s.setNodeExtent, - setConnectionMode: s.setConnectionMode, - setOnNodesChange: s.setOnNodesChange, - setOnEdgesChange: s.setOnEdgesChange, -}); - const GraphView = ({ + nodes, + edges, nodeTypes, edgeTypes, onMove, onMoveStart, onMoveEnd, onLoad, - onElementClick, + onNodeClick, + onEdgeClick, onNodeDoubleClick, onEdgeDoubleClick, onNodeMouseEnter, @@ -68,7 +49,6 @@ const GraphView = ({ onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, - connectionMode, connectionLineType, connectionLineStyle, connectionLineComponent, @@ -76,24 +56,12 @@ const GraphView = ({ multiSelectionKeyCode, zoomActivationKeyCode, deleteKeyCode, - onConnect, - onConnectStart, - onConnectStop, - onConnectEnd, - snapToGrid, - snapGrid, onlyRenderVisibleElements, - nodesDraggable, - nodesConnectable, elementsSelectable, selectNodesOnDrag, - minZoom, - maxZoom, defaultZoom, defaultPosition, - translateExtent, preventScrolling, - nodeExtent, arrowHeadColor, markerEndId, zoomOnScroll, @@ -114,31 +82,9 @@ const GraphView = ({ edgeUpdaterRadius, onEdgeUpdateStart, onEdgeUpdateEnd, - onNodesChange, - onEdgesChange, }: GraphViewProps) => { const isInitialized = useRef(false); const store = useStoreApi(); - - const { - setOnConnect, - setOnConnectStart, - setOnConnectStop, - setOnConnectEnd, - setSnapGrid, - setSnapToGrid, - setNodesDraggable, - setNodesConnectable, - setElementsSelectable, - setMinZoom, - setMaxZoom, - setTranslateExtent, - setNodeExtent, - setConnectionMode, - setOnNodesChange, - setOnEdgesChange, - } = useStore(selector, shallow); - const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); useEffect(() => { @@ -160,102 +106,6 @@ const GraphView = ({ } }, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView, initialized]); - useEffect(() => { - if (onConnect) { - setOnConnect(onConnect); - } - }, [onConnect]); - - useEffect(() => { - if (onConnectStart) { - setOnConnectStart(onConnectStart); - } - }, [onConnectStart]); - - useEffect(() => { - if (onConnectStop) { - setOnConnectStop(onConnectStop); - } - }, [onConnectStop]); - - useEffect(() => { - if (onConnectEnd) { - setOnConnectEnd(onConnectEnd); - } - }, [onConnectEnd]); - - useEffect(() => { - if (typeof snapToGrid !== 'undefined') { - setSnapToGrid(snapToGrid); - } - }, [snapToGrid]); - - useEffect(() => { - if (typeof snapGrid !== 'undefined') { - setSnapGrid(snapGrid); - } - }, [snapGrid]); - - useEffect(() => { - if (typeof nodesDraggable !== 'undefined') { - setNodesDraggable(nodesDraggable); - } - }, [nodesDraggable]); - - useEffect(() => { - if (typeof nodesConnectable !== 'undefined') { - setNodesConnectable(nodesConnectable); - } - }, [nodesConnectable]); - - useEffect(() => { - if (typeof elementsSelectable !== 'undefined') { - setElementsSelectable(elementsSelectable); - } - }, [elementsSelectable]); - - useEffect(() => { - if (typeof minZoom !== 'undefined') { - setMinZoom(minZoom); - } - }, [minZoom]); - - useEffect(() => { - if (typeof maxZoom !== 'undefined') { - setMaxZoom(maxZoom); - } - }, [maxZoom]); - - useEffect(() => { - if (typeof translateExtent !== 'undefined') { - setTranslateExtent(translateExtent); - } - }, [translateExtent]); - - useEffect(() => { - if (typeof nodeExtent !== 'undefined') { - setNodeExtent(nodeExtent); - } - }, [nodeExtent]); - - useEffect(() => { - if (typeof connectionMode !== 'undefined') { - setConnectionMode(connectionMode); - } - }, [connectionMode]); - - useEffect(() => { - if (typeof onNodesChange !== 'undefined') { - setOnNodesChange(onNodesChange); - } - }, [onNodesChange]); - - useEffect(() => { - if (typeof onEdgesChange !== 'undefined') { - setOnEdgesChange(onEdgesChange); - } - }, [onEdgesChange]); - return ( ); diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 6dbde07d..a808fb79 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -2,11 +2,11 @@ import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import shallow from 'zustand/shallow'; import { useStore } from '../../store'; -import { Node, NodeTypesType, ReactFlowState, Edge, WrapNodeProps } from '../../types'; +import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; interface NodeRendererProps { nodeTypes: NodeTypesType; selectNodesOnDrag: boolean; - onElementClick?: (event: MouseEvent, element: Node | Edge) => void; + onNodeClick?: (event: MouseEvent, element: Node) => void; onNodeDoubleClick?: (event: MouseEvent, element: Node) => void; onNodeMouseEnter?: (event: MouseEvent, node: Node) => void; onNodeMouseMove?: (event: MouseEvent, node: Node) => void; @@ -15,9 +15,8 @@ interface NodeRendererProps { onNodeDragStart?: (event: MouseEvent, node: Node) => void; onNodeDrag?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: (event: MouseEvent, node: Node) => void; - snapToGrid: boolean; - snapGrid: [number, number]; onlyRenderVisibleElements: boolean; + nodes: Node[]; } const selector = (s: ReactFlowState) => ({ @@ -25,15 +24,21 @@ const selector = (s: ReactFlowState) => ({ nodesDraggable: s.nodesDraggable, nodesConnectable: s.nodesConnectable, elementsSelectable: s.elementsSelectable, - nodes: s.nodes, updateNodeDimensions: s.updateNodeDimensions, + snapGrid: s.snapGrid, + snapToGrid: s.snapToGrid, }); const NodeRenderer = (props: NodeRendererProps) => { - const { transform, nodesDraggable, nodesConnectable, elementsSelectable, nodes, updateNodeDimensions } = useStore( - selector, - shallow - ); + const { + transform, + nodesDraggable, + nodesConnectable, + elementsSelectable, + updateNodeDimensions, + snapGrid, + snapToGrid, + } = useStore(selector, shallow); // const visibleNodes = props.onlyRenderVisibleElements // ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) @@ -63,7 +68,7 @@ const NodeRenderer = (props: NodeRendererProps) => { return (
- {nodes.map((node) => { + {props.nodes.map((node) => { const nodeType = node.type || 'default'; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType; @@ -74,6 +79,11 @@ const NodeRenderer = (props: NodeRendererProps) => { const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); + const isInitialized = + node.width !== null && + node.height !== null && + typeof node.width !== 'undefined' && + typeof node.height !== 'undefined'; return ( { xPos={node.position.x} yPos={node.position.y} isDragging={node.isDragging} - isInitialized={node.width !== null && node.height !== null} - snapGrid={props.snapGrid} - snapToGrid={props.snapToGrid} + isInitialized={isInitialized} + snapGrid={snapGrid} + snapToGrid={snapToGrid} selectNodesOnDrag={props.selectNodesOnDrag} - onClick={props.onElementClick} + onClick={props.onNodeClick} onMouseEnter={props.onNodeMouseEnter} onMouseMove={props.onNodeMouseMove} onMouseLeave={props.onNodeMouseLeave} @@ -103,7 +113,7 @@ const NodeRenderer = (props: NodeRendererProps) => { onNodeDrag={props.onNodeDrag} onNodeDragStop={props.onNodeDragStop} scale={transform[2]} - selected={!!node.selected} + isSelected={!!node.isSelected} isDraggable={isDraggable} isSelectable={isSelectable} isConnectable={isConnectable} diff --git a/src/container/ReactFlow/Wrapper.tsx b/src/container/ReactFlow/Wrapper.tsx index 67f22cf6..43aac450 100644 --- a/src/container/ReactFlow/Wrapper.tsx +++ b/src/container/ReactFlow/Wrapper.tsx @@ -1,24 +1,21 @@ import React, { FC } from 'react'; -import { Provider, createStore } from '../../store'; -// import { ReactFlowState } from '../../types'; - -// const reactFlowVersionSelector = (s: ReactFlowState) => s.reactFlowVersion; +import { Provider, createStore, useStoreApi } from '../../store'; const Wrapper: FC = ({ children }) => { - // let isWrapped = useRef(true); + let isWrapped = true; - // try { - // useStoreApi(); - // } catch { - // isWrapped.current = false; - // } + try { + useStoreApi(); + } catch (e) { + isWrapped = false; + } - // if (isWrapped) { - // // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode - // // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 - // return <>{children}; - // } + if (isWrapped) { + // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + return <>{children}; + } return {children}; }; diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index b6bcc6a3..3f7b4106 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -9,7 +9,7 @@ import React, { import cc from 'classcat'; import GraphView from '../GraphView'; -import ElementUpdater from '../../components/ElementUpdater'; +import StoreUpdater from '../../components/StoreUpdater'; import DefaultNode from '../../components/Nodes/DefaultNode'; import InputNode from '../../components/Nodes/InputNode'; import OutputNode from '../../components/Nodes/OutputNode'; @@ -38,7 +38,8 @@ import { PanOnScrollMode, OnEdgeUpdateFunc, NodeExtent, - ElementChange, + NodeChange, + EdgeChange, } from '../../types'; import '../../style.css'; @@ -60,9 +61,10 @@ const defaultEdgeTypes = { export interface ReactFlowProps extends Omit, 'onLoad'> { nodes: Node[]; edges: Edge[]; - onNodesChange?: (nodeChanges: ElementChange[]) => void; - onEdgesChange?: (edgeChanges: ElementChange[]) => void; - onElementClick?: (event: ReactMouseEvent, element: Node | Edge) => void; + onNodesChange?: (nodeChanges: NodeChange[]) => void; + onEdgesChange?: (edgeChanges: EdgeChange[]) => void; + onNodeClick?: (event: React.MouseEvent, node: Node) => void; + onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void; @@ -129,8 +131,6 @@ export interface ReactFlowProps extends Omit, 'on onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; - nodeTypesId?: string; - edgeTypesId?: string; } export type ReactFlowRefType = HTMLDivElement; @@ -146,7 +146,8 @@ const ReactFlow = forwardRef( className, nodeTypes = defaultNodeTypes, edgeTypes = defaultEdgeTypes, - onElementClick, + onNodeClick, + onEdgeClick, onLoad, onMove, onMoveStart, @@ -212,27 +213,28 @@ const ReactFlow = forwardRef( onEdgeUpdateStart, onEdgeUpdateEnd, edgeUpdaterRadius = 10, - nodeTypesId = '1', - edgeTypesId = '1', onNodesChange, onEdgesChange, ...rest }, ref ) => { - const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), [nodeTypesId]); - const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), [edgeTypesId]); + const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), [nodeTypes]); + const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), [edgeTypes]); const reactFlowClasses = cc(['react-flow', className]); return (
( onNodeDragStop={onNodeDragStop} nodeTypes={nodeTypesParsed} edgeTypes={edgeTypesParsed} - connectionMode={connectionMode} connectionLineType={connectionLineType} connectionLineStyle={connectionLineStyle} connectionLineComponent={connectionLineComponent} @@ -251,24 +252,11 @@ const ReactFlow = forwardRef( deleteKeyCode={deleteKeyCode} multiSelectionKeyCode={multiSelectionKeyCode} zoomActivationKeyCode={zoomActivationKeyCode} - onConnect={onConnect} - onConnectStart={onConnectStart} - onConnectStop={onConnectStop} - onConnectEnd={onConnectEnd} - snapToGrid={snapToGrid} - snapGrid={snapGrid} onlyRenderVisibleElements={onlyRenderVisibleElements} - nodesDraggable={nodesDraggable} - nodesConnectable={nodesConnectable} - elementsSelectable={elementsSelectable} selectNodesOnDrag={selectNodesOnDrag} - minZoom={minZoom} - maxZoom={maxZoom} defaultZoom={defaultZoom} defaultPosition={defaultPosition} - translateExtent={translateExtent} preventScrolling={preventScrolling} - nodeExtent={nodeExtent} arrowHeadColor={arrowHeadColor} markerEndId={markerEndId} zoomOnScroll={zoomOnScroll} @@ -294,10 +282,28 @@ const ReactFlow = forwardRef( onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} edgeUpdaterRadius={edgeUpdaterRadius} + /> + - + {onSelectionChange && } {children} diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 3993c017..c5e25404 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -7,7 +7,7 @@ import { clamp } from '../../utils'; import useKeyPress from '../../hooks/useKeyPress'; import useResizeHandler from '../../hooks/useResizeHandler'; import { useStore, useStoreApi } from '../../store'; -import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode, ReactFlowState } from '../../types'; +import { FlowTransform, PanOnScrollMode, KeyCode, ReactFlowState } from '../../types'; interface ZoomPaneProps { selectionKeyPressed: boolean; @@ -21,7 +21,6 @@ interface ZoomPaneProps { paneMoveable?: boolean; defaultPosition?: [number, number]; defaultZoom?: number; - translateExtent?: TranslateExtent; onMove?: (flowTransform?: FlowTransform) => void; onMoveStart?: (flowTransform?: FlowTransform) => void; onMoveEnd?: (flowTransform?: FlowTransform) => void; @@ -67,7 +66,6 @@ const ZoomPane = ({ paneMoveable = true, defaultPosition = [0, 0], defaultZoom = 1, - translateExtent, zoomActivationKeyCode, preventScrolling = true, children, @@ -82,14 +80,13 @@ const ZoomPane = ({ useEffect(() => { if (zoomPane.current) { - const state = store.getState(); - const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent; - const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent); + const { minZoom, maxZoom, translateExtent } = store.getState(); + const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent); const selection = select(zoomPane.current as Element).call(d3ZoomInstance); - const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]); - const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]); - const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom); + const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0]); + const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1]); + const clampedZoom = clamp(defaultZoom, minZoom, maxZoom); const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom); d3ZoomInstance.transform(selection, updatedTransform); diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 30367672..87b8a360 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -4,7 +4,7 @@ import shallow from 'zustand/shallow'; import { useStore, useStoreApi } from '../store'; import useKeyPress from './useKeyPress'; import { getConnectedEdges } from '../utils/graph'; -import { KeyCode, ReactFlowState } from '../types'; +import { EdgeChange, KeyCode, NodeChange, ReactFlowState } from '../types'; interface HookParams { deleteKeyCode: KeyCode; @@ -29,14 +29,17 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { useEffect(() => { const { nodes, edges } = store.getState(); - const selectedNodes = nodes.filter((n) => n.selected); - const selectedEdges = edges.filter((e) => e.selected); + const selectedNodes = nodes.filter((n) => n.isSelected); + const selectedEdges = edges.filter((e) => e.isSelected); if (deleteKeyPressed && (selectedNodes || selectedEdges)) { const connectedEdges = getConnectedEdges(selectedNodes, edges); - const nodeChanges = selectedNodes.map((n) => ({ id: n.id, delete: true })); - const edgeChanges = [...selectedEdges, ...connectedEdges].map((e) => ({ id: e.id, delete: true })); + const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ id: n.id, type: 'remove' })); + const edgeChanges: EdgeChange[] = [...selectedEdges, ...connectedEdges].map((e) => ({ + id: e.id, + type: 'remove', + })); onNodesChange?.(nodeChanges); onEdgesChange?.(edgeChanges); diff --git a/src/store/index.ts b/src/store/index.ts index f0b61b65..5976bb3b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -7,7 +7,6 @@ import { ConnectionMode, Node, Edge, - ElementChange, NodeDimensionUpdate, NodeDiffUpdate, XYPosition, @@ -23,28 +22,24 @@ import { OnConnectEndFunc, SetConnectionId, SnapGrid, - OnElementsChange, + NodeChange, + OnNodesChange, + OnEdgesChange, + EdgeChange, + NodePositionChange, } from '../types'; -import { - parseNode, - parseEdge, - isNode, - isEdge, - getRectOfNodes, - getNodesInside, - getConnectedEdges, -} from '../utils/graph'; -import { extendEdgeWithSourceAndTarget } from '../container/EdgeRenderer/utils'; +import { isNode, isEdge, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; const { Provider, useStore, useStoreApi } = createContext(); -const unselectElements = (elements: Elements) => +const unselectElements = (elements: Elements): NodeChange[] | EdgeChange[] => elements - .filter((e) => e.selected) + .filter((e) => e.isSelected) .map((e) => ({ id: e.id, - change: { selected: false }, + type: 'select', + isSelected: false, })); const createStore = () => @@ -104,53 +99,37 @@ const createStore = () => reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', setNodes: (propNodes: Node[]) => { - const { nodes, edges, nodeExtent } = get(); + const { nodes } = get(); const nextNodes = propNodes.map((propNode: Node) => { const storeNode = nodes.find((node) => node.id === propNode.id); if (storeNode) { if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { - const updatedNode: Node = { - ...storeNode, - ...propNode, - }; // 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. - updatedNode.width = null; - return updatedNode; + return { + ...propNode, + width: null, + height: null, + }; } } - return parseNode(propNode, nodeExtent); + return propNode; }); - const updatedEdges = edges.map((edge) => extendEdgeWithSourceAndTarget(edge, nextNodes)); - set({ nodes: nextNodes, - edges: updatedEdges, }); }, - setEdges: (propEdges: Edge[]) => { - const { edges, nodes } = get(); - - const nextEdges = propEdges.map((propEdge: Edge) => { - const storeEdge = edges.find((se) => se.id === propEdge.id); - - if (storeEdge) { - return parseEdge(propEdge); - } else { - return extendEdgeWithSourceAndTarget(parseEdge(propEdge), nodes); - } - }); - - set({ edges: nextEdges }); + setEdges: (edges: Edge[]) => { + set({ edges }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { const { onNodesChange, nodes, transform } = get(); - const initialChanges: ElementChange[] = []; - const nodesToChange: ElementChange[] = nodes.reduce((res, node) => { + const initialChanges: NodeChange[] = []; + const nodesToChange: NodeChange[] = nodes.reduce((res, node) => { const update = updates.find((u) => u.id === node.id); if (update) { const dimensions = getDimensions(update.nodeElement); @@ -163,12 +142,10 @@ const createStore = () => const handleBounds = getHandleBounds(update.nodeElement, transform[2]); const change = { id: node.id, - change: { - ...dimensions, - handleBounds, - }, - } as ElementChange; - + type: 'dimensions', + dimensions, + handleBounds, + } as NodeChange; res.push(change); } } @@ -176,28 +153,37 @@ const createStore = () => return res; }, initialChanges); - if (onNodesChange) { - onNodesChange(nodesToChange); - } + onNodesChange?.(nodesToChange); }, - updateNodePosDiff: ({ id, diff, isDragging }: NodeDiffUpdate) => { - const { onNodesChange, nodes } = get(); + updateNodePosition: ({ id, diff, isDragging }: NodeDiffUpdate) => { + const { onNodesChange, nodes, nodeExtent } = get(); - if (onNodesChange && diff) { - const matchingNodes = nodes.filter((n) => n.id === id || n.selected); + if (onNodesChange) { + const matchingNodes = nodes.filter((n) => n.id === id || n.isSelected); if (matchingNodes?.length) { onNodesChange( - matchingNodes.map((n) => ({ - id: n.id, - change: { - position: { - x: n.position.x + diff.x, - y: n.position.y + diff.y, - isDragging, - }, - }, - })) + matchingNodes.map((n) => { + const change: NodePositionChange = { + id: n.id, + type: 'position', + isDragging: !!isDragging, + }; + + if (diff) { + change.position = nodeExtent + ? clampPosition( + { + x: n.position.x + diff.x, + y: n.position.y + diff.y, + }, + nodeExtent + ) + : { x: n.position.x + diff.x, y: n.position.y + diff.y }; + } + + return change; + }) ); } } @@ -230,15 +216,11 @@ const createStore = () => }; const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, false, true); - const selectedEdges = getConnectedEdges(selectedNodes, edges); + const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id); + const selectedNodeIds = selectedNodes.map((n) => n.id); - if (selectedNodes?.length) { - onNodesChange?.(selectedNodes.map((n) => ({ id: n.id, change: { selected: true } }))); - } - - if (selectedEdges?.length) { - onEdgesChange?.(selectedEdges.map((n) => ({ id: n.id, change: { selected: true } }))); - } + onNodesChange?.(nodes.map((n) => ({ id: n.id, type: 'select', isSelected: selectedNodeIds.includes(n.id) }))); + onEdgesChange?.(edges.map((e) => ({ id: e.id, type: 'select', isSelected: selectedEdgeIds.includes(e.id) }))); set({ userSelectionRect: nextUserSelectRect, @@ -246,7 +228,7 @@ const createStore = () => }, unsetUserSelection: () => { const { userSelectionRect, nodes } = get(); - const selectedNodes = nodes.filter((node) => node.selected); + const selectedNodes = nodes.filter((node) => node.isSelected); const stateUpdate = { selectionActive: false, @@ -274,21 +256,32 @@ const createStore = () => let changedEdges; if (multiSelectionActive) { - changedNodes = selectedElementsArr.filter(isNode).map((node) => ({ id: node.id, change: { selected: true } })); - changedEdges = selectedElementsArr.filter(isEdge).map((edge) => ({ id: edge.id, change: { selected: true } })); + changedNodes = selectedElementsArr + .filter(isNode) + .map((node) => ({ id: node.id, type: 'select', isSelected: true })); + changedEdges = selectedElementsArr + .filter(isEdge) + .map((edge) => ({ id: edge.id, type: 'select', isSelected: true })); } else { changedNodes = nodes.map((node) => ({ id: node.id, - change: { selected: selectedElementsArr.some((e) => e.id === node.id) }, + type: 'select', + isSelected: selectedElementsArr.some((e) => e.id === node.id), })); changedEdges = edges.map((edge) => ({ id: edge.id, - change: { selected: selectedElementsArr.some((e) => e.id === edge.id) }, + type: 'select', + isSelected: selectedElementsArr.some((e) => e.id === edge.id), })); } - onNodesChange?.(changedNodes); - onEdgesChange?.(changedEdges); + if (changedNodes.length) { + onNodesChange?.(changedNodes as NodeChange[]); + } + + if (changedEdges.length) { + onEdgesChange?.(changedEdges as EdgeChange[]); + } }, initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => set({ @@ -319,8 +312,15 @@ const createStore = () => resetSelectedElements: () => { const { nodes, edges, onNodesChange, onEdgesChange } = get(); - onNodesChange?.(unselectElements(nodes)); - onEdgesChange?.(unselectElements(edges)); + const nodesToUnselect = unselectElements(nodes) as NodeChange[]; + const edgesToUnselect = unselectElements(edges) as EdgeChange[]; + + if (nodesToUnselect.length) { + onNodesChange?.(nodesToUnselect); + } + if (edgesToUnselect.length) { + onEdgesChange?.(edgesToUnselect); + } }, setNodeExtent: (nodeExtent: NodeExtent) => set({ @@ -354,8 +354,8 @@ const createStore = () => setElementsSelectable: (elementsSelectable: boolean) => set({ elementsSelectable }), setMultiSelectionActive: (multiSelectionActive: boolean) => set({ multiSelectionActive }), setConnectionMode: (connectionMode: ConnectionMode) => set({ connectionMode }), - setOnNodesChange: (onNodesChange: OnElementsChange) => set({ onNodesChange }), - setOnEdgesChange: (onEdgesChange: OnElementsChange) => set({ onEdgesChange }), + setOnNodesChange: (onNodesChange: OnNodesChange) => set({ onNodesChange }), + setOnEdgesChange: (onEdgesChange: OnEdgesChange) => set({ onEdgesChange }), })); export { Provider, useStore, createStore, useStoreApi }; diff --git a/src/types/index.ts b/src/types/index.ts index b011b1b5..32f98f6e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,11 +9,32 @@ export type Elements = Array>; export type Transform = [number, number, number]; -export type ElementChange = { +export type NodeDimensionChange = { id: string; - change?: any; - delete?: boolean; + type: 'dimensions'; + dimensions: Dimensions; + handleBounds?: NodeHandleBounds; }; +export type NodePositionChange = { + id: string; + type: 'position'; + position?: XYPosition; + isDragging?: boolean; +}; +export type NodeSelectionChange = { + id: string; + type: 'select'; + isSelected: boolean; +}; +export type NodeRemoveChange = { + id: string; + type: 'remove'; +}; +export type NodeChange = NodeDimensionChange | NodePositionChange | NodeSelectionChange | NodeRemoveChange; + +export type EdgeSelectionChange = NodeSelectionChange; +export type EdgeRemoveChange = NodeRemoveChange; +export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange; export enum Position { Left = 'left', @@ -41,26 +62,30 @@ export interface Box extends XYPosition { export type SnapGrid = [number, number]; +export type NodeHandleBounds = { + source: HandleElement[] | null; + target: HandleElement[] | null; +}; + export interface Node { id: ElementId; position: XYPosition; type?: string; - __rf?: any; data?: T; style?: CSSProperties; className?: string; targetPosition?: Position; sourcePosition?: Position; isHidden?: boolean; + isSelected?: boolean; + isDragging?: boolean; draggable?: boolean; selectable?: boolean; connectable?: boolean; dragHandle?: string; - isDragging?: boolean; width?: number | null; height?: number | null; - handleBounds?: any; - selected?: boolean; + handleBounds?: NodeHandleBounds; } export enum ArrowHeadType { @@ -89,7 +114,7 @@ export interface Edge { className?: string; sourceNode?: Node; targetNode?: Node; - selected?: boolean; + isSelected?: boolean; } export enum BackgroundVariant { @@ -116,7 +141,7 @@ export interface WrapEdgeProps { data?: T; onClick?: (event: React.MouseEvent, edge: Edge) => void; onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void; - selected: boolean; + isSelected: boolean; animated?: boolean; label?: string | ReactNode; labelStyle?: CSSProperties; @@ -158,7 +183,7 @@ export interface EdgeProps { sourceY: number; targetX: number; targetY: number; - selected?: boolean; + isSelected?: boolean; animated?: boolean; sourcePosition: Position; targetPosition: Position; @@ -194,7 +219,7 @@ export interface NodeProps { id: ElementId; type: string; data: T; - selected: boolean; + isSelected: boolean; isConnectable: boolean; xPos?: number; yPos?: number; @@ -208,7 +233,7 @@ export interface NodeComponentProps { id: ElementId; type: string; data: T; - selected?: boolean; + isSelected?: boolean; isConnectable: boolean; transform?: Transform; xPos?: number; @@ -233,7 +258,7 @@ export interface WrapNodeProps { id: ElementId; type: string; data: T; - selected: boolean; + isSelected: boolean; scale: number; xPos: number; yPos: number; @@ -271,7 +296,8 @@ export type FitViewParams = { }; export type FlowExportObject = { - elements: Elements; + nodes: Node[]; + edges: Edge[]; position: [number, number]; zoom: number; }; @@ -412,7 +438,8 @@ export type InitD3ZoomPayload = { transform: Transform; }; -export type OnElementsChange = (nodes: ElementChange[]) => void; +export type OnNodesChange = (nodes: NodeChange[]) => void; +export type OnEdgesChange = (nodes: EdgeChange[]) => void; export interface ReactFlowState { width: number; @@ -421,8 +448,8 @@ export interface ReactFlowState { nodes: Node[]; edges: Edge[]; selectedNodesBbox: Rect; - onNodesChange: OnElementsChange | null; - onEdgesChange: OnElementsChange | null; + onNodesChange: OnNodesChange | null; + onEdgesChange: OnEdgesChange | null; d3Zoom: ZoomBehavior | null; d3Selection: D3Selection | null; @@ -455,9 +482,9 @@ export interface ReactFlowState { reactFlowVersion: string; setNodes: (nodes: Node[]) => void; - setEdges: (edges: Edge[]) => void; + setEdges: (edges: Edge[], nodes: Node[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; - updateNodePosDiff: (update: NodeDiffUpdate) => void; + updateNodePosition: (update: NodeDiffUpdate) => void; setUserSelection: (mousePos: XYPosition) => void; updateUserSelection: (mousePos: XYPosition) => void; unsetUserSelection: () => void; @@ -485,8 +512,8 @@ export interface ReactFlowState { setElementsSelectable: (elementsSelectable: boolean) => void; setMultiSelectionActive: (multiSelectionActive: boolean) => void; setConnectionMode: (connectionMode: ConnectionMode) => void; - setOnNodesChange: (onNodesChange: OnElementsChange) => void; - setOnEdgesChange: (onEdgesChange: OnElementsChange) => void; + setOnNodesChange: (onNodesChange: OnNodesChange) => void; + setOnEdgesChange: (onEdgesChange: OnEdgesChange) => void; onConnect?: OnConnectFunc; onConnectStart?: OnConnectStartFunc; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 8e84b139..109406c5 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,7 +1,6 @@ import { GetState } from 'zustand'; -import { clampPosition, clamp } from '../utils'; -import { ReactFlowState } from '../types'; +import { clamp } from '../utils'; import { ElementId, @@ -14,8 +13,9 @@ import { Box, Connection, FlowExportObject, - NodeExtent, - ElementChange, + EdgeChange, + NodeChange, + ReactFlowState, } from '../types'; export const isEdge = (element: Node | Connection | Edge): element is Edge => @@ -147,26 +147,6 @@ export const onLoadProject = (getState: GetState) => { }; }; -export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => { - if (!node.type) { - node.type = 'default'; - } - - if (nodeExtent) { - node.position = clampPosition(node.position, nodeExtent); - } - - return node; -}; - -export const parseEdge = (edge: Edge): Edge => { - if (!edge.type) { - edge.type = 'default'; - } - - return edge; -}; - const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ x: Math.min(box1.x, box2.x), y: Math.min(box1.y, box2.y), @@ -269,7 +249,8 @@ export const onLoadToObject = (getState: GetState) => { const { nodes = [], edges = [], transform } = getState(); return { - elements: parseElements(nodes, edges), + nodes: nodes.map((n) => ({ ...n })), + edges: edges.map((e) => ({ ...e })), position: [transform[0], transform[1]], zoom: transform[2], }; @@ -296,30 +277,51 @@ export const getTransformForBounds = ( return [x, y, clampedZoom]; }; -function applyChanges(changes: ElementChange[], elements: any[]): any[] { +function applyChanges(changes: NodeChange[] | EdgeChange[], elements: any[]): any[] { const initElements: any[] = []; - return elements.reduce((res: any[], node: any) => { - const hasChange = changes.find((c) => c.id === node.id); + return elements.reduce((res: any[], item: any) => { + const currentChange = changes.find((c) => c.id === item.id); - if (hasChange?.delete) { - return res; - } + if (currentChange) { + switch (currentChange.type) { + case 'dimensions': { + res.push({ ...item, ...currentChange.dimensions, handleBounds: currentChange.handleBounds }); + return res; + } + case 'select': { + res.push({ ...item, isSelected: currentChange.isSelected }); + return res; + } + case 'position': { + const updateItem = { ...item }; - if (hasChange?.change) { - res.push({ ...node, ...hasChange.change }); - } else { - res.push(node); + if (typeof currentChange.position !== 'undefined') { + updateItem.position = currentChange.position; + } + + if (typeof currentChange.isDragging !== 'undefined') { + updateItem.isDragging = currentChange.isDragging; + } + + res.push(updateItem); + return res; + } + case 'remove': { + return res; + } + } } + res.push(item); return res; }, initElements); } -export function applyNodeChanges(changes: ElementChange[], nodes: Node[]): Node[] { +export function applyNodeChanges(changes: NodeChange[], nodes: Node[]): Node[] { return applyChanges(changes, nodes) as Node[]; } -export function applyEdgeChanges(changes: ElementChange[], edges: Edge[]): Edge[] { +export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] { return applyChanges(changes, edges) as Edge[]; }