Merge branch 'next' into hotfix/base-edge-add-suport-classname

This commit is contained in:
moklick
2023-12-19 13:22:52 +01:00
116 changed files with 2793 additions and 2194 deletions
@@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = {
left: 'auto', left: 'auto',
}; };
const CustomNode: FC<NodeProps> = ({ data, positionAbsolute }) => { const CustomNode: FC<NodeProps> = ({ data, positionAbsoluteX, positionAbsoluteY }) => {
return ( return (
<> <>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
@@ -18,7 +18,7 @@ const CustomNode: FC<NodeProps> = ({ data, positionAbsolute }) => {
<div> <div>
Position:{' '} Position:{' '}
<strong> <strong>
{positionAbsolute.x.toFixed(2)},{positionAbsolute.y.toFixed(2)} {positionAbsoluteX.toFixed(2)},{positionAbsoluteY.toFixed(2)}
</strong> </strong>
</div> </div>
</div> </div>
-6
View File
@@ -21,7 +21,6 @@ import Interaction from '../examples/Interaction';
import Intersection from '../examples/Intersection'; import Intersection from '../examples/Intersection';
import Layouting from '../examples/Layouting'; import Layouting from '../examples/Layouting';
import MultiFlows from '../examples/MultiFlows'; import MultiFlows from '../examples/MultiFlows';
import NestedNodes from '../examples/NestedNodes';
import NodeResizer from '../examples/NodeResizer'; import NodeResizer from '../examples/NodeResizer';
import NodeTypeChange from '../examples/NodeTypeChange'; import NodeTypeChange from '../examples/NodeTypeChange';
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange'; import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
@@ -185,11 +184,6 @@ const routes: IRoute[] = [
path: 'multiflows', path: 'multiflows',
component: MultiFlows, component: MultiFlows,
}, },
{
name: 'Nested Nodes',
path: 'nested-nodes',
component: NestedNodes,
},
{ {
name: 'Node Type Change', name: 'Node Type Change',
path: 'nodetype-change', path: 'nodetype-change',
@@ -4,8 +4,12 @@ import { Node, Position, MarkerType, XYPosition } from '@xyflow/react';
// of the line between the center of the intersectionNode and the target node // of the line between the center of the intersectionNode and the target node
function getNodeIntersection(intersectionNode: Node, targetNode: Node) { function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a // 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 } = intersectionNode;
const intersectionNodePosition = intersectionNode.computed?.positionAbsolute!; const {
width: intersectionNodeWidth,
height: intersectionNodeHeight,
positionAbsolute: intersectionNodePosition,
} = intersectionNode.computed || {};
const targetPosition = targetNode.computed?.positionAbsolute!; const targetPosition = targetNode.computed?.positionAbsolute!;
const w = intersectionNodeWidth! / 2; const w = intersectionNodeWidth! / 2;
@@ -5,11 +5,11 @@ import { Position, XYPosition, Node, Edge } from '@xyflow/react';
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition { 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 // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const { const { position: intersectionNodePosition } = intersectionNode;
width: intersectionNodeWidth, const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.computed ?? {
height: intersectionNodeHeight, width: 0,
position: intersectionNodePosition, height: 0,
} = intersectionNode; };
const targetPosition = targetNode.position; const targetPosition = targetNode.position;
const w = (intersectionNodeWidth ?? 0) / 2; const w = (intersectionNodeWidth ?? 0) / 2;
+4 -2
View File
@@ -48,8 +48,10 @@ const initialEdges: Edge[] = [
const setHidden = (hidden: boolean) => (els: any[]) => const setHidden = (hidden: boolean) => (els: any[]) =>
els.map((e: any) => { els.map((e: any) => {
e.hidden = hidden; return {
return e; ...e,
hidden,
};
}); });
const HiddenFlow = () => { const HiddenFlow = () => {
@@ -56,16 +56,16 @@ const LayoutFlow = () => {
const layoutedNodes = nodes.map((node) => { const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id); 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 node; return {
...node,
targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
position: {
x: nodeWithPosition.x,
y: nodeWithPosition.y,
},
};
}); });
setNodes(layoutedNodes); setNodes(layoutedNodes);
@@ -1,187 +0,0 @@
import { useState, MouseEvent, useCallback } from 'react';
import {
ReactFlow,
Controls,
MiniMap,
Background,
addEdge,
useNodesState,
useEdgesState,
Node,
Edge,
ReactFlowInstance,
Connection,
} from '@xyflow/react';
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' },
];
const NestedFlow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
},
[setEdges]
);
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => 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?.setViewport({ 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;
});
});
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}
>
<MiniMap pannable />
<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;
+167 -2
View File
@@ -1,4 +1,4 @@
import { useState, useCallback } from 'react'; import { useState, useCallback, useRef, useEffect } from 'react';
import { import {
ReactFlow, ReactFlow,
Edge, Edge,
@@ -16,6 +16,7 @@ import {
} from '@xyflow/react'; } from '@xyflow/react';
import { getNodesAndEdges } from './utils'; import { getNodesAndEdges } from './utils';
import { FrameRecorder, generateMouseEventParamsTargetingNode, nextFrame } from './performanceUtils';
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25); const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
@@ -25,6 +26,166 @@ const StressFlow = () => {
const onConnect = useCallback((connection: Connection) => { const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds)); setEdges((eds) => addEdge(connection, eds));
}, []); }, []);
const dragInViewport = async () => {
// Note: selecting specifically node 18, as its normally located in the right part of the viewport
// which means dragging it left is safe to do without scrolling the viewport.
const nodeElement = document.querySelector('.react-flow__node[data-id="18"]');
if (!nodeElement) throw new Error('Node with id 18 not found');
const frameRecorder = new FrameRecorder();
// Hold down the mouse
frameRecorder.setStage('mousedown');
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
await nextFrame();
// Start at the node position and move the mouse 5px to the left on every frame
frameRecorder.setStage('mousemove');
let currentXPosition = mouseDownEvent.clientX;
for (let iteration = 0; iteration < 20; ++iteration) {
const movementX = -5;
currentXPosition += movementX;
nodeElement.dispatchEvent(
new MouseEvent('mousemove', {
...mouseDownEvent,
clientX: currentXPosition,
screenX: currentXPosition,
movementX,
})
);
await nextFrame();
}
// Release the mouse
frameRecorder.setStage('mouseup');
nodeElement.dispatchEvent(
new MouseEvent('mouseup', {
...mouseDownEvent,
clientX: currentXPosition,
screenX: currentXPosition,
})
);
await nextFrame();
// Log the results
await frameRecorder.endRecordingAsync();
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
};
const dragOutsideViewport = async () => {
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
if (!nodeElement) throw new Error('Node not found');
const frameRecorder = new FrameRecorder();
// Hold down the mouse
frameRecorder.setStage('mousedown');
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
await nextFrame();
// Move the mouse to the top of the viewport (so that the viewport starts
// scrolling up). Then, wiggle the mouse up and down to keep the viewport
// scrolling.
frameRecorder.setStage('mousemove');
let currentYPosition = 50;
for (let iteration = 0; iteration < 20; ++iteration) {
const movementY = Math.random() > 0.5 ? +2 : -2;
currentYPosition += movementY;
nodeElement.dispatchEvent(
new MouseEvent('mousemove', {
...mouseDownEvent,
clientY: currentYPosition,
screenY: currentYPosition,
movementY,
})
);
await nextFrame();
}
// Release the mouse
frameRecorder.setStage('mouseup');
nodeElement.dispatchEvent(
new MouseEvent('mouseup', {
...mouseDownEvent,
clientY: currentYPosition,
screenY: currentYPosition,
})
);
await nextFrame();
// Log the results
await frameRecorder.endRecordingAsync();
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
};
const selectNode = async () => {
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
if (!nodeElement) throw new Error('Node not found');
const frameRecorder = new FrameRecorder();
const mouseEvent = generateMouseEventParamsTargetingNode(nodeElement);
// mousedown
frameRecorder.setStage('mousedown');
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseEvent));
await nextFrame();
// click
frameRecorder.setStage('click');
nodeElement.dispatchEvent(new MouseEvent('click', mouseEvent));
await nextFrame();
// mouseup
frameRecorder.setStage('mouseup');
nodeElement.dispatchEvent(new MouseEvent('mouseup', mouseEvent));
await nextFrame();
// Log the results
await frameRecorder.endRecordingAsync();
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
};
const [key, setKey] = useState(0);
const frameRecorderRef = useRef<FrameRecorder | null>(null);
function remount() {
frameRecorderRef.current = new FrameRecorder();
setKey((k) => k + 1);
}
useEffect(() => {
const frameRecorder = frameRecorderRef.current;
if (!frameRecorder) return;
frameRecorder.endRecordingAsync().then(() => {
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
frameRecorderRef.current = null;
});
}, [key]);
const updatePos = () => { const updatePos = () => {
setNodes((nds) => { setNodes((nds) => {
return nds.map((n) => { return nds.map((n) => {
@@ -56,6 +217,7 @@ const StressFlow = () => {
return ( return (
<ReactFlow <ReactFlow
key={key}
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
onConnect={onConnect} onConnect={onConnect}
@@ -64,11 +226,14 @@ const StressFlow = () => {
minZoom={0.2} minZoom={0.2}
fitView fitView
> >
<MiniMap />
<Controls /> <Controls />
<Background /> <Background />
<Panel position="top-right"> <Panel position="top-right">
<button onClick={selectNode}>select node</button>
<button onClick={dragInViewport}>drag node within the viewport</button>
<button onClick={dragOutsideViewport}>drag node outside of the viewport</button>
<button onClick={remount}>re-mount</button>
<button onClick={updatePos}>change pos</button> <button onClick={updatePos}>change pos</button>
<button onClick={updateElements}>update elements</button> <button onClick={updateElements}>update elements</button>
</Panel> </Panel>
@@ -0,0 +1,150 @@
type Frame = {
duration: number;
stage: string;
};
/**
* Measures and outputs the duration of every frame that happens between the
* instance is created and `endRecording()` is called.
*
* Usage:
*
* ```ts
* const recorder = new FrameRecorder();
*
* // Do some performance-intensive stuff
*
* await recorder.endRecordingAsync();
*
* console.log(recorder.getFrames());
* console.log(recorder.getFramesForObservable()); // → paste into https://observablehq.com/@iamakulov/long-frame-visualizer
* ```
*/
export class FrameRecorder {
private frames: Frame[] = [];
private animationFrameId: number;
private stage: string = '<no stage>';
constructor() {
let lastFrameTimestamp = performance.now();
const measureFrame = () => {
const timestamp = performance.now();
// Visualize the frames in the Performance pane (see the collapsed
// “Timings” section) so its easier to see what exactly each frame
// captured
performance.measure(`frame (${this.stage})`, {
start: lastFrameTimestamp,
end: timestamp,
});
this.frames.push({
duration: timestamp - lastFrameTimestamp,
stage: this.stage,
});
lastFrameTimestamp = timestamp;
this.animationFrameId = requestAnimationFrame(measureFrame);
};
this.animationFrameId = requestAnimationFrame(measureFrame);
}
// The method is explicitly marked `async` in its name to make sure the caller
// doesnt forget to `await` it. (Otherwise, some events might be lost.)
async endRecordingAsync() {
this.setStage('waiting for idle');
await new Promise((resolve) => requestIdleCallback(resolve));
requestAnimationFrame(() => {
cancelAnimationFrame(this.animationFrameId);
});
}
/**
* Adds an optional annotation to all subsequent frames. Useful to
* differentiate frames from different events e.g. you can call
* `setState("mousedown")` before dispatching a mousedown event, and then
* `setState("mouseup")` before a mouseup one.
*
* When used, will affect both `getFramesForObservable()` and `getFrames()`.
*/
setStage(stage: string) {
this.stage = stage;
}
getFramesForObservable() {
return this.frames.map((frame, index) => ({ ...frame, index }));
}
getFrames() {
// Group frames by stage so you could see which frames originated from `mousedown` vs `mousemove` vs `mouseup` events
const framesPerStage: Record<string, number[]> = {};
for (const frame of this.frames) {
const stage = frame.stage;
if (!framesPerStage[stage]) {
framesPerStage[stage] = [];
}
framesPerStage[stage].push(frame.duration);
}
// If theres only one stage, return the frames directly
return framesPerStage;
}
}
/**
* Returns a promise that resolves when the next frame starts, and everything
* that was already scheduled in the event queue has been processed.
*/
export function nextFrame() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
/**
* Generates params for a new MouseEvent() that will target the given node.
*/
export function generateMouseEventParamsTargetingNode(node: Element) {
const nodePosition = node.getBoundingClientRect();
// Lets make the event (eg click) happen 5px to the right and 5px to the
// bottom of the nodes top-left corner
const positionRelativeToNode = {
left: 5,
top: 5,
};
return {
clientX: Math.round(nodePosition.left + positionRelativeToNode.left),
clientY: Math.round(nodePosition.top + positionRelativeToNode.top),
movementX: 0,
movementY: 0,
offsetX: positionRelativeToNode.left,
offsetY: positionRelativeToNode.top,
screenX: Math.round(nodePosition.left + positionRelativeToNode.left),
screenY: Math.round(nodePosition.top + positionRelativeToNode.top),
// Required boilerplate
altKey: false,
bubbles: true,
button: 0,
buttons: 1,
cancelBubble: false,
cancelable: true,
composed: true,
ctrlKey: false,
currentTarget: null,
defaultPrevented: false,
detail: 1,
eventPhase: 0,
fromElement: null,
isTrusted: true,
metaKey: false,
relatedTarget: null,
returnValue: true,
shiftKey: false,
view: window,
which: 1,
};
}
@@ -11,13 +11,13 @@ const idStyle: CSSProperties = {
left: 2, left: 2,
}; };
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsolute, id }) => { const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
return ( return (
<> <>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
<div style={idStyle}>{id}</div> <div style={idStyle}>{id}</div>
<div style={infoStyle}> <div style={infoStyle}>
x:{Math.round(positionAbsolute.x)} y:{Math.round(positionAbsolute.y)} z:{zIndex} x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
</div> </div>
<Handle type="source" position={Position.Bottom} /> <Handle type="source" position={Position.Bottom} />
</> </>
@@ -6,7 +6,6 @@ import {
addEdge, addEdge,
applyNodeChanges, applyNodeChanges,
applyEdgeChanges, applyEdgeChanges,
ReactFlowInstance,
Connection, Connection,
Edge, Edge,
Node, Node,
@@ -97,7 +96,6 @@ const initialEdges: Edge[] = [
{ id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' }, { id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' },
]; ];
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) => const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
console.log(`start update ${handleType} handle`, edge); console.log(`start update ${handleType} handle`, edge);
const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) =>
@@ -111,7 +109,6 @@ const UpdatableEdge = () => {
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els)); const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
const onNodesChange = useCallback((changes: NodeChange[]) => { const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log(changes);
setNodes((ns) => applyNodeChanges(changes, ns)); setNodes((ns) => applyNodeChanges(changes, ns));
}, []); }, []);
@@ -125,12 +122,12 @@ const UpdatableEdge = () => {
edges={edges} edges={edges}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onInit={onInit}
snapToGrid={true} snapToGrid={true}
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onConnect={onConnect} onConnect={onConnect}
onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
fitView
> >
<Controls /> <Controls />
</ReactFlow> </ReactFlow>
@@ -24,9 +24,12 @@ const UpdateNode = () => {
nds.map((n) => { nds.map((n) => {
if (n.id === '1') { if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change // it's important that you create a new object here in order to notify react flow about the change
n.data = { return {
...n.data, ...n,
label: nodeName, data: {
...n.data,
label: nodeName,
},
}; };
} }
@@ -40,7 +43,10 @@ const UpdateNode = () => {
nds.map((n) => { nds.map((n) => {
if (n.id === '1') { if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change // it's important that you create a new object here in order to notify react flow about the change
n.style = { ...n.style, backgroundColor: nodeBg }; return {
...n,
style: { ...n.style, backgroundColor: nodeBg },
};
} }
return n; return n;
@@ -52,8 +58,10 @@ const UpdateNode = () => {
setNodes((nds) => setNodes((nds) =>
nds.map((n) => { nds.map((n) => {
if (n.id === '1' || n.id === 'e1-2') { if (n.id === '1' || n.id === 'e1-2') {
// when you update a simple type you can just update the value return {
n.hidden = nodeHidden; ...n,
hidden: nodeHidden,
};
} }
return n; return n;
@@ -12,8 +12,8 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI
[nodeId] [nodeId]
); );
const connections = useHandleConnections({ const connections = useHandleConnections({
handleType: handleProps.type, type: handleProps.type,
handleId: handleProps.id, id: handleProps.id,
onConnect, onConnect,
onDisconnect, onDisconnect,
}); });
@@ -15,8 +15,8 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI
[nodeId] [nodeId]
); );
const connections = useHandleConnections({ const connections = useHandleConnections({
handleType: handleProps.type, type: handleProps.type,
handleId: handleProps.id, id: handleProps.id,
onConnect, onConnect,
onDisconnect, onDisconnect,
}); });
@@ -1,15 +1,16 @@
<script lang="ts"> <script lang="ts">
import { Handle, Position, type NodeProps, type XYPosition } from '@xyflow/svelte'; import { Handle, Position, type NodeProps } from '@xyflow/svelte';
type $$Props = NodeProps; type $$Props = NodeProps;
export let data: { label: string } = { label: 'Node' }; export let data: { label: string } = { label: 'Node' };
export let positionAbsolute: XYPosition = { x: 0, y: 0 }; export let positionAbsoluteX: number = 0;
export let positionAbsoluteY: number = 0;
</script> </script>
<div class="custom"> <div class="custom">
<div>{data.label}</div> <div>{data.label}</div>
<div>{~~positionAbsolute.x}, {~~positionAbsolute.y}</div> <div>{~~positionAbsoluteX}, {~~positionAbsoluteY}</div>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
@@ -1,21 +1,18 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { memo } from 'react'; import { ComponentType, memo } from 'react';
import { NodeOrigin, getNodePositionWithOrigin } from '@xyflow/system';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { getNodePositionWithOrigin } from '@xyflow/system';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types'; import type { ReactFlowState } from '../../types';
import MiniMapNode from './MiniMapNode'; import MiniMapNode from './MiniMapNode';
import type { MiniMapNodes, GetMiniMapNodeAttribute } from './types'; import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
declare const window: any; declare const window: any;
const selector = (s: ReactFlowState) => s.nodeOrigin; const selector = (s: ReactFlowState) => s.nodeOrigin;
const selectorNodes = (s: ReactFlowState) => const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id);
s.nodes.filter(
(node) => !node.hidden && (node.computed?.width || node.width) && (node.computed?.height || node.height)
);
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
function MiniMapNodes({ function MiniMapNodes({
@@ -28,8 +25,8 @@ function MiniMapNodes({
// a component properly. // a component properly.
nodeComponent: NodeComponent = MiniMapNode, nodeComponent: NodeComponent = MiniMapNode,
onClick, onClick,
}: MiniMapNodes) { }: MiniMapNodesProps) {
const nodes = useStore(selectorNodes, shallow); const nodeIds = useStore(selectorNodeIds, shallow);
const nodeOrigin = useStore(selector); const nodeOrigin = useStore(selector);
const nodeColorFunc = getAttrFunction(nodeColor); const nodeColorFunc = getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
@@ -39,33 +36,78 @@ function MiniMapNodes({
return ( return (
<> <>
{nodes.map((node) => { {nodeIds.map((nodeId) => (
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute; // The split of responsibilities between MiniMapNodes and
const color = nodeColor === undefined ? undefined : nodeColorFunc(node); // NodeComponentWrapper may appear weird. However, its designed to
const strokeColor = nodeStrokeColor === undefined ? undefined : nodeStrokeColorFunc(node); // minimize the cost of updates when individual nodes change.
//
return ( // For more details, see a similar commit in `NodeRenderer/index.tsx`.
<NodeComponent <NodeComponentWrapper
key={node.id} key={nodeId}
x={x} id={nodeId}
y={y} nodeOrigin={nodeOrigin}
width={node.computed?.width ?? node.width ?? 0} nodeColorFunc={nodeColorFunc}
height={node.computed?.height ?? node.height ?? 0} nodeStrokeColorFunc={nodeStrokeColorFunc}
style={node.style} nodeClassNameFunc={nodeClassNameFunc}
selected={!!node.selected} nodeBorderRadius={nodeBorderRadius}
className={nodeClassNameFunc(node)} nodeStrokeWidth={nodeStrokeWidth}
color={color} NodeComponent={NodeComponent}
borderRadius={nodeBorderRadius} onClick={onClick}
strokeColor={strokeColor} shapeRendering={shapeRendering}
strokeWidth={nodeStrokeWidth} />
shapeRendering={shapeRendering} ))}
onClick={onClick}
id={node.id}
/>
);
})}
</> </>
); );
} }
const NodeComponentWrapper = memo(function NodeComponentWrapper({
id,
nodeOrigin,
nodeColorFunc,
nodeStrokeColorFunc,
nodeClassNameFunc,
nodeBorderRadius,
nodeStrokeWidth,
shapeRendering,
NodeComponent,
onClick,
}: {
id: string;
nodeOrigin: NodeOrigin;
nodeColorFunc: GetMiniMapNodeAttribute;
nodeStrokeColorFunc: GetMiniMapNodeAttribute;
nodeClassNameFunc: GetMiniMapNodeAttribute;
nodeBorderRadius: number;
nodeStrokeWidth?: number;
NodeComponent: ComponentType<MiniMapNodeProps>;
onClick: MiniMapNodesProps['onClick'];
shapeRendering: string;
}) {
const node = useStore((s) => s.nodeLookup.get(id));
if (!node || node.hidden || !(node.computed?.width || node.width) || !(node.computed?.height || node.height)) {
return null;
}
const positionOrigin = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
return (
<NodeComponent
x={positionOrigin.x}
y={positionOrigin.y}
width={node.computed?.width ?? node.width ?? 0}
height={node.computed?.height ?? node.height ?? 0}
style={node.style}
selected={!!node.selected}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
onClick={onClick}
id={node.id}
/>
);
});
export default memo(MiniMapNodes); export default memo(MiniMapNodes);
@@ -4,12 +4,12 @@ import type { PanelPosition, XYPosition } from '@xyflow/system';
import type { Node } from '../../types'; import type { Node } from '../../types';
export type GetMiniMapNodeAttribute<NodeData = any> = (node: Node<NodeData>) => string; export type GetMiniMapNodeAttribute<NodeType extends Node = Node> = (node: NodeType) => string;
export type MiniMapProps<NodeData = any> = Omit<HTMLAttributes<SVGSVGElement>, 'onClick'> & { export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVGSVGElement>, 'onClick'> & {
nodeColor?: string | GetMiniMapNodeAttribute<NodeData>; nodeColor?: string | GetMiniMapNodeAttribute<NodeType>;
nodeStrokeColor?: string | GetMiniMapNodeAttribute<NodeData>; nodeStrokeColor?: string | GetMiniMapNodeAttribute<NodeType>;
nodeClassName?: string | GetMiniMapNodeAttribute<NodeData>; nodeClassName?: string | GetMiniMapNodeAttribute<NodeType>;
nodeBorderRadius?: number; nodeBorderRadius?: number;
nodeStrokeWidth?: number; nodeStrokeWidth?: number;
nodeComponent?: ComponentType<MiniMapNodeProps>; nodeComponent?: ComponentType<MiniMapNodeProps>;
@@ -18,7 +18,7 @@ export type MiniMapProps<NodeData = any> = Omit<HTMLAttributes<SVGSVGElement>, '
maskStrokeWidth?: number; maskStrokeWidth?: number;
position?: PanelPosition; position?: PanelPosition;
onClick?: (event: MouseEvent, position: XYPosition) => void; onClick?: (event: MouseEvent, position: XYPosition) => void;
onNodeClick?: (event: MouseEvent, node: Node<NodeData>) => void; onNodeClick?: (event: MouseEvent, node: NodeType) => void;
pannable?: boolean; pannable?: boolean;
zoomable?: boolean; zoomable?: boolean;
ariaLabel?: string | null; ariaLabel?: string | null;
@@ -27,8 +27,8 @@ export type MiniMapProps<NodeData = any> = Omit<HTMLAttributes<SVGSVGElement>, '
offsetScale?: number; offsetScale?: number;
}; };
export type MiniMapNodes = Pick< export type MiniMapNodes<NodeType extends Node = Node> = Pick<
MiniMapProps, MiniMapProps<NodeType>,
'nodeColor' | 'nodeStrokeColor' | 'nodeClassName' | 'nodeBorderRadius' | 'nodeStrokeWidth' | 'nodeComponent' 'nodeColor' | 'nodeStrokeColor' | 'nodeClassName' | 'nodeBorderRadius' | 'nodeStrokeWidth' | 'nodeComponent'
> & { > & {
onClick?: (event: MouseEvent, nodeId: string) => void; onClick?: (event: MouseEvent, nodeId: string) => void;
@@ -9,16 +9,20 @@ import { useNodeId } from '../../contexts/NodeIdContext';
import NodeToolbarPortal from './NodeToolbarPortal'; import NodeToolbarPortal from './NodeToolbarPortal';
import { NodeToolbarProps } from './types'; import { NodeToolbarProps } from './types';
const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) => const nodeEqualityFn = (a?: Node, b?: Node) =>
a?.computed?.positionAbsolute?.x === b?.computed?.positionAbsolute?.x && a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x ||
a?.computed?.positionAbsolute?.y === b?.computed?.positionAbsolute?.y && a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y ||
a?.width === b?.width && a?.computed?.width !== b?.computed?.width ||
a?.height === b?.height && a?.computed?.height !== b?.computed?.height ||
a?.selected === b?.selected && a?.selected !== b?.selected ||
a?.[internalsSymbol]?.z === b?.[internalsSymbol]?.z; a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z;
const nodesEqualityFn = (a: Node[], b: Node[]) => { const nodesEqualityFn = (a: Node[], b: Node[]) => {
return a.length === b.length && a.every((node, i) => nodeEqualityFn(node, b[i])); if (a.length !== b.length) {
return false;
}
return !a.some((node, i) => nodeEqualityFn(node, b[i]));
}; };
const storeSelector = (state: ReactFlowState) => ({ const storeSelector = (state: ReactFlowState) => ({
@@ -0,0 +1,137 @@
// Updatable edges have a anchors around their handles to update the edge.
import { XYHandle, type Connection, EdgePosition } from '@xyflow/system';
import { EdgeAnchor } from '../Edges/EdgeAnchor';
import type { EdgeWrapperProps, Edge } from '../../types/edges';
import { useStoreApi } from '../../hooks/useStore';
type EdgeUpdateAnchorsProps = {
edge: Edge;
isUpdatable: boolean | 'source' | 'target';
edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius'];
sourceHandleId: EdgeWrapperProps['sourceHandleId'];
targetHandleId: EdgeWrapperProps['targetHandleId'];
onEdgeUpdate: EdgeWrapperProps['onEdgeUpdate'];
onEdgeUpdateStart: EdgeWrapperProps['onEdgeUpdateStart'];
onEdgeUpdateEnd: EdgeWrapperProps['onEdgeUpdateEnd'];
setUpdateHover: (hover: boolean) => void;
setUpdating: (updating: boolean) => void;
} & EdgePosition;
function EdgeUpdateAnchors({
isUpdatable,
edgeUpdaterRadius,
edge,
targetHandleId,
sourceHandleId,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
setUpdating,
setUpdateHover,
}: EdgeUpdateAnchorsProps) {
const store = useStoreApi();
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
// avoid triggering edge updater if mouse btn is not left
if (event.button !== 0) {
return;
}
const {
autoPanOnConnect,
domNode,
isValidConnection,
connectionMode,
connectionRadius,
lib,
onConnectStart,
onConnectEnd,
cancelConnection,
nodes,
panBy,
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? edge.target : edge.source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isTarget = isSourceHandle;
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType);
};
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
nodes,
isTarget,
edgeUpdaterType: handleType,
lib,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
updateConnection,
getTransform: () => store.getState().transform,
});
};
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdateHover(true);
const onEdgeUpdaterMouseOut = () => setUpdateHover(false);
return (
<>
{(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor
position={sourcePosition}
centerX={sourceX}
centerY={sourceY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="source"
/>
)}
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={targetPosition}
centerX={targetX}
centerY={targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="target"
/>
)}
</>
);
}
export default EdgeUpdateAnchors;
@@ -0,0 +1,266 @@
import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
getMarkerId,
elementSelectionKeys,
getEdgePosition,
errorMessages,
getElevatedEdgeZIndex,
} from '@xyflow/system';
import { useStoreApi, useStore } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import type { EdgeWrapperProps } from '../../types';
import { builtinEdgeTypes, nullPosition } from './utils';
import EdgeUpdateAnchors from './EdgeUpdateAnchors';
function EdgeWrapper({
id,
edgesFocusable,
edgesUpdatable,
elementsSelectable,
onClick,
onDoubleClick,
sourceHandleId,
targetHandleId,
onContextMenu,
onMouseEnter,
onMouseMove,
onMouseLeave,
edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
rfId,
edgeTypes,
noPanClassName,
onError,
}: EdgeWrapperProps): JSX.Element | null {
let edge = useStore((s) => s.edgeLookup.get(id)!);
const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);
edge = defaultEdgeOptions ? { ...defaultEdgeOptions, ...edge } : edge;
let edgeType = edge.type || 'default';
let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType];
if (EdgeComponent === undefined) {
onError?.('011', errorMessages['error011'](edgeType));
edgeType = 'default';
EdgeComponent = builtinEdgeTypes.default;
}
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isUpdatable =
typeof onEdgeUpdate !== 'undefined' &&
(edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined'));
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
const edgeRef = useRef<SVGGElement>(null);
const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi();
const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore(
useCallback(
(store) => {
const sourceNode = store.nodeLookup.get(edge.source);
const targetNode = store.nodeLookup.get(edge.target);
if (!sourceNode || !targetNode) {
return {
zIndex: edge.zIndex,
...nullPosition,
};
}
const edgePosition = getEdgePosition({
id,
sourceNode,
targetNode,
sourceHandle: sourceHandleId || null,
targetHandle: targetHandleId || null,
connectionMode: store.connectionMode,
onError,
});
const zIndex = getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: store.elevateEdgesOnSelect,
});
return {
zIndex,
...(edgePosition || nullPosition),
};
},
[edge.source, edge.target, edge.selected, edge.zIndex]
),
shallow
);
const markerStartUrl = useMemo(
() => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined),
[edge.markerStart, rfId]
);
const markerEndUrl = useMemo(
() => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined),
[edge.markerEnd, rfId]
);
if (edge.hidden || !sourceX || !sourceY || !targetX || !targetY) {
return null;
}
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
const { addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
if (isSelectable) {
store.setState({ nodesSelectionActive: false });
if (edge.selected && multiSelectionActive) {
unselectNodesAndEdges({ nodes: [], edges: [edge] });
edgeRef.current?.blur();
} else {
addSelectedEdges([id]);
}
}
if (onClick) {
onClick(event, edge);
}
};
const onEdgeDoubleClick = onDoubleClick
? (event: React.MouseEvent) => {
onDoubleClick(event, { ...edge });
}
: undefined;
const onEdgeContextMenu = onContextMenu
? (event: React.MouseEvent) => {
onContextMenu(event, { ...edge });
}
: undefined;
const onEdgeMouseEnter = onMouseEnter
? (event: React.MouseEvent) => {
onMouseEnter(event, { ...edge });
}
: undefined;
const onEdgeMouseMove = onMouseMove
? (event: React.MouseEvent) => {
onMouseMove(event, { ...edge });
}
: undefined;
const onEdgeMouseLeave = onMouseLeave
? (event: React.MouseEvent) => {
onMouseLeave(event, { ...edge });
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const { unselectNodesAndEdges, addSelectedEdges } = store.getState();
const unselect = event.key === 'Escape';
if (unselect) {
edgeRef.current?.blur();
unselectNodesAndEdges({ edges: [edge] });
} else {
addSelectedEdges([id]);
}
}
};
return (
<svg style={{ zIndex }}>
<g
className={cc([
'react-flow__edge',
`react-flow__edge-${edgeType}`,
edge.className,
noPanClassName,
{
selected: edge.selected,
animated: edge.animated,
inactive: !isSelectable && !onClick,
updating: updateHover,
},
])}
onClick={onEdgeClick}
onDoubleClick={onEdgeDoubleClick}
onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : 'img'}
data-id={id}
data-testid={`rf__edge-${id}`}
aria-label={
edge.ariaLabel === null ? undefined : edge.ariaLabel || `Edge from ${edge.source} to ${edge.target}`
}
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef}
>
{!updating && (
<EdgeComponent
id={id}
source={edge.source}
target={edge.target}
selected={edge.selected}
animated={edge.animated}
label={edge.label}
labelStyle={edge.labelStyle}
labelShowBg={edge.labelShowBg}
labelBgStyle={edge.labelBgStyle}
labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
data={edge.data}
style={edge.style}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
interactionWidth={edge.interactionWidth}
/>
)}
{isUpdatable && (
<EdgeUpdateAnchors
edge={edge}
isUpdatable={isUpdatable}
edgeUpdaterRadius={edgeUpdaterRadius}
onEdgeUpdate={onEdgeUpdate}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
setUpdateHover={setUpdateHover}
setUpdating={setUpdating}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
/>
)}
</g>
</svg>
);
}
EdgeWrapper.displayName = 'EdgeWrapper';
export default memo(EdgeWrapper);
@@ -0,0 +1,26 @@
import type { ComponentType } from 'react';
import type { EdgeProps, EdgeTypes } from '../../types';
import {
BezierEdgeInternal,
StraightEdgeInternal,
StepEdgeInternal,
SmoothStepEdgeInternal,
SimpleBezierEdgeInternal,
} from '../Edges';
export const builtinEdgeTypes: EdgeTypes = {
default: BezierEdgeInternal as ComponentType<EdgeProps>,
straight: StraightEdgeInternal as ComponentType<EdgeProps>,
step: StepEdgeInternal as ComponentType<EdgeProps>,
smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>,
simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>,
};
export const nullPosition = {
sourceX: null,
sourceY: null,
targetX: null,
targetY: null,
sourcePosition: null,
targetPosition: null,
};
@@ -1,20 +0,0 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { StoreApi } from 'zustand';
import type { Edge, ReactFlowState } from '../../types';
export function getMouseHandler(
id: string,
getState: StoreApi<ReactFlowState>['getState'],
handler?: (event: ReactMouseEvent<SVGGElement, MouseEvent>, edge: Edge) => void
) {
return handler === undefined
? handler
: (event: ReactMouseEvent<SVGGElement, MouseEvent>) => {
const edge = getState().edges.find((e) => e.id === id);
if (edge) {
handler(event, { ...edge });
}
};
}
@@ -1,290 +0,0 @@
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent, useCallback } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import { getMarkerId, elementSelectionKeys, XYHandle, type Connection, getEdgePosition } from '@xyflow/system';
import { useStoreApi, useStore } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { EdgeAnchor } from './EdgeAnchor';
import { getMouseHandler } from './utils';
import type { EdgeProps, WrapEdgeProps } from '../../types';
export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({
id,
className,
type,
data,
onClick,
onEdgeDoubleClick,
selected,
animated,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
source,
target,
isSelectable,
hidden,
sourceHandleId,
targetHandleId,
onContextMenu,
onMouseEnter,
onMouseMove,
onMouseLeave,
edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
markerEnd,
markerStart,
rfId,
ariaLabel,
isFocusable,
isUpdatable,
pathOptions,
interactionWidth,
}: WrapEdgeProps): JSX.Element | null => {
const edgeRef = useRef<SVGGElement>(null);
const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi();
const edgePosition = useStore(
useCallback(
(state) => {
const sourceNode = state.nodeLookup.get(source);
const targetNode = state.nodeLookup.get(target);
if (!sourceNode || !targetNode) {
return null;
}
return getEdgePosition({
id,
sourceNode,
targetNode,
sourceHandle: sourceHandleId || null,
targetHandle: targetHandleId || null,
connectionMode: state.connectionMode,
onError: state.onError,
});
},
[source, target]
),
shallow
);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
if (hidden || !edgePosition) {
return null;
}
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
const { edges, addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
const edge = edges.find((e) => e.id === id);
if (!edge) {
return;
}
if (isSelectable) {
store.setState({ nodesSelectionActive: false });
if (edge.selected && multiSelectionActive) {
unselectNodesAndEdges({ nodes: [], edges: [edge] });
edgeRef.current?.blur();
} else {
addSelectedEdges([id]);
}
}
if (onClick) {
onClick(event, edge);
}
};
const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick);
const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu);
const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter);
const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove);
const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave);
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
// avoid triggering edge updater if mouse btn is not left
if (event.button !== 0) {
return;
}
const {
autoPanOnConnect,
domNode,
edges,
isValidConnection,
connectionMode,
connectionRadius,
lib,
onConnectStart,
onConnectEnd,
cancelConnection,
nodes,
panBy,
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isTarget = isSourceHandle;
const edge = edges.find((e) => e.id === id)!;
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType);
};
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
nodes,
isTarget,
edgeUpdaterType: handleType,
lib,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
updateConnection,
getTransform: () => store.getState().transform,
});
};
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdateHover(true);
const onEdgeUpdaterMouseOut = () => setUpdateHover(false);
const inactive = !isSelectable && !onClick;
const onKeyDown = (event: KeyboardEvent) => {
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const { unselectNodesAndEdges, addSelectedEdges, edges } = store.getState();
const unselect = event.key === 'Escape';
if (unselect) {
edgeRef.current?.blur();
unselectNodesAndEdges({ edges: [edges.find((e) => e.id === id)!] });
} else {
addSelectedEdges([id]);
}
}
};
return (
<g
className={cc([
'react-flow__edge',
`react-flow__edge-${type}`,
className,
{ selected, animated, inactive, updating: updateHover },
])}
onClick={onEdgeClick}
onDoubleClick={onEdgeDoubleClickHandler}
onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : 'img'}
data-id={id}
data-testid={`rf__edge-${id}`}
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`}
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef}
>
{!updating && (
<EdgeComponent
id={id}
source={source}
target={target}
selected={selected}
animated={animated}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
data={data}
style={style}
sourceX={edgePosition.sourceX}
sourceY={edgePosition.sourceY}
targetX={edgePosition.targetX}
targetY={edgePosition.targetY}
sourcePosition={edgePosition.sourcePosition}
targetPosition={edgePosition.targetPosition}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
pathOptions={pathOptions}
interactionWidth={interactionWidth}
/>
)}
{isUpdatable && (
<>
{(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.sourcePosition}
centerX={edgePosition.sourceX}
centerY={edgePosition.sourceY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="source"
/>
)}
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.targetPosition}
centerX={edgePosition.targetX}
centerY={edgePosition.targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="target"
/>
)}
</>
)}
</g>
);
};
EdgeWrapper.displayName = 'EdgeWrapper';
return memo(EdgeWrapper);
};
@@ -0,0 +1,264 @@
import { useEffect, useRef, memo, type MouseEvent, type KeyboardEvent } from 'react';
import cc from 'classcat';
import {
clampPosition,
elementSelectionKeys,
errorMessages,
getPositionWithOrigin,
internalsSymbol,
isInputDOMNode,
} from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { Provider } from '../../contexts/NodeIdContext';
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
import useDrag from '../../hooks/useDrag';
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
import { handleNodeClick } from '../Nodes/utils';
import type { NodeWrapperProps } from '../../types';
import { arrowKeyDiffs, builtinNodeTypes } from './utils';
import { shallow } from 'zustand/shallow';
const NodeWrapper = ({
id,
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave,
onContextMenu,
onDoubleClick,
nodesDraggable,
elementsSelectable,
nodesConnectable,
nodesFocusable,
resizeObserver,
noDragClassName,
noPanClassName,
disableKeyboardA11y,
rfId,
nodeTypes,
nodeExtent,
nodeOrigin,
onError,
}: NodeWrapperProps) => {
const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => {
const node = s.nodeLookup.get(id)!;
const positionAbsolute = nodeExtent
? clampPosition(node.computed?.positionAbsolute, nodeExtent)
: node.computed?.positionAbsolute || { x: 0, y: 0 };
return {
node,
// we are mutating positionAbsolute, z and isParent attributes for sub flows
// so we we need to force a re-render when some change
positionAbsoluteX: positionAbsolute.x,
positionAbsoluteY: positionAbsolute.y,
zIndex: node[internalsSymbol]?.z ?? 0,
isParent: !!node[internalsSymbol]?.isParent,
};
}, shallow);
let nodeType = node.type || 'default';
let NodeComponent = nodeTypes?.[nodeType] || builtinNodeTypes[nodeType];
if (NodeComponent === undefined) {
onError?.('003', errorMessages['error003'](nodeType));
nodeType = 'default';
NodeComponent = builtinNodeTypes.default;
}
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 isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
const store = useStoreApi();
const nodeRef = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(node.sourcePosition);
const prevTargetPosition = useRef(node.targetPosition);
const prevType = useRef(nodeType);
const updatePositions = useUpdateNodePositions();
useEffect(() => {
if (nodeRef.current && !node.hidden) {
const currNode = nodeRef.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
}
}, [node.hidden]);
useEffect(() => {
// when the user programmatically changes the source or handle position, we re-initialize the node
const typeChanged = prevType.current !== nodeType;
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;
if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = nodeType;
}
if (sourcePosChanged) {
prevSourcePosition.current = node.sourcePosition;
}
if (targetPosChanged) {
prevTargetPosition.current = node.targetPosition;
}
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
}
}, [id, nodeType, node.sourcePosition, node.targetPosition]);
const dragging = useDrag({
nodeRef,
disabled: node.hidden || !isDraggable,
noDragClassName,
handleSelector: node.dragHandle,
nodeId: id,
isSelectable,
});
if (node.hidden) {
return null;
}
const width = node.width ?? undefined;
const height = node.height ?? undefined;
const computedWidth = node.computed?.width;
const computedHeight = node.computed?.height;
const positionAbsoluteOrigin = getPositionWithOrigin({
x: positionAbsoluteX,
y: positionAbsoluteY,
width: computedWidth ?? width ?? 0,
height: computedHeight ?? height ?? 0,
origin: node.origin || nodeOrigin,
});
const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined;
const onMouseMoveHandler = onMouseMove ? (event: MouseEvent) => onMouseMove(event, { ...node }) : undefined;
const onMouseLeaveHandler = onMouseLeave ? (event: MouseEvent) => onMouseLeave(event, { ...node }) : undefined;
const onContextMenuHandler = onContextMenu ? (event: MouseEvent) => onContextMenu(event, { ...node }) : undefined;
const onDoubleClickHandler = onDoubleClick ? (event: MouseEvent) => onDoubleClick(event, { ...node }) : undefined;
const onSelectNodeHandler = (event: MouseEvent) => {
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false
handleNodeClick({
id,
store,
nodeRef,
});
}
if (onClick) {
onClick(event, { ...node });
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (isInputDOMNode(event.nativeEvent)) {
return;
}
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const unselect = event.key === 'Escape';
handleNodeClick({
id,
store,
unselect,
nodeRef,
});
} else if (
!disableKeyboardA11y &&
isDraggable &&
node.selected &&
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
) {
store.setState({
ariaLiveMessage: `Moved selected node ${event.key
.replace('Arrow', '')
.toLowerCase()}. New position, x: ${~~positionAbsoluteX}, y: ${~~positionAbsoluteY}`,
});
updatePositions({
x: arrowKeyDiffs[event.key].x,
y: arrowKeyDiffs[event.key].y,
isShiftPressed: event.shiftKey,
});
}
};
return (
<div
className={cc([
'react-flow__node',
`react-flow__node-${nodeType}`,
{
// this is overwritable by passing `nopan` as a class name
[noPanClassName]: isDraggable,
},
node.className,
{
selected: node.selected,
selectable: isSelectable,
parent: isParent,
dragging,
},
])}
ref={nodeRef}
style={{
zIndex,
transform: `translate(${positionAbsoluteOrigin.x}px,${positionAbsoluteOrigin.y}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: initialized ? 'visible' : 'hidden',
width,
height,
...node.style,
}}
data-id={id}
data-testid={`rf__node-${id}`}
onMouseEnter={onMouseEnterHandler}
onMouseMove={onMouseMoveHandler}
onMouseLeave={onMouseLeaveHandler}
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onDoubleClickHandler}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : undefined}
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
aria-label={node.ariaLabel}
>
<Provider value={id}>
<NodeComponent
id={id}
data={node.data}
type={nodeType}
width={computedWidth}
height={computedHeight}
positionAbsoluteX={positionAbsoluteX}
positionAbsoluteY={positionAbsoluteY}
selected={node.selected}
isConnectable={isConnectable}
sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition}
dragging={dragging}
dragHandle={node.dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>
);
};
NodeWrapper.displayName = 'NodeWrapper';
export default memo(NodeWrapper);
@@ -0,0 +1,22 @@
import type { ComponentType } from 'react';
import type { NodeProps, XYPosition } from '@xyflow/system';
import InputNode from '../Nodes/InputNode';
import DefaultNode from '../Nodes/DefaultNode';
import GroupNode from '../Nodes/GroupNode';
import OutputNode from '../Nodes/OutputNode';
import type { NodeTypes } from '../../types';
export const arrowKeyDiffs: Record<string, XYPosition> = {
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
};
export const builtinNodeTypes: NodeTypes = {
input: InputNode as ComponentType<NodeProps>,
default: DefaultNode as ComponentType<NodeProps>,
output: OutputNode as ComponentType<NodeProps>,
group: GroupNode as ComponentType<NodeProps>,
};
+2 -15
View File
@@ -1,22 +1,9 @@
import type { MouseEvent, RefObject } from 'react'; import type { RefObject } from 'react';
import type { StoreApi } from 'zustand'; import type { StoreApi } from 'zustand';
import type { Node, ReactFlowState } from '../../types'; import type { ReactFlowState } from '../../types';
import { errorMessages } from '@xyflow/system'; import { errorMessages } from '@xyflow/system';
export function getMouseHandler(
id: string,
getState: StoreApi<ReactFlowState>['getState'],
handler?: (event: MouseEvent, node: Node) => void
) {
return handler === undefined
? handler
: (event: MouseEvent) => {
const node = getState().nodeLookup.get(id)!;
handler(event, { ...node });
};
}
// this handler is called by // this handler is called by
// 1. the click handler when node is not draggable or selectNodesOnDrag = false // 1. the click handler when node is not draggable or selectNodesOnDrag = false
// or // or
@@ -1,232 +0,0 @@
import { useEffect, useRef, memo, type ComponentType, type MouseEvent, type KeyboardEvent } from 'react';
import cc from 'classcat';
import { elementSelectionKeys, isInputDOMNode, type NodeProps, type XYPosition } from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { Provider } from '../../contexts/NodeIdContext';
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
import useDrag from '../../hooks/useDrag';
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
import { getMouseHandler, handleNodeClick } from './utils';
import type { WrapNodeProps } from '../../types';
export const arrowKeyDiffs: Record<string, XYPosition> = {
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
};
export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({
id,
type,
data,
xPos,
yPos,
xPosOrigin,
yPosOrigin,
selected,
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave,
onContextMenu,
onDoubleClick,
style,
className,
isDraggable,
isSelectable,
isConnectable,
isFocusable,
sourcePosition,
targetPosition,
hidden,
resizeObserver,
dragHandle,
zIndex,
isParent,
noDragClassName,
noPanClassName,
initialized,
disableKeyboardA11y,
ariaLabel,
rfId,
positionAbsolute,
width,
height,
}: WrapNodeProps) => {
const store = useStoreApi();
const nodeRef = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition);
const prevType = useRef(type);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const updatePositions = useUpdateNodePositions();
const onMouseEnterHandler = getMouseHandler(id, store.getState, onMouseEnter);
const onMouseMoveHandler = getMouseHandler(id, store.getState, onMouseMove);
const onMouseLeaveHandler = getMouseHandler(id, store.getState, onMouseLeave);
const onContextMenuHandler = getMouseHandler(id, store.getState, onContextMenu);
const onDoubleClickHandler = getMouseHandler(id, store.getState, onDoubleClick);
const onSelectNodeHandler = (event: MouseEvent) => {
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false
handleNodeClick({
id,
store,
nodeRef,
});
}
if (onClick) {
const node = store.getState().nodes.find((n) => n.id === id)!;
onClick(event, { ...node });
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (isInputDOMNode(event.nativeEvent)) {
return;
}
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const unselect = event.key === 'Escape';
handleNodeClick({
id,
store,
unselect,
nodeRef,
});
} else if (
!disableKeyboardA11y &&
isDraggable &&
selected &&
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
) {
store.setState({
ariaLiveMessage: `Moved selected node ${event.key
.replace('Arrow', '')
.toLowerCase()}. New position, x: ${~~xPos}, y: ${~~yPos}`,
});
updatePositions({
x: arrowKeyDiffs[event.key].x,
y: arrowKeyDiffs[event.key].y,
isShiftPressed: event.shiftKey,
});
}
};
useEffect(() => {
if (nodeRef.current && !hidden) {
const currNode = nodeRef.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
}
}, [hidden]);
useEffect(() => {
// when the user programmatically changes the source or handle position, we re-initialize the node
const typeChanged = prevType.current !== type;
const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
const targetPosChanged = prevTargetPosition.current !== targetPosition;
if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = type;
}
if (sourcePosChanged) {
prevSourcePosition.current = sourcePosition;
}
if (targetPosChanged) {
prevTargetPosition.current = targetPosition;
}
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
}
}, [id, type, sourcePosition, targetPosition]);
const dragging = useDrag({
nodeRef,
disabled: hidden || !isDraggable,
noDragClassName,
handleSelector: dragHandle,
nodeId: id,
isSelectable,
});
if (hidden) {
return null;
}
return (
<div
className={cc([
'react-flow__node',
`react-flow__node-${type}`,
{
// this is overwritable by passing `nopan` as a class name
[noPanClassName]: isDraggable,
},
className,
{
selected,
selectable: isSelectable,
parent: isParent,
dragging,
},
])}
ref={nodeRef}
style={{
zIndex,
transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: initialized ? 'visible' : 'hidden',
width,
height,
...style,
}}
data-id={id}
data-testid={`rf__node-${id}`}
onMouseEnter={onMouseEnterHandler}
onMouseMove={onMouseMoveHandler}
onMouseLeave={onMouseLeaveHandler}
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onDoubleClickHandler}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : undefined}
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
aria-label={ariaLabel}
>
<Provider value={id}>
<NodeComponent
id={id}
data={data}
type={type}
width={width}
height={height}
positionAbsolute={positionAbsolute}
selected={selected}
isConnectable={isConnectable}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
dragging={dragging}
dragHandle={dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>
);
};
NodeWrapper.displayName = 'NodeWrapper';
return memo(NodeWrapper);
};
@@ -10,9 +10,9 @@ import { getNodesBounds } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import useDrag from '../../hooks/useDrag'; import useDrag from '../../hooks/useDrag';
import { arrowKeyDiffs } from '../Nodes/wrapNode';
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions'; import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
import type { Node, ReactFlowState } from '../../types'; import type { Node, ReactFlowState } from '../../types';
import { arrowKeyDiffs } from '../NodeWrapper/utils';
export type NodesSelectionProps = { export type NodesSelectionProps = {
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
@@ -3,67 +3,74 @@
* We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`) * We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`)
* and values that have a dedicated setter function in the store (like `setNodes`). * and values that have a dedicated setter function in the store (like `setNodes`).
*/ */
import { useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { StoreApi } from 'zustand';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { devWarn, type CoordinateExtent } from '@xyflow/system'; import { infiniteExtent, type CoordinateExtent } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, ReactFlowStore } from '../../types'; import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
import { initNodeOrigin } from '../../container/ReactFlow';
type StoreUpdaterProps = Pick< // these fields exist in the global store and we need to keep them up to date
ReactFlowProps, const reactFlowFieldsToTrack = [
| 'nodes' 'nodes',
| 'edges' 'edges',
| 'defaultNodes' 'defaultNodes',
| 'defaultEdges' 'defaultEdges',
| 'onConnect' 'onConnect',
| 'onConnectStart' 'onConnectStart',
| 'onConnectEnd' 'onConnectEnd',
| 'onClickConnectStart' 'onClickConnectStart',
| 'onClickConnectEnd' 'onClickConnectEnd',
| 'nodesDraggable' 'nodesDraggable',
| 'nodesConnectable' 'nodesConnectable',
| 'nodesFocusable' 'nodesFocusable',
| 'edgesFocusable' 'edgesFocusable',
| 'edgesUpdatable' 'edgesUpdatable',
| 'minZoom' 'elevateNodesOnSelect',
| 'maxZoom' 'elevateEdgesOnSelect',
| 'nodeExtent' 'minZoom',
| 'onNodesChange' 'maxZoom',
| 'onEdgesChange' 'nodeExtent',
| 'elementsSelectable' 'onNodesChange',
| 'connectionMode' 'onEdgesChange',
| 'snapToGrid' 'elementsSelectable',
| 'snapGrid' 'connectionMode',
| 'translateExtent' 'snapGrid',
| 'connectOnClick' 'snapToGrid',
| 'defaultEdgeOptions' 'translateExtent',
| 'fitView' 'connectOnClick',
| 'fitViewOptions' 'defaultEdgeOptions',
| 'onNodesDelete' 'fitView',
| 'onEdgesDelete' 'fitViewOptions',
| 'onDelete' 'onNodesDelete',
| 'onNodeDragStart' 'onEdgesDelete',
| 'onNodeDrag' 'onDelete',
| 'onNodeDragStop' 'onNodeDrag',
| 'onSelectionDragStart' 'onNodeDragStart',
| 'onSelectionDrag' 'onNodeDragStop',
| 'onSelectionDragStop' 'onSelectionDrag',
| 'onMove' 'onSelectionDragStart',
| 'onMoveStart' 'onSelectionDragStop',
| 'onMoveEnd' 'onMoveStart',
| 'noPanClassName' 'onMove',
| 'nodeOrigin' 'onMoveEnd',
| 'elevateNodesOnSelect' 'noPanClassName',
| 'autoPanOnConnect' 'nodeOrigin',
| 'autoPanOnNodeDrag' 'autoPanOnConnect',
| 'onError' 'autoPanOnNodeDrag',
| 'connectionRadius' 'onError',
| 'isValidConnection' 'connectionRadius',
| 'selectNodesOnDrag' 'isValidConnection',
| 'nodeDragThreshold' 'selectNodesOnDrag',
> & { rfId: string }; 'nodeDragThreshold',
] as const;
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];
type StoreUpdaterProps = Pick<ReactFlowProps, ReactFlowFieldsToTrack> & { rfId: string };
// rfId doesn't exist in ReactFlowProps, but it's one of the fields we want to update
const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'] as const;
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
setNodes: s.setNodes, setNodes: s.setNodes,
@@ -76,80 +83,7 @@ const selector = (s: ReactFlowState) => ({
reset: s.reset, reset: s.reset,
}); });
function useStoreUpdater<T>(value: T | undefined, setStoreAction: (param: T) => void) { const StoreUpdater = (props: StoreUpdaterProps) => {
useEffect(() => {
if (typeof value !== 'undefined') {
setStoreAction(value);
}
}, [value]);
}
// updates with values in store that don't have a dedicated setter function
function useDirectStoreUpdater(
key: keyof ReactFlowStore,
value: unknown,
setState: StoreApi<ReactFlowState>['setState']
) {
useEffect(() => {
if (typeof value !== 'undefined') {
setState({ [key]: value });
}
}, [value]);
}
const StoreUpdater = ({
nodes,
edges,
defaultNodes,
defaultEdges,
onConnect,
onConnectStart,
onConnectEnd,
onClickConnectStart,
onClickConnectEnd,
nodesDraggable,
nodesConnectable,
nodesFocusable,
edgesFocusable,
edgesUpdatable,
elevateNodesOnSelect,
minZoom,
maxZoom,
nodeExtent,
onNodesChange,
onEdgesChange,
elementsSelectable,
connectionMode,
snapGrid,
snapToGrid,
translateExtent,
connectOnClick,
defaultEdgeOptions,
fitView,
fitViewOptions,
onNodesDelete,
onEdgesDelete,
onDelete,
onNodeDrag,
onNodeDragStart,
onNodeDragStop,
onSelectionDrag,
onSelectionDragStart,
onSelectionDragStop,
onMoveStart,
onMove,
onMoveEnd,
noPanClassName,
nodeOrigin,
rfId,
autoPanOnConnect,
autoPanOnNodeDrag,
onError,
connectionRadius,
isValidConnection,
selectNodesOnDrag,
nodeDragThreshold,
}: StoreUpdaterProps) => {
const { const {
setNodes, setNodes,
setEdges, setEdges,
@@ -163,64 +97,55 @@ const StoreUpdater = ({
const store = useStoreApi(); const store = useStoreApi();
useEffect(() => { useEffect(() => {
const edgesWithDefaults = defaultEdges?.map((e) => ({ ...e, ...defaultEdgeOptions })); const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions }));
setDefaultNodesAndEdges(defaultNodes, edgesWithDefaults); setDefaultNodesAndEdges(props.defaultNodes, edgesWithDefaults);
return () => { return () => {
reset(); reset();
}; };
}, []); }, []);
useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState); const previousFields = useRef<Partial<StoreUpdaterProps>>({
useDirectStoreUpdater('connectionMode', connectionMode, store.setState); // these are values that are also passed directly to other components
useDirectStoreUpdater('onConnect', onConnect, store.setState); // than the StoreUpdater. We can reduce the number of setStore calls
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState); // by setting the same values here as prev fields.
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState); translateExtent: infiniteExtent,
useDirectStoreUpdater('onClickConnectStart', onClickConnectStart, store.setState); nodeOrigin: initNodeOrigin,
useDirectStoreUpdater('onClickConnectEnd', onClickConnectEnd, store.setState); minZoom: 0.5,
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState); maxZoom: 2,
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState); elementsSelectable: true,
useDirectStoreUpdater('nodesFocusable', nodesFocusable, store.setState); noPanClassName: 'nopan',
useDirectStoreUpdater('edgesFocusable', edgesFocusable, store.setState); rfId: '1',
useDirectStoreUpdater('edgesUpdatable', edgesUpdatable, store.setState); });
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
useDirectStoreUpdater('elevateNodesOnSelect', elevateNodesOnSelect, store.setState);
useDirectStoreUpdater('snapToGrid', snapToGrid, store.setState);
useDirectStoreUpdater('snapGrid', snapGrid, store.setState);
useDirectStoreUpdater('onNodesChange', onNodesChange, store.setState);
useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState);
useDirectStoreUpdater('connectOnClick', connectOnClick, store.setState);
useDirectStoreUpdater('fitViewOnInit', fitView, store.setState);
useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState);
useDirectStoreUpdater('onNodesDelete', onNodesDelete, store.setState);
useDirectStoreUpdater('onEdgesDelete', onEdgesDelete, store.setState);
useDirectStoreUpdater('onDelete', onDelete, store.setState);
useDirectStoreUpdater('onNodeDrag', onNodeDrag, store.setState);
useDirectStoreUpdater('onNodeDragStart', onNodeDragStart, store.setState);
useDirectStoreUpdater('onNodeDragStop', onNodeDragStop, store.setState);
useDirectStoreUpdater('onSelectionDrag', onSelectionDrag, store.setState);
useDirectStoreUpdater('onSelectionDragStart', onSelectionDragStart, store.setState);
useDirectStoreUpdater('onSelectionDragStop', onSelectionDragStop, store.setState);
useDirectStoreUpdater('onMove', onMove, store.setState);
useDirectStoreUpdater('onMoveStart', onMoveStart, store.setState);
useDirectStoreUpdater('onMoveEnd', onMoveEnd, store.setState);
useDirectStoreUpdater('noPanClassName', noPanClassName, store.setState);
useDirectStoreUpdater('nodeOrigin', nodeOrigin, store.setState);
useDirectStoreUpdater('rfId', rfId, store.setState);
useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState);
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
useDirectStoreUpdater('onError', onError || devWarn, store.setState);
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState);
useDirectStoreUpdater('nodeDragThreshold', nodeDragThreshold, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes); useEffect(
useStoreUpdater<Edge[]>(edges, setEdges); () => {
useStoreUpdater<number>(minZoom, setMinZoom); for (const fieldName of fieldsToTrack) {
useStoreUpdater<number>(maxZoom, setMaxZoom); const fieldValue = props[fieldName];
useStoreUpdater<CoordinateExtent>(translateExtent, setTranslateExtent); const previousFieldValue = previousFields.current[fieldName];
useStoreUpdater<CoordinateExtent>(nodeExtent, setNodeExtent);
if (fieldValue === previousFieldValue) continue;
if (typeof props[fieldName] === 'undefined') continue;
// Custom handling with dedicated setters for some fields
if (fieldName === 'nodes') setNodes(fieldValue as Node[]);
else if (fieldName === 'edges') setEdges(fieldValue as Edge[]);
else if (fieldName === 'minZoom') setMinZoom(fieldValue as number);
else if (fieldName === 'maxZoom') setMaxZoom(fieldValue as number);
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
// Renamed fields
else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue as boolean });
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue as FitViewOptions });
// General case
else store.setState({ [fieldName]: fieldValue });
}
previousFields.current = props;
},
// Only re-run the effect if one of the fields we track changes
fieldsToTrack.map((fieldName) => props[fieldName])
);
return null; return null;
}; };
@@ -0,0 +1,19 @@
import type { ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__viewport-portal');
function ViewportPortal({ children }: { children: ReactNode }) {
const viewPortalDiv = useStore(selector);
if (!viewPortalDiv) {
return null;
}
return createPortal(children, viewPortalDiv);
}
export default ViewportPortal;
@@ -51,32 +51,38 @@ const markerSelector =
return markers; return markers;
}; };
const markersEqual = (a: MarkerProps[], b: MarkerProps[]) =>
// the id includes all marker options, so we just need to look at that part of the marker
!(a.length !== b.length || a.some((m, i) => m.id !== b[i].id));
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore // when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper // when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
// that we can then use for creating our unique marker ids // that we can then use for creating our unique marker ids
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => { const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
const markers = useStore( const markers = useStore(useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]), markersEqual);
useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]),
// the id includes all marker options, so we just need to look at that part of the marker if (!markers.length) {
(a, b) => !(a.length !== b.length || a.some((m, i) => m.id !== b[i].id)) return null;
); }
return ( return (
<defs> <svg className="react-flow__marker">
{markers.map((marker: MarkerProps) => ( <defs>
<Marker {markers.map((marker: MarkerProps) => (
id={marker.id} <Marker
key={marker.id} id={marker.id}
type={marker.type} key={marker.id}
color={marker.color} type={marker.type}
width={marker.width} color={marker.color}
height={marker.height} width={marker.width}
markerUnits={marker.markerUnits} height={marker.height}
strokeWidth={marker.strokeWidth} markerUnits={marker.markerUnits}
orient={marker.orient} strokeWidth={marker.strokeWidth}
/> orient={marker.orient}
))} />
</defs> ))}
</defs>
</svg>
); );
}; };
@@ -1,13 +1,12 @@
import { memo, ReactNode } from 'react'; import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import { errorMessages } from '@xyflow/system';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges'; import useVisibleEdgeIds from '../../hooks/useVisibleEdgeIds';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import type { EdgeTypesWrapped, ReactFlowState } from '../../types'; import EdgeWrapper from '../../components/EdgeWrapper';
import type { ReactFlowState } from '../../types';
type EdgeRendererProps = Pick< type EdgeRendererProps = Pick<
GraphViewProps, GraphViewProps,
@@ -24,12 +23,10 @@ type EdgeRendererProps = Pick<
| 'onEdgeUpdateEnd' | 'onEdgeUpdateEnd'
| 'edgeUpdaterRadius' | 'edgeUpdaterRadius'
| 'noPanClassName' | 'noPanClassName'
| 'elevateEdgesOnSelect'
| 'rfId' | 'rfId'
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
| 'edgeTypes'
> & { > & {
edgeTypes: EdgeTypesWrapped;
elevateEdgesOnSelect: boolean;
children: ReactNode; children: ReactNode;
}; };
@@ -46,7 +43,6 @@ const selector = (s: ReactFlowState) => ({
const EdgeRenderer = ({ const EdgeRenderer = ({
defaultMarkerColor, defaultMarkerColor,
onlyRenderVisibleElements, onlyRenderVisibleElements,
elevateEdgesOnSelect,
rfId, rfId,
edgeTypes, edgeTypes,
noPanClassName, noPanClassName,
@@ -63,82 +59,39 @@ const EdgeRenderer = ({
children, children,
}: EdgeRendererProps) => { }: EdgeRendererProps) => {
const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
// we are grouping edges by zIndex here in order to be able to render them in the correct order const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements);
// each zIndex gets its own svg element
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
return ( return (
<> <div className="react-flow__edges">
{edgeTree.map(({ level, edges, isMaxLevel }) => ( <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />
<svg key={level} style={{ zIndex: level }} className="react-flow__edges react-flow__container">
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<>
{edges.map((edge) => {
let edgeType = edge.type || 'default';
if (!edgeTypes[edgeType]) { {edgeIds.map((id) => {
onError?.('011', errorMessages['error011'](edgeType)); return (
edgeType = 'default'; <EdgeWrapper
} key={id}
id={id}
const EdgeComponent = edgeTypes[edgeType]; edgesFocusable={edgesFocusable}
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); edgesUpdatable={edgesUpdatable}
const isUpdatable = elementsSelectable={elementsSelectable}
typeof onEdgeUpdate !== 'undefined' && noPanClassName={noPanClassName}
(edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined')); onEdgeUpdate={onEdgeUpdate}
const isSelectable = !!( onContextMenu={onEdgeContextMenu}
edge.selectable || onMouseEnter={onEdgeMouseEnter}
(elementsSelectable && typeof edge.selectable === 'undefined') onMouseMove={onEdgeMouseMove}
); onMouseLeave={onEdgeMouseLeave}
onClick={onEdgeClick}
return ( edgeUpdaterRadius={edgeUpdaterRadius}
<EdgeComponent onDoubleClick={onEdgeDoubleClick}
key={edge.id} onEdgeUpdateStart={onEdgeUpdateStart}
id={edge.id} onEdgeUpdateEnd={onEdgeUpdateEnd}
className={cc([edge.className, noPanClassName])} rfId={rfId}
type={edge.type} onError={onError}
data={edge.data} edgeTypes={edgeTypes}
selected={!!edge.selected} />
animated={!!edge.animated} );
hidden={!!edge.hidden} })}
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={edge.sourceHandle}
targetHandleId={edge.targetHandle}
markerEnd={edge.markerEnd}
markerStart={edge.markerStart}
isSelectable={isSelectable}
onEdgeUpdate={onEdgeUpdate}
onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
onClick={onEdgeClick}
edgeUpdaterRadius={edgeUpdaterRadius}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
rfId={rfId}
ariaLabel={edge.ariaLabel}
isFocusable={isFocusable}
isUpdatable={isUpdatable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
interactionWidth={edge.interactionWidth}
/>
);
})}
</>
</svg>
))}
{children} {children}
</> </div>
); );
}; };
@@ -1,37 +0,0 @@
import type { ComponentType } from 'react';
import {
BezierEdgeInternal,
SmoothStepEdgeInternal,
StepEdgeInternal,
StraightEdgeInternal,
SimpleBezierEdgeInternal,
} from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge';
import type { EdgeProps, EdgeTypes, EdgeTypesWrapped } from '../../types';
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypesWrapped;
export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypesWrapped {
const standardTypes: EdgeTypesWrapped = {
default: wrapEdge((edgeTypes.default || BezierEdgeInternal) as ComponentType<EdgeProps>),
straight: wrapEdge((edgeTypes.bezier || StraightEdgeInternal) as ComponentType<EdgeProps>),
step: wrapEdge((edgeTypes.step || StepEdgeInternal) as ComponentType<EdgeProps>),
smoothstep: wrapEdge((edgeTypes.step || SmoothStepEdgeInternal) as ComponentType<EdgeProps>),
simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdgeInternal) as ComponentType<EdgeProps>),
};
const wrappedTypes = {} as EdgeTypesWrapped;
const specialTypes: EdgeTypesWrapped = Object.keys(edgeTypes)
.filter((k) => !['default', 'bezier'].includes(k))
.reduce((res, key) => {
res[key] = wrapEdge((edgeTypes[key] || BezierEdgeInternal) as ComponentType<EdgeProps>);
return res;
}, wrappedTypes);
return {
...standardTypes,
...specialTypes,
};
}
@@ -8,19 +8,15 @@ import useOnInitHandler from '../../hooks/useOnInitHandler';
import useViewportSync from '../../hooks/useViewportSync'; import useViewportSync from '../../hooks/useViewportSync';
import ConnectionLine from '../../components/ConnectionLine'; import ConnectionLine from '../../components/ConnectionLine';
import type { ReactFlowProps } from '../../types'; import type { ReactFlowProps } from '../../types';
import { createNodeTypes } from '../NodeRenderer/utils'; import useNodeOrEdgeTypesWarning from './useNodeOrEdgeTypesWarning';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import { useNodeOrEdgeTypes } from './utils';
export type GraphViewProps = Omit< export type GraphViewProps = Omit<
ReactFlowProps, ReactFlowProps,
'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes' | 'onMove' | 'onMoveStart' | 'onMoveEnd' 'onSelectionChange' | 'nodes' | 'edges' | 'onMove' | 'onMoveStart' | 'onMoveEnd' | 'elevateEdgesOnSelect'
> & > &
Required< Required<
Pick< Pick<
ReactFlowProps, ReactFlowProps,
| 'nodeTypes'
| 'edgeTypes'
| 'selectionKeyCode' | 'selectionKeyCode'
| 'deleteKeyCode' | 'deleteKeyCode'
| 'multiSelectionKeyCode' | 'multiSelectionKeyCode'
@@ -100,7 +96,6 @@ const GraphView = ({
noDragClassName, noDragClassName,
noWheelClassName, noWheelClassName,
noPanClassName, noPanClassName,
elevateEdgesOnSelect,
disableKeyboardA11y, disableKeyboardA11y,
nodeOrigin, nodeOrigin,
nodeExtent, nodeExtent,
@@ -108,8 +103,8 @@ const GraphView = ({
viewport, viewport,
onViewportChange, onViewportChange,
}: GraphViewProps) => { }: GraphViewProps) => {
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes); useNodeOrEdgeTypesWarning(nodeTypes);
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes); useNodeOrEdgeTypesWarning(edgeTypes);
useOnInitHandler(onInit); useOnInitHandler(onInit);
useViewportSync(viewport); useViewportSync(viewport);
@@ -154,7 +149,7 @@ const GraphView = ({
> >
<ViewportWrapper> <ViewportWrapper>
<EdgeRenderer <EdgeRenderer
edgeTypes={edgeTypesWrapped} edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick} onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
@@ -168,7 +163,6 @@ const GraphView = ({
edgeUpdaterRadius={edgeUpdaterRadius} edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor} defaultMarkerColor={defaultMarkerColor}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
rfId={rfId} rfId={rfId}
> >
@@ -180,9 +174,9 @@ const GraphView = ({
/> />
</EdgeRenderer> </EdgeRenderer>
<div className="react-flow__edgelabel-renderer" /> <div className="react-flow__edgelabel-renderer" />
<div className="react-flow__viewport-portal" />
<NodeRenderer <NodeRenderer
nodeTypes={nodeTypesWrapped} nodeTypes={nodeTypes}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick} onNodeDoubleClick={onNodeDoubleClick}
onNodeMouseEnter={onNodeMouseEnter} onNodeMouseEnter={onNodeMouseEnter}
@@ -0,0 +1,27 @@
import { useEffect, useRef } from 'react';
import { errorMessages } from '@xyflow/system';
import type { EdgeTypes, NodeTypes } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
const emptyTypes = {};
/*
* This hook warns the user if node or edgeTypes change.
*/
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: NodeTypes): void;
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: EdgeTypes): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes: any = emptyTypes): any {
const updateCount = useRef(0);
const store = useStoreApi();
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
if (updateCount.current > 1) {
store.getState().onError?.('002', errorMessages['error002']());
}
updateCount.current += 1;
}
}, [nodeOrEdgeTypes]);
}
@@ -1,31 +0,0 @@
import { useMemo, useRef } from 'react';
import { shallow } from 'zustand/shallow';
import { errorMessages } from '@xyflow/system';
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils';
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
const typesKeysRef = useRef<string[] | null>(null);
const store = useStoreApi();
const typesParsed = useMemo(() => {
if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) {
store.getState().onError?.('002', errorMessages['error002']());
}
typesKeysRef.current = typeKeys;
}
return createTypes(nodeOrEdgeTypes);
}, [nodeOrEdgeTypes]);
return typesParsed;
}
@@ -1,14 +1,15 @@
import { memo, useMemo, useEffect, useRef, type ComponentType } from 'react'; import { memo } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system';
import useVisibleNodes from '../../hooks/useVisibleNodes'; import useVisibleNodesIds from '../../hooks/useVisibleNodeIds';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { containerStyle } from '../../styles/utils'; import { containerStyle } from '../../styles/utils';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import type { NodeTypesWrapped, ReactFlowState, WrapNodeProps } from '../../types'; import type { ReactFlowState } from '../../types';
import useResizeObserver from './useResizeObserver';
import NodeWrapper from '../../components/NodeWrapper';
type NodeRendererProps = Pick< export type NodeRendererProps = Pick<
GraphViewProps, GraphViewProps,
| 'onNodeClick' | 'onNodeClick'
| 'onNodeDoubleClick' | 'onNodeDoubleClick'
@@ -23,127 +24,71 @@ type NodeRendererProps = Pick<
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
| 'nodeOrigin' | 'nodeOrigin'
| 'nodeExtent' | 'nodeExtent'
> & { | 'nodeTypes'
nodeTypes: NodeTypesWrapped; >;
};
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
nodesDraggable: s.nodesDraggable, nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable, nodesConnectable: s.nodesConnectable,
nodesFocusable: s.nodesFocusable, nodesFocusable: s.nodesFocusable,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
onError: s.onError, onError: s.onError,
}); });
const NodeRenderer = (props: NodeRendererProps) => { const NodeRenderer = (props: NodeRendererProps) => {
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } = const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow);
useStore(selector, shallow); const nodeIds = useVisibleNodesIds(props.onlyRenderVisibleElements);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const resizeObserver = useResizeObserver();
const resizeObserverRef = useRef<ResizeObserver>();
const resizeObserver = useMemo(() => {
if (typeof ResizeObserver === 'undefined') {
return null;
}
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = new Map();
entries.forEach((entry: ResizeObserverEntry) => {
const id = entry.target.getAttribute('data-id') as string;
updates.set(id, {
id,
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
});
});
updateNodeDimensions(updates);
});
resizeObserverRef.current = observer;
return observer;
}, []);
useEffect(() => {
return () => {
resizeObserverRef?.current?.disconnect();
};
}, []);
return ( return (
<div className="react-flow__nodes" style={containerStyle}> <div className="react-flow__nodes" style={containerStyle}>
{nodes.map((node) => { {nodeIds.map((nodeId) => {
let nodeType = node.type || 'default';
if (!props.nodeTypes[nodeType]) {
onError?.('003', errorMessages['error003'](nodeType));
nodeType = '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 isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
const clampedPosition = props.nodeExtent
? clampPosition(node.computed?.positionAbsolute, props.nodeExtent)
: node.computed?.positionAbsolute;
const posX = clampedPosition?.x ?? 0;
const posY = clampedPosition?.y ?? 0;
const posOrigin = getPositionWithOrigin({
x: posX,
y: posY,
width: node.computed?.width ?? node.width ?? 0,
height: node.computed?.height ?? node.height ?? 0,
origin: node.origin || props.nodeOrigin,
});
const initialized = (!!node.computed?.width && !!node.computed?.height) || (!!node.width && !!node.height);
return ( return (
<NodeComponent // The split of responsibilities between NodeRenderer and
key={node.id} // NodeComponentWrapper may appear weird. However, its designed to
id={node.id} // minimize the cost of updates when individual nodes change.
className={node.className} //
style={node.style} // For example, when youre dragging a single node, that node gets
width={node.width ?? undefined} // updated multiple times per second. If `NodeRenderer` were to update
height={node.height ?? undefined} // every time, it would have to re-run the `nodes.map()` loop every
type={nodeType} // time. This gets pricey with hundreds of nodes, especially if every
data={node.data} // loop cycle does more than just rendering a JSX element!
sourcePosition={node.sourcePosition || Position.Bottom} //
targetPosition={node.targetPosition || Position.Top} // As a result of this choice, we took the following implementation
hidden={node.hidden} // decisions:
xPos={posX} // - NodeRenderer subscribes *only* to node IDs and therefore
yPos={posY} // rerender *only* when visible nodes are added or removed.
xPosOrigin={posOrigin.x} // - NodeRenderer performs all operations the result of which can be
yPosOrigin={posOrigin.y} // shared between nodes (such as creating the `ResizeObserver`
positionAbsolute={clampedPosition || { x: 0, y: 0 }} // instance, or subscribing to `selector`). This means extra prop
// drilling into `NodeComponentWrapper`, but it means we need to run
// these operations only once instead of once per node.
// - Any operations that youd normally write inside `nodes.map` are
// moved into `NodeComponentWrapper`. This ensures they are
// memorized so if `NodeRenderer` *has* to rerender, it only
// needs to regenerate the list of nodes, nothing else.
<NodeWrapper
key={nodeId}
id={nodeId}
nodeTypes={props.nodeTypes}
nodeExtent={props.nodeExtent}
nodeOrigin={props.nodeOrigin}
onClick={props.onNodeClick} onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter} onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove} onMouseMove={props.onNodeMouseMove}
onMouseLeave={props.onNodeMouseLeave} onMouseLeave={props.onNodeMouseLeave}
onContextMenu={props.onNodeContextMenu} onContextMenu={props.onNodeContextMenu}
onDoubleClick={props.onNodeDoubleClick} onDoubleClick={props.onNodeDoubleClick}
selected={!!node.selected}
isDraggable={isDraggable}
isSelectable={isSelectable}
isConnectable={isConnectable}
isFocusable={isFocusable}
resizeObserver={resizeObserver}
dragHandle={node.dragHandle}
zIndex={node[internalsSymbol]?.z ?? 0}
isParent={!!node[internalsSymbol]?.isParent}
noDragClassName={props.noDragClassName} noDragClassName={props.noDragClassName}
noPanClassName={props.noPanClassName} noPanClassName={props.noPanClassName}
initialized={initialized}
rfId={props.rfId} rfId={props.rfId}
disableKeyboardA11y={props.disableKeyboardA11y} disableKeyboardA11y={props.disableKeyboardA11y}
ariaLabel={node.ariaLabel} resizeObserver={resizeObserver}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
nodesFocusable={nodesFocusable}
elementsSelectable={elementsSelectable}
onError={onError}
/> />
); );
})} })}
@@ -0,0 +1,44 @@
import { useEffect, useMemo, useRef } from 'react';
import { ReactFlowState } from '../../types';
import { useStore } from '../../hooks/useStore';
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
export default function useResizeObserver() {
const updateNodeDimensions = useStore(selector);
const resizeObserverRef = useRef<ResizeObserver>();
const resizeObserver = useMemo(() => {
if (typeof ResizeObserver === 'undefined') {
return null;
}
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = new Map();
entries.forEach((entry: ResizeObserverEntry) => {
const id = entry.target.getAttribute('data-id') as string;
updates.set(id, {
id,
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
});
});
updateNodeDimensions(updates);
});
resizeObserverRef.current = observer;
return observer;
}, []);
useEffect(() => {
return () => {
resizeObserverRef?.current?.disconnect();
};
}, []);
return resizeObserver;
}
@@ -1,34 +0,0 @@
import type { ComponentType } from 'react';
import type { NodeProps } from '@xyflow/system';
import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode';
import wrapNode from '../../components/Nodes/wrapNode';
import type { NodeTypes, NodeTypesWrapped } from '../../types';
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
export function createNodeTypes(nodeTypes: NodeTypes): NodeTypesWrapped {
const standardTypes: NodeTypesWrapped = {
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeProps>),
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeProps>),
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeProps>),
group: wrapNode((nodeTypes.group || GroupNode) as ComponentType<NodeProps>),
};
const wrappedTypes = {} as NodeTypesWrapped;
const specialTypes: NodeTypesWrapped = Object.keys(nodeTypes)
.filter((k) => !['input', 'default', 'output', 'group'].includes(k))
.reduce((res, key) => {
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>);
return res;
}, wrappedTypes);
return {
...standardTypes,
...specialTypes,
};
}
+19 -8
View File
@@ -10,7 +10,7 @@ import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system'
import UserSelection from '../../components/UserSelection'; import UserSelection from '../../components/UserSelection';
import { containerStyle } from '../../styles/utils'; import { containerStyle } from '../../styles/utils';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { getSelectionChanges, getConnectedEdges } from '../../utils'; import { getSelectionChanges } from '../../utils';
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types'; import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
type PaneProps = { type PaneProps = {
@@ -156,19 +156,30 @@ const Pane = memo(
true, true,
nodeOrigin nodeOrigin
); );
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
if (prevSelectedNodesCount.current !== selectedNodeIds.length) { const selectedEdgeIds = new Set<string>();
prevSelectedNodesCount.current = selectedNodeIds.length; const selectedNodeIds = new Set<string>();
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
for (const selectedNode of selectedNodes) {
selectedNodeIds.add(selectedNode.id);
for (const edge of edges) {
if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
selectedEdgeIds.add(edge.id);
}
}
}
if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
prevSelectedNodesCount.current = selectedNodeIds.size;
const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[];
if (changes.length) { if (changes.length) {
onNodesChange?.(changes); onNodesChange?.(changes);
} }
} }
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) { if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
prevSelectedEdgesCount.current = selectedEdgeIds.length; prevSelectedEdgesCount.current = selectedEdgeIds.size;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) { if (changes.length) {
onEdgesChange?.(changes); onEdgesChange?.(changes);
@@ -2,7 +2,6 @@ import { forwardRef, type CSSProperties } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { import {
ConnectionLineType, ConnectionLineType,
ConnectionMode,
PanOnScrollMode, PanOnScrollMode,
SelectionMode, SelectionMode,
infiniteExtent, infiniteExtent,
@@ -12,42 +11,16 @@ import {
} from '@xyflow/system'; } from '@xyflow/system';
import Attribution from '../../components/Attribution'; import Attribution from '../../components/Attribution';
import {
BezierEdgeInternal,
SmoothStepEdgeInternal,
StepEdgeInternal,
StraightEdgeInternal,
SimpleBezierEdgeInternal,
} from '../../components/Edges';
import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode';
import SelectionListener from '../../components/SelectionListener'; import SelectionListener from '../../components/SelectionListener';
import StoreUpdater from '../../components/StoreUpdater'; import StoreUpdater from '../../components/StoreUpdater';
import A11yDescriptions from '../../components/A11yDescriptions'; import A11yDescriptions from '../../components/A11yDescriptions';
import GraphView from '../GraphView'; import GraphView from '../GraphView';
import Wrapper from './Wrapper'; import Wrapper from './Wrapper';
import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types'; import type { ReactFlowProps, ReactFlowRefType } from '../../types';
import useColorModeClass from '../../hooks/useColorModeClass'; import useColorModeClass from '../../hooks/useColorModeClass';
const defaultNodeTypes: NodeTypes = { export const initNodeOrigin: NodeOrigin = [0, 0];
input: InputNode,
default: DefaultNode,
output: OutputNode,
group: GroupNode,
};
const defaultEdgeTypes: EdgeTypes = {
default: BezierEdgeInternal,
straight: StraightEdgeInternal,
step: StepEdgeInternal,
smoothstep: SmoothStepEdgeInternal,
simplebezier: SimpleBezierEdgeInternal,
};
const initNodeOrigin: NodeOrigin = [0, 0];
const initSnapGrid: [number, number] = [15, 15];
const initDefaultViewport: Viewport = { x: 0, y: 0, zoom: 1 }; const initDefaultViewport: Viewport = { x: 0, y: 0, zoom: 1 };
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
@@ -66,8 +39,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
defaultNodes, defaultNodes,
defaultEdges, defaultEdges,
className, className,
nodeTypes = defaultNodeTypes, nodeTypes,
edgeTypes = defaultEdgeTypes, edgeTypes,
onNodeClick, onNodeClick,
onEdgeClick, onEdgeClick,
onInit, onInit,
@@ -97,7 +70,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onSelectionContextMenu, onSelectionContextMenu,
onSelectionStart, onSelectionStart,
onSelectionEnd, onSelectionEnd,
connectionMode = ConnectionMode.Strict, connectionMode,
connectionLineType = ConnectionLineType.Bezier, connectionLineType = ConnectionLineType.Bezier,
connectionLineStyle, connectionLineStyle,
connectionLineComponent, connectionLineComponent,
@@ -109,10 +82,10 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
panActivationKeyCode = 'Space', panActivationKeyCode = 'Space',
multiSelectionKeyCode = isMacOs() ? 'Meta' : 'Control', multiSelectionKeyCode = isMacOs() ? 'Meta' : 'Control',
zoomActivationKeyCode = isMacOs() ? 'Meta' : 'Control', zoomActivationKeyCode = isMacOs() ? 'Meta' : 'Control',
snapToGrid = false, snapToGrid,
snapGrid = initSnapGrid, snapGrid,
onlyRenderVisibleElements = false, onlyRenderVisibleElements = false,
selectNodesOnDrag = true, selectNodesOnDrag,
nodesDraggable, nodesDraggable,
nodesConnectable, nodesConnectable,
nodesFocusable, nodesFocusable,
@@ -155,18 +128,18 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
noDragClassName = 'nodrag', noDragClassName = 'nodrag',
noWheelClassName = 'nowheel', noWheelClassName = 'nowheel',
noPanClassName = 'nopan', noPanClassName = 'nopan',
fitView = false, fitView,
fitViewOptions, fitViewOptions,
connectOnClick = true, connectOnClick,
attributionPosition, attributionPosition,
proOptions, proOptions,
defaultEdgeOptions, defaultEdgeOptions,
elevateNodesOnSelect = true, elevateNodesOnSelect,
elevateEdgesOnSelect = false, elevateEdgesOnSelect,
disableKeyboardA11y = false, disableKeyboardA11y = false,
autoPanOnConnect = true, autoPanOnConnect,
autoPanOnNodeDrag = true, autoPanOnNodeDrag,
connectionRadius = 20, connectionRadius,
isValidConnection, isValidConnection,
onError, onError,
style, style,
@@ -251,7 +224,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
noDragClassName={noDragClassName} noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName} noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
elevateEdgesOnSelect={elevateEdgesOnSelect}
rfId={rfId} rfId={rfId}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
nodeOrigin={nodeOrigin} nodeOrigin={nodeOrigin}
@@ -276,6 +248,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
edgesUpdatable={edgesUpdatable} edgesUpdatable={edgesUpdatable}
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
elevateNodesOnSelect={elevateNodesOnSelect} elevateNodesOnSelect={elevateNodesOnSelect}
elevateEdgesOnSelect={elevateEdgesOnSelect}
minZoom={minZoom} minZoom={minZoom}
maxZoom={maxZoom} maxZoom={maxZoom}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
@@ -9,6 +9,12 @@ function getMediaQuery() {
return window.matchMedia('(prefers-color-scheme: dark)'); return window.matchMedia('(prefers-color-scheme: dark)');
} }
/**
* Hook for receiving the current color mode class 'dark' or 'light'.
*
* @internal
* @param colorMode - The color mode to use ('dark', 'light' or 'system')
*/
export default function useColorModeClass(colorMode: ColorMode): ColorModeClass { export default function useColorModeClass(colorMode: ColorMode): ColorModeClass {
const [colorModeClass, setColorModeClass] = useState<ColorModeClass | null>( const [colorModeClass, setColorModeClass] = useState<ColorModeClass | null>(
colorMode === 'system' ? null : colorMode colorMode === 'system' ? null : colorMode
+5
View File
@@ -13,6 +13,11 @@ type UseDragParams = {
isSelectable?: boolean; isSelectable?: boolean;
}; };
/**
* Hook for calling XYDrag helper from @xyflow/system.
*
* @internal
*/
function useDrag({ nodeRef, disabled = false, noDragClassName, handleSelector, nodeId, isSelectable }: UseDragParams) { function useDrag({ nodeRef, disabled = false, noDragClassName, handleSelector, nodeId, isSelectable }: UseDragParams) {
const store = useStoreApi(); const store = useStoreApi();
const [dragging, setDragging] = useState<boolean>(false); const [dragging, setDragging] = useState<boolean>(false);
+6
View File
@@ -5,6 +5,12 @@ import type { Edge, ReactFlowState } from '../types';
const edgesSelector = (state: ReactFlowState) => state.edges; const edgesSelector = (state: ReactFlowState) => state.edges;
/**
* Hook for getting the current edges from the store.
*
* @public
* @returns An array of edges
*/
function useEdges<EdgeData>(): Edge<EdgeData>[] { function useEdges<EdgeData>(): Edge<EdgeData>[] {
const edges = useStore(edgesSelector, shallow); const edges = useStore(edgesSelector, shallow);
@@ -10,6 +10,11 @@ const selected = (item: Node | Edge) => item.selected;
const deleteKeyOptions: UseKeyPressOptions = { actInsideInputWithModifier: false }; const deleteKeyOptions: UseKeyPressOptions = { actInsideInputWithModifier: false };
/**
* Hook for handling global key events.
*
* @internal
*/
export default ({ export default ({
deleteKeyCode, deleteKeyCode,
multiSelectionKeyCode, multiSelectionKeyCode,
@@ -17,8 +17,8 @@ type useHandleConnectionsParams = {
* *
* @public * @public
* @param param.type - handle type 'source' or 'target' * @param param.type - handle type 'source' or 'target'
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used * @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @param param.onConnect - gets called when a connection is established * @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed * @param param.onDisconnect - gets called when a connection is removed
* @returns an array with connections * @returns an array with connections
+12 -4
View File
@@ -12,11 +12,19 @@ export type UseKeyPressOptions = {
const defaultDoc = typeof document !== 'undefined' ? document : null; const defaultDoc = 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' * Hook for handling key events.
// an array means different possibilites. Explainer: ['a', 'd+s'] here the *
// user can use the single key 'a' or the combination 'd' + 's' * @public
* @param param.keyCode - The key code (string or array of strings) to use
* @param param.options - Options
* @returns boolean
*/
export default ( export default (
// 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'
keyCode: KeyCode | null = null, keyCode: KeyCode | null = null,
options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true } options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true }
): boolean => { ): boolean => {
+8 -2
View File
@@ -5,8 +5,14 @@ import type { Node, ReactFlowState } from '../types';
const nodesSelector = (state: ReactFlowState) => state.nodes; const nodesSelector = (state: ReactFlowState) => state.nodes;
function useNodes<NodeData>(): Node<NodeData>[] { /**
const nodes = useStore(nodesSelector, shallow); * Hook for getting the current nodes from the store.
*
* @public
* @returns An array of nodes
*/
function useNodes<NodeType extends Node = Node>(): NodeType[] {
const nodes = useStore(nodesSelector, shallow) as NodeType[];
return nodes; return nodes;
} }
+8
View File
@@ -4,6 +4,14 @@ import { shallow } from 'zustand/shallow';
import { useStore } from '../hooks/useStore'; import { useStore } from '../hooks/useStore';
import type { Node } from '../types'; import type { Node } from '../types';
/**
* Hook for receiving data of one or multiple nodes
*
* @public
* @param nodeId - The id (or ids) of the node to get the data from
* @param guard - Optional guard function to narrow down the node type
* @returns An array od data objects
*/
export function useNodesData<NodeType extends Node = Node>(nodeId: string): NodeType['data'] | null; export function useNodesData<NodeType extends Node = Node>(nodeId: string): NodeType['data'] | null;
export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): NodeType['data'][]; export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): NodeType['data'][];
export function useNodesData<NodeType extends Node = Node>( export function useNodesData<NodeType extends Node = Node>(
+21 -8
View File
@@ -7,18 +7,16 @@ import type { Node, NodeChange, Edge, EdgeChange } from '../types';
type ApplyChanges<ItemType, ChangesType> = (changes: ChangesType[], items: ItemType[]) => ItemType[]; type ApplyChanges<ItemType, ChangesType> = (changes: ChangesType[], items: ItemType[]) => ItemType[];
type OnChange<ChangesType> = (changes: ChangesType[]) => void; type OnChange<ChangesType> = (changes: ChangesType[]) => void;
// returns a hook that can be used liked this:
// const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
function createUseItemsState( function createUseItemsState(
applyChanges: ApplyChanges<Node, NodeChange> applyChanges: ApplyChanges<Node, NodeChange>
): <NodeData = any>( ): <NodeType extends Node = Node>(
initialItems: Node<NodeData>[] initialItems: NodeType[]
) => [Node<NodeData>[], Dispatch<SetStateAction<Node<NodeData>[]>>, OnChange<NodeChange>]; ) => [NodeType[], Dispatch<SetStateAction<NodeType[]>>, OnChange<NodeChange>];
function createUseItemsState( function createUseItemsState(
applyChanges: ApplyChanges<Edge, EdgeChange> applyChanges: ApplyChanges<Edge, EdgeChange>
): <EdgeData = any>( ): <EdgeType extends Edge = Edge>(
initialItems: Edge<EdgeData>[] initialItems: EdgeType[]
) => [Edge<EdgeData>[], Dispatch<SetStateAction<Edge<EdgeData>[]>>, OnChange<EdgeChange>]; ) => [EdgeType[], Dispatch<SetStateAction<EdgeType[]>>, OnChange<EdgeChange>];
function createUseItemsState( function createUseItemsState(
applyChanges: ApplyChanges<any, any> applyChanges: ApplyChanges<any, any>
): (initialItems: any[]) => [any[], Dispatch<SetStateAction<any[]>>, OnChange<any>] { ): (initialItems: any[]) => [any[], Dispatch<SetStateAction<any[]>>, OnChange<any>] {
@@ -31,5 +29,20 @@ function createUseItemsState(
}; };
} }
/**
* Hook for managing the state of nodes - should only be used for prototyping / simple use cases.
*
* @public
* @param initialNodes
* @returns an array [nodes, setNodes, onNodesChange]
*/
export const useNodesState = createUseItemsState(applyNodeChanges); export const useNodesState = createUseItemsState(applyNodeChanges);
/**
* Hook for managing the state of edges - should only be used for prototyping / simple use cases.
*
* @public
* @param initialEdges
* @returns an array [edges, setEdges, onEdgesChange]
*/
export const useEdgesState = createUseItemsState(applyEdgeChanges); export const useEdgesState = createUseItemsState(applyEdgeChanges);
@@ -21,6 +21,13 @@ const defaultOptions = {
includeHiddenNodes: false, includeHiddenNodes: false,
}; };
/**
* Hook which returns true when all nodes are initialized.
*
* @public
* @param options.includeHiddenNodes - defaults to false
* @returns boolean indicating whether all nodes are initialized
*/
function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean { function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean {
const initialized = useStore(selector(options)); const initialized = useStore(selector(options));
@@ -3,6 +3,11 @@ import { useEffect, useRef } from 'react';
import useReactFlow from './useReactFlow'; import useReactFlow from './useReactFlow';
import type { OnInit } from '../types'; import type { OnInit } from '../types';
/**
* Hook for calling onInit handler.
*
* @internal
*/
function useOnInitHandler(onInit: OnInit | undefined) { function useOnInitHandler(onInit: OnInit | undefined) {
const rfInstance = useReactFlow(); const rfInstance = useReactFlow();
const isInitialized = useRef<boolean>(false); const isInitialized = useRef<boolean>(false);
@@ -7,6 +7,12 @@ export type UseOnSelectionChangeOptions = {
onChange: OnSelectionChangeFunc; onChange: OnSelectionChangeFunc;
}; };
/**
* Hook for registering an onSelectionChange handler.
*
* @public
* @params params.onChange - The handler to register
*/
function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) { function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
const store = useStoreApi(); const store = useStoreApi();
@@ -9,6 +9,14 @@ export type UseOnViewportChangeOptions = {
onEnd?: OnViewportChange; onEnd?: OnViewportChange;
}; };
/**
* Hook for registering an onViewportChange handler.
*
* @public
* @param params.onStart - gets called when the viewport starts changing
* @param params.onChange - gets called when the viewport changes
* @param params.onEnd - gets called when the viewport stops changing
*/
function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) { function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) {
const store = useStoreApi(); const store = useStoreApi();
+43 -84
View File
@@ -1,13 +1,5 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
getElementsToRemove,
getIncomersBase,
getOutgoersBase,
getOverlappingArea,
isRectObject,
nodeToRect,
type Rect,
} from '@xyflow/system';
import useViewportHelper from './useViewportHelper'; import useViewportHelper from './useViewportHelper';
import { useStoreApi } from './useStore'; import { useStoreApi } from './useStore';
@@ -26,32 +18,40 @@ import type {
} from '../types'; } from '../types';
import { isNode } from '../utils'; import { isNode } from '../utils';
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ /**
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData> { * Hook for accessing the ReactFlow instance.
*
* @public
* @returns ReactFlowInstance
*/
export default function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): ReactFlowInstance<
NodeType,
EdgeType
> {
const viewportHelper = useViewportHelper(); const viewportHelper = useViewportHelper();
const store = useStoreApi(); const store = useStoreApi();
const getNodes = useCallback<Instance.GetNodes<NodeData>>(() => { const getNodes = useCallback<Instance.GetNodes<NodeType>>(() => {
return store.getState().nodes.map((n) => ({ ...n })); return store.getState().nodes.map((n) => ({ ...n })) as NodeType[];
}, []); }, []);
const getNode = useCallback<Instance.GetNode<NodeData>>((id) => { const getNode = useCallback<Instance.GetNode<NodeType>>((id) => {
return store.getState().nodeLookup.get(id); return store.getState().nodeLookup.get(id) as NodeType;
}, []); }, []);
const getEdges = useCallback<Instance.GetEdges<EdgeData>>(() => { const getEdges = useCallback<Instance.GetEdges<EdgeType>>(() => {
const { edges = [] } = store.getState(); const { edges = [] } = store.getState();
return edges.map((e) => ({ ...e })); return edges.map((e) => ({ ...e })) as EdgeType[];
}, []); }, []);
const getEdge = useCallback<Instance.GetEdge<EdgeData>>((id) => { const getEdge = useCallback<Instance.GetEdge<EdgeType>>((id) => {
const { edges = [] } = store.getState(); const { edges = [] } = store.getState();
return edges.find((e) => e.id === id); return edges.find((e) => e.id === id) as EdgeType;
}, []); }, []);
const setNodes = useCallback<Instance.SetNodes<NodeData>>((payload) => { const setNodes = useCallback<Instance.SetNodes<NodeType>>((payload) => {
const { nodes, setNodes, hasDefaultNodes, onNodesChange } = store.getState(); const { nodes, setNodes, hasDefaultNodes, onNodesChange } = store.getState();
const nextNodes = typeof payload === 'function' ? payload(nodes) : payload; const nextNodes = typeof payload === 'function' ? payload(nodes as NodeType[]) : payload;
if (hasDefaultNodes) { if (hasDefaultNodes) {
setNodes(nextNodes); setNodes(nextNodes);
@@ -59,14 +59,14 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const changes = const changes =
nextNodes.length === 0 nextNodes.length === 0
? nodes.map((node) => ({ type: 'remove', id: node.id } as NodeRemoveChange)) ? nodes.map((node) => ({ type: 'remove', id: node.id } as NodeRemoveChange))
: nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange<NodeData>)); : nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange<NodeType>));
onNodesChange(changes); onNodesChange(changes);
} }
}, []); }, []);
const setEdges = useCallback<Instance.SetEdges<EdgeData>>((payload) => { const setEdges = useCallback<Instance.SetEdges<EdgeType>>((payload) => {
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState(); const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
const nextEdges = typeof payload === 'function' ? payload(edges) : payload; const nextEdges = typeof payload === 'function' ? payload(edges as EdgeType[]) : payload;
if (hasDefaultEdges) { if (hasDefaultEdges) {
setEdges(nextEdges); setEdges(nextEdges);
@@ -74,12 +74,12 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const changes = const changes =
nextEdges.length === 0 nextEdges.length === 0
? edges.map((edge) => ({ type: 'remove', id: edge.id } as EdgeRemoveChange)) ? edges.map((edge) => ({ type: 'remove', id: edge.id } as EdgeRemoveChange))
: nextEdges.map((edge) => ({ item: edge, type: 'reset' } as EdgeResetChange<EdgeData>)); : nextEdges.map((edge) => ({ item: edge, type: 'reset' } as EdgeResetChange<EdgeType>));
onEdgesChange(changes); onEdgesChange(changes);
} }
}, []); }, []);
const addNodes = useCallback<Instance.AddNodes<NodeData>>((payload) => { const addNodes = useCallback<Instance.AddNodes<NodeType>>((payload) => {
const nodes = Array.isArray(payload) ? payload : [payload]; const nodes = Array.isArray(payload) ? payload : [payload];
const { nodes: currentNodes, hasDefaultNodes, onNodesChange, setNodes } = store.getState(); const { nodes: currentNodes, hasDefaultNodes, onNodesChange, setNodes } = store.getState();
@@ -87,29 +87,29 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const nextNodes = [...currentNodes, ...nodes]; const nextNodes = [...currentNodes, ...nodes];
setNodes(nextNodes); setNodes(nextNodes);
} else if (onNodesChange) { } else if (onNodesChange) {
const changes = nodes.map((node) => ({ item: node, type: 'add' } as NodeAddChange<NodeData>)); const changes = nodes.map((node) => ({ item: node, type: 'add' } as NodeAddChange<NodeType>));
onNodesChange(changes); onNodesChange(changes);
} }
}, []); }, []);
const addEdges = useCallback<Instance.AddEdges<EdgeData>>((payload) => { const addEdges = useCallback<Instance.AddEdges<EdgeType>>((payload) => {
const nextEdges = Array.isArray(payload) ? payload : [payload]; const nextEdges = Array.isArray(payload) ? payload : [payload];
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState(); const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
if (hasDefaultEdges) { if (hasDefaultEdges) {
setEdges([...edges, ...nextEdges]); setEdges([...edges, ...nextEdges]);
} else if (onEdgesChange) { } else if (onEdgesChange) {
const changes = nextEdges.map((edge) => ({ item: edge, type: 'add' } as EdgeAddChange<EdgeData>)); const changes = nextEdges.map((edge) => ({ item: edge, type: 'add' } as EdgeAddChange<EdgeType>));
onEdgesChange(changes); onEdgesChange(changes);
} }
}, []); }, []);
const toObject = useCallback<Instance.ToObject<NodeData, EdgeData>>(() => { const toObject = useCallback<Instance.ToObject<NodeType, EdgeType>>(() => {
const { nodes = [], edges = [], transform } = store.getState(); const { nodes = [], edges = [], transform } = store.getState();
const [x, y, zoom] = transform; const [x, y, zoom] = transform;
return { return {
nodes: nodes.map((n) => ({ ...n })), nodes: nodes.map((n) => ({ ...n })) as NodeType[],
edges: edges.map((e) => ({ ...e })), edges: edges.map((e) => ({ ...e })) as EdgeType[],
viewport: { viewport: {
x, x,
y, y,
@@ -181,11 +181,9 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
}, []); }, []);
const getNodeRect = useCallback( const getNodeRect = useCallback(
( (nodeOrRect: NodeType | { id: Node['id'] } | Rect): [Rect | null, NodeType | null | undefined, boolean] => {
nodeOrRect: Node<NodeData> | { id: Node['id'] } | Rect
): [Rect | null, Node<NodeData> | null | undefined, boolean] => {
const isRect = isRectObject(nodeOrRect); const isRect = isRectObject(nodeOrRect);
const node = isRect ? null : store.getState().nodeLookup.get(nodeOrRect.id); const node = isRect ? null : (store.getState().nodeLookup.get(nodeOrRect.id) as NodeType);
if (!isRect && !node) { if (!isRect && !node) {
[null, null, isRect]; [null, null, isRect];
@@ -198,7 +196,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
[] []
); );
const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeData>>( const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeType>>(
(nodeOrRect, partially = true, nodes) => { (nodeOrRect, partially = true, nodes) => {
const [nodeRect, node, isRect] = getNodeRect(nodeOrRect); const [nodeRect, node, isRect] = getNodeRect(nodeOrRect);
@@ -216,12 +214,12 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
const partiallyVisible = partially && overlappingArea > 0; const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height; return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
}); }) as NodeType[];
}, },
[] []
); );
const isNodeIntersecting = useCallback<Instance.IsNodeIntersecting<NodeData>>( const isNodeIntersecting = useCallback<Instance.IsNodeIntersecting<NodeType>>(
(nodeOrRect, area, partially = true) => { (nodeOrRect, area, partially = true) => {
const [nodeRect] = getNodeRect(nodeOrRect); const [nodeRect] = getNodeRect(nodeOrRect);
@@ -237,48 +235,13 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
[] []
); );
const getConnectedEdges = useCallback<Instance.getConnectedEdges>((node) => { const updateNode = useCallback<Instance.UpdateNode<NodeType>>(
const { edges } = store.getState();
const nodeIds = new Set();
if (typeof node === 'string') {
nodeIds.add(node);
} else if (node.length >= 1) {
node.forEach((n) => {
nodeIds.add(n.id);
});
}
return edges.filter((edge) => nodeIds.has(edge.source) || nodeIds.has(edge.target));
}, []);
const getIncomers = useCallback<Instance.getIncomers>((node) => {
const { nodes, edges } = store.getState();
if (typeof node === 'string') {
return getIncomersBase({ id: node }, nodes, edges);
}
return getIncomersBase(node, nodes, edges);
}, []);
const getOutgoers = useCallback<Instance.getOutgoers>((node) => {
const { nodes, edges } = store.getState();
if (typeof node == 'string') {
return getOutgoersBase({ id: node }, nodes, edges);
}
return getOutgoersBase(node, nodes, edges);
}, []);
const updateNode = useCallback<Instance.UpdateNode>(
(id, nodeUpdate, options = { replace: true }) => { (id, nodeUpdate, options = { replace: true }) => {
setNodes((prevNodes) => setNodes((prevNodes) =>
prevNodes.map((node) => { prevNodes.map((node) => {
if (node.id === id) { if (node.id === id) {
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate; const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as NodeType) : nodeUpdate;
return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode }; return options.replace && isNode(nextNode) ? (nextNode as NodeType) : { ...node, ...nextNode };
} }
return node; return node;
@@ -288,7 +251,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
[setNodes] [setNodes]
); );
const updateNodeData = useCallback<Instance.UpdateNodeData>( const updateNodeData = useCallback<Instance.UpdateNodeData<NodeType>>(
(id, dataUpdate, options = { replace: false }) => { (id, dataUpdate, options = { replace: false }) => {
updateNode( updateNode(
id, id,
@@ -317,9 +280,6 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
deleteElements, deleteElements,
getIntersectingNodes, getIntersectingNodes,
isNodeIntersecting, isNodeIntersecting,
getConnectedEdges,
getIncomers,
getOutgoers,
updateNode, updateNode,
updateNodeData, updateNodeData,
}; };
@@ -337,8 +297,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
deleteElements, deleteElements,
getIntersectingNodes, getIntersectingNodes,
isNodeIntersecting, isNodeIntersecting,
getConnectedEdges, updateNode,
getIncomers, updateNodeData,
getOutgoers,
]); ]);
} }
@@ -3,6 +3,11 @@ import { errorMessages, getDimensions } from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore'; import { useStoreApi } from '../hooks/useStore';
/**
* Hook for handling resize events.
*
* @internal
*/
function useResizeHandler(domNode: MutableRefObject<HTMLDivElement | null>): void { function useResizeHandler(domNode: MutableRefObject<HTMLDivElement | null>): void {
const store = useStoreApi(); const store = useStoreApi();
+8
View File
@@ -10,6 +10,14 @@ const zustandErrorMessage = errorMessages['error001']();
type ExtractState = StoreApi<ReactFlowState> extends { getState: () => infer T } ? T : never; type ExtractState = StoreApi<ReactFlowState> extends { getState: () => infer T } ? T : never;
/**
* Hook for accessing the internal store. Should only be used in rare cases.
*
* @public
* @param selector
* @param equalityFn
* @returns The selected state slice
*/
function useStore<StateSlice = ExtractState>( function useStore<StateSlice = ExtractState>(
selector: (state: ReactFlowState) => StateSlice, selector: (state: ReactFlowState) => StateSlice,
equalityFn?: (a: StateSlice, b: StateSlice) => boolean equalityFn?: (a: StateSlice, b: StateSlice) => boolean
@@ -3,6 +3,12 @@ import type { UpdateNodeInternals, NodeDimensionUpdate } from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore'; import { useStoreApi } from '../hooks/useStore';
/**
* Hook for updating node internals.
*
* @public
* @returns function for updating node internals
*/
function useUpdateNodeInternals(): UpdateNodeInternals { function useUpdateNodeInternals(): UpdateNodeInternals {
const store = useStoreApi(); const store = useStoreApi();
@@ -7,6 +7,12 @@ import { useStoreApi } from '../hooks/useStore';
const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) => const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) =>
n.selected && (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')); n.selected && (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined'));
/**
* Hook for updating node positions.
*
* @internal
* @returns function for updating node positions
*/
function useUpdateNodePositions() { function useUpdateNodePositions() {
const store = useStoreApi(); const store = useStoreApi();
+6
View File
@@ -10,6 +10,12 @@ const viewportSelector = (state: ReactFlowState) => ({
zoom: state.transform[2], zoom: state.transform[2],
}); });
/**
* Hook for getting the current viewport from the store.
*
* @public
* @returns The current viewport
*/
function useViewport(): Viewport { function useViewport(): Viewport {
const viewport = useStore(viewportSelector, shallow); const viewport = useStore(viewportSelector, shallow);
@@ -12,6 +12,12 @@ import type { ViewportHelperFunctions, ReactFlowState } from '../types';
const selector = (s: ReactFlowState) => !!s.panZoom; const selector = (s: ReactFlowState) => !!s.panZoom;
/**
* Hook for getting viewport helper functions.
*
* @internal
* @returns viewport helper functions
*/
const useViewportHelper = (): ViewportHelperFunctions => { const useViewportHelper = (): ViewportHelperFunctions => {
const store = useStoreApi(); const store = useStoreApi();
const panZoomInitialized = useStore(selector); const panZoomInitialized = useStore(selector);
@@ -6,6 +6,12 @@ import type { ReactFlowState } from '../types';
const selector = (state: ReactFlowState) => state.panZoom?.syncViewport; const selector = (state: ReactFlowState) => state.panZoom?.syncViewport;
/**
* Hook for syncing the viewport with the panzoom instance.
*
* @internal
* @param viewport
*/
export default function useViewportSync(viewport?: Viewport) { export default function useViewportSync(viewport?: Viewport) {
const syncViewport = useStore(selector); const syncViewport = useStore(selector);
const store = useStoreApi(); const store = useStoreApi();
@@ -0,0 +1,56 @@
import { useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import { isEdgeVisible } from '@xyflow/system';
import { useStore } from './useStore';
import { type ReactFlowState } from '../types';
/**
* Hook for getting the visible edge ids from the store.
*
* @internal
* @param onlyRenderVisible
* @returns array with visible edge ids
*/
function useVisibleEdgeIds(onlyRenderVisible: boolean): string[] {
const edgeIds = useStore(
useCallback(
(s: ReactFlowState) => {
if (!onlyRenderVisible) {
return s.edges.map((edge) => edge.id);
}
const visibleEdgeIds = [];
if (s.width && s.height) {
for (const edge of s.edges) {
const sourceNode = s.nodeLookup.get(edge.source);
const targetNode = s.nodeLookup.get(edge.target);
if (
sourceNode &&
targetNode &&
isEdgeVisible({
sourceNode,
targetNode,
width: s.width,
height: s.height,
transform: s.transform,
})
) {
visibleEdgeIds.push(edge.id);
}
}
}
return visibleEdgeIds;
},
[onlyRenderVisible]
),
shallow
);
return edgeIds;
}
export default useVisibleEdgeIds;
@@ -1,51 +0,0 @@
import { useCallback } from 'react';
import { GroupedEdges, groupEdgesByZLevel, isEdgeVisible } from '@xyflow/system';
import { useStore } from '../hooks/useStore';
import { Edge, type ReactFlowState } from '../types';
import { shallow } from 'zustand/shallow';
function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): GroupedEdges<Edge>[] {
const edges = useStore(
useCallback(
(s: ReactFlowState) => {
const visibleEdges =
onlyRenderVisible && s.width && s.height
? s.edges.filter((e) => {
const sourceNode = s.nodeLookup.get(e.source);
const targetNode = s.nodeLookup.get(e.target);
return (
sourceNode &&
targetNode &&
isEdgeVisible({
sourceNode,
targetNode,
width: s.width,
height: s.height,
transform: s.transform,
})
);
})
: s.edges;
return groupEdgesByZLevel(visibleEdges, s.nodeLookup, elevateEdgesOnSelect);
},
[onlyRenderVisible, elevateEdgesOnSelect]
),
(groupA, groupB) => {
const unEqual = groupA.some(
(item, index) =>
item.isMaxLevel !== groupB[index].isMaxLevel ||
item.level !== groupB[index].level ||
!shallow(item.edges, groupB[index].edges)
);
return !unEqual;
}
);
return edges;
}
export default useVisibleEdges;
@@ -0,0 +1,29 @@
import { getNodesInside } from '@xyflow/system';
import { shallow } from 'zustand/shallow';
import { useStore } from './useStore';
import type { Node, ReactFlowState } from '../types';
import { useCallback } from 'react';
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
return onlyRenderVisible
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
(node) => node.id
)
: Array.from(s.nodeLookup.keys());
};
/**
* Hook for getting the visible node ids from the store.
*
* @internal
* @param onlyRenderVisible
* @returns array with visible node ids
*/
function useVisibleNodeIds(onlyRenderVisible: boolean) {
const nodeIds = useStore(useCallback(selector(onlyRenderVisible), [onlyRenderVisible]), shallow);
return nodeIds;
}
export default useVisibleNodeIds;
@@ -1,21 +0,0 @@
import { useCallback } from 'react';
import { getNodesInside } from '@xyflow/system';
import { useStore } from '../hooks/useStore';
import type { Node, ReactFlowState } from '../types';
function useVisibleNodes(onlyRenderVisible: boolean) {
const nodes = useStore(
useCallback(
(s: ReactFlowState) =>
onlyRenderVisible
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: s.nodes,
[onlyRenderVisible]
)
);
return nodes;
}
export default useVisibleNodes;
+2
View File
@@ -10,6 +10,7 @@ export { default as BaseEdge } from './components/Edges/BaseEdge';
export { default as ReactFlowProvider } from './components/ReactFlowProvider'; export { default as ReactFlowProvider } from './components/ReactFlowProvider';
export { default as Panel, type PanelProps } from './components/Panel'; export { default as Panel, type PanelProps } from './components/Panel';
export { default as EdgeLabelRenderer } from './components/EdgeLabelRenderer'; export { default as EdgeLabelRenderer } from './components/EdgeLabelRenderer';
export { default as ViewportPortal } from './components/ViewportPortal';
export { default as useReactFlow } from './hooks/useReactFlow'; export { default as useReactFlow } from './hooks/useReactFlow';
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals';
@@ -77,6 +78,7 @@ export {
type CoordinateExtent, type CoordinateExtent,
type ColorMode, type ColorMode,
type ColorModeClass, type ColorModeClass,
type HandleType,
} from '@xyflow/system'; } from '@xyflow/system';
// system utils // system utils
+25 -17
View File
@@ -2,7 +2,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
import { import {
clampPosition, clampPosition,
fitView as fitViewSystem, fitView as fitViewSystem,
updateNodes, adoptUserProvidedNodes,
updateAbsolutePositions, updateAbsolutePositions,
panBy as panBySystem, panBy as panBySystem,
Dimensions, Dimensions,
@@ -43,19 +43,23 @@ const createRFStore = ({
...getInitialState({ nodes, edges, width, height, fitView }), ...getInitialState({ nodes, edges, width, height, fitView }),
setNodes: (nodes: Node[]) => { setNodes: (nodes: Node[]) => {
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
// Whenver new nodes are set, we need to calculate the absolute positions of the nodes // setNodes() is called exclusively in response to user actions:
// and update the nodeLookup. // - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); // - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
//
// When this happens, we take the note objects passed by the user and extend them with fields
// relevant for internal React Flow operations.
// TODO: consider updating the types to reflect the distinction between user-provided nodes and internal nodes.
const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
set({ nodes: nextNodes }); set({ nodes: nodesWithInternalData });
}, },
setEdges: (edges: Edge[]) => { setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions = {}, connectionLookup } = get(); const { connectionLookup, edgeLookup } = get();
const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e }));
updateConnectionLookup(connectionLookup, nextEdges); updateConnectionLookup(connectionLookup, edgeLookup, edges);
set({ edges: nextEdges }); set({ edges });
}, },
// when the user works with an uncontrolled flow, // when the user works with an uncontrolled flow,
// we set a flag `hasDefaultNodes` / `hasDefaultEdges` // we set a flag `hasDefaultNodes` / `hasDefaultEdges`
@@ -74,12 +78,16 @@ const createRFStore = ({
}; };
if (hasDefaultNodes) { if (hasDefaultNodes) {
nextState.nodes = updateNodes(nodes, new Map(), { const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
nodeOrigin: get().nodeOrigin, nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, {
elevateNodesOnSelect: get().elevateNodesOnSelect, nodeOrigin,
elevateNodesOnSelect,
}); });
} }
if (hasDefaultEdges) { if (hasDefaultEdges) {
const { connectionLookup, edgeLookup } = get();
updateConnectionLookup(connectionLookup, edgeLookup, edges);
nextState.edges = edges; nextState.edges = edges;
} }
@@ -168,7 +176,7 @@ const createRFStore = ({
if (changes?.length) { if (changes?.length) {
if (hasDefaultNodes) { if (hasDefaultNodes) {
const updatedNodes = applyNodeChanges(changes, nodes); const updatedNodes = applyNodeChanges(changes, nodes);
const nextNodes = updateNodes(updatedNodes, nodeLookup, { const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, {
nodeOrigin, nodeOrigin,
elevateNodesOnSelect, elevateNodesOnSelect,
}); });
@@ -187,8 +195,8 @@ const createRFStore = ({
if (multiSelectionActive) { if (multiSelectionActive) {
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[]; changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[];
} else { } else {
changedNodes = getSelectionChanges(nodes, selectedNodeIds); changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true);
changedEdges = getSelectionChanges(edges, []); changedEdges = getSelectionChanges(edges);
} }
updateNodesAndEdgesSelections({ updateNodesAndEdgesSelections({
@@ -206,8 +214,8 @@ const createRFStore = ({
if (multiSelectionActive) { if (multiSelectionActive) {
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[]; changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[];
} else { } else {
changedEdges = getSelectionChanges(edges, selectedEdgeIds); changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds]));
changedNodes = getSelectionChanges(nodes, []); changedNodes = getSelectionChanges(nodes, new Set(), true);
} }
updateNodesAndEdgesSelections({ updateNodesAndEdgesSelections({
+13 -5
View File
@@ -1,7 +1,7 @@
import { import {
infiniteExtent, infiniteExtent,
ConnectionMode, ConnectionMode,
updateNodes, adoptUserProvidedNodes,
getNodesBounds, getNodesBounds,
getViewportForBounds, getViewportForBounds,
Transform, Transform,
@@ -24,8 +24,14 @@ const getInitialState = ({
fitView?: boolean; fitView?: boolean;
} = {}): ReactFlowStore => { } = {}): ReactFlowStore => {
const nodeLookup = new Map(); const nodeLookup = new Map();
const connectionLookup = updateConnectionLookup(new Map(), edges); const connectionLookup = new Map();
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false }); const edgeLookup = new Map();
updateConnectionLookup(connectionLookup, edgeLookup, edges);
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
nodeOrigin: [0, 0],
elevateNodesOnSelect: false,
});
let transform: Transform = [0, 0, 1]; let transform: Transform = [0, 0, 1];
@@ -43,7 +49,8 @@ const getInitialState = ({
transform, transform,
nodes: nextNodes, nodes: nextNodes,
nodeLookup, nodeLookup,
edges: edges, edges,
edgeLookup,
connectionLookup, connectionLookup,
onNodesChange: null, onNodesChange: null,
onEdgesChange: null, onEdgesChange: null,
@@ -64,7 +71,7 @@ const getInitialState = ({
paneDragging: false, paneDragging: false,
noPanClassName: 'nopan', noPanClassName: 'nopan',
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
nodeDragThreshold: 0, nodeDragThreshold: 1,
snapGrid: [15, 15], snapGrid: [15, 15],
snapToGrid: false, snapToGrid: false,
@@ -76,6 +83,7 @@ const getInitialState = ({
edgesUpdatable: true, edgesUpdatable: true,
elementsSelectable: true, elementsSelectable: true,
elevateNodesOnSelect: true, elevateNodesOnSelect: true,
elevateEdgesOnSelect: false,
fitViewOnInit: false, fitViewOnInit: false,
fitViewDone: false, fitViewDone: false,
fitViewOnInitOptions: undefined, fitViewOnInitOptions: undefined,
+12 -8
View File
@@ -30,16 +30,20 @@ export type NodeRemoveChange = {
type: 'remove'; type: 'remove';
}; };
export type NodeAddChange<NodeData = any> = { export type NodeAddChange<NodeType extends Node = Node> = {
item: Node<NodeData>; item: NodeType;
type: 'add'; type: 'add';
}; };
export type NodeResetChange<NodeData = any> = { export type NodeResetChange<NodeType extends Node = Node> = {
item: Node<NodeData>; item: NodeType;
type: 'reset'; type: 'reset';
}; };
/**
* Union type of all possible node changes.
* @public
*/
export type NodeChange = export type NodeChange =
| NodeDimensionChange | NodeDimensionChange
| NodePositionChange | NodePositionChange
@@ -50,12 +54,12 @@ export type NodeChange =
export type EdgeSelectionChange = NodeSelectionChange; export type EdgeSelectionChange = NodeSelectionChange;
export type EdgeRemoveChange = NodeRemoveChange; export type EdgeRemoveChange = NodeRemoveChange;
export type EdgeAddChange<EdgeData = any> = { export type EdgeAddChange<EdgeType extends Edge = Edge> = {
item: Edge<EdgeData>; item: EdgeType;
type: 'add'; type: 'add';
}; };
export type EdgeResetChange<EdgeData = any> = { export type EdgeResetChange<EdgeType extends Edge = Edge> = {
item: Edge<EdgeData>; item: EdgeType;
type: 'reset'; type: 'reset';
}; };
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange; export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange;
+6 -1
View File
@@ -20,6 +20,7 @@ import type {
OnError, OnError,
IsValidConnection, IsValidConnection,
ColorMode, ColorMode,
SnapGrid,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -44,6 +45,10 @@ import type {
EdgeMouseHandler, EdgeMouseHandler,
} from '.'; } from '.';
/**
* ReactFlow component props.
* @public
*/
export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & { export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
@@ -110,7 +115,7 @@ export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
multiSelectionKeyCode?: KeyCode | null; multiSelectionKeyCode?: KeyCode | null;
zoomActivationKeyCode?: KeyCode | null; zoomActivationKeyCode?: KeyCode | null;
snapToGrid?: boolean; snapToGrid?: boolean;
snapGrid?: [number, number]; snapGrid?: SnapGrid;
onlyRenderVisibleElements?: boolean; onlyRenderVisibleElements?: boolean;
nodesDraggable?: boolean; nodesDraggable?: boolean;
nodesConnectable?: boolean; nodesConnectable?: boolean;
+87 -37
View File
@@ -13,11 +13,11 @@ import type {
HandleElement, HandleElement,
ConnectionStatus, ConnectionStatus,
EdgePosition, EdgePosition,
Optional,
StepPathOptions, StepPathOptions,
OnError,
} from '@xyflow/system'; } from '@xyflow/system';
import { Node } from '.'; import { EdgeTypes, Node } from '.';
export type EdgeLabelOptions = { export type EdgeLabelOptions = {
label?: string | ReactNode; label?: string | ReactNode;
@@ -30,13 +30,13 @@ export type EdgeLabelOptions = {
export type EdgeUpdatable = boolean | HandleType; export type EdgeUpdatable = boolean | HandleType;
export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & { export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> &
style?: CSSProperties; EdgeLabelOptions & {
className?: string; style?: CSSProperties;
sourceNode?: Node; className?: string;
targetNode?: Node; updatable?: EdgeUpdatable;
updatable?: EdgeUpdatable; focusable?: boolean;
} & EdgeLabelOptions; };
type SmoothStepEdgeType<T> = DefaultEdge<T> & { type SmoothStepEdgeType<T> = DefaultEdge<T> & {
type: 'smoothstep'; type: 'smoothstep';
@@ -53,13 +53,22 @@ type StepEdgeType<T> = DefaultEdge<T> & {
pathOptions?: StepPathOptions; pathOptions?: StepPathOptions;
}; };
/**
* The Edge type is mainly used for the `edges` that get passed to the ReactFlow component
* @public
*/
export type Edge<T = any> = DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeType<T> | StepEdgeType<T>; export type Edge<T = any> = DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeType<T> | StepEdgeType<T>;
export type EdgeMouseHandler = (event: ReactMouseEvent, edge: Edge) => void; export type EdgeMouseHandler = (event: ReactMouseEvent, edge: Edge) => void;
export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle'> & { export type EdgeWrapperProps = {
id: string;
edgesFocusable: boolean;
edgesUpdatable: boolean;
elementsSelectable: boolean;
noPanClassName: string;
onClick?: EdgeMouseHandler; onClick?: EdgeMouseHandler;
onEdgeDoubleClick?: EdgeMouseHandler; onDoubleClick?: EdgeMouseHandler;
sourceHandleId?: string | null; sourceHandleId?: string | null;
targetHandleId?: string | null; targetHandleId?: string | null;
onEdgeUpdate?: OnEdgeUpdateFunc; onEdgeUpdate?: OnEdgeUpdateFunc;
@@ -71,10 +80,8 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void; onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void; onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void;
rfId?: string; rfId?: string;
isFocusable: boolean; edgeTypes?: EdgeTypes;
isUpdatable: EdgeUpdatable; onError?: OnError;
isSelectable: boolean;
pathOptions?: BezierPathOptions | SmoothStepPathOptions;
}; };
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>; export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
@@ -85,46 +92,89 @@ export type EdgeTextProps = HTMLAttributes<SVGElement> &
y: number; y: number;
}; };
// props that get passed to a custom edge /**
* Custom edge component props
* @public
*/
export type EdgeProps<T = any> = Pick< export type EdgeProps<T = any> = Pick<
Edge<T>, Edge<T>,
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
> & > &
Pick<WrapEdgeProps, 'sourceHandleId' | 'targetHandleId' | 'interactionWidth'> &
EdgePosition & EdgePosition &
EdgeLabelOptions & { EdgeLabelOptions & {
sourceHandleId?: string | null;
targetHandleId?: string | null;
markerStart?: string; markerStart?: string;
markerEnd?: string; markerEnd?: string;
// @TODO: how can we get better types for pathOptions? // @TODO: how can we get better types for pathOptions?
pathOptions?: any; pathOptions?: any;
interactionWidth?: number;
}; };
export type BaseEdgeProps = Pick<EdgeProps, 'style' | 'markerStart' | 'markerEnd' | 'interactionWidth'> & { /**
className?: CSSProperties; * BaseEdge component props
} & EdgeLabelOptions & { * @public
id?: string; */
labelX?: number; export type BaseEdgeProps = EdgeLabelOptions & {
labelY?: number; id?: string;
path: string; interactionWidth?: number;
className?: string;
labelX?: number;
labelY?: number;
markerStart?: string;
markerEnd?: string;
path: string;
style?: CSSProperties;
};
/**
* Helper type for edge components that get exported by the library
* @public
*/
export type EdgeComponentProps = EdgePosition &
EdgeLabelOptions & {
id?: EdgeProps['id'];
markerStart?: EdgeProps['markerStart'];
markerEnd?: EdgeProps['markerEnd'];
interactionWidth?: EdgeProps['interactionWidth'];
style?: EdgeProps['style'];
sourceHandleId?: EdgeProps['sourceHandleId'];
targetHandleId?: EdgeProps['targetHandleId'];
}; };
export type EdgeComponentProps<T = any> = Optional<Omit<EdgeProps<T>, 'source' | 'target'>, 'id'>; export type EdgeComponentWithPathOptions<PathOptions> = EdgeComponentProps & {
pathOptions?: PathOptions;
export type StraightEdgeProps<T = any> = Omit<EdgeComponentProps<T>, 'sourcePosition' | 'targetPosition'>;
export type SmoothStepEdgeProps<T = any> = EdgeComponentProps<T> & {
pathOptions?: SmoothStepPathOptions;
}; };
export type BezierEdgeProps<T = any> = EdgeComponentProps<T> & { /**
pathOptions?: BezierPathOptions; * BezierEdge component props
}; * @public
*/
export type BezierEdgeProps = EdgeComponentWithPathOptions<BezierPathOptions>;
export type StepEdgeProps<T = any> = EdgeComponentProps<T> & { /**
pathOptions?: StepPathOptions; * SmoothStepEdge component props
}; * @public
*/
export type SmoothStepEdgeProps = EdgeComponentWithPathOptions<SmoothStepPathOptions>;
export type SimpleBezierEdgeProps<T = any> = EdgeComponentProps<T>; /**
* StepEdge component props
* @public
*/
export type StepEdgeProps = EdgeComponentWithPathOptions<StepPathOptions>;
/**
* StraightEdge component props
* @public
*/
export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>;
/**
* SimpleBezier component props
* @public
*/
export type SimpleBezierEdgeProps = EdgeComponentProps;
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void; export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;
+6 -6
View File
@@ -1,9 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type { ComponentType, MemoExoticComponent } from 'react';
import { import {
FitViewParamsBase, FitViewParamsBase,
FitViewOptionsBase, FitViewOptionsBase,
NodeProps,
ZoomInOut, ZoomInOut,
ZoomTo, ZoomTo,
SetViewport, SetViewport,
@@ -12,9 +10,11 @@ import {
SetCenter, SetCenter,
FitBounds, FitBounds,
XYPosition, XYPosition,
NodeProps,
} from '@xyflow/system'; } from '@xyflow/system';
import type { NodeChange, EdgeChange, Node, WrapNodeProps, Edge, EdgeProps, WrapEdgeProps, ReactFlowInstance } from '.'; import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps } from '.';
import { ComponentType } from 'react';
export type OnNodesChange = (changes: NodeChange[]) => void; export type OnNodesChange = (changes: NodeChange[]) => void;
export type OnEdgesChange = (changes: EdgeChange[]) => void; export type OnEdgesChange = (changes: EdgeChange[]) => void;
@@ -24,9 +24,7 @@ export type OnEdgesDelete = (edges: Edge[]) => void;
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void; export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
export type NodeTypes = { [key: string]: ComponentType<NodeProps> }; export type NodeTypes = { [key: string]: ComponentType<NodeProps> };
export type NodeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapNodeProps>> };
export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> }; export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> };
export type EdgeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapEdgeProps>> };
export type UnselectNodesAndEdgesParams = { export type UnselectNodesAndEdgesParams = {
nodes?: Node[]; nodes?: Node[];
@@ -43,7 +41,9 @@ export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void;
export type FitViewParams = FitViewParamsBase<Node>; export type FitViewParams = FitViewParamsBase<Node>;
export type FitViewOptions = FitViewOptionsBase<Node>; export type FitViewOptions = FitViewOptionsBase<Node>;
export type FitView = (fitViewOptions?: FitViewOptions) => boolean; export type FitView = (fitViewOptions?: FitViewOptions) => boolean;
export type OnInit<NodeData = any, EdgeData = any> = (reactFlowInstance: ReactFlowInstance<NodeData, EdgeData>) => void; export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
) => void;
export type ViewportHelperFunctions = { export type ViewportHelperFunctions = {
zoomIn: ZoomInOut; zoomIn: ZoomInOut;
+41 -38
View File
@@ -3,9 +3,9 @@
import type { Rect, Viewport } from '@xyflow/system'; import type { Rect, Viewport } from '@xyflow/system';
import type { Node, Edge, ViewportHelperFunctions } from '.'; import type { Node, Edge, ViewportHelperFunctions } from '.';
export type ReactFlowJsonObject<NodeData = any, EdgeData = any> = { export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
nodes: Node<NodeData>[]; nodes: NodeType[];
edges: Edge<EdgeData>[]; edges: EdgeType[];
viewport: Viewport; viewport: Viewport;
}; };
@@ -15,30 +15,33 @@ export type DeleteElementsOptions = {
}; };
export namespace Instance { export namespace Instance {
export type GetNodes<NodeData> = () => Node<NodeData>[]; export type GetNodes<NodeType extends Node = Node> = () => NodeType[];
export type SetNodes<NodeData> = ( export type SetNodes<NodeType extends Node = Node> = (
payload: Node<NodeData>[] | ((nodes: Node<NodeData>[]) => Node<NodeData>[]) payload: NodeType[] | ((nodes: NodeType[]) => NodeType[])
) => void; ) => void;
export type AddNodes<NodeData> = (payload: Node<NodeData>[] | Node<NodeData>) => void; export type AddNodes<NodeType extends Node = Node> = (payload: NodeType[] | NodeType) => void;
export type GetNode<NodeData> = (id: string) => Node<NodeData> | undefined; export type GetNode<NodeType extends Node = Node> = (id: string) => NodeType | undefined;
export type GetEdges<EdgeData> = () => Edge<EdgeData>[]; export type GetEdges<EdgeType extends Edge = Edge> = () => EdgeType[];
export type SetEdges<EdgeData> = ( export type SetEdges<EdgeType extends Edge = Edge> = (
payload: Edge<EdgeData>[] | ((edges: Edge<EdgeData>[]) => Edge<EdgeData>[]) payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])
) => void; ) => void;
export type GetEdge<EdgeData> = (id: string) => Edge<EdgeData> | undefined; export type GetEdge<EdgeType extends Edge = Edge> = (id: string) => EdgeType | undefined;
export type AddEdges<EdgeData> = (payload: Edge<EdgeData>[] | Edge<EdgeData>) => void; export type AddEdges<EdgeType extends Edge = Edge> = (payload: EdgeType[] | EdgeType) => void;
export type ToObject<NodeData = any, EdgeData = any> = () => ReactFlowJsonObject<NodeData, EdgeData>; export type ToObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = () => ReactFlowJsonObject<
NodeType,
EdgeType
>;
export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => { export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => {
deletedNodes: Node[]; deletedNodes: Node[];
deletedEdges: Edge[]; deletedEdges: Edge[];
}; };
export type GetIntersectingNodes<NodeData> = ( export type GetIntersectingNodes<NodeType extends Node = Node> = (
node: Node<NodeData> | { id: Node['id'] } | Rect, node: NodeType | { id: Node['id'] } | Rect,
partially?: boolean, partially?: boolean,
nodes?: Node<NodeData>[] nodes?: NodeType[]
) => Node<NodeData>[]; ) => NodeType[];
export type IsNodeIntersecting<NodeData> = ( export type IsNodeIntersecting<NodeType extends Node = Node> = (
node: Node<NodeData> | { id: Node['id'] } | Rect, node: NodeType | { id: Node['id'] } | Rect,
area: Rect, area: Rect,
partially?: boolean partially?: boolean
) => boolean; ) => boolean;
@@ -46,32 +49,32 @@ export namespace Instance {
export type getIncomers = (node: string | Node | { id: Node['id'] }) => Node[]; export type getIncomers = (node: string | Node | { id: Node['id'] }) => Node[];
export type getOutgoers = (node: string | Node | { id: Node['id'] }) => Node[]; export type getOutgoers = (node: string | Node | { id: Node['id'] }) => Node[];
export type UpdateNode = ( export type UpdateNode<NodeType extends Node = Node> = (
id: string, id: string,
dataUpdate: Partial<Node> | ((node: Node) => Partial<Node>), nodeUpdate: Partial<NodeType> | ((node: NodeType) => Partial<NodeType>),
options?: { replace: boolean } options?: { replace: boolean }
) => void; ) => void;
export type UpdateNodeData = ( export type UpdateNodeData<NodeType extends Node = Node> = (
id: string, id: string,
dataUpdate: object | ((node: Node) => object), dataUpdate: object | ((node: NodeType) => object),
options?: { replace: boolean } options?: { replace: boolean }
) => void; ) => void;
} }
export type ReactFlowInstance<NodeData = any, EdgeData = any> = { export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
getNodes: Instance.GetNodes<NodeData>; getNodes: Instance.GetNodes<NodeType>;
setNodes: Instance.SetNodes<NodeData>; setNodes: Instance.SetNodes<NodeType>;
addNodes: Instance.AddNodes<NodeData>; addNodes: Instance.AddNodes<NodeType>;
getNode: Instance.GetNode<NodeData>; getNode: Instance.GetNode<NodeType>;
getEdges: Instance.GetEdges<EdgeData>; getEdges: Instance.GetEdges<EdgeType>;
setEdges: Instance.SetEdges<EdgeData>; setEdges: Instance.SetEdges<EdgeType>;
addEdges: Instance.AddEdges<EdgeData>; addEdges: Instance.AddEdges<EdgeType>;
getEdge: Instance.GetEdge<EdgeData>; getEdge: Instance.GetEdge<EdgeType>;
toObject: Instance.ToObject<NodeData, EdgeData>; toObject: Instance.ToObject<NodeType, EdgeType>;
deleteElements: Instance.DeleteElements; deleteElements: Instance.DeleteElements;
getIntersectingNodes: Instance.GetIntersectingNodes<NodeData>; getIntersectingNodes: Instance.GetIntersectingNodes<NodeType>;
isNodeIntersecting: Instance.IsNodeIntersecting<NodeData>; isNodeIntersecting: Instance.IsNodeIntersecting<NodeType>;
updateNode: Instance.UpdateNode; updateNode: Instance.UpdateNode<NodeType>;
updateNodeData: Instance.UpdateNodeData; updateNodeData: Instance.UpdateNodeData<NodeType>;
viewportInitialized: boolean; viewportInitialized: boolean;
} & Omit<ViewportHelperFunctions, 'initialized'>; } & Omit<ViewportHelperFunctions, 'initialized'>;
+31 -31
View File
@@ -1,6 +1,13 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { NodeBase, XYPosition } from '@xyflow/system'; import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system';
import { NodeTypes } from './general';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/**
* The node data structure that gets used for the nodes prop.
* @public
*/
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase< export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
NodeData, NodeData,
NodeType NodeType
@@ -8,39 +15,32 @@ export type Node<NodeData = any, NodeType extends string | undefined = string |
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
resizing?: boolean; resizing?: boolean;
focusable?: boolean;
}; };
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void; export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void; export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
export type SelectionDragHandler = (event: ReactMouseEvent, nodes: Node[]) => void; export type SelectionDragHandler = (event: ReactMouseEvent, nodes: Node[]) => void;
export type WrapNodeProps<NodeData = any> = Pick< export type NodeWrapperProps = {
Node<NodeData>, id: string;
'id' | 'data' | 'style' | 'className' | 'dragHandle' | 'sourcePosition' | 'targetPosition' | 'hidden' | 'ariaLabel' nodesConnectable: boolean;
> & elementsSelectable: boolean;
Required<Pick<Node<NodeData>, 'selected' | 'type' | 'zIndex'>> & { nodesDraggable: boolean;
isConnectable: boolean; nodesFocusable: boolean;
xPos: number; onClick?: NodeMouseHandler;
yPos: number; onDoubleClick?: NodeMouseHandler;
xPosOrigin: number; onMouseEnter?: NodeMouseHandler;
yPosOrigin: number; onMouseMove?: NodeMouseHandler;
positionAbsolute: XYPosition; onMouseLeave?: NodeMouseHandler;
initialized: boolean; onContextMenu?: NodeMouseHandler;
isSelectable: boolean; resizeObserver: ResizeObserver | null;
isDraggable: boolean; noDragClassName: string;
isFocusable: boolean; noPanClassName: string;
onClick?: NodeMouseHandler; rfId: string;
onDoubleClick?: NodeMouseHandler; disableKeyboardA11y: boolean;
onMouseEnter?: NodeMouseHandler; nodeTypes?: NodeTypes;
onMouseMove?: NodeMouseHandler; nodeExtent?: CoordinateExtent;
onMouseLeave?: NodeMouseHandler; nodeOrigin: NodeOrigin;
onContextMenu?: NodeMouseHandler; onError?: OnError;
resizeObserver: ResizeObserver | null; };
isParent: boolean;
noDragClassName: string;
noPanClassName: string;
rfId: string;
disableKeyboardA11y: boolean;
width?: number;
height?: number;
};
+7 -4
View File
@@ -24,7 +24,9 @@ import {
type OnMoveEnd, type OnMoveEnd,
type IsValidConnection, type IsValidConnection,
type UpdateConnection, type UpdateConnection,
Connection, type EdgeLookup,
type ConnectionLookup,
type NodeLookup,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -48,10 +50,10 @@ export type ReactFlowStore = {
height: number; height: number;
transform: Transform; transform: Transform;
nodes: Node[]; nodes: Node[];
nodeLookup: Map<string, Node>; nodeLookup: NodeLookup<Node>;
edges: Edge[]; edges: Edge[];
connectionLookup: Map<string, Map<string, Connection>>; edgeLookup: EdgeLookup<Edge>;
connectionLookup: ConnectionLookup;
onNodesChange: OnNodesChange | null; onNodesChange: OnNodesChange | null;
onEdgesChange: OnEdgesChange | null; onEdgesChange: OnEdgesChange | null;
hasDefaultNodes: boolean; hasDefaultNodes: boolean;
@@ -86,6 +88,7 @@ export type ReactFlowStore = {
edgesUpdatable: boolean; edgesUpdatable: boolean;
elementsSelectable: boolean; elementsSelectable: boolean;
elevateNodesOnSelect: boolean; elevateNodesOnSelect: boolean;
elevateEdgesOnSelect: boolean;
selectNodesOnDrag: boolean; selectNodesOnDrag: boolean;
multiSelectionActive: boolean; multiSelectionActive: boolean;
+82 -30
View File
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type { Node, Edge, EdgeChange, NodeChange } from '../types'; import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types';
export function handleParentExpand(res: any[], updateItem: any) { export function handleParentExpand(res: any[], updateItem: any) {
const parent = res.find((e) => e.id === updateItem.parentNode); const parent = res.find((e) => e.id === updateItem.parentNode);
@@ -55,24 +55,27 @@ function applyChanges(changes: any[], elements: any[]): any[] {
} }
let remainingChanges = changes; let remainingChanges = changes;
const initElements: any[] = changes.filter((c) => c.type === 'add').map((c) => c.item); const updatedElements: any[] = [];
return elements.reduce((res: any[], item: any) => { for (const item of elements) {
const nextChanges: any[] = []; const nextChanges: any[] = [];
const _remainingChanges: any[] = []; const _remainingChanges: any[] = [];
remainingChanges.forEach((c) => { for (const c of remainingChanges) {
if (c.id === item.id) { if (c.type === 'add') {
updatedElements.push(c.item);
} else if (c.id === item.id) {
nextChanges.push(c); nextChanges.push(c);
} else { } else {
_remainingChanges.push(c); _remainingChanges.push(c);
} }
}); }
remainingChanges = _remainingChanges; remainingChanges = _remainingChanges;
if (nextChanges.length === 0) { if (nextChanges.length === 0) {
res.push(item); updatedElements.push(item);
return res; continue;
} }
const updateItem = { ...item }; const updateItem = { ...item };
@@ -101,7 +104,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
} }
if (updateItem.expandParent) { if (updateItem.expandParent) {
handleParentExpand(res, updateItem); handleParentExpand(updatedElements, updateItem);
} }
break; break;
} }
@@ -123,48 +126,97 @@ function applyChanges(changes: any[], elements: any[]): any[] {
} }
if (updateItem.expandParent) { if (updateItem.expandParent) {
handleParentExpand(res, updateItem); handleParentExpand(updatedElements, updateItem);
} }
break; break;
} }
case 'remove': { case 'remove': {
return res; continue;
} }
} }
} }
updatedElements.push(updateItem);
} }
}
res.push(updateItem); return updatedElements;
return res;
}, initElements);
} }
export function applyNodeChanges<NodeData = any>(changes: NodeChange[], nodes: Node<NodeData>[]): Node<NodeData>[] { /**
return applyChanges(changes, nodes) as Node<NodeData>[]; * Drop in function that applies node changes to an array of nodes.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param nodes - Array of nodes to apply the changes to
* @returns Array of updated nodes
* @example
* const onNodesChange = useCallback(
(changes) => {
setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
},
[setNodes],
);
return (
<ReactFLow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
);
*/
export function applyNodeChanges<NodeType extends Node = Node>(changes: NodeChange[], nodes: NodeType[]): NodeType[] {
return applyChanges(changes, nodes) as NodeType[];
} }
export function applyEdgeChanges<EdgeData = any>(changes: EdgeChange[], edges: Edge<EdgeData>[]): Edge<EdgeData>[] { /**
return applyChanges(changes, edges) as Edge<EdgeData>[]; * Drop in function that applies edge changes to an array of edges.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param edges - Array of edge to apply the changes to
* @returns Array of updated edges
* @example
* const onEdgesChange = useCallback(
(changes) => {
setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges));
},
[setEdges],
);
return (
<ReactFLow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} />
);
*/
export function applyEdgeChanges<EdgeType extends Edge = Edge>(changes: EdgeChange[], edges: EdgeType[]): EdgeType[] {
return applyChanges(changes, edges) as EdgeType[];
} }
export const createSelectionChange = (id: string, selected: boolean) => ({ export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({
id, id,
type: 'select', type: 'select',
selected, selected,
}); });
export function getSelectionChanges(items: any[], selectedIds: string[]) { export function getSelectionChanges(
return items.reduce((res, item) => { items: any[],
const willBeSelected = selectedIds.includes(item.id); selectedIds: Set<string> = new Set(),
mutateItem = false
): NodeSelectionChange[] | EdgeSelectionChange[] {
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
if (!item.selected && willBeSelected) { for (const item of items) {
item.selected = true; const willBeSelected = selectedIds.has(item.id);
res.push(createSelectionChange(item.id, true));
} else if (item.selected && !willBeSelected) { // we don't want to set all items to selected=false on the first selection
item.selected = false; if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) {
res.push(createSelectionChange(item.id, false)); if (mutateItem) {
// this hack is needed for nodes. When the user dragged a node, it's selected.
// When another node gets dragged, we need to deselect the previous one,
// in order to have only one selected node at a time - the onNodesChange callback comes too late here :/
item.selected = willBeSelected;
}
changes.push(createSelectionChange(item.id, willBeSelected));
} }
}
return res; return changes;
}, []);
} }
+58
View File
@@ -10,10 +10,68 @@ import {
import type { Edge, Node } from '../types'; import type { Edge, Node } from '../types';
/**
* Test whether an object is useable as a Node
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Node
*/
export const isNode = isNodeBase<Node>; export const isNode = isNodeBase<Node>;
/**
* Test whether an object is useable as an Edge
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Edge
*/
export const isEdge = isEdgeBase<Edge>; export const isEdge = isEdgeBase<Edge>;
/**
* Pass in a node, and get connected nodes where edge.source === node.id
* @public
* @param node - The node to get the connected nodes from
* @param nodes - The array of all nodes
* @param edges - The array of all edges
* @returns An array of nodes that are connected over eges where the source is the given node
*/
export const getOutgoers = getOutgoersBase<Node, Edge>; export const getOutgoers = getOutgoersBase<Node, Edge>;
/**
* Pass in a node, and get connected nodes where edge.target === node.id
* @public
* @param node - The node to get the connected nodes from
* @param nodes - The array of all nodes
* @param edges - The array of all edges
* @returns An array of nodes that are connected over eges where the target is the given node
*/
export const getIncomers = getIncomersBase<Node, Edge>; export const getIncomers = getIncomersBase<Node, Edge>;
/**
* This util is a convenience function to add a new Edge to an array of edges
* @remarks It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one.
* @public
* @param edgeParams - Either an Edge or a Connection you want to add
* @param edges - The array of all current edges
* @returns A new array of edges with the new edge added
*/
export const addEdge = addEdgeBase<Edge>; export const addEdge = addEdgeBase<Edge>;
/**
* A handy utility to update an existing Edge with new properties
* @param oldEdge - The edge you want to update
* @param newConnection - The new connection you want to update the edge with
* @param edges - The array of all current edges
* @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id
* @returns the updated edges array
*/
export const updateEdge = updateEdgeBase<Edge>; export const updateEdge = updateEdgeBase<Edge>;
/**
* Get all connecting edges for a given set of nodes
* @param nodes - Nodes you want to get the connected edges for
* @param edges - All edges
* @returns Array of edges that connect any of the given nodes with each other
*/
export const getConnectedEdges = getConnectedEdgesBase<Node, Edge>; export const getConnectedEdges = getConnectedEdgesBase<Node, Edge>;
@@ -18,6 +18,7 @@
export let target: $$Props['target'] = ''; export let target: $$Props['target'] = '';
export let data: $$Props['data'] = {}; export let data: $$Props['data'] = {};
export let style: $$Props['style'] = undefined; export let style: $$Props['style'] = undefined;
export let zIndex: $$Props['zIndex'] = undefined;
export let animated: $$Props['animated'] = false; export let animated: $$Props['animated'] = false;
export let selected: $$Props['selected'] = false; export let selected: $$Props['selected'] = false;
@@ -95,42 +96,44 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
{#if !hidden} {#if !hidden}
<g <svg style:zIndex>
class={cc(['svelte-flow__edge', className])} <g
class:animated class={cc(['svelte-flow__edge', className])}
class:selected class:animated
data-id={id} class:selected
on:click={onClick} data-id={id}
on:contextmenu={onContextMenu} on:click={onClick}
aria-label={ariaLabel === null on:contextmenu={onContextMenu}
? undefined aria-label={ariaLabel === null
: ariaLabel ? undefined
? ariaLabel : ariaLabel
: `Edge from ${source} to ${target}`} ? ariaLabel
role="img" : `Edge from ${source} to ${target}`}
> role="img"
<svelte:component >
this={edgeComponent} <svelte:component
{id} this={edgeComponent}
{source} {id}
{target} {source}
{sourceX} {target}
{sourceY} {sourceX}
{targetX} {sourceY}
{targetY} {targetX}
{sourcePosition} {targetY}
{targetPosition} {sourcePosition}
{animated} {targetPosition}
{selected} {animated}
{label} {selected}
{labelStyle} {label}
{data} {labelStyle}
{style} {data}
{interactionWidth} {style}
sourceHandleId={sourceHandle} {interactionWidth}
targetHandleId={targetHandle} sourceHandleId={sourceHandle}
markerStart={markerStartUrl} targetHandleId={targetHandle}
markerEnd={markerEndUrl} markerStart={markerStartUrl}
/> markerEnd={markerEndUrl}
</g> />
</g>
</svg>
{/if} {/if}
@@ -191,7 +191,8 @@
{dragging} {dragging}
{dragHandle} {dragHandle}
isConnectable={connectable} isConnectable={connectable}
positionAbsolute={{ x: positionX, y: positionY }} positionAbsoluteX={positionX}
positionAbsoluteY={positionY}
{width} {width}
{height} {height}
/> />
@@ -0,0 +1,12 @@
<script lang="ts">
import portal from '$lib/actions/portal';
import { useStore } from '$lib/store';
type $$Props = {};
const { domNode } = useStore();
</script>
<div use:portal={{ target: '.svelte-flow__viewport-portal', domNode: $domNode }}>
<slot />
</div>
@@ -0,0 +1 @@
export { default as ViewportPortal } from './ViewportPortal.svelte';
@@ -18,10 +18,8 @@
export let zIndex: $$Props['zIndex'] = undefined; export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false; export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsolute: $$Props['positionAbsolute'] = { export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
x: 0, export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
y: 0
};
export let isConnectable: $$Props['isConnectable'] = undefined; export let isConnectable: $$Props['isConnectable'] = undefined;
// @todo: there must be a better way to do this // @todo: there must be a better way to do this
@@ -33,7 +31,8 @@
zIndex; zIndex;
dragging; dragging;
dragHandle; dragHandle;
positionAbsolute; positionAbsoluteX;
positionAbsoluteY;
isConnectable; isConnectable;
</script> </script>
@@ -15,10 +15,8 @@
export let zIndex: $$Props['zIndex'] = undefined; export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false; export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsolute: $$Props['positionAbsolute'] = { export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
x: 0, export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
y: 0
};
export let isConnectable: $$Props['isConnectable'] = undefined; export let isConnectable: $$Props['isConnectable'] = undefined;
// @todo: there must be a better way to do this // @todo: there must be a better way to do this
@@ -33,6 +31,7 @@
zIndex; zIndex;
dragging; dragging;
dragHandle; dragHandle;
positionAbsolute; positionAbsoluteX;
positionAbsoluteY;
isConnectable; isConnectable;
</script> </script>
@@ -18,10 +18,8 @@
export let zIndex: $$Props['zIndex'] = undefined; export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false; export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsolute: $$Props['positionAbsolute'] = { export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
x: 0, export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
y: 0
};
export let isConnectable: $$Props['isConnectable'] = undefined; export let isConnectable: $$Props['isConnectable'] = undefined;
// @todo: there must be a better way to do this // @todo: there must be a better way to do this
@@ -34,7 +32,8 @@
zIndex; zIndex;
dragging; dragging;
dragHandle; dragHandle;
positionAbsolute; positionAbsoluteX;
positionAbsoluteY;
isConnectable; isConnectable;
</script> </script>
@@ -18,7 +18,8 @@
export let zIndex: $$Props['zIndex'] = undefined; export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false; export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsolute: $$Props['positionAbsolute'] = { x: 0, y: 0 }; export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined; export let isConnectable: $$Props['isConnectable'] = undefined;
// @todo: there must be a better way to do this // @todo: there must be a better way to do this
@@ -31,7 +32,8 @@
zIndex; zIndex;
dragging; dragging;
dragHandle; dragHandle;
positionAbsolute; positionAbsoluteX;
positionAbsoluteY;
isConnectable; isConnectable;
</script> </script>
@@ -9,7 +9,7 @@
const { const {
elementsSelectable, elementsSelectable,
edgeTree, visibleEdges,
edges: { setDefaultOptions } edges: { setDefaultOptions }
} = useStore(); } = useStore();
@@ -18,57 +18,47 @@
}); });
</script> </script>
{#each $edgeTree as group (group.level)} <div class="svelte-flow__edges">
<svg style="z-index: {group.level}" class="svelte-flow__edges"> <svg class="svelte-flow__marker">
{#if group.isMaxLevel} <MarkerDefinition />{/if} <MarkerDefinition />
<g>
{#each group.edges as edge (edge.id)}
{@const edgeType = edge.type || 'default'}
{@const selectable = !!(
edge.selectable ||
($elementsSelectable && typeof edge.selectable === 'undefined')
)}
<EdgeWrapper
id={edge.id}
source={edge.source}
target={edge.target}
data={edge.data}
style={edge.style}
animated={edge.animated}
selected={edge.selected}
hidden={edge.hidden}
label={edge.label}
labelStyle={edge.labelStyle}
markerStart={edge.markerStart}
markerEnd={edge.markerEnd}
sourceHandle={edge.sourceHandle}
targetHandle={edge.targetHandle}
sourceX={edge.sourceX}
sourceY={edge.sourceY}
targetX={edge.targetX}
targetY={edge.targetY}
sourcePosition={edge.sourcePosition}
targetPosition={edge.targetPosition}
ariaLabel={edge.ariaLabel}
interactionWidth={edge.interactionWidth}
class={edge.class}
type={edgeType}
{selectable}
on:edgeclick
on:edgecontextmenu
/>
{/each}
</g>
</svg> </svg>
{/each}
<style> {#each $visibleEdges as edge (edge.id)}
.svelte-flow__edges { {@const edgeType = edge.type || 'default'}
width: 100%; {@const selectable = !!(
height: 100%; edge.selectable ||
position: absolute; ($elementsSelectable && typeof edge.selectable === 'undefined')
top: 0; )}
left: 0;
} <EdgeWrapper
</style> id={edge.id}
source={edge.source}
target={edge.target}
data={edge.data}
style={edge.style}
animated={edge.animated}
selected={edge.selected}
hidden={edge.hidden}
label={edge.label}
labelStyle={edge.labelStyle}
markerStart={edge.markerStart}
markerEnd={edge.markerEnd}
sourceHandle={edge.sourceHandle}
targetHandle={edge.targetHandle}
sourceX={edge.sourceX}
sourceY={edge.sourceY}
targetX={edge.targetX}
targetY={edge.targetY}
sourcePosition={edge.sourcePosition}
targetPosition={edge.targetPosition}
ariaLabel={edge.ariaLabel}
interactionWidth={edge.interactionWidth}
class={edge.class}
type={edgeType}
zIndex={edge.zIndex}
{selectable}
on:edgeclick
on:edgecontextmenu
/>
{/each}
</div>
@@ -218,6 +218,7 @@
<slot name="connectionLine" slot="connectionLine" /> <slot name="connectionLine" slot="connectionLine" />
</ConnectionLine> </ConnectionLine>
<div class="svelte-flow__edgelabel-renderer" /> <div class="svelte-flow__edgelabel-renderer" />
<div class="svelte-flow__viewport-portal" />
<NodeRenderer <NodeRenderer
on:nodeclick on:nodeclick
on:nodemouseenter on:nodemouseenter
@@ -9,6 +9,12 @@ function getMediaQuery() {
return window.matchMedia('(prefers-color-scheme: dark)'); return window.matchMedia('(prefers-color-scheme: dark)');
} }
/**
* Hook for receiving the current color mode class 'dark' or 'light'.
*
* @internal
* @param colorMode - The color mode to use ('dark', 'light' or 'system')
*/
export function useColorModeClass(colorMode: ColorMode = 'light'): Readable<ColorModeClass> { export function useColorModeClass(colorMode: ColorMode = 'light'): Readable<ColorModeClass> {
const colorModeClass = readable<ColorModeClass>('light', (set) => { const colorModeClass = readable<ColorModeClass>('light', (set) => {
if (colorMode !== 'system') { if (colorMode !== 'system') {
@@ -3,6 +3,12 @@ import type { Readable } from 'svelte/store';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { ConnectionProps } from '$lib/store/derived-connection-props'; import type { ConnectionProps } from '$lib/store/derived-connection-props';
/**
* Hook for receiving the current connection.
*
* @public
* @returns current connection as a readable store
*/
export function useConnection(): Readable<ConnectionProps> { export function useConnection(): Readable<ConnectionProps> {
const { connection } = useStore(); const { connection } = useStore();
@@ -11,6 +11,15 @@ export type useHandleConnectionsParams = {
const initialConnections: Connection[] = []; const initialConnections: Connection[] = [];
/**
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
*
* @public
* @param param.nodeId
* @param param.type - handle type 'source' or 'target'
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @returns an array with connections
*/
export function useHandleConnections({ nodeId, type, id = null }: useHandleConnectionsParams) { export function useHandleConnections({ nodeId, type, id = null }: useHandleConnectionsParams) {
const { edges, connectionLookup } = useStore(); const { edges, connectionLookup } = useStore();
let prevConnections: Map<string, Connection> | undefined = undefined; let prevConnections: Map<string, Connection> | undefined = undefined;
@@ -21,6 +21,14 @@ function areNodesDataEqual(a: Node['data'][] | null, b: Node['data'][] | null) {
return true; return true;
} }
/**
* Hook for receiving data of one or multiple nodes
*
* @public
* @param nodeId - The id (or ids) of the node to get the data from
* @param guard - Optional guard function to narrow down the node type
* @returns A readable store with an array of data objects
*/
export function useNodesData<NodeType extends Node = Node>( export function useNodesData<NodeType extends Node = Node>(
nodeId: string nodeId: string
): Readable<NodeType['data'] | null>; ): Readable<NodeType['data'] | null>;
@@ -1,10 +1,22 @@
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
/**
* Hook for getting the current nodes from the store.
*
* @public
* @returns store with an array of nodes
*/
export function useNodes() { export function useNodes() {
const { nodes } = useStore(); const { nodes } = useStore();
return nodes; return nodes;
} }
/**
* Hook for getting the current edges from the store.
*
* @public
* @returns store with an array of edges
*/
export function useEdges() { export function useEdges() {
const { edges } = useStore(); const { edges } = useStore();
return edges; return edges;
@@ -20,6 +20,12 @@ import { useStore } from '$lib/store';
import type { Edge, FitViewOptions, Node } from '$lib/types'; import type { Edge, FitViewOptions, Node } from '$lib/types';
import { isNode } from '$lib/utils'; import { isNode } from '$lib/utils';
/**
* Hook for accessing the ReactFlow instance.
*
* @public
* @returns helper functions
*/
export function useSvelteFlow(): { export function useSvelteFlow(): {
zoomIn: ZoomInOut; zoomIn: ZoomInOut;
zoomOut: ZoomInOut; zoomOut: ZoomInOut;
@@ -3,6 +3,12 @@ import type { UpdateNodeInternals } from '@xyflow/system';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
/**
* Hook for updating node internals.
*
* @public
* @returns function for updating node internals
*/
export function useUpdateNodeInternals(): UpdateNodeInternals { export function useUpdateNodeInternals(): UpdateNodeInternals {
const { domNode, updateNodeDimensions } = useStore(); const { domNode, updateNodeDimensions } = useStore();
+1
View File
@@ -6,6 +6,7 @@ export * from '$lib/container/SvelteFlow/types';
export * from '$lib/container/Panel'; export * from '$lib/container/Panel';
export * from '$lib/components/SvelteFlowProvider'; export * from '$lib/components/SvelteFlowProvider';
export * from '$lib/components/EdgeLabelRenderer'; export * from '$lib/components/EdgeLabelRenderer';
export * from '$lib/components/ViewportPortal';
export * from '$lib/components/BaseEdge'; export * from '$lib/components/BaseEdge';
export { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '$lib/components/edges'; export { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '$lib/components/edges';
export * from '$lib/components/Handle'; export * from '$lib/components/Handle';
+2 -2
View File
@@ -24,7 +24,7 @@ import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData }
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
import type { SvelteFlowStore } from './types'; import type { SvelteFlowStore } from './types';
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils'; import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
import { getEdgeTree } from './edge-tree'; import { getVisibleEdges } from './visible-edges';
import { getVisibleNodes } from './visible-nodes'; import { getVisibleNodes } from './visible-nodes';
import { getDerivedConnectionProps } from './derived-connection-props'; import { getDerivedConnectionProps } from './derived-connection-props';
@@ -357,8 +357,8 @@ export function createStore({
...store, ...store,
// derived state // derived state
edgeTree: getEdgeTree(store),
connection: getDerivedConnectionProps(store, currentConnection), connection: getDerivedConnectionProps(store, currentConnection),
visibleEdges: getVisibleEdges(store),
visibleNodes: getVisibleNodes(store), visibleNodes: getVisibleNodes(store),
markers: derived( markers: derived(
[store.edges, store.defaultMarkerColor, store.flowId], [store.edges, store.defaultMarkerColor, store.flowId],
+15 -13
View File
@@ -4,25 +4,25 @@ import {
SelectionMode, SelectionMode,
ConnectionMode, ConnectionMode,
ConnectionLineType, ConnectionLineType,
devWarn,
adoptUserProvidedNodes,
getNodesBounds,
getViewportForBounds,
updateConnectionLookup,
type SelectionRect, type SelectionRect,
type SnapGrid, type SnapGrid,
type MarkerProps, type MarkerProps,
type PanZoomInstance, type PanZoomInstance,
type CoordinateExtent, type CoordinateExtent,
type IsValidConnection, type IsValidConnection,
type GroupedEdges,
type NodeOrigin, type NodeOrigin,
type OnError, type OnError,
devWarn,
type Viewport, type Viewport,
updateNodes,
getNodesBounds,
getViewportForBounds,
updateConnectionLookup,
type ConnectionLookup, type ConnectionLookup,
type OnConnect, type OnConnect,
type OnConnectStart, type OnConnectStart,
type OnConnectEnd type OnConnectEnd,
type NodeLookup
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -78,11 +78,13 @@ export const getInitialStore = ({
fitView?: boolean; fitView?: boolean;
}) => { }) => {
const nodeLookup = new Map(); const nodeLookup = new Map();
const nextNodes = updateNodes(nodes, nodeLookup, { const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
elevateNodesOnSelect: false elevateNodesOnSelect: false
}); });
const connectionLookup = updateConnectionLookup(new Map(), edges); const connectionLookup = new Map();
const edgeLookup = new Map();
updateConnectionLookup(connectionLookup, edgeLookup, edges);
let viewport: Viewport = { x: 0, y: 0, zoom: 1 }; let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
@@ -95,17 +97,17 @@ export const getInitialStore = ({
return { return {
flowId: writable<string | null>(null), flowId: writable<string | null>(null),
nodes: createNodesStore(nextNodes, nodeLookup), nodes: createNodesStore(nextNodes, nodeLookup),
nodeLookup: readable<Map<string, Node>>(nodeLookup), nodeLookup: readable<NodeLookup>(nodeLookup),
visibleNodes: readable<Node[]>([]), visibleNodes: readable<Node[]>([]),
edges: createEdgesStore(edges, connectionLookup), edges: createEdgesStore(edges, connectionLookup, edgeLookup),
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]), visibleEdges: readable<EdgeLayouted[]>([]),
connectionLookup: readable<ConnectionLookup>(connectionLookup), connectionLookup: readable<ConnectionLookup>(connectionLookup),
height: writable<number>(500), height: writable<number>(500),
width: writable<number>(500), width: writable<number>(500),
minZoom: writable<number>(0.5), minZoom: writable<number>(0.5),
maxZoom: writable<number>(2), maxZoom: writable<number>(2),
nodeOrigin: writable<NodeOrigin>([0, 0]), nodeOrigin: writable<NodeOrigin>([0, 0]),
nodeDragThreshold: writable<number>(0), nodeDragThreshold: writable<number>(1),
nodeExtent: writable<CoordinateExtent>(infiniteExtent), nodeExtent: writable<CoordinateExtent>(infiniteExtent),
translateExtent: writable<CoordinateExtent>(infiniteExtent), translateExtent: writable<CoordinateExtent>(infiniteExtent),
autoPanOnNodeDrag: writable<boolean>(true), autoPanOnNodeDrag: writable<boolean>(true),
+10 -7
View File
@@ -1,17 +1,19 @@
import { import {
writable, writable,
get,
type Unsubscriber, type Unsubscriber,
type Subscriber, type Subscriber,
type Updater, type Updater,
type Writable, type Writable
get
} from 'svelte/store'; } from 'svelte/store';
import { import {
updateNodes, adoptUserProvidedNodes,
updateConnectionLookup,
type Viewport, type Viewport,
type PanZoomInstance, type PanZoomInstance,
type ConnectionLookup, type ConnectionLookup,
updateConnectionLookup type EdgeLookup,
type NodeLookup
} from '@xyflow/system'; } from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types'; import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
@@ -125,7 +127,7 @@ export type NodeStoreOptions = {
// The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes. // The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes.
export const createNodesStore = ( export const createNodesStore = (
nodes: Node[], nodes: Node[],
nodeLookup: Map<string, Node> nodeLookup: NodeLookup<Node>
): { ): {
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber; subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
update: (this: void, updater: Updater<Node[]>) => void; update: (this: void, updater: Updater<Node[]>) => void;
@@ -139,7 +141,7 @@ export const createNodesStore = (
let elevateNodesOnSelect = true; let elevateNodesOnSelect = true;
const _set = (nds: Node[]): Node[] => { const _set = (nds: Node[]): Node[] => {
const nextNodes = updateNodes(nds, nodeLookup, { const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, {
elevateNodesOnSelect, elevateNodesOnSelect,
defaults defaults
}); });
@@ -175,6 +177,7 @@ export const createNodesStore = (
export const createEdgesStore = ( export const createEdgesStore = (
edges: Edge[], edges: Edge[],
connectionLookup: ConnectionLookup, connectionLookup: ConnectionLookup,
edgeLookup: EdgeLookup<Edge>,
defaultOptions?: DefaultEdgeOptions defaultOptions?: DefaultEdgeOptions
): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => { ): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => {
const { subscribe, set, update } = writable<Edge[]>([]); const { subscribe, set, update } = writable<Edge[]>([]);
@@ -184,7 +187,7 @@ export const createEdgesStore = (
const _set: typeof set = (eds: Edge[]) => { const _set: typeof set = (eds: Edge[]) => {
const nextEdges = defaults ? eds.map((edge) => ({ ...defaults, ...edge })) : eds; const nextEdges = defaults ? eds.map((edge) => ({ ...defaults, ...edge })) : eds;
updateConnectionLookup(connectionLookup, nextEdges); updateConnectionLookup(connectionLookup, edgeLookup, nextEdges);
value = nextEdges; value = nextEdges;
set(value); set(value);
@@ -1,10 +1,10 @@
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import { groupEdgesByZLevel, isEdgeVisible, getEdgePosition } from '@xyflow/system'; import { isEdgeVisible, getEdgePosition, getElevatedEdgeZIndex } from '@xyflow/system';
import type { EdgeLayouted } from '$lib/types'; import type { EdgeLayouted } from '$lib/types';
import type { SvelteFlowStoreState } from './types'; import type { SvelteFlowStoreState } from './types';
export function getEdgeTree(store: SvelteFlowStoreState) { export function getVisibleEdges(store: SvelteFlowStoreState) {
const visibleEdges = derived( const visibleEdges = derived(
[ [
store.edges, store.edges,
@@ -64,6 +64,13 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
if (edgePosition) { if (edgePosition) {
res.push({ res.push({
...edge, ...edge,
zIndex: getElevatedEdgeZIndex({
selected: edge.selected,
zIndex: edge.zIndex,
sourceNode,
targetNode,
elevateOnSelect: false
}),
...edgePosition ...edgePosition
}); });
} }
@@ -71,9 +78,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
return res; return res;
}, []); }, []);
const groupedEdges = groupEdgesByZLevel<EdgeLayouted>(layoutedEdges, nodeLookup, false); return layoutedEdges;
return groupedEdges;
} }
); );
} }

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