refactor(elements): render only visible elements

This commit is contained in:
moklick
2021-10-19 14:57:34 +02:00
parent 410b12946d
commit 1525af39cf
21 changed files with 376 additions and 217 deletions
+1 -63
View File
@@ -1,4 +1,4 @@
import { isNode, isEdge, getOutgoers, getIncomers, removeElements, addEdge } from '../../../dist/ReactFlow.js'; import { isNode, isEdge, getOutgoers, getIncomers, addEdge } from '../../../dist/ReactFlow.js';
const nodes = [ const nodes = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
@@ -69,66 +69,4 @@ describe('Graph Utils Testing', () => {
} }
}); });
}); });
describe('tests removeElements function', () => {
it('removes a node', () => {
const nextElements = removeElements([nodes[0]], elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length - 1);
expect(nextEdges.length).to.be.equal(edges.length - 2);
});
it('removes multiple nodes', () => {
const elementsToRemove = [nodes[0], nodes[1]];
const nextElements = removeElements(elementsToRemove, elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length - 2);
expect(nextEdges.length).to.be.equal(0);
});
it('removes no node', () => {
const nextElementsNoRemove = removeElements([], elements);
expect(nextElementsNoRemove.length).to.be.equal(elements.length);
});
it('tries to removes node that does not exist', () => {
const nextElementsNoRemove = removeElements([{ id: 'id-that-does-not-exist' }], elements);
expect(nextElementsNoRemove.length).to.be.equal(elements.length);
});
it('removes an edge', () => {
const nextElements = removeElements([edges[0]], elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length);
expect(nextEdges.length).to.be.equal(edges.length - 1);
});
it('removes multiple edges', () => {
const nextElements = removeElements([edges[0], edges[1]], elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length);
expect(nextEdges.length).to.be.equal(edges.length - 2);
});
it('removes node and edge', () => {
const nextElements = removeElements([nodes[0], edges[0]], elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length - 1);
expect(nextEdges.length).to.be.equal(edges.length - 2);
});
});
}); });
+2 -2
View File
@@ -36,8 +36,8 @@ const BasicFlow = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onConnect = useCallback((params: Edge | Connection, nds: Node[]) => { const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => addEdge(params, nds, eds)); setEdges((eds) => addEdge(params, eds));
}, []); }, []);
const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []);
@@ -0,0 +1,25 @@
import React, { memo, FC, CSSProperties } from 'react';
import { Handle, Position, NodeProps, Connection, Edge } from 'react-flow-renderer';
const targetHandleStyle: CSSProperties = { background: '#555' };
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
const sourceHandleStyleB: CSSProperties = { ...targetHandleStyle, bottom: 10, top: 'auto' };
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => {
return (
<>
<Handle type="target" position={Position.Left} style={targetHandleStyle} onConnect={onConnect} />
<div>
Custom Color Picker Node: <strong>{data.color}</strong>
</div>
<input className="nodrag" type="color" onChange={data.onChange} defaultValue={data.color} />
<Handle type="source" position={Position.Right} id="a" style={sourceHandleStyleA} isConnectable={isConnectable} />
<Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} />
</>
);
};
export default memo(ColorSelectorNode);
+147
View File
@@ -0,0 +1,147 @@
import { useState, useEffect, MouseEvent, useCallback } from 'react';
import { ChangeEvent } from 'react';
import ReactFlow, {
addEdge,
MiniMap,
Controls,
Node,
OnLoadParams,
Position,
SnapGrid,
Connection,
Edge,
NodeChange,
applyNodeChanges,
applyEdgeChanges,
EdgeChange,
} from 'react-flow-renderer';
import ColorSelectorNode from './ColorSelectorNode';
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16];
const nodeTypes = {
selectorNode: ColorSelectorNode,
};
const CustomNodeFlow = () => {
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setNodes((nds) =>
nds.map((node) => {
if (node.id !== '2') {
return node;
}
const color = event.target.value;
setBgColor(color);
return {
...node,
data: {
...node.data,
color,
},
};
})
);
};
setNodes([
{
id: '1',
type: 'input',
data: { label: 'An input node' },
position: { x: 0, y: 50 },
sourcePosition: Position.Right,
},
{
id: '2',
type: 'selectorNode',
data: { onChange: onChange, color: initBgColor },
style: { border: '1px solid #777', padding: 10 },
position: { x: 250, y: 50 },
},
{
id: '3',
type: 'output',
data: { label: 'Output A' },
position: { x: 550, y: 25 },
targetPosition: Position.Left,
},
{
id: '4',
type: 'output',
data: { label: 'Output B' },
position: { x: 550, y: 100 },
targetPosition: Position.Left,
},
]);
setEdges([
{ id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } },
{ id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } },
{ id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } },
]);
}, []);
const onConnect = (params: Connection | Edge) =>
setEdges((eds) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, eds));
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
style={{ background: bgColor }}
onLoad={onLoad}
nodeTypes={nodeTypes}
connectionLineStyle={connectionLineStyle}
snapToGrid={true}
snapGrid={snapGrid}
defaultZoom={1.5}
>
<MiniMap
nodeStrokeColor={(n: Node): string => {
if (n.type === 'input') return '#0041d0';
if (n.type === 'selectorNode') return bgColor;
if (n.type === 'output') return '#ff0072';
return '#eee';
}}
nodeColor={(n: Node): string => {
if (n.type === 'selectorNode') return bgColor;
return '#fff';
}}
/>
<Controls />
</ReactFlow>
);
};
export default CustomNodeFlow;
+19 -10
View File
@@ -8,25 +8,27 @@ import ReactFlow, {
Node, Node,
NodeChange, NodeChange,
applyNodeChanges, applyNodeChanges,
Connection,
addEdge,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import { getElements } from './utils'; import { getNodesAndEdges } from './utils';
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 }; const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 };
const onLoad = (reactFlowInstance: OnLoadParams) => { const onLoad = (reactFlowInstance: OnLoadParams) => {
reactFlowInstance.fitView(); reactFlowInstance.fitView();
console.log(reactFlowInstance.getElements()); console.log(reactFlowInstance.getNodes());
}; };
const initialElements = getElements(30, 30); const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(30, 30);
const StressFlow = () => { const StressFlow = () => {
const [nodes, setNodes] = useState<Node[]>(initialElements.nodes); const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialElements.edges); const [edges, setEdges] = useState<Edge[]>(initialEdges);
// const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const onConnect = useCallback((params: Edge | Connection) => {
// const onConnect = (params: Connection | Edge, nds: Node[]) => setElements((els) => addEdge(params, els)); setEdges((eds) => addEdge(params, eds));
}, []);
const updatePos = () => { const updatePos = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
@@ -43,7 +45,7 @@ const StressFlow = () => {
const updateElements = () => { const updateElements = () => {
const grid = Math.ceil(Math.random() * 10); const grid = Math.ceil(Math.random() * 10);
const initialElements = getElements(grid, grid); const initialElements = getNodesAndEdges(grid, grid);
setNodes(initialElements.nodes); setNodes(initialElements.nodes);
setEdges(initialElements.edges); setEdges(initialElements.edges);
}; };
@@ -53,7 +55,14 @@ const StressFlow = () => {
}, []); }, []);
return ( return (
<ReactFlow nodes={nodes} edges={edges} onLoad={onLoad} onNodesChange={onNodesChange}> <ReactFlow
onlyRenderVisibleElements
nodes={nodes}
edges={edges}
onLoad={onLoad}
onConnect={onConnect}
onNodesChange={onNodesChange}
>
<MiniMap /> <MiniMap />
<Controls /> <Controls />
<Background /> <Background />
+1 -1
View File
@@ -5,7 +5,7 @@ type ElementsCollection = {
edges: Edge[]; edges: Edge[];
}; };
export function getElements(xElements: number = 10, yElements: number = 10): ElementsCollection { export function getNodesAndEdges(xElements: number = 10, yElements: number = 10): ElementsCollection {
const initialNodes = []; const initialNodes = [];
const initialEdges: Edge[] = []; const initialEdges: Edge[] = [];
let nodeId = 1; let nodeId = 1;
+5
View File
@@ -5,6 +5,7 @@ import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router
import Basic from './Basic'; import Basic from './Basic';
import UpdateNode from './UpdateNode'; import UpdateNode from './UpdateNode';
import Stress from './Stress'; import Stress from './Stress';
import CustomNode from './CustomNode';
import './index.css'; import './index.css';
@@ -21,6 +22,10 @@ const routes = [
path: '/stress', path: '/stress',
component: Stress, component: Stress,
}, },
{
path: '/custom-node',
component: CustomNode,
},
]; ];
const Header = withRouter(({ history, location }) => { const Header = withRouter(({ history, location }) => {
+2 -6
View File
@@ -1,8 +1,6 @@
import { MouseEvent as ReactMouseEvent } from 'react'; import { MouseEvent as ReactMouseEvent } from 'react';
import { GetState } from 'zustand';
import { getHostForElement } from '../../utils'; import { getHostForElement } from '../../utils';
import { ReactFlowState } from '../../types';
import { import {
ElementId, ElementId,
@@ -105,8 +103,7 @@ export function onMouseDown(
onEdgeUpdateEnd?: (evt: MouseEvent) => void, onEdgeUpdateEnd?: (evt: MouseEvent) => void,
onConnectStart?: OnConnectStartFunc, onConnectStart?: OnConnectStartFunc,
onConnectStop?: OnConnectStopFunc, onConnectStop?: OnConnectStopFunc,
onConnectEnd?: OnConnectEndFunc, onConnectEnd?: OnConnectEndFunc
getState?: GetState<ReactFlowState>
): void { ): void {
const reactFlowNode = (event.target as Element).closest('.react-flow'); const reactFlowNode = (event.target as Element).closest('.react-flow');
// when react-flow is used inside a shadow root we can't use document // when react-flow is used inside a shadow root we can't use document
@@ -180,8 +177,7 @@ export function onMouseDown(
onConnectStop?.(event); onConnectStop?.(event);
if (isValid) { if (isValid) {
const nodes = getState?.().nodes; onConnect?.(connection);
onConnect?.(connection, nodes || []);
} }
onConnectEnd?.(event); onConnectEnd?.(event);
+4 -4
View File
@@ -4,7 +4,7 @@ import shallow from 'zustand/shallow';
import { useStore } from '../../store'; import { useStore } from '../../store';
import NodeIdContext from '../../contexts/NodeIdContext'; import NodeIdContext from '../../contexts/NodeIdContext';
import { HandleProps, Connection, ElementId, Position, Node, ReactFlowState } from '../../types'; import { HandleProps, Connection, ElementId, Position, ReactFlowState } from '../../types';
import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler';
@@ -52,9 +52,9 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
const isTarget = type === 'target'; const isTarget = type === 'target';
const onConnectExtended = useCallback( const onConnectExtended = useCallback(
(params: Connection, nodes: Node[]) => { (params: Connection) => {
onConnectAction?.(params, nodes); onConnectAction?.(params);
onConnect?.(params, nodes); onConnect?.(params);
}, },
[onConnectAction, onConnect] [onConnectAction, onConnect]
); );
+9 -5
View File
@@ -12,6 +12,7 @@ const selector = (s: ReactFlowState) => ({
unsetNodesSelection: s.unsetNodesSelection, unsetNodesSelection: s.unsetNodesSelection,
updateNodePosition: s.updateNodePosition, updateNodePosition: s.updateNodePosition,
updateNodeDimensions: s.updateNodeDimensions, updateNodeDimensions: s.updateNodeDimensions,
unselectNodesAndEdges: s.unselectNodesAndEdges,
}); });
export default (NodeComponent: ComponentType<NodeComponentProps>) => { export default (NodeComponent: ComponentType<NodeComponentProps>) => {
@@ -48,10 +49,13 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
resizeObserver, resizeObserver,
dragHandle, dragHandle,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const { addSelectedElements, unsetNodesSelection, updateNodePosition, updateNodeDimensions } = useStore( const {
selector, addSelectedElements,
shallow unselectNodesAndEdges,
); unsetNodesSelection,
updateNodePosition,
updateNodeDimensions,
} = useStore(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]);
@@ -142,8 +146,8 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
addSelectedElements([node]); addSelectedElements([node]);
} }
} else if (!selectNodesOnDrag && !isSelected && isSelectable) { } else if (!selectNodesOnDrag && !isSelected && isSelectable) {
unselectNodesAndEdges();
unsetNodesSelection(); unsetNodesSelection();
addSelectedElements([]);
} }
}, },
[node, isSelected, selectNodesOnDrag, isSelectable, onNodeDragStart] [node, isSelected, selectNodesOnDrag, isSelectable, onNodeDragStart]
+9 -9
View File
@@ -1,26 +1,26 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
import { Elements, ReactFlowState } from '../../types'; import { ReactFlowState, OnSelectionChangeFunc } from '../../types';
import { useStore } from '../../store'; import { useStore } from '../../store';
interface SelectionListenerProps { interface SelectionListenerProps {
onSelectionChange: (elements: Elements | null) => void; onSelectionChange: OnSelectionChangeFunc;
} }
const selectedElementsSelector = (s: ReactFlowState) => [ const selectedElementsSelector = (s: ReactFlowState) => ({
...s.nodes.filter((n) => n.isSelected), selectedNodes: s.nodes.filter((n) => n.isSelected),
...s.edges.filter((e) => e.isSelected), selectedEdges: 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.
export default ({ onSelectionChange }: SelectionListenerProps) => { export default ({ onSelectionChange }: SelectionListenerProps) => {
const selectedElements = useStore(selectedElementsSelector, shallow); const { selectedNodes, selectedEdges } = useStore(selectedElementsSelector, shallow);
useEffect(() => { useEffect(() => {
onSelectionChange(selectedElements); onSelectionChange({ nodes: selectedNodes, edges: selectedEdges });
}, [selectedElements]); }, [selectedNodes, selectedEdges]);
return null; return null;
}; };
+39 -21
View File
@@ -4,11 +4,10 @@ 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, getSourceTargetNodes } from './utils'; import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from './utils';
import { import {
Position, Position,
Edge, Edge,
Node,
Connection, Connection,
ConnectionLineType, ConnectionLineType,
ConnectionLineComponent, ConnectionLineComponent,
@@ -19,8 +18,6 @@ import {
} from '../../types'; } from '../../types';
interface EdgeRendererProps { interface EdgeRendererProps {
nodes: Node[];
edges: Edge[];
edgeTypes: any; edgeTypes: any;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
@@ -162,26 +159,12 @@ const Edge = memo(
targetPosition targetPosition
); );
// const isVisible = onlyRenderVisibleElements
// ? isEdgeVisible({
// sourcePos: { x: sourceX, y: sourceY },
// targetPos: { x: targetX, y: targetY },
// width,
// height,
// transform,
// })
// : true;
// if (!isVisible) {
// return null;
// }
return ( return (
<EdgeComponent <EdgeComponent
key={edge.id} key={edge.id}
id={edge.id} id={edge.id}
className={edge.className} className={edge.className}
type={edge.type} type={edgeType}
data={edge.data} data={edge.data}
onClick={onEdgeClick} onClick={onEdgeClick}
isSelected={!!edge.isSelected} isSelected={!!edge.isSelected}
@@ -233,6 +216,7 @@ const selector = (s: ReactFlowState) => ({
width: s.width, width: s.width,
height: s.height, height: s.height,
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
nodes: s.nodes,
}); });
const EdgeRenderer = (props: EdgeRendererProps) => { const EdgeRenderer = (props: EdgeRendererProps) => {
@@ -247,8 +231,42 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
width, width,
height, height,
connectionMode, connectionMode,
nodes,
} = useStore(selector, shallow); } = useStore(selector, shallow);
const edges = useStore(
useCallback(
(s: ReactFlowState) => {
if (!props.onlyRenderVisibleElements) {
return s.edges;
}
return s.edges.filter((e) => {
const { sourceNode, targetNode } = getSourceTargetNodes(e, s.nodes);
return (
sourceNode?.width &&
sourceNode?.height &&
targetNode?.width &&
targetNode?.height &&
isEdgeVisible({
sourcePos: sourceNode.position,
targetPos: targetNode.position,
sourceWidth: sourceNode.width,
sourceHeight: sourceNode.height,
targetWidth: targetNode.width,
targetHeight: targetNode.height,
width: s.width,
height: s.height,
transform: s.transform,
})
);
});
},
[props.onlyRenderVisibleElements]
)
);
if (!width) { if (!width) {
return null; return null;
} }
@@ -260,8 +278,8 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
<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={`translate(${transform[0]},${transform[1]}) scale(${transform[2]})`}> <g transform={`translate(${transform[0]},${transform[1]}) scale(${transform[2]})`}>
{props.edges.map((edge: Edge) => { {edges.map((edge: Edge) => {
const { sourceNode, targetNode } = getSourceTargetNodes(edge, props.nodes); const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes);
return ( return (
<Edge <Edge
key={edge.id} key={edge.id}
+17 -3
View File
@@ -116,17 +116,31 @@ export const getEdgePositions = (
interface IsEdgeVisibleParams { interface IsEdgeVisibleParams {
sourcePos: XYPosition; sourcePos: XYPosition;
targetPos: XYPosition; targetPos: XYPosition;
sourceWidth: number;
sourceHeight: number;
targetWidth: number;
targetHeight: number;
width: number; width: number;
height: number; height: number;
transform: Transform; transform: Transform;
} }
export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }: IsEdgeVisibleParams): boolean { export function isEdgeVisible({
sourcePos,
targetPos,
sourceWidth,
sourceHeight,
targetWidth,
targetHeight,
width,
height,
transform,
}: IsEdgeVisibleParams): boolean {
const edgeBox = { const edgeBox = {
x: Math.min(sourcePos.x, targetPos.x), x: Math.min(sourcePos.x, targetPos.x),
y: Math.min(sourcePos.y, targetPos.y), y: Math.min(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x, targetPos.x), x2: Math.max(sourcePos.x + sourceWidth, targetPos.x + targetWidth),
y2: Math.max(sourcePos.y, targetPos.y), y2: Math.max(sourcePos.y + sourceHeight, targetPos.y + targetHeight),
}; };
if (edgeBox.x === edgeBox.x2) { if (edgeBox.x === edgeBox.x2) {
-3
View File
@@ -13,7 +13,6 @@ import { ReactFlowState } from '../../types';
interface FlowRendererProps interface FlowRendererProps
extends Omit< extends Omit<
GraphViewProps, GraphViewProps,
| 'elements'
| 'snapToGrid' | 'snapToGrid'
| 'nodeTypes' | 'nodeTypes'
| 'edgeTypes' | 'edgeTypes'
@@ -22,8 +21,6 @@ interface FlowRendererProps
| 'arrowHeadColor' | 'arrowHeadColor'
| 'onlyRenderVisibleElements' | 'onlyRenderVisibleElements'
| 'selectNodesOnDrag' | 'selectNodesOnDrag'
| 'nodes'
| 'edges'
> { > {
children: ReactNode; children: ReactNode;
} }
+4 -8
View File
@@ -4,14 +4,14 @@ 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';
import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph'; import { onLoadProject, onLoadGetNodes, onLoadGetEdges, onLoadToObject } from '../../utils/graph';
import useZoomPanHelper from '../../hooks/useZoomPanHelper'; import useZoomPanHelper from '../../hooks/useZoomPanHelper';
import { ReactFlowProps } from '../ReactFlow'; import { ReactFlowProps } from '../ReactFlow';
import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types';
export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange'> { export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges'> {
nodeTypes: NodeTypesType; nodeTypes: NodeTypesType;
edgeTypes: EdgeTypesType; edgeTypes: EdgeTypesType;
selectionKeyCode: KeyCode; selectionKeyCode: KeyCode;
@@ -26,8 +26,6 @@ export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange'
} }
const GraphView = ({ const GraphView = ({
nodes,
edges,
nodeTypes, nodeTypes,
edgeTypes, edgeTypes,
onMove, onMove,
@@ -97,7 +95,8 @@ const GraphView = ({
zoomTo, zoomTo,
setTransform: transform, setTransform: transform,
project: onLoadProject(store.getState), project: onLoadProject(store.getState),
getElements: onLoadGetElements(store.getState), getNodes: onLoadGetNodes(store.getState),
getEdges: onLoadGetEdges(store.getState),
toObject: onLoadToObject(store.getState), toObject: onLoadToObject(store.getState),
}); });
} }
@@ -147,7 +146,6 @@ const GraphView = ({
onNodeDragStart={onNodeDragStart} onNodeDragStart={onNodeDragStart}
selectNodesOnDrag={selectNodesOnDrag} selectNodesOnDrag={selectNodesOnDrag}
onlyRenderVisibleElements={onlyRenderVisibleElements} onlyRenderVisibleElements={onlyRenderVisibleElements}
nodes={nodes}
/> />
<EdgeRenderer <EdgeRenderer
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
@@ -167,8 +165,6 @@ const GraphView = ({
onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius} edgeUpdaterRadius={edgeUpdaterRadius}
nodes={nodes}
edges={edges}
/> />
</FlowRenderer> </FlowRenderer>
); );
+13 -6
View File
@@ -1,8 +1,9 @@
import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import React, { memo, useMemo, ComponentType, MouseEvent, useCallback } from 'react';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types';
import { getNodesInside } from '../../utils/graph';
interface NodeRendererProps { interface NodeRendererProps {
nodeTypes: NodeTypesType; nodeTypes: NodeTypesType;
selectNodesOnDrag: boolean; selectNodesOnDrag: boolean;
@@ -16,7 +17,6 @@ interface NodeRendererProps {
onNodeDrag?: (event: MouseEvent, node: Node) => void; onNodeDrag?: (event: MouseEvent, node: Node) => void;
onNodeDragStop?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: (event: MouseEvent, node: Node) => void;
onlyRenderVisibleElements: boolean; onlyRenderVisibleElements: boolean;
nodes: Node[];
} }
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
@@ -40,9 +40,16 @@ const NodeRenderer = (props: NodeRendererProps) => {
snapToGrid, snapToGrid,
} = useStore(selector, shallow); } = useStore(selector, shallow);
// const visibleNodes = props.onlyRenderVisibleElements const nodes = useStore(
// ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) useCallback(
// : nodes; (s: ReactFlowState) => {
return props.onlyRenderVisibleElements
? getNodesInside(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: s.nodes;
},
[props.onlyRenderVisibleElements]
)
);
const transformStyle = useMemo( const transformStyle = useMemo(
() => ({ () => ({
@@ -68,7 +75,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
return ( return (
<div className="react-flow__nodes" style={transformStyle}> <div className="react-flow__nodes" style={transformStyle}>
{props.nodes.map((node) => { {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>;
+2 -4
View File
@@ -19,7 +19,7 @@ import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../compon
import { createEdgeTypes } from '../EdgeRenderer/utils'; import { createEdgeTypes } from '../EdgeRenderer/utils';
import Wrapper from './Wrapper'; import Wrapper from './Wrapper';
import { import {
Elements, OnSelectionChangeFunc,
NodeTypesType, NodeTypesType,
EdgeTypesType, EdgeTypesType,
OnLoadFunc, OnLoadFunc,
@@ -81,7 +81,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
onMove?: (flowTransform?: FlowTransform) => void; onMove?: (flowTransform?: FlowTransform) => void;
onMoveStart?: (flowTransform?: FlowTransform) => void; onMoveStart?: (flowTransform?: FlowTransform) => void;
onMoveEnd?: (flowTransform?: FlowTransform) => void; onMoveEnd?: (flowTransform?: FlowTransform) => void;
onSelectionChange?: (elements: Elements | null) => void; onSelectionChange?: OnSelectionChangeFunc;
onSelectionDragStart?: (event: ReactMouseEvent, nodes: Node[]) => void; onSelectionDragStart?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: ReactMouseEvent, nodes: Node[]) => void; onSelectionDrag?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: ReactMouseEvent, nodes: Node[]) => void; onSelectionDragStop?: (event: ReactMouseEvent, nodes: Node[]) => void;
@@ -227,8 +227,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
<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}
-1
View File
@@ -15,7 +15,6 @@ export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/uti
export { export {
isNode, isNode,
isEdge, isEdge,
removeElements,
addEdge, addEdge,
getOutgoers, getOutgoers,
getIncomers, getIncomers,
+39 -33
View File
@@ -10,7 +10,6 @@ import {
NodeDimensionUpdate, NodeDimensionUpdate,
NodeDiffUpdate, NodeDiffUpdate,
XYPosition, XYPosition,
Elements,
InitD3ZoomPayload, InitD3ZoomPayload,
TranslateExtent, TranslateExtent,
NodeExtent, NodeExtent,
@@ -33,14 +32,11 @@ import { getHandleBounds } from '../components/Nodes/utils';
const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>(); const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>();
const unselectElements = (elements: Elements): NodeChange[] | EdgeChange[] => const createNodeOrEdgeSelectionChange = (isSelected: boolean) => (item: Node | Edge) => ({
elements id: item.id,
.filter((e) => e.isSelected) type: 'select',
.map((e) => ({ isSelected,
id: e.id, });
type: 'select',
isSelected: false,
}));
const createStore = () => const createStore = () =>
create<ReactFlowState>((set, get) => ({ create<ReactFlowState>((set, get) => ({
@@ -219,8 +215,12 @@ const createStore = () =>
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id); const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id); const selectedNodeIds = selectedNodes.map((n) => n.id);
onNodesChange?.(nodes.map((n) => ({ id: n.id, type: 'select', isSelected: selectedNodeIds.includes(n.id) }))); onNodesChange?.(
onEdgesChange?.(edges.map((e) => ({ id: e.id, type: 'select', isSelected: selectedEdgeIds.includes(e.id) }))); nodes.map((n) => createNodeOrEdgeSelectionChange(selectedNodeIds.includes(n.id))(n)) as NodeChange[]
);
onEdgesChange?.(
edges.map((e) => createNodeOrEdgeSelectionChange(selectedEdgeIds.includes(e.id))(e)) as EdgeChange[]
);
set({ set({
userSelectionRect: nextUserSelectRect, userSelectionRect: nextUserSelectRect,
@@ -248,31 +248,22 @@ const createStore = () =>
set(stateUpdate); set(stateUpdate);
}, },
addSelectedElements: (elements: Elements) => { addSelectedElements: (selectedElementsArr: Array<Node | Edge>) => {
const { multiSelectionActive, onNodesChange, onEdgesChange, nodes, edges } = get(); const { multiSelectionActive, onNodesChange, onEdgesChange, nodes, edges } = get();
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
let changedNodes; let changedNodes;
let changedEdges; let changedEdges;
if (multiSelectionActive) { if (multiSelectionActive) {
changedNodes = selectedElementsArr changedNodes = selectedElementsArr.filter(isNode).map(createNodeOrEdgeSelectionChange(true));
.filter(isNode) changedEdges = selectedElementsArr.filter(isEdge).map(createNodeOrEdgeSelectionChange(true));
.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, createNodeOrEdgeSelectionChange(selectedElementsArr.some((e) => e.id === node.id))(node)
type: 'select', );
isSelected: selectedElementsArr.some((e) => e.id === node.id), changedEdges = edges.map((edge) =>
})); createNodeOrEdgeSelectionChange(selectedElementsArr.some((e) => e.id === edge.id))(edge)
changedEdges = edges.map((edge) => ({ );
id: edge.id,
type: 'select',
isSelected: selectedElementsArr.some((e) => e.id === edge.id),
}));
} }
if (changedNodes.length) { if (changedNodes.length) {
@@ -283,6 +274,21 @@ const createStore = () =>
onEdgesChange?.(changedEdges as EdgeChange[]); onEdgesChange?.(changedEdges as EdgeChange[]);
} }
}, },
unselectNodesAndEdges: () => {
const { nodes, edges, onNodesChange, onEdgesChange } = get();
const nodesToUnselect = nodes.map((n) => {
n.isSelected = false;
return createNodeOrEdgeSelectionChange(false)(n);
}) as NodeChange[];
const edgesToUnselect = edges.map(createNodeOrEdgeSelectionChange(false)) as EdgeChange[];
if (nodesToUnselect.length) {
onNodesChange?.(nodesToUnselect);
}
if (edgesToUnselect.length) {
onEdgesChange?.(edgesToUnselect);
}
},
initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) =>
set({ set({
d3Zoom, d3Zoom,
@@ -312,14 +318,14 @@ const createStore = () =>
resetSelectedElements: () => { resetSelectedElements: () => {
const { nodes, edges, onNodesChange, onEdgesChange } = get(); const { nodes, edges, onNodesChange, onEdgesChange } = get();
const nodesToUnselect = unselectElements(nodes) as NodeChange[]; const nodesToUnselect = nodes.filter((e) => e.isSelected).map(createNodeOrEdgeSelectionChange(false));
const edgesToUnselect = unselectElements(edges) as EdgeChange[]; const edgesToUnselect = edges.filter((e) => e.isSelected).map(createNodeOrEdgeSelectionChange(false));
if (nodesToUnselect.length) { if (nodesToUnselect.length) {
onNodesChange?.(nodesToUnselect); onNodesChange?.(nodesToUnselect as NodeChange[]);
} }
if (edgesToUnselect.length) { if (edgesToUnselect.length) {
onEdgesChange?.(edgesToUnselect); onEdgesChange?.(edgesToUnselect as EdgeChange[]);
} }
}, },
setNodeExtent: (nodeExtent: NodeExtent) => setNodeExtent: (nodeExtent: NodeExtent) =>
+6 -2
View File
@@ -312,7 +312,8 @@ export type OnLoadParams<T = any> = {
zoomTo: (zoomLevel: number) => void; zoomTo: (zoomLevel: number) => void;
fitView: FitViewFunc; fitView: FitViewFunc;
project: ProjectFunc; project: ProjectFunc;
getElements: () => Elements<T>; getNodes: () => Node<T>[];
getEdges: () => Edge<T>[];
setTransform: (transform: FlowTransform) => void; setTransform: (transform: FlowTransform) => void;
toObject: ToObjectFunc<T>; toObject: ToObjectFunc<T>;
}; };
@@ -353,7 +354,7 @@ export type ConnectionLineComponentProps = {
export type ConnectionLineComponent = React.ComponentType<ConnectionLineComponentProps>; export type ConnectionLineComponent = React.ComponentType<ConnectionLineComponentProps>;
export type OnConnectFunc = (connection: Connection, nodes: Node[]) => void; export type OnConnectFunc = (connection: Connection) => void;
export type OnConnectStartParams = { export type OnConnectStartParams = {
nodeId: ElementId | null; nodeId: ElementId | null;
handleId: ElementId | null; handleId: ElementId | null;
@@ -490,6 +491,7 @@ export interface ReactFlowState {
unsetUserSelection: () => void; unsetUserSelection: () => void;
unsetNodesSelection: () => void; unsetNodesSelection: () => void;
resetSelectedElements: () => void; resetSelectedElements: () => void;
unselectNodesAndEdges: () => void;
addSelectedElements: (elements: Elements) => void; addSelectedElements: (elements: Elements) => void;
updateTransform: (transform: Transform) => void; updateTransform: (transform: Transform) => void;
updateSize: (size: Dimensions) => void; updateSize: (size: Dimensions) => void;
@@ -522,3 +524,5 @@ export interface ReactFlowState {
} }
export type UpdateNodeInternals = (nodeId: ElementId) => void; export type UpdateNodeInternals = (nodeId: ElementId) => void;
export type OnSelectionChangeFunc = (params: { nodes: Node[]; edges: Edge[] }) => void;
+32 -36
View File
@@ -24,52 +24,38 @@ export const isEdge = (element: Node | Connection | Edge): element is Edge =>
export const isNode = (element: Node | Connection | Edge): element is Node => export const isNode = (element: Node | Connection | Edge): element is Node =>
'id' in element && !('source' in element) && !('target' in element); 'id' in element && !('source' in element) && !('target' in element);
export const getOutgoers = (node: Node, elements: Elements): Node[] => { export const getOutgoers = (node: Node, nodes: Node[], edges: Edge[]): Node[] => {
if (!isNode(node)) { if (!isNode(node)) {
return []; return [];
} }
const outgoerIds = elements.filter((e) => isEdge(e) && e.source === node.id).map((e) => (e as Edge).target); const outgoerIds = edges.filter((e) => e.source === node.id).map((e) => e.target);
return elements.filter((e) => outgoerIds.includes(e.id)) as Node[]; return nodes.filter((n) => outgoerIds.includes(n.id));
}; };
export const getIncomers = (node: Node, elements: Elements): Node[] => { export const getIncomers = (node: Node, nodes: Node[], edges: Edge[]): Node[] => {
if (!isNode(node)) { if (!isNode(node)) {
return []; return [];
} }
const incomersIds = elements.filter((e) => isEdge(e) && e.target === node.id).map((e) => (e as Edge).source); const incomersIds = edges.filter((e) => e.target === node.id).map((e) => e.source);
return elements.filter((e) => incomersIds.includes(e.id)) as Node[]; return nodes.filter((n) => incomersIds.includes(n.id));
};
export const removeElements = (elementsToRemove: Elements, elements: Elements): Elements => {
const nodeIdsToRemove = elementsToRemove.map((n) => n.id);
return elements.filter((element) => {
const edgeElement = element as Edge;
return !(
nodeIdsToRemove.includes(element.id) ||
nodeIdsToRemove.includes(edgeElement.target) ||
nodeIdsToRemove.includes(edgeElement.source)
);
});
}; };
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId => const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection): ElementId =>
`reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`; `reactflow__edge-${source}${sourceHandle}-${target}${targetHandle}`;
const connectionExists = (edge: Edge, elements: Elements) => { const connectionExists = (edge: Edge, edges: Edge[]) => {
return elements.some( return edges.some(
(el) => (e) =>
isEdge(el) && edge.source === e.source &&
el.source === edge.source && edge.target === e.target &&
el.target === edge.target && (edge.sourceHandle === e.sourceHandle || (!edge.sourceHandle && !e.sourceHandle)) &&
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) && (edge.targetHandle === e.targetHandle || (!edge.targetHandle && !e.targetHandle))
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle))
); );
}; };
export const addEdge = (edgeParams: Edge | Connection, nodes: Node[], edges: Edge[]): Edge[] => { export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => {
if (!edgeParams.source || !edgeParams.target) { if (!edgeParams.source || !edgeParams.target) {
console.warn("Can't create edge. An edge needs a source and a target."); console.warn("Can't create edge. An edge needs a source and a target.");
return edges; return edges;
@@ -85,7 +71,7 @@ export const addEdge = (edgeParams: Edge | Connection, nodes: Node[], edges: Edg
} as Edge; } as Edge;
} }
if (connectionExists(edge, nodes)) { if (connectionExists(edge, edges)) {
return edges; return edges;
} }
@@ -211,7 +197,13 @@ export const getNodesInside = (
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap); const overlappingArea = Math.ceil(xOverlap * yOverlap);
if (width === null || height === null || isDragging) { if (
typeof width === 'undefined' ||
typeof height === 'undefined' ||
width === null ||
height === null ||
isDragging
) {
// nodes are initialized with width and height = null // nodes are initialized with width and height = null
return true; return true;
} }
@@ -232,15 +224,19 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target)); return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target));
}; };
const parseElements = (nodes: Node[], edges: Edge[]): Elements => { export const onLoadGetNodes = (getState: GetState<ReactFlowState>) => {
return [...nodes.map((n) => ({ ...n })), ...edges.map((e) => ({ ...e }))]; return (): Node[] => {
const { nodes = [] } = getState();
return nodes.map((n) => ({ ...n }));
};
}; };
export const onLoadGetElements = (getState: GetState<ReactFlowState>) => { export const onLoadGetEdges = (getState: GetState<ReactFlowState>) => {
return (): Elements => { return (): Edge[] => {
const { nodes = [], edges = [] } = getState(); const { edges = [] } = getState();
return parseElements(nodes, edges); return edges.map((e) => ({ ...e }));
}; };
}; };