Merge branch 'next' into refactor/tsdoc

This commit is contained in:
moklick
2023-12-19 11:59:44 +01:00
117 changed files with 3388 additions and 2000 deletions

View File

@@ -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>

View File

@@ -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',

View File

@@ -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' };

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = () => {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 its normally located in the right part of the viewport
// which means dragging it left is safe to do without scrolling the viewport.
const nodeElement = document.querySelector('.react-flow__node[data-id="18"]');
if (!nodeElement) throw new Error('Node with id 18 not found');
const frameRecorder = new FrameRecorder();
// Hold down the mouse
frameRecorder.setStage('mousedown');
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
await nextFrame();
// Start at the node position and move the mouse 5px to the left on every frame
frameRecorder.setStage('mousemove');
let currentXPosition = mouseDownEvent.clientX;
for (let iteration = 0; iteration < 20; ++iteration) {
const movementX = -5;
currentXPosition += movementX;
nodeElement.dispatchEvent(
new MouseEvent('mousemove', {
...mouseDownEvent,
clientX: currentXPosition,
screenX: currentXPosition,
movementX,
})
);
await nextFrame();
}
// Release the mouse
frameRecorder.setStage('mouseup');
nodeElement.dispatchEvent(
new MouseEvent('mouseup', {
...mouseDownEvent,
clientX: currentXPosition,
screenX: currentXPosition,
})
);
await nextFrame();
// Log the results
await frameRecorder.endRecordingAsync();
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
};
const dragOutsideViewport = async () => {
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
if (!nodeElement) throw new Error('Node not found');
const frameRecorder = new FrameRecorder();
// Hold down the mouse
frameRecorder.setStage('mousedown');
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
await nextFrame();
// Move the mouse to the top of the viewport (so that the viewport starts
// scrolling up). Then, wiggle the mouse up and down to keep the viewport
// scrolling.
frameRecorder.setStage('mousemove');
let currentYPosition = 50;
for (let iteration = 0; iteration < 20; ++iteration) {
const movementY = Math.random() > 0.5 ? +2 : -2;
currentYPosition += movementY;
nodeElement.dispatchEvent(
new MouseEvent('mousemove', {
...mouseDownEvent,
clientY: currentYPosition,
screenY: currentYPosition,
movementY,
})
);
await nextFrame();
}
// Release the mouse
frameRecorder.setStage('mouseup');
nodeElement.dispatchEvent(
new MouseEvent('mouseup', {
...mouseDownEvent,
clientY: currentYPosition,
screenY: currentYPosition,
})
);
await nextFrame();
// Log the results
await frameRecorder.endRecordingAsync();
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
};
const selectNode = async () => {
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
if (!nodeElement) throw new Error('Node not found');
const frameRecorder = new FrameRecorder();
const mouseEvent = generateMouseEventParamsTargetingNode(nodeElement);
// mousedown
frameRecorder.setStage('mousedown');
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseEvent));
await nextFrame();
// click
frameRecorder.setStage('click');
nodeElement.dispatchEvent(new MouseEvent('click', mouseEvent));
await nextFrame();
// mouseup
frameRecorder.setStage('mouseup');
nodeElement.dispatchEvent(new MouseEvent('mouseup', mouseEvent));
await nextFrame();
// Log the results
await frameRecorder.endRecordingAsync();
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
};
const [key, setKey] = useState(0);
const frameRecorderRef = useRef<FrameRecorder | null>(null);
function remount() {
frameRecorderRef.current = new FrameRecorder();
setKey((k) => k + 1);
}
useEffect(() => {
const frameRecorder = frameRecorderRef.current;
if (!frameRecorder) return;
frameRecorder.endRecordingAsync().then(() => {
console.log('Frame durations:', frameRecorder.getFrames());
console.log(
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
frameRecorder.getFramesForObservable()
);
frameRecorderRef.current = null;
});
}, [key]);
const updatePos = () => {
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>

View File

@@ -0,0 +1,150 @@
type Frame = {
duration: number;
stage: string;
};
/**
* Measures and outputs the duration of every frame that happens between the
* instance is created and `endRecording()` is called.
*
* Usage:
*
* ```ts
* const recorder = new FrameRecorder();
*
* // Do some performance-intensive stuff
*
* await recorder.endRecordingAsync();
*
* console.log(recorder.getFrames());
* console.log(recorder.getFramesForObservable()); // → paste into https://observablehq.com/@iamakulov/long-frame-visualizer
* ```
*/
export class FrameRecorder {
private frames: Frame[] = [];
private animationFrameId: number;
private stage: string = '<no stage>';
constructor() {
let lastFrameTimestamp = performance.now();
const measureFrame = () => {
const timestamp = performance.now();
// Visualize the frames in the Performance pane (see the collapsed
// “Timings” section) so its easier to see what exactly each frame
// captured
performance.measure(`frame (${this.stage})`, {
start: lastFrameTimestamp,
end: timestamp,
});
this.frames.push({
duration: timestamp - lastFrameTimestamp,
stage: this.stage,
});
lastFrameTimestamp = timestamp;
this.animationFrameId = requestAnimationFrame(measureFrame);
};
this.animationFrameId = requestAnimationFrame(measureFrame);
}
// The method is explicitly marked `async` in its name to make sure the caller
// doesnt forget to `await` it. (Otherwise, some events might be lost.)
async endRecordingAsync() {
this.setStage('waiting for idle');
await new Promise((resolve) => requestIdleCallback(resolve));
requestAnimationFrame(() => {
cancelAnimationFrame(this.animationFrameId);
});
}
/**
* Adds an optional annotation to all subsequent frames. Useful to
* differentiate frames from different events e.g. you can call
* `setState("mousedown")` before dispatching a mousedown event, and then
* `setState("mouseup")` before a mouseup one.
*
* When used, will affect both `getFramesForObservable()` and `getFrames()`.
*/
setStage(stage: string) {
this.stage = stage;
}
getFramesForObservable() {
return this.frames.map((frame, index) => ({ ...frame, index }));
}
getFrames() {
// Group frames by stage so you could see which frames originated from `mousedown` vs `mousemove` vs `mouseup` events
const framesPerStage: Record<string, number[]> = {};
for (const frame of this.frames) {
const stage = frame.stage;
if (!framesPerStage[stage]) {
framesPerStage[stage] = [];
}
framesPerStage[stage].push(frame.duration);
}
// If theres only one stage, return the frames directly
return framesPerStage;
}
}
/**
* Returns a promise that resolves when the next frame starts, and everything
* that was already scheduled in the event queue has been processed.
*/
export function nextFrame() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
/**
* Generates params for a new MouseEvent() that will target the given node.
*/
export function generateMouseEventParamsTargetingNode(node: Element) {
const nodePosition = node.getBoundingClientRect();
// Lets make the event (eg click) happen 5px to the right and 5px to the
// bottom of the nodes top-left corner
const positionRelativeToNode = {
left: 5,
top: 5,
};
return {
clientX: Math.round(nodePosition.left + positionRelativeToNode.left),
clientY: Math.round(nodePosition.top + positionRelativeToNode.top),
movementX: 0,
movementY: 0,
offsetX: positionRelativeToNode.left,
offsetY: positionRelativeToNode.top,
screenX: Math.round(nodePosition.left + positionRelativeToNode.left),
screenY: Math.round(nodePosition.top + positionRelativeToNode.top),
// Required boilerplate
altKey: false,
bubbles: true,
button: 0,
buttons: 1,
cancelBubble: false,
cancelable: true,
composed: true,
ctrlKey: false,
currentTarget: null,
defaultPrevented: false,
detail: 1,
eventPhase: 0,
fromElement: null,
isTrusted: true,
metaKey: false,
relatedTarget: null,
returnValue: true,
shiftKey: false,
view: window,
which: 1,
};
}

View File

@@ -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} />
</>

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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'

View File

@@ -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>

View File

@@ -87,7 +87,7 @@
{nodeTypes}
style="--xy-background-color: {$bgColor}"
fitView
on:connect={onConnect}
onconnect={onConnect}
>
<Controls />
<Background variant={BackgroundVariant.Dots} />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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, its 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);

View File

@@ -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) => ({

View File

@@ -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;

View File

@@ -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);

View File

@@ -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,
};

View File

@@ -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 });
}
};
}

View File

@@ -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);
};

View File

@@ -171,7 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
lib,
});
if (isValid) {
if (isValid && connection) {
onConnectExtended(connection);
}

View File

@@ -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);

View File

@@ -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>,
};

View File

@@ -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

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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,
};
}

View File

@@ -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}

View File

@@ -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]);
}

View File

@@ -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;
}

View File

@@ -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, its designed to
// minimize the cost of updates when individual nodes change.
//
// For example, when youre 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 youd normally write inside `nodes.map` are
// moved into `NodeComponentWrapper`. This ensures they are
// memorized so if `NodeRenderer` *has* to rerender, it only
// needs to regenerate the list of nodes, nothing else.
<NodeWrapper
key={nodeId}
id={nodeId}
nodeTypes={props.nodeTypes}
nodeExtent={props.nodeExtent}
nodeOrigin={props.nodeOrigin}
onClick={props.onNodeClick}
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}
/>
);
})}

View File

@@ -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;
}

View File

@@ -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,
};
}

View File

@@ -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);

View File

@@ -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}

View File

@@ -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]);
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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>> };

View File

@@ -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[];

View File

@@ -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'>;

View File

@@ -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>> };

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 `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
@@ -15,7 +43,7 @@ There are some breaking changes again (sorry!) but we are very close to the fina
## 0.0.27
- add `selectionOnDrag` prop - can be used to create figma-like controls in combination with `panOnDrag={false}` / `panOnDrag={[1, 2]}` + `panOnScroll={true}`
- add `selectionOnDrag` prop - can be used to create figma-like controls in combination with `panOnDrag={false}` / `panOnDrag={[1, 2]}` + `panOnScroll={true}`
- ⚠️ rename `screenToFlowCoordinate` to `screenToFlowPosition`
- ⚠️ rename `flowToScreenCoordinate` to `flowToScreenPosition`
- ⚠️ rename `getTransformForBounds` to `getViewportForBounds` (return `{ x: number, y: number, zoom: number }` instead of `[number, number, number]`)
@@ -64,7 +92,7 @@ There are some breaking changes again (sorry!) but we are very close to the fina
## 0.0.22
- add `connectionLine` slot for rendering a custom connection line
- add `connectionLine` slot for rendering a custom connection line
- add `connectionLineStyle` and `connectionLineContainerStyle` props
- add `useConnection` hook
- add `nodeDragThreshold` prop
@@ -97,7 +125,7 @@ There are some breaking changes again (sorry!) but we are very close to the fina
## 0.0.18
- add `nodesDraggable` prop
- add `nodesDraggable` prop
- minimap: add default background
## 0.0.17
@@ -110,7 +138,7 @@ There are some breaking changes again (sorry!) but we are very close to the fina
## 0.0.15
- fix wrongly displayed connectionline
- fix wrongly displayed connectionline
## 0.0.14
@@ -149,14 +177,14 @@ this version is broken because of a wrong path in the package.json
- add `connectionRadius`
## 0.0.1
## 0.0.1
Svelte Flow alpha is here 🔥 You can expect some changes until we reach 1.0.0 but we try to stick as close as possible to the React Flow API. There are no docs yet, but we are working on it! For now the easiest way is to use the autocomplete of your IDE, lookup the props in the SvelteFlow component or check out the React Flow docs.
Svelte Flow alpha is here 🔥 You can expect some changes until we reach 1.0.0 but we try to stick as close as possible to the React Flow API. There are no docs yet, but we are working on it! For now the easiest way is to use the autocomplete of your IDE, lookup the props in the SvelteFlow component or check out the React Flow docs.
This very first release comes with lots of features already:
- pass `nodes` and `edges` as writables
- draggable, selectable and deletable nodes
- pass `nodes` and `edges` as writables
- draggable, selectable and deletable nodes
- support for custom `nodeTypes` and `edgeTypes`
- basic viewport settings like `fitView`, `minZoom` and `maxZoom`
- additional components: `<MiniMap />`, `<Controls />` & `<Background />`
- additional components: `<MiniMap />`, `<Controls />` & `<Background />`

View File

@@ -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"
},

View File

@@ -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}

View File

@@ -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>

View File

@@ -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'

View File

@@ -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}

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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} />

View File

@@ -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} />

View File

@@ -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>

View File

@@ -83,9 +83,6 @@
on:nodemouseenter
on:nodemousemove
on:nodemouseleave
on:connectstart
on:connect
on:connectend
on:nodedrag
on:nodedragstart
on:nodedragstop

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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) {

View File

@@ -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
);
}

View File

@@ -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]);
}
});
}

View File

@@ -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
};
}

View File

@@ -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 {

View File

@@ -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],

View File

@@ -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)
};
};

View File

@@ -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);
};

View File

@@ -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