Merge branch 'next' into refactor/tsdoc
This commit is contained in:
@@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = {
|
||||
left: 'auto',
|
||||
};
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ data, positionAbsolute }) => {
|
||||
const CustomNode: FC<NodeProps> = ({ data, positionAbsoluteX, positionAbsoluteY }) => {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
@@ -18,7 +18,7 @@ const CustomNode: FC<NodeProps> = ({ data, positionAbsolute }) => {
|
||||
<div>
|
||||
Position:{' '}
|
||||
<strong>
|
||||
{positionAbsolute.x.toFixed(2)},{positionAbsolute.y.toFixed(2)}
|
||||
{positionAbsoluteX.toFixed(2)},{positionAbsoluteY.toFixed(2)}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,6 @@ import Interaction from '../examples/Interaction';
|
||||
import Intersection from '../examples/Intersection';
|
||||
import Layouting from '../examples/Layouting';
|
||||
import MultiFlows from '../examples/MultiFlows';
|
||||
import NestedNodes from '../examples/NestedNodes';
|
||||
import NodeResizer from '../examples/NodeResizer';
|
||||
import NodeTypeChange from '../examples/NodeTypeChange';
|
||||
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
||||
@@ -44,7 +43,9 @@ import CancelConnection from '../examples/CancelConnection';
|
||||
import InteractiveMinimap from '../examples/InteractiveMinimap';
|
||||
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
|
||||
import NodeToolbar from '../examples/NodeToolbar';
|
||||
import useNodesInitialized from '../examples/UseNodesInit';
|
||||
import UseNodesInitialized from '../examples/UseNodesInit';
|
||||
import UseNodesData from '../examples/UseNodesData';
|
||||
import UseHandleConnections from '../examples/UseHandleConnections';
|
||||
|
||||
export interface IRoute {
|
||||
name: string;
|
||||
@@ -183,11 +184,6 @@ const routes: IRoute[] = [
|
||||
path: 'multiflows',
|
||||
component: MultiFlows,
|
||||
},
|
||||
{
|
||||
name: 'Nested Nodes',
|
||||
path: 'nested-nodes',
|
||||
component: NestedNodes,
|
||||
},
|
||||
{
|
||||
name: 'Node Type Change',
|
||||
path: 'nodetype-change',
|
||||
@@ -261,7 +257,7 @@ const routes: IRoute[] = [
|
||||
{
|
||||
name: 'useNodesInitialized',
|
||||
path: 'use-nodes-initialized',
|
||||
component: useNodesInitialized,
|
||||
component: UseNodesInitialized,
|
||||
},
|
||||
{
|
||||
name: 'useOnSelectionChange',
|
||||
@@ -273,6 +269,16 @@ const routes: IRoute[] = [
|
||||
path: 'usereactflow',
|
||||
component: UseReactFlow,
|
||||
},
|
||||
{
|
||||
name: 'useHandleConnections',
|
||||
path: 'usehandleconnections',
|
||||
component: UseHandleConnections,
|
||||
},
|
||||
{
|
||||
name: 'useNodesData',
|
||||
path: 'usenodesdata',
|
||||
component: UseNodesData,
|
||||
},
|
||||
{
|
||||
name: 'useUpdateNodeInternals',
|
||||
path: 'useupdatenodeinternals',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo, FC, CSSProperties, useCallback } from 'react';
|
||||
import React, { memo, FC, CSSProperties, useCallback, useEffect } from 'react';
|
||||
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
|
||||
|
||||
const targetHandleStyle: CSSProperties = { background: '#555' };
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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 w = intersectionNodeWidth! / 2;
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Position, XYPosition, Node, Edge } from '@xyflow/react';
|
||||
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
|
||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||
|
||||
const {
|
||||
width: intersectionNodeWidth,
|
||||
height: intersectionNodeHeight,
|
||||
position: intersectionNodePosition,
|
||||
} = intersectionNode;
|
||||
const { position: intersectionNodePosition } = intersectionNode;
|
||||
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.computed ?? {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
const targetPosition = targetNode.position;
|
||||
|
||||
const w = (intersectionNodeWidth ?? 0) / 2;
|
||||
|
||||
@@ -48,8 +48,10 @@ const initialEdges: Edge[] = [
|
||||
|
||||
const setHidden = (hidden: boolean) => (els: any[]) =>
|
||||
els.map((e: any) => {
|
||||
e.hidden = hidden;
|
||||
return e;
|
||||
return {
|
||||
...e,
|
||||
hidden,
|
||||
};
|
||||
});
|
||||
|
||||
const HiddenFlow = () => {
|
||||
|
||||
@@ -56,16 +56,16 @@ const LayoutFlow = () => {
|
||||
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id);
|
||||
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
|
||||
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
|
||||
// we need to pass a slightly different position in order to notify react flow about the change
|
||||
// @TODO how can we change the position handling so that we dont need this hack?
|
||||
node.position = {
|
||||
x: nodeWithPosition.x + Math.random() / 1000,
|
||||
y: nodeWithPosition.y,
|
||||
};
|
||||
|
||||
return node;
|
||||
return {
|
||||
...node,
|
||||
targetPosition: isHorizontal ? Position.Left : Position.Top,
|
||||
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
|
||||
position: {
|
||||
x: nodeWithPosition.x,
|
||||
y: nodeWithPosition.y,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Edge,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@xyflow/react';
|
||||
|
||||
import { getNodesAndEdges } from './utils';
|
||||
import { FrameRecorder, generateMouseEventParamsTargetingNode, nextFrame } from './performanceUtils';
|
||||
|
||||
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
|
||||
|
||||
@@ -25,6 +26,166 @@ const StressFlow = () => {
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
}, []);
|
||||
|
||||
const dragInViewport = async () => {
|
||||
// Note: selecting specifically node 18, as it’s 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 = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
@@ -56,6 +217,7 @@ const StressFlow = () => {
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
key={key}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onConnect={onConnect}
|
||||
@@ -64,11 +226,14 @@ const StressFlow = () => {
|
||||
minZoom={0.2}
|
||||
fitView
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
|
||||
<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={updateElements}>update elements</button>
|
||||
</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 it’s 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
|
||||
// doesn’t 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 there’s 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();
|
||||
|
||||
// Let’s make the event (eg click) happen 5px to the right and 5px to the
|
||||
// bottom of the node’s 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,
|
||||
};
|
||||
|
||||
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsolute, id }) => {
|
||||
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<div style={idStyle}>{id}</div>
|
||||
<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>
|
||||
<Handle type="source" position={Position.Bottom} />
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
addEdge,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
@@ -97,7 +96,6 @@ const initialEdges: Edge[] = [
|
||||
{ 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) =>
|
||||
console.log(`start update ${handleType} handle`, edge);
|
||||
const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) =>
|
||||
@@ -111,7 +109,6 @@ const UpdatableEdge = () => {
|
||||
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
|
||||
|
||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||
console.log(changes);
|
||||
setNodes((ns) => applyNodeChanges(changes, ns));
|
||||
}, []);
|
||||
|
||||
@@ -125,12 +122,12 @@ const UpdatableEdge = () => {
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={onInit}
|
||||
snapToGrid={true}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onConnect={onConnect}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
fitView
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ReactFlow, Node, Edge, useNodesState, useEdgesState } from '@xyflow/react';
|
||||
import { ReactFlow, Node, Edge, useNodesState, useEdgesState, ReactFlowProvider, useReactFlow } from '@xyflow/react';
|
||||
|
||||
import styles from './updatenode.module.css';
|
||||
|
||||
@@ -13,6 +13,7 @@ const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||
const UpdateNode = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const { updateNode } = useReactFlow();
|
||||
|
||||
const [nodeName, setNodeName] = useState<string>('Node 1');
|
||||
const [nodeBg, setNodeBg] = useState<string>('#eee');
|
||||
@@ -23,9 +24,12 @@ const UpdateNode = () => {
|
||||
nds.map((n) => {
|
||||
if (n.id === '1') {
|
||||
// it's important that you create a new object here in order to notify react flow about the change
|
||||
n.data = {
|
||||
...n.data,
|
||||
label: nodeName,
|
||||
return {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
label: nodeName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +43,10 @@ const UpdateNode = () => {
|
||||
nds.map((n) => {
|
||||
if (n.id === '1') {
|
||||
// it's important that you create a new object here in order to notify react flow about the change
|
||||
n.style = { ...n.style, backgroundColor: nodeBg };
|
||||
return {
|
||||
...n,
|
||||
style: { ...n.style, backgroundColor: nodeBg },
|
||||
};
|
||||
}
|
||||
|
||||
return n;
|
||||
@@ -51,8 +58,10 @@ const UpdateNode = () => {
|
||||
setNodes((nds) =>
|
||||
nds.map((n) => {
|
||||
if (n.id === '1' || n.id === 'e1-2') {
|
||||
// when you update a simple type you can just update the value
|
||||
n.hidden = nodeHidden;
|
||||
return {
|
||||
...n,
|
||||
hidden: nodeHidden,
|
||||
};
|
||||
}
|
||||
|
||||
return n;
|
||||
@@ -80,9 +89,19 @@ const UpdateNode = () => {
|
||||
<label>hidden:</label>
|
||||
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => updateNode('1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }))}
|
||||
>
|
||||
update position
|
||||
</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateNode;
|
||||
export default () => (
|
||||
<ReactFlowProvider>
|
||||
<UpdateNode />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { memo, FC, useEffect, useCallback } from 'react';
|
||||
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleComponentProps } from '@xyflow/react';
|
||||
|
||||
function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) {
|
||||
const onConnect = useCallback(
|
||||
(connections: Connection[]) => console.log('onConnect handler, node id:', nodeId, connections),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const onDisconnect = useCallback(
|
||||
(connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections),
|
||||
[nodeId]
|
||||
);
|
||||
const connections = useHandleConnections({
|
||||
type: handleProps.type,
|
||||
id: handleProps.id,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
|
||||
}, [connections]);
|
||||
|
||||
return <Handle {...handleProps} />;
|
||||
}
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ id }) => {
|
||||
return (
|
||||
<div style={{ background: '#333', color: '#fff', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||
<CustomHandle nodeId={id} type="target" position={Position.Left} />
|
||||
<div>node {id}</div>
|
||||
<CustomHandle nodeId={id} type="source" position={Position.Right} id="a" style={{ top: 10 }} />
|
||||
<CustomHandle nodeId={id} type="source" position={Position.Right} id="b" style={{ top: 20 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomNode);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { memo, FC, useEffect, useCallback } from 'react';
|
||||
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleComponentProps } from '@xyflow/react';
|
||||
|
||||
function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) {
|
||||
const onConnect = useCallback(
|
||||
(connections: Connection[]) => {
|
||||
console.log('onConnect handler, node id:', nodeId, connections);
|
||||
},
|
||||
[nodeId]
|
||||
);
|
||||
const onDisconnect = useCallback(
|
||||
(connections: Connection[]) => {
|
||||
console.log('onDisconnect handler, node id:', nodeId, connections);
|
||||
},
|
||||
[nodeId]
|
||||
);
|
||||
const connections = useHandleConnections({
|
||||
type: handleProps.type,
|
||||
id: handleProps.id,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
|
||||
}, [connections]);
|
||||
|
||||
return <Handle {...handleProps} />;
|
||||
}
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ id }) => {
|
||||
return (
|
||||
<div style={{ background: '#333', color: '#fff', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||
<CustomHandle nodeId={id} type="target" position={Position.Left} />
|
||||
<div>node {id}</div>
|
||||
<CustomHandle nodeId={id} type="source" position={Position.Right} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomNode);
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
MiniMap,
|
||||
Controls,
|
||||
addEdge,
|
||||
Connection,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Background,
|
||||
} from '@xyflow/react';
|
||||
|
||||
import MultiHandleNode from './MultiHandleNode';
|
||||
import SingleHandleNode from './SingleHandleNode';
|
||||
|
||||
const nodeTypes = {
|
||||
multi: MultiHandleNode,
|
||||
single: SingleHandleNode,
|
||||
};
|
||||
|
||||
const initNodes = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'single',
|
||||
data: {},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'single',
|
||||
data: {},
|
||||
position: { x: 200, y: -100 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'single',
|
||||
data: {},
|
||||
position: { x: 200, y: 100 },
|
||||
},
|
||||
|
||||
{
|
||||
id: '4',
|
||||
type: 'multi',
|
||||
data: {},
|
||||
position: { x: 400, y: 0 },
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'multi',
|
||||
data: {},
|
||||
position: { x: 600, y: -100 },
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'multi',
|
||||
data: {},
|
||||
position: { x: 600, y: 100 },
|
||||
},
|
||||
];
|
||||
|
||||
const initEdges = [
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: '1',
|
||||
target: '2',
|
||||
},
|
||||
{
|
||||
id: 'e1-3',
|
||||
source: '1',
|
||||
target: '3',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'e4a-5',
|
||||
source: '4',
|
||||
sourceHandle: 'a',
|
||||
target: '5',
|
||||
},
|
||||
{
|
||||
id: 'e4b-5',
|
||||
source: '4',
|
||||
sourceHandle: 'b',
|
||||
target: '6',
|
||||
},
|
||||
];
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
animated: true,
|
||||
};
|
||||
|
||||
const CustomNodeFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||
|
||||
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
minZoom={0.3}
|
||||
maxZoom={2}
|
||||
colorMode="dark"
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomNodeFlow;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { memo } from 'react';
|
||||
import { Handle, Position, useHandleConnections, useNodesData } from '@xyflow/react';
|
||||
|
||||
function ResultNode() {
|
||||
const connections = useHandleConnections({
|
||||
type: 'target',
|
||||
});
|
||||
const nodesData = useNodesData(connections.map((connection) => connection.source));
|
||||
|
||||
return (
|
||||
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<div>
|
||||
incoming texts:{' '}
|
||||
{nodesData?.filter((nodeData) => nodeData.text !== undefined).map(({ text }, i) => <div key={i}>{text}</div>) ||
|
||||
'none'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ResultNode);
|
||||
@@ -0,0 +1,20 @@
|
||||
import { memo, ChangeEventHandler } from 'react';
|
||||
import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react';
|
||||
|
||||
function TextNode({ id, data }: NodeProps) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
|
||||
const onChange: ChangeEventHandler<HTMLInputElement> = (evt) => updateNodeData(id, { text: evt.target.value });
|
||||
|
||||
return (
|
||||
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||
<div>node {id}</div>
|
||||
<div>
|
||||
<input onChange={onChange} value={data.text} />
|
||||
</div>
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(TextNode);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { memo, useEffect } from 'react';
|
||||
import { Position, NodeProps, useReactFlow, Handle, useHandleConnections, useNodesData } from '@xyflow/react';
|
||||
|
||||
function UppercaseNode({ id }: NodeProps) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const connections = useHandleConnections({
|
||||
type: 'target',
|
||||
});
|
||||
const nodeData = useNodesData(connections[0]?.source);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeData(id, { text: nodeData?.text.toUpperCase() });
|
||||
}, [nodeData]);
|
||||
|
||||
return (
|
||||
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||
<Handle type="target" position={Position.Left} isConnectable={connections.length === 0} />
|
||||
<div>uppercase transform</div>
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(UppercaseNode);
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Controls,
|
||||
addEdge,
|
||||
Connection,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Background,
|
||||
Node,
|
||||
Edge,
|
||||
} from '@xyflow/react';
|
||||
|
||||
import TextNode from './TextNode';
|
||||
import ResultNode from './ResultNode';
|
||||
import UppercaseNode from './UppercaseNode';
|
||||
|
||||
export type TextNode = Node<{ text: string }, 'text'>;
|
||||
export type ResultNode = Node<{}, 'result'>;
|
||||
export type UppercaseNode = Node<{}, 'uppercase'>;
|
||||
export type MyNode = Node<{ text: string }, 'text'> | Node<{}, 'result'> | Node<{}, 'uppercase'>;
|
||||
|
||||
const nodeTypes = {
|
||||
text: TextNode,
|
||||
result: ResultNode,
|
||||
uppercase: UppercaseNode,
|
||||
};
|
||||
|
||||
const initNodes: MyNode[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'text',
|
||||
data: {
|
||||
text: 'hello',
|
||||
},
|
||||
position: { x: -100, y: -50 },
|
||||
},
|
||||
{
|
||||
id: '1a',
|
||||
type: 'uppercase',
|
||||
data: {},
|
||||
position: { x: 100, y: 0 },
|
||||
},
|
||||
|
||||
{
|
||||
id: '2',
|
||||
type: 'text',
|
||||
data: {
|
||||
text: 'world',
|
||||
},
|
||||
position: { x: 0, y: 100 },
|
||||
},
|
||||
|
||||
{
|
||||
id: '3',
|
||||
type: 'result',
|
||||
data: {},
|
||||
position: { x: 300, y: 50 },
|
||||
},
|
||||
];
|
||||
|
||||
const initEdges: Edge[] = [
|
||||
{
|
||||
id: 'e1-1a',
|
||||
source: '1',
|
||||
target: '1a',
|
||||
},
|
||||
{
|
||||
id: 'e1a-3',
|
||||
source: '1a',
|
||||
target: '3',
|
||||
},
|
||||
{
|
||||
id: 'e2-3',
|
||||
source: '2',
|
||||
target: '3',
|
||||
},
|
||||
];
|
||||
|
||||
const CustomNodeFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||
|
||||
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
>
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomNodeFlow;
|
||||
@@ -11,6 +11,7 @@
|
||||
'drag-n-drop',
|
||||
'edges',
|
||||
'figma',
|
||||
'handle-connect',
|
||||
'interaction',
|
||||
'intersections',
|
||||
'node-toolbar',
|
||||
@@ -18,6 +19,7 @@
|
||||
'stress',
|
||||
'subflows',
|
||||
'two-way-viewport',
|
||||
'usenodesdata',
|
||||
'usesvelteflow',
|
||||
'useupdatenodeinternals',
|
||||
'validation'
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { writable } from 'svelte/store';
|
||||
import { SvelteFlow, useSvelteFlow, type Edge, type Node } from '@xyflow/svelte';
|
||||
import {
|
||||
SvelteFlow,
|
||||
useSvelteFlow,
|
||||
type Edge,
|
||||
type Node,
|
||||
type OnConnectEnd
|
||||
} from '@xyflow/svelte';
|
||||
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
|
||||
@@ -16,16 +22,18 @@
|
||||
const nodes = writable<Node[]>(initialNodes);
|
||||
const edges = writable<Edge[]>([]);
|
||||
|
||||
let connectingNodeId: string = '0';
|
||||
let connectingNodeId: string | null = '0';
|
||||
let rect: DOMRectReadOnly;
|
||||
let id = 1;
|
||||
const getId = () => `${id++}`;
|
||||
|
||||
const { screenToFlowPosition, flowToScreenPosition } = useSvelteFlow();
|
||||
|
||||
function handleConnectEnd({ detail: { event } }: { detail: { event: MouseEvent | TouchEvent } }) {
|
||||
const handleConnectEnd: OnConnectEnd = (event) => {
|
||||
if (!connectingNodeId) return;
|
||||
|
||||
// See of connection landed inside the flow pane
|
||||
const targetIsPane = event.target?.classList.contains('svelte-flow__pane');
|
||||
const targetIsPane = (event.target as HTMLDivElement)?.classList.contains('svelte-flow__pane');
|
||||
if (targetIsPane) {
|
||||
const id = getId();
|
||||
const position = {
|
||||
@@ -58,7 +66,7 @@
|
||||
$nodes = $nodes;
|
||||
$edges = $edges;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window />
|
||||
@@ -69,11 +77,11 @@
|
||||
{edges}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 2 }}
|
||||
on:connectstart={({ detail: { nodeId } }) => {
|
||||
onconnectstart={(_, { nodeId }) => {
|
||||
// Memorize the nodeId you start draggin a connection line from a node
|
||||
connectingNodeId = nodeId;
|
||||
}}
|
||||
on:connectend={handleConnectEnd}
|
||||
onconnectend={handleConnectEnd}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
{nodeTypes}
|
||||
style="--xy-background-color: {$bgColor}"
|
||||
fitView
|
||||
on:connect={onConnect}
|
||||
onconnect={onConnect}
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { Handle, Position, type NodeProps, BezierEdge, SmoothStepEdge } from '@xyflow/svelte';
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
<Handle type="target" position={Position.Left} on:connect />
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<div>
|
||||
Custom Color Picker Node: <strong>{$colorStore}</strong>
|
||||
</div>
|
||||
@@ -20,14 +20,8 @@
|
||||
on:input={(evt) => colorStore.set(evt.currentTarget.value)}
|
||||
value={$colorStore}
|
||||
/>
|
||||
<Handle type="source" position={Position.Right} id="a" style="top: 20px;" on:connect />
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="b"
|
||||
style="top: auto; bottom: 10px;"
|
||||
on:connect
|
||||
/>
|
||||
<Handle type="source" position={Position.Right} id="a" style="top: 20px;" />
|
||||
<Handle type="source" position={Position.Right} id="b" style="top: auto; bottom: 10px;" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { writable } from 'svelte/store';
|
||||
import {
|
||||
SvelteFlow,
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
MiniMap,
|
||||
type Node,
|
||||
type NodeTypes,
|
||||
type Edge
|
||||
} from '@xyflow/svelte';
|
||||
import SingleHandleNode from './SingleHandleNode.svelte';
|
||||
import MultiHandleNode from './MultiHandleNode.svelte';
|
||||
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
single: SingleHandleNode,
|
||||
multi: MultiHandleNode
|
||||
};
|
||||
|
||||
const nodes = writable<Node[]>([
|
||||
{
|
||||
id: '1',
|
||||
type: 'single',
|
||||
data: {},
|
||||
position: { x: 0, y: 0 }
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'single',
|
||||
data: {},
|
||||
position: { x: 200, y: -100 }
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'single',
|
||||
data: {},
|
||||
position: { x: 200, y: 100 }
|
||||
},
|
||||
|
||||
{
|
||||
id: '4',
|
||||
type: 'multi',
|
||||
data: {},
|
||||
position: { x: 400, y: 0 }
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'multi',
|
||||
data: {},
|
||||
position: { x: 600, y: -100 }
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'multi',
|
||||
data: {},
|
||||
position: { x: 600, y: 100 }
|
||||
}
|
||||
]);
|
||||
|
||||
const edges = writable<Edge[]>([
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: '1',
|
||||
target: '2'
|
||||
},
|
||||
{
|
||||
id: 'e1-3',
|
||||
source: '1',
|
||||
target: '3'
|
||||
},
|
||||
|
||||
{
|
||||
id: 'e4a-5',
|
||||
source: '4',
|
||||
sourceHandle: 'a',
|
||||
target: '5'
|
||||
},
|
||||
{
|
||||
id: 'e4b-5',
|
||||
source: '4',
|
||||
sourceHandle: 'b',
|
||||
target: '6'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<SvelteFlow {nodes} {edges} {nodeTypes} fitView colorMode="dark">
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<MiniMap />
|
||||
</SvelteFlow>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Handle,
|
||||
Position,
|
||||
type NodeProps,
|
||||
type Connection,
|
||||
useHandleConnections
|
||||
} from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: $$Props['id'];
|
||||
|
||||
function onConnectTarget(connection: Connection[]) {
|
||||
console.log('connect target', connection);
|
||||
}
|
||||
|
||||
function onConnectSource(handleId: string, connection: Connection[]) {
|
||||
console.log('connect source', handleId, connection);
|
||||
}
|
||||
|
||||
function onDisconnectTarget(connection: Connection[]) {
|
||||
console.log('disconnect target', connection);
|
||||
}
|
||||
|
||||
function onDisconnectSource(handleId: string, connection: Connection[]) {
|
||||
console.log('disconnect source', handleId, connection);
|
||||
}
|
||||
|
||||
const connections = useHandleConnections({ nodeId: id, type: 'target' });
|
||||
|
||||
$: {
|
||||
console.log('connections', id, $connections);
|
||||
}
|
||||
|
||||
export let data: $$Props['data'];
|
||||
export let targetPosition: $$Props['targetPosition'] = Position.Top;
|
||||
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let type: $$Props['type'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let positionAbsolute: $$Props['positionAbsolute'] = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
data;
|
||||
targetPosition;
|
||||
sourcePosition;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
positionAbsolute;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
onconnect={onConnectTarget}
|
||||
ondisconnect={onDisconnectTarget}
|
||||
/>
|
||||
<div>node {id}</div>
|
||||
<Handle
|
||||
id="a"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
onconnect={(connections) => onConnectSource('a', connections)}
|
||||
ondisconnect={(connections) => onDisconnectSource('a', connections)}
|
||||
class="source-a"
|
||||
/>
|
||||
<Handle
|
||||
id="b"
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
onconnect={(connections) => onConnectSource('b', connections)}
|
||||
ondisconnect={(connections) => onDisconnectSource('b', connections)}
|
||||
class="source-b"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.custom {
|
||||
background-color: #333;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom :global(.source-a) {
|
||||
top: 5px;
|
||||
transform: translate(50%, 0);
|
||||
}
|
||||
|
||||
.custom :global(.source-b) {
|
||||
bottom: 5px;
|
||||
top: auto;
|
||||
transform: translate(50%, 0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position, type NodeProps, type Connection } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: $$Props['id'];
|
||||
|
||||
function onConnectTarget(connection: Connection[]) {
|
||||
console.log('connect target', connection);
|
||||
}
|
||||
|
||||
function onConnectSource(connection: Connection[]) {
|
||||
console.log('connect source', connection);
|
||||
}
|
||||
|
||||
function onDisconnectTarget(connection: Connection[]) {
|
||||
console.log('disconnect target', connection);
|
||||
}
|
||||
|
||||
function onDisconnectSource(connection: Connection[]) {
|
||||
console.log('disconnect source', connection);
|
||||
}
|
||||
|
||||
export let data: $$Props['data'];
|
||||
export let targetPosition: $$Props['targetPosition'] = Position.Top;
|
||||
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let type: $$Props['type'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let positionAbsolute: $$Props['positionAbsolute'] = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
data;
|
||||
targetPosition;
|
||||
sourcePosition;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
positionAbsolute;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
onconnect={onConnectTarget}
|
||||
ondisconnect={onDisconnectTarget}
|
||||
/>
|
||||
<div>node {id}</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
onconnect={onConnectSource}
|
||||
ondisconnect={onDisconnectSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.custom {
|
||||
background-color: #333;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -12,7 +12,8 @@
|
||||
type EdgeTypes,
|
||||
type Node,
|
||||
type Edge,
|
||||
ConnectionMode
|
||||
ConnectionMode,
|
||||
useSvelteFlow
|
||||
} from '@xyflow/svelte';
|
||||
|
||||
import CustomNode from './CustomNode.svelte';
|
||||
@@ -150,9 +151,9 @@
|
||||
on:nodemouseenter={(event) => console.log('on node enter', event)}
|
||||
on:nodemouseleave={(event) => console.log('on node leave', event)}
|
||||
on:edgeclick={(event) => console.log('edge click', event)}
|
||||
on:connectstart={(event) => console.log('on connect start', event)}
|
||||
on:connect={(event) => console.log('on connect', event)}
|
||||
on:connectend={(event) => console.log('on connect end', event)}
|
||||
onconnectstart={(event) => console.log('on connect start', event)}
|
||||
onconnect={(event) => console.log('on connect', event)}
|
||||
onconnectend={(event) => console.log('on connect end', event)}
|
||||
on:paneclick={(event) => console.log('on pane click', event)}
|
||||
on:panecontextmenu={(event) => {
|
||||
console.log('on pane contextmenu', event);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position, type NodeProps, type XYPosition } from '@xyflow/svelte';
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
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>
|
||||
|
||||
<div class="custom">
|
||||
<div>{data.label}</div>
|
||||
<div>{~~positionAbsolute.x}, {~~positionAbsolute.y}</div>
|
||||
<div>{~~positionAbsoluteX}, {~~positionAbsoluteY}</div>
|
||||
|
||||
<Handle type="target" position={Position.Top} />
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { writable } from 'svelte/store';
|
||||
import {
|
||||
SvelteFlow,
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
MiniMap,
|
||||
type Node,
|
||||
type NodeTypes,
|
||||
type Edge
|
||||
} from '@xyflow/svelte';
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
|
||||
import TextNode from './TextNode.svelte';
|
||||
import UppercaseNode from './UppercaseNode.svelte';
|
||||
import ResultNode from './ResultNode.svelte';
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
text: TextNode,
|
||||
uppercase: UppercaseNode,
|
||||
result: ResultNode
|
||||
};
|
||||
|
||||
const nodes = writable<Node[]>([
|
||||
{
|
||||
id: '1',
|
||||
type: 'text',
|
||||
data: {
|
||||
text: 'hello'
|
||||
},
|
||||
position: { x: -100, y: -50 }
|
||||
},
|
||||
{
|
||||
id: '1a',
|
||||
type: 'uppercase',
|
||||
data: {},
|
||||
position: { x: 100, y: 0 }
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'text',
|
||||
data: {
|
||||
text: 'world'
|
||||
},
|
||||
position: { x: 0, y: 100 }
|
||||
},
|
||||
|
||||
{
|
||||
id: '3',
|
||||
type: 'result',
|
||||
data: {},
|
||||
position: { x: 300, y: 50 }
|
||||
}
|
||||
]);
|
||||
|
||||
const edges = writable<Edge[]>([
|
||||
{
|
||||
id: 'e1-1a',
|
||||
source: '1',
|
||||
target: '1a'
|
||||
},
|
||||
{
|
||||
id: 'e1a-3',
|
||||
source: '1a',
|
||||
target: '3'
|
||||
},
|
||||
{
|
||||
id: 'e2-3',
|
||||
source: '2',
|
||||
target: '3'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<SvelteFlow {nodes} {edges} {nodeTypes} fitView>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<MiniMap />
|
||||
</SvelteFlow>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Handle,
|
||||
Position,
|
||||
useHandleConnections,
|
||||
useNodesData,
|
||||
type NodeProps
|
||||
} from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: $$Props['id'];
|
||||
|
||||
const connections = useHandleConnections({
|
||||
nodeId: id,
|
||||
type: 'target'
|
||||
});
|
||||
|
||||
$: nodeData = useNodesData($connections.map((connection) => connection.source));
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<div>incoming texts:</div>
|
||||
|
||||
{#each $nodeData as data}
|
||||
<div>{data.text}</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.custom {
|
||||
background-color: #eee;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position, type NodeProps, useSvelteFlow } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: $$Props['id'];
|
||||
export let data: $$Props['data'];
|
||||
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
<div>node {id}</div>
|
||||
<div>
|
||||
<input
|
||||
value={data.text}
|
||||
on:input={(evt) => updateNodeData(id, { text: evt.currentTarget.value })}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.custom {
|
||||
background-color: #eee;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Handle,
|
||||
Position,
|
||||
useHandleConnections,
|
||||
useNodesData,
|
||||
useSvelteFlow,
|
||||
type NodeProps
|
||||
} from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: $$Props['id'];
|
||||
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
const connections = useHandleConnections({
|
||||
nodeId: id,
|
||||
type: 'target'
|
||||
});
|
||||
|
||||
$: nodeData = useNodesData($connections[0]?.source);
|
||||
|
||||
$: {
|
||||
updateNodeData(id, { text: $nodeData?.text?.toUpperCase() || '' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
<Handle type="target" position={Position.Left} isConnectable={$connections.length === 0} />
|
||||
<div>uppercase transform</div>
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.custom {
|
||||
background-color: #eee;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -15,15 +15,9 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<Handle type="target" position={Position.Top} on:connect />
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<button on:click={onClick}>add handle</button>
|
||||
|
||||
{#each Array.from({ length: handleCount }) as handle, i}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id={`${i}`}
|
||||
style={`left: ${i * 10}px;`}
|
||||
on:connect
|
||||
/>
|
||||
<Handle type="source" position={Position.Bottom} id={`${i}`} style={`left: ${i * 10}px;`} />
|
||||
{/each}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
MiniMap,
|
||||
type NodeTypes
|
||||
type NodeTypes,
|
||||
useSvelteFlow,
|
||||
Panel
|
||||
} from '@xyflow/svelte';
|
||||
|
||||
import CustomNode from './CustomNode.svelte';
|
||||
@@ -50,6 +52,12 @@
|
||||
target: '3'
|
||||
}
|
||||
]);
|
||||
|
||||
const { updateNode } = useSvelteFlow();
|
||||
|
||||
const updateNodePosition = () => {
|
||||
updateNode('1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
|
||||
};
|
||||
</script>
|
||||
|
||||
<main>
|
||||
@@ -57,6 +65,8 @@
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<MiniMap />
|
||||
|
||||
<Panel><button on:click={updateNodePosition}>update node</button></Panel>
|
||||
</SvelteFlow>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* 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 { getNodePositionWithOrigin } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
import MiniMapNode from './MiniMapNode';
|
||||
import type { MiniMapNodes, GetMiniMapNodeAttribute } from './types';
|
||||
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
|
||||
|
||||
declare const window: any;
|
||||
|
||||
const selector = (s: ReactFlowState) => s.nodeOrigin;
|
||||
const selectorNodes = (s: ReactFlowState) =>
|
||||
s.nodes.filter(
|
||||
(node) => !node.hidden && (node.computed?.width || node.width) && (node.computed?.height || node.height)
|
||||
);
|
||||
const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id);
|
||||
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
|
||||
|
||||
function MiniMapNodes({
|
||||
@@ -28,8 +25,8 @@ function MiniMapNodes({
|
||||
// a component properly.
|
||||
nodeComponent: NodeComponent = MiniMapNode,
|
||||
onClick,
|
||||
}: MiniMapNodes) {
|
||||
const nodes = useStore(selectorNodes, shallow);
|
||||
}: MiniMapNodesProps) {
|
||||
const nodeIds = useStore(selectorNodeIds, shallow);
|
||||
const nodeOrigin = useStore(selector);
|
||||
const nodeColorFunc = getAttrFunction(nodeColor);
|
||||
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
||||
@@ -39,33 +36,78 @@ function MiniMapNodes({
|
||||
|
||||
return (
|
||||
<>
|
||||
{nodes.map((node) => {
|
||||
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
||||
const color = nodeColor === undefined ? undefined : nodeColorFunc(node);
|
||||
const strokeColor = nodeStrokeColor === undefined ? undefined : nodeStrokeColorFunc(node);
|
||||
|
||||
return (
|
||||
<NodeComponent
|
||||
key={node.id}
|
||||
x={x}
|
||||
y={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={color}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={strokeColor}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
shapeRendering={shapeRendering}
|
||||
onClick={onClick}
|
||||
id={node.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{nodeIds.map((nodeId) => (
|
||||
// The split of responsibilities between MiniMapNodes and
|
||||
// NodeComponentWrapper may appear weird. However, it’s designed to
|
||||
// minimize the cost of updates when individual nodes change.
|
||||
//
|
||||
// For more details, see a similar commit in `NodeRenderer/index.tsx`.
|
||||
<NodeComponentWrapper
|
||||
key={nodeId}
|
||||
id={nodeId}
|
||||
nodeOrigin={nodeOrigin}
|
||||
nodeColorFunc={nodeColorFunc}
|
||||
nodeStrokeColorFunc={nodeStrokeColorFunc}
|
||||
nodeClassNameFunc={nodeClassNameFunc}
|
||||
nodeBorderRadius={nodeBorderRadius}
|
||||
nodeStrokeWidth={nodeStrokeWidth}
|
||||
NodeComponent={NodeComponent}
|
||||
onClick={onClick}
|
||||
shapeRendering={shapeRendering}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -9,16 +9,20 @@ import { useNodeId } from '../../contexts/NodeIdContext';
|
||||
import NodeToolbarPortal from './NodeToolbarPortal';
|
||||
import { NodeToolbarProps } from './types';
|
||||
|
||||
const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) =>
|
||||
a?.computed?.positionAbsolute?.x === b?.computed?.positionAbsolute?.x &&
|
||||
a?.computed?.positionAbsolute?.y === b?.computed?.positionAbsolute?.y &&
|
||||
a?.width === b?.width &&
|
||||
a?.height === b?.height &&
|
||||
a?.selected === b?.selected &&
|
||||
a?.[internalsSymbol]?.z === b?.[internalsSymbol]?.z;
|
||||
const nodeEqualityFn = (a?: Node, b?: Node) =>
|
||||
a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x ||
|
||||
a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y ||
|
||||
a?.computed?.width !== b?.computed?.width ||
|
||||
a?.computed?.height !== b?.computed?.height ||
|
||||
a?.selected !== b?.selected ||
|
||||
a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z;
|
||||
|
||||
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) => ({
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -171,7 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
lib,
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
if (isValid && connection) {
|
||||
onConnectExtended(connection);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
};
|
||||
@@ -1,22 +1,9 @@
|
||||
import type { MouseEvent, RefObject } from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
import type { StoreApi } from 'zustand';
|
||||
|
||||
import type { Node, ReactFlowState } from '../../types';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
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
|
||||
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
|
||||
// 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 useDrag from '../../hooks/useDrag';
|
||||
import { arrowKeyDiffs } from '../Nodes/wrapNode';
|
||||
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
|
||||
import type { Node, ReactFlowState } from '../../types';
|
||||
import { arrowKeyDiffs } from '../NodeWrapper/utils';
|
||||
|
||||
export type NodesSelectionProps = {
|
||||
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
|
||||
@@ -3,67 +3,74 @@
|
||||
* 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`).
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { StoreApi } from 'zustand';
|
||||
import { useEffect, useRef } from 'react';
|
||||
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 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<
|
||||
ReactFlowProps,
|
||||
| 'nodes'
|
||||
| 'edges'
|
||||
| 'defaultNodes'
|
||||
| 'defaultEdges'
|
||||
| 'onConnect'
|
||||
| 'onConnectStart'
|
||||
| 'onConnectEnd'
|
||||
| 'onClickConnectStart'
|
||||
| 'onClickConnectEnd'
|
||||
| 'nodesDraggable'
|
||||
| 'nodesConnectable'
|
||||
| 'nodesFocusable'
|
||||
| 'edgesFocusable'
|
||||
| 'edgesUpdatable'
|
||||
| 'minZoom'
|
||||
| 'maxZoom'
|
||||
| 'nodeExtent'
|
||||
| 'onNodesChange'
|
||||
| 'onEdgesChange'
|
||||
| 'elementsSelectable'
|
||||
| 'connectionMode'
|
||||
| 'snapToGrid'
|
||||
| 'snapGrid'
|
||||
| 'translateExtent'
|
||||
| 'connectOnClick'
|
||||
| 'defaultEdgeOptions'
|
||||
| 'fitView'
|
||||
| 'fitViewOptions'
|
||||
| 'onNodesDelete'
|
||||
| 'onEdgesDelete'
|
||||
| 'onDelete'
|
||||
| 'onNodeDragStart'
|
||||
| 'onNodeDrag'
|
||||
| 'onNodeDragStop'
|
||||
| 'onSelectionDragStart'
|
||||
| 'onSelectionDrag'
|
||||
| 'onSelectionDragStop'
|
||||
| 'onMove'
|
||||
| 'onMoveStart'
|
||||
| 'onMoveEnd'
|
||||
| 'noPanClassName'
|
||||
| 'nodeOrigin'
|
||||
| 'elevateNodesOnSelect'
|
||||
| 'autoPanOnConnect'
|
||||
| 'autoPanOnNodeDrag'
|
||||
| 'onError'
|
||||
| 'connectionRadius'
|
||||
| 'isValidConnection'
|
||||
| 'selectNodesOnDrag'
|
||||
| 'nodeDragThreshold'
|
||||
> & { rfId: string };
|
||||
// these fields exist in the global store and we need to keep them up to date
|
||||
const reactFlowFieldsToTrack = [
|
||||
'nodes',
|
||||
'edges',
|
||||
'defaultNodes',
|
||||
'defaultEdges',
|
||||
'onConnect',
|
||||
'onConnectStart',
|
||||
'onConnectEnd',
|
||||
'onClickConnectStart',
|
||||
'onClickConnectEnd',
|
||||
'nodesDraggable',
|
||||
'nodesConnectable',
|
||||
'nodesFocusable',
|
||||
'edgesFocusable',
|
||||
'edgesUpdatable',
|
||||
'elevateNodesOnSelect',
|
||||
'elevateEdgesOnSelect',
|
||||
'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',
|
||||
'autoPanOnConnect',
|
||||
'autoPanOnNodeDrag',
|
||||
'onError',
|
||||
'connectionRadius',
|
||||
'isValidConnection',
|
||||
'selectNodesOnDrag',
|
||||
'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) => ({
|
||||
setNodes: s.setNodes,
|
||||
@@ -76,80 +83,7 @@ const selector = (s: ReactFlowState) => ({
|
||||
reset: s.reset,
|
||||
});
|
||||
|
||||
function useStoreUpdater<T>(value: T | undefined, setStoreAction: (param: T) => void) {
|
||||
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 StoreUpdater = (props: StoreUpdaterProps) => {
|
||||
const {
|
||||
setNodes,
|
||||
setEdges,
|
||||
@@ -163,64 +97,55 @@ const StoreUpdater = ({
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
const edgesWithDefaults = defaultEdges?.map((e) => ({ ...e, ...defaultEdgeOptions }));
|
||||
setDefaultNodesAndEdges(defaultNodes, edgesWithDefaults);
|
||||
const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions }));
|
||||
setDefaultNodesAndEdges(props.defaultNodes, edgesWithDefaults);
|
||||
|
||||
return () => {
|
||||
reset();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState);
|
||||
useDirectStoreUpdater('connectionMode', connectionMode, store.setState);
|
||||
useDirectStoreUpdater('onConnect', onConnect, store.setState);
|
||||
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState);
|
||||
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState);
|
||||
useDirectStoreUpdater('onClickConnectStart', onClickConnectStart, store.setState);
|
||||
useDirectStoreUpdater('onClickConnectEnd', onClickConnectEnd, store.setState);
|
||||
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
|
||||
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
|
||||
useDirectStoreUpdater('nodesFocusable', nodesFocusable, store.setState);
|
||||
useDirectStoreUpdater('edgesFocusable', edgesFocusable, store.setState);
|
||||
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);
|
||||
const previousFields = useRef<Partial<StoreUpdaterProps>>({
|
||||
// these are values that are also passed directly to other components
|
||||
// than the StoreUpdater. We can reduce the number of setStore calls
|
||||
// by setting the same values here as prev fields.
|
||||
translateExtent: infiniteExtent,
|
||||
nodeOrigin: initNodeOrigin,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 2,
|
||||
elementsSelectable: true,
|
||||
noPanClassName: 'nopan',
|
||||
rfId: '1',
|
||||
});
|
||||
|
||||
useStoreUpdater<Node[]>(nodes, setNodes);
|
||||
useStoreUpdater<Edge[]>(edges, setEdges);
|
||||
useStoreUpdater<number>(minZoom, setMinZoom);
|
||||
useStoreUpdater<number>(maxZoom, setMaxZoom);
|
||||
useStoreUpdater<CoordinateExtent>(translateExtent, setTranslateExtent);
|
||||
useStoreUpdater<CoordinateExtent>(nodeExtent, setNodeExtent);
|
||||
useEffect(
|
||||
() => {
|
||||
for (const fieldName of fieldsToTrack) {
|
||||
const fieldValue = props[fieldName];
|
||||
const previousFieldValue = previousFields.current[fieldName];
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -51,32 +51,38 @@ const markerSelector =
|
||||
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 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
|
||||
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
|
||||
const markers = useStore(
|
||||
useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]),
|
||||
// the id includes all marker options, so we just need to look at that part of the marker
|
||||
(a, b) => !(a.length !== b.length || a.some((m, i) => m.id !== b[i].id))
|
||||
);
|
||||
const markers = useStore(useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]), markersEqual);
|
||||
|
||||
if (!markers.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<defs>
|
||||
{markers.map((marker: MarkerProps) => (
|
||||
<Marker
|
||||
id={marker.id}
|
||||
key={marker.id}
|
||||
type={marker.type}
|
||||
color={marker.color}
|
||||
width={marker.width}
|
||||
height={marker.height}
|
||||
markerUnits={marker.markerUnits}
|
||||
strokeWidth={marker.strokeWidth}
|
||||
orient={marker.orient}
|
||||
/>
|
||||
))}
|
||||
</defs>
|
||||
<svg className="react-flow__marker">
|
||||
<defs>
|
||||
{markers.map((marker: MarkerProps) => (
|
||||
<Marker
|
||||
id={marker.id}
|
||||
key={marker.id}
|
||||
type={marker.type}
|
||||
color={marker.color}
|
||||
width={marker.width}
|
||||
height={marker.height}
|
||||
markerUnits={marker.markerUnits}
|
||||
strokeWidth={marker.strokeWidth}
|
||||
orient={marker.orient}
|
||||
/>
|
||||
))}
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { memo, ReactNode } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import { errorMessages } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import useVisibleEdges from '../../hooks/useVisibleEdges';
|
||||
import useVisibleEdgeIds from '../../hooks/useVisibleEdgeIds';
|
||||
import MarkerDefinitions from './MarkerDefinitions';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import type { EdgeTypesWrapped, ReactFlowState } from '../../types';
|
||||
import EdgeWrapper from '../../components/EdgeWrapper';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
|
||||
type EdgeRendererProps = Pick<
|
||||
GraphViewProps,
|
||||
@@ -24,12 +23,10 @@ type EdgeRendererProps = Pick<
|
||||
| 'onEdgeUpdateEnd'
|
||||
| 'edgeUpdaterRadius'
|
||||
| 'noPanClassName'
|
||||
| 'elevateEdgesOnSelect'
|
||||
| 'rfId'
|
||||
| 'disableKeyboardA11y'
|
||||
| 'edgeTypes'
|
||||
> & {
|
||||
edgeTypes: EdgeTypesWrapped;
|
||||
elevateEdgesOnSelect: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -46,7 +43,6 @@ const selector = (s: ReactFlowState) => ({
|
||||
const EdgeRenderer = ({
|
||||
defaultMarkerColor,
|
||||
onlyRenderVisibleElements,
|
||||
elevateEdgesOnSelect,
|
||||
rfId,
|
||||
edgeTypes,
|
||||
noPanClassName,
|
||||
@@ -63,82 +59,39 @@ const EdgeRenderer = ({
|
||||
children,
|
||||
}: EdgeRendererProps) => {
|
||||
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
|
||||
// each zIndex gets its own svg element
|
||||
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
|
||||
const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements);
|
||||
|
||||
return (
|
||||
<>
|
||||
{edgeTree.map(({ level, edges, isMaxLevel }) => (
|
||||
<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';
|
||||
<div className="react-flow__edges">
|
||||
<MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />
|
||||
|
||||
if (!edgeTypes[edgeType]) {
|
||||
onError?.('011', errorMessages['error011'](edgeType));
|
||||
edgeType = 'default';
|
||||
}
|
||||
|
||||
const EdgeComponent = edgeTypes[edgeType];
|
||||
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')
|
||||
);
|
||||
|
||||
return (
|
||||
<EdgeComponent
|
||||
key={edge.id}
|
||||
id={edge.id}
|
||||
className={cc([edge.className, noPanClassName])}
|
||||
type={edge.type}
|
||||
data={edge.data}
|
||||
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>
|
||||
))}
|
||||
{edgeIds.map((id) => {
|
||||
return (
|
||||
<EdgeWrapper
|
||||
key={id}
|
||||
id={id}
|
||||
edgesFocusable={edgesFocusable}
|
||||
edgesUpdatable={edgesUpdatable}
|
||||
elementsSelectable={elementsSelectable}
|
||||
noPanClassName={noPanClassName}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onContextMenu={onEdgeContextMenu}
|
||||
onMouseEnter={onEdgeMouseEnter}
|
||||
onMouseMove={onEdgeMouseMove}
|
||||
onMouseLeave={onEdgeMouseLeave}
|
||||
onClick={onEdgeClick}
|
||||
edgeUpdaterRadius={edgeUpdaterRadius}
|
||||
onDoubleClick={onEdgeDoubleClick}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
rfId={rfId}
|
||||
onError={onError}
|
||||
edgeTypes={edgeTypes}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{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 ConnectionLine from '../../components/ConnectionLine';
|
||||
import type { ReactFlowProps } from '../../types';
|
||||
import { createNodeTypes } from '../NodeRenderer/utils';
|
||||
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
||||
import { useNodeOrEdgeTypes } from './utils';
|
||||
import useNodeOrEdgeTypesWarning from './useNodeOrEdgeTypesWarning';
|
||||
|
||||
export type GraphViewProps = Omit<
|
||||
ReactFlowProps,
|
||||
'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
|
||||
'onSelectionChange' | 'nodes' | 'edges' | 'onMove' | 'onMoveStart' | 'onMoveEnd' | 'elevateEdgesOnSelect'
|
||||
> &
|
||||
Required<
|
||||
Pick<
|
||||
ReactFlowProps,
|
||||
| 'nodeTypes'
|
||||
| 'edgeTypes'
|
||||
| 'selectionKeyCode'
|
||||
| 'deleteKeyCode'
|
||||
| 'multiSelectionKeyCode'
|
||||
@@ -100,7 +96,6 @@ const GraphView = ({
|
||||
noDragClassName,
|
||||
noWheelClassName,
|
||||
noPanClassName,
|
||||
elevateEdgesOnSelect,
|
||||
disableKeyboardA11y,
|
||||
nodeOrigin,
|
||||
nodeExtent,
|
||||
@@ -108,8 +103,8 @@ const GraphView = ({
|
||||
viewport,
|
||||
onViewportChange,
|
||||
}: GraphViewProps) => {
|
||||
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
|
||||
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
|
||||
useNodeOrEdgeTypesWarning(nodeTypes);
|
||||
useNodeOrEdgeTypesWarning(edgeTypes);
|
||||
|
||||
useOnInitHandler(onInit);
|
||||
useViewportSync(viewport);
|
||||
@@ -154,7 +149,7 @@ const GraphView = ({
|
||||
>
|
||||
<ViewportWrapper>
|
||||
<EdgeRenderer
|
||||
edgeTypes={edgeTypesWrapped}
|
||||
edgeTypes={edgeTypes}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
@@ -168,7 +163,6 @@ const GraphView = ({
|
||||
edgeUpdaterRadius={edgeUpdaterRadius}
|
||||
defaultMarkerColor={defaultMarkerColor}
|
||||
noPanClassName={noPanClassName}
|
||||
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
|
||||
disableKeyboardA11y={disableKeyboardA11y}
|
||||
rfId={rfId}
|
||||
>
|
||||
@@ -182,7 +176,7 @@ const GraphView = ({
|
||||
<div className="react-flow__edgelabel-renderer" />
|
||||
|
||||
<NodeRenderer
|
||||
nodeTypes={nodeTypesWrapped}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
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 { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system';
|
||||
|
||||
import useVisibleNodes from '../../hooks/useVisibleNodes';
|
||||
import useVisibleNodesIds from '../../hooks/useVisibleNodeIds';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
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,
|
||||
| 'onNodeClick'
|
||||
| 'onNodeDoubleClick'
|
||||
@@ -23,127 +24,71 @@ type NodeRendererProps = Pick<
|
||||
| 'disableKeyboardA11y'
|
||||
| 'nodeOrigin'
|
||||
| 'nodeExtent'
|
||||
> & {
|
||||
nodeTypes: NodeTypesWrapped;
|
||||
};
|
||||
| 'nodeTypes'
|
||||
>;
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
nodesDraggable: s.nodesDraggable,
|
||||
nodesConnectable: s.nodesConnectable,
|
||||
nodesFocusable: s.nodesFocusable,
|
||||
elementsSelectable: s.elementsSelectable,
|
||||
updateNodeDimensions: s.updateNodeDimensions,
|
||||
onError: s.onError,
|
||||
});
|
||||
|
||||
const NodeRenderer = (props: NodeRendererProps) => {
|
||||
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
|
||||
useStore(selector, shallow);
|
||||
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
|
||||
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();
|
||||
};
|
||||
}, []);
|
||||
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow);
|
||||
const nodeIds = useVisibleNodesIds(props.onlyRenderVisibleElements);
|
||||
const resizeObserver = useResizeObserver();
|
||||
|
||||
return (
|
||||
<div className="react-flow__nodes" style={containerStyle}>
|
||||
{nodes.map((node) => {
|
||||
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);
|
||||
|
||||
{nodeIds.map((nodeId) => {
|
||||
return (
|
||||
<NodeComponent
|
||||
key={node.id}
|
||||
id={node.id}
|
||||
className={node.className}
|
||||
style={node.style}
|
||||
width={node.width ?? undefined}
|
||||
height={node.height ?? undefined}
|
||||
type={nodeType}
|
||||
data={node.data}
|
||||
sourcePosition={node.sourcePosition || Position.Bottom}
|
||||
targetPosition={node.targetPosition || Position.Top}
|
||||
hidden={node.hidden}
|
||||
xPos={posX}
|
||||
yPos={posY}
|
||||
xPosOrigin={posOrigin.x}
|
||||
yPosOrigin={posOrigin.y}
|
||||
positionAbsolute={clampedPosition || { x: 0, y: 0 }}
|
||||
// The split of responsibilities between NodeRenderer and
|
||||
// NodeComponentWrapper may appear weird. However, it’s designed to
|
||||
// minimize the cost of updates when individual nodes change.
|
||||
//
|
||||
// For example, when you’re dragging a single node, that node gets
|
||||
// updated multiple times per second. If `NodeRenderer` were to update
|
||||
// every time, it would have to re-run the `nodes.map()` loop every
|
||||
// time. This gets pricey with hundreds of nodes, especially if every
|
||||
// loop cycle does more than just rendering a JSX element!
|
||||
//
|
||||
// As a result of this choice, we took the following implementation
|
||||
// decisions:
|
||||
// - NodeRenderer subscribes *only* to node IDs – and therefore
|
||||
// rerender *only* when visible nodes are added or removed.
|
||||
// - NodeRenderer performs all operations the result of which can be
|
||||
// shared between nodes (such as creating the `ResizeObserver`
|
||||
// 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 you’d 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}
|
||||
onMouseEnter={props.onNodeMouseEnter}
|
||||
onMouseMove={props.onNodeMouseMove}
|
||||
onMouseLeave={props.onNodeMouseLeave}
|
||||
onContextMenu={props.onNodeContextMenu}
|
||||
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}
|
||||
noPanClassName={props.noPanClassName}
|
||||
initialized={initialized}
|
||||
rfId={props.rfId}
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system'
|
||||
import UserSelection from '../../components/UserSelection';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { getSelectionChanges, getConnectedEdges } from '../../utils';
|
||||
import { getSelectionChanges } from '../../utils';
|
||||
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
|
||||
|
||||
type PaneProps = {
|
||||
@@ -156,19 +156,30 @@ const Pane = memo(
|
||||
true,
|
||||
nodeOrigin
|
||||
);
|
||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
|
||||
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
||||
|
||||
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
|
||||
prevSelectedNodesCount.current = selectedNodeIds.length;
|
||||
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
|
||||
const selectedEdgeIds = new Set<string>();
|
||||
const selectedNodeIds = new Set<string>();
|
||||
|
||||
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) {
|
||||
onNodesChange?.(changes);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
|
||||
prevSelectedEdgesCount.current = selectedEdgeIds.length;
|
||||
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
|
||||
prevSelectedEdgesCount.current = selectedEdgeIds.size;
|
||||
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
|
||||
if (changes.length) {
|
||||
onEdgesChange?.(changes);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { forwardRef, type CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
PanOnScrollMode,
|
||||
SelectionMode,
|
||||
infiniteExtent,
|
||||
@@ -12,42 +11,16 @@ import {
|
||||
} from '@xyflow/system';
|
||||
|
||||
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 StoreUpdater from '../../components/StoreUpdater';
|
||||
import A11yDescriptions from '../../components/A11yDescriptions';
|
||||
import GraphView from '../GraphView';
|
||||
import Wrapper from './Wrapper';
|
||||
import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types';
|
||||
import type { ReactFlowProps, ReactFlowRefType } from '../../types';
|
||||
import useColorModeClass from '../../hooks/useColorModeClass';
|
||||
|
||||
const defaultNodeTypes: NodeTypes = {
|
||||
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];
|
||||
export const initNodeOrigin: NodeOrigin = [0, 0];
|
||||
const initDefaultViewport: Viewport = { x: 0, y: 0, zoom: 1 };
|
||||
|
||||
const wrapperStyle: CSSProperties = {
|
||||
@@ -66,8 +39,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
defaultNodes,
|
||||
defaultEdges,
|
||||
className,
|
||||
nodeTypes = defaultNodeTypes,
|
||||
edgeTypes = defaultEdgeTypes,
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
onNodeClick,
|
||||
onEdgeClick,
|
||||
onInit,
|
||||
@@ -97,7 +70,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
onSelectionContextMenu,
|
||||
onSelectionStart,
|
||||
onSelectionEnd,
|
||||
connectionMode = ConnectionMode.Strict,
|
||||
connectionMode,
|
||||
connectionLineType = ConnectionLineType.Bezier,
|
||||
connectionLineStyle,
|
||||
connectionLineComponent,
|
||||
@@ -109,10 +82,10 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
panActivationKeyCode = 'Space',
|
||||
multiSelectionKeyCode = isMacOs() ? 'Meta' : 'Control',
|
||||
zoomActivationKeyCode = isMacOs() ? 'Meta' : 'Control',
|
||||
snapToGrid = false,
|
||||
snapGrid = initSnapGrid,
|
||||
snapToGrid,
|
||||
snapGrid,
|
||||
onlyRenderVisibleElements = false,
|
||||
selectNodesOnDrag = true,
|
||||
selectNodesOnDrag,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
nodesFocusable,
|
||||
@@ -155,18 +128,18 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
noDragClassName = 'nodrag',
|
||||
noWheelClassName = 'nowheel',
|
||||
noPanClassName = 'nopan',
|
||||
fitView = false,
|
||||
fitView,
|
||||
fitViewOptions,
|
||||
connectOnClick = true,
|
||||
connectOnClick,
|
||||
attributionPosition,
|
||||
proOptions,
|
||||
defaultEdgeOptions,
|
||||
elevateNodesOnSelect = true,
|
||||
elevateEdgesOnSelect = false,
|
||||
elevateNodesOnSelect,
|
||||
elevateEdgesOnSelect,
|
||||
disableKeyboardA11y = false,
|
||||
autoPanOnConnect = true,
|
||||
autoPanOnNodeDrag = true,
|
||||
connectionRadius = 20,
|
||||
autoPanOnConnect,
|
||||
autoPanOnNodeDrag,
|
||||
connectionRadius,
|
||||
isValidConnection,
|
||||
onError,
|
||||
style,
|
||||
@@ -251,7 +224,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
noDragClassName={noDragClassName}
|
||||
noWheelClassName={noWheelClassName}
|
||||
noPanClassName={noPanClassName}
|
||||
elevateEdgesOnSelect={elevateEdgesOnSelect}
|
||||
rfId={rfId}
|
||||
disableKeyboardA11y={disableKeyboardA11y}
|
||||
nodeOrigin={nodeOrigin}
|
||||
@@ -276,6 +248,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
edgesUpdatable={edgesUpdatable}
|
||||
elementsSelectable={elementsSelectable}
|
||||
elevateNodesOnSelect={elevateNodesOnSelect}
|
||||
elevateEdgesOnSelect={elevateEdgesOnSelect}
|
||||
minZoom={minZoom}
|
||||
maxZoom={maxZoom}
|
||||
nodeExtent={nodeExtent}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { Connection, HandleType, areConnectionMapsEqual, handleConnectionChange } from '@xyflow/system';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import { useNodeId } from '../contexts/NodeIdContext';
|
||||
|
||||
type useHandleConnectionsParams = {
|
||||
type: HandleType;
|
||||
id?: string | null;
|
||||
nodeId?: string;
|
||||
onConnect?: (connections: Connection[]) => void;
|
||||
onDisconnect?: (connections: Connection[]) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
|
||||
*
|
||||
* @public
|
||||
* @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.onConnect - gets called when a connection is established
|
||||
* @param param.onDisconnect - gets called when a connection is removed
|
||||
* @returns an array with connections
|
||||
*/
|
||||
export function useHandleConnections({
|
||||
type,
|
||||
id = null,
|
||||
nodeId,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
}: useHandleConnectionsParams): Connection[] {
|
||||
const _nodeId = useNodeId();
|
||||
const prevConnections = useRef<Map<string, Connection> | null>(null);
|
||||
const currentNodeId = nodeId || _nodeId;
|
||||
|
||||
const connections = useStore(
|
||||
(state) => state.connectionLookup.get(`${currentNodeId}-${type}-${id}`),
|
||||
areConnectionMapsEqual
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts
|
||||
if (prevConnections.current && prevConnections.current !== connections) {
|
||||
const _connections = connections ?? new Map();
|
||||
handleConnectionChange(prevConnections.current, _connections, onDisconnect);
|
||||
handleConnectionChange(_connections, prevConnections.current, onConnect);
|
||||
}
|
||||
|
||||
prevConnections.current = connections ?? new Map();
|
||||
}, [connections, onConnect, onDisconnect]);
|
||||
|
||||
return useMemo(() => Array.from(connections?.values() ?? []), [connections]);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useCallback } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../hooks/useStore';
|
||||
import type { Node } from '../types';
|
||||
|
||||
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[],
|
||||
guard: (node: Node) => node is NodeType
|
||||
): NodeType['data'][];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useNodesData(nodeIds: any): any {
|
||||
const nodesData = useStore(
|
||||
useCallback(
|
||||
(s) => {
|
||||
if (!Array.isArray(nodeIds)) {
|
||||
return s.nodeLookup.get(nodeIds)?.data || null;
|
||||
}
|
||||
|
||||
const data = [];
|
||||
|
||||
for (const nodeId of nodeIds) {
|
||||
const nodeData = s.nodeLookup.get(nodeId)?.data;
|
||||
|
||||
if (nodeData) {
|
||||
data.push(nodeData);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
[nodeIds]
|
||||
),
|
||||
shallow
|
||||
);
|
||||
|
||||
return nodesData;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@xyflow/system';
|
||||
|
||||
import useViewportHelper from './useViewportHelper';
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
import { useStoreApi } from './useStore';
|
||||
import type {
|
||||
ReactFlowInstance,
|
||||
Instance,
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
Node,
|
||||
Edge,
|
||||
} from '../types';
|
||||
import { isNode } from '../utils';
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export default function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): ReactFlowInstance<
|
||||
@@ -272,6 +273,36 @@ export default function useReactFlow<NodeType extends Node = Node, EdgeType exte
|
||||
return getOutgoersBase(node, nodes, edges);
|
||||
}, []);
|
||||
|
||||
const updateNode = useCallback<Instance.UpdateNode>(
|
||||
(id, nodeUpdate, options = { replace: true }) => {
|
||||
setNodes((prevNodes) =>
|
||||
prevNodes.map((node) => {
|
||||
if (node.id === id) {
|
||||
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
|
||||
return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode };
|
||||
}
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
const updateNodeData = useCallback<Instance.UpdateNodeData>(
|
||||
(id, dataUpdate, options = { replace: false }) => {
|
||||
updateNode(
|
||||
id,
|
||||
(node) => {
|
||||
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
|
||||
return options.replace ? { ...node, data: nextData } : { ...node, data: { ...node.data, ...nextData } };
|
||||
},
|
||||
options
|
||||
);
|
||||
},
|
||||
[updateNode]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...viewportHelper,
|
||||
@@ -290,6 +321,8 @@ export default function useReactFlow<NodeType extends Node = Node, EdgeType exte
|
||||
getConnectedEdges,
|
||||
getIncomers,
|
||||
getOutgoers,
|
||||
updateNode,
|
||||
updateNodeData,
|
||||
};
|
||||
}, [
|
||||
viewportHelper,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useCallback } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { isEdgeVisible } from '@xyflow/system';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import { type ReactFlowState } from '../types';
|
||||
|
||||
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,22 @@
|
||||
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());
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -1,5 +1,5 @@
|
||||
export { default as ReactFlow } from './container/ReactFlow';
|
||||
export { default as Handle } from './components/Handle';
|
||||
export { default as Handle, type HandleComponentProps } from './components/Handle';
|
||||
export { default as EdgeText } from './components/Edges/EdgeText';
|
||||
export { StraightEdge } from './components/Edges/StraightEdge';
|
||||
export { StepEdge } from './components/Edges/StepEdge';
|
||||
@@ -22,6 +22,8 @@ export { useStore, useStoreApi } from './hooks/useStore';
|
||||
export { default as useOnViewportChange, type UseOnViewportChangeOptions } from './hooks/useOnViewportChange';
|
||||
export { default as useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange';
|
||||
export { default as useNodesInitialized, type UseNodesInitializedOptions } from './hooks/useNodesInitialized';
|
||||
export { useHandleConnections } from './hooks/useHandleConnections';
|
||||
export { useNodesData } from './hooks/useNodesData';
|
||||
export { useNodeId } from './contexts/NodeIdContext';
|
||||
|
||||
export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes';
|
||||
@@ -75,6 +77,7 @@ export {
|
||||
type CoordinateExtent,
|
||||
type ColorMode,
|
||||
type ColorModeClass,
|
||||
type HandleType,
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -2,11 +2,12 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
||||
import {
|
||||
clampPosition,
|
||||
fitView as fitViewSystem,
|
||||
updateNodes,
|
||||
adoptUserProvidedNodes,
|
||||
updateAbsolutePositions,
|
||||
panBy as panBySystem,
|
||||
Dimensions,
|
||||
updateNodeDimensions as updateNodeDimensionsSystem,
|
||||
updateConnectionLookup,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
||||
@@ -42,15 +43,23 @@ const createRFStore = ({
|
||||
...getInitialState({ nodes, edges, width, height, fitView }),
|
||||
setNodes: (nodes: Node[]) => {
|
||||
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
// Whenver new nodes are set, we need to calculate the absolute positions of the nodes
|
||||
// and update the nodeLookup.
|
||||
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||
// setNodes() is called exclusively in response to user actions:
|
||||
// - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
|
||||
// - 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[]) => {
|
||||
const { defaultEdgeOptions = {} } = get();
|
||||
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
|
||||
const { connectionLookup, edgeLookup } = get();
|
||||
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, edges);
|
||||
|
||||
set({ edges });
|
||||
},
|
||||
// when the user works with an uncontrolled flow,
|
||||
// we set a flag `hasDefaultNodes` / `hasDefaultEdges`
|
||||
@@ -69,12 +78,16 @@ const createRFStore = ({
|
||||
};
|
||||
|
||||
if (hasDefaultNodes) {
|
||||
nextState.nodes = updateNodes(nodes, new Map(), {
|
||||
nodeOrigin: get().nodeOrigin,
|
||||
elevateNodesOnSelect: get().elevateNodesOnSelect,
|
||||
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||
nodeOrigin,
|
||||
elevateNodesOnSelect,
|
||||
});
|
||||
}
|
||||
if (hasDefaultEdges) {
|
||||
const { connectionLookup, edgeLookup } = get();
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, edges);
|
||||
|
||||
nextState.edges = edges;
|
||||
}
|
||||
|
||||
@@ -163,7 +176,7 @@ const createRFStore = ({
|
||||
if (changes?.length) {
|
||||
if (hasDefaultNodes) {
|
||||
const updatedNodes = applyNodeChanges(changes, nodes);
|
||||
const nextNodes = updateNodes(updatedNodes, nodeLookup, {
|
||||
const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, {
|
||||
nodeOrigin,
|
||||
elevateNodesOnSelect,
|
||||
});
|
||||
@@ -182,8 +195,8 @@ const createRFStore = ({
|
||||
if (multiSelectionActive) {
|
||||
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[];
|
||||
} else {
|
||||
changedNodes = getSelectionChanges(nodes, selectedNodeIds);
|
||||
changedEdges = getSelectionChanges(edges, []);
|
||||
changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true);
|
||||
changedEdges = getSelectionChanges(edges);
|
||||
}
|
||||
|
||||
updateNodesAndEdgesSelections({
|
||||
@@ -201,8 +214,8 @@ const createRFStore = ({
|
||||
if (multiSelectionActive) {
|
||||
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[];
|
||||
} else {
|
||||
changedEdges = getSelectionChanges(edges, selectedEdgeIds);
|
||||
changedNodes = getSelectionChanges(nodes, []);
|
||||
changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds]));
|
||||
changedNodes = getSelectionChanges(nodes, new Set(), true);
|
||||
}
|
||||
|
||||
updateNodesAndEdgesSelections({
|
||||
@@ -326,6 +339,7 @@ const createRFStore = ({
|
||||
|
||||
set(currentConnection);
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
// @todo: what should we do about this? Do we still need it?
|
||||
// if you are on a SPA with multiple flows, we want to make sure that the store gets resetted
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {
|
||||
infiniteExtent,
|
||||
ConnectionMode,
|
||||
updateNodes,
|
||||
adoptUserProvidedNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
Transform,
|
||||
updateConnectionLookup,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { Edge, Node, ReactFlowStore } from '../types';
|
||||
@@ -22,8 +23,15 @@ const getInitialState = ({
|
||||
height?: number;
|
||||
fitView?: boolean;
|
||||
} = {}): ReactFlowStore => {
|
||||
const nodeLookup = new Map<string, Node>();
|
||||
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
|
||||
const nodeLookup = new Map();
|
||||
const connectionLookup = new Map();
|
||||
const edgeLookup = new Map();
|
||||
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, edges);
|
||||
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||
nodeOrigin: [0, 0],
|
||||
elevateNodesOnSelect: false,
|
||||
});
|
||||
|
||||
let transform: Transform = [0, 0, 1];
|
||||
|
||||
@@ -41,7 +49,9 @@ const getInitialState = ({
|
||||
transform,
|
||||
nodes: nextNodes,
|
||||
nodeLookup,
|
||||
edges: edges,
|
||||
edges,
|
||||
edgeLookup,
|
||||
connectionLookup,
|
||||
onNodesChange: null,
|
||||
onEdgesChange: null,
|
||||
hasDefaultNodes: false,
|
||||
@@ -61,7 +71,7 @@ const getInitialState = ({
|
||||
paneDragging: false,
|
||||
noPanClassName: 'nopan',
|
||||
nodeOrigin: [0, 0],
|
||||
nodeDragThreshold: 0,
|
||||
nodeDragThreshold: 1,
|
||||
|
||||
snapGrid: [15, 15],
|
||||
snapToGrid: false,
|
||||
@@ -73,6 +83,7 @@ const getInitialState = ({
|
||||
edgesUpdatable: true,
|
||||
elementsSelectable: true,
|
||||
elevateNodesOnSelect: true,
|
||||
elevateEdgesOnSelect: false,
|
||||
fitViewOnInit: false,
|
||||
fitViewDone: false,
|
||||
fitViewOnInitOptions: undefined,
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import type {
|
||||
CSSProperties,
|
||||
HTMLAttributes,
|
||||
ReactNode,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
ComponentType,
|
||||
MemoExoticComponent,
|
||||
} from 'react';
|
||||
import type { CSSProperties, HTMLAttributes, ReactNode, MouseEvent as ReactMouseEvent, ComponentType } from 'react';
|
||||
import type {
|
||||
EdgeBase,
|
||||
BezierPathOptions,
|
||||
@@ -21,9 +14,10 @@ import type {
|
||||
ConnectionStatus,
|
||||
EdgePosition,
|
||||
StepPathOptions,
|
||||
OnError,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { Node } from '.';
|
||||
import { EdgeTypes, Node } from '.';
|
||||
|
||||
export type EdgeLabelOptions = {
|
||||
label?: string | ReactNode;
|
||||
@@ -66,9 +60,14 @@ export type Edge<T = any> = DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeT
|
||||
|
||||
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;
|
||||
onEdgeDoubleClick?: EdgeMouseHandler;
|
||||
onDoubleClick?: EdgeMouseHandler;
|
||||
sourceHandleId?: string | null;
|
||||
targetHandleId?: string | null;
|
||||
onEdgeUpdate?: OnEdgeUpdateFunc;
|
||||
@@ -80,10 +79,8 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
|
||||
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge, handleType: HandleType) => void;
|
||||
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => void;
|
||||
rfId?: string;
|
||||
isFocusable: boolean;
|
||||
isUpdatable: EdgeUpdatable;
|
||||
isSelectable: boolean;
|
||||
pathOptions?: BezierPathOptions | SmoothStepPathOptions;
|
||||
edgeTypes?: EdgeTypes;
|
||||
onError?: OnError;
|
||||
};
|
||||
|
||||
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
|
||||
@@ -103,13 +100,13 @@ export type EdgeProps<T = any> = Pick<
|
||||
> &
|
||||
EdgePosition &
|
||||
EdgeLabelOptions & {
|
||||
interactionWidth?: number;
|
||||
sourceHandleId?: string | null;
|
||||
targetHandleId?: string | null;
|
||||
markerStart?: string;
|
||||
markerEnd?: string;
|
||||
// @TODO: how can we get better types for pathOptions?
|
||||
pathOptions?: any;
|
||||
interactionWidth?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -186,6 +183,3 @@ export type ConnectionLineComponentProps = {
|
||||
};
|
||||
|
||||
export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>;
|
||||
|
||||
export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> };
|
||||
export type EdgeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapEdgeProps>> };
|
||||
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
SetCenter,
|
||||
FitBounds,
|
||||
XYPosition,
|
||||
NodeProps,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance } from '.';
|
||||
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps } from '.';
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type OnNodesChange = (changes: NodeChange[]) => void;
|
||||
export type OnEdgesChange = (changes: EdgeChange[]) => void;
|
||||
@@ -21,6 +23,9 @@ export type OnNodesDelete = (nodes: Node[]) => void;
|
||||
export type OnEdgesDelete = (edges: Edge[]) => void;
|
||||
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
|
||||
|
||||
export type NodeTypes = { [key: string]: ComponentType<NodeProps> };
|
||||
export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> };
|
||||
|
||||
export type UnselectNodesAndEdgesParams = {
|
||||
nodes?: Node[];
|
||||
edges?: Edge[];
|
||||
|
||||
@@ -48,6 +48,17 @@ export namespace Instance {
|
||||
export type getConnectedEdges = (id: string | (Node | { id: Node['id'] })[]) => Edge[];
|
||||
export type getIncomers = (node: string | Node | { id: Node['id'] }) => Node[];
|
||||
export type getOutgoers = (node: string | Node | { id: Node['id'] }) => Node[];
|
||||
|
||||
export type UpdateNode<NodeType extends Node = Node> = (
|
||||
id: string,
|
||||
dataUpdate: Partial<NodeType> | ((node: NodeType) => Partial<NodeType>),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
export type UpdateNodeData<NodeType extends Node = Node> = (
|
||||
id: string,
|
||||
dataUpdate: object | ((node: NodeType) => object),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
@@ -63,5 +74,7 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
||||
deleteElements: Instance.DeleteElements;
|
||||
getIntersectingNodes: Instance.GetIntersectingNodes<NodeType>;
|
||||
isNodeIntersecting: Instance.IsNodeIntersecting<NodeType>;
|
||||
updateNode: Instance.UpdateNode<NodeType>;
|
||||
updateNodeData: Instance.UpdateNodeData<NodeType>;
|
||||
viewportInitialized: boolean;
|
||||
} & Omit<ViewportHelperFunctions, 'initialized'>;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { CSSProperties, ComponentType, MemoExoticComponent, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { NodeBase, NodeProps, XYPosition } from '@xyflow/system';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
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
|
||||
@@ -13,29 +15,19 @@ export type Node<NodeData = any, NodeType extends string | undefined = string |
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
resizing?: boolean;
|
||||
focusable?: boolean;
|
||||
};
|
||||
|
||||
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
|
||||
export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
|
||||
export type SelectionDragHandler = (event: ReactMouseEvent, nodes: Node[]) => void;
|
||||
|
||||
export type WrapNodeProps<NodeType extends Node = Node> = Pick<
|
||||
NodeType,
|
||||
'id' | 'data' | 'style' | 'className' | 'dragHandle' | 'sourcePosition' | 'targetPosition' | 'hidden' | 'ariaLabel'
|
||||
> & {
|
||||
type: string;
|
||||
selected: boolean;
|
||||
zIndex: number;
|
||||
isConnectable: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
xPosOrigin: number;
|
||||
yPosOrigin: number;
|
||||
positionAbsolute: XYPosition;
|
||||
initialized: boolean;
|
||||
isSelectable: boolean;
|
||||
isDraggable: boolean;
|
||||
isFocusable: boolean;
|
||||
export type NodeWrapperProps = {
|
||||
id: string;
|
||||
nodesConnectable: boolean;
|
||||
elementsSelectable: boolean;
|
||||
nodesDraggable: boolean;
|
||||
nodesFocusable: boolean;
|
||||
onClick?: NodeMouseHandler;
|
||||
onDoubleClick?: NodeMouseHandler;
|
||||
onMouseEnter?: NodeMouseHandler;
|
||||
@@ -43,14 +35,12 @@ export type WrapNodeProps<NodeType extends Node = Node> = Pick<
|
||||
onMouseLeave?: NodeMouseHandler;
|
||||
onContextMenu?: NodeMouseHandler;
|
||||
resizeObserver: ResizeObserver | null;
|
||||
isParent: boolean;
|
||||
noDragClassName: string;
|
||||
noPanClassName: string;
|
||||
rfId: string;
|
||||
disableKeyboardA11y: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
nodeTypes?: NodeTypes;
|
||||
nodeExtent?: CoordinateExtent;
|
||||
nodeOrigin: NodeOrigin;
|
||||
onError?: OnError;
|
||||
};
|
||||
|
||||
export type NodeTypes = { [key: string]: ComponentType<NodeProps> };
|
||||
export type NodeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapNodeProps>> };
|
||||
|
||||
@@ -24,6 +24,9 @@ import {
|
||||
type OnMoveEnd,
|
||||
type IsValidConnection,
|
||||
type UpdateConnection,
|
||||
type EdgeLookup,
|
||||
type ConnectionLookup,
|
||||
type NodeLookup,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -47,8 +50,10 @@ export type ReactFlowStore = {
|
||||
height: number;
|
||||
transform: Transform;
|
||||
nodes: Node[];
|
||||
nodeLookup: Map<string, Node>;
|
||||
nodeLookup: NodeLookup<Node>;
|
||||
edges: Edge[];
|
||||
edgeLookup: EdgeLookup<Edge>;
|
||||
connectionLookup: ConnectionLookup;
|
||||
onNodesChange: OnNodesChange | null;
|
||||
onEdgesChange: OnEdgesChange | null;
|
||||
hasDefaultNodes: boolean;
|
||||
@@ -83,6 +88,7 @@ export type ReactFlowStore = {
|
||||
edgesUpdatable: boolean;
|
||||
elementsSelectable: boolean;
|
||||
elevateNodesOnSelect: boolean;
|
||||
elevateEdgesOnSelect: boolean;
|
||||
selectNodesOnDrag: boolean;
|
||||
|
||||
multiSelectionActive: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* 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) {
|
||||
const parent = res.find((e) => e.id === updateItem.parentNode);
|
||||
@@ -55,24 +55,27 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
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 _remainingChanges: any[] = [];
|
||||
|
||||
remainingChanges.forEach((c) => {
|
||||
if (c.id === item.id) {
|
||||
for (const c of remainingChanges) {
|
||||
if (c.type === 'add') {
|
||||
updatedElements.push(c.item);
|
||||
} else if (c.id === item.id) {
|
||||
nextChanges.push(c);
|
||||
} else {
|
||||
_remainingChanges.push(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
remainingChanges = _remainingChanges;
|
||||
|
||||
if (nextChanges.length === 0) {
|
||||
res.push(item);
|
||||
return res;
|
||||
updatedElements.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const updateItem = { ...item };
|
||||
@@ -101,7 +104,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
if (updateItem.expandParent) {
|
||||
handleParentExpand(res, updateItem);
|
||||
handleParentExpand(updatedElements, updateItem);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -123,20 +126,20 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
if (updateItem.expandParent) {
|
||||
handleParentExpand(res, updateItem);
|
||||
handleParentExpand(updatedElements, updateItem);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'remove': {
|
||||
return res;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
updatedElements.push(updateItem);
|
||||
}
|
||||
}
|
||||
|
||||
res.push(updateItem);
|
||||
return res;
|
||||
}, initElements);
|
||||
return updatedElements;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,24 +190,33 @@ export function applyEdgeChanges<EdgeType extends Edge = Edge>(changes: EdgeChan
|
||||
return applyChanges(changes, edges) as EdgeType[];
|
||||
}
|
||||
|
||||
export const createSelectionChange = (id: string, selected: boolean) => ({
|
||||
export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({
|
||||
id,
|
||||
type: 'select',
|
||||
selected,
|
||||
});
|
||||
|
||||
export function getSelectionChanges(items: any[], selectedIds: string[]) {
|
||||
return items.reduce((res, item) => {
|
||||
const willBeSelected = selectedIds.includes(item.id);
|
||||
export function getSelectionChanges(
|
||||
items: any[],
|
||||
selectedIds: Set<string> = new Set(),
|
||||
mutateItem = false
|
||||
): NodeSelectionChange[] | EdgeSelectionChange[] {
|
||||
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
|
||||
|
||||
if (!item.selected && willBeSelected) {
|
||||
item.selected = true;
|
||||
res.push(createSelectionChange(item.id, true));
|
||||
} else if (item.selected && !willBeSelected) {
|
||||
item.selected = false;
|
||||
res.push(createSelectionChange(item.id, false));
|
||||
for (const item of items) {
|
||||
const willBeSelected = selectedIds.has(item.id);
|
||||
|
||||
// we don't want to set all items to selected=false on the first selection
|
||||
if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { Edge, Node } from '../types';
|
||||
* @param element - The element to test
|
||||
* @returns A boolean indicating whether the element is an Node
|
||||
*/
|
||||
export const isNode = isNodeBase<Node, Edge>;
|
||||
export const isNode = isNodeBase<Node>;
|
||||
|
||||
/**
|
||||
* Test whether an object is useable as an Edge
|
||||
@@ -26,7 +26,7 @@ export const isNode = isNodeBase<Node, Edge>;
|
||||
* @param element - The element to test
|
||||
* @returns A boolean indicating whether the element is an Edge
|
||||
*/
|
||||
export const isEdge = isEdgeBase<Node, Edge>;
|
||||
export const isEdge = isEdgeBase<Edge>;
|
||||
|
||||
/**
|
||||
* Pass in a node, and get connected nodes where edge.source === node.id
|
||||
|
||||
@@ -1,12 +1,40 @@
|
||||
## 0.0.29
|
||||
|
||||
Another huge update for Svelte Flow 🙏 Handling data flows will be way easier with the new hooks and functions. You can now subscribe to connected nodes, receive data and update nodes more easily. We fix a big issue about the `<Handle />` component. No more `on:connect` that only worked for target `<Handle />` components but `onconnect` and `ondisconnect` that works for every `<Handle />`.
|
||||
|
||||
### Features
|
||||
|
||||
- add `useHandleConnections` hook for receiving connected node and handle ids for a specific handle
|
||||
- add `useNodesData(ids: string | string[])` hook for receiving data from other nodes
|
||||
- export `updateNode` and `updateNodeData` from `useSvelteFlow` to update a node or the data object
|
||||
- add `onedgecreate` function for passing a certain id or other attributes to a newly created edge
|
||||
|
||||
### ⚠️ Breaking
|
||||
|
||||
- replace `on:connect`, `on:connectstart` and `on:connectend` with `onconnect`, `onconnectstart` and `onconnectend`, no need to forward `on:connect..` anymore
|
||||
|
||||
### Fixes and minor changes
|
||||
|
||||
- `onconnect` and `ondisconnect` callback work for `<Handle />` component
|
||||
- don't delete a node when user presses Backspace inside an input/textarea/.nokey element
|
||||
- `bgColor` prop for Background didn't work
|
||||
- prefix css vars with "xy-"
|
||||
- don't update nodes and edges on pane click if not necessary
|
||||
- cleaner types for exported edges
|
||||
- fix `getIntersectingNodes` bug when passing `Rect`
|
||||
|
||||
## 0.0.28
|
||||
|
||||
There are some breaking changes again (sorry!) but we are very close to the final API for Svelte Flow 1.0.0. The biggest change is that we group node attriubutes (`width`, `height`, `positionAbsolute`) that are added by the library under `node.computed`. This makes it easier to understand, that this stuff comes from the library itself. `node.width` and `node.height` is still an optional node option and can be used to set certain dimensions for SSR or on the client.
|
||||
This is a huge update! We added a new `<NodeToolbar />` component and a new `colorMode` ('light' | 'dark' | 'system') prop for toggling dark/light mode.
|
||||
|
||||
There are also some breaking changes again (sorry!) but we are very close to the final API for Svelte Flow 1.0.0. The biggest change is that we group node attriubutes (`width`, `height`, `positionAbsolute`) that are added by the library under `node.computed`. This makes it easier to understand, that this stuff comes from the library itself. `node.width` and `node.height` is still an optional node option and can be used to set certain dimensions for SSR or on the client.
|
||||
|
||||
- add `<NodeToolbar />` component
|
||||
- add `on:selectionclick` and `on:selectioncontextmenu` event handlers
|
||||
- add `ondelete({ nodes, edges })` handler
|
||||
- add `zoomActivationKey` prop
|
||||
- add `width` and `height` prop to custom `NodeProps` type
|
||||
- add `colorMode` prop ('light' | 'dark' | 'system')
|
||||
- ⚠️ replace `xPos` and `yPos` with `positionAbsolute` prop to custom `NodeProps` type
|
||||
- ⚠️ `node.width/height` and `node.positionAbsolute` can now be found under `node.computed.width/height/positionAbsolute`
|
||||
- ⚠️ `node.width/height` is still optional an can be used for forcing certain dimensions and SSR
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/svelte",
|
||||
"version": "0.0.28",
|
||||
"version": "0.0.29",
|
||||
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
||||
"keywords": [
|
||||
"svelte",
|
||||
@@ -41,7 +41,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@svelte-put/shortcut": "^3.0.0",
|
||||
"@svelte-put/shortcut": "^3.1.0",
|
||||
"@xyflow/system": "workspace:*",
|
||||
"classcat": "^5.0.4"
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
export let target: $$Props['target'] = '';
|
||||
export let data: $$Props['data'] = {};
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
|
||||
export let animated: $$Props['animated'] = false;
|
||||
export let selected: $$Props['selected'] = false;
|
||||
@@ -95,42 +96,44 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
{#if !hidden}
|
||||
<g
|
||||
class={cc(['svelte-flow__edge', className])}
|
||||
class:animated
|
||||
class:selected
|
||||
data-id={id}
|
||||
on:click={onClick}
|
||||
on:contextmenu={onContextMenu}
|
||||
aria-label={ariaLabel === null
|
||||
? undefined
|
||||
: ariaLabel
|
||||
? ariaLabel
|
||||
: `Edge from ${source} to ${target}`}
|
||||
role="img"
|
||||
>
|
||||
<svelte:component
|
||||
this={edgeComponent}
|
||||
{id}
|
||||
{source}
|
||||
{target}
|
||||
{sourceX}
|
||||
{sourceY}
|
||||
{targetX}
|
||||
{targetY}
|
||||
{sourcePosition}
|
||||
{targetPosition}
|
||||
{animated}
|
||||
{selected}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{data}
|
||||
{style}
|
||||
{interactionWidth}
|
||||
sourceHandleId={sourceHandle}
|
||||
targetHandleId={targetHandle}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
/>
|
||||
</g>
|
||||
<svg style:zIndex>
|
||||
<g
|
||||
class={cc(['svelte-flow__edge', className])}
|
||||
class:animated
|
||||
class:selected
|
||||
data-id={id}
|
||||
on:click={onClick}
|
||||
on:contextmenu={onContextMenu}
|
||||
aria-label={ariaLabel === null
|
||||
? undefined
|
||||
: ariaLabel
|
||||
? ariaLabel
|
||||
: `Edge from ${source} to ${target}`}
|
||||
role="img"
|
||||
>
|
||||
<svelte:component
|
||||
this={edgeComponent}
|
||||
{id}
|
||||
{source}
|
||||
{target}
|
||||
{sourceX}
|
||||
{sourceY}
|
||||
{targetX}
|
||||
{targetY}
|
||||
{sourcePosition}
|
||||
{targetPosition}
|
||||
{animated}
|
||||
{selected}
|
||||
{label}
|
||||
{labelStyle}
|
||||
{data}
|
||||
{style}
|
||||
{interactionWidth}
|
||||
sourceHandleId={sourceHandle}
|
||||
targetHandleId={targetHandle}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
Position,
|
||||
XYHandle,
|
||||
isMouseEvent,
|
||||
type Connection,
|
||||
type HandleType
|
||||
areConnectionMapsEqual,
|
||||
handleConnectionChange
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { HandleComponentProps } from '$lib/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
type $$Props = HandleComponentProps;
|
||||
|
||||
@@ -20,6 +21,8 @@
|
||||
export let position: $$Props['position'] = Position.Top;
|
||||
export let style: $$Props['style'] = undefined;
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
export let onconnect: $$Props['onconnect'] = undefined;
|
||||
export let ondisconnect: $$Props['ondisconnect'] = undefined;
|
||||
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined;
|
||||
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined;
|
||||
|
||||
@@ -32,18 +35,6 @@
|
||||
$: handleConnectable = isConnectable !== undefined ? isConnectable : $connectable;
|
||||
|
||||
const handleId = id || null;
|
||||
const dispatch = createEventDispatcher<{
|
||||
connect: { connection: Connection };
|
||||
connectstart: {
|
||||
event: MouseEvent | TouchEvent;
|
||||
nodeId: string | null;
|
||||
handleId: string | null;
|
||||
handleType: HandleType | null;
|
||||
};
|
||||
connectend: {
|
||||
event: MouseEvent | TouchEvent;
|
||||
};
|
||||
}>();
|
||||
|
||||
const store = useStore();
|
||||
const {
|
||||
@@ -59,7 +50,12 @@
|
||||
panBy,
|
||||
cancelConnection,
|
||||
updateConnection,
|
||||
autoPanOnConnect
|
||||
autoPanOnConnect,
|
||||
edges,
|
||||
connectionLookup,
|
||||
onconnect: onConnectAction,
|
||||
onconnectstart: onConnectStartAction,
|
||||
onconnectend: onConnectEndAction
|
||||
} = store;
|
||||
|
||||
function onPointerDown(event: MouseEvent | TouchEvent) {
|
||||
@@ -88,26 +84,43 @@
|
||||
}
|
||||
|
||||
addEdge(edge);
|
||||
// @todo: should we change/ improve the stuff we are passing here?
|
||||
// instead of source/target we could pass fromNodeId, fromHandleId, etc
|
||||
dispatch('connect', { connection });
|
||||
$onConnectAction?.(connection);
|
||||
},
|
||||
onConnectStart: (event, startParams) => {
|
||||
dispatch('connectstart', {
|
||||
event,
|
||||
$onConnectStartAction?.(event, {
|
||||
nodeId: startParams.nodeId,
|
||||
handleId: startParams.handleId,
|
||||
handleType: startParams.handleType
|
||||
});
|
||||
},
|
||||
onConnectEnd: (event) => {
|
||||
dispatch('connectend', { event });
|
||||
$onConnectEndAction?.(event);
|
||||
},
|
||||
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let prevConnections: Map<string, Connection> | null = null;
|
||||
let connections: Map<string, Connection> | undefined;
|
||||
|
||||
$: if (onconnect || ondisconnect) {
|
||||
// connectionLookup is not reactive, so we use edges to get notified about updates
|
||||
$edges;
|
||||
connections = $connectionLookup.get(`${nodeId}-${type}-${id || null}`);
|
||||
}
|
||||
|
||||
$: {
|
||||
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
|
||||
const _connections = connections ?? new Map();
|
||||
|
||||
handleConnectionChange(prevConnections, _connections, ondisconnect);
|
||||
handleConnectionChange(_connections, prevConnections, onconnect);
|
||||
}
|
||||
|
||||
prevConnections = connections ?? new Map();
|
||||
}
|
||||
|
||||
// @todo implement connectablestart, connectableend
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { shortcut, type ShortcutModifierDefinition } from '@svelte-put/shortcut';
|
||||
import { isInputDOMNode, isMacOs } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { KeyHandlerProps } from './types';
|
||||
import type { KeyDefinition, KeyDefinitionObject } from '$lib/types';
|
||||
import { isMacOs } from '@xyflow/system';
|
||||
|
||||
type $$Props = KeyHandlerProps;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
} = useStore();
|
||||
|
||||
function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject {
|
||||
return typeof key === 'object';
|
||||
return key !== null && typeof key === 'object';
|
||||
}
|
||||
|
||||
function getModifier(key?: KeyDefinition | null): ShortcutModifierDefinition {
|
||||
@@ -71,16 +71,19 @@
|
||||
trigger: [
|
||||
{
|
||||
...selectionKeyDefinition,
|
||||
callback: () => selectionKeyDefinition.key && selectionKeyPressed.set(true)
|
||||
enabled: selectionKeyDefinition.key !== null,
|
||||
callback: () => selectionKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
|
||||
type: 'keydown'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
...selectionKeyDefinition,
|
||||
callback: () => selectionKeyDefinition.key && selectionKeyPressed.set(false)
|
||||
enabled: selectionKeyDefinition.key !== null,
|
||||
callback: () => selectionKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -89,7 +92,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...multiSelectionKeyDefinition,
|
||||
callback: () => multiSelectionKeyDefinition.key && multiselectionKeyPressed.set(true)
|
||||
enabled: multiSelectionKeyDefinition.key !== null,
|
||||
callback: () => multiselectionKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -98,7 +102,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...multiSelectionKeyDefinition,
|
||||
callback: () => multiSelectionKeyDefinition.key && multiselectionKeyPressed.set(false)
|
||||
enabled: multiSelectionKeyDefinition.key !== null,
|
||||
callback: () => multiselectionKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -107,7 +112,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...deleteKeyDefinition,
|
||||
callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(true)
|
||||
enabled: deleteKeyDefinition.key !== null,
|
||||
callback: (detail) => !isInputDOMNode(detail.originalEvent) && deleteKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -116,7 +122,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...deleteKeyDefinition,
|
||||
callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(false)
|
||||
enabled: deleteKeyDefinition.key !== null,
|
||||
callback: () => deleteKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -125,7 +132,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...panActivationKeyDefinition,
|
||||
callback: () => panActivationKeyDefinition.key && panActivationKeyPressed.set(true)
|
||||
enabled: panActivationKeyDefinition.key !== null,
|
||||
callback: () => panActivationKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -134,7 +142,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...panActivationKeyDefinition,
|
||||
callback: () => panActivationKeyDefinition.key && panActivationKeyPressed.set(false)
|
||||
enabled: panActivationKeyDefinition.key !== null,
|
||||
callback: () => panActivationKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -143,7 +152,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...zoomActivationKeyDefinition,
|
||||
callback: () => zoomActivationKeyDefinition.key && zoomActivationKeyPressed.set(true)
|
||||
enabled: zoomActivationKeyDefinition.key !== null,
|
||||
callback: () => zoomActivationKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -152,7 +162,8 @@
|
||||
trigger: [
|
||||
{
|
||||
...zoomActivationKeyDefinition,
|
||||
callback: () => zoomActivationKeyDefinition.key && zoomActivationKeyPressed.set(false)
|
||||
enabled: zoomActivationKeyDefinition.key !== null,
|
||||
callback: () => zoomActivationKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
|
||||
@@ -191,12 +191,10 @@
|
||||
{dragging}
|
||||
{dragHandle}
|
||||
isConnectable={connectable}
|
||||
positionAbsolute={{ x: positionX, y: positionY }}
|
||||
positionAbsoluteX={positionX}
|
||||
positionAbsoluteY={positionY}
|
||||
{width}
|
||||
{height}
|
||||
on:connectstart
|
||||
on:connect
|
||||
on:connectend
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
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;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
@@ -33,10 +31,11 @@
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
positionAbsolute;
|
||||
positionAbsoluteX;
|
||||
positionAbsoluteY;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
<Handle type="target" position={targetPosition} on:connectstart on:connect on:connectend />
|
||||
<Handle type="target" position={targetPosition} />
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition} on:connectstart on:connect on:connectend />
|
||||
<Handle type="source" position={sourcePosition} />
|
||||
|
||||
@@ -15,10 +15,8 @@
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
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;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
@@ -33,6 +31,7 @@
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
positionAbsolute;
|
||||
positionAbsoluteX;
|
||||
positionAbsoluteY;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
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;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
@@ -34,9 +32,10 @@
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
positionAbsolute;
|
||||
positionAbsoluteX;
|
||||
positionAbsoluteY;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition} on:connectstart on:connect on:connectend />
|
||||
<Handle type="source" position={sourcePosition} />
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
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;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
@@ -31,9 +32,10 @@
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
positionAbsolute;
|
||||
positionAbsoluteX;
|
||||
positionAbsoluteY;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
{data?.label}
|
||||
<Handle type="target" position={targetPosition} on:connectstart on:connect on:connectend />
|
||||
<Handle type="target" position={targetPosition} />
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
const {
|
||||
elementsSelectable,
|
||||
edgeTree,
|
||||
visibleEdges,
|
||||
edges: { setDefaultOptions }
|
||||
} = useStore();
|
||||
|
||||
@@ -18,57 +18,47 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#each $edgeTree as group (group.level)}
|
||||
<svg style="z-index: {group.level}" class="svelte-flow__edges">
|
||||
{#if group.isMaxLevel} <MarkerDefinition />{/if}
|
||||
<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>
|
||||
<div class="svelte-flow__edges">
|
||||
<svg class="svelte-flow__marker">
|
||||
<MarkerDefinition />
|
||||
</svg>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.svelte-flow__edges {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
{#each $visibleEdges 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}
|
||||
zIndex={edge.zIndex}
|
||||
{selectable}
|
||||
on:edgeclick
|
||||
on:edgecontextmenu
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -83,9 +83,6 @@
|
||||
on:nodemouseenter
|
||||
on:nodemousemove
|
||||
on:nodemouseleave
|
||||
on:connectstart
|
||||
on:connect
|
||||
on:connectend
|
||||
on:nodedrag
|
||||
on:nodedragstart
|
||||
on:nodedragstop
|
||||
|
||||
@@ -78,6 +78,9 @@
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let colorMode: $$Props['colorMode'] = 'light';
|
||||
export let onconnect: $$Props['onconnect'] = undefined;
|
||||
export let onconnectstart: $$Props['onconnectstart'] = undefined;
|
||||
export let onconnectend: $$Props['onconnectend'] = undefined;
|
||||
|
||||
export let defaultMarkerColor = '#b1b1b7';
|
||||
|
||||
@@ -152,7 +155,10 @@
|
||||
ondelete,
|
||||
onedgecreate,
|
||||
connectionMode,
|
||||
nodeDragThreshold
|
||||
nodeDragThreshold,
|
||||
onconnect,
|
||||
onconnectstart,
|
||||
onconnectend
|
||||
};
|
||||
|
||||
updateStoreByKeys(store, updatableProps);
|
||||
@@ -217,9 +223,6 @@
|
||||
on:nodemouseenter
|
||||
on:nodemousemove
|
||||
on:nodemouseleave
|
||||
on:connectstart
|
||||
on:connect
|
||||
on:connectend
|
||||
on:nodedragstart
|
||||
on:nodedrag
|
||||
on:nodedragstop
|
||||
|
||||
@@ -15,7 +15,10 @@ import type {
|
||||
ConnectionMode,
|
||||
PanelPosition,
|
||||
ProOptions,
|
||||
ColorMode
|
||||
ColorMode,
|
||||
OnConnect,
|
||||
OnConnectStart,
|
||||
OnConnectEnd
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -92,4 +95,8 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
||||
ondelete?: OnDelete;
|
||||
|
||||
onedgecreate?: OnEdgeCreate;
|
||||
|
||||
onconnect?: OnConnect;
|
||||
onconnectstart?: OnConnectStart;
|
||||
onconnectend?: OnConnectEnd;
|
||||
};
|
||||
|
||||
@@ -66,6 +66,9 @@ export type UpdatableStoreProps = {
|
||||
ondelete?: UnwrapWritable<SvelteFlowStore['ondelete']>;
|
||||
onedgecreate?: UnwrapWritable<SvelteFlowStore['onedgecreate']>;
|
||||
nodeDragThreshold?: UnwrapWritable<SvelteFlowStore['nodeDragThreshold']>;
|
||||
onconnect?: UnwrapWritable<SvelteFlowStore['onconnect']>;
|
||||
onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>;
|
||||
onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>;
|
||||
};
|
||||
|
||||
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { areConnectionMapsEqual, type Connection, type HandleType } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
export type useHandleConnectionsParams = {
|
||||
nodeId: string;
|
||||
type: HandleType;
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
const initialConnections: Connection[] = [];
|
||||
|
||||
export function useHandleConnections({ nodeId, type, id = null }: useHandleConnectionsParams) {
|
||||
const { edges, connectionLookup } = useStore();
|
||||
let prevConnections: Map<string, Connection> | undefined = undefined;
|
||||
|
||||
return derived(
|
||||
[edges, connectionLookup],
|
||||
([, connectionLookup], set) => {
|
||||
const nextConnections = connectionLookup.get(`${nodeId}-${type}-${id || null}`);
|
||||
|
||||
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
|
||||
prevConnections = nextConnections;
|
||||
set(Array.from(prevConnections?.values() || []));
|
||||
}
|
||||
},
|
||||
initialConnections
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
|
||||
import type { Node } from '$lib/types';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
function areNodesDataEqual(a: Node['data'][] | null, b: Node['data'][] | null) {
|
||||
if ((!a && !b) || (!a?.length && !b?.length)) {
|
||||
true;
|
||||
}
|
||||
|
||||
if (!a || !b || a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function useNodesData<NodeType extends Node = Node>(
|
||||
nodeId: string
|
||||
): Readable<NodeType['data'] | null>;
|
||||
export function useNodesData<NodeType extends Node = Node>(
|
||||
nodeIds: string[]
|
||||
): Readable<NodeType['data'][]>;
|
||||
export function useNodesData<NodeType extends Node = Node>(
|
||||
nodeIds: string[],
|
||||
guard: (node: Node) => node is NodeType
|
||||
): Readable<NodeType['data'][]>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useNodesData(nodeIds: any): any {
|
||||
const { nodes, nodeLookup } = useStore();
|
||||
let prevNodesData: (Node['data'] | null)[] | null = null;
|
||||
|
||||
return derived([nodes, nodeLookup], ([, nodeLookup], set) => {
|
||||
let nextNodesData: (Node['data'] | null)[] | null = null;
|
||||
const nodeIdArray = Array.isArray(nodeIds);
|
||||
|
||||
if (!nodeIdArray) {
|
||||
nextNodesData = [nodeLookup.get(nodeIds)?.data || null];
|
||||
} else {
|
||||
const data = [];
|
||||
|
||||
for (const nodeId of nodeIds) {
|
||||
const nodeData = nodeLookup.get(nodeId)?.data;
|
||||
|
||||
if (nodeData) {
|
||||
data.push(nodeData);
|
||||
}
|
||||
}
|
||||
|
||||
nextNodesData = data;
|
||||
}
|
||||
|
||||
if (!areNodesDataEqual(nextNodesData, prevNodesData)) {
|
||||
prevNodesData = nextNodesData;
|
||||
set(nodeIdArray ? nextNodesData : nextNodesData[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
import {
|
||||
getIncomersBase,
|
||||
getOutgoersBase,
|
||||
getOverlappingArea,
|
||||
isRectObject,
|
||||
nodeToRect,
|
||||
@@ -20,6 +18,7 @@ import {
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { Edge, FitViewOptions, Node } from '$lib/types';
|
||||
import { isNode } from '$lib/utils';
|
||||
|
||||
export function useSvelteFlow(): {
|
||||
zoomIn: ZoomInOut;
|
||||
@@ -48,9 +47,16 @@ export function useSvelteFlow(): {
|
||||
screenToFlowPosition: (position: XYPosition) => XYPosition;
|
||||
flowToScreenPosition: (position: XYPosition) => XYPosition;
|
||||
viewport: Writable<Viewport>;
|
||||
getConnectedEdges: (id: string | (Node | { id: Node['id'] })[]) => Edge[];
|
||||
getIncomers: (node: string | Node | { id: Node['id'] }) => Node[];
|
||||
getOutgoers: (node: string | Node | { id: Node['id'] }) => Node[];
|
||||
updateNode: (
|
||||
id: string,
|
||||
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
updateNodeData: (
|
||||
id: string,
|
||||
dataUpdate: object | ((node: Node) => object),
|
||||
options?: { replace: boolean }
|
||||
) => void;
|
||||
toObject: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
|
||||
} {
|
||||
const {
|
||||
@@ -84,6 +90,24 @@ export function useSvelteFlow(): {
|
||||
return [nodeRect, node, isRect];
|
||||
};
|
||||
|
||||
const updateNode = (
|
||||
id: string,
|
||||
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
|
||||
options: { replace: boolean } = { replace: false }
|
||||
) => {
|
||||
nodes.update((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === id) {
|
||||
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
|
||||
|
||||
return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode };
|
||||
}
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
@@ -232,29 +256,6 @@ export function useSvelteFlow(): {
|
||||
y: rendererPosition.y + domY
|
||||
};
|
||||
},
|
||||
getConnectedEdges: (node) => {
|
||||
const nodeIds = new Set();
|
||||
|
||||
if (typeof node === 'string') {
|
||||
nodeIds.add(node);
|
||||
} else if (node.length >= 1) {
|
||||
node.forEach((n) => {
|
||||
nodeIds.add(n.id);
|
||||
});
|
||||
}
|
||||
|
||||
return get(edges).filter((edge) => nodeIds.has(edge.source) || nodeIds.has(edge.target));
|
||||
},
|
||||
getIncomers: (node) => {
|
||||
const _node = typeof node === 'string' ? { id: node } : node;
|
||||
|
||||
return getIncomersBase(_node, get(nodes), get(edges));
|
||||
},
|
||||
getOutgoers: (node) => {
|
||||
const _node = typeof node === 'string' ? { id: node } : node;
|
||||
|
||||
return getOutgoersBase(_node, get(nodes), get(edges));
|
||||
},
|
||||
toObject: () => {
|
||||
return {
|
||||
nodes: get(nodes).map((node) => ({
|
||||
@@ -268,6 +269,16 @@ export function useSvelteFlow(): {
|
||||
viewport: { ...get(viewport) }
|
||||
};
|
||||
},
|
||||
updateNode,
|
||||
updateNodeData: (id, dataUpdate, options) => {
|
||||
updateNode(id, (node) => {
|
||||
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
|
||||
|
||||
return options?.replace
|
||||
? { ...node, data: nextData }
|
||||
: { ...node, data: { ...node.data, ...nextData } };
|
||||
});
|
||||
},
|
||||
viewport
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export * from '$lib/hooks/useSvelteFlow';
|
||||
export * from '$lib/hooks/useUpdateNodeInternals';
|
||||
export * from '$lib/hooks/useConnection';
|
||||
export * from '$lib/hooks/useNodesEdges';
|
||||
export * from '$lib/hooks/useHandleConnections';
|
||||
export * from '$lib/hooks/useNodesData';
|
||||
|
||||
// types
|
||||
export type {
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData }
|
||||
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
|
||||
import type { SvelteFlowStore } from './types';
|
||||
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
|
||||
import { getEdgeTree } from './edge-tree';
|
||||
import { getVisibleEdges } from './visible-edges';
|
||||
import { getVisibleNodes } from './visible-nodes';
|
||||
import { getDerivedConnectionProps } from './derived-connection-props';
|
||||
|
||||
@@ -357,8 +357,8 @@ export function createStore({
|
||||
...store,
|
||||
|
||||
// derived state
|
||||
edgeTree: getEdgeTree(store),
|
||||
connection: getDerivedConnectionProps(store, currentConnection),
|
||||
visibleEdges: getVisibleEdges(store),
|
||||
visibleNodes: getVisibleNodes(store),
|
||||
markers: derived(
|
||||
[store.edges, store.defaultMarkerColor, store.flowId],
|
||||
|
||||
@@ -4,20 +4,25 @@ import {
|
||||
SelectionMode,
|
||||
ConnectionMode,
|
||||
ConnectionLineType,
|
||||
devWarn,
|
||||
adoptUserProvidedNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
updateConnectionLookup,
|
||||
type SelectionRect,
|
||||
type SnapGrid,
|
||||
type MarkerProps,
|
||||
type PanZoomInstance,
|
||||
type CoordinateExtent,
|
||||
type IsValidConnection,
|
||||
type GroupedEdges,
|
||||
type NodeOrigin,
|
||||
type OnError,
|
||||
devWarn,
|
||||
type Viewport,
|
||||
updateNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds
|
||||
type ConnectionLookup,
|
||||
type OnConnect,
|
||||
type OnConnectStart,
|
||||
type OnConnectEnd,
|
||||
type NodeLookup
|
||||
} from '@xyflow/system';
|
||||
|
||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||
@@ -72,11 +77,14 @@ export const getInitialStore = ({
|
||||
height?: number;
|
||||
fitView?: boolean;
|
||||
}) => {
|
||||
const nodeLookup = new Map<string, Node>();
|
||||
const nextNodes = updateNodes(nodes, nodeLookup, {
|
||||
const nodeLookup = new Map();
|
||||
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||
nodeOrigin: [0, 0],
|
||||
elevateNodesOnSelect: false
|
||||
});
|
||||
const connectionLookup = new Map();
|
||||
const edgeLookup = new Map();
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, edges);
|
||||
|
||||
let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
|
||||
|
||||
@@ -89,16 +97,17 @@ export const getInitialStore = ({
|
||||
return {
|
||||
flowId: writable<string | null>(null),
|
||||
nodes: createNodesStore(nextNodes, nodeLookup),
|
||||
nodeLookup: readable<Map<string, Node>>(nodeLookup),
|
||||
nodeLookup: readable<NodeLookup>(nodeLookup),
|
||||
visibleNodes: readable<Node[]>([]),
|
||||
edges: createEdgesStore(edges),
|
||||
edgeTree: readable<GroupedEdges<EdgeLayouted>[]>([]),
|
||||
edges: createEdgesStore(edges, connectionLookup, edgeLookup),
|
||||
visibleEdges: readable<EdgeLayouted[]>([]),
|
||||
connectionLookup: readable<ConnectionLookup>(connectionLookup),
|
||||
height: writable<number>(500),
|
||||
width: writable<number>(500),
|
||||
minZoom: writable<number>(0.5),
|
||||
maxZoom: writable<number>(2),
|
||||
nodeOrigin: writable<NodeOrigin>([0, 0]),
|
||||
nodeDragThreshold: writable<number>(0),
|
||||
nodeDragThreshold: writable<number>(1),
|
||||
nodeExtent: writable<CoordinateExtent>(infiniteExtent),
|
||||
translateExtent: writable<CoordinateExtent>(infiniteExtent),
|
||||
autoPanOnNodeDrag: writable<boolean>(true),
|
||||
@@ -136,6 +145,9 @@ export const getInitialStore = ({
|
||||
onlyRenderVisibleElements: writable<boolean>(false),
|
||||
onerror: writable<OnError>(devWarn),
|
||||
ondelete: writable<OnDelete>(undefined),
|
||||
onedgecreate: writable<OnEdgeCreate>(undefined)
|
||||
onedgecreate: writable<OnEdgeCreate>(undefined),
|
||||
onconnect: writable<OnConnect>(undefined),
|
||||
onconnectstart: writable<OnConnectStart>(undefined),
|
||||
onconnectend: writable<OnConnectEnd>(undefined)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import {
|
||||
writable,
|
||||
get,
|
||||
type Unsubscriber,
|
||||
type Subscriber,
|
||||
type Updater,
|
||||
type Writable,
|
||||
get
|
||||
type Writable
|
||||
} from 'svelte/store';
|
||||
import { updateNodes, type Viewport, type PanZoomInstance } from '@xyflow/system';
|
||||
import {
|
||||
adoptUserProvidedNodes,
|
||||
updateConnectionLookup,
|
||||
type Viewport,
|
||||
type PanZoomInstance,
|
||||
type ConnectionLookup,
|
||||
type EdgeLookup,
|
||||
type NodeLookup
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
|
||||
|
||||
@@ -119,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.
|
||||
export const createNodesStore = (
|
||||
nodes: Node[],
|
||||
nodeLookup: Map<string, Node>
|
||||
nodeLookup: NodeLookup<Node>
|
||||
): {
|
||||
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
|
||||
update: (this: void, updater: Updater<Node[]>) => void;
|
||||
@@ -133,7 +141,7 @@ export const createNodesStore = (
|
||||
let elevateNodesOnSelect = true;
|
||||
|
||||
const _set = (nds: Node[]): Node[] => {
|
||||
const nextNodes = updateNodes(nds, nodeLookup, {
|
||||
const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, {
|
||||
elevateNodesOnSelect,
|
||||
defaults
|
||||
});
|
||||
@@ -168,6 +176,8 @@ export const createNodesStore = (
|
||||
|
||||
export const createEdgesStore = (
|
||||
edges: Edge[],
|
||||
connectionLookup: ConnectionLookup,
|
||||
edgeLookup: EdgeLookup<Edge>,
|
||||
defaultOptions?: DefaultEdgeOptions
|
||||
): Writable<Edge[]> & { setDefaultOptions: (opts: DefaultEdgeOptions) => void } => {
|
||||
const { subscribe, set, update } = writable<Edge[]>([]);
|
||||
@@ -176,6 +186,9 @@ export const createEdgesStore = (
|
||||
|
||||
const _set: typeof set = (eds: Edge[]) => {
|
||||
const nextEdges = defaults ? eds.map((edge) => ({ ...defaults, ...edge })) : eds;
|
||||
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, nextEdges);
|
||||
|
||||
value = nextEdges;
|
||||
set(value);
|
||||
};
|
||||
|
||||
+10
-5
@@ -1,10 +1,10 @@
|
||||
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 { SvelteFlowStoreState } from './types';
|
||||
|
||||
export function getEdgeTree(store: SvelteFlowStoreState) {
|
||||
export function getVisibleEdges(store: SvelteFlowStoreState) {
|
||||
const visibleEdges = derived(
|
||||
[
|
||||
store.edges,
|
||||
@@ -64,6 +64,13 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
|
||||
if (edgePosition) {
|
||||
res.push({
|
||||
...edge,
|
||||
zIndex: getElevatedEdgeZIndex({
|
||||
selected: edge.selected,
|
||||
zIndex: edge.zIndex,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
elevateOnSelect: false
|
||||
}),
|
||||
...edgePosition
|
||||
});
|
||||
}
|
||||
@@ -71,9 +78,7 @@ export function getEdgeTree(store: SvelteFlowStoreState) {
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
const groupedEdges = groupEdgesByZLevel<EdgeLayouted>(layoutedEdges, nodeLookup, false);
|
||||
|
||||
return groupedEdges;
|
||||
return layoutedEdges;
|
||||
}
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user