Merge pull request #1644 from wbkd/refactor/nested-relative

new node and edge handling, state management, subflows, layering
This commit is contained in:
Christopher Möller
2021-11-18 18:09:21 +01:00
committed by GitHub
121 changed files with 3874 additions and 2573 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 = [
{ 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);
});
});
});
+27
View File
@@ -0,0 +1,27 @@
import { memo, FC, CSSProperties } from 'react';
import { Handle, NodeProps, Position } from 'react-flow-renderer';
const infoStyle: CSSProperties = { fontSize: 11 };
const idStyle: CSSProperties = {
fontSize: 10,
color: '#888899',
position: 'absolute',
top: 2,
left: 2,
};
const ColorSelectorNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
return (
<>
<Handle type="target" position={Position.Top} />
<div style={idStyle}>{id}</div>
<div style={infoStyle}>
x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex}
</div>
<Handle type="source" position={Position.Bottom} />
</>
);
};
export default memo(ColorSelectorNode);
+136 -34
View File
@@ -1,49 +1,126 @@
import React, { useState, MouseEvent } from 'react';
import { useState, MouseEvent, useCallback } from 'react';
import ReactFlow, {
removeElements,
addEdge,
isNode,
Background,
Elements,
BackgroundVariant,
FlowElement,
applyNodeChanges,
applyEdgeChanges,
MiniMap,
Controls,
Node,
Edge,
Connection,
NodeChange,
EdgeChange,
OnLoadParams,
Connection,
} from 'react-flow-renderer';
import DebugNode from './DebugNode';
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 initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{
id: '4',
data: { label: 'Node 4' },
position: { x: 100, y: 200 },
className: 'light',
style: { backgroundColor: 'rgba(255,50, 50, 0.5)', width: 500, height: 300 },
},
{
id: '4a',
data: { label: 'Node 4a' },
position: { x: 15, y: 15 },
className: 'light',
parentNode: '4',
extent: 'parent',
},
{
id: '4b',
data: { label: 'Node 4b' },
position: { x: 150, y: 50 },
className: 'light',
style: { backgroundColor: 'rgba(50, 50, 255, 0.5)', height: 200, width: 300 },
parentNode: '4',
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 20, y: 20 },
className: 'light',
parentNode: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 100, y: 100 },
className: 'light',
parentNode: '4b',
},
{
id: '5',
data: { label: 'Node 5' },
position: { x: 650, y: 250 },
className: 'light',
style: { backgroundColor: 'rgba(20 ,200, 255, 1.5)', width: 400, height: 150 },
zIndex: 1000,
},
{
id: '5a',
data: { label: 'Node 5a' },
position: { x: 25, y: 50 },
className: 'light',
parentNode: '5',
},
{
id: '5b',
data: { label: 'Node 5b' },
position: { x: 225, y: 50 },
className: 'light',
parentNode: '5',
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', zIndex: 100 },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
{ id: '3-5', source: '3', target: '5' },
];
const nodeTypes = {
default: DebugNode,
};
const BasicFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els));
const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance);
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => {
return addEdge(params, eds);
});
}, []);
const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []);
const updatePos = () => {
setElements((elms) => {
return elms.map((el) => {
if (isNode(el)) {
el.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
}
setNodes((nds) => {
return nds.map((n) => {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return el;
return n;
});
});
};
@@ -52,31 +129,53 @@ const BasicFlow = () => {
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setElements((elms) => {
return elms.map((el) => {
if (isNode(el)) {
el.className = el.className === 'light' ? 'dark' : 'light';
}
return el;
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
});
});
};
const toggleChildNodes = () => {
setNodes((nds) => {
return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden;
return n;
});
});
};
const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log('node change', changes);
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return (
<ReactFlow
elements={elements}
nodes={nodes}
edges={edges}
onLoad={onLoad}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}
nodeTypes={nodeTypes}
>
<Background variant={BackgroundVariant.Lines} />
<MiniMap />
<Controls />
<Background />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
@@ -88,6 +187,9 @@ const BasicFlow = () => {
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
toggleChildNodes
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
+35 -20
View File
@@ -1,27 +1,30 @@
import React, { useState, useEffect, MouseEvent } from 'react';
import { useState, useEffect, MouseEvent, useCallback } from 'react';
import { ChangeEvent } from 'react';
import ReactFlow, {
isEdge,
removeElements,
addEdge,
MiniMap,
Controls,
Node,
FlowElement,
OnLoadParams,
Elements,
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 onLoad = (reactFlowInstance: OnLoadParams) => {
console.log('flow loaded:', reactFlowInstance);
reactFlowInstance.fitView();
};
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 initBgColor = '#1A192B';
@@ -32,15 +35,16 @@ const nodeTypes = {
};
const CustomNodeFlow = () => {
const [elements, setElements] = useState<Elements>([]);
const [nodes, setNodes] = useState<Node[]>([]);
const [edges, setEdges] = useState<Edge[]>([]);
const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setElements((els) =>
els.map((e) => {
if (isEdge(e) || e.id !== '2') {
return e;
setNodes((nds) =>
nds.map((node) => {
if (node.id !== '2') {
return node;
}
const color = event.target.value;
@@ -48,9 +52,9 @@ const CustomNodeFlow = () => {
setBgColor(color);
return {
...e,
...node,
data: {
...e.data,
...node.data,
color,
},
};
@@ -58,7 +62,7 @@ const CustomNodeFlow = () => {
);
};
setElements([
setNodes([
{
id: '1',
type: 'input',
@@ -87,22 +91,33 @@ const CustomNodeFlow = () => {
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 onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) =>
setElements((els) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, els));
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
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
style={{ background: bgColor }}
@@ -16,7 +16,9 @@ const FloatingConnectionLine: FC<ConnectionLineComponentProps> = ({
const targetNode = {
id: 'connection-target',
__rf: { width: 1, height: 1, position: { x: targetX, y: targetY } },
width: 1,
height: 1,
position: { x: targetX, y: targetY },
} as Node;
const { sx, sy } = getEdgeParams(sourceNode, targetNode);
+7 -5
View File
@@ -1,13 +1,15 @@
import { FC, useMemo, CSSProperties } from 'react';
import { EdgeProps, useStoreState, getBezierPath } from 'react-flow-renderer';
import { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'react-flow-renderer';
import { getEdgeParams } from './utils';
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const nodes = useStoreState((state) => state.nodes);
const nodeSelector = (s: ReactFlowState) => s.nodeInternals;
const sourceNode = useMemo(() => nodes.find((n) => n.id === source), [source, nodes]);
const targetNode = useMemo(() => nodes.find((n) => n.id === target), [target, nodes]);
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const nodeInternals = useStore(nodeSelector);
const sourceNode = useMemo(() => nodeInternals.get(source), [source, nodeInternals]);
const targetNode = useMemo(() => nodeInternals.get(target), [target, nodeInternals]);
if (!sourceNode || !targetNode) {
return null;
+25 -11
View File
@@ -1,14 +1,17 @@
import React, { useState } from 'react';
import { useState, useCallback } from 'react';
import ReactFlow, {
removeElements,
addEdge,
Background,
OnLoadParams,
EdgeTypesType,
Elements,
Node,
Connection,
Edge,
applyNodeChanges,
applyEdgeChanges,
NodeChange,
EdgeChange,
} from 'react-flow-renderer';
import './style.css';
@@ -19,24 +22,35 @@ import { createElements } from './utils';
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
const initialElements: Elements = createElements();
const { nodes: initialNodes, edges: initialEdges } = createElements();
const edgeTypes: EdgeTypesType = {
floating: FloatingEdge,
};
const NodeAsHandleFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const FloatingEdges = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => addEdge(params, eds));
}, []);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge({ ...params, type: 'floating' }, els));
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return (
<div className="floatingedges">
<ReactFlow
elements={elements}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onLoad={onLoad}
edgeTypes={edgeTypes}
@@ -48,4 +62,4 @@ const NodeAsHandleFlow = () => {
);
};
export default NodeAsHandleFlow;
export default FloatingEdges;
+22 -15
View File
@@ -1,18 +1,19 @@
import { Position, ArrowHeadType, Node, XYPosition } from 'react-flow-renderer';
import { Position, ArrowHeadType, XYPosition, Node, Edge } from 'react-flow-renderer';
// this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const {
width: intersectionNodeWidth,
height: intersectionNodeHeight,
position: intersectionNodePosition,
} = intersectionNode.__rf;
const targetPosition = targetNode.__rf.position;
} = intersectionNode;
const targetPosition = targetNode.position;
const w = intersectionNodeWidth / 2;
const h = intersectionNodeHeight / 2;
const w = (intersectionNodeWidth ?? 0) / 2;
const h = (intersectionNodeHeight ?? 0) / 2;
const x2 = intersectionNodePosition.x + w;
const y2 = intersectionNodePosition.y + h;
@@ -32,7 +33,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPositi
// returns the position (top,right,bottom or right) passed node compared to the intersection point
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
const n = { ...node.__rf.position, ...node.__rf };
const n = { ...node.position, ...node };
const nx = Math.round(n.x);
const ny = Math.round(n.y);
const px = Math.round(intersectionPoint.x);
@@ -41,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) {
return Position.Left;
}
if (px >= nx + n.width - 1) {
if (px >= nx + (n.width ?? 0) - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
if (py >= n.y + n.height - 1) {
if (py >= n.y + (n.height ?? 0) - 1) {
return Position.Bottom;
}
@@ -72,11 +73,18 @@ export function getEdgeParams(source: Node, target: Node) {
};
}
export function createElements() {
const elements = [];
type NodesAndEdges = {
nodes: Node[];
edges: Edge[];
};
export function createElements(): NodesAndEdges {
const nodes: Node[] = [];
const edges: Edge[] = [];
const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
elements.push({ id: 'target', data: { label: 'Target' }, position: center });
nodes.push({ id: 'target', data: { label: 'Target' }, position: center });
for (let i = 0; i < 8; i++) {
const degrees = i * (360 / 8);
@@ -84,16 +92,15 @@ export function createElements() {
const x = 250 * Math.cos(radians) + center.x;
const y = 250 * Math.sin(radians) + center.y;
elements.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
elements.push({
edges.push({
id: `edge-${i}`,
target: 'target',
source: `${i}`,
type: 'floating',
arrowHeadType: ArrowHeadType.Arrow,
});
}
return elements;
return { nodes, edges };
}
+26 -17
View File
@@ -1,34 +1,43 @@
import React, { useState } from 'react';
import { useState, useCallback } from 'react';
import { useEffect } from 'react';
import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Elements } from 'react-flow-renderer';
import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Node } from 'react-flow-renderer';
const initialElements: Elements = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
const initialNodes: Node[] = [
{ id: '1', type: 'input', hidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', hidden: true, data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', hidden: true, data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', hidden: true, data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4' },
];
const setHidden = (hidden: boolean) => (els: any[]) =>
els.map((e: any) => {
e.hidden = hidden;
return e;
});
const HiddenFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const [isHidden, setIsHidden] = useState<boolean>(false);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const [isHidden, setIsHidden] = useState<boolean>(true);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => addEdge(params, eds));
}, []);
useEffect(() => {
setElements((els) =>
els.map((e) => {
e.isHidden = isHidden;
return e;
})
);
setNodes(setHidden(isHidden));
setNodes(setHidden(isHidden));
}, [isHidden]);
return (
<ReactFlow elements={elements} onConnect={onConnect}>
<ReactFlow nodes={nodes} edges={edges} onConnect={onConnect}>
<MiniMap />
<Controls />
+42 -29
View File
@@ -1,74 +1,87 @@
import React, { useState } from 'react';
import { useState, useCallback } from 'react';
import ReactFlow, {
ReactFlowProvider,
addEdge,
removeElements,
applyNodeChanges,
applyEdgeChanges,
Controls,
isNode,
Elements,
NodeChange,
EdgeChange,
Node,
Connection,
Edge,
NodeExtent,
CoordinateExtent,
Position,
} from 'react-flow-renderer';
import dagre from 'dagre';
import initialElements from './initial-elements';
import initialNodesAndEdges from './initial-elements';
import './layouting.css';
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeExtent: NodeExtent = [
const nodeExtent: CoordinateExtent = [
[0, 0],
[1000, 1000],
];
const LayoutFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const [nodes, setNodes] = useState<Node[]>(initialNodesAndEdges.nodes);
const [edges, setEdges] = useState<Edge[]>(initialNodesAndEdges.edges);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => {
return addEdge(params, eds);
});
}, []);
const onLayout = (direction: string) => {
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
elements.forEach((el) => {
if (isNode(el)) {
dagreGraph.setNode(el.id, { width: 150, height: 50 });
} else {
dagreGraph.setEdge(el.source, el.target);
}
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: 150, height: 50 });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
const layoutedElements = elements.map((el) => {
if (isNode(el)) {
const nodeWithPosition = dagreGraph.node(el.id);
el.targetPosition = isHorizontal ? Position.Left : Position.Top;
el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// we need to pass a slightly different position in order to notify react flow about the change
// @TODO how can we change the position handling so that we dont need this hack?
el.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y };
}
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// we need to pass a slightly different position in order to notify react flow about the change
// @TODO how can we change the position handling so that we dont need this hack?
node.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y };
return el;
return node;
});
setElements(layoutedElements);
setNodes(layoutedNodes);
};
const onNodesChange = useCallback((changes: NodeChange[]) => setNodes((ns) => applyNodeChanges(changes, ns)), []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return (
<div className="layoutflow">
<ReactFlowProvider>
<ReactFlow
elements={elements}
nodes={nodes}
edges={edges}
onConnect={onConnect}
onElementsRemove={onElementsRemove}
nodeExtent={nodeExtent}
onLoad={() => onLayout('TB')}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
>
<Controls />
</ReactFlow>
+8 -3
View File
@@ -1,8 +1,8 @@
import { Elements, XYPosition } from 'react-flow-renderer';
import { Node, Edge, XYPosition } from 'react-flow-renderer';
const position: XYPosition = { x: 0, y: 0 };
const elements: Elements = [
const nodes: Node[] = [
{
id: '1',
type: 'input',
@@ -56,6 +56,9 @@ const elements: Elements = [
position,
},
{ id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } },
];
const edges: Edge[] = [
{ id: 'e12', source: '1', target: '2', type: 'smoothstep' },
{ id: 'e13', source: '1', target: '3', type: 'smoothstep' },
{ id: 'e22a', source: '2', target: '2a', type: 'smoothstep' },
@@ -68,4 +71,6 @@ const elements: Elements = [
{ id: 'e57', source: '5', target: '7', type: 'smoothstep' },
];
export default elements;
const nodesAndEdges = { nodes, edges };
export default nodesAndEdges;
+186
View File
@@ -0,0 +1,186 @@
import { useState, MouseEvent, useCallback } from 'react';
import ReactFlow, {
addEdge,
Background,
applyNodeChanges,
applyEdgeChanges,
MiniMap,
Controls,
Node,
Edge,
NodeChange,
EdgeChange,
OnLoadParams,
Connection,
} from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 },
},
{
id: '2a',
data: { label: 'Node 2a' },
position: { x: 10, y: 50 },
parentNode: '2',
},
{ id: '3', data: { label: 'Node 3' }, position: { x: 320, y: 100 }, className: 'light' },
{
id: '4',
data: { label: 'Node 4' },
position: { x: 320, y: 200 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 },
},
{
id: '4a',
data: { label: 'Node 4a' },
position: { x: 15, y: 65 },
className: 'light',
parentNode: '4',
extent: 'parent',
},
{
id: '4b',
data: { label: 'Node 4b' },
position: { x: 15, y: 120 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 255, 0.7)', height: 150, width: 270 },
parentNode: '4',
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 20, y: 40 },
className: 'light',
parentNode: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 100, y: 100 },
className: 'light',
parentNode: '4b',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2a-4a', source: '2a', target: '4a' },
{ id: 'e3-4', source: '3', target: '4' },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
];
function shuffle() {
return Math.random() - Math.random();
}
const NestedFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => {
return addEdge(params, eds);
});
}, []);
const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []);
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return n;
});
});
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
});
});
};
const toggleChildNodes = () => {
setNodes((nds) => {
return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden;
return n;
});
});
};
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}
onLoad={onLoad}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}
>
<MiniMap />
<Controls />
<Background />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
toggleChildNodes
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default NestedFlow;
+33 -28
View File
@@ -1,58 +1,63 @@
import React, { useState, CSSProperties } from 'react';
import { useState, CSSProperties, useCallback } from 'react';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
isNode,
Controls,
Background,
OnLoadParams,
Elements,
Connection,
Edge,
Node,
NodeChange,
applyNodeChanges,
Connection,
addEdge,
} from 'react-flow-renderer';
import { getElements } from './utils';
import { getNodesAndEdges } from './utils';
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 };
const onLoad = (reactFlowInstance: OnLoadParams) => {
reactFlowInstance.fitView();
console.log(reactFlowInstance.getElements());
console.log(reactFlowInstance.getNodes());
};
const initialElements: Elements = getElements(30, 30);
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(30, 30);
const StressFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onConnect = useCallback((params: Edge | Connection) => {
setEdges((eds) => addEdge(params, eds));
}, []);
const updatePos = () => {
setElements((elms) => {
return elms.map((el) => {
if (isNode(el)) {
return {
...el,
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
};
}
return el;
setNodes((nds) => {
return nds.map((n) => {
return {
...n,
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
};
});
});
};
const updateElements = () => {
const grid = Math.ceil(Math.random() * 10);
setElements(getElements(grid, grid));
const initialElements = getNodesAndEdges(grid, grid);
setNodes(initialElements.nodes);
setEdges(initialElements.edges);
};
const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log('node change', changes);
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
return (
<ReactFlow elements={elements} onLoad={onLoad} onElementsRemove={onElementsRemove} onConnect={onConnect}>
<ReactFlow nodes={nodes} edges={edges} onLoad={onLoad} onConnect={onConnect} onNodesChange={onNodesChange}>
<MiniMap />
<Controls />
<Background />
+15 -6
View File
@@ -1,7 +1,13 @@
import { Elements } from 'react-flow-renderer';
import { Node, Edge } from 'react-flow-renderer';
export function getElements(xElements: number = 10, yElements: number = 10): Elements {
const initialElements = [];
type ElementsCollection = {
nodes: Node[];
edges: Edge[];
};
export function getNodesAndEdges(xElements: number = 10, yElements: number = 10): ElementsCollection {
const initialNodes = [];
const initialEdges: Edge[] = [];
let nodeId = 1;
let recentNodeId = null;
@@ -15,10 +21,10 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele
data,
position,
};
initialElements.push(node);
initialNodes.push(node);
if (recentNodeId && nodeId <= xElements * yElements) {
initialElements.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() });
initialEdges.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() });
}
recentNodeId = nodeId;
@@ -26,5 +32,8 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele
}
}
return initialElements;
return {
nodes: initialNodes,
edges: initialEdges,
};
}
+32 -25
View File
@@ -1,64 +1,71 @@
import React, { useEffect, useState } from 'react';
import ReactFlow, { Elements } from 'react-flow-renderer';
import { useEffect, useState, useCallback } from 'react';
import ReactFlow, { Node, Edge, applyNodeChanges, NodeChange } from 'react-flow-renderer';
import './updatenode.css';
const initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', data: { label: '-' }, position: { x: 100, y: 100 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } },
{ id: 'e1-2', source: '1', target: '2' },
];
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const UpdateNode = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const [nodeName, setNodeName] = useState<string>('Node 1');
const [nodeBg, setNodeBg] = useState<string>('#eee');
const [nodeHidden, setNodeHidden] = useState<boolean>(false);
useEffect(() => {
setElements((els) =>
els.map((el) => {
if (el.id === '1') {
setNodes((nds) =>
nds.map((n) => {
if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change
el.data = {
...el.data,
n.data = {
...n.data,
label: nodeName,
};
}
return el;
return n;
})
);
}, [nodeName, setElements]);
}, [nodeName]);
useEffect(() => {
setElements((els) =>
els.map((el) => {
if (el.id === '1') {
setNodes((nds) =>
nds.map((n) => {
if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change
el.style = { ...el.style, backgroundColor: nodeBg };
n.style = { ...n.style, backgroundColor: nodeBg };
}
return el;
return n;
})
);
}, [nodeBg, setElements]);
}, [nodeBg]);
useEffect(() => {
setElements((els) =>
els.map((el) => {
if (el.id === '1' || el.id === 'e1-2') {
setNodes((nds) =>
nds.map((n) => {
if (n.id === '1' || n.id === 'e1-2') {
// when you update a simple type you can just update the value
el.isHidden = nodeHidden;
n.hidden = nodeHidden;
}
return el;
return n;
})
);
}, [nodeHidden, setElements]);
}, [nodeHidden]);
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
return (
<ReactFlow elements={elements} defaultZoom={1.5} minZoom={0.2} maxZoom={4}>
<ReactFlow nodes={nodes} edges={edges} defaultZoom={1.5} minZoom={0.2} maxZoom={4} onNodesChange={onNodesChange}>
<div className="updatenode__controls">
<label>label:</label>
<input value={nodeName} onChange={(evt) => setNodeName(evt.target.value)} />
+20 -110
View File
@@ -1,140 +1,50 @@
import React, { ChangeEvent } from 'react';
import { ChangeEvent } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom';
import Overview from './Overview';
import Basic from './Basic';
import CustomNode from './CustomNode';
import Stress from './Stress';
import Interaction from './Interaction';
import Empty from './Empty';
import Edges from './Edges';
import Validation from './Validation';
import Provider from './Provider';
import Hidden from './Hidden';
import EdgeTypes from './EdgeTypes';
import CustomConnectionLine from './CustomConnectionLine';
import FloatingEdges from './FloatingEdges';
import NodeTypeChange from './NodeTypeChange';
import NodeTypesObjectChange from './NodeTypesObjectChange';
import UpdatableEdge from './UpdatableEdge';
import UpdateNode from './UpdateNode';
import SaveRestore from './SaveRestore';
import DragNDrop from './DragNDrop';
import Layout from './Layouting';
import SwitchFlows from './Switch';
import UseZoomPanHelper from './UseZoomPanHelper';
import UseUpdateNodeInternals from './UseUpdateNodeInternals';
import Undirectional from './Undirectional';
import MultiFlows from './MultiFlows';
import DragHandle from './DragHandle';
import Stress from './Stress';
import CustomNode from './CustomNode';
import FloatingEdges from './FloatingEdges';
import Layouting from './Layouting';
import NestedNodes from './NestedNodes';
import Hidden from './Hidden';
import './index.css';
const routes = [
{
path: '/',
component: Overview,
},
{
path: '/edges',
component: Edges,
},
{
path: '/custom-node',
component: CustomNode,
},
{
path: '/validation',
component: Validation,
},
{
path: '/provider',
component: Provider,
},
{
path: '/stress',
component: Stress,
},
{
path: '/interaction',
component: Interaction,
},
{
path: '/basic',
component: Basic,
},
{
path: '/empty',
component: Empty,
},
{
path: '/hidden',
component: Hidden,
},
{
path: '/edge-types',
component: EdgeTypes,
},
{
path: '/custom-connectionline',
component: CustomConnectionLine,
},
{
path: '/floating-edges',
component: FloatingEdges,
},
{
path: '/nodetype-change',
component: NodeTypeChange,
},
{
path: '/nodetypesobject-change',
component: NodeTypesObjectChange,
},
{
path: '/updatable-edge',
component: UpdatableEdge,
},
{
path: '/update-node',
component: UpdateNode,
},
{
path: '/save-restore',
component: SaveRestore,
path: '/stress',
component: Stress,
},
{
path: '/drag-and-drop',
component: DragNDrop,
path: '/custom-node',
component: CustomNode,
},
{
path: '/floating-edges',
component: FloatingEdges,
},
{
path: '/layouting',
component: Layout,
component: Layouting,
},
{
path: '/switch',
component: SwitchFlows,
path: '/nested-nodes',
component: NestedNodes,
},
{
path: '/usezoompanhelper',
component: UseZoomPanHelper,
},
{
path: '/useupdatenodeinternals',
component: UseUpdateNodeInternals,
},
{
path: '/undirectional',
component: Undirectional,
},
{
path: '/multiflows',
component: MultiFlows,
},
{
path: '/draghandle',
component: DragHandle,
path: '/hidden',
component: Hidden,
},
];
+98
View File
@@ -0,0 +1,98 @@
import React, { useState, MouseEvent } from 'react';
import ReactFlow, {
removeElements,
addEdge,
isNode,
Background,
Elements,
BackgroundVariant,
FlowElement,
Node,
Edge,
Connection,
OnLoadParams,
} from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
const BasicFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
// const onElementsRemove = (elementsToRemove: Elements) => setNodes((els) => removeElements(elementsToRemove, els));
// const onConnect = (params: Edge | Connection) => setNodes((els) => addEdge(params, els));
const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance);
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return n;
});
});
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
});
});
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onLoad={onLoad}
onElementClick={onElementClick}
// onElementsRemove={onElementsRemove}
// onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
>
<Background variant={BackgroundVariant.Lines} />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default BasicFlow;
@@ -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);
+135
View File
@@ -0,0 +1,135 @@
import React, { useState, useEffect, MouseEvent } from 'react';
import { ChangeEvent } from 'react';
import ReactFlow, {
isEdge,
removeElements,
addEdge,
MiniMap,
Controls,
Node,
FlowElement,
OnLoadParams,
Elements,
Position,
SnapGrid,
Connection,
Edge,
} 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 onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16];
const nodeTypes = {
selectorNode: ColorSelectorNode,
};
const CustomNodeFlow = () => {
const [elements, setElements] = useState<Elements>([]);
const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setElements((els) =>
els.map((e) => {
if (isEdge(e) || e.id !== '2') {
return e;
}
const color = event.target.value;
setBgColor(color);
return {
...e,
data: {
...e.data,
color,
},
};
})
);
};
setElements([
{
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,
},
{ 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 onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) =>
setElements((els) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, els));
return (
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
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;
+70
View File
@@ -0,0 +1,70 @@
import React, { useState, CSSProperties } from 'react';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
isNode,
Controls,
Background,
OnLoadParams,
Elements,
Connection,
Edge,
} from 'react-flow-renderer';
import { getElements } from './utils';
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 };
const onLoad = (reactFlowInstance: OnLoadParams) => {
reactFlowInstance.fitView();
console.log(reactFlowInstance.getElements());
};
const initialElements: Elements = getElements(30, 30);
const StressFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const updatePos = () => {
setElements((elms) => {
return elms.map((el) => {
if (isNode(el)) {
return {
...el,
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
};
}
return el;
});
});
};
const updateElements = () => {
const grid = Math.ceil(Math.random() * 10);
setElements(getElements(grid, grid));
};
return (
<ReactFlow elements={elements} onLoad={onLoad} onElementsRemove={onElementsRemove} onConnect={onConnect}>
<MiniMap />
<Controls />
<Background />
<div style={buttonWrapperStyles}>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={updateElements}>update elements</button>
</div>
</ReactFlow>
);
};
export default StressFlow;
+30
View File
@@ -0,0 +1,30 @@
import { Elements } from 'react-flow-renderer';
export function getElements(xElements: number = 10, yElements: number = 10): Elements {
const initialElements = [];
let nodeId = 1;
let recentNodeId = null;
for (let y = 0; y < yElements; y++) {
for (let x = 0; x < xElements; x++) {
const position = { x: x * 100, y: y * 50 };
const data = { label: `Node ${nodeId}` };
const node = {
id: nodeId.toString(),
style: { width: 50, fontSize: 11 },
data,
position,
};
initialElements.push(node);
if (recentNodeId && nodeId <= xElements * yElements) {
initialElements.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() });
}
recentNodeId = nodeId;
nodeId++;
}
}
return initialElements;
}
+78
View File
@@ -0,0 +1,78 @@
import React, { useEffect, useState } from 'react';
import ReactFlow, { Elements } from 'react-flow-renderer';
import './updatenode.css';
const initialElements: Elements = [
{ id: '1', data: { label: '-' }, position: { x: 100, y: 100 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } },
{ id: 'e1-2', source: '1', target: '2' },
];
const UpdateNode = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const [nodeName, setNodeName] = useState<string>('Node 1');
const [nodeBg, setNodeBg] = useState<string>('#eee');
const [nodeHidden, setNodeHidden] = useState<boolean>(false);
useEffect(() => {
setElements((els) =>
els.map((el) => {
if (el.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change
el.data = {
...el.data,
label: nodeName,
};
}
return el;
})
);
}, [nodeName, setElements]);
useEffect(() => {
setElements((els) =>
els.map((el) => {
if (el.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change
el.style = { ...el.style, backgroundColor: nodeBg };
}
return el;
})
);
}, [nodeBg, setElements]);
useEffect(() => {
setElements((els) =>
els.map((el) => {
if (el.id === '1' || el.id === 'e1-2') {
// when you update a simple type you can just update the value
el.isHidden = nodeHidden;
}
return el;
})
);
}, [nodeHidden, setElements]);
return (
<ReactFlow elements={elements} defaultZoom={1.5} minZoom={0.2} maxZoom={4}>
<div className="updatenode__controls">
<label>label:</label>
<input value={nodeName} onChange={(evt) => setNodeName(evt.target.value)} />
<label className="updatenode__bglabel">background:</label>
<input value={nodeBg} onChange={(evt) => setNodeBg(evt.target.value)} />
<div className="updatenode__checkboxwrapper">
<label>hidden:</label>
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
</div>
</div>
</ReactFlow>
);
};
export default UpdateNode;
@@ -0,0 +1,21 @@
.updatenode__controls {
position: absolute;
right: 10px;
top: 10px;
z-index: 4;
font-size: 12px;
}
.updatenode__controls label {
display: block;
}
.updatenode__bglabel {
margin-top: 10px;
}
.updatenode__checkboxwrapper {
margin-top: 10px;
display: flex;
align-items: center;
}
+82
View File
@@ -0,0 +1,82 @@
body {
font-family: sans-serif;
color: #111;
}
html,
body,
#root {
margin: 0;
height: 100%;
}
#root {
display: flex;
flex-direction: column;
}
header {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
font-weight: 700;
align-items: center;
}
.logo {
text-decoration: none;
display: block;
line-height: 1;
}
header a,
header a:focus,
header a:active,
header a:visited {
color: #111;
}
header a:hover {
color: #333;
}
header select {
margin-left: 1em;
}
.overview-example__add {
display: none;
}
.react-flow__node a {
font-weight: 700;
color: #111;
}
.react-flow__node.dark-node {
background: #0041d0;
color: #f8f8f8;
}
.react-flow__node.dark {
background: #557;
color: #f8f8f8;
}
.react-flow__node-selectorNode {
font-size: 12px;
background: #f0f2f3;
border: 1px solid 555;
border-radius: 5px;
text-align: center;
}
.react-flow__node-selectorNode .react-flow__handle {
border-color: #f0f2f3;
}
@media screen and (min-width: 768px) {
.overview-example__add {
display: block;
}
}
+165
View File
@@ -0,0 +1,165 @@
import React, { ChangeEvent } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom';
import Overview from './Overview';
import Basic from './Basic';
import CustomNode from './CustomNode';
import Stress from './Stress';
import Interaction from './Interaction';
import Empty from './Empty';
import Edges from './Edges';
import Validation from './Validation';
import Provider from './Provider';
import Hidden from './Hidden';
import EdgeTypes from './EdgeTypes';
import CustomConnectionLine from './CustomConnectionLine';
import NodeTypeChange from './NodeTypeChange';
import NodeTypesObjectChange from './NodeTypesObjectChange';
import UpdatableEdge from './UpdatableEdge';
import UpdateNode from './UpdateNode';
import SaveRestore from './SaveRestore';
import DragNDrop from './DragNDrop';
import Layout from './Layouting';
import SwitchFlows from './Switch';
import UseZoomPanHelper from './UseZoomPanHelper';
import UseUpdateNodeInternals from './UseUpdateNodeInternals';
import Undirectional from './Undirectional';
import MultiFlows from './MultiFlows';
import DragHandle from './DragHandle';
import './index.css';
const routes = [
{
path: '/',
component: Overview,
},
{
path: '/edges',
component: Edges,
},
{
path: '/custom-node',
component: CustomNode,
},
{
path: '/validation',
component: Validation,
},
{
path: '/provider',
component: Provider,
},
{
path: '/stress',
component: Stress,
},
{
path: '/interaction',
component: Interaction,
},
{
path: '/basic',
component: Basic,
},
{
path: '/empty',
component: Empty,
},
{
path: '/hidden',
component: Hidden,
},
{
path: '/edge-types',
component: EdgeTypes,
},
{
path: '/custom-connectionline',
component: CustomConnectionLine,
},
{
path: '/nodetype-change',
component: NodeTypeChange,
},
{
path: '/nodetypesobject-change',
component: NodeTypesObjectChange,
},
{
path: '/updatable-edge',
component: UpdatableEdge,
},
{
path: '/update-node',
component: UpdateNode,
},
{
path: '/save-restore',
component: SaveRestore,
},
{
path: '/drag-and-drop',
component: DragNDrop,
},
{
path: '/layouting',
component: Layout,
},
{
path: '/switch',
component: SwitchFlows,
},
{
path: '/usezoompanhelper',
component: UseZoomPanHelper,
},
{
path: '/useupdatenodeinternals',
component: UseUpdateNodeInternals,
},
{
path: '/undirectional',
component: Undirectional,
},
{
path: '/multiflows',
component: MultiFlows,
},
{
path: '/draghandle',
component: DragHandle,
},
];
const Header = withRouter(({ history, location }) => {
const onChange = (event: ChangeEvent<HTMLSelectElement>) => history.push(event.target.value);
return (
<header>
<a className="logo" href="https://github.com/wbkd/react-flow">
React Flow Dev
</a>
<select defaultValue={location.pathname} onChange={onChange}>
{routes.map((route) => (
<option value={route.path} key={route.path}>
{route.path === '/' ? 'overview' : route.path.substr(1, route.path.length)}
</option>
))}
</select>
</header>
);
});
ReactDOM.render(
<Router forceRefresh={true}>
<Header />
<Switch>
{routes.map((route) => (
<Route exact path={route.path} render={() => <route.component />} key={route.path} />
))}
</Switch>
</Router>,
document.getElementById('root')
);
+1
View File
@@ -0,0 +1 @@
/// <reference types="react-scripts" />
+3 -8
View File
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@@ -20,7 +16,6 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
"include": ["src"],
"exclude": ["src_oldapi"]
}
+63 -7
View File
@@ -1,12 +1,12 @@
{
"name": "react-flow-renderer",
"version": "10.0.0-next.2",
"version": "10.0.0-next.16",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "react-flow-renderer",
"version": "10.0.0-next.2",
"version": "10.0.0-next.16",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.15.4",
@@ -15,10 +15,11 @@
"classcat": "^5.0.3",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"fast-deep-equal": "^3.1.3",
"react-draggable": "^4.4.4",
"react-redux": "^7.2.5",
"redux": "^4.1.1"
"redux": "^4.1.1",
"redux-thunk": "^2.3.0",
"zustand": "^3.5.13"
},
"devDependencies": {
"@babel/core": "^7.15.8",
@@ -32,7 +33,8 @@
"@svgr/rollup": "^5.5.0",
"@types/redux": "^3.6.31",
"@types/resize-observer-browser": "^0.1.6",
"autoprefixer": "^10.3.7",
"@welldone-software/why-did-you-render": "^6.2.1",
"autoprefixer": "^10.3.6",
"babel-preset-react-app": "^10.0.0",
"cypress": "^8.5.0",
"postcss": "^8.3.9",
@@ -2836,6 +2838,18 @@
"@types/node": "*"
}
},
"node_modules/@welldone-software/why-did-you-render": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-6.2.1.tgz",
"integrity": "sha512-eIVKeK6ueS3tuzCqMVTaaNrPYvb9cA8NHiNgLA7Op8SD4TiT31zqNjxmhzLEK+y3sBxcwr6YhsiQGX9EThrvaw==",
"dev": true,
"dependencies": {
"lodash": "^4"
},
"peerDependencies": {
"react": "^16 || ^17"
}
},
"node_modules/aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -5255,7 +5269,8 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-glob": {
"version": "3.2.7",
@@ -9255,6 +9270,11 @@
"@babel/runtime": "^7.9.2"
}
},
"node_modules/redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
},
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -11229,6 +11249,22 @@
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/zustand": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.13.tgz",
"integrity": "sha512-orO/XcYwSWffsrPVTdCtuKM/zkUaOIyKDasOk/lecsD3R0euELsj+cB65uKZ1KyinrK2STHIuUhRoLpH8QprQg==",
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"react": ">=16.8"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
}
},
"dependencies": {
@@ -13290,6 +13326,15 @@
"@types/node": "*"
}
},
"@welldone-software/why-did-you-render": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-6.2.1.tgz",
"integrity": "sha512-eIVKeK6ueS3tuzCqMVTaaNrPYvb9cA8NHiNgLA7Op8SD4TiT31zqNjxmhzLEK+y3sBxcwr6YhsiQGX9EThrvaw==",
"dev": true,
"requires": {
"lodash": "^4"
}
},
"aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
@@ -15147,7 +15192,8 @@
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"fast-glob": {
"version": "3.2.7",
@@ -18028,6 +18074,11 @@
"@babel/runtime": "^7.9.2"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
},
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -19538,6 +19589,11 @@
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"zustand": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.13.tgz",
"integrity": "sha512-orO/XcYwSWffsrPVTdCtuKM/zkUaOIyKDasOk/lecsD3R0euELsj+cB65uKZ1KyinrK2STHIuUhRoLpH8QprQg=="
}
}
}
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "react-flow-renderer",
"version": "10.0.0-next.2",
"version": "10.0.0-next.16",
"engines": {
"node": ">=12"
},
@@ -37,10 +37,11 @@
"classcat": "^5.0.3",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"fast-deep-equal": "^3.1.3",
"react-draggable": "^4.4.4",
"react-redux": "^7.2.5",
"redux": "^4.1.1"
"redux": "^4.1.1",
"redux-thunk": "^2.3.0",
"zustand": "^3.5.13"
},
"devDependencies": {
"@babel/core": "^7.15.8",
@@ -54,7 +55,8 @@
"@svgr/rollup": "^5.5.0",
"@types/redux": "^3.6.31",
"@types/resize-observer-browser": "^0.1.6",
"autoprefixer": "^10.3.7",
"@welldone-software/why-did-you-render": "^6.2.1",
"autoprefixer": "^10.3.6",
"babel-preset-react-app": "^10.0.0",
"cypress": "^8.5.0",
"postcss": "^8.3.9",
@@ -1,8 +1,8 @@
import React, { memo, useMemo, FC, HTMLAttributes } from 'react';
import cc from 'classcat';
import { useStoreState } from '../../store/hooks';
import { BackgroundVariant } from '../../types';
import { useStore } from '../../store';
import { BackgroundVariant, ReactFlowState } from '../../types';
import { createGridLinesPath, createGridDotsPath } from './utils';
export interface BackgroundProps extends HTMLAttributes<SVGElement> {
@@ -17,6 +17,8 @@ const defaultColors = {
[BackgroundVariant.Lines]: '#eee',
};
const transformSelector = (s: ReactFlowState) => s.transform;
const Background: FC<BackgroundProps> = ({
variant = BackgroundVariant.Dots,
gap = 15,
@@ -25,11 +27,11 @@ const Background: FC<BackgroundProps> = ({
style,
className,
}) => {
const [x, y, scale] = useStoreState((s) => s.transform);
const [x, y, scale] = useStore(transformSelector);
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
const patternId = useMemo(() => `pattern-${Math.floor(Math.random() * 100000)}`, []);
const bgClasses = cc(['react-flow__background', className]);
const bgClasses = cc(['react-flow__background', 'react-flow__container', className]);
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
const yOffset = y % scaledGap;
+7 -4
View File
@@ -1,7 +1,7 @@
import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react';
import cc from 'classcat';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { useStore } from '../../store';
import PlusIcon from '../../../assets/icons/plus.svg';
import MinusIcon from '../../../assets/icons/minus.svg';
@@ -10,7 +10,7 @@ import LockIcon from '../../../assets/icons/lock.svg';
import UnlockIcon from '../../../assets/icons/unlock.svg';
import useZoomPanHelper from '../../hooks/useZoomPanHelper';
import { FitViewParams } from '../../types';
import { FitViewParams, ReactFlowState } from '../../types';
export interface ControlProps extends HTMLAttributes<HTMLDivElement> {
showZoom?: boolean;
@@ -31,6 +31,9 @@ export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...
</button>
);
const setInteractiveSelector = (s: ReactFlowState) => s.setInteractive;
const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable;
const Controls: FC<ControlProps> = ({
style,
showZoom = true,
@@ -45,10 +48,10 @@ const Controls: FC<ControlProps> = ({
children,
}) => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const setInteractive = useStoreActions((actions) => actions.setInteractive);
const setInteractive = useStore(setInteractiveSelector);
const isInteractive = useStore(isInteractiveSelector);
const { zoomIn, zoomOut, fitView } = useZoomPanHelper();
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
const mapClasses = cc(['react-flow__controls', className]);
const onZoomInHandler = useCallback(() => {
@@ -26,7 +26,7 @@ const MiniMapNode = ({
strokeWidth,
className,
borderRadius,
shapeRendering
shapeRendering,
}: MiniMapNodeProps) => {
const { background, backgroundColor } = style || {};
const fill = (color || background || backgroundColor) as string;
+43 -30
View File
@@ -1,9 +1,11 @@
import React, { memo, HTMLAttributes } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreState } from '../../store/hooks';
import { getRectOfNodes, getBoundsofRects } from '../../utils/graph';
import { Node, Rect } from '../../types';
import { useStore } from '../../store';
import { getRectOfNodes } from '../../utils/graph';
import { getBoundsofRects } from '../../utils';
import { Node, ReactFlowState, Rect } from '../../types';
import MiniMapNode from './MiniMapNode';
type StringFunc = (node: Node) => string;
@@ -22,6 +24,13 @@ declare const window: any;
const defaultWidth = 200;
const defaultHeight = 150;
const selector = (s: ReactFlowState) => ({
width: s.width,
height: s.height,
transform: s.transform,
nodeInternals: s.nodeInternals,
});
const MiniMap = ({
style,
className,
@@ -32,20 +41,20 @@ const MiniMap = ({
nodeStrokeWidth = 2,
maskColor = 'rgb(240, 242, 243, 0.7)',
}: MiniMapProps) => {
const containerWidth = useStoreState((s) => s.width);
const containerHeight = useStoreState((s) => s.height);
const [tX, tY, tScale] = useStoreState((s) => s.transform);
const nodes = useStoreState((s) => s.nodes);
const { width: containerWidth, height: containerHeight, transform, nodeInternals } = useStore(selector, shallow);
const [tX, tY, tScale] = transform;
const mapClasses = cc(['react-flow__minimap', className]);
const elementWidth = (style?.width || defaultWidth)! as number;
const elementHeight = (style?.height || defaultHeight)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc;
const nodeStrokeColorFunc = (nodeStrokeColor instanceof Function
? nodeStrokeColor
: () => nodeStrokeColor) as StringFunc;
const nodeStrokeColorFunc = (
nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
) as StringFunc;
const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc;
const hasNodes = nodes && nodes.length;
const hasNodes = nodeInternals && nodeInternals.size > 0;
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const bb = getRectOfNodes(nodes);
const viewBB: Rect = {
x: -tX / tScale,
@@ -64,7 +73,7 @@ const MiniMap = ({
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
const width = viewWidth + offset * 2;
const height = viewHeight + offset * 2;
const shapeRendering = (typeof window === "undefined" || !!window.chrome) ? "crispEdges" : "geometricPrecision";
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
return (
<svg
@@ -74,24 +83,28 @@ const MiniMap = ({
style={style}
className={mapClasses}
>
{nodes
.filter((node) => !node.isHidden)
.map((node) => (
<MiniMapNode
key={node.id}
x={node.__rf.position.x}
y={node.__rf.position.y}
width={node.__rf.width}
height={node.__rf.height}
style={node.style}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
/>
))}
{Array.from(nodeInternals)
.filter(([_, node]) => !node.hidden && node.width && node.height)
.map(([_, node]) => {
const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute;
return (
<MiniMapNode
key={node.id}
x={positionAbsolute?.x || 0}
y={positionAbsolute?.y || 0}
width={node.width!}
height={node.height!}
style={node.style}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
/>
);
})}
<path
className="react-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
@@ -1,16 +1,8 @@
import React, { FC, useMemo } from 'react';
import { Provider } from 'react-redux';
import React, { FC } from 'react';
import { initialState } from '../../store';
import configureStore from '../../store/configure-store';
import { Provider, createStore } from '../../store';
const ReactFlowProvider: FC = ({ children }) => {
const store = useMemo(() => {
return configureStore(initialState);
}, []);
return <Provider store={store}>{children}</Provider>;
};
const ReactFlowProvider: FC = ({ children }) => <Provider createStore={createStore}>{children}</Provider>;
ReactFlowProvider.displayName = 'ReactFlowProvider';
+27 -24
View File
@@ -1,32 +1,34 @@
import React, { useEffect, useState, CSSProperties } from 'react';
import React, { useRef, CSSProperties } from 'react';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import {
ElementId,
Node,
Transform,
NodeInternalsItem,
HandleElement,
Position,
ConnectionLineType,
ConnectionLineComponent,
HandleType,
Node,
ReactFlowState,
Position,
} from '../../types';
interface ConnectionLineProps {
connectionNodeId: ElementId;
connectionHandleId: ElementId | null;
connectionNodeId: string;
connectionHandleId: string | null;
connectionHandleType: HandleType;
connectionPositionX: number;
connectionPositionY: number;
connectionLineType: ConnectionLineType;
nodes: Node[];
transform: Transform;
isConnectable: boolean;
connectionLineStyle?: CSSProperties;
CustomConnectionLineComponent?: ConnectionLineComponent;
}
const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform });
export default ({
connectionNodeId,
connectionHandleId,
@@ -35,31 +37,32 @@ export default ({
connectionPositionX,
connectionPositionY,
connectionLineType = ConnectionLineType.Bezier,
nodes = [],
transform,
isConnectable,
CustomConnectionLineComponent,
}: ConnectionLineProps) => {
const [sourceNode, setSourceNode] = useState<Node | null>(null);
const nodeId = connectionNodeId;
const handleId = connectionHandleId;
useEffect(() => {
const nextSourceNode = nodes.find((n) => n.id === nodeId) || null;
setSourceNode(nextSourceNode);
}, []);
const { nodeInternals, transform } = useStore(selector, shallow);
const sourceNodeInternals = useRef<NodeInternalsItem | undefined>(nodeInternals.get(nodeId));
const sourceNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
if (!sourceNode || !isConnectable) {
if (
!sourceNode.current ||
!sourceNodeInternals.current ||
!isConnectable ||
!sourceNodeInternals.current.handleBounds?.[connectionHandleType]
) {
return null;
}
const sourceHandle = handleId
? sourceNode.__rf.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId)
: sourceNode.__rf.handleBounds[connectionHandleType][0];
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height;
const sourceX = sourceNode.__rf.position.x + sourceHandleX;
const sourceY = sourceNode.__rf.position.y + sourceHandleY;
? sourceNodeInternals.current.handleBounds[connectionHandleType]!.find((d: HandleElement) => d.id === handleId)
: sourceNodeInternals.current.handleBounds[connectionHandleType]![0];
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNodeInternals.current.width! / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNodeInternals.current.height!;
const sourceX = sourceNodeInternals.current.positionAbsolute!.x + sourceHandleX;
const sourceY = sourceNodeInternals.current.positionAbsolute!.y + sourceHandleY;
const targetX = (connectionPositionX - transform[0]) / transform[2];
const targetY = (connectionPositionY - transform[1]) / transform[2];
@@ -79,7 +82,7 @@ export default ({
targetPosition={targetPosition}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
sourceNode={sourceNode}
sourceNode={sourceNode.current as Node}
sourceHandle={sourceHandle}
/>
</g>
-1
View File
@@ -1,7 +1,6 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
import { getCenter } from './utils';
import { EdgeProps, Position } from '../../types';
+21 -11
View File
@@ -1,12 +1,21 @@
import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { Edge, EdgeProps, WrapEdgeProps } from '../../types';
import { useStore, useStoreApi } from '../../store';
import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState } from '../../types';
import { onMouseDown } from '../../components/Handle/handler';
import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
const selector = (s: ReactFlowState) => ({
addSelectedElements: s.addSelectedElements,
setConnectionNodeId: s.setConnectionNodeId,
unsetNodesSelection: s.unsetNodesSelection,
setPosition: s.setConnectionPosition,
connectionMode: s.connectionMode,
});
export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({
id,
@@ -33,7 +42,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
sourcePosition,
targetPosition,
elementsSelectable,
isHidden,
hidden,
sourceHandleId,
targetHandleId,
handleEdgeUpdate,
@@ -48,11 +57,11 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
markerEnd,
markerStart,
}: WrapEdgeProps): JSX.Element | null => {
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const connectionMode = useStoreState((state) => state.connectionMode);
const store = useStoreApi();
const { addSelectedElements, setConnectionNodeId, unsetNodesSelection, setPosition, connectionMode } = useStore(
selector,
shallow
);
const [updating, setUpdating] = useState<boolean>(false);
@@ -91,7 +100,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
if (elementsSelectable) {
unsetNodesSelection();
addSelectedElements(edgeElement);
addSelectedElements([edgeElement]);
}
onClick?.(event, edgeElement);
@@ -158,7 +167,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
isValidConnection,
connectionMode,
isSourceHandle ? 'target' : 'source',
_onEdgeUpdate
_onEdgeUpdate,
store.getState
);
},
[
@@ -194,7 +204,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]);
if (isHidden) {
if (hidden) {
return null;
}
-20
View File
@@ -1,20 +0,0 @@
import { useEffect } from 'react';
import { useStoreActions } from '../../store/hooks';
import { Elements } from '../../types';
interface ElementUpdaterProps {
elements: Elements;
}
const ElementUpdater = ({ elements }: ElementUpdaterProps) => {
const setElements = useStoreActions((actions) => actions.setElements);
useEffect(() => {
setElements(elements);
}, [elements]);
return null;
};
export default ElementUpdater;
+13 -14
View File
@@ -3,16 +3,15 @@ import { MouseEvent as ReactMouseEvent } from 'react';
import { getHostForElement } from '../../utils';
import {
ElementId,
XYPosition,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
OnConnectEndFunc,
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
ConnectionMode,
SetConnectionId,
Connection,
HandleType,
XYPosition,
} from '../../types';
type ValidConnectionFunc = (connection: Connection) => boolean;
@@ -32,8 +31,8 @@ function checkElementBelowIsValid(
event: MouseEvent,
connectionMode: ConnectionMode,
isTarget: boolean,
nodeId: ElementId,
handleId: ElementId | null,
nodeId: string,
handleId: string | null,
isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot
) {
@@ -91,19 +90,19 @@ function resetRecentHandle(hoveredHandle: Element): void {
export function onMouseDown(
event: ReactMouseEvent,
handleId: ElementId | null,
nodeId: ElementId,
handleId: string | null,
nodeId: string,
setConnectionNodeId: SetSourceIdFunc,
setPosition: SetPosition,
onConnect: OnConnectFunc,
onConnect: OnConnect,
isTarget: boolean,
isValidConnection: ValidConnectionFunc,
connectionMode: ConnectionMode,
elementEdgeUpdaterType?: HandleType,
onEdgeUpdateEnd?: (evt: MouseEvent) => void,
onConnectStart?: OnConnectStartFunc,
onConnectStop?: OnConnectStopFunc,
onConnectEnd?: OnConnectEndFunc
onConnectStart?: OnConnectStart,
onConnectStop?: OnConnectStop,
onConnectEnd?: OnConnectEnd
): void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
// when react-flow is used inside a shadow root we can't use document
+24 -10
View File
@@ -1,9 +1,10 @@
import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { useStore } from '../../store';
import NodeIdContext from '../../contexts/NodeIdContext';
import { HandleProps, Connection, ElementId, Position } from '../../types';
import { HandleProps, Connection, ReactFlowState, Position } from '../../types';
import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler';
@@ -11,6 +12,16 @@ const alwaysValid = () => true;
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
const selector = (s: ReactFlowState) => ({
setPosition: s.setConnectionPosition,
setConnectionNodeId: s.setConnectionNodeId,
onConnectAction: s.onConnect,
onConnectStart: s.onConnectStart,
onConnectStop: s.onConnectStop,
onConnectEnd: s.onConnectEnd,
connectionMode: s.connectionMode,
});
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
(
{
@@ -26,14 +37,17 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
},
ref
) => {
const nodeId = useContext(NodeIdContext) as ElementId;
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
const onConnectAction = useStoreState((state) => state.onConnect);
const onConnectStart = useStoreState((state) => state.onConnectStart);
const onConnectStop = useStoreState((state) => state.onConnectStop);
const onConnectEnd = useStoreState((state) => state.onConnectEnd);
const connectionMode = useStoreState((state) => state.connectionMode);
const nodeId = useContext(NodeIdContext) as string;
const {
setPosition,
setConnectionNodeId,
onConnectAction,
onConnectStart,
onConnectStop,
onConnectEnd,
connectionMode,
} = useStore(selector, shallow);
const handleId = id || null;
const isTarget = type === 'target';
+9 -7
View File
@@ -8,13 +8,15 @@ const DefaultNode = ({
isConnectable,
targetPosition = Position.Top,
sourcePosition = Position.Bottom,
}: NodeProps) => (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);
}: NodeProps) => {
return (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);
};
DefaultNode.displayName = 'DefaultNode';
+14 -16
View File
@@ -13,7 +13,7 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
export const getHandleBoundsByHandleType = (
selector: string,
nodeElement: HTMLDivElement,
parentBounds: ClientRect | DOMRect,
parentBounds: DOMRect,
k: number
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector);
@@ -24,20 +24,18 @@ export const getHandleBoundsByHandleType = (
const handlesArray = Array.from(handles) as HTMLDivElement[];
return handlesArray.map(
(handle): HandleElement => {
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
const handleId = handle.getAttribute('data-handleid');
const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position;
return handlesArray.map((handle): HandleElement => {
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
const handleId = handle.getAttribute('data-handleid');
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
return {
id: handleId,
position: handlePosition,
x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k,
...dimensions,
};
}
);
return {
id: handleId,
position: handlePosition,
x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k,
...dimensions,
};
});
};
+54 -56
View File
@@ -1,22 +1,21 @@
import React, {
useEffect,
useLayoutEffect,
useRef,
memo,
ComponentType,
CSSProperties,
useMemo,
MouseEvent,
useCallback,
} from 'react';
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions } from '../../store/hooks';
import { useStore } from '../../store';
import { Provider } from '../../contexts/NodeIdContext';
import { NodeComponentProps, WrapNodeProps } from '../../types';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
export default (NodeComponent: ComponentType<NodeComponentProps>) => {
const selector = (s: ReactFlowState) => ({
addSelectedElements: s.addSelectedElements,
unsetNodesSelection: s.unsetNodesSelection,
updateNodePosition: s.updateNodePosition,
updateNodeDimensions: s.updateNodeDimensions,
unselectNodesAndEdges: s.unselectNodesAndEdges,
});
export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({
id,
type,
@@ -42,19 +41,25 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
selectNodesOnDrag,
sourcePosition,
targetPosition,
isHidden,
hidden,
isInitialized,
snapToGrid,
snapGrid,
isDragging,
dragging,
resizeObserver,
dragHandle,
zIndex,
isParent,
noPanClassName,
noDragClassName,
}: WrapNodeProps) => {
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const {
addSelectedElements,
unselectNodesAndEdges,
unsetNodesSelection,
updateNodePosition,
// updateNodeDimensions,
} = useStore(selector, shallow);
const nodeElement = useRef<HTMLDivElement>(null);
const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]);
@@ -62,7 +67,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
const nodeStyle: CSSProperties = useMemo(
() => ({
zIndex: selected ? 10 : 3,
zIndex,
transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents:
isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none',
@@ -71,7 +76,6 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
...style,
}),
[
selected,
xPos,
yPos,
isSelectable,
@@ -82,31 +86,34 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
onMouseEnter,
onMouseMove,
onMouseLeave,
isParent,
zIndex,
]
);
const onMouseEnterHandler = useMemo(() => {
if (!onMouseEnter || isDragging) {
if (!onMouseEnter || dragging) {
return;
}
return (event: MouseEvent) => onMouseEnter(event, node);
}, [onMouseEnter, isDragging, node]);
}, [onMouseEnter, dragging, node]);
const onMouseMoveHandler = useMemo(() => {
if (!onMouseMove || isDragging) {
if (!onMouseMove || dragging) {
return;
}
return (event: MouseEvent) => onMouseMove(event, node);
}, [onMouseMove, isDragging, node]);
}, [onMouseMove, dragging, node]);
const onMouseLeaveHandler = useMemo(() => {
if (!onMouseLeave || isDragging) {
if (!onMouseLeave || dragging) {
return;
}
return (event: MouseEvent) => onMouseLeave(event, node);
}, [onMouseLeave, isDragging, node]);
}, [onMouseLeave, dragging, node]);
const onContextMenuHandler = useMemo(() => {
if (!onContextMenu) {
@@ -123,7 +130,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
unsetNodesSelection();
if (!selected) {
addSelectedElements(node);
addSelectedElements([node]);
}
}
@@ -141,11 +148,11 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
unsetNodesSelection();
if (!selected) {
addSelectedElements(node);
addSelectedElements([node]);
}
} else if (!selectNodesOnDrag && !selected && isSelectable) {
unselectNodesAndEdges();
unsetNodesSelection();
addSelectedElements([]);
}
},
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
@@ -153,20 +160,14 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
const onDrag = useCallback(
(event: DraggableEvent, draggableData: DraggableData) => {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
if (onNodeDrag) {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
onNodeDrag(event as MouseEvent, node);
}
updateNodePosDiff({
id,
diff: {
x: draggableData.deltaX,
y: draggableData.deltaY,
},
isDragging: true,
});
updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
},
[id, node, onNodeDrag]
);
@@ -175,9 +176,9 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
(event: DraggableEvent) => {
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!isDragging) {
if (!dragging) {
if (isSelectable && !selectNodesOnDrag && !selected) {
addSelectedElements(node);
addSelectedElements([node]);
}
onClick?.(event as MouseEvent, node);
@@ -185,14 +186,14 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
return;
}
updateNodePosDiff({
updateNodePosition({
id: node.id,
isDragging: false,
dragging: false,
});
onNodeDragStop?.(event as MouseEvent, node);
},
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected]
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected]
);
const onNodeDoubleClickHandler = useCallback(
@@ -202,12 +203,6 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
[node, onNodeDoubleClick]
);
useLayoutEffect(() => {
if (nodeElement.current && !isHidden) {
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
}
}, [id, isHidden, sourcePosition, targetPosition]);
useEffect(() => {
if (nodeElement.current) {
const currNode = nodeElement.current;
@@ -217,17 +212,19 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
}
}, []);
if (isHidden) {
if (hidden) {
return null;
}
const nodeClasses = cc([
'react-flow__node',
`react-flow__node-${type}`,
noPanClassName,
className,
{
selected,
selectable: isSelectable,
parent: isParent,
},
]);
@@ -238,7 +235,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
onStop={onDragStop}
scale={scale}
disabled={!isDraggable}
cancel=".nodrag"
cancel={`.${noDragClassName}`}
nodeRef={nodeElement}
grid={grid}
enableUserSelectHack={false}
@@ -267,8 +264,9 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
isConnectable={isConnectable}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
isDragging={isDragging}
dragging={dragging}
dragHandle={dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>
+28 -40
View File
@@ -5,53 +5,45 @@
import React, { useMemo, useCallback, useRef, MouseEvent } from 'react';
import ReactDraggable, { DraggableData } from 'react-draggable';
import cc from 'classcat';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { isNode } from '../../utils/graph';
import { Node } from '../../types';
import { useStore } from '../../store';
import { Node, ReactFlowState } from '../../types';
export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string;
}
// @TODO: work with nodeInternals instead of converting it to an array
const selector = (s: ReactFlowState) => ({
transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox,
selectionActive: s.selectionActive,
selectedNodes: Array.from(s.nodeInternals)
.filter(([_, n]) => n.selected)
.map(([_, n]) => n),
snapToGrid: s.snapToGrid,
snapGrid: s.snapGrid,
updateNodePosition: s.updateNodePosition,
});
export default ({
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
noPanClassName,
}: NodesSelectionProps) => {
const [tX, tY, tScale] = useStoreState((state) => state.transform);
const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox);
const selectionActive = useStoreState((state) => state.selectionActive);
const selectedElements = useStoreState((state) => state.selectedElements);
const snapToGrid = useStoreState((state) => state.snapToGrid);
const snapGrid = useStoreState((state) => state.snapGrid);
const nodes = useStoreState((state) => state.nodes);
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } =
useStore(selector);
const [tX, tY, tScale] = transform;
const nodeRef = useRef(null);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
const selectedNodes = useMemo(
() =>
selectedElements
? selectedElements.filter(isNode).map((selectedNode) => {
const matchingNode = nodes.find((node) => node.id === selectedNode.id);
return {
...matchingNode,
position: matchingNode?.__rf.position,
} as Node;
})
: [],
[selectedElements, nodes]
);
const style = useMemo(
() => ({
transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
@@ -82,21 +74,21 @@ export default ({
onSelectionDrag(event, selectedNodes);
}
updateNodePosDiff({
updateNodePosition({
diff: {
x: data.deltaX,
y: data.deltaY,
},
isDragging: true,
dragging: true,
});
},
[onSelectionDrag, selectedNodes, updateNodePosDiff]
[onSelectionDrag, selectedNodes, updateNodePosition]
);
const onStop = useCallback(
(event: MouseEvent) => {
updateNodePosDiff({
isDragging: false,
updateNodePosition({
dragging: false,
});
onSelectionDragStop?.(event, selectedNodes);
@@ -106,21 +98,17 @@ export default ({
const onContextMenu = useCallback(
(event: MouseEvent) => {
const selectedNodes = selectedElements
? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!)
: [];
onSelectionContextMenu?.(event, selectedNodes);
},
[onSelectionContextMenu]
[onSelectionContextMenu, selectedNodes]
);
if (!selectedElements || selectionActive) {
if (!selectedNodes || selectionActive) {
return null;
}
return (
<div className="react-flow__nodesselection" style={style}>
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
<ReactDraggable
scale={tScale}
grid={grid}
+16 -7
View File
@@ -1,20 +1,29 @@
import { useEffect } from 'react';
import shallow from 'zustand/shallow';
import { Elements } from '../../types';
import { useStoreState } from '../../store/hooks';
import { ReactFlowState, OnSelectionChangeFunc } from '../../types';
import { useStore } from '../../store';
interface SelectionListenerProps {
onSelectionChange: (elements: Elements | null) => void;
onSelectionChange: OnSelectionChangeFunc;
}
// This is a helper component for calling the onSelectionChange listener
// @TODO: work with nodeInternals instead of converting it to an array
const selectedElementsSelector = (s: ReactFlowState) => ({
selectedNodes: Array.from(s.nodeInternals)
.filter(([_, n]) => n.selected)
.map(([_, node]) => node),
selectedEdges: s.edges.filter((e) => e.selected),
});
// This is just a helper component for calling the onSelectionChange listener.
export default ({ onSelectionChange }: SelectionListenerProps) => {
const selectedElements = useStoreState((s) => s.selectedElements);
const { selectedNodes, selectedEdges } = useStore(selectedElementsSelector, shallow);
useEffect(() => {
onSelectionChange(selectedElements);
}, [selectedElements]);
onSelectionChange({ nodes: selectedNodes, edges: selectedEdges });
}, [selectedNodes, selectedEdges]);
return null;
};
+210
View File
@@ -0,0 +1,210 @@
import { useEffect } from 'react';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import {
Node,
Edge,
ReactFlowState,
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
CoordinateExtent,
OnNodesChange,
OnEdgesChange,
ConnectionMode,
SnapGrid,
} from '../../types';
interface StoreUpdaterProps {
nodes: Node[];
edges: Edge[];
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
minZoom?: number;
maxZoom?: number;
nodeExtent?: CoordinateExtent;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
elementsSelectable?: boolean;
connectionMode?: ConnectionMode;
snapToGrid?: boolean;
snapGrid?: SnapGrid;
translateExtent?: CoordinateExtent;
}
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;
+24 -11
View File
@@ -4,8 +4,8 @@
import React, { memo } from 'react';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { XYPosition } from '../../types';
import { useStore } from '../../store';
import { XYPosition, ReactFlowState } from '../../types';
type UserSelectionProps = {
selectionKeyPressed: boolean;
@@ -25,8 +25,10 @@ function getMousePosition(event: React.MouseEvent): XYPosition | void {
};
}
const userSelectionRectSelector = (state: ReactFlowState) => state.userSelectionRect;
const SelectionRect = () => {
const userSelectionRect = useStoreState((state) => state.userSelectionRect);
const userSelectionRect = useStore(userSelectionRectSelector);
if (!userSelectionRect.draw) {
return null;
@@ -34,7 +36,7 @@ const SelectionRect = () => {
return (
<div
className="react-flow__selection"
className="react-flow__selection react-flow__container"
style={{
width: userSelectionRect.width,
height: userSelectionRect.height,
@@ -44,14 +46,25 @@ const SelectionRect = () => {
);
};
const selector = (s: ReactFlowState) => ({
selectionActive: s.selectionActive,
elementsSelectable: s.elementsSelectable,
setUserSelection: s.setUserSelection,
updateUserSelection: s.updateUserSelection,
unsetUserSelection: s.unsetUserSelection,
unsetNodesSelection: s.unsetNodesSelection,
});
export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const selectionActive = useStoreState((state) => state.selectionActive);
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const {
selectionActive,
elementsSelectable,
setUserSelection,
updateUserSelection,
unsetUserSelection,
unsetNodesSelection,
} = useStore(selector);
const setUserSelection = useStoreActions((actions) => actions.setUserSelection);
const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection);
const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const renderUserSelectionPane = selectionActive || selectionKeyPressed;
if (!elementsSelectable || !renderUserSelectionPane) {
@@ -89,7 +102,7 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
return (
<div
className="react-flow__selectionpane"
className="react-flow__selectionpane react-flow__container"
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import { useStoreState } from '../../store/hooks';
import { EdgeMarker, ArrowHeadType } from '../../types';
import { useStore } from '../../store';
import { EdgeMarker, ArrowHeadType, ReactFlowState } from '../../types';
import { getMarkerId } from '../../utils/graph';
interface MarkerProps extends EdgeMarker {
id: string;
@@ -71,8 +72,10 @@ const Marker = ({
);
};
const edgesSelector = (s: ReactFlowState) => s.edges;
const MarkerDefinitions = ({ defaultColor }: MarkerDefinitionsProps) => {
const edges = useStoreState((state) => state.edges);
const edges = useStore(edgesSelector);
const markers = useMemo(() => {
const ids: string[] = [];
+258 -195
View File
@@ -1,30 +1,30 @@
import React, { memo, CSSProperties, useCallback } from 'react';
import shallow from 'zustand/shallow';
import cc from 'classcat';
import { useStoreState } from '../../store/hooks';
import { useStore } from '../../store';
import ConnectionLine from '../../components/ConnectionLine/index';
import { isEdge } from '../../utils/graph';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, isEdgeVisible, getSourceTargetNodes } from './utils';
import { getEdgePositions, getHandle } from './utils';
import {
Position,
Edge,
Node,
Elements,
Connection,
ConnectionLineType,
ConnectionLineComponent,
ConnectionMode,
Transform,
OnEdgeUpdateFunc,
ReactFlowState,
NodeHandleBounds,
} from '../../types';
import useVisibleEdges from '../../hooks/useVisibleEdges';
interface EdgeRendererProps {
edgeTypes: any;
connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
connectionMode?: ConnectionMode;
onElementClick?: (event: React.MouseEvent, element: Node | Edge) => void;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
defaultMarkerColor: string;
onlyRenderVisibleElements: boolean;
@@ -36,215 +36,278 @@ interface EdgeRendererProps {
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void;
edgeUpdaterRadius?: number;
noPanClassName?: string;
}
interface EdgeWrapperProps {
edge: Edge;
props: EdgeRendererProps;
nodes: Node[];
selectedElements: Elements | null;
edgeTypes: any;
markerEndId?: string;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeMouseMove?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeMouseLeave?: (event: React.MouseEvent, edge: Edge) => void;
edgeUpdaterRadius?: number;
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void;
onEdgeUpdate?: OnEdgeUpdateFunc;
elementsSelectable: boolean;
transform: Transform;
width: number;
height: number;
onlyRenderVisibleElements: boolean;
connectionMode?: ConnectionMode;
sourceNodeWidth?: number | null;
sourceNodeHeight?: number | null;
sourceNodeX?: number;
sourceNodeY?: number;
sourceNodeHandleBounds?: NodeHandleBounds;
targetNodeWidth?: number | null;
targetNodeHeight?: number | null;
targetNodeX?: number;
targetNodeY?: number;
targetNodeHandleBounds?: NodeHandleBounds;
noPanClassName?: string;
}
const Edge = ({
edge,
props,
nodes,
selectedElements,
elementsSelectable,
transform,
width,
height,
onlyRenderVisibleElements,
connectionMode,
}: EdgeWrapperProps) => {
const sourceHandleId = edge.sourceHandle || null;
const targetHandleId = edge.targetHandle || null;
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nodes);
const Edge = memo(
({
edge,
edgeTypes,
markerEndId,
onEdgeClick,
onEdgeContextMenu,
onEdgeMouseEnter,
onEdgeMouseMove,
onEdgeMouseLeave,
edgeUpdaterRadius,
onEdgeDoubleClick,
onEdgeUpdateStart,
onEdgeUpdateEnd,
onEdgeUpdate,
connectionMode,
elementsSelectable,
sourceNodeWidth,
sourceNodeHeight,
sourceNodeX,
sourceNodeY,
sourceNodeHandleBounds,
targetNodeWidth,
targetNodeHeight,
targetNodeX,
targetNodeY,
targetNodeHandleBounds,
noPanClassName,
}: EdgeWrapperProps) => {
const sourceHandleId = edge.sourceHandle || null;
const targetHandleId = edge.targetHandle || null;
const onConnectEdge = useCallback(
(connection: Connection) => {
props.onEdgeUpdate?.(edge, connection);
},
[edge, props.onEdgeUpdate]
);
const onConnectEdge = useCallback(
(connection: Connection) => {
onEdgeUpdate?.(edge, connection);
},
[edge, onEdgeUpdate]
);
if (!sourceNode) {
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`);
return null;
// source and target node need to be initialized
if (!sourceNodeHandleBounds || !targetNodeHandleBounds) {
return null;
}
if (
!sourceNodeWidth ||
!sourceNodeHeight ||
typeof sourceNodeX === 'undefined' ||
typeof sourceNodeY === 'undefined'
) {
console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`);
return null;
}
if (
!targetNodeWidth ||
!targetNodeHeight ||
typeof targetNodeX === 'undefined' ||
typeof targetNodeY === 'undefined'
) {
console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`);
return null;
}
const edgeType = edge.type || 'default';
const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
// when connection type is loose we can define all handles as sources
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetNodeHandleBounds.target
: targetNodeHandleBounds.target || targetNodeHandleBounds.source;
const sourceHandle = getHandle(sourceNodeHandleBounds.source!, sourceHandleId);
const targetHandle = getHandle(targetNodeHandles!, targetHandleId);
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom;
const targetPosition = targetHandle ? targetHandle.position : Position.Top;
if (!sourceHandle) {
console.warn(`couldn't create edge for source handle id: ${sourceHandleId}; edge id: ${edge.id}`);
return null;
}
if (!targetHandle) {
console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`);
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
{ x: sourceNodeX, y: sourceNodeY, width: sourceNodeWidth, height: sourceNodeHeight },
sourceHandle,
sourcePosition,
{ x: targetNodeX, y: targetNodeY, width: targetNodeWidth, height: targetNodeHeight },
targetHandle,
targetPosition
);
return (
<EdgeComponent
key={edge.id}
id={edge.id}
className={cc([edge.className, noPanClassName])}
type={edgeType}
data={edge.data}
onClick={onEdgeClick}
selected={!!edge.selected}
animated={edge.animated}
label={edge.label}
labelStyle={edge.labelStyle}
labelShowBg={edge.labelShowBg}
labelBgStyle={edge.labelBgStyle}
labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius}
style={edge.style}
source={edge.source}
target={edge.target}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerEnd={edge.markerEnd}
markerStart={edge.markerStart}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
markerEndId={markerEndId}
hidden={edge.hidden}
onConnectEdge={onConnectEdge}
handleEdgeUpdate={typeof onEdgeUpdate !== 'undefined'}
onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
edgeUpdaterRadius={edgeUpdaterRadius}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
/>
);
}
);
if (!targetNode) {
console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`);
return null;
}
// source and target node need to be initialized
if (!sourceNode.__rf.width || !targetNode.__rf.width) {
return null;
}
const edgeType = edge.type || 'default';
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
const targetNodeBounds = targetNode.__rf.handleBounds;
// when connection type is loose we can define all handles as sources
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetNodeBounds.target
: targetNodeBounds.target || targetNodeBounds.source;
const sourceHandle = getHandle(sourceNode.__rf.handleBounds.source, sourceHandleId);
const targetHandle = getHandle(targetNodeHandles, targetHandleId);
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom;
const targetPosition = targetHandle ? targetHandle.position : Position.Top;
if (!sourceHandle) {
console.warn(`couldn't create edge for source handle id: ${sourceHandleId}; edge id: ${edge.id}`);
return null;
}
if (!targetHandle) {
console.warn(`couldn't create edge for target handle id: ${targetHandleId}; edge id: ${edge.id}`);
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNode,
sourceHandle,
sourcePosition,
targetNode,
targetHandle,
targetPosition
);
const isVisible = onlyRenderVisibleElements
? isEdgeVisible({
sourcePos: { x: sourceX, y: sourceY },
targetPos: { x: targetX, y: targetY },
width,
height,
transform,
})
: true;
if (!isVisible) {
return null;
}
const isSelected = selectedElements?.some((elm) => isEdge(elm) && elm.id === edge.id) || false;
return (
<EdgeComponent
key={edge.id}
id={edge.id}
className={edge.className}
type={edge.type}
data={edge.data}
onClick={props.onElementClick}
selected={isSelected}
animated={edge.animated}
label={edge.label}
labelStyle={edge.labelStyle}
labelShowBg={edge.labelShowBg}
labelBgStyle={edge.labelBgStyle}
labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius}
style={edge.style}
markerEnd={edge.markerEnd}
markerStart={edge.markerStart}
source={edge.source}
target={edge.target}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
isHidden={edge.isHidden}
onConnectEdge={onConnectEdge}
handleEdgeUpdate={typeof props.onEdgeUpdate !== 'undefined'}
onContextMenu={props.onEdgeContextMenu}
onMouseEnter={props.onEdgeMouseEnter}
onMouseMove={props.onEdgeMouseMove}
onMouseLeave={props.onEdgeMouseLeave}
edgeUpdaterRadius={props.edgeUpdaterRadius}
onEdgeDoubleClick={props.onEdgeDoubleClick}
onEdgeUpdateStart={props.onEdgeUpdateStart}
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
/>
);
};
const selector = (s: ReactFlowState) => ({
connectionNodeId: s.connectionNodeId,
connectionHandleId: s.connectionHandleId,
connectionHandleType: s.connectionHandleType,
connectionPosition: s.connectionPosition,
nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable,
width: s.width,
height: s.height,
connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals,
});
const EdgeRenderer = (props: EdgeRendererProps) => {
const transform = useStoreState((state) => state.transform);
const nodes = useStoreState((state) => state.nodes);
const edges = useStoreState((state) => state.edges);
const connectionNodeId = useStoreState((state) => state.connectionNodeId);
const connectionHandleId = useStoreState((state) => state.connectionHandleId);
const connectionHandleType = useStoreState((state) => state.connectionHandleType);
const connectionPosition = useStoreState((state) => state.connectionPosition);
const selectedElements = useStoreState((state) => state.selectedElements);
const nodesConnectable = useStoreState((state) => state.nodesConnectable);
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const width = useStoreState((state) => state.width);
const height = useStoreState((state) => state.height);
const {
connectionNodeId,
connectionHandleId,
connectionHandleType,
connectionPosition,
nodesConnectable,
elementsSelectable,
width,
height,
connectionMode,
nodeInternals,
} = useStore(selector, shallow);
const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals);
if (!width) {
return null;
}
const {
connectionLineType,
defaultMarkerColor,
connectionLineStyle,
connectionLineComponent,
onlyRenderVisibleElements,
} = props;
const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`;
const { connectionLineType, defaultMarkerColor, connectionLineStyle, connectionLineComponent } = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return (
<svg width={width} height={height} className="react-flow__edges">
<MarkerDefinitions defaultColor={defaultMarkerColor} />
<g transform={transformStyle}>
{edges.map((edge: Edge) => (
<Edge
key={edge.id}
edge={edge}
props={props}
nodes={nodes}
selectedElements={selectedElements}
elementsSelectable={elementsSelectable}
transform={transform}
width={width}
height={height}
onlyRenderVisibleElements={onlyRenderVisibleElements}
/>
))}
{renderConnectionLine && (
<ConnectionLine
nodes={nodes}
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
transform={transform}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
)}
</g>
</svg>
<>
{edgeTree.map(({ level, edges, isMaxLevel }) => (
<svg
key={level}
style={{ zIndex: level }}
width={width}
height={height}
className="react-flow__edges react-flow__container"
>
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} />}
<g>
{edges.map((edge: Edge) => {
const sourceNode = nodeInternals.get(edge.source);
const targetNode = nodeInternals.get(edge.target);
return (
<Edge
key={edge.id}
edge={edge}
sourceNodeWidth={sourceNode?.width}
sourceNodeHeight={sourceNode?.height}
sourceNodeX={sourceNode?.positionAbsolute?.x}
sourceNodeY={sourceNode?.positionAbsolute?.y}
sourceNodeHandleBounds={sourceNode?.handleBounds}
targetNodeWidth={targetNode?.width}
targetNodeHeight={targetNode?.height}
targetNodeX={targetNode?.positionAbsolute?.x}
targetNodeY={targetNode?.positionAbsolute?.y}
targetNodeHandleBounds={targetNode?.handleBounds}
elementsSelectable={elementsSelectable}
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}
noPanClassName={props.noPanClassName}
/>
);
})}
{renderConnectionLine && isMaxLevel && (
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
)}
</g>
</svg>
))}
</>
);
};
+29 -45
View File
@@ -2,19 +2,9 @@ import { ComponentType } from 'react';
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge';
import { rectToBox } from '../../utils/graph';
import { rectToBox } from '../../utils';
import {
EdgeTypesType,
EdgeProps,
Position,
Node,
XYPosition,
ElementId,
HandleElement,
Transform,
Edge,
} from '../../types';
import { EdgeTypesType, EdgeProps, HandleElement, Position, XYPosition, Transform, Rect } from '../../types';
export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
const standardTypes: EdgeTypesType = {
@@ -39,11 +29,11 @@ export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType {
};
}
export function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + node.__rf.position.x;
const y = (handle?.y || 0) + node.__rf.position.y;
const width = handle?.width || node.__rf.width;
const height = handle?.height || node.__rf.height;
export function getHandlePosition(position: Position, nodeRect: Rect, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + nodeRect.x;
const y = (handle?.y || 0) + nodeRect.y;
const width = handle?.width || nodeRect.width;
const height = handle?.height || nodeRect.height;
switch (position) {
case Position.Top:
@@ -69,7 +59,7 @@ export function getHandlePosition(position: Position, node: Node, handle: any |
}
}
export function getHandle(bounds: HandleElement[], handleId: ElementId | null): HandleElement | null {
export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null {
if (!bounds) {
return null;
}
@@ -94,15 +84,15 @@ interface EdgePositions {
}
export const getEdgePositions = (
sourceNode: Node,
sourceNodeRect: Rect,
sourceHandle: HandleElement | unknown,
sourcePosition: Position,
targetNode: Node,
targetNodeRect: Rect,
targetHandle: HandleElement | unknown,
targetPosition: Position
): EdgePositions => {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle);
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return {
sourceX: sourceHandlePos.x,
@@ -115,17 +105,31 @@ export const getEdgePositions = (
interface IsEdgeVisibleParams {
sourcePos: XYPosition;
targetPos: XYPosition;
sourceWidth: number;
sourceHeight: number;
targetWidth: number;
targetHeight: number;
width: number;
height: number;
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 = {
x: Math.min(sourcePos.x, targetPos.x),
y: Math.min(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x, targetPos.x),
y2: Math.max(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x + sourceWidth, targetPos.x + targetWidth),
y2: Math.max(sourcePos.y + sourceHeight, targetPos.y + targetHeight),
};
if (edgeBox.x === edgeBox.x2) {
@@ -149,23 +153,3 @@ export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }
return overlappingArea > 0;
}
type SourceTargetNode = {
sourceNode: Node | null;
targetNode: Node | null;
};
export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => {
return nodes.reduce(
(res, node) => {
if (node.id === edge.source) {
res.sourceNode = node;
}
if (node.id === edge.target) {
res.targetNode = node;
}
return res;
},
{ sourceNode: null, targetNode: null } as SourceTargetNode
);
};
+22 -11
View File
@@ -1,18 +1,18 @@
import React, { useCallback, memo, ReactNode, WheelEvent, MouseEvent } from 'react';
import { useStoreActions, useStoreState } from '../../store/hooks';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useKeyPress from '../../hooks/useKeyPress';
import { GraphViewProps } from '../GraphView';
import ZoomPane from '../ZoomPane';
import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection';
import { ReactFlowState } from '../../types';
interface FlowRendererProps
extends Omit<
GraphViewProps,
| 'elements'
| 'snapToGrid'
| 'nodeTypes'
| 'edgeTypes'
@@ -26,12 +26,17 @@ interface FlowRendererProps
children: ReactNode;
}
const selector = (s: ReactFlowState) => ({
unsetNodesSelection: s.unsetNodesSelection,
resetSelectedElements: s.resetSelectedElements,
nodesSelectionActive: s.nodesSelectionActive,
});
const FlowRenderer = ({
children,
onPaneClick,
onPaneContextMenu,
onPaneScroll,
onElementsRemove,
deleteKeyCode,
onMove,
onMoveStart,
@@ -49,20 +54,19 @@ const FlowRenderer = ({
paneMoveable,
defaultPosition,
defaultZoom,
translateExtent,
preventScrolling,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
noWheelClassName,
noPanClassName,
}: FlowRendererProps) => {
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements);
const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive);
const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow);
const selectionKeyPressed = useKeyPress(selectionKeyCode);
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode, multiSelectionKeyCode });
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
const onClick = useCallback(
(event: MouseEvent) => {
@@ -103,9 +107,10 @@ const FlowRenderer = ({
paneMoveable={paneMoveable}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}
translateExtent={translateExtent}
zoomActivationKeyCode={zoomActivationKeyCode}
preventScrolling={preventScrolling}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
>
{children}
<UserSelection selectionKeyPressed={selectionKeyPressed} />
@@ -115,9 +120,15 @@ const FlowRenderer = ({
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
noPanClassName={noPanClassName}
/>
)}
<div className="react-flow__pane" onClick={onClick} onContextMenu={onContextMenu} onWheel={onWheel} />
<div
className="react-flow__pane react-flow__container"
onClick={onClick}
onContextMenu={onContextMenu}
onWheel={onWheel}
/>
</ZoomPane>
);
};
+53 -181
View File
@@ -1,30 +1,28 @@
import React, { useEffect, useRef, memo } from 'react';
import React, { memo } from 'react';
import { useStoreActions, useStore } from '../../store/hooks';
import FlowRenderer from '../FlowRenderer';
import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import { onLoadProject, onLoadGetElements, onLoadToObject } from '../../utils/graph';
import useZoomPanHelper from '../../hooks/useZoomPanHelper';
import Viewport from '../Viewport';
import { ReactFlowProps } from '../ReactFlow';
import useOnLoadHandler from '../../hooks/useOnLoadHandler';
import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types';
export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'elements'> {
export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges'> {
nodeTypes: NodeTypesType;
edgeTypes: EdgeTypesType;
selectionKeyCode: KeyCode;
deleteKeyCode: KeyCode;
multiSelectionKeyCode: KeyCode;
connectionLineType: ConnectionLineType;
snapToGrid: boolean;
snapGrid: [number, number];
onlyRenderVisibleElements: boolean;
defaultZoom: number;
defaultPosition: [number, number];
defaultMarkerColor: string;
selectNodesOnDrag: boolean;
noDragClassName: string;
noWheelClassName: string;
noPanClassName: string;
}
const GraphView = ({
@@ -34,7 +32,8 @@ const GraphView = ({
onMoveStart,
onMoveEnd,
onLoad,
onElementClick,
onNodeClick,
onEdgeClick,
onNodeDoubleClick,
onEdgeDoubleClick,
onNodeMouseEnter,
@@ -48,33 +47,19 @@ const GraphView = ({
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
connectionMode,
connectionLineType,
connectionLineStyle,
connectionLineComponent,
selectionKeyCode,
multiSelectionKeyCode,
zoomActivationKeyCode,
onElementsRemove,
deleteKeyCode,
onConnect,
onConnectStart,
onConnectStop,
onConnectEnd,
snapToGrid,
snapGrid,
onlyRenderVisibleElements,
nodesDraggable,
nodesConnectable,
elementsSelectable,
selectNodesOnDrag,
minZoom,
maxZoom,
defaultZoom,
defaultPosition,
translateExtent,
preventScrolling,
nodeExtent,
defaultMarkerColor,
zoomOnScroll,
zoomOnPinch,
@@ -94,134 +79,17 @@ const GraphView = ({
edgeUpdaterRadius,
onEdgeUpdateStart,
onEdgeUpdateEnd,
noDragClassName,
noWheelClassName,
noPanClassName,
}: GraphViewProps) => {
const isInitialized = useRef<boolean>(false);
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart);
const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop);
const setOnConnectEnd = useStoreActions((actions) => actions.setOnConnectEnd);
const setSnapGrid = useStoreActions((actions) => actions.setSnapGrid);
const setSnapToGrid = useStoreActions((actions) => actions.setSnapToGrid);
const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable);
const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable);
const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable);
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent);
const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode);
const currentStore = useStore();
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
useEffect(() => {
if (!isInitialized.current && initialized) {
if (onLoad) {
onLoad({
fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn,
zoomOut,
zoomTo,
setTransform: transform,
project: onLoadProject(currentStore),
getElements: onLoadGetElements(currentStore),
toObject: onLoadToObject(currentStore),
});
}
isInitialized.current = true;
}
}, [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]);
useOnLoadHandler(onLoad);
return (
<FlowRenderer
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
onPaneScroll={onPaneScroll}
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode}
selectionKeyCode={selectionKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
@@ -239,48 +107,52 @@ const GraphView = ({
paneMoveable={paneMoveable}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}
translateExtent={translateExtent}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
preventScrolling={preventScrolling}
noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
>
<NodeRenderer
nodeTypes={nodeTypes}
onElementClick={onElementClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
onNodeDragStart={onNodeDragStart}
selectNodesOnDrag={selectNodesOnDrag}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
onlyRenderVisibleElements={onlyRenderVisibleElements}
/>
<EdgeRenderer
edgeTypes={edgeTypes}
onElementClick={onElementClick}
onEdgeDoubleClick={onEdgeDoubleClick}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionMode={connectionMode}
defaultMarkerColor={defaultMarkerColor}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
/>
<Viewport>
<EdgeRenderer
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor}
noPanClassName={noPanClassName}
/>
<NodeRenderer
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
onNodeDragStart={onNodeDragStart}
selectNodesOnDrag={selectNodesOnDrag}
onlyRenderVisibleElements={onlyRenderVisibleElements}
noPanClassName={noPanClassName}
noDragClassName={noDragClassName}
/>
</Viewport>
</FlowRenderer>
);
};
+51 -39
View File
@@ -1,12 +1,13 @@
import React, { memo, useMemo, ComponentType, MouseEvent } from 'react';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types';
import { getNodesInside } from '../../utils/graph';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types';
interface NodeRendererProps {
nodeTypes: NodeTypesType;
selectNodesOnDrag: boolean;
onElementClick?: (event: MouseEvent, element: Node | Edge) => void;
onNodeClick?: (event: MouseEvent, element: Node) => void;
onNodeDoubleClick?: (event: MouseEvent, element: Node) => void;
onNodeMouseEnter?: (event: MouseEvent, node: Node) => void;
onNodeMouseMove?: (event: MouseEvent, node: Node) => void;
@@ -15,32 +16,33 @@ interface NodeRendererProps {
onNodeDragStart?: (event: MouseEvent, node: Node) => void;
onNodeDrag?: (event: MouseEvent, node: Node) => void;
onNodeDragStop?: (event: MouseEvent, node: Node) => void;
snapToGrid: boolean;
snapGrid: [number, number];
onlyRenderVisibleElements: boolean;
noPanClassName: string;
noDragClassName: string;
}
const selector = (s: ReactFlowState) => ({
scale: s.transform[2],
nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
snapGrid: s.snapGrid,
snapToGrid: s.snapToGrid,
nodeInternals: s.nodeInternals,
});
const NodeRenderer = (props: NodeRendererProps) => {
const transform = useStoreState((state) => state.transform);
const selectedElements = useStoreState((state) => state.selectedElements);
const nodesDraggable = useStoreState((state) => state.nodesDraggable);
const nodesConnectable = useStoreState((state) => state.nodesConnectable);
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const width = useStoreState((state) => state.width);
const height = useStoreState((state) => state.height);
const nodes = useStoreState((state) => state.nodes);
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const visibleNodes = props.onlyRenderVisibleElements
? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true)
: nodes;
const transformStyle = useMemo(
() => ({
transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})`,
}),
[transform[0], transform[1], transform[2]]
);
const {
scale,
nodesDraggable,
nodesConnectable,
elementsSelectable,
updateNodeDimensions,
snapGrid,
snapToGrid,
nodeInternals,
} = useStore(selector, shallow);
const resizeObserver = useMemo(() => {
if (typeof ResizeObserver === 'undefined') {
@@ -58,18 +60,24 @@ const NodeRenderer = (props: NodeRendererProps) => {
}, []);
return (
<div className="react-flow__nodes" style={transformStyle}>
{visibleNodes.map((node) => {
<div className="react-flow__nodes react-flow__container">
{Array.from(nodeInternals).map(([_, node]) => {
const nodeType = node.type || 'default';
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
const internals = nodeInternals.get(node.id);
if (!props.nodeTypes[nodeType]) {
console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`);
}
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
const isInitialized =
node.width !== null &&
node.height !== null &&
typeof node.width !== 'undefined' &&
typeof node.height !== 'undefined';
return (
<NodeComponent
@@ -81,15 +89,15 @@ const NodeRenderer = (props: NodeRendererProps) => {
data={node.data}
sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition}
isHidden={node.isHidden}
xPos={node.__rf.position.x}
yPos={node.__rf.position.y}
isDragging={node.__rf.isDragging}
isInitialized={node.__rf.width !== null && node.__rf.height !== null}
snapGrid={props.snapGrid}
snapToGrid={props.snapToGrid}
hidden={node.hidden}
xPos={internals?.positionAbsolute?.x || 0}
yPos={internals?.positionAbsolute?.y || 0}
dragging={!!node.dragging}
isInitialized={isInitialized}
snapGrid={snapGrid}
snapToGrid={snapToGrid}
selectNodesOnDrag={props.selectNodesOnDrag}
onClick={props.onElementClick}
onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove}
onMouseLeave={props.onNodeMouseLeave}
@@ -98,13 +106,17 @@ const NodeRenderer = (props: NodeRendererProps) => {
onNodeDragStart={props.onNodeDragStart}
onNodeDrag={props.onNodeDrag}
onNodeDragStop={props.onNodeDragStop}
scale={transform[2]}
selected={selectedElements?.some(({ id }) => id === node.id) || false}
scale={scale}
selected={!!node.selected}
isDraggable={isDraggable}
isSelectable={isSelectable}
isConnectable={isConnectable}
resizeObserver={resizeObserver}
dragHandle={node.dragHandle}
zIndex={internals?.z || 0}
isParent={!!internals?.isParent}
noDragClassName={props.noDragClassName}
noPanClassName={props.noPanClassName}
/>
);
})}
+6 -14
View File
@@ -4,28 +4,20 @@ import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import wrapNode from '../../components/Nodes/wrapNode';
import { NodeTypesType, NodeComponentProps } from '../../types';
import { NodeTypesType, NodeProps } from '../../types';
export function createNodeTypes(nodeTypes: NodeTypesType): NodeTypesType {
const standardTypes: NodeTypesType = {
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<
NodeComponentProps
>),
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<
NodeComponentProps
>),
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<
NodeComponentProps
>),
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeProps>),
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeProps>),
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeProps>),
};
const wrappedTypes = {} as NodeTypesType;
const specialTypes: NodeTypesType = Object.keys(nodeTypes)
.filter(k => !['input', 'default', 'output'].includes(k))
.filter((k) => !['input', 'default', 'output'].includes(k))
.reduce((res, key) => {
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<
NodeComponentProps
>);
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>);
return res;
}, wrappedTypes);
+11 -9
View File
@@ -1,21 +1,23 @@
import React, { FC, useContext, useMemo } from 'react';
import { Provider, ReactReduxContext } from 'react-redux';
import React, { FC } from 'react';
import store from '../../store';
import { Provider, createStore, useStoreApi } from '../../store';
const Wrapper: FC = ({ children }) => {
const contextValue = useContext(ReactReduxContext);
const isWrappedWithReactFlowProvider = useMemo(() => contextValue?.store?.getState()?.reactFlowVersion, [
contextValue,
]);
let isWrapped = true;
if (isWrappedWithReactFlowProvider) {
try {
useStoreApi();
} catch (e) {
isWrapped = false;
}
if (isWrapped) {
// we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
return <>{children}</>;
}
return <Provider store={store}>{children}</Provider>;
return <Provider createStore={createStore}>{children}</Provider>;
};
Wrapper.displayName = 'ReactFlowWrapper';
+72 -50
View File
@@ -5,11 +5,12 @@ import React, {
MouseEvent as ReactMouseEvent,
WheelEvent,
forwardRef,
FunctionComponent,
} from 'react';
import cc from 'classcat';
import GraphView from '../GraphView';
import ElementUpdater from '../../components/ElementUpdater';
import StoreUpdater from '../../components/StoreUpdater';
import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
@@ -19,25 +20,26 @@ import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../compon
import { createEdgeTypes } from '../EdgeRenderer/utils';
import Wrapper from './Wrapper';
import {
Elements,
OnSelectionChangeFunc,
NodeTypesType,
EdgeTypesType,
OnLoadFunc,
OnLoad,
Node,
Edge,
Connection,
ConnectionMode,
ConnectionLineType,
ConnectionLineComponent,
FlowTransform,
OnConnectStartFunc,
OnConnectStopFunc,
OnConnectEndFunc,
TranslateExtent,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
OnConnect,
CoordinateExtent,
KeyCode,
PanOnScrollMode,
OnEdgeUpdateFunc,
NodeExtent,
NodeChange,
EdgeChange,
} from '../../types';
import '../../style.css';
@@ -57,9 +59,12 @@ const defaultEdgeTypes = {
};
export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad'> {
elements: Elements;
onElementClick?: (event: ReactMouseEvent, element: Node | Edge) => void;
onElementsRemove?: (elements: Elements) => void;
nodes: Node[];
edges: Edge[];
onNodesChange?: (nodeChanges: NodeChange[]) => void;
onEdgesChange?: (edgeChanges: EdgeChange[]) => void;
onNodeClick?: (event: React.MouseEvent, node: Node) => void;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void;
@@ -68,15 +73,15 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void;
onNodeDrag?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void;
onConnect?: (connection: Edge | Connection) => void;
onConnectStart?: OnConnectStartFunc;
onConnectStop?: OnConnectStopFunc;
onConnectEnd?: OnConnectEndFunc;
onLoad?: OnLoadFunc;
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
onLoad?: OnLoad;
onMove?: (flowTransform?: FlowTransform) => void;
onMoveStart?: (flowTransform?: FlowTransform) => void;
onMoveEnd?: (flowTransform?: FlowTransform) => void;
onSelectionChange?: (elements: Elements | null) => void;
onSelectionChange?: OnSelectionChangeFunc;
onSelectionDragStart?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: ReactMouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: ReactMouseEvent, nodes: Node[]) => void;
@@ -106,9 +111,9 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
maxZoom?: number;
defaultZoom?: number;
defaultPosition?: [number, number];
translateExtent?: TranslateExtent;
translateExtent?: CoordinateExtent;
preventScrolling?: boolean;
nodeExtent?: NodeExtent;
nodeExtent?: CoordinateExtent;
defaultMarkerColor?: string;
zoomOnScroll?: boolean;
zoomOnPinch?: boolean;
@@ -125,25 +130,30 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void;
edgeUpdaterRadius?: number;
nodeTypesId?: string;
edgeTypesId?: string;
noDragClassName?: string;
noWheelClassName?: string;
noPanClassName?: string;
}
export type ReactFlowRefType = HTMLDivElement;
const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
const initSnapGrid: [number, number] = [15, 15];
const initDefaultPosition: [number, number] = [0, 0];
const ReactFlow: FunctionComponent<ReactFlowProps> = forwardRef<ReactFlowRefType, ReactFlowProps>(
(
{
elements = [],
nodes = [],
edges = [],
className,
nodeTypes = defaultNodeTypes,
edgeTypes = defaultEdgeTypes,
onElementClick,
onNodeClick,
onEdgeClick,
onLoad,
onMove,
onMoveStart,
onMoveEnd,
onElementsRemove,
onConnect,
onConnectStart,
onConnectStop,
@@ -170,7 +180,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
multiSelectionKeyCode = 'Meta',
zoomActivationKeyCode = 'Meta',
snapToGrid = false,
snapGrid = [15, 15],
snapGrid = initSnapGrid,
onlyRenderVisibleElements = false,
selectNodesOnDrag = true,
nodesDraggable,
@@ -179,7 +189,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
minZoom,
maxZoom,
defaultZoom = 1,
defaultPosition = [0, 0],
defaultPosition = initDefaultPosition,
translateExtent,
preventScrolling = true,
nodeExtent,
@@ -204,14 +214,17 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onEdgeUpdateStart,
onEdgeUpdateEnd,
edgeUpdaterRadius = 10,
nodeTypesId = '1',
edgeTypesId = '1',
onNodesChange,
onEdgesChange,
noDragClassName = 'nodrag',
noWheelClassName = 'nowheel',
noPanClassName = 'nopan',
...rest
},
ref
) => {
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), [nodeTypesId]);
const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), [edgeTypesId]);
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), [nodeTypes]);
const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), [edgeTypes]);
const reactFlowClasses = cc(['react-flow', className]);
return (
@@ -222,7 +235,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
onElementClick={onElementClick}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
@@ -233,34 +247,18 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onNodeDragStop={onNodeDragStop}
nodeTypes={nodeTypesParsed}
edgeTypes={edgeTypesParsed}
connectionMode={connectionMode}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
selectionKeyCode={selectionKeyCode}
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
zoomActivationKeyCode={zoomActivationKeyCode}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
onlyRenderVisibleElements={onlyRenderVisibleElements}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
elementsSelectable={elementsSelectable}
selectNodesOnDrag={selectNodesOnDrag}
minZoom={minZoom}
maxZoom={maxZoom}
defaultZoom={defaultZoom}
defaultPosition={defaultPosition}
translateExtent={translateExtent}
preventScrolling={preventScrolling}
nodeExtent={nodeExtent}
defaultMarkerColor={defaultMarkerColor}
zoomOnScroll={zoomOnScroll}
zoomOnPinch={zoomOnPinch}
zoomOnDoubleClick={zoomOnDoubleClick}
@@ -284,8 +282,32 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor}
noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
/>
<ElementUpdater elements={elements} />
<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}
onEdgesChange={onEdgesChange}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
connectionMode={connectionMode}
translateExtent={translateExtent}
/>
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children}
</Wrapper>
+25
View File
@@ -0,0 +1,25 @@
import React, { ReactNode } from 'react';
import { useStore } from '../../store';
import { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.transform;
type ViewportProps = {
children: ReactNode;
};
function Viewport({ children }: ViewportProps) {
const transform = useStore(selector);
return (
<div
className="react-flow__viewport react-flow__container"
style={{ transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})` }}
>
{children}
</div>
);
}
export default Viewport;
+45 -34
View File
@@ -1,12 +1,13 @@
import React, { useEffect, useRef, ReactNode } from 'react';
import { zoom, zoomIdentity } from 'd3-zoom';
import { select, pointer } from 'd3-selection';
import shallow from 'zustand/shallow';
import { clamp } from '../../utils';
import useKeyPress from '../../hooks/useKeyPress';
import useResizeHandler from '../../hooks/useResizeHandler';
import { useStoreState, useStoreActions, useStore } from '../../store/hooks';
import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode } from '../../types';
import { useStore, useStoreApi } from '../../store';
import { FlowTransform, PanOnScrollMode, KeyCode, ReactFlowState } from '../../types';
interface ZoomPaneProps {
selectionKeyPressed: boolean;
@@ -20,13 +21,14 @@ interface ZoomPaneProps {
paneMoveable?: boolean;
defaultPosition?: [number, number];
defaultZoom?: number;
translateExtent?: TranslateExtent;
onMove?: (flowTransform?: FlowTransform) => void;
onMoveStart?: (flowTransform?: FlowTransform) => void;
onMoveEnd?: (flowTransform?: FlowTransform) => void;
zoomActivationKeyCode?: KeyCode;
preventScrolling?: boolean;
children: ReactNode;
noWheelClassName: string;
noPanClassName: string;
}
const viewChanged = (prevTransform: FlowTransform, eventTransform: any): boolean =>
@@ -40,7 +42,16 @@ const eventToFlowTransform = (eventTransform: any): FlowTransform => ({
zoom: eventTransform.k,
});
const hasNoWheelClass = (event: any) => event.target.closest('.nowheel');
const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`);
const selector = (s: ReactFlowState) => ({
d3Zoom: s.d3Zoom,
d3Selection: s.d3Selection,
d3ZoomHandler: s.d3ZoomHandler,
initD3Zoom: s.initD3Zoom,
updateTransform: s.updateTransform,
});
const ZoomPane = ({
onMove,
@@ -57,36 +68,29 @@ const ZoomPane = ({
paneMoveable = true,
defaultPosition = [0, 0],
defaultZoom = 1,
translateExtent,
zoomActivationKeyCode,
preventScrolling = true,
children,
noWheelClassName,
noPanClassName,
}: ZoomPaneProps) => {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
const prevTransform = useRef<FlowTransform>({ x: 0, y: 0, zoom: 0 });
const store = useStore();
const d3Zoom = useStoreState((s) => s.d3Zoom);
const d3Selection = useStoreState((s) => s.d3Selection);
const d3ZoomHandler = useStoreState((s) => s.d3ZoomHandler);
const initD3Zoom = useStoreActions((actions) => actions.initD3Zoom);
const updateTransform = useStoreActions((actions) => actions.updateTransform);
const { d3Zoom, d3Selection, d3ZoomHandler, initD3Zoom, updateTransform } = useStore(selector, shallow);
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
useResizeHandler(zoomPane);
useEffect(() => {
if (zoomPane.current) {
const state = store.getState();
const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent;
const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent);
const { minZoom, maxZoom, translateExtent } = store.getState();
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
const selection = select(zoomPane.current as Element).call(d3ZoomInstance);
const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]);
const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]);
const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom);
const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0]);
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1]);
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom);
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom);
d3ZoomInstance.transform(selection, updatedTransform);
@@ -106,7 +110,7 @@ const ZoomPane = ({
if (panOnScroll && !zoomActivationKeyPressed) {
d3Selection
.on('wheel', (event: any) => {
if (hasNoWheelClass(event)) {
if (isWrappedWithClass(event, noWheelClassName)) {
return false;
}
event.preventDefault();
@@ -140,7 +144,7 @@ const ZoomPane = ({
} else if (typeof d3ZoomHandler !== 'undefined') {
d3Selection
.on('wheel', (event: any) => {
if (!preventScrolling || hasNoWheelClass(event)) {
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
return null;
}
@@ -158,6 +162,7 @@ const ZoomPane = ({
zoomActivationKeyPressed,
zoomOnPinch,
preventScrolling,
noWheelClassName,
]);
useEffect(() => {
@@ -230,22 +235,28 @@ const ZoomPane = ({
return false;
}
if (hasNoWheelClass(event) && event.type === 'wheel') {
// if the target element is inside the nowheel class, we prevent zooming
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
return false;
}
// if the target element is inside the nopan class, we prevent panning
if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') {
return false;
}
// when the target element is a node, we still allow zooming
if (
(event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) &&
event.type !== 'wheel'
) {
return false;
}
// if (
// (event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) &&
// event.type !== 'wheel'
// ) {
// return false;
// }
// when the target element is a node selection, we still allow zooming
if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') {
return false;
}
// // when the target element is a node selection, we still allow zooming
// if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') {
// return false;
// }
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
return false;
@@ -278,7 +289,7 @@ const ZoomPane = ({
]);
return (
<div className="react-flow__renderer react-flow__zoompane" ref={zoomPane}>
<div className="react-flow__renderer react-flow__container" ref={zoomPane}>
{children}
</div>
);
+1 -3
View File
@@ -1,8 +1,6 @@
import { createContext } from 'react';
import { ElementId } from '../types';
type ContextProps = ElementId | null;
type ContextProps = string | null;
export const NodeIdContext = createContext<Partial<ContextProps>>(null);
export const Provider = NodeIdContext.Provider;
+31 -18
View File
@@ -1,42 +1,55 @@
import { useEffect } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreActions } from '../store/hooks';
import { useStore, useStoreApi } from '../store';
import useKeyPress from './useKeyPress';
import { isNode, getConnectedEdges } from '../utils/graph';
import { Elements, KeyCode, ElementId, FlowElement } from '../types';
import { getConnectedEdges } from '../utils/graph';
import { EdgeChange, KeyCode, NodeChange, ReactFlowState } from '../types';
interface HookParams {
deleteKeyCode: KeyCode;
multiSelectionKeyCode: KeyCode;
onElementsRemove?: (elements: Elements) => void;
}
export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => {
const store = useStore();
const selector = (s: ReactFlowState) => ({
unsetNodesSelection: s.unsetNodesSelection,
setMultiSelectionActive: s.setMultiSelectionActive,
resetSelectedElements: s.resetSelectedElements,
onNodesChange: s.onNodesChange,
onEdgesChange: s.onEdgesChange,
});
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive);
const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements);
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
const store = useStoreApi();
const { unsetNodesSelection, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } =
useStore(selector, shallow);
const deleteKeyPressed = useKeyPress(deleteKeyCode);
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
useEffect(() => {
const { edges, selectedElements } = store.getState();
const { nodeInternals, edges } = store.getState();
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const selectedNodes = nodes.filter((n) => n.selected);
const selectedEdges = edges.filter((e) => e.selected);
if (onElementsRemove && deleteKeyPressed && selectedElements) {
const selectedNodes = selectedElements.filter(isNode);
if (deleteKeyPressed && (selectedNodes || selectedEdges)) {
const connectedEdges = getConnectedEdges(selectedNodes, edges);
const elementsToRemove = [...selectedElements, ...connectedEdges].reduce(
(res, item) => res.set(item.id, item),
new Map<ElementId, FlowElement>()
);
onElementsRemove(Array.from(elementsToRemove.values()));
const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ id: n.id, type: 'remove' }));
const edgeChanges: EdgeChange[] = [...selectedEdges, ...connectedEdges].map((e) => ({
id: e.id,
type: 'remove',
}));
onNodesChange?.(nodeChanges);
onEdgesChange?.(edgeChanges);
unsetNodesSelection();
resetSelectedElements();
}
}, [deleteKeyPressed, onElementsRemove]);
}, [deleteKeyPressed, onNodesChange, onEdgesChange]);
useEffect(() => {
setMultiSelectionActive(multiSelectionKeyPressed);
+10 -8
View File
@@ -6,14 +6,16 @@ type Keys = Array<string>;
type PressedKeys = Set<string>;
type KeyOrCode = 'key' | 'code';
export interface UseKeyPressOptions {
target: Document | HTMLElement | ShadowRoot;
target: Document | HTMLElement | ShadowRoot | null;
}
const doc = typeof document !== 'undefined' ? document : null;
// the keycode can be a string 'a' or an array of strings ['a', 'a+d']
// a string means a single key 'a' or a combination when '+' is used 'a+d'
// an array means different possibilites. Explainer: ['a', 'd+s'] here the
// user can use the single key 'a' or the combination 'd' + 's'
export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: document }): boolean => {
export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: doc }): boolean => {
const [keyPressed, setKeyPressed] = useState(false);
// we need to remember the pressed keys in order to support combinations
@@ -64,16 +66,16 @@ export default (keyCode: KeyCode | null = null, options: UseKeyPressOptions = {
setKeyPressed(false);
};
options.target.addEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
options.target.addEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
options.target.addEventListener('blur', resetHandler);
options?.target?.addEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
options?.target?.addEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
options?.target?.addEventListener('blur', resetHandler);
return () => {
pressedKeys.current.clear();
options.target.removeEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
options.target.removeEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
options.target.removeEventListener('blur', resetHandler);
options?.target?.removeEventListener('keydown', downHandler as EventListenerOrEventListenerObject);
options?.target?.removeEventListener('keyup', upHandler as EventListenerOrEventListenerObject);
options?.target?.removeEventListener('blur', resetHandler);
};
}
}, [keyCode, setKeyPressed]);
+63
View File
@@ -0,0 +1,63 @@
import { useEffect, useRef } from 'react';
import { pointToRendererPoint } from '../utils/graph';
import { useStoreApi } from '../store';
import useZoomPanHelper from '../hooks/useZoomPanHelper';
import { OnLoad, XYPosition, Node, Edge, FlowExportObject } from '../types';
function useOnLoadHandler(onLoad: OnLoad<any> | undefined) {
const isInitialized = useRef<boolean>(false);
const store = useStoreApi();
const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView, initialized } = useZoomPanHelper();
useEffect(() => {
if (!isInitialized.current && initialized) {
if (onLoad) {
const project = (position: XYPosition): XYPosition => {
const { transform, snapToGrid, snapGrid } = store.getState();
return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
};
const getNodes = (): Node[] => {
const { nodeInternals } = store.getState();
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
return nodes.map((n) => ({ ...n }));
};
const getEdges = (): Edge[] => {
const { edges = [] } = store.getState();
return edges.map((e) => ({ ...e }));
};
const toObject = (): FlowExportObject => {
const { nodeInternals, edges = [], transform } = store.getState();
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
return {
nodes: nodes.map((n) => ({ ...n })),
edges: edges.map((e) => ({ ...e })),
position: [transform[0], transform[1]],
zoom: transform[2],
};
};
onLoad({
fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn,
zoomOut,
zoomTo,
setTransform,
project,
getNodes,
getEdges,
toObject,
});
}
isInitialized.current = true;
}
}, [onLoad, zoomIn, zoomOut, zoomTo, setTransform, fitView, initialized]);
}
export default useOnLoadHandler;
+9 -4
View File
@@ -1,10 +1,13 @@
import { useEffect, MutableRefObject } from 'react';
import { useStoreActions } from '../store/hooks';
import { useStore } from '../store';
import { getDimensions } from '../utils';
import { ReactFlowState } from '../types';
export default (rendererNode: MutableRefObject<HTMLDivElement | null>) => {
const updateSize = useStoreActions((actions) => actions.updateSize);
const updateSizeSelector = (state: ReactFlowState) => state.updateSize;
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>) {
const updateSize = useStore(updateSizeSelector);
useEffect(() => {
let resizeObserver: ResizeObserver;
@@ -39,4 +42,6 @@ export default (rendererNode: MutableRefObject<HTMLDivElement | null>) => {
}
};
}, []);
};
}
export default useResizeHandler;
+7 -5
View File
@@ -1,13 +1,15 @@
import { useCallback } from 'react';
import { useStoreActions } from '../store/hooks';
import { ElementId, UpdateNodeInternals } from '../types';
import { useStore } from '../store';
import { UpdateNodeInternals, ReactFlowState } from '../types';
const updateNodeDimsSelector = (state: ReactFlowState) => state.updateNodeDimensions;
function useUpdateNodeInternals(): UpdateNodeInternals {
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const updateNodeDimensions = useStore(updateNodeDimsSelector);
return useCallback<UpdateNodeInternals>((id: ElementId) => {
const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`);
return useCallback<UpdateNodeInternals>((id: string) => {
const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`) as HTMLDivElement;
if (nodeElement) {
updateNodeDimensions([{ id, nodeElement, forceUpdate: true }]);
+72
View File
@@ -0,0 +1,72 @@
import { useCallback } from 'react';
import { useStore } from '../store';
import { isEdgeVisible } from '../container/EdgeRenderer/utils';
import { ReactFlowState, NodeInternals, Edge } from '../types';
function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals) {
let maxLevel = -1;
const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => {
const z = edge.zIndex || Math.max(nodeInternals.get(edge.source)?.z || 0, nodeInternals.get(edge.target)?.z || 0);
if (tree[z]) {
tree[z].push(edge);
} else {
tree[z] = [edge];
}
maxLevel = z > maxLevel ? z : maxLevel;
return tree;
}, {});
return Object.entries(levelLookup).map(([key, edges]) => {
const level = +key;
return {
edges,
level,
isMaxLevel: level === maxLevel,
};
});
}
function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternals) {
const edges = useStore(
useCallback(
(s: ReactFlowState) => {
if (!onlyRenderVisible) {
return s.edges;
}
return s.edges.filter((e) => {
const sourceNode = nodeInternals.get(e.source);
const targetNode = nodeInternals.get(e.target);
return (
sourceNode?.width &&
sourceNode?.height &&
targetNode?.width &&
targetNode?.height &&
isEdgeVisible({
sourcePos: sourceNode.position || { x: 0, y: 0 },
targetPos: targetNode.position || { x: 0, y: 0 },
sourceWidth: sourceNode.width,
sourceHeight: sourceNode.height,
targetWidth: targetNode.width,
targetHeight: targetNode.height,
width: s.width,
height: s.height,
transform: s.transform,
})
);
});
},
[onlyRenderVisible, nodeInternals]
)
);
return groupEdgesByZLevel(edges, nodeInternals);
}
export default useVisibleEdges;
+24
View File
@@ -0,0 +1,24 @@
import { useCallback } from 'react';
import { useStore } from '../store';
import { getNodesInside } from '../utils/graph';
import { ReactFlowState } from '../types';
function useVisibleNodes(onlyRenderVisible: boolean) {
const nodes = useStore(
useCallback(
(s: ReactFlowState) => {
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(s.nodeInternals).map(([_, node]) => node);
return onlyRenderVisible
? getNodesInside(nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: nodes;
},
[onlyRenderVisible]
)
);
return nodes;
}
export default useVisibleNodes;

Some files were not shown because too many files have changed in this diff Show More