refactor(state): separate nodes and edges

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