Merge pull request #1644 from wbkd/refactor/nested-relative
new node and edge handling, state management, subflows, layering
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Generated
+63
-7
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,7 +1,6 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
|
||||
import { getCenter } from './utils';
|
||||
import { EdgeProps, Position } from '../../types';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }]);
|
||||
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user