Merge branch 'next' into refactor/edge-rendering
This commit is contained in:
@@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = {
|
|||||||
left: 'auto',
|
left: 'auto',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
|
const CustomNode: FC<NodeProps> = ({ data, positionAbsoluteX, positionAbsoluteY }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Handle type="target" position={Position.Top} />
|
<Handle type="target" position={Position.Top} />
|
||||||
@@ -18,7 +18,7 @@ const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
|
|||||||
<div>
|
<div>
|
||||||
Position:{' '}
|
Position:{' '}
|
||||||
<strong>
|
<strong>
|
||||||
{xPos.toFixed(2)},{yPos.toFixed(2)}
|
{positionAbsoluteX.toFixed(2)},{positionAbsoluteY.toFixed(2)}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const initialNodes: Node[] = [
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
data: { label: 'Node 1' },
|
data: { label: 'Node 1' },
|
||||||
position: { x: 250, y: 5 },
|
position: { x: 250, y: 5 },
|
||||||
size: nodeSize,
|
...nodeSize,
|
||||||
handles: [
|
handles: [
|
||||||
{
|
{
|
||||||
type: 'source',
|
type: 'source',
|
||||||
@@ -41,23 +41,19 @@ const initialNodes: Node[] = [
|
|||||||
id: '2',
|
id: '2',
|
||||||
data: { label: 'Node 2' },
|
data: { label: 'Node 2' },
|
||||||
position: { x: 100, y: 100 },
|
position: { x: 100, y: 100 },
|
||||||
size: nodeSize,
|
...nodeSize,
|
||||||
handles: [
|
handles: [
|
||||||
{
|
{
|
||||||
type: 'source',
|
type: 'source',
|
||||||
position: Position.Bottom,
|
position: Position.Bottom,
|
||||||
x: nodeSize.width * 0.5,
|
x: nodeSize.width * 0.5,
|
||||||
y: nodeSize.height,
|
y: nodeSize.height,
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'target',
|
type: 'target',
|
||||||
position: Position.Top,
|
position: Position.Top,
|
||||||
x: nodeSize.width * 0.5,
|
x: nodeSize.width * 0.5,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -65,23 +61,19 @@ const initialNodes: Node[] = [
|
|||||||
id: '3',
|
id: '3',
|
||||||
data: { label: 'Node 3' },
|
data: { label: 'Node 3' },
|
||||||
position: { x: 400, y: 100 },
|
position: { x: 400, y: 100 },
|
||||||
size: nodeSize,
|
...nodeSize,
|
||||||
handles: [
|
handles: [
|
||||||
{
|
{
|
||||||
type: 'source',
|
type: 'source',
|
||||||
position: Position.Bottom,
|
position: Position.Bottom,
|
||||||
x: nodeSize.width * 0.5,
|
x: nodeSize.width * 0.5,
|
||||||
y: nodeSize.height,
|
y: nodeSize.height,
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'target',
|
type: 'target',
|
||||||
position: Position.Top,
|
position: Position.Top,
|
||||||
x: nodeSize.width * 0.5,
|
x: nodeSize.width * 0.5,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,10 +11,8 @@
|
|||||||
data: { label: 'Node 0' },
|
data: { label: 'Node 0' },
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
size: {
|
width: 100,
|
||||||
width: 100,
|
height: 40,
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
handles: [
|
handles: [
|
||||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||||
@@ -26,10 +24,8 @@
|
|||||||
data: { label: 'A' },
|
data: { label: 'A' },
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
size: {
|
width: 100,
|
||||||
width: 100,
|
height: 40,
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
handles: [
|
handles: [
|
||||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||||
@@ -41,10 +37,8 @@
|
|||||||
data: { label: 'B' },
|
data: { label: 'B' },
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
size: {
|
width: 100,
|
||||||
width: 100,
|
height: 40,
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
handles: [
|
handles: [
|
||||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||||
@@ -56,10 +50,8 @@
|
|||||||
data: { label: 'C' },
|
data: { label: 'C' },
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: Position.Right,
|
||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
size: {
|
width: 100,
|
||||||
width: 100,
|
height: 40,
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
handles: [
|
handles: [
|
||||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Basic from '../examples/Basic';
|
import Basic from '../examples/Basic';
|
||||||
import Backgrounds from '../examples/Backgrounds';
|
import Backgrounds from '../examples/Backgrounds';
|
||||||
|
import ColorMode from '../examples/ColorMode';
|
||||||
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
|
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
|
||||||
import ControlledViewport from '../examples/ControlledViewport';
|
import ControlledViewport from '../examples/ControlledViewport';
|
||||||
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
||||||
@@ -20,7 +21,6 @@ import Interaction from '../examples/Interaction';
|
|||||||
import Intersection from '../examples/Intersection';
|
import Intersection from '../examples/Intersection';
|
||||||
import Layouting from '../examples/Layouting';
|
import Layouting from '../examples/Layouting';
|
||||||
import MultiFlows from '../examples/MultiFlows';
|
import MultiFlows from '../examples/MultiFlows';
|
||||||
import NestedNodes from '../examples/NestedNodes';
|
|
||||||
import NodeResizer from '../examples/NodeResizer';
|
import NodeResizer from '../examples/NodeResizer';
|
||||||
import NodeTypeChange from '../examples/NodeTypeChange';
|
import NodeTypeChange from '../examples/NodeTypeChange';
|
||||||
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
||||||
@@ -43,7 +43,9 @@ import CancelConnection from '../examples/CancelConnection';
|
|||||||
import InteractiveMinimap from '../examples/InteractiveMinimap';
|
import InteractiveMinimap from '../examples/InteractiveMinimap';
|
||||||
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
|
import UseOnSelectionChange from '../examples/UseOnSelectionChange';
|
||||||
import NodeToolbar from '../examples/NodeToolbar';
|
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 {
|
export interface IRoute {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -62,6 +64,11 @@ const routes: IRoute[] = [
|
|||||||
path: 'backgrounds',
|
path: 'backgrounds',
|
||||||
component: Backgrounds,
|
component: Backgrounds,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Color Mode',
|
||||||
|
path: 'color-mode',
|
||||||
|
component: ColorMode,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Cancel Connection',
|
name: 'Cancel Connection',
|
||||||
path: 'cancel-connection',
|
path: 'cancel-connection',
|
||||||
@@ -177,11 +184,6 @@ const routes: IRoute[] = [
|
|||||||
path: 'multiflows',
|
path: 'multiflows',
|
||||||
component: MultiFlows,
|
component: MultiFlows,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Nested Nodes',
|
|
||||||
path: 'nested-nodes',
|
|
||||||
component: NestedNodes,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Node Type Change',
|
name: 'Node Type Change',
|
||||||
path: 'nodetype-change',
|
path: 'nodetype-change',
|
||||||
@@ -255,7 +257,7 @@ const routes: IRoute[] = [
|
|||||||
{
|
{
|
||||||
name: 'useNodesInitialized',
|
name: 'useNodesInitialized',
|
||||||
path: 'use-nodes-initialized',
|
path: 'use-nodes-initialized',
|
||||||
component: useNodesInitialized,
|
component: UseNodesInitialized,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'useOnSelectionChange',
|
name: 'useOnSelectionChange',
|
||||||
@@ -267,6 +269,16 @@ const routes: IRoute[] = [
|
|||||||
path: 'usereactflow',
|
path: 'usereactflow',
|
||||||
component: UseReactFlow,
|
component: UseReactFlow,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'useHandleConnections',
|
||||||
|
path: 'usehandleconnections',
|
||||||
|
component: UseHandleConnections,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useNodesData',
|
||||||
|
path: 'usenodesdata',
|
||||||
|
component: UseNodesData,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'useUpdateNodeInternals',
|
name: 'useUpdateNodeInternals',
|
||||||
path: 'useupdatenodeinternals',
|
path: 'useupdatenodeinternals',
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { ChangeEventHandler, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
addEdge,
|
||||||
|
Node,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
OnConnect,
|
||||||
|
Edge,
|
||||||
|
MiniMap,
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
Panel,
|
||||||
|
ColorMode,
|
||||||
|
Position,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
const nodeDefaults = {
|
||||||
|
sourcePosition: Position.Right,
|
||||||
|
targetPosition: Position.Left,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
{ id: 'A', type: 'input', position: { x: 0, y: 150 }, data: { label: 'A' }, ...nodeDefaults },
|
||||||
|
{ id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults },
|
||||||
|
{ id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults },
|
||||||
|
{ id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults },
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = [
|
||||||
|
{
|
||||||
|
id: 'A-B',
|
||||||
|
source: 'A',
|
||||||
|
target: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A-C',
|
||||||
|
source: 'A',
|
||||||
|
target: 'C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'A-D',
|
||||||
|
source: 'A',
|
||||||
|
target: 'D',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ColorModeFlow = () => {
|
||||||
|
const [colorMode, setColorMode] = useState<ColorMode>('light');
|
||||||
|
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
|
const onConnect: OnConnect = useCallback(
|
||||||
|
(params) => {
|
||||||
|
console.log('on connect', params);
|
||||||
|
setEdges((eds) => addEdge(params, eds));
|
||||||
|
},
|
||||||
|
[setEdges]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChange: ChangeEventHandler<HTMLSelectElement> = (evt) => setColorMode(evt.target.value as ColorMode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
colorMode={colorMode}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<MiniMap />
|
||||||
|
<Background />
|
||||||
|
<Controls />
|
||||||
|
|
||||||
|
<Panel position="top-right">
|
||||||
|
<select onChange={onChange} data-testid="colormode-select">
|
||||||
|
<option value="light">light</option>
|
||||||
|
<option value="dark">dark</option>
|
||||||
|
<option value="system">system</option>
|
||||||
|
</select>
|
||||||
|
</Panel>
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorModeFlow;
|
||||||
@@ -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';
|
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from '@xyflow/react';
|
||||||
|
|
||||||
const targetHandleStyle: CSSProperties = { background: '#555' };
|
const targetHandleStyle: CSSProperties = { background: '#555' };
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import CustomNode from './CustomNode';
|
|||||||
import FloatingEdge from './FloatingEdge';
|
import FloatingEdge from './FloatingEdge';
|
||||||
import CustomConnectionLine from './CustomConnectionLine';
|
import CustomConnectionLine from './CustomConnectionLine';
|
||||||
|
|
||||||
import '@xyflow/react/dist/style.css';
|
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { Node, Position, MarkerType, XYPosition } from '@xyflow/react';
|
|||||||
// of the line between the center of the intersectionNode and the target node
|
// of the line between the center of the intersectionNode and the target node
|
||||||
function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
|
function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
|
||||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||||
|
|
||||||
const {
|
const {
|
||||||
width: intersectionNodeWidth,
|
width: intersectionNodeWidth,
|
||||||
height: intersectionNodeHeight,
|
height: intersectionNodeHeight,
|
||||||
positionAbsolute: intersectionNodePosition,
|
positionAbsolute: intersectionNodePosition,
|
||||||
} = intersectionNode;
|
} = intersectionNode.computed || {};
|
||||||
const targetPosition = targetNode.positionAbsolute!;
|
const targetPosition = targetNode.computed?.positionAbsolute!;
|
||||||
|
|
||||||
const w = intersectionNodeWidth! / 2;
|
const w = intersectionNodeWidth! / 2;
|
||||||
const h = intersectionNodeHeight! / 2;
|
const h = intersectionNodeHeight! / 2;
|
||||||
@@ -32,7 +33,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
|
|||||||
|
|
||||||
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
||||||
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||||
const n = { ...node.positionAbsolute, ...node };
|
const n = { ...node.computed?.positionAbsolute, ...node };
|
||||||
const nx = Math.round(n.x!);
|
const nx = Math.round(n.x!);
|
||||||
const ny = Math.round(n.y!);
|
const ny = Math.round(n.y!);
|
||||||
const px = Math.round(intersectionPoint.x);
|
const px = Math.round(intersectionPoint.x);
|
||||||
@@ -41,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
|||||||
if (px <= nx + 1) {
|
if (px <= nx + 1) {
|
||||||
return Position.Left;
|
return Position.Left;
|
||||||
}
|
}
|
||||||
if (px >= nx + n.width! - 1) {
|
if (px >= nx + n.computed?.width! - 1) {
|
||||||
return Position.Right;
|
return Position.Right;
|
||||||
}
|
}
|
||||||
if (py <= ny + 1) {
|
if (py <= ny + 1) {
|
||||||
return Position.Top;
|
return Position.Top;
|
||||||
}
|
}
|
||||||
if (py >= n.y! + n.height! - 1) {
|
if (py >= n.y! + n.computed?.height! - 1) {
|
||||||
return Position.Bottom;
|
return Position.Bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { Position, XYPosition, Node, Edge } from '@xyflow/react';
|
|||||||
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
|
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
|
||||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||||
|
|
||||||
const {
|
const { position: intersectionNodePosition } = intersectionNode;
|
||||||
width: intersectionNodeWidth,
|
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.computed ?? {
|
||||||
height: intersectionNodeHeight,
|
width: 0,
|
||||||
position: intersectionNodePosition,
|
height: 0,
|
||||||
} = intersectionNode;
|
};
|
||||||
const targetPosition = targetNode.position;
|
const targetPosition = targetNode.position;
|
||||||
|
|
||||||
const w = (intersectionNodeWidth ?? 0) / 2;
|
const w = (intersectionNodeWidth ?? 0) / 2;
|
||||||
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
|||||||
if (px <= nx + 1) {
|
if (px <= nx + 1) {
|
||||||
return Position.Left;
|
return Position.Left;
|
||||||
}
|
}
|
||||||
if (px >= nx + (n.width ?? 0) - 1) {
|
if (px >= nx + (n.computed?.width ?? 0) - 1) {
|
||||||
return Position.Right;
|
return Position.Right;
|
||||||
}
|
}
|
||||||
if (py <= ny + 1) {
|
if (py <= ny + 1) {
|
||||||
return Position.Top;
|
return Position.Top;
|
||||||
}
|
}
|
||||||
if (py >= n.y + (n.height ?? 0) - 1) {
|
if (py >= n.y + (n.computed?.height ?? 0) - 1) {
|
||||||
return Position.Bottom;
|
return Position.Bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ const initialEdges: Edge[] = [
|
|||||||
|
|
||||||
const setHidden = (hidden: boolean) => (els: any[]) =>
|
const setHidden = (hidden: boolean) => (els: any[]) =>
|
||||||
els.map((e: any) => {
|
els.map((e: any) => {
|
||||||
e.hidden = hidden;
|
return {
|
||||||
return e;
|
...e,
|
||||||
|
hidden,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const HiddenFlow = () => {
|
const HiddenFlow = () => {
|
||||||
|
|||||||
@@ -56,16 +56,16 @@ const LayoutFlow = () => {
|
|||||||
|
|
||||||
const layoutedNodes = nodes.map((node) => {
|
const layoutedNodes = nodes.map((node) => {
|
||||||
const nodeWithPosition = dagreGraph.node(node.id);
|
const nodeWithPosition = dagreGraph.node(node.id);
|
||||||
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
|
|
||||||
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
|
|
||||||
// we need to pass a slightly different position in order to notify react flow about the change
|
|
||||||
// @TODO how can we change the position handling so that we dont need this hack?
|
|
||||||
node.position = {
|
|
||||||
x: nodeWithPosition.x + Math.random() / 1000,
|
|
||||||
y: nodeWithPosition.y,
|
|
||||||
};
|
|
||||||
|
|
||||||
return node;
|
return {
|
||||||
|
...node,
|
||||||
|
targetPosition: isHorizontal ? Position.Left : Position.Top,
|
||||||
|
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
|
||||||
|
position: {
|
||||||
|
x: nodeWithPosition.x,
|
||||||
|
y: nodeWithPosition.y,
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setNodes(layoutedNodes);
|
setNodes(layoutedNodes);
|
||||||
|
|||||||
@@ -1,187 +0,0 @@
|
|||||||
import { useState, MouseEvent, useCallback } from 'react';
|
|
||||||
import {
|
|
||||||
ReactFlow,
|
|
||||||
Controls,
|
|
||||||
MiniMap,
|
|
||||||
Background,
|
|
||||||
addEdge,
|
|
||||||
useNodesState,
|
|
||||||
useEdgesState,
|
|
||||||
Node,
|
|
||||||
Edge,
|
|
||||||
ReactFlowInstance,
|
|
||||||
Connection,
|
|
||||||
} from '@xyflow/react';
|
|
||||||
|
|
||||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
|
||||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
|
||||||
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
|
|
||||||
|
|
||||||
const initialNodes: Node[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
type: 'input',
|
|
||||||
data: { label: 'Node 1' },
|
|
||||||
position: { x: 250, y: 5 },
|
|
||||||
className: 'light',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
data: { label: 'Node 2' },
|
|
||||||
position: { x: 100, y: 100 },
|
|
||||||
className: 'light',
|
|
||||||
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2a',
|
|
||||||
data: { label: 'Node 2a' },
|
|
||||||
position: { x: 10, y: 50 },
|
|
||||||
parentNode: '2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
data: { label: 'Node 3' },
|
|
||||||
position: { x: 320, y: 100 },
|
|
||||||
className: 'light',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
data: { label: 'Node 4' },
|
|
||||||
position: { x: 320, y: 200 },
|
|
||||||
className: 'light',
|
|
||||||
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4a',
|
|
||||||
data: { label: 'Node 4a' },
|
|
||||||
position: { x: 15, y: 65 },
|
|
||||||
className: 'light',
|
|
||||||
parentNode: '4',
|
|
||||||
extent: 'parent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4b',
|
|
||||||
data: { label: 'Node 4b' },
|
|
||||||
position: { x: 15, y: 120 },
|
|
||||||
className: 'light',
|
|
||||||
style: {
|
|
||||||
backgroundColor: 'rgba(255, 0, 255, 0.7)',
|
|
||||||
height: 150,
|
|
||||||
width: 270,
|
|
||||||
},
|
|
||||||
parentNode: '4',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4b1',
|
|
||||||
data: { label: 'Node 4b1' },
|
|
||||||
position: { x: 20, y: 40 },
|
|
||||||
className: 'light',
|
|
||||||
parentNode: '4b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4b2',
|
|
||||||
data: { label: 'Node 4b2' },
|
|
||||||
position: { x: 100, y: 100 },
|
|
||||||
className: 'light',
|
|
||||||
parentNode: '4b',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const initialEdges: Edge[] = [
|
|
||||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
|
||||||
{ id: 'e1-3', source: '1', target: '3' },
|
|
||||||
{ id: 'e2a-4a', source: '2a', target: '4a' },
|
|
||||||
{ id: 'e3-4', source: '3', target: '4' },
|
|
||||||
{ id: 'e3-4b', source: '3', target: '4b' },
|
|
||||||
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
|
|
||||||
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
|
|
||||||
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const NestedFlow = () => {
|
|
||||||
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
||||||
|
|
||||||
const onConnect = useCallback(
|
|
||||||
(connection: Connection) => {
|
|
||||||
setEdges((eds) => addEdge(connection, eds));
|
|
||||||
},
|
|
||||||
[setEdges]
|
|
||||||
);
|
|
||||||
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
|
|
||||||
|
|
||||||
const updatePos = () => {
|
|
||||||
setNodes((nds) => {
|
|
||||||
return nds.map((n) => {
|
|
||||||
n.position = {
|
|
||||||
x: Math.random() * 400,
|
|
||||||
y: Math.random() * 400,
|
|
||||||
};
|
|
||||||
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const logToObject = () => console.log(rfInstance?.toObject());
|
|
||||||
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
|
|
||||||
|
|
||||||
const toggleClassnames = () => {
|
|
||||||
setNodes((nds) => {
|
|
||||||
return nds.map((n) => {
|
|
||||||
n.className = n.className === 'light' ? 'dark' : 'light';
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleChildNodes = () => {
|
|
||||||
setNodes((nds) => {
|
|
||||||
return nds.map((n) => {
|
|
||||||
n.hidden = !!n.parentNode && !n.hidden;
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactFlow
|
|
||||||
nodes={nodes}
|
|
||||||
edges={edges}
|
|
||||||
onInit={onInit}
|
|
||||||
onNodesChange={onNodesChange}
|
|
||||||
onEdgesChange={onEdgesChange}
|
|
||||||
onNodeClick={onNodeClick}
|
|
||||||
onEdgeClick={onEdgeClick}
|
|
||||||
onConnect={onConnect}
|
|
||||||
onNodeDragStop={onNodeDragStop}
|
|
||||||
className="react-flow-basic-example"
|
|
||||||
minZoom={0.2}
|
|
||||||
maxZoom={4}
|
|
||||||
onlyRenderVisibleElements={false}
|
|
||||||
>
|
|
||||||
<MiniMap pannable />
|
|
||||||
<Controls />
|
|
||||||
<Background />
|
|
||||||
|
|
||||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
|
||||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
|
||||||
reset transform
|
|
||||||
</button>
|
|
||||||
<button onClick={updatePos} style={{ marginRight: 5 }}>
|
|
||||||
change pos
|
|
||||||
</button>
|
|
||||||
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
|
|
||||||
toggle classnames
|
|
||||||
</button>
|
|
||||||
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
|
|
||||||
toggleChildNodes
|
|
||||||
</button>
|
|
||||||
<button onClick={logToObject}>toObject</button>
|
|
||||||
</div>
|
|
||||||
</ReactFlow>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NestedFlow;
|
|
||||||
@@ -16,8 +16,6 @@ import CustomResizer from './CustomResizer';
|
|||||||
import VerticalResizer from './VerticalResizer';
|
import VerticalResizer from './VerticalResizer';
|
||||||
import HorizontalResizer from './HorizontalResizer';
|
import HorizontalResizer from './HorizontalResizer';
|
||||||
|
|
||||||
import '@xyflow/react/dist/style.css';
|
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
defaultResizer: DefaultResizer,
|
defaultResizer: DefaultResizer,
|
||||||
customResizer: CustomResizer,
|
customResizer: CustomResizer,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const onNodeDragStop = (_: ReactMouseEvent, node: Node, nodes: Node[]) => consol
|
|||||||
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
|
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
|
||||||
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
|
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
|
||||||
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
|
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
|
||||||
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event);
|
const onPaneContextMenu = (event: ReactMouseEvent | MouseEvent) => console.log('pane context menu', event);
|
||||||
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
|
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
|
||||||
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
|
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
|
||||||
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
|
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
|
||||||
@@ -230,9 +230,9 @@ const OverviewFlow = () => {
|
|||||||
onEdgesDelete={onEdgesDelete}
|
onEdgesDelete={onEdgesDelete}
|
||||||
onPaneMouseMove={onPaneMouseMove}
|
onPaneMouseMove={onPaneMouseMove}
|
||||||
>
|
>
|
||||||
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
|
<MiniMap nodeBorderRadius={2} />
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background color="#aaa" gap={25} />
|
<Background gap={25} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Edge,
|
Edge,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
import { getNodesAndEdges } from './utils';
|
import { getNodesAndEdges } from './utils';
|
||||||
|
import { FrameRecorder, generateMouseEventParamsTargetingNode, nextFrame } from './performanceUtils';
|
||||||
|
|
||||||
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
|
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
|
||||||
|
|
||||||
@@ -25,6 +26,166 @@ const StressFlow = () => {
|
|||||||
const onConnect = useCallback((connection: Connection) => {
|
const onConnect = useCallback((connection: Connection) => {
|
||||||
setEdges((eds) => addEdge(connection, eds));
|
setEdges((eds) => addEdge(connection, eds));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const dragInViewport = async () => {
|
||||||
|
// Note: selecting specifically node 18, as it’s normally located in the right part of the viewport –
|
||||||
|
// which means dragging it left is safe to do without scrolling the viewport.
|
||||||
|
const nodeElement = document.querySelector('.react-flow__node[data-id="18"]');
|
||||||
|
if (!nodeElement) throw new Error('Node with id 18 not found');
|
||||||
|
|
||||||
|
const frameRecorder = new FrameRecorder();
|
||||||
|
|
||||||
|
// Hold down the mouse
|
||||||
|
frameRecorder.setStage('mousedown');
|
||||||
|
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Start at the node position and move the mouse 5px to the left on every frame
|
||||||
|
frameRecorder.setStage('mousemove');
|
||||||
|
let currentXPosition = mouseDownEvent.clientX;
|
||||||
|
for (let iteration = 0; iteration < 20; ++iteration) {
|
||||||
|
const movementX = -5;
|
||||||
|
currentXPosition += movementX;
|
||||||
|
|
||||||
|
nodeElement.dispatchEvent(
|
||||||
|
new MouseEvent('mousemove', {
|
||||||
|
...mouseDownEvent,
|
||||||
|
clientX: currentXPosition,
|
||||||
|
screenX: currentXPosition,
|
||||||
|
movementX,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await nextFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release the mouse
|
||||||
|
frameRecorder.setStage('mouseup');
|
||||||
|
nodeElement.dispatchEvent(
|
||||||
|
new MouseEvent('mouseup', {
|
||||||
|
...mouseDownEvent,
|
||||||
|
clientX: currentXPosition,
|
||||||
|
screenX: currentXPosition,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Log the results
|
||||||
|
await frameRecorder.endRecordingAsync();
|
||||||
|
console.log('Frame durations:', frameRecorder.getFrames());
|
||||||
|
console.log(
|
||||||
|
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
|
||||||
|
frameRecorder.getFramesForObservable()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragOutsideViewport = async () => {
|
||||||
|
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
|
||||||
|
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
|
||||||
|
if (!nodeElement) throw new Error('Node not found');
|
||||||
|
|
||||||
|
const frameRecorder = new FrameRecorder();
|
||||||
|
|
||||||
|
// Hold down the mouse
|
||||||
|
frameRecorder.setStage('mousedown');
|
||||||
|
const mouseDownEvent = generateMouseEventParamsTargetingNode(nodeElement);
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseDownEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Move the mouse to the top of the viewport (so that the viewport starts
|
||||||
|
// scrolling up). Then, wiggle the mouse up and down to keep the viewport
|
||||||
|
// scrolling.
|
||||||
|
frameRecorder.setStage('mousemove');
|
||||||
|
let currentYPosition = 50;
|
||||||
|
for (let iteration = 0; iteration < 20; ++iteration) {
|
||||||
|
const movementY = Math.random() > 0.5 ? +2 : -2;
|
||||||
|
currentYPosition += movementY;
|
||||||
|
|
||||||
|
nodeElement.dispatchEvent(
|
||||||
|
new MouseEvent('mousemove', {
|
||||||
|
...mouseDownEvent,
|
||||||
|
clientY: currentYPosition,
|
||||||
|
screenY: currentYPosition,
|
||||||
|
movementY,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await nextFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release the mouse
|
||||||
|
frameRecorder.setStage('mouseup');
|
||||||
|
nodeElement.dispatchEvent(
|
||||||
|
new MouseEvent('mouseup', {
|
||||||
|
...mouseDownEvent,
|
||||||
|
clientY: currentYPosition,
|
||||||
|
screenY: currentYPosition,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Log the results
|
||||||
|
await frameRecorder.endRecordingAsync();
|
||||||
|
console.log('Frame durations:', frameRecorder.getFrames());
|
||||||
|
console.log(
|
||||||
|
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
|
||||||
|
frameRecorder.getFramesForObservable()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectNode = async () => {
|
||||||
|
const randomNodeIndex = Math.floor(Math.random() * nodes.length);
|
||||||
|
const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodes[randomNodeIndex].id}"]`);
|
||||||
|
if (!nodeElement) throw new Error('Node not found');
|
||||||
|
|
||||||
|
const frameRecorder = new FrameRecorder();
|
||||||
|
|
||||||
|
const mouseEvent = generateMouseEventParamsTargetingNode(nodeElement);
|
||||||
|
|
||||||
|
// mousedown
|
||||||
|
frameRecorder.setStage('mousedown');
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('mousedown', mouseEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// click
|
||||||
|
frameRecorder.setStage('click');
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('click', mouseEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// mouseup
|
||||||
|
frameRecorder.setStage('mouseup');
|
||||||
|
nodeElement.dispatchEvent(new MouseEvent('mouseup', mouseEvent));
|
||||||
|
await nextFrame();
|
||||||
|
|
||||||
|
// Log the results
|
||||||
|
await frameRecorder.endRecordingAsync();
|
||||||
|
console.log('Frame durations:', frameRecorder.getFrames());
|
||||||
|
console.log(
|
||||||
|
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
|
||||||
|
frameRecorder.getFramesForObservable()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
const frameRecorderRef = useRef<FrameRecorder | null>(null);
|
||||||
|
function remount() {
|
||||||
|
frameRecorderRef.current = new FrameRecorder();
|
||||||
|
setKey((k) => k + 1);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
const frameRecorder = frameRecorderRef.current;
|
||||||
|
if (!frameRecorder) return;
|
||||||
|
|
||||||
|
frameRecorder.endRecordingAsync().then(() => {
|
||||||
|
console.log('Frame durations:', frameRecorder.getFrames());
|
||||||
|
console.log(
|
||||||
|
'Frame durations for Observable (copy and paste to https://observablehq.com/@iamakulov/long-frame-visualizer):',
|
||||||
|
frameRecorder.getFramesForObservable()
|
||||||
|
);
|
||||||
|
|
||||||
|
frameRecorderRef.current = null;
|
||||||
|
});
|
||||||
|
}, [key]);
|
||||||
|
|
||||||
const updatePos = () => {
|
const updatePos = () => {
|
||||||
setNodes((nds) => {
|
setNodes((nds) => {
|
||||||
return nds.map((n) => {
|
return nds.map((n) => {
|
||||||
@@ -56,6 +217,7 @@ const StressFlow = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
key={key}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
@@ -64,11 +226,14 @@ const StressFlow = () => {
|
|||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
fitView
|
fitView
|
||||||
>
|
>
|
||||||
<MiniMap />
|
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background />
|
<Background />
|
||||||
|
|
||||||
<Panel position="top-right">
|
<Panel position="top-right">
|
||||||
|
<button onClick={selectNode}>select node</button>
|
||||||
|
<button onClick={dragInViewport}>drag node within the viewport</button>
|
||||||
|
<button onClick={dragOutsideViewport}>drag node outside of the viewport</button>
|
||||||
|
<button onClick={remount}>re-mount</button>
|
||||||
<button onClick={updatePos}>change pos</button>
|
<button onClick={updatePos}>change pos</button>
|
||||||
<button onClick={updateElements}>update elements</button>
|
<button onClick={updateElements}>update elements</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
type Frame = {
|
||||||
|
duration: number;
|
||||||
|
stage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures and outputs the duration of every frame that happens between the
|
||||||
|
* instance is created and `endRecording()` is called.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const recorder = new FrameRecorder();
|
||||||
|
*
|
||||||
|
* // Do some performance-intensive stuff
|
||||||
|
*
|
||||||
|
* await recorder.endRecordingAsync();
|
||||||
|
*
|
||||||
|
* console.log(recorder.getFrames());
|
||||||
|
* console.log(recorder.getFramesForObservable()); // → paste into https://observablehq.com/@iamakulov/long-frame-visualizer
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class FrameRecorder {
|
||||||
|
private frames: Frame[] = [];
|
||||||
|
private animationFrameId: number;
|
||||||
|
private stage: string = '<no stage>';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
let lastFrameTimestamp = performance.now();
|
||||||
|
|
||||||
|
const measureFrame = () => {
|
||||||
|
const timestamp = performance.now();
|
||||||
|
|
||||||
|
// Visualize the frames in the Performance pane (see the collapsed
|
||||||
|
// “Timings” section) – so it’s easier to see what exactly each frame
|
||||||
|
// captured
|
||||||
|
performance.measure(`frame (${this.stage})`, {
|
||||||
|
start: lastFrameTimestamp,
|
||||||
|
end: timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.frames.push({
|
||||||
|
duration: timestamp - lastFrameTimestamp,
|
||||||
|
stage: this.stage,
|
||||||
|
});
|
||||||
|
|
||||||
|
lastFrameTimestamp = timestamp;
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(measureFrame);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.animationFrameId = requestAnimationFrame(measureFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The method is explicitly marked `async` in its name to make sure the caller
|
||||||
|
// doesn’t forget to `await` it. (Otherwise, some events might be lost.)
|
||||||
|
async endRecordingAsync() {
|
||||||
|
this.setStage('waiting for idle');
|
||||||
|
await new Promise((resolve) => requestIdleCallback(resolve));
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an optional annotation to all subsequent frames. Useful to
|
||||||
|
* differentiate frames from different events – e.g. you can call
|
||||||
|
* `setState("mousedown")` before dispatching a mousedown event, and then
|
||||||
|
* `setState("mouseup")` before a mouseup one.
|
||||||
|
*
|
||||||
|
* When used, will affect both `getFramesForObservable()` and `getFrames()`.
|
||||||
|
*/
|
||||||
|
setStage(stage: string) {
|
||||||
|
this.stage = stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFramesForObservable() {
|
||||||
|
return this.frames.map((frame, index) => ({ ...frame, index }));
|
||||||
|
}
|
||||||
|
|
||||||
|
getFrames() {
|
||||||
|
// Group frames by stage – so you could see which frames originated from `mousedown` vs `mousemove` vs `mouseup` events
|
||||||
|
const framesPerStage: Record<string, number[]> = {};
|
||||||
|
for (const frame of this.frames) {
|
||||||
|
const stage = frame.stage;
|
||||||
|
if (!framesPerStage[stage]) {
|
||||||
|
framesPerStage[stage] = [];
|
||||||
|
}
|
||||||
|
framesPerStage[stage].push(frame.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there’s only one stage, return the frames directly
|
||||||
|
return framesPerStage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a promise that resolves when the next frame starts, and everything
|
||||||
|
* that was already scheduled in the event queue has been processed.
|
||||||
|
*/
|
||||||
|
export function nextFrame() {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates params for a new MouseEvent() that will target the given node.
|
||||||
|
*/
|
||||||
|
export function generateMouseEventParamsTargetingNode(node: Element) {
|
||||||
|
const nodePosition = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Let’s make the event (eg click) happen 5px to the right and 5px to the
|
||||||
|
// bottom of the node’s top-left corner
|
||||||
|
const positionRelativeToNode = {
|
||||||
|
left: 5,
|
||||||
|
top: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientX: Math.round(nodePosition.left + positionRelativeToNode.left),
|
||||||
|
clientY: Math.round(nodePosition.top + positionRelativeToNode.top),
|
||||||
|
movementX: 0,
|
||||||
|
movementY: 0,
|
||||||
|
offsetX: positionRelativeToNode.left,
|
||||||
|
offsetY: positionRelativeToNode.top,
|
||||||
|
screenX: Math.round(nodePosition.left + positionRelativeToNode.left),
|
||||||
|
screenY: Math.round(nodePosition.top + positionRelativeToNode.top),
|
||||||
|
|
||||||
|
// Required boilerplate
|
||||||
|
altKey: false,
|
||||||
|
bubbles: true,
|
||||||
|
button: 0,
|
||||||
|
buttons: 1,
|
||||||
|
cancelBubble: false,
|
||||||
|
cancelable: true,
|
||||||
|
composed: true,
|
||||||
|
ctrlKey: false,
|
||||||
|
currentTarget: null,
|
||||||
|
defaultPrevented: false,
|
||||||
|
detail: 1,
|
||||||
|
eventPhase: 0,
|
||||||
|
fromElement: null,
|
||||||
|
isTrusted: true,
|
||||||
|
metaKey: false,
|
||||||
|
relatedTarget: null,
|
||||||
|
returnValue: true,
|
||||||
|
shiftKey: false,
|
||||||
|
view: window,
|
||||||
|
which: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,13 +11,13 @@ const idStyle: CSSProperties = {
|
|||||||
left: 2,
|
left: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DebugNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
|
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Handle type="target" position={Position.Top} />
|
<Handle type="target" position={Position.Top} />
|
||||||
<div style={idStyle}>{id}</div>
|
<div style={idStyle}>{id}</div>
|
||||||
<div style={infoStyle}>
|
<div style={infoStyle}>
|
||||||
x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex}
|
x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
|
||||||
</div>
|
</div>
|
||||||
<Handle type="source" position={Position.Bottom} />
|
<Handle type="source" position={Position.Bottom} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
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';
|
import styles from './updatenode.module.css';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
|
|||||||
const UpdateNode = () => {
|
const UpdateNode = () => {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const { updateNode } = useReactFlow();
|
||||||
|
|
||||||
const [nodeName, setNodeName] = useState<string>('Node 1');
|
const [nodeName, setNodeName] = useState<string>('Node 1');
|
||||||
const [nodeBg, setNodeBg] = useState<string>('#eee');
|
const [nodeBg, setNodeBg] = useState<string>('#eee');
|
||||||
@@ -23,9 +24,12 @@ const UpdateNode = () => {
|
|||||||
nds.map((n) => {
|
nds.map((n) => {
|
||||||
if (n.id === '1') {
|
if (n.id === '1') {
|
||||||
// it's important that you create a new object here in order to notify react flow about the change
|
// it's important that you create a new object here in order to notify react flow about the change
|
||||||
n.data = {
|
return {
|
||||||
...n.data,
|
...n,
|
||||||
label: nodeName,
|
data: {
|
||||||
|
...n.data,
|
||||||
|
label: nodeName,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +43,10 @@ const UpdateNode = () => {
|
|||||||
nds.map((n) => {
|
nds.map((n) => {
|
||||||
if (n.id === '1') {
|
if (n.id === '1') {
|
||||||
// it's important that you create a new object here in order to notify react flow about the change
|
// it's important that you create a new object here in order to notify react flow about the change
|
||||||
n.style = { ...n.style, backgroundColor: nodeBg };
|
return {
|
||||||
|
...n,
|
||||||
|
style: { ...n.style, backgroundColor: nodeBg },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return n;
|
return n;
|
||||||
@@ -51,8 +58,10 @@ const UpdateNode = () => {
|
|||||||
setNodes((nds) =>
|
setNodes((nds) =>
|
||||||
nds.map((n) => {
|
nds.map((n) => {
|
||||||
if (n.id === '1' || n.id === 'e1-2') {
|
if (n.id === '1' || n.id === 'e1-2') {
|
||||||
// when you update a simple type you can just update the value
|
return {
|
||||||
n.hidden = nodeHidden;
|
...n,
|
||||||
|
hidden: nodeHidden,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return n;
|
return n;
|
||||||
@@ -80,9 +89,19 @@ const UpdateNode = () => {
|
|||||||
<label>hidden:</label>
|
<label>hidden:</label>
|
||||||
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
|
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => updateNode('1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }))}
|
||||||
|
>
|
||||||
|
update position
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UpdateNode;
|
export default () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<UpdateNode />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { memo, FC, useEffect, useCallback } from 'react';
|
||||||
|
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleComponentProps } from '@xyflow/react';
|
||||||
|
|
||||||
|
function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) {
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(connections: Connection[]) => console.log('onConnect handler, node id:', nodeId, connections),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDisconnect = useCallback(
|
||||||
|
(connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections),
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
type: handleProps.type,
|
||||||
|
id: handleProps.id,
|
||||||
|
onConnect,
|
||||||
|
onDisconnect,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
|
||||||
|
}, [connections]);
|
||||||
|
|
||||||
|
return <Handle {...handleProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomNode: FC<NodeProps> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#333', color: '#fff', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<CustomHandle nodeId={id} type="target" position={Position.Left} />
|
||||||
|
<div>node {id}</div>
|
||||||
|
<CustomHandle nodeId={id} type="source" position={Position.Right} id="a" style={{ top: 10 }} />
|
||||||
|
<CustomHandle nodeId={id} type="source" position={Position.Right} id="b" style={{ top: 20 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CustomNode);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { memo, FC, useEffect, useCallback } from 'react';
|
||||||
|
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleComponentProps } from '@xyflow/react';
|
||||||
|
|
||||||
|
function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeId: string }) {
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(connections: Connection[]) => {
|
||||||
|
console.log('onConnect handler, node id:', nodeId, connections);
|
||||||
|
},
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
const onDisconnect = useCallback(
|
||||||
|
(connections: Connection[]) => {
|
||||||
|
console.log('onDisconnect handler, node id:', nodeId, connections);
|
||||||
|
},
|
||||||
|
[nodeId]
|
||||||
|
);
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
type: handleProps.type,
|
||||||
|
id: handleProps.id,
|
||||||
|
onConnect,
|
||||||
|
onDisconnect,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('useEffect, node id:', nodeId, handleProps.type, connections);
|
||||||
|
}, [connections]);
|
||||||
|
|
||||||
|
return <Handle {...handleProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomNode: FC<NodeProps> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#333', color: '#fff', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<CustomHandle nodeId={id} type="target" position={Position.Left} />
|
||||||
|
<div>node {id}</div>
|
||||||
|
<CustomHandle nodeId={id} type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CustomNode);
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
MiniMap,
|
||||||
|
Controls,
|
||||||
|
addEdge,
|
||||||
|
Connection,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
Background,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import MultiHandleNode from './MultiHandleNode';
|
||||||
|
import SingleHandleNode from './SingleHandleNode';
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
multi: MultiHandleNode,
|
||||||
|
single: SingleHandleNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initNodes = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'single',
|
||||||
|
data: {},
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'single',
|
||||||
|
data: {},
|
||||||
|
position: { x: 200, y: -100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'single',
|
||||||
|
data: {},
|
||||||
|
position: { x: 200, y: 100 },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'multi',
|
||||||
|
data: {},
|
||||||
|
position: { x: 400, y: 0 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: 'multi',
|
||||||
|
data: {},
|
||||||
|
position: { x: 600, y: -100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
type: 'multi',
|
||||||
|
data: {},
|
||||||
|
position: { x: 600, y: 100 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initEdges = [
|
||||||
|
{
|
||||||
|
id: 'e1-2',
|
||||||
|
source: '1',
|
||||||
|
target: '2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e1-3',
|
||||||
|
source: '1',
|
||||||
|
target: '3',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'e4a-5',
|
||||||
|
source: '4',
|
||||||
|
sourceHandle: 'a',
|
||||||
|
target: '5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e4b-5',
|
||||||
|
source: '4',
|
||||||
|
sourceHandle: 'b',
|
||||||
|
target: '6',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultEdgeOptions = {
|
||||||
|
animated: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomNodeFlow = () => {
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
|
||||||
|
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
minZoom={0.3}
|
||||||
|
maxZoom={2}
|
||||||
|
colorMode="dark"
|
||||||
|
defaultEdgeOptions={defaultEdgeOptions}
|
||||||
|
>
|
||||||
|
<MiniMap />
|
||||||
|
<Controls />
|
||||||
|
<Background />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomNodeFlow;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { Handle, Position, useHandleConnections, useNodesData } from '@xyflow/react';
|
||||||
|
|
||||||
|
function ResultNode() {
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
type: 'target',
|
||||||
|
});
|
||||||
|
const nodesData = useNodesData(connections.map((connection) => connection.source));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<div>
|
||||||
|
incoming texts:{' '}
|
||||||
|
{nodesData?.filter((nodeData) => nodeData.text !== undefined).map(({ text }, i) => <div key={i}>{text}</div>) ||
|
||||||
|
'none'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ResultNode);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { memo, ChangeEventHandler } from 'react';
|
||||||
|
import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
|
function TextNode({ id, data }: NodeProps) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
|
||||||
|
const onChange: ChangeEventHandler<HTMLInputElement> = (evt) => updateNodeData(id, { text: evt.target.value });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<div>node {id}</div>
|
||||||
|
<div>
|
||||||
|
<input onChange={onChange} value={data.text} />
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TextNode);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { memo, useEffect } from 'react';
|
||||||
|
import { Position, NodeProps, useReactFlow, Handle, useHandleConnections, useNodesData } from '@xyflow/react';
|
||||||
|
|
||||||
|
function UppercaseNode({ id }: NodeProps) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
type: 'target',
|
||||||
|
});
|
||||||
|
const nodeData = useNodesData(connections[0]?.source);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateNodeData(id, { text: nodeData?.text.toUpperCase() });
|
||||||
|
}, [nodeData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
|
<Handle type="target" position={Position.Left} isConnectable={connections.length === 0} />
|
||||||
|
<div>uppercase transform</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(UppercaseNode);
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Controls,
|
||||||
|
addEdge,
|
||||||
|
Connection,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
Background,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import TextNode from './TextNode';
|
||||||
|
import ResultNode from './ResultNode';
|
||||||
|
import UppercaseNode from './UppercaseNode';
|
||||||
|
|
||||||
|
export type TextNode = Node<{ text: string }, 'text'>;
|
||||||
|
export type ResultNode = Node<{}, 'result'>;
|
||||||
|
export type UppercaseNode = Node<{}, 'uppercase'>;
|
||||||
|
export type MyNode = Node<{ text: string }, 'text'> | Node<{}, 'result'> | Node<{}, 'uppercase'>;
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
text: TextNode,
|
||||||
|
result: ResultNode,
|
||||||
|
uppercase: UppercaseNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initNodes: MyNode[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'text',
|
||||||
|
data: {
|
||||||
|
text: 'hello',
|
||||||
|
},
|
||||||
|
position: { x: -100, y: -50 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1a',
|
||||||
|
type: 'uppercase',
|
||||||
|
data: {},
|
||||||
|
position: { x: 100, y: 0 },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'text',
|
||||||
|
data: {
|
||||||
|
text: 'world',
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 100 },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'result',
|
||||||
|
data: {},
|
||||||
|
position: { x: 300, y: 50 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initEdges: Edge[] = [
|
||||||
|
{
|
||||||
|
id: 'e1-1a',
|
||||||
|
source: '1',
|
||||||
|
target: '1a',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e1a-3',
|
||||||
|
source: '1a',
|
||||||
|
target: '3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e2-3',
|
||||||
|
source: '2',
|
||||||
|
target: '3',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CustomNodeFlow = () => {
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
|
||||||
|
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
<Background />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomNodeFlow;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { memo, FC } from 'react';
|
||||||
|
import { Handle, Position, NodeProps, NodeToolbar } from '@xyflow/react';
|
||||||
|
|
||||||
|
const CustomNode: FC<NodeProps> = ({ id, data }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NodeToolbar isVisible={data.toolbarVisible} position={data.toolbarPosition} align={data.toolbarAlign}>
|
||||||
|
<button>delete</button>
|
||||||
|
<button>copy</button>
|
||||||
|
<button>expand</button>
|
||||||
|
</NodeToolbar>
|
||||||
|
<div>{data.label}</div>
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CustomNode);
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Position, type Node } from '@xyflow/react';
|
||||||
|
import ToolbarNode from './components/ToolbarNode';
|
||||||
|
|
||||||
|
const positions = ['top', 'right', 'bottom', 'left'];
|
||||||
|
const alignments = ['start', 'center', 'end'];
|
||||||
|
|
||||||
|
const nodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: 'default-node',
|
||||||
|
type: 'ToolbarNode',
|
||||||
|
data: { label: 'toolbar top', toolbarPosition: Position.Top },
|
||||||
|
position: { x: 0, y: -200 },
|
||||||
|
className: 'react-flow__node-default',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
positions.forEach((position, posIndex) => {
|
||||||
|
alignments.forEach((align, alignIndex) => {
|
||||||
|
const id = `node-${align}-${position}`;
|
||||||
|
nodes.push({
|
||||||
|
id,
|
||||||
|
type: 'ToolbarNode',
|
||||||
|
data: {
|
||||||
|
label: `toolbar ${position} ${align}`,
|
||||||
|
toolbarPosition: position as Position,
|
||||||
|
toolbarAlign: align,
|
||||||
|
toolbarVisible: true,
|
||||||
|
},
|
||||||
|
className: 'react-flow__node-default',
|
||||||
|
position: { x: posIndex * 300, y: alignIndex * 100 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
flowProps: {
|
||||||
|
fitView: true,
|
||||||
|
nodeTypes: {
|
||||||
|
ToolbarNode,
|
||||||
|
},
|
||||||
|
nodes,
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'first-edge',
|
||||||
|
source: 'default-node',
|
||||||
|
target: 'node-start-top',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} satisfies FlowConfig;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import App from './App';
|
import App from './App/index';
|
||||||
|
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|||||||
@@ -4,18 +4,22 @@
|
|||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
'add-node-on-drop',
|
'add-node-on-drop',
|
||||||
|
'color-mode',
|
||||||
'custom-connection-line',
|
'custom-connection-line',
|
||||||
'customnode',
|
'customnode',
|
||||||
'dagre',
|
'dagre',
|
||||||
'drag-n-drop',
|
'drag-n-drop',
|
||||||
'edges',
|
'edges',
|
||||||
'figma',
|
'figma',
|
||||||
|
'handle-connect',
|
||||||
'interaction',
|
'interaction',
|
||||||
'intersections',
|
'intersections',
|
||||||
|
'node-toolbar',
|
||||||
'overview',
|
'overview',
|
||||||
'stress',
|
'stress',
|
||||||
'subflows',
|
'subflows',
|
||||||
'two-way-viewport',
|
'two-way-viewport',
|
||||||
|
'usenodesdata',
|
||||||
'usesvelteflow',
|
'usesvelteflow',
|
||||||
'useupdatenodeinternals',
|
'useupdatenodeinternals',
|
||||||
'validation'
|
'validation'
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let data: $$Props['data'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeToolbar
|
||||||
|
isVisible={data.toolbarVisible}
|
||||||
|
position={data.toolbarPosition}
|
||||||
|
align={data.toolbarAlign}
|
||||||
|
>
|
||||||
|
<button>delete</button>
|
||||||
|
<button>copy</button>
|
||||||
|
<button>expand</button>
|
||||||
|
</NodeToolbar>
|
||||||
|
<div class="node">
|
||||||
|
<div>{data.label}</div>
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.node {
|
||||||
|
width: 200px;
|
||||||
|
height: 50px;
|
||||||
|
border: solid 1px black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Position, type Node } from '@xyflow/svelte';
|
||||||
|
import ToolbarNode from './components/ToolbarNode.svelte';
|
||||||
|
|
||||||
|
const positions = ['top', 'right', 'bottom', 'left'];
|
||||||
|
const alignments = ['start', 'center', 'end'];
|
||||||
|
|
||||||
|
const nodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: 'default-node',
|
||||||
|
type: 'ToolbarNode',
|
||||||
|
data: { label: 'toolbar top', toolbarPosition: Position.Top },
|
||||||
|
position: { x: 0, y: -200 },
|
||||||
|
class: 'react-flow__node-default'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
positions.forEach((position, posIndex) => {
|
||||||
|
alignments.forEach((align, alignIndex) => {
|
||||||
|
const id = `node-${align}-${position}`;
|
||||||
|
nodes.push({
|
||||||
|
id,
|
||||||
|
type: 'ToolbarNode',
|
||||||
|
data: {
|
||||||
|
label: `toolbar ${position} ${align}`,
|
||||||
|
toolbarPosition: position as Position,
|
||||||
|
toolbarAlign: align,
|
||||||
|
toolbarVisible: true
|
||||||
|
},
|
||||||
|
class: 'react-flow__node-default',
|
||||||
|
position: { x: posIndex * 300, y: alignIndex * 100 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
flowProps: {
|
||||||
|
fitView: true,
|
||||||
|
nodeTypes: {
|
||||||
|
ToolbarNode
|
||||||
|
},
|
||||||
|
nodes,
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
id: 'first-edge',
|
||||||
|
source: 'default-node',
|
||||||
|
target: 'node-start-top'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} satisfies FlowConfig;
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { writable } from 'svelte/store';
|
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';
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
@@ -16,16 +22,18 @@
|
|||||||
const nodes = writable<Node[]>(initialNodes);
|
const nodes = writable<Node[]>(initialNodes);
|
||||||
const edges = writable<Edge[]>([]);
|
const edges = writable<Edge[]>([]);
|
||||||
|
|
||||||
let connectingNodeId: string = '0';
|
let connectingNodeId: string | null = '0';
|
||||||
let rect: DOMRectReadOnly;
|
let rect: DOMRectReadOnly;
|
||||||
let id = 1;
|
let id = 1;
|
||||||
const getId = () => `${id++}`;
|
const getId = () => `${id++}`;
|
||||||
|
|
||||||
const { screenToFlowPosition, flowToScreenPosition } = useSvelteFlow();
|
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
|
// 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) {
|
if (targetIsPane) {
|
||||||
const id = getId();
|
const id = getId();
|
||||||
const position = {
|
const position = {
|
||||||
@@ -58,7 +66,7 @@
|
|||||||
$nodes = $nodes;
|
$nodes = $nodes;
|
||||||
$edges = $edges;
|
$edges = $edges;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window />
|
<svelte:window />
|
||||||
@@ -69,11 +77,11 @@
|
|||||||
{edges}
|
{edges}
|
||||||
fitView
|
fitView
|
||||||
fitViewOptions={{ padding: 2 }}
|
fitViewOptions={{ padding: 2 }}
|
||||||
on:connectstart={({ detail: { nodeId } }) => {
|
onconnectstart={(_, { nodeId }) => {
|
||||||
// Memorize the nodeId you start draggin a connection line from a node
|
// Memorize the nodeId you start draggin a connection line from a node
|
||||||
connectingNodeId = nodeId;
|
connectingNodeId = nodeId;
|
||||||
}}
|
}}
|
||||||
on:connectend={handleConnectEnd}
|
onconnectend={handleConnectEnd}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import {
|
||||||
|
SvelteFlow,
|
||||||
|
Controls,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
Position,
|
||||||
|
MiniMap,
|
||||||
|
Panel,
|
||||||
|
type ColorMode
|
||||||
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
|
const nodeDefaults = {
|
||||||
|
sourcePosition: Position.Right,
|
||||||
|
targetPosition: Position.Left
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodes = writable([
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
position: { x: 0, y: 150 },
|
||||||
|
data: { label: 'A' },
|
||||||
|
...nodeDefaults
|
||||||
|
},
|
||||||
|
{ id: 'B', position: { x: 250, y: 0 }, data: { label: 'B' }, ...nodeDefaults },
|
||||||
|
{ id: 'C', position: { x: 250, y: 150 }, data: { label: 'C' }, ...nodeDefaults },
|
||||||
|
{ id: 'D', position: { x: 250, y: 300 }, data: { label: 'D' }, ...nodeDefaults }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const edges = writable([
|
||||||
|
{ id: 'A-B', source: 'A', target: 'B' },
|
||||||
|
{ id: 'A-C', source: 'A', target: 'C' },
|
||||||
|
{ id: 'A-D', source: 'A', target: 'D' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
let colorMode: ColorMode = 'light';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SvelteFlow {nodes} {edges} {colorMode} fitView>
|
||||||
|
<Controls />
|
||||||
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
<MiniMap />
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<select bind:value={colorMode} data-testid="colormode-select">
|
||||||
|
<option value="light">light</option>
|
||||||
|
<option value="dark">dark</option>
|
||||||
|
<option value="system">system</option>
|
||||||
|
</select>
|
||||||
|
</Panel>
|
||||||
|
</SvelteFlow>
|
||||||
@@ -85,9 +85,9 @@
|
|||||||
{nodes}
|
{nodes}
|
||||||
{edges}
|
{edges}
|
||||||
{nodeTypes}
|
{nodeTypes}
|
||||||
style="--background-color: {$bgColor}"
|
style="--xy-background-color: {$bgColor}"
|
||||||
fitView
|
fitView
|
||||||
on:connect={onConnect}
|
onconnect={onConnect}
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background variant={BackgroundVariant.Dots} />
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="custom">
|
<div class="custom">
|
||||||
<Handle type="target" position={Position.Left} on:connect />
|
<Handle type="target" position={Position.Left} />
|
||||||
<div>
|
<div>
|
||||||
Custom Color Picker Node: <strong>{$colorStore}</strong>
|
Custom Color Picker Node: <strong>{$colorStore}</strong>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,14 +20,8 @@
|
|||||||
on:input={(evt) => colorStore.set(evt.currentTarget.value)}
|
on:input={(evt) => colorStore.set(evt.currentTarget.value)}
|
||||||
value={$colorStore}
|
value={$colorStore}
|
||||||
/>
|
/>
|
||||||
<Handle type="source" position={Position.Right} id="a" style="top: 20px;" on:connect />
|
<Handle type="source" position={Position.Right} id="a" style="top: 20px;" />
|
||||||
<Handle
|
<Handle type="source" position={Position.Right} id="b" style="top: auto; bottom: 10px;" />
|
||||||
type="source"
|
|
||||||
position={Position.Right}
|
|
||||||
id="b"
|
|
||||||
style="top: auto; bottom: 10px;"
|
|
||||||
on:connect
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
Background,
|
Background,
|
||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
MarkerType
|
MarkerType,
|
||||||
|
type Connection
|
||||||
} from '@xyflow/svelte';
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
import '@xyflow/svelte/dist/style.css';
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
import ButtonEdge from './ButtonEdge.svelte';
|
||||||
|
import CustomBezierEdge from './CustomBezierEdge.svelte';
|
||||||
|
|
||||||
const nodes = writable([
|
const nodes = writable([
|
||||||
{
|
{
|
||||||
@@ -110,13 +113,14 @@
|
|||||||
id: 'e5-8',
|
id: 'e5-8',
|
||||||
source: '5',
|
source: '5',
|
||||||
target: '8',
|
target: '8',
|
||||||
data: { text: 'custom edge' }
|
type: 'button'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'e5-9',
|
id: 'e5-9',
|
||||||
source: '5',
|
source: '5',
|
||||||
target: '9',
|
target: '9',
|
||||||
data: { text: 'custom edge 2' }
|
type: 'customBezier',
|
||||||
|
label: 'custom bezier'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'e5-6',
|
id: 'e5-6',
|
||||||
@@ -143,9 +147,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const edgeTypes = {
|
||||||
|
button: ButtonEdge,
|
||||||
|
customBezier: CustomBezierEdge
|
||||||
|
};
|
||||||
|
|
||||||
|
function getEdgeId(connection: Connection) {
|
||||||
|
return `edge-${connection.source}-${connection.target}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: console.log('edges', $edges);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SvelteFlow {nodes} {edges} fitView nodeDragThreshold={2} ondelete={console.log}>
|
<SvelteFlow
|
||||||
|
{nodes}
|
||||||
|
{edges}
|
||||||
|
{edgeTypes}
|
||||||
|
fitView
|
||||||
|
nodeDragThreshold={2}
|
||||||
|
onedgecreate={(connection) => {
|
||||||
|
console.log('on edge create', connection);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...connection,
|
||||||
|
id: getEdgeId(connection)
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background variant={BackgroundVariant.Dots} />
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getBezierPath, BaseEdge, type EdgeProps, EdgeLabelRenderer } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = EdgeProps;
|
||||||
|
|
||||||
|
export let id: $$Props['id'] = '';
|
||||||
|
export let source: $$Props['source'] = '';
|
||||||
|
export let target: $$Props['target'] = '';
|
||||||
|
export let animated: $$Props['animated'] = undefined;
|
||||||
|
export let selected: $$Props['selected'] = undefined;
|
||||||
|
export let label: $$Props['label'] = undefined;
|
||||||
|
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||||
|
export let data: $$Props['data'] = undefined;
|
||||||
|
export let style: $$Props['style'] = undefined;
|
||||||
|
export let markerStart: $$Props['markerStart'] = undefined;
|
||||||
|
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||||
|
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||||
|
|
||||||
|
export let sourceX: $$Props['sourceX'];
|
||||||
|
export let sourceY: $$Props['sourceY'];
|
||||||
|
export let sourcePosition: $$Props['sourcePosition'];
|
||||||
|
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
|
||||||
|
|
||||||
|
export let targetX: $$Props['targetX'];
|
||||||
|
export let targetY: $$Props['targetY'];
|
||||||
|
export let targetPosition: $$Props['targetPosition'];
|
||||||
|
export let targetHandleId: $$Props['targetHandleId'] = undefined;
|
||||||
|
|
||||||
|
$: [edgePath, labelX, labelY] = getBezierPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition
|
||||||
|
});
|
||||||
|
|
||||||
|
source;
|
||||||
|
target;
|
||||||
|
animated;
|
||||||
|
selected;
|
||||||
|
data;
|
||||||
|
label;
|
||||||
|
labelStyle;
|
||||||
|
markerStart;
|
||||||
|
interactionWidth;
|
||||||
|
sourceHandleId;
|
||||||
|
targetHandleId;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BaseEdge path={edgePath} {markerEnd} {style} />
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<div
|
||||||
|
class="edgeButtonContainer nodrag nopan"
|
||||||
|
style:transform="translate(-50%, -50%) translate({labelX}px,{labelY}px)"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="edgeButton"
|
||||||
|
on:click={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
alert(`remove ${id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.edgeButtonContainer {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 12pt;
|
||||||
|
/* everything inside EdgeLabelRenderer has no pointer events by default */
|
||||||
|
/* if you have an interactive element, set pointer-events: all */
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edgeButton {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: #eee;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edgeButton:hover {
|
||||||
|
box-shadow: 0 0 6px 2px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { BezierEdge, type EdgeProps } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = EdgeProps;
|
||||||
|
|
||||||
|
export let id: $$Props['id'] = '';
|
||||||
|
export let source: $$Props['source'] = '';
|
||||||
|
export let target: $$Props['target'] = '';
|
||||||
|
export let animated: $$Props['animated'] = undefined;
|
||||||
|
export let selected: $$Props['selected'] = undefined;
|
||||||
|
export let label: $$Props['label'] = undefined;
|
||||||
|
export let labelStyle: $$Props['labelStyle'] = undefined;
|
||||||
|
export let data: $$Props['data'] = undefined;
|
||||||
|
export let style: $$Props['style'] = undefined;
|
||||||
|
export let markerStart: $$Props['markerStart'] = undefined;
|
||||||
|
export let markerEnd: $$Props['markerEnd'] = undefined;
|
||||||
|
export let interactionWidth: $$Props['interactionWidth'] = undefined;
|
||||||
|
|
||||||
|
export let sourceX: $$Props['sourceX'];
|
||||||
|
export let sourceY: $$Props['sourceY'];
|
||||||
|
export let sourcePosition: $$Props['sourcePosition'];
|
||||||
|
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
|
||||||
|
|
||||||
|
export let targetX: $$Props['targetX'];
|
||||||
|
export let targetY: $$Props['targetY'];
|
||||||
|
export let targetPosition: $$Props['targetPosition'];
|
||||||
|
export let targetHandleId: $$Props['targetHandleId'] = undefined;
|
||||||
|
|
||||||
|
id;
|
||||||
|
source;
|
||||||
|
target;
|
||||||
|
animated;
|
||||||
|
selected;
|
||||||
|
data;
|
||||||
|
sourcePosition;
|
||||||
|
targetPosition;
|
||||||
|
sourceHandleId;
|
||||||
|
targetHandleId;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BezierEdge
|
||||||
|
{sourceX}
|
||||||
|
{sourceY}
|
||||||
|
{sourcePosition}
|
||||||
|
{targetX}
|
||||||
|
{targetY}
|
||||||
|
{targetPosition}
|
||||||
|
{label}
|
||||||
|
{labelStyle}
|
||||||
|
{markerStart}
|
||||||
|
{markerEnd}
|
||||||
|
{style}
|
||||||
|
{interactionWidth}
|
||||||
|
pathOptions={{ curvature: 1 }}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import {
|
||||||
|
SvelteFlow,
|
||||||
|
Controls,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
MiniMap,
|
||||||
|
type Node,
|
||||||
|
type NodeTypes,
|
||||||
|
type Edge
|
||||||
|
} from '@xyflow/svelte';
|
||||||
|
import SingleHandleNode from './SingleHandleNode.svelte';
|
||||||
|
import MultiHandleNode from './MultiHandleNode.svelte';
|
||||||
|
|
||||||
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
|
const nodeTypes: NodeTypes = {
|
||||||
|
single: SingleHandleNode,
|
||||||
|
multi: MultiHandleNode
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodes = writable<Node[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'single',
|
||||||
|
data: {},
|
||||||
|
position: { x: 0, y: 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'single',
|
||||||
|
data: {},
|
||||||
|
position: { x: 200, y: -100 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'single',
|
||||||
|
data: {},
|
||||||
|
position: { x: 200, y: 100 }
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'multi',
|
||||||
|
data: {},
|
||||||
|
position: { x: 400, y: 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
type: 'multi',
|
||||||
|
data: {},
|
||||||
|
position: { x: 600, y: -100 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6',
|
||||||
|
type: 'multi',
|
||||||
|
data: {},
|
||||||
|
position: { x: 600, y: 100 }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const edges = writable<Edge[]>([
|
||||||
|
{
|
||||||
|
id: 'e1-2',
|
||||||
|
source: '1',
|
||||||
|
target: '2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e1-3',
|
||||||
|
source: '1',
|
||||||
|
target: '3'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'e4a-5',
|
||||||
|
source: '4',
|
||||||
|
sourceHandle: 'a',
|
||||||
|
target: '5'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e4b-5',
|
||||||
|
source: '4',
|
||||||
|
sourceHandle: 'b',
|
||||||
|
target: '6'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SvelteFlow {nodes} {edges} {nodeTypes} fitView colorMode="dark">
|
||||||
|
<Controls />
|
||||||
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
<MiniMap />
|
||||||
|
</SvelteFlow>
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Handle,
|
||||||
|
Position,
|
||||||
|
type NodeProps,
|
||||||
|
type Connection,
|
||||||
|
useHandleConnections
|
||||||
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let id: $$Props['id'];
|
||||||
|
|
||||||
|
function onConnectTarget(connection: Connection[]) {
|
||||||
|
console.log('connect target', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConnectSource(handleId: string, connection: Connection[]) {
|
||||||
|
console.log('connect source', handleId, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnectTarget(connection: Connection[]) {
|
||||||
|
console.log('disconnect target', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnectSource(handleId: string, connection: Connection[]) {
|
||||||
|
console.log('disconnect source', handleId, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connections = useHandleConnections({ nodeId: id, type: 'target' });
|
||||||
|
|
||||||
|
$: {
|
||||||
|
console.log('connections', id, $connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
export let data: $$Props['data'];
|
||||||
|
export let targetPosition: $$Props['targetPosition'] = Position.Top;
|
||||||
|
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
|
||||||
|
export let width: $$Props['width'] = undefined;
|
||||||
|
export let height: $$Props['height'] = undefined;
|
||||||
|
export let selected: $$Props['selected'] = undefined;
|
||||||
|
export let type: $$Props['type'] = undefined;
|
||||||
|
export let zIndex: $$Props['zIndex'] = undefined;
|
||||||
|
export let dragging: $$Props['dragging'] = false;
|
||||||
|
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||||
|
export let positionAbsolute: $$Props['positionAbsolute'] = {
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
};
|
||||||
|
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||||
|
|
||||||
|
data;
|
||||||
|
targetPosition;
|
||||||
|
sourcePosition;
|
||||||
|
width;
|
||||||
|
height;
|
||||||
|
selected;
|
||||||
|
type;
|
||||||
|
zIndex;
|
||||||
|
dragging;
|
||||||
|
dragHandle;
|
||||||
|
positionAbsolute;
|
||||||
|
isConnectable;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="custom">
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
onconnect={onConnectTarget}
|
||||||
|
ondisconnect={onDisconnectTarget}
|
||||||
|
/>
|
||||||
|
<div>node {id}</div>
|
||||||
|
<Handle
|
||||||
|
id="a"
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
onconnect={(connections) => onConnectSource('a', connections)}
|
||||||
|
ondisconnect={(connections) => onDisconnectSource('a', connections)}
|
||||||
|
class="source-a"
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
id="b"
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
onconnect={(connections) => onConnectSource('b', connections)}
|
||||||
|
ondisconnect={(connections) => onDisconnectSource('b', connections)}
|
||||||
|
class="source-b"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom {
|
||||||
|
background-color: #333;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom :global(.source-a) {
|
||||||
|
top: 5px;
|
||||||
|
transform: translate(50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom :global(.source-b) {
|
||||||
|
bottom: 5px;
|
||||||
|
top: auto;
|
||||||
|
transform: translate(50%, 0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Handle, Position, type NodeProps, type Connection } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let id: $$Props['id'];
|
||||||
|
|
||||||
|
function onConnectTarget(connection: Connection[]) {
|
||||||
|
console.log('connect target', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConnectSource(connection: Connection[]) {
|
||||||
|
console.log('connect source', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnectTarget(connection: Connection[]) {
|
||||||
|
console.log('disconnect target', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnectSource(connection: Connection[]) {
|
||||||
|
console.log('disconnect source', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
export let data: $$Props['data'];
|
||||||
|
export let targetPosition: $$Props['targetPosition'] = Position.Top;
|
||||||
|
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom;
|
||||||
|
export let width: $$Props['width'] = undefined;
|
||||||
|
export let height: $$Props['height'] = undefined;
|
||||||
|
export let selected: $$Props['selected'] = undefined;
|
||||||
|
export let type: $$Props['type'] = undefined;
|
||||||
|
export let zIndex: $$Props['zIndex'] = undefined;
|
||||||
|
export let dragging: $$Props['dragging'] = false;
|
||||||
|
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||||
|
export let positionAbsolute: $$Props['positionAbsolute'] = {
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
};
|
||||||
|
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||||
|
|
||||||
|
data;
|
||||||
|
targetPosition;
|
||||||
|
sourcePosition;
|
||||||
|
width;
|
||||||
|
height;
|
||||||
|
selected;
|
||||||
|
type;
|
||||||
|
zIndex;
|
||||||
|
dragging;
|
||||||
|
dragHandle;
|
||||||
|
positionAbsolute;
|
||||||
|
isConnectable;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="custom">
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
onconnect={onConnectTarget}
|
||||||
|
ondisconnect={onDisconnectTarget}
|
||||||
|
/>
|
||||||
|
<div>node {id}</div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
onconnect={onConnectSource}
|
||||||
|
ondisconnect={onDisconnectSource}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom {
|
||||||
|
background-color: #333;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import {
|
||||||
|
SvelteFlow,
|
||||||
|
Background,
|
||||||
|
type Edge,
|
||||||
|
type Node,
|
||||||
|
Position,
|
||||||
|
type NodeTypes
|
||||||
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
|
import CustomNode from './CustomNode.svelte';
|
||||||
|
import SelectedNodesToolbar from './SelectedNodesToolbar.svelte';
|
||||||
|
|
||||||
|
const nodeTypes: NodeTypes = {
|
||||||
|
custom: CustomNode
|
||||||
|
};
|
||||||
|
|
||||||
|
const positions = ['top', 'right', 'bottom', 'left'];
|
||||||
|
const alignments = ['start', 'center', 'end'];
|
||||||
|
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: 'default-node',
|
||||||
|
type: 'custom',
|
||||||
|
data: { label: 'toolbar top', toolbarPosition: Position.Top },
|
||||||
|
position: { x: 0, y: -200 },
|
||||||
|
class: 'react-flow__node-default'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
positions.forEach((position, posIndex) => {
|
||||||
|
alignments.forEach((align, alignIndex) => {
|
||||||
|
const id = `node-${align}-${position}`;
|
||||||
|
initialNodes.push({
|
||||||
|
id,
|
||||||
|
type: 'custom',
|
||||||
|
data: {
|
||||||
|
label: `toolbar ${position} ${align}`,
|
||||||
|
toolbarPosition: position as Position,
|
||||||
|
toolbarAlign: align,
|
||||||
|
toolbarVisible: true
|
||||||
|
},
|
||||||
|
class: 'react-flow__node-default',
|
||||||
|
position: { x: posIndex * 300, y: alignIndex * 100 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = [];
|
||||||
|
|
||||||
|
const nodes = writable<Node[]>(initialNodes);
|
||||||
|
const edges = writable<Edge[]>(initialEdges);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="height: 100vh;">
|
||||||
|
<SvelteFlow {nodes} {edges} {nodeTypes} fitView>
|
||||||
|
<Background />
|
||||||
|
<SelectedNodesToolbar />
|
||||||
|
</SvelteFlow>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { NodeToolbar, type NodeProps, Handle, Position } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let data: $$Props['data'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeToolbar
|
||||||
|
isVisible={data.toolbarVisible}
|
||||||
|
position={data.toolbarPosition}
|
||||||
|
align={data.toolbarAlign}
|
||||||
|
>
|
||||||
|
<button>delete</button>
|
||||||
|
<button>copy</button>
|
||||||
|
<button>expand</button>
|
||||||
|
</NodeToolbar>
|
||||||
|
<div class="node">
|
||||||
|
<div>{data.label}</div>
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.node {
|
||||||
|
width: 180px;
|
||||||
|
height: 50px;
|
||||||
|
border: solid 1px black;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { NodeToolbar, useNodes } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
const nodes = useNodes();
|
||||||
|
|
||||||
|
$: selectedNodeIds = $nodes.filter((node) => node.selected).map((node) => node.id);
|
||||||
|
$: isVisible = selectedNodeIds.length > 1;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeToolbar nodeId={selectedNodeIds} {isVisible}>
|
||||||
|
<button>Selection action</button>
|
||||||
|
</NodeToolbar>
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
type EdgeTypes,
|
type EdgeTypes,
|
||||||
type Node,
|
type Node,
|
||||||
type Edge,
|
type Edge,
|
||||||
ConnectionMode
|
ConnectionMode,
|
||||||
|
useSvelteFlow
|
||||||
} from '@xyflow/svelte';
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
import CustomNode from './CustomNode.svelte';
|
import CustomNode from './CustomNode.svelte';
|
||||||
@@ -150,9 +151,9 @@
|
|||||||
on:nodemouseenter={(event) => console.log('on node enter', event)}
|
on:nodemouseenter={(event) => console.log('on node enter', event)}
|
||||||
on:nodemouseleave={(event) => console.log('on node leave', event)}
|
on:nodemouseleave={(event) => console.log('on node leave', event)}
|
||||||
on:edgeclick={(event) => console.log('edge click', event)}
|
on:edgeclick={(event) => console.log('edge click', event)}
|
||||||
on:connectstart={(event) => console.log('on connect start', event)}
|
onconnectstart={(event) => console.log('on connect start', event)}
|
||||||
on:connect={(event) => console.log('on connect', event)}
|
onconnect={(event) => console.log('on connect', event)}
|
||||||
on:connectend={(event) => console.log('on connect end', event)}
|
onconnectend={(event) => console.log('on connect end', event)}
|
||||||
on:paneclick={(event) => console.log('on pane click', event)}
|
on:paneclick={(event) => console.log('on pane click', event)}
|
||||||
on:panecontextmenu={(event) => {
|
on:panecontextmenu={(event) => {
|
||||||
console.log('on pane contextmenu', event);
|
console.log('on pane contextmenu', event);
|
||||||
@@ -174,6 +175,8 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log('on edge contextmenu', edge);
|
console.log('on edge contextmenu', edge);
|
||||||
}}
|
}}
|
||||||
|
on:selectionclick={(event) => console.log('on selection click', event)}
|
||||||
|
on:selectioncontextmenu={(event) => console.log('on selection contextmenu', event)}
|
||||||
autoPanOnConnect
|
autoPanOnConnect
|
||||||
autoPanOnNodeDrag
|
autoPanOnNodeDrag
|
||||||
connectionMode={ConnectionMode.Strict}
|
connectionMode={ConnectionMode.Strict}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
type $$Props = NodeProps;
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
export let data: { label: string } = { label: 'Node' };
|
export let data: { label: string } = { label: 'Node' };
|
||||||
export let xPos: number = 0;
|
export let positionAbsoluteX: number = 0;
|
||||||
export let yPos: number = 0;
|
export let positionAbsoluteY: number = 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="custom">
|
<div class="custom">
|
||||||
<div>{data.label}</div>
|
<div>{data.label}</div>
|
||||||
<div>{~~xPos}, {~~yPos}</div>
|
<div>{~~positionAbsoluteX}, {~~positionAbsoluteY}</div>
|
||||||
|
|
||||||
<Handle type="target" position={Position.Top} />
|
<Handle type="target" position={Position.Top} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
import { Handle, Position, type NodeProps, type XYPosition } from '@xyflow/svelte';
|
||||||
|
|
||||||
type $$Props = NodeProps;
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let xPos: number = 0;
|
export let positionAbsolute: XYPosition = { x: 0, y: 0 };
|
||||||
export let yPos: number = 0;
|
|
||||||
export let zIndex: number = 0;
|
export let zIndex: number = 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Handle type="target" position={Position.Top} />
|
<Handle type="target" position={Position.Top} />
|
||||||
<div>{id}</div>
|
<div>{id}</div>
|
||||||
<div>
|
<div>
|
||||||
x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex}
|
x:{Math.round(positionAbsolute.x)} y:{Math.round(positionAbsolute.y)} z:{zIndex}
|
||||||
</div>
|
</div>
|
||||||
<Handle type="source" position={Position.Bottom} />
|
<Handle type="source" position={Position.Bottom} />
|
||||||
|
|
||||||
<style>
|
|
||||||
.custom {
|
|
||||||
background: #ffcc00;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import {
|
||||||
|
SvelteFlow,
|
||||||
|
Controls,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
MiniMap,
|
||||||
|
type Node,
|
||||||
|
type NodeTypes,
|
||||||
|
type Edge
|
||||||
|
} from '@xyflow/svelte';
|
||||||
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
|
import TextNode from './TextNode.svelte';
|
||||||
|
import UppercaseNode from './UppercaseNode.svelte';
|
||||||
|
import ResultNode from './ResultNode.svelte';
|
||||||
|
|
||||||
|
const nodeTypes: NodeTypes = {
|
||||||
|
text: TextNode,
|
||||||
|
uppercase: UppercaseNode,
|
||||||
|
result: ResultNode
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodes = writable<Node[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'text',
|
||||||
|
data: {
|
||||||
|
text: 'hello'
|
||||||
|
},
|
||||||
|
position: { x: -100, y: -50 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1a',
|
||||||
|
type: 'uppercase',
|
||||||
|
data: {},
|
||||||
|
position: { x: 100, y: 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'text',
|
||||||
|
data: {
|
||||||
|
text: 'world'
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 100 }
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'result',
|
||||||
|
data: {},
|
||||||
|
position: { x: 300, y: 50 }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const edges = writable<Edge[]>([
|
||||||
|
{
|
||||||
|
id: 'e1-1a',
|
||||||
|
source: '1',
|
||||||
|
target: '1a'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e1a-3',
|
||||||
|
source: '1a',
|
||||||
|
target: '3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e2-3',
|
||||||
|
source: '2',
|
||||||
|
target: '3'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SvelteFlow {nodes} {edges} {nodeTypes} fitView>
|
||||||
|
<Controls />
|
||||||
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
<MiniMap />
|
||||||
|
</SvelteFlow>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Handle,
|
||||||
|
Position,
|
||||||
|
useHandleConnections,
|
||||||
|
useNodesData,
|
||||||
|
type NodeProps
|
||||||
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let id: $$Props['id'];
|
||||||
|
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
nodeId: id,
|
||||||
|
type: 'target'
|
||||||
|
});
|
||||||
|
|
||||||
|
$: nodeData = useNodesData($connections.map((connection) => connection.source));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="custom">
|
||||||
|
<Handle type="target" position={Position.Left} />
|
||||||
|
<div>incoming texts:</div>
|
||||||
|
|
||||||
|
{#each $nodeData as data}
|
||||||
|
<div>{data.text}</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom {
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Handle, Position, type NodeProps, useSvelteFlow } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let id: $$Props['id'];
|
||||||
|
export let data: $$Props['data'];
|
||||||
|
|
||||||
|
const { updateNodeData } = useSvelteFlow();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="custom">
|
||||||
|
<div>node {id}</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
value={data.text}
|
||||||
|
on:input={(evt) => updateNodeData(id, { text: evt.currentTarget.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom {
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
Handle,
|
||||||
|
Position,
|
||||||
|
useHandleConnections,
|
||||||
|
useNodesData,
|
||||||
|
useSvelteFlow,
|
||||||
|
type NodeProps
|
||||||
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
|
type $$Props = NodeProps;
|
||||||
|
|
||||||
|
export let id: $$Props['id'];
|
||||||
|
|
||||||
|
const { updateNodeData } = useSvelteFlow();
|
||||||
|
const connections = useHandleConnections({
|
||||||
|
nodeId: id,
|
||||||
|
type: 'target'
|
||||||
|
});
|
||||||
|
|
||||||
|
$: nodeData = useNodesData($connections[0]?.source);
|
||||||
|
|
||||||
|
$: {
|
||||||
|
updateNodeData(id, { text: $nodeData?.text?.toUpperCase() || '' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="custom">
|
||||||
|
<Handle type="target" position={Position.Left} isConnectable={$connections.length === 0} />
|
||||||
|
<div>uppercase transform</div>
|
||||||
|
<Handle type="source" position={Position.Right} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom {
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,15 +15,9 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Handle type="target" position={Position.Top} on:connect />
|
<Handle type="target" position={Position.Top} />
|
||||||
<button on:click={onClick}>add handle</button>
|
<button on:click={onClick}>add handle</button>
|
||||||
|
|
||||||
{#each Array.from({ length: handleCount }) as handle, i}
|
{#each Array.from({ length: handleCount }) as handle, i}
|
||||||
<Handle
|
<Handle type="source" position={Position.Bottom} id={`${i}`} style={`left: ${i * 10}px;`} />
|
||||||
type="source"
|
|
||||||
position={Position.Bottom}
|
|
||||||
id={`${i}`}
|
|
||||||
style={`left: ${i * 10}px;`}
|
|
||||||
on:connect
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
Background,
|
Background,
|
||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
type NodeTypes
|
type NodeTypes,
|
||||||
|
useSvelteFlow,
|
||||||
|
Panel
|
||||||
} from '@xyflow/svelte';
|
} from '@xyflow/svelte';
|
||||||
|
|
||||||
import CustomNode from './CustomNode.svelte';
|
import CustomNode from './CustomNode.svelte';
|
||||||
@@ -50,6 +52,12 @@
|
|||||||
target: '3'
|
target: '3'
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { updateNode } = useSvelteFlow();
|
||||||
|
|
||||||
|
const updateNodePosition = () => {
|
||||||
|
updateNode('1', (node) => ({ position: { x: node.position.x + 10, y: node.position.y } }));
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@@ -57,6 +65,8 @@
|
|||||||
<Controls />
|
<Controls />
|
||||||
<Background variant={BackgroundVariant.Dots} />
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
|
||||||
|
<Panel><button on:click={updateNodePosition}>update node</button></Panel>
|
||||||
</SvelteFlow>
|
</SvelteFlow>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ function Background({
|
|||||||
{
|
{
|
||||||
...style,
|
...style,
|
||||||
...containerStyle,
|
...containerStyle,
|
||||||
'--background-color-props': bgColor,
|
'--xy-background-color-props': bgColor,
|
||||||
'--background-pattern-color-props': color,
|
'--xy-background-pattern-color-props': color,
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ type DotPatternProps = {
|
|||||||
|
|
||||||
export function DotPattern({ radius, className }: DotPatternProps) {
|
export function DotPattern({ radius, className }: DotPatternProps) {
|
||||||
return (
|
return (
|
||||||
<circle cx={radius} cy={radius} r={radius} className={cc(['react-flow__background-pattern', 'dot', className])} />
|
<circle cx={radius} cy={radius} r={radius} className={cc(['react-flow__background-pattern', 'dots', className])} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { memo, useEffect, useRef, type MouseEvent, useCallback } from 'react';
|
import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
|
import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
|
||||||
@@ -40,15 +40,15 @@ const ARIA_LABEL_KEY = 'react-flow__minimap-desc';
|
|||||||
function MiniMap({
|
function MiniMap({
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
nodeStrokeColor = 'transparent',
|
nodeStrokeColor,
|
||||||
nodeColor = '#e2e2e2',
|
nodeColor,
|
||||||
nodeClassName = '',
|
nodeClassName = '',
|
||||||
nodeBorderRadius = 5,
|
nodeBorderRadius = 5,
|
||||||
nodeStrokeWidth = 2,
|
nodeStrokeWidth,
|
||||||
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
|
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
|
||||||
// a component properly.
|
// a component properly.
|
||||||
nodeComponent,
|
nodeComponent,
|
||||||
maskColor = 'rgb(240, 240, 240, 0.6)',
|
maskColor,
|
||||||
maskStrokeColor = 'none',
|
maskStrokeColor = 'none',
|
||||||
maskStrokeWidth = 1,
|
maskStrokeWidth = 1,
|
||||||
position = 'bottom-right',
|
position = 'bottom-right',
|
||||||
@@ -126,7 +126,15 @@ function MiniMap({
|
|||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
position={position}
|
position={position}
|
||||||
style={style}
|
style={
|
||||||
|
{
|
||||||
|
...style,
|
||||||
|
'--xy-minimap-mask-color-props': typeof maskColor === 'string' ? maskColor : undefined,
|
||||||
|
'--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined,
|
||||||
|
'--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined,
|
||||||
|
'--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
className={cc(['react-flow__minimap', className])}
|
className={cc(['react-flow__minimap', className])}
|
||||||
data-testid="rf__minimap"
|
data-testid="rf__minimap"
|
||||||
>
|
>
|
||||||
@@ -153,7 +161,6 @@ function MiniMap({
|
|||||||
className="react-flow__minimap-mask"
|
className="react-flow__minimap-mask"
|
||||||
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
|
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
|
||||||
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
|
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
|
||||||
fill={maskColor}
|
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
stroke={maskStrokeColor}
|
stroke={maskStrokeColor}
|
||||||
strokeWidth={maskStrokeWidth}
|
strokeWidth={maskStrokeWidth}
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ function MiniMapNode({
|
|||||||
ry={borderRadius}
|
ry={borderRadius}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
fill={fill}
|
style={{
|
||||||
stroke={strokeColor}
|
fill,
|
||||||
strokeWidth={strokeWidth}
|
stroke: strokeColor,
|
||||||
|
strokeWidth,
|
||||||
|
}}
|
||||||
shapeRendering={shapeRendering}
|
shapeRendering={shapeRendering}
|
||||||
onClick={onClick ? (event) => onClick(event, id) : undefined}
|
onClick={onClick ? (event) => onClick(event, id) : undefined}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { memo } from 'react';
|
import { ComponentType, memo } from 'react';
|
||||||
|
import { NodeOrigin, getNodePositionWithOrigin } from '@xyflow/system';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { getNodePositionWithOrigin } from '@xyflow/system';
|
|
||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import type { ReactFlowState } from '../../types';
|
import type { ReactFlowState } from '../../types';
|
||||||
import MiniMapNode from './MiniMapNode';
|
import MiniMapNode from './MiniMapNode';
|
||||||
import type { MiniMapNodes, GetMiniMapNodeAttribute } from './types';
|
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
|
||||||
|
|
||||||
declare const window: any;
|
declare const window: any;
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => s.nodeOrigin;
|
const selector = (s: ReactFlowState) => s.nodeOrigin;
|
||||||
const selectorNodes = (s: ReactFlowState) => s.nodes.filter((node) => !node.hidden && node.width && node.height);
|
const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id);
|
||||||
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
|
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
|
||||||
|
|
||||||
function MiniMapNodes({
|
function MiniMapNodes({
|
||||||
nodeStrokeColor = 'transparent',
|
nodeStrokeColor,
|
||||||
nodeColor = '#e2e2e2',
|
nodeColor,
|
||||||
nodeClassName = '',
|
nodeClassName = '',
|
||||||
nodeBorderRadius = 5,
|
nodeBorderRadius = 5,
|
||||||
nodeStrokeWidth = 2,
|
nodeStrokeWidth,
|
||||||
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
|
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
|
||||||
// a component properly.
|
// a component properly.
|
||||||
nodeComponent: NodeComponent = MiniMapNode,
|
nodeComponent: NodeComponent = MiniMapNode,
|
||||||
onClick,
|
onClick,
|
||||||
}: MiniMapNodes) {
|
}: MiniMapNodesProps) {
|
||||||
const nodes = useStore(selectorNodes, shallow);
|
const nodeIds = useStore(selectorNodeIds, shallow);
|
||||||
const nodeOrigin = useStore(selector);
|
const nodeOrigin = useStore(selector);
|
||||||
const nodeColorFunc = getAttrFunction(nodeColor);
|
const nodeColorFunc = getAttrFunction(nodeColor);
|
||||||
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
||||||
@@ -36,31 +36,78 @@ function MiniMapNodes({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{nodes.map((node) => {
|
{nodeIds.map((nodeId) => (
|
||||||
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
// The split of responsibilities between MiniMapNodes and
|
||||||
|
// NodeComponentWrapper may appear weird. However, it’s designed to
|
||||||
return (
|
// minimize the cost of updates when individual nodes change.
|
||||||
<NodeComponent
|
//
|
||||||
key={node.id}
|
// For more details, see a similar commit in `NodeRenderer/index.tsx`.
|
||||||
x={x}
|
<NodeComponentWrapper
|
||||||
y={y}
|
key={nodeId}
|
||||||
width={node.width!}
|
id={nodeId}
|
||||||
height={node.height!}
|
nodeOrigin={nodeOrigin}
|
||||||
style={node.style}
|
nodeColorFunc={nodeColorFunc}
|
||||||
selected={!!node.selected}
|
nodeStrokeColorFunc={nodeStrokeColorFunc}
|
||||||
className={nodeClassNameFunc(node)}
|
nodeClassNameFunc={nodeClassNameFunc}
|
||||||
color={nodeColorFunc(node)}
|
nodeBorderRadius={nodeBorderRadius}
|
||||||
borderRadius={nodeBorderRadius}
|
nodeStrokeWidth={nodeStrokeWidth}
|
||||||
strokeColor={nodeStrokeColorFunc(node)}
|
NodeComponent={NodeComponent}
|
||||||
strokeWidth={nodeStrokeWidth}
|
onClick={onClick}
|
||||||
shapeRendering={shapeRendering}
|
shapeRendering={shapeRendering}
|
||||||
onClick={onClick}
|
/>
|
||||||
id={node.id}
|
))}
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NodeComponentWrapper = memo(function NodeComponentWrapper({
|
||||||
|
id,
|
||||||
|
nodeOrigin,
|
||||||
|
nodeColorFunc,
|
||||||
|
nodeStrokeColorFunc,
|
||||||
|
nodeClassNameFunc,
|
||||||
|
nodeBorderRadius,
|
||||||
|
nodeStrokeWidth,
|
||||||
|
shapeRendering,
|
||||||
|
NodeComponent,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
nodeOrigin: NodeOrigin;
|
||||||
|
nodeColorFunc: GetMiniMapNodeAttribute;
|
||||||
|
nodeStrokeColorFunc: GetMiniMapNodeAttribute;
|
||||||
|
nodeClassNameFunc: GetMiniMapNodeAttribute;
|
||||||
|
nodeBorderRadius: number;
|
||||||
|
nodeStrokeWidth?: number;
|
||||||
|
NodeComponent: ComponentType<MiniMapNodeProps>;
|
||||||
|
onClick: MiniMapNodesProps['onClick'];
|
||||||
|
shapeRendering: string;
|
||||||
|
}) {
|
||||||
|
const node = useStore((s) => s.nodeLookup.get(id));
|
||||||
|
if (!node || node.hidden || !(node.computed?.width || node.width) || !(node.computed?.height || node.height)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positionOrigin = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeComponent
|
||||||
|
x={positionOrigin.x}
|
||||||
|
y={positionOrigin.y}
|
||||||
|
width={node.computed?.width ?? node.width ?? 0}
|
||||||
|
height={node.computed?.height ?? node.height ?? 0}
|
||||||
|
style={node.style}
|
||||||
|
selected={!!node.selected}
|
||||||
|
className={nodeClassNameFunc(node)}
|
||||||
|
color={nodeColorFunc(node)}
|
||||||
|
borderRadius={nodeBorderRadius}
|
||||||
|
strokeColor={nodeStrokeColorFunc(node)}
|
||||||
|
strokeWidth={nodeStrokeWidth}
|
||||||
|
shapeRendering={shapeRendering}
|
||||||
|
onClick={onClick}
|
||||||
|
id={node.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default memo(MiniMapNodes);
|
export default memo(MiniMapNodes);
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ export type MiniMapNodeProps = {
|
|||||||
height: number;
|
height: number;
|
||||||
borderRadius: number;
|
borderRadius: number;
|
||||||
className: string;
|
className: string;
|
||||||
color: string;
|
color?: string;
|
||||||
shapeRendering: string;
|
shapeRendering: string;
|
||||||
strokeColor: string;
|
strokeColor?: string;
|
||||||
strokeWidth: number;
|
strokeWidth?: number;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClick?: (event: MouseEvent, id: string) => void;
|
onClick?: (event: MouseEvent, id: string) => void;
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ function ResizeControl({
|
|||||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||||
|
|
||||||
prevValues.current = {
|
prevValues.current = {
|
||||||
width: node?.width ?? 0,
|
width: node?.computed?.width ?? 0,
|
||||||
height: node?.height ?? 0,
|
height: node?.computed?.height ?? 0,
|
||||||
x: node?.position.x ?? 0,
|
x: node?.position.x ?? 0,
|
||||||
y: node?.position.y ?? 0,
|
y: node?.position.y ?? 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,74 +1,40 @@
|
|||||||
import { useCallback, CSSProperties } from 'react';
|
import { useCallback, CSSProperties } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { getNodesBounds, Transform, Rect, Position, internalsSymbol } from '@xyflow/system';
|
import { getNodesBounds, Rect, Position, internalsSymbol, getNodeToolbarTransform } from '@xyflow/system';
|
||||||
|
|
||||||
import { Node, ReactFlowState } from '../../types';
|
import { Node, ReactFlowState } from '../../types';
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { useNodeId } from '../../contexts/NodeIdContext';
|
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||||
import NodeToolbarPortal from './NodeToolbarPortal';
|
import NodeToolbarPortal from './NodeToolbarPortal';
|
||||||
import { Align, NodeToolbarProps } from './types';
|
import { NodeToolbarProps } from './types';
|
||||||
|
|
||||||
const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) =>
|
const nodeEqualityFn = (a?: Node, b?: Node) =>
|
||||||
a?.positionAbsolute?.x === b?.positionAbsolute?.x &&
|
a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x ||
|
||||||
a?.positionAbsolute?.y === b?.positionAbsolute?.y &&
|
a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y ||
|
||||||
a?.width === b?.width &&
|
a?.computed?.width !== b?.computed?.width ||
|
||||||
a?.height === b?.height &&
|
a?.computed?.height !== b?.computed?.height ||
|
||||||
a?.selected === b?.selected &&
|
a?.selected !== b?.selected ||
|
||||||
a?.[internalsSymbol]?.z === b?.[internalsSymbol]?.z;
|
a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z;
|
||||||
|
|
||||||
const nodesEqualityFn = (a: Node[], b: Node[]) => {
|
const nodesEqualityFn = (a: Node[], b: Node[]) => {
|
||||||
return a.length === b.length && a.every((node, i) => nodeEqualityFn(node, b[i]));
|
if (a.length !== b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !a.some((node, i) => nodeEqualityFn(node, b[i]));
|
||||||
};
|
};
|
||||||
|
|
||||||
const storeSelector = (state: ReactFlowState) => ({
|
const storeSelector = (state: ReactFlowState) => ({
|
||||||
transform: state.transform,
|
viewport: {
|
||||||
|
x: state.transform[0],
|
||||||
|
y: state.transform[1],
|
||||||
|
zoom: state.transform[2],
|
||||||
|
},
|
||||||
nodeOrigin: state.nodeOrigin,
|
nodeOrigin: state.nodeOrigin,
|
||||||
selectedNodesCount: state.nodes.filter((node) => node.selected).length,
|
selectedNodesCount: state.nodes.filter((node) => node.selected).length,
|
||||||
});
|
});
|
||||||
|
|
||||||
function getTransform(nodeRect: Rect, transform: Transform, position: Position, offset: number, align: Align): string {
|
|
||||||
let alignmentOffset = 0.5;
|
|
||||||
|
|
||||||
if (align === 'start') {
|
|
||||||
alignmentOffset = 0;
|
|
||||||
} else if (align === 'end') {
|
|
||||||
alignmentOffset = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// position === Position.Top
|
|
||||||
// we set the x any y position of the toolbar based on the nodes position
|
|
||||||
let pos = [
|
|
||||||
(nodeRect.x + nodeRect.width * alignmentOffset) * transform[2] + transform[0],
|
|
||||||
nodeRect.y * transform[2] + transform[1] - offset,
|
|
||||||
];
|
|
||||||
// and than shift it based on the alignment. The shift values are in %.
|
|
||||||
let shift = [-100 * alignmentOffset, -100];
|
|
||||||
|
|
||||||
switch (position) {
|
|
||||||
case Position.Right:
|
|
||||||
pos = [
|
|
||||||
(nodeRect.x + nodeRect.width) * transform[2] + transform[0] + offset,
|
|
||||||
(nodeRect.y + nodeRect.height * alignmentOffset) * transform[2] + transform[1],
|
|
||||||
];
|
|
||||||
shift = [0, -100 * alignmentOffset];
|
|
||||||
break;
|
|
||||||
case Position.Bottom:
|
|
||||||
pos[1] = (nodeRect.y + nodeRect.height) * transform[2] + transform[1] + offset;
|
|
||||||
shift[1] = 0;
|
|
||||||
break;
|
|
||||||
case Position.Left:
|
|
||||||
pos = [
|
|
||||||
nodeRect.x * transform[2] + transform[0] - offset,
|
|
||||||
(nodeRect.y + nodeRect.height * alignmentOffset) * transform[2] + transform[1],
|
|
||||||
];
|
|
||||||
shift = [-100, -100 * alignmentOffset];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `translate(${pos[0]}px, ${pos[1]}px) translate(${shift[0]}%, ${shift[1]}%)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NodeToolbar({
|
function NodeToolbar({
|
||||||
nodeId,
|
nodeId,
|
||||||
children,
|
children,
|
||||||
@@ -97,7 +63,9 @@ function NodeToolbar({
|
|||||||
[nodeId, contextNodeId]
|
[nodeId, contextNodeId]
|
||||||
);
|
);
|
||||||
const nodes = useStore(nodesSelector, nodesEqualityFn);
|
const nodes = useStore(nodesSelector, nodesEqualityFn);
|
||||||
const { transform, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow);
|
const { viewport, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow);
|
||||||
|
|
||||||
|
// if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected
|
||||||
const isActive =
|
const isActive =
|
||||||
typeof isVisible === 'boolean' ? isVisible : nodes.length === 1 && nodes[0].selected && selectedNodesCount === 1;
|
typeof isVisible === 'boolean' ? isVisible : nodes.length === 1 && nodes[0].selected && selectedNodesCount === 1;
|
||||||
|
|
||||||
@@ -110,14 +78,19 @@ function NodeToolbar({
|
|||||||
|
|
||||||
const wrapperStyle: CSSProperties = {
|
const wrapperStyle: CSSProperties = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
transform: getTransform(nodeRect, transform, position, offset, align),
|
transform: getNodeToolbarTransform(nodeRect, viewport, position, offset, align),
|
||||||
zIndex,
|
zIndex,
|
||||||
...style,
|
...style,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeToolbarPortal>
|
<NodeToolbarPortal>
|
||||||
<div style={wrapperStyle} className={cc(['react-flow__node-toolbar', className])} {...rest}>
|
<div
|
||||||
|
style={wrapperStyle}
|
||||||
|
className={cc(['react-flow__node-toolbar', className])}
|
||||||
|
{...rest}
|
||||||
|
data-id={nodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</NodeToolbarPortal>
|
</NodeToolbarPortal>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import type { Position } from '@xyflow/system';
|
import type { Position, Align } from '@xyflow/system';
|
||||||
|
|
||||||
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
|
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
nodeId?: string | string[];
|
nodeId?: string | string[];
|
||||||
@@ -8,5 +8,3 @@ export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
align?: Align;
|
align?: Align;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Align = 'center' | 'start' | 'end';
|
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ const ConnectionLine = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
|
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
|
||||||
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2;
|
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed?.width ?? 0) / 2;
|
||||||
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0;
|
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0;
|
||||||
const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX;
|
const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX;
|
||||||
const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY;
|
const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY;
|
||||||
const fromPosition = fromHandle?.position;
|
const fromPosition = fromHandle?.position;
|
||||||
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
|
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
|
||||||
|
import cc from 'classcat';
|
||||||
|
import { shallow } from 'zustand/shallow';
|
||||||
|
import {
|
||||||
|
getMarkerId,
|
||||||
|
elementSelectionKeys,
|
||||||
|
XYHandle,
|
||||||
|
type Connection,
|
||||||
|
getEdgePosition,
|
||||||
|
errorMessages,
|
||||||
|
} from '@xyflow/system';
|
||||||
|
|
||||||
|
import { useStoreApi, useStore } from '../../hooks/useStore';
|
||||||
|
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
|
||||||
|
import { EdgeAnchor } from '../Edges/EdgeAnchor';
|
||||||
|
import { getMouseHandler } from '../Edges/utils';
|
||||||
|
import type { EdgeWrapperProps } from '../../types';
|
||||||
|
import { builtinEdgeTypes } from './utils';
|
||||||
|
|
||||||
|
function 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,
|
||||||
|
edgeTypes,
|
||||||
|
zIndex,
|
||||||
|
onError,
|
||||||
|
}: EdgeWrapperProps): JSX.Element | null {
|
||||||
|
let edgeType = type || 'default';
|
||||||
|
let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType];
|
||||||
|
|
||||||
|
if (EdgeComponent === undefined) {
|
||||||
|
onError?.('011', errorMessages['error011'](edgeType));
|
||||||
|
edgeType = 'default';
|
||||||
|
EdgeComponent = builtinEdgeTypes.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<svg style={{ zIndex }}>
|
||||||
|
<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>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EdgeWrapper.displayName = 'EdgeWrapper';
|
||||||
|
|
||||||
|
export default memo(EdgeWrapper);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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>,
|
||||||
|
};
|
||||||
@@ -4,56 +4,67 @@ import { Position, getBezierPath } from '@xyflow/system';
|
|||||||
import BaseEdge from './BaseEdge';
|
import BaseEdge from './BaseEdge';
|
||||||
import type { BezierEdgeProps } from '../../types';
|
import type { BezierEdgeProps } from '../../types';
|
||||||
|
|
||||||
const BezierEdge = memo(
|
function createBezierEdge(params: { isInternal: boolean }) {
|
||||||
({
|
// eslint-disable-next-line react/display-name
|
||||||
sourceX,
|
return memo(
|
||||||
sourceY,
|
({
|
||||||
targetX,
|
id,
|
||||||
targetY,
|
|
||||||
sourcePosition = Position.Bottom,
|
|
||||||
targetPosition = Position.Top,
|
|
||||||
label,
|
|
||||||
labelStyle,
|
|
||||||
labelShowBg,
|
|
||||||
labelBgStyle,
|
|
||||||
labelBgPadding,
|
|
||||||
labelBgBorderRadius,
|
|
||||||
style,
|
|
||||||
markerEnd,
|
|
||||||
markerStart,
|
|
||||||
pathOptions,
|
|
||||||
interactionWidth,
|
|
||||||
}: BezierEdgeProps) => {
|
|
||||||
const [path, labelX, labelY] = getBezierPath({
|
|
||||||
sourceX,
|
sourceX,
|
||||||
sourceY,
|
sourceY,
|
||||||
sourcePosition,
|
|
||||||
targetX,
|
targetX,
|
||||||
targetY,
|
targetY,
|
||||||
targetPosition,
|
sourcePosition = Position.Bottom,
|
||||||
curvature: pathOptions?.curvature,
|
targetPosition = Position.Top,
|
||||||
});
|
label,
|
||||||
|
labelStyle,
|
||||||
|
labelShowBg,
|
||||||
|
labelBgStyle,
|
||||||
|
labelBgPadding,
|
||||||
|
labelBgBorderRadius,
|
||||||
|
style,
|
||||||
|
markerEnd,
|
||||||
|
markerStart,
|
||||||
|
pathOptions,
|
||||||
|
interactionWidth,
|
||||||
|
}: BezierEdgeProps) => {
|
||||||
|
const [path, labelX, labelY] = getBezierPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition,
|
||||||
|
curvature: pathOptions?.curvature,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
const _id = params.isInternal ? undefined : id;
|
||||||
<BaseEdge
|
|
||||||
path={path}
|
return (
|
||||||
labelX={labelX}
|
<BaseEdge
|
||||||
labelY={labelY}
|
id={_id}
|
||||||
label={label}
|
path={path}
|
||||||
labelStyle={labelStyle}
|
labelX={labelX}
|
||||||
labelShowBg={labelShowBg}
|
labelY={labelY}
|
||||||
labelBgStyle={labelBgStyle}
|
label={label}
|
||||||
labelBgPadding={labelBgPadding}
|
labelStyle={labelStyle}
|
||||||
labelBgBorderRadius={labelBgBorderRadius}
|
labelShowBg={labelShowBg}
|
||||||
style={style}
|
labelBgStyle={labelBgStyle}
|
||||||
markerEnd={markerEnd}
|
labelBgPadding={labelBgPadding}
|
||||||
markerStart={markerStart}
|
labelBgBorderRadius={labelBgBorderRadius}
|
||||||
interactionWidth={interactionWidth}
|
style={style}
|
||||||
/>
|
markerEnd={markerEnd}
|
||||||
);
|
markerStart={markerStart}
|
||||||
}
|
interactionWidth={interactionWidth}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BezierEdge = createBezierEdge({ isInternal: false });
|
||||||
|
const BezierEdgeInternal = createBezierEdge({ isInternal: true });
|
||||||
|
|
||||||
BezierEdge.displayName = 'BezierEdge';
|
BezierEdge.displayName = 'BezierEdge';
|
||||||
|
BezierEdgeInternal.displayName = 'BezierEdgeInternal';
|
||||||
|
|
||||||
export default BezierEdge;
|
export { BezierEdge, BezierEdgeInternal };
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { memo } from 'react';
|
|||||||
import { Position, getBezierEdgeCenter } from '@xyflow/system';
|
import { Position, getBezierEdgeCenter } from '@xyflow/system';
|
||||||
|
|
||||||
import BaseEdge from './BaseEdge';
|
import BaseEdge from './BaseEdge';
|
||||||
import type { EdgeProps } from '../../types';
|
import type { SimpleBezierEdgeProps } from '../../types';
|
||||||
|
|
||||||
export interface GetSimpleBezierPathParams {
|
export interface GetSimpleBezierPathParams {
|
||||||
sourceX: number;
|
sourceX: number;
|
||||||
@@ -71,54 +71,65 @@ export function getSimpleBezierPath({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const SimpleBezierEdge = memo(
|
function createSimpleBezierEdge(params: { isInternal: boolean }) {
|
||||||
({
|
// eslint-disable-next-line react/display-name
|
||||||
sourceX,
|
return memo(
|
||||||
sourceY,
|
({
|
||||||
targetX,
|
id,
|
||||||
targetY,
|
|
||||||
sourcePosition = Position.Bottom,
|
|
||||||
targetPosition = Position.Top,
|
|
||||||
label,
|
|
||||||
labelStyle,
|
|
||||||
labelShowBg,
|
|
||||||
labelBgStyle,
|
|
||||||
labelBgPadding,
|
|
||||||
labelBgBorderRadius,
|
|
||||||
style,
|
|
||||||
markerEnd,
|
|
||||||
markerStart,
|
|
||||||
interactionWidth,
|
|
||||||
}: EdgeProps) => {
|
|
||||||
const [path, labelX, labelY] = getSimpleBezierPath({
|
|
||||||
sourceX,
|
sourceX,
|
||||||
sourceY,
|
sourceY,
|
||||||
sourcePosition,
|
|
||||||
targetX,
|
targetX,
|
||||||
targetY,
|
targetY,
|
||||||
targetPosition,
|
sourcePosition = Position.Bottom,
|
||||||
});
|
targetPosition = Position.Top,
|
||||||
|
label,
|
||||||
|
labelStyle,
|
||||||
|
labelShowBg,
|
||||||
|
labelBgStyle,
|
||||||
|
labelBgPadding,
|
||||||
|
labelBgBorderRadius,
|
||||||
|
style,
|
||||||
|
markerEnd,
|
||||||
|
markerStart,
|
||||||
|
interactionWidth,
|
||||||
|
}: SimpleBezierEdgeProps) => {
|
||||||
|
const [path, labelX, labelY] = getSimpleBezierPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
const _id = params.isInternal ? undefined : id;
|
||||||
<BaseEdge
|
|
||||||
path={path}
|
return (
|
||||||
labelX={labelX}
|
<BaseEdge
|
||||||
labelY={labelY}
|
id={_id}
|
||||||
label={label}
|
path={path}
|
||||||
labelStyle={labelStyle}
|
labelX={labelX}
|
||||||
labelShowBg={labelShowBg}
|
labelY={labelY}
|
||||||
labelBgStyle={labelBgStyle}
|
label={label}
|
||||||
labelBgPadding={labelBgPadding}
|
labelStyle={labelStyle}
|
||||||
labelBgBorderRadius={labelBgBorderRadius}
|
labelShowBg={labelShowBg}
|
||||||
style={style}
|
labelBgStyle={labelBgStyle}
|
||||||
markerEnd={markerEnd}
|
labelBgPadding={labelBgPadding}
|
||||||
markerStart={markerStart}
|
labelBgBorderRadius={labelBgBorderRadius}
|
||||||
interactionWidth={interactionWidth}
|
style={style}
|
||||||
/>
|
markerEnd={markerEnd}
|
||||||
);
|
markerStart={markerStart}
|
||||||
}
|
interactionWidth={interactionWidth}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SimpleBezierEdge = createSimpleBezierEdge({ isInternal: false });
|
||||||
|
const SimpleBezierEdgeInternal = createSimpleBezierEdge({ isInternal: true });
|
||||||
|
|
||||||
SimpleBezierEdge.displayName = 'SimpleBezierEdge';
|
SimpleBezierEdge.displayName = 'SimpleBezierEdge';
|
||||||
|
SimpleBezierEdgeInternal.displayName = 'SimpleBezierEdgeInternal';
|
||||||
|
|
||||||
export default SimpleBezierEdge;
|
export { SimpleBezierEdge, SimpleBezierEdgeInternal };
|
||||||
|
|||||||
@@ -4,57 +4,68 @@ import { Position, getSmoothStepPath } from '@xyflow/system';
|
|||||||
import BaseEdge from './BaseEdge';
|
import BaseEdge from './BaseEdge';
|
||||||
import type { SmoothStepEdgeProps } from '../../types';
|
import type { SmoothStepEdgeProps } from '../../types';
|
||||||
|
|
||||||
const SmoothStepEdge = memo(
|
function createSmoothStepEdge(params: { isInternal: boolean }) {
|
||||||
({
|
// eslint-disable-next-line react/display-name
|
||||||
sourceX,
|
return memo(
|
||||||
sourceY,
|
({
|
||||||
targetX,
|
id,
|
||||||
targetY,
|
|
||||||
label,
|
|
||||||
labelStyle,
|
|
||||||
labelShowBg,
|
|
||||||
labelBgStyle,
|
|
||||||
labelBgPadding,
|
|
||||||
labelBgBorderRadius,
|
|
||||||
style,
|
|
||||||
sourcePosition = Position.Bottom,
|
|
||||||
targetPosition = Position.Top,
|
|
||||||
markerEnd,
|
|
||||||
markerStart,
|
|
||||||
pathOptions,
|
|
||||||
interactionWidth,
|
|
||||||
}: SmoothStepEdgeProps) => {
|
|
||||||
const [path, labelX, labelY] = getSmoothStepPath({
|
|
||||||
sourceX,
|
sourceX,
|
||||||
sourceY,
|
sourceY,
|
||||||
sourcePosition,
|
|
||||||
targetX,
|
targetX,
|
||||||
targetY,
|
targetY,
|
||||||
targetPosition,
|
label,
|
||||||
borderRadius: pathOptions?.borderRadius,
|
labelStyle,
|
||||||
offset: pathOptions?.offset,
|
labelShowBg,
|
||||||
});
|
labelBgStyle,
|
||||||
|
labelBgPadding,
|
||||||
|
labelBgBorderRadius,
|
||||||
|
style,
|
||||||
|
sourcePosition = Position.Bottom,
|
||||||
|
targetPosition = Position.Top,
|
||||||
|
markerEnd,
|
||||||
|
markerStart,
|
||||||
|
pathOptions,
|
||||||
|
interactionWidth,
|
||||||
|
}: SmoothStepEdgeProps) => {
|
||||||
|
const [path, labelX, labelY] = getSmoothStepPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition,
|
||||||
|
borderRadius: pathOptions?.borderRadius,
|
||||||
|
offset: pathOptions?.offset,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
const _id = params.isInternal ? undefined : id;
|
||||||
<BaseEdge
|
|
||||||
path={path}
|
return (
|
||||||
labelX={labelX}
|
<BaseEdge
|
||||||
labelY={labelY}
|
id={_id}
|
||||||
label={label}
|
path={path}
|
||||||
labelStyle={labelStyle}
|
labelX={labelX}
|
||||||
labelShowBg={labelShowBg}
|
labelY={labelY}
|
||||||
labelBgStyle={labelBgStyle}
|
label={label}
|
||||||
labelBgPadding={labelBgPadding}
|
labelStyle={labelStyle}
|
||||||
labelBgBorderRadius={labelBgBorderRadius}
|
labelShowBg={labelShowBg}
|
||||||
style={style}
|
labelBgStyle={labelBgStyle}
|
||||||
markerEnd={markerEnd}
|
labelBgPadding={labelBgPadding}
|
||||||
markerStart={markerStart}
|
labelBgBorderRadius={labelBgBorderRadius}
|
||||||
interactionWidth={interactionWidth}
|
style={style}
|
||||||
/>
|
markerEnd={markerEnd}
|
||||||
);
|
markerStart={markerStart}
|
||||||
}
|
interactionWidth={interactionWidth}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SmoothStepEdge = createSmoothStepEdge({ isInternal: false });
|
||||||
|
const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true });
|
||||||
|
|
||||||
SmoothStepEdge.displayName = 'SmoothStepEdge';
|
SmoothStepEdge.displayName = 'SmoothStepEdge';
|
||||||
|
SmoothStepEdgeInternal.displayName = 'SmoothStepEdgeInternal';
|
||||||
|
|
||||||
export default SmoothStepEdge;
|
export { SmoothStepEdge, SmoothStepEdgeInternal };
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
import SmoothStepEdge from './SmoothStepEdge';
|
import { SmoothStepEdge } from './SmoothStepEdge';
|
||||||
import type { SmoothStepEdgeProps } from '../../types';
|
import type { StepEdgeProps } from '../../types';
|
||||||
|
|
||||||
const StepEdge = memo((props: SmoothStepEdgeProps) => (
|
function createStepEdge(params: { isInternal: boolean }) {
|
||||||
<SmoothStepEdge
|
// eslint-disable-next-line react/display-name
|
||||||
{...props}
|
return memo(({ id, ...props }: StepEdgeProps) => {
|
||||||
pathOptions={useMemo(() => ({ borderRadius: 0, offset: props.pathOptions?.offset }), [props.pathOptions?.offset])}
|
const _id = params.isInternal ? undefined : id;
|
||||||
/>
|
|
||||||
));
|
return (
|
||||||
|
<SmoothStepEdge
|
||||||
|
{...props}
|
||||||
|
id={_id}
|
||||||
|
pathOptions={useMemo(
|
||||||
|
() => ({ borderRadius: 0, offset: props.pathOptions?.offset }),
|
||||||
|
[props.pathOptions?.offset]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepEdge = createStepEdge({ isInternal: false });
|
||||||
|
const StepEdgeInternal = createStepEdge({ isInternal: true });
|
||||||
|
|
||||||
StepEdge.displayName = 'StepEdge';
|
StepEdge.displayName = 'StepEdge';
|
||||||
|
StepEdgeInternal.displayName = 'StepEdgeInternal';
|
||||||
|
|
||||||
export default StepEdge;
|
export { StepEdge, StepEdgeInternal };
|
||||||
|
|||||||
@@ -2,47 +2,58 @@ import { memo } from 'react';
|
|||||||
import { getStraightPath } from '@xyflow/system';
|
import { getStraightPath } from '@xyflow/system';
|
||||||
|
|
||||||
import BaseEdge from './BaseEdge';
|
import BaseEdge from './BaseEdge';
|
||||||
import type { EdgeProps } from '../../types';
|
import type { StraightEdgeProps } from '../../types';
|
||||||
|
|
||||||
const StraightEdge = memo(
|
function createStraightEdge(params: { isInternal: boolean }) {
|
||||||
({
|
// eslint-disable-next-line react/display-name
|
||||||
sourceX,
|
return memo(
|
||||||
sourceY,
|
({
|
||||||
targetX,
|
id,
|
||||||
targetY,
|
sourceX,
|
||||||
label,
|
sourceY,
|
||||||
labelStyle,
|
targetX,
|
||||||
labelShowBg,
|
targetY,
|
||||||
labelBgStyle,
|
label,
|
||||||
labelBgPadding,
|
labelStyle,
|
||||||
labelBgBorderRadius,
|
labelShowBg,
|
||||||
style,
|
labelBgStyle,
|
||||||
markerEnd,
|
labelBgPadding,
|
||||||
markerStart,
|
labelBgBorderRadius,
|
||||||
interactionWidth,
|
style,
|
||||||
}: EdgeProps) => {
|
markerEnd,
|
||||||
const [path, labelX, labelY] = getStraightPath({ sourceX, sourceY, targetX, targetY });
|
markerStart,
|
||||||
|
interactionWidth,
|
||||||
|
}: StraightEdgeProps) => {
|
||||||
|
const [path, labelX, labelY] = getStraightPath({ sourceX, sourceY, targetX, targetY });
|
||||||
|
|
||||||
return (
|
const _id = params.isInternal ? undefined : id;
|
||||||
<BaseEdge
|
|
||||||
path={path}
|
return (
|
||||||
labelX={labelX}
|
<BaseEdge
|
||||||
labelY={labelY}
|
id={_id}
|
||||||
label={label}
|
path={path}
|
||||||
labelStyle={labelStyle}
|
labelX={labelX}
|
||||||
labelShowBg={labelShowBg}
|
labelY={labelY}
|
||||||
labelBgStyle={labelBgStyle}
|
label={label}
|
||||||
labelBgPadding={labelBgPadding}
|
labelStyle={labelStyle}
|
||||||
labelBgBorderRadius={labelBgBorderRadius}
|
labelShowBg={labelShowBg}
|
||||||
style={style}
|
labelBgStyle={labelBgStyle}
|
||||||
markerEnd={markerEnd}
|
labelBgPadding={labelBgPadding}
|
||||||
markerStart={markerStart}
|
labelBgBorderRadius={labelBgBorderRadius}
|
||||||
interactionWidth={interactionWidth}
|
style={style}
|
||||||
/>
|
markerEnd={markerEnd}
|
||||||
);
|
markerStart={markerStart}
|
||||||
}
|
interactionWidth={interactionWidth}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StraightEdge = createStraightEdge({ isInternal: false });
|
||||||
|
const StraightEdgeInternal = createStraightEdge({ isInternal: true });
|
||||||
|
|
||||||
StraightEdge.displayName = 'StraightEdge';
|
StraightEdge.displayName = 'StraightEdge';
|
||||||
|
StraightEdgeInternal.displayName = 'StraightEdgeInternal';
|
||||||
|
|
||||||
export default StraightEdge;
|
export { StraightEdge, StraightEdgeInternal };
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
export { default as SimpleBezierEdge } from './SimpleBezierEdge';
|
// We distinguish between internal and exported edges
|
||||||
export { default as SmoothStepEdge } from './SmoothStepEdge';
|
// The internal edges are used directly like custom edges and always get an id, source and target props
|
||||||
export { default as StepEdge } from './StepEdge';
|
// If you import an edge from the library, the id is optional and source and target are not used at all
|
||||||
export { default as StraightEdge } from './StraightEdge';
|
|
||||||
export { default as BezierEdge } from './BezierEdge';
|
export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge';
|
||||||
|
export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge';
|
||||||
|
export { StepEdge, StepEdgeInternal } from './StepEdge';
|
||||||
|
export { StraightEdge, StraightEdgeInternal } from './StraightEdge';
|
||||||
|
export { BezierEdge, BezierEdgeInternal } from './BezierEdge';
|
||||||
|
|||||||
@@ -1,293 +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,
|
|
||||||
zIndex,
|
|
||||||
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 (
|
|
||||||
<svg style={{ zIndex }}>
|
|
||||||
<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>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EdgeWrapper.displayName = 'EdgeWrapper';
|
|
||||||
|
|
||||||
return memo(EdgeWrapper);
|
|
||||||
};
|
|
||||||
@@ -171,7 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
|||||||
lib,
|
lib,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isValid) {
|
if (isValid && connection) {
|
||||||
onConnectExtended(connection);
|
onConnectExtended(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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 = useStore((s) => s.nodeLookup.get(id)!);
|
||||||
|
|
||||||
|
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 positionAbsolute = nodeExtent
|
||||||
|
? clampPosition(node.computed?.positionAbsolute, nodeExtent)
|
||||||
|
: node.computed?.positionAbsolute || { x: 0, y: 0 };
|
||||||
|
const positionAbsoluteOrigin = getPositionWithOrigin({
|
||||||
|
x: positionAbsolute.x,
|
||||||
|
y: positionAbsolute.y,
|
||||||
|
width: computedWidth ?? width ?? 0,
|
||||||
|
height: computedHeight ?? height ?? 0,
|
||||||
|
origin: node.origin || nodeOrigin,
|
||||||
|
});
|
||||||
|
const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height);
|
||||||
|
const zIndex = node[internalsSymbol]?.z ?? 0;
|
||||||
|
const isParent = !!node[internalsSymbol]?.isParent;
|
||||||
|
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
||||||
|
|
||||||
|
const onMouseEnterHandler =
|
||||||
|
onMouseEnter === undefined ? undefined : (event: MouseEvent) => onMouseEnter(event, { ...node });
|
||||||
|
const onMouseMoveHandler =
|
||||||
|
onMouseMove === undefined ? undefined : (event: MouseEvent) => onMouseMove(event, { ...node });
|
||||||
|
const onMouseLeaveHandler =
|
||||||
|
onMouseLeave === undefined ? undefined : (event: MouseEvent) => onMouseLeave(event, { ...node });
|
||||||
|
const onContextMenuHandler =
|
||||||
|
onContextMenu === undefined ? undefined : (event: MouseEvent) => onContextMenu(event, { ...node });
|
||||||
|
const onDoubleClickHandler =
|
||||||
|
onDoubleClick === undefined ? undefined : (event: MouseEvent) => onDoubleClick(event, { ...node });
|
||||||
|
|
||||||
|
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: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
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={positionAbsolute.x}
|
||||||
|
positionAbsoluteY={positionAbsolute.y}
|
||||||
|
selected={node.selected}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
sourcePosition={node.sourcePosition}
|
||||||
|
targetPosition={node.targetPosition}
|
||||||
|
dragging={dragging}
|
||||||
|
dragHandle={node.dragHandle}
|
||||||
|
zIndex={zIndex}
|
||||||
|
/>
|
||||||
|
</Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NodeWrapper.displayName = 'NodeWrapper';
|
||||||
|
|
||||||
|
export default memo(NodeWrapper);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ComponentType } from 'react';
|
||||||
|
import type { NodeProps, XYPosition } from '@xyflow/system';
|
||||||
|
|
||||||
|
import InputNode from '../Nodes/InputNode';
|
||||||
|
import DefaultNode from '../Nodes/DefaultNode';
|
||||||
|
import GroupNode from '../Nodes/GroupNode';
|
||||||
|
import OutputNode from '../Nodes/OutputNode';
|
||||||
|
import type { NodeTypes } from '../../types';
|
||||||
|
|
||||||
|
export const arrowKeyDiffs: Record<string, XYPosition> = {
|
||||||
|
ArrowUp: { x: 0, y: -1 },
|
||||||
|
ArrowDown: { x: 0, y: 1 },
|
||||||
|
ArrowLeft: { x: -1, y: 0 },
|
||||||
|
ArrowRight: { x: 1, y: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const builtinNodeTypes: NodeTypes = {
|
||||||
|
input: InputNode as ComponentType<NodeProps>,
|
||||||
|
default: DefaultNode as ComponentType<NodeProps>,
|
||||||
|
output: OutputNode as ComponentType<NodeProps>,
|
||||||
|
group: GroupNode as ComponentType<NodeProps>,
|
||||||
|
};
|
||||||
@@ -1,22 +1,9 @@
|
|||||||
import type { MouseEvent, RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import type { StoreApi } from 'zustand';
|
import type { StoreApi } from 'zustand';
|
||||||
|
|
||||||
import type { Node, ReactFlowState } from '../../types';
|
import type { ReactFlowState } from '../../types';
|
||||||
import { errorMessages } from '@xyflow/system';
|
import { errorMessages } from '@xyflow/system';
|
||||||
|
|
||||||
export function getMouseHandler(
|
|
||||||
id: string,
|
|
||||||
getState: StoreApi<ReactFlowState>['getState'],
|
|
||||||
handler?: (event: MouseEvent, node: Node) => void
|
|
||||||
) {
|
|
||||||
return handler === undefined
|
|
||||||
? handler
|
|
||||||
: (event: MouseEvent) => {
|
|
||||||
const node = getState().nodeLookup.get(id)!;
|
|
||||||
handler(event, { ...node });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// this handler is called by
|
// this handler is called by
|
||||||
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
|
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
|
||||||
// or
|
// or
|
||||||
|
|||||||
@@ -1,230 +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,
|
|
||||||
sizeWidth,
|
|
||||||
sizeHeight,
|
|
||||||
}: 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: sizeWidth,
|
|
||||||
height: sizeHeight,
|
|
||||||
...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}
|
|
||||||
xPos={xPos}
|
|
||||||
yPos={yPos}
|
|
||||||
selected={selected}
|
|
||||||
isConnectable={isConnectable}
|
|
||||||
sourcePosition={sourcePosition}
|
|
||||||
targetPosition={targetPosition}
|
|
||||||
dragging={dragging}
|
|
||||||
dragHandle={dragHandle}
|
|
||||||
zIndex={zIndex}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
NodeWrapper.displayName = 'NodeWrapper';
|
|
||||||
|
|
||||||
return memo(NodeWrapper);
|
|
||||||
};
|
|
||||||
@@ -10,9 +10,9 @@ import { getNodesBounds } from '@xyflow/system';
|
|||||||
|
|
||||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||||
import useDrag from '../../hooks/useDrag';
|
import useDrag from '../../hooks/useDrag';
|
||||||
import { arrowKeyDiffs } from '../Nodes/wrapNode';
|
|
||||||
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
|
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
|
||||||
import type { Node, ReactFlowState } from '../../types';
|
import type { Node, ReactFlowState } from '../../types';
|
||||||
|
import { arrowKeyDiffs } from '../NodeWrapper/utils';
|
||||||
|
|
||||||
export type NodesSelectionProps = {
|
export type NodesSelectionProps = {
|
||||||
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
|
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
|
||||||
|
|||||||
@@ -3,67 +3,72 @@
|
|||||||
* We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`)
|
* We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`)
|
||||||
* and values that have a dedicated setter function in the store (like `setNodes`).
|
* and values that have a dedicated setter function in the store (like `setNodes`).
|
||||||
*/
|
*/
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { StoreApi } from 'zustand';
|
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { devWarn, type CoordinateExtent } from '@xyflow/system';
|
import { type CoordinateExtent } from '@xyflow/system';
|
||||||
|
|
||||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||||
import type { Node, Edge, ReactFlowState, ReactFlowProps, ReactFlowStore } from '../../types';
|
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
|
||||||
|
|
||||||
type StoreUpdaterProps = Pick<
|
// these fields exist in the global store and we need to keep them up to date
|
||||||
ReactFlowProps,
|
const reactFlowFieldsToTrack = [
|
||||||
| 'nodes'
|
'nodes',
|
||||||
| 'edges'
|
'edges',
|
||||||
| 'defaultNodes'
|
'defaultNodes',
|
||||||
| 'defaultEdges'
|
'defaultEdges',
|
||||||
| 'onConnect'
|
'onConnect',
|
||||||
| 'onConnectStart'
|
'onConnectStart',
|
||||||
| 'onConnectEnd'
|
'onConnectEnd',
|
||||||
| 'onClickConnectStart'
|
'onClickConnectStart',
|
||||||
| 'onClickConnectEnd'
|
'onClickConnectEnd',
|
||||||
| 'nodesDraggable'
|
'nodesDraggable',
|
||||||
| 'nodesConnectable'
|
'nodesConnectable',
|
||||||
| 'nodesFocusable'
|
'nodesFocusable',
|
||||||
| 'edgesFocusable'
|
'edgesFocusable',
|
||||||
| 'edgesUpdatable'
|
'edgesUpdatable',
|
||||||
| 'minZoom'
|
'elevateNodesOnSelect',
|
||||||
| 'maxZoom'
|
'minZoom',
|
||||||
| 'nodeExtent'
|
'maxZoom',
|
||||||
| 'onNodesChange'
|
'nodeExtent',
|
||||||
| 'onEdgesChange'
|
'onNodesChange',
|
||||||
| 'elementsSelectable'
|
'onEdgesChange',
|
||||||
| 'connectionMode'
|
'elementsSelectable',
|
||||||
| 'snapToGrid'
|
'connectionMode',
|
||||||
| 'snapGrid'
|
'snapGrid',
|
||||||
| 'translateExtent'
|
'snapToGrid',
|
||||||
| 'connectOnClick'
|
'translateExtent',
|
||||||
| 'defaultEdgeOptions'
|
'connectOnClick',
|
||||||
| 'fitView'
|
'defaultEdgeOptions',
|
||||||
| 'fitViewOptions'
|
'fitView',
|
||||||
| 'onNodesDelete'
|
'fitViewOptions',
|
||||||
| 'onEdgesDelete'
|
'onNodesDelete',
|
||||||
| 'onDelete'
|
'onEdgesDelete',
|
||||||
| 'onNodeDragStart'
|
'onDelete',
|
||||||
| 'onNodeDrag'
|
'onNodeDrag',
|
||||||
| 'onNodeDragStop'
|
'onNodeDragStart',
|
||||||
| 'onSelectionDragStart'
|
'onNodeDragStop',
|
||||||
| 'onSelectionDrag'
|
'onSelectionDrag',
|
||||||
| 'onSelectionDragStop'
|
'onSelectionDragStart',
|
||||||
| 'onMove'
|
'onSelectionDragStop',
|
||||||
| 'onMoveStart'
|
'onMoveStart',
|
||||||
| 'onMoveEnd'
|
'onMove',
|
||||||
| 'noPanClassName'
|
'onMoveEnd',
|
||||||
| 'nodeOrigin'
|
'noPanClassName',
|
||||||
| 'elevateNodesOnSelect'
|
'nodeOrigin',
|
||||||
| 'autoPanOnConnect'
|
'autoPanOnConnect',
|
||||||
| 'autoPanOnNodeDrag'
|
'autoPanOnNodeDrag',
|
||||||
| 'onError'
|
'onError',
|
||||||
| 'connectionRadius'
|
'connectionRadius',
|
||||||
| 'isValidConnection'
|
'isValidConnection',
|
||||||
| 'selectNodesOnDrag'
|
'selectNodesOnDrag',
|
||||||
| 'nodeDragThreshold'
|
'nodeDragThreshold',
|
||||||
> & { rfId: string };
|
] as const;
|
||||||
|
|
||||||
|
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];
|
||||||
|
type StoreUpdaterProps = Pick<ReactFlowProps, ReactFlowFieldsToTrack> & { rfId: string };
|
||||||
|
|
||||||
|
// rfId doesn't exist in ReactFlowProps, but it's one of the fields we want to update
|
||||||
|
const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'] as const;
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
setNodes: s.setNodes,
|
setNodes: s.setNodes,
|
||||||
@@ -76,80 +81,7 @@ const selector = (s: ReactFlowState) => ({
|
|||||||
reset: s.reset,
|
reset: s.reset,
|
||||||
});
|
});
|
||||||
|
|
||||||
function useStoreUpdater<T>(value: T | undefined, setStoreAction: (param: T) => void) {
|
const StoreUpdater = (props: StoreUpdaterProps) => {
|
||||||
useEffect(() => {
|
|
||||||
if (typeof value !== 'undefined') {
|
|
||||||
setStoreAction(value);
|
|
||||||
}
|
|
||||||
}, [value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates with values in store that don't have a dedicated setter function
|
|
||||||
function useDirectStoreUpdater(
|
|
||||||
key: keyof ReactFlowStore,
|
|
||||||
value: unknown,
|
|
||||||
setState: StoreApi<ReactFlowState>['setState']
|
|
||||||
) {
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof value !== 'undefined') {
|
|
||||||
setState({ [key]: value });
|
|
||||||
}
|
|
||||||
}, [value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const StoreUpdater = ({
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
defaultNodes,
|
|
||||||
defaultEdges,
|
|
||||||
onConnect,
|
|
||||||
onConnectStart,
|
|
||||||
onConnectEnd,
|
|
||||||
onClickConnectStart,
|
|
||||||
onClickConnectEnd,
|
|
||||||
nodesDraggable,
|
|
||||||
nodesConnectable,
|
|
||||||
nodesFocusable,
|
|
||||||
edgesFocusable,
|
|
||||||
edgesUpdatable,
|
|
||||||
elevateNodesOnSelect,
|
|
||||||
minZoom,
|
|
||||||
maxZoom,
|
|
||||||
nodeExtent,
|
|
||||||
onNodesChange,
|
|
||||||
onEdgesChange,
|
|
||||||
elementsSelectable,
|
|
||||||
connectionMode,
|
|
||||||
snapGrid,
|
|
||||||
snapToGrid,
|
|
||||||
translateExtent,
|
|
||||||
connectOnClick,
|
|
||||||
defaultEdgeOptions,
|
|
||||||
fitView,
|
|
||||||
fitViewOptions,
|
|
||||||
onNodesDelete,
|
|
||||||
onEdgesDelete,
|
|
||||||
onDelete,
|
|
||||||
onNodeDrag,
|
|
||||||
onNodeDragStart,
|
|
||||||
onNodeDragStop,
|
|
||||||
onSelectionDrag,
|
|
||||||
onSelectionDragStart,
|
|
||||||
onSelectionDragStop,
|
|
||||||
onMoveStart,
|
|
||||||
onMove,
|
|
||||||
onMoveEnd,
|
|
||||||
noPanClassName,
|
|
||||||
nodeOrigin,
|
|
||||||
rfId,
|
|
||||||
autoPanOnConnect,
|
|
||||||
autoPanOnNodeDrag,
|
|
||||||
onError,
|
|
||||||
connectionRadius,
|
|
||||||
isValidConnection,
|
|
||||||
selectNodesOnDrag,
|
|
||||||
nodeDragThreshold,
|
|
||||||
}: StoreUpdaterProps) => {
|
|
||||||
const {
|
const {
|
||||||
setNodes,
|
setNodes,
|
||||||
setEdges,
|
setEdges,
|
||||||
@@ -163,64 +95,44 @@ const StoreUpdater = ({
|
|||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const edgesWithDefaults = defaultEdges?.map((e) => ({ ...e, ...defaultEdgeOptions }));
|
const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions }));
|
||||||
setDefaultNodesAndEdges(defaultNodes, edgesWithDefaults);
|
setDefaultNodesAndEdges(props.defaultNodes, edgesWithDefaults);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState);
|
const previousFields = useRef<Partial<StoreUpdaterProps>>({});
|
||||||
useDirectStoreUpdater('connectionMode', connectionMode, store.setState);
|
|
||||||
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);
|
|
||||||
|
|
||||||
useStoreUpdater<Node[]>(nodes, setNodes);
|
useEffect(
|
||||||
useStoreUpdater<Edge[]>(edges, setEdges);
|
() => {
|
||||||
useStoreUpdater<number>(minZoom, setMinZoom);
|
for (const fieldName of fieldsToTrack) {
|
||||||
useStoreUpdater<number>(maxZoom, setMaxZoom);
|
const fieldValue = props[fieldName];
|
||||||
useStoreUpdater<CoordinateExtent>(translateExtent, setTranslateExtent);
|
const previousFieldValue = previousFields.current[fieldName];
|
||||||
useStoreUpdater<CoordinateExtent>(nodeExtent, setNodeExtent);
|
|
||||||
|
if (fieldValue === previousFieldValue) continue;
|
||||||
|
if (typeof props[fieldName] === 'undefined') continue;
|
||||||
|
|
||||||
|
// Custom handling with dedicated setters for some fields
|
||||||
|
if (fieldName === 'nodes') setNodes(fieldValue as Node[]);
|
||||||
|
else if (fieldName === 'edges') setEdges(fieldValue as Edge[]);
|
||||||
|
else if (fieldName === 'minZoom') setMinZoom(fieldValue as number);
|
||||||
|
else if (fieldName === 'maxZoom') setMaxZoom(fieldValue as number);
|
||||||
|
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
|
||||||
|
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
|
||||||
|
// Renamed fields
|
||||||
|
else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue as boolean });
|
||||||
|
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue as FitViewOptions });
|
||||||
|
// General case
|
||||||
|
else store.setState({ [fieldName]: fieldValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
previousFields.current = props;
|
||||||
|
},
|
||||||
|
// Only re-run the effect if one of the fields we track changes
|
||||||
|
fieldsToTrack.map((fieldName) => props[fieldName])
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { memo, ReactNode } from 'react';
|
import { memo, ReactNode } from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import { errorMessages } from '@xyflow/system';
|
|
||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import useVisibleEdges from '../../hooks/useVisibleEdges';
|
import useVisibleEdges from '../../hooks/useVisibleEdges';
|
||||||
import MarkerDefinitions from './MarkerDefinitions';
|
import MarkerDefinitions from './MarkerDefinitions';
|
||||||
import { GraphViewProps } from '../GraphView';
|
import { GraphViewProps } from '../GraphView';
|
||||||
import type { EdgeTypesWrapped, ReactFlowState } from '../../types';
|
import type { ReactFlowState } from '../../types';
|
||||||
|
import EdgeWrapper from '../../components/EdgeWrapper';
|
||||||
|
|
||||||
type EdgeRendererProps = Pick<
|
type EdgeRendererProps = Pick<
|
||||||
GraphViewProps,
|
GraphViewProps,
|
||||||
@@ -27,8 +27,8 @@ type EdgeRendererProps = Pick<
|
|||||||
| 'elevateEdgesOnSelect'
|
| 'elevateEdgesOnSelect'
|
||||||
| 'rfId'
|
| 'rfId'
|
||||||
| 'disableKeyboardA11y'
|
| 'disableKeyboardA11y'
|
||||||
|
| 'edgeTypes'
|
||||||
> & {
|
> & {
|
||||||
edgeTypes: EdgeTypesWrapped;
|
|
||||||
elevateEdgesOnSelect: boolean;
|
elevateEdgesOnSelect: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
@@ -72,14 +72,6 @@ const EdgeRenderer = ({
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{edges.map((edge) => {
|
{edges.map((edge) => {
|
||||||
let edgeType = edge.type || 'default';
|
|
||||||
|
|
||||||
if (!edgeTypes[edgeType]) {
|
|
||||||
onError?.('011', errorMessages['error011'](edgeType));
|
|
||||||
edgeType = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
const EdgeComponent = edgeTypes[edgeType];
|
|
||||||
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
|
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
|
||||||
const isUpdatable =
|
const isUpdatable =
|
||||||
typeof onEdgeUpdate !== 'undefined' &&
|
typeof onEdgeUpdate !== 'undefined' &&
|
||||||
@@ -87,15 +79,16 @@ const EdgeRenderer = ({
|
|||||||
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
|
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EdgeComponent
|
<EdgeWrapper
|
||||||
key={edge.id}
|
key={edge.id}
|
||||||
id={edge.id}
|
id={edge.id}
|
||||||
className={cc([edge.className, noPanClassName])}
|
className={cc([edge.className, noPanClassName])}
|
||||||
type={edgeType}
|
type={edge.type}
|
||||||
data={edge.data}
|
data={edge.data}
|
||||||
selected={!!edge.selected}
|
selected={!!edge.selected}
|
||||||
animated={!!edge.animated}
|
animated={!!edge.animated}
|
||||||
hidden={!!edge.hidden}
|
hidden={!!edge.hidden}
|
||||||
|
zIndex={edge.zIndex}
|
||||||
label={edge.label}
|
label={edge.label}
|
||||||
labelStyle={edge.labelStyle}
|
labelStyle={edge.labelStyle}
|
||||||
labelShowBg={edge.labelShowBg}
|
labelShowBg={edge.labelShowBg}
|
||||||
@@ -103,7 +96,6 @@ const EdgeRenderer = ({
|
|||||||
labelBgPadding={edge.labelBgPadding}
|
labelBgPadding={edge.labelBgPadding}
|
||||||
labelBgBorderRadius={edge.labelBgBorderRadius}
|
labelBgBorderRadius={edge.labelBgBorderRadius}
|
||||||
style={edge.style}
|
style={edge.style}
|
||||||
zIndex={edge.zIndex}
|
|
||||||
source={edge.source}
|
source={edge.source}
|
||||||
target={edge.target}
|
target={edge.target}
|
||||||
sourceHandleId={edge.sourceHandle}
|
sourceHandleId={edge.sourceHandle}
|
||||||
@@ -127,10 +119,11 @@ const EdgeRenderer = ({
|
|||||||
isUpdatable={isUpdatable}
|
isUpdatable={isUpdatable}
|
||||||
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
|
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
|
||||||
interactionWidth={edge.interactionWidth}
|
interactionWidth={edge.interactionWidth}
|
||||||
|
onError={onError}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import type { ComponentType } from 'react';
|
|
||||||
|
|
||||||
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } 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 || BezierEdge) as ComponentType<EdgeProps>),
|
|
||||||
straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType<EdgeProps>),
|
|
||||||
step: wrapEdge((edgeTypes.step || StepEdge) as ComponentType<EdgeProps>),
|
|
||||||
smoothstep: wrapEdge((edgeTypes.step || SmoothStepEdge) as ComponentType<EdgeProps>),
|
|
||||||
simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdge) 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] || BezierEdge) as ComponentType<EdgeProps>);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}, wrappedTypes);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...standardTypes,
|
|
||||||
...specialTypes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -8,19 +8,15 @@ import useOnInitHandler from '../../hooks/useOnInitHandler';
|
|||||||
import useViewportSync from '../../hooks/useViewportSync';
|
import useViewportSync from '../../hooks/useViewportSync';
|
||||||
import ConnectionLine from '../../components/ConnectionLine';
|
import ConnectionLine from '../../components/ConnectionLine';
|
||||||
import type { ReactFlowProps } from '../../types';
|
import type { ReactFlowProps } from '../../types';
|
||||||
import { createNodeTypes } from '../NodeRenderer/utils';
|
import useNodeOrEdgeTypesWarning from './useNodeOrEdgeTypesWarning';
|
||||||
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
|
||||||
import { useNodeOrEdgeTypes } from './utils';
|
|
||||||
|
|
||||||
export type GraphViewProps = Omit<
|
export type GraphViewProps = Omit<
|
||||||
ReactFlowProps,
|
ReactFlowProps,
|
||||||
'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
|
'onSelectionChange' | 'nodes' | 'edges' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
|
||||||
> &
|
> &
|
||||||
Required<
|
Required<
|
||||||
Pick<
|
Pick<
|
||||||
ReactFlowProps,
|
ReactFlowProps,
|
||||||
| 'nodeTypes'
|
|
||||||
| 'edgeTypes'
|
|
||||||
| 'selectionKeyCode'
|
| 'selectionKeyCode'
|
||||||
| 'deleteKeyCode'
|
| 'deleteKeyCode'
|
||||||
| 'multiSelectionKeyCode'
|
| 'multiSelectionKeyCode'
|
||||||
@@ -108,8 +104,8 @@ const GraphView = ({
|
|||||||
viewport,
|
viewport,
|
||||||
onViewportChange,
|
onViewportChange,
|
||||||
}: GraphViewProps) => {
|
}: GraphViewProps) => {
|
||||||
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
|
useNodeOrEdgeTypesWarning(nodeTypes);
|
||||||
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
|
useNodeOrEdgeTypesWarning(edgeTypes);
|
||||||
|
|
||||||
useOnInitHandler(onInit);
|
useOnInitHandler(onInit);
|
||||||
useViewportSync(viewport);
|
useViewportSync(viewport);
|
||||||
@@ -154,7 +150,7 @@ const GraphView = ({
|
|||||||
>
|
>
|
||||||
<ViewportWrapper>
|
<ViewportWrapper>
|
||||||
<EdgeRenderer
|
<EdgeRenderer
|
||||||
edgeTypes={edgeTypesWrapped}
|
edgeTypes={edgeTypes}
|
||||||
onEdgeClick={onEdgeClick}
|
onEdgeClick={onEdgeClick}
|
||||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||||
onEdgeUpdate={onEdgeUpdate}
|
onEdgeUpdate={onEdgeUpdate}
|
||||||
@@ -182,7 +178,7 @@ const GraphView = ({
|
|||||||
<div className="react-flow__edgelabel-renderer" />
|
<div className="react-flow__edgelabel-renderer" />
|
||||||
|
|
||||||
<NodeRenderer
|
<NodeRenderer
|
||||||
nodeTypes={nodeTypesWrapped}
|
nodeTypes={nodeTypes}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onNodeDoubleClick={onNodeDoubleClick}
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
onNodeMouseEnter={onNodeMouseEnter}
|
onNodeMouseEnter={onNodeMouseEnter}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { errorMessages } from '@xyflow/system';
|
||||||
|
|
||||||
|
import type { EdgeTypes, NodeTypes } from '../../types';
|
||||||
|
import { useStoreApi } from '../../hooks/useStore';
|
||||||
|
|
||||||
|
const emptyTypes = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This hook warns the user if node or edgeTypes change.
|
||||||
|
*/
|
||||||
|
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: NodeTypes): void;
|
||||||
|
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: EdgeTypes): void;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export default function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes: any = emptyTypes): any {
|
||||||
|
const updateCount = useRef(0);
|
||||||
|
const store = useStoreApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
if (updateCount.current > 1) {
|
||||||
|
store.getState().onError?.('002', errorMessages['error002']());
|
||||||
|
}
|
||||||
|
updateCount.current += 1;
|
||||||
|
}
|
||||||
|
}, [nodeOrEdgeTypes]);
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { useMemo, useRef } from 'react';
|
|
||||||
import { shallow } from 'zustand/shallow';
|
|
||||||
import { errorMessages } from '@xyflow/system';
|
|
||||||
|
|
||||||
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
|
|
||||||
import { CreateNodeTypes } from '../NodeRenderer/utils';
|
|
||||||
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
|
|
||||||
import { useStoreApi } from '../../hooks/useStore';
|
|
||||||
|
|
||||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
|
|
||||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
|
|
||||||
const typesKeysRef = useRef<string[] | null>(null);
|
|
||||||
const store = useStoreApi();
|
|
||||||
|
|
||||||
const typesParsed = useMemo(() => {
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
const typeKeys = Object.keys(nodeOrEdgeTypes);
|
|
||||||
|
|
||||||
if (shallow(typesKeysRef.current, typeKeys)) {
|
|
||||||
store.getState().onError?.('002', errorMessages['error002']());
|
|
||||||
}
|
|
||||||
|
|
||||||
typesKeysRef.current = typeKeys;
|
|
||||||
}
|
|
||||||
return createTypes(nodeOrEdgeTypes);
|
|
||||||
}, [nodeOrEdgeTypes]);
|
|
||||||
|
|
||||||
return typesParsed;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { memo, useMemo, useEffect, useRef, type ComponentType } from 'react';
|
import { memo } from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system';
|
|
||||||
|
|
||||||
import useVisibleNodes from '../../hooks/useVisibleNodes';
|
import useVisibleNodesIds from '../../hooks/useVisibleNodes';
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { containerStyle } from '../../styles/utils';
|
import { containerStyle } from '../../styles/utils';
|
||||||
import { GraphViewProps } from '../GraphView';
|
import { GraphViewProps } from '../GraphView';
|
||||||
import type { NodeTypesWrapped, ReactFlowState, WrapNodeProps } from '../../types';
|
import type { ReactFlowState } from '../../types';
|
||||||
|
import useResizeObserver from './useResizeObserver';
|
||||||
|
import NodeWrapper from '../../components/NodeWrapper';
|
||||||
|
|
||||||
type NodeRendererProps = Pick<
|
export type NodeRendererProps = Pick<
|
||||||
GraphViewProps,
|
GraphViewProps,
|
||||||
| 'onNodeClick'
|
| 'onNodeClick'
|
||||||
| 'onNodeDoubleClick'
|
| 'onNodeDoubleClick'
|
||||||
@@ -23,126 +24,71 @@ type NodeRendererProps = Pick<
|
|||||||
| 'disableKeyboardA11y'
|
| 'disableKeyboardA11y'
|
||||||
| 'nodeOrigin'
|
| 'nodeOrigin'
|
||||||
| 'nodeExtent'
|
| 'nodeExtent'
|
||||||
> & {
|
| 'nodeTypes'
|
||||||
nodeTypes: NodeTypesWrapped;
|
>;
|
||||||
};
|
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
nodesDraggable: s.nodesDraggable,
|
nodesDraggable: s.nodesDraggable,
|
||||||
nodesConnectable: s.nodesConnectable,
|
nodesConnectable: s.nodesConnectable,
|
||||||
nodesFocusable: s.nodesFocusable,
|
nodesFocusable: s.nodesFocusable,
|
||||||
elementsSelectable: s.elementsSelectable,
|
elementsSelectable: s.elementsSelectable,
|
||||||
updateNodeDimensions: s.updateNodeDimensions,
|
|
||||||
onError: s.onError,
|
onError: s.onError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const NodeRenderer = (props: NodeRendererProps) => {
|
const NodeRenderer = (props: NodeRendererProps) => {
|
||||||
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
|
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow);
|
||||||
useStore(selector, shallow);
|
const nodeIds = useVisibleNodesIds(props.onlyRenderVisibleElements);
|
||||||
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
|
const resizeObserver = useResizeObserver();
|
||||||
const resizeObserverRef = useRef<ResizeObserver>();
|
|
||||||
|
|
||||||
const resizeObserver = useMemo(() => {
|
|
||||||
if (typeof ResizeObserver === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
|
||||||
const updates = new Map();
|
|
||||||
|
|
||||||
entries.forEach((entry: ResizeObserverEntry) => {
|
|
||||||
const id = entry.target.getAttribute('data-id') as string;
|
|
||||||
updates.set(id, {
|
|
||||||
id,
|
|
||||||
nodeElement: entry.target as HTMLDivElement,
|
|
||||||
forceUpdate: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
updateNodeDimensions(updates);
|
|
||||||
});
|
|
||||||
|
|
||||||
resizeObserverRef.current = observer;
|
|
||||||
|
|
||||||
return observer;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
resizeObserverRef?.current?.disconnect();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="react-flow__nodes" style={containerStyle}>
|
<div className="react-flow__nodes" style={containerStyle}>
|
||||||
{nodes.map((node) => {
|
{nodeIds.map((nodeId) => {
|
||||||
let nodeType = node.type || 'default';
|
|
||||||
|
|
||||||
if (!props.nodeTypes[nodeType]) {
|
|
||||||
onError?.('003', errorMessages['error003'](nodeType));
|
|
||||||
|
|
||||||
nodeType = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
|
|
||||||
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
|
|
||||||
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
|
|
||||||
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
|
|
||||||
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
|
|
||||||
|
|
||||||
const clampedPosition = props.nodeExtent
|
|
||||||
? clampPosition(node.positionAbsolute, props.nodeExtent)
|
|
||||||
: node.positionAbsolute;
|
|
||||||
|
|
||||||
const posX = clampedPosition?.x ?? 0;
|
|
||||||
const posY = clampedPosition?.y ?? 0;
|
|
||||||
const posOrigin = getPositionWithOrigin({
|
|
||||||
x: posX,
|
|
||||||
y: posY,
|
|
||||||
width: node.width ?? 0,
|
|
||||||
height: node.height ?? 0,
|
|
||||||
origin: node.origin || props.nodeOrigin,
|
|
||||||
});
|
|
||||||
const initialized = (!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeComponent
|
// The split of responsibilities between NodeRenderer and
|
||||||
key={node.id}
|
// NodeComponentWrapper may appear weird. However, it’s designed to
|
||||||
id={node.id}
|
// minimize the cost of updates when individual nodes change.
|
||||||
className={node.className}
|
//
|
||||||
style={node.style}
|
// For example, when you’re dragging a single node, that node gets
|
||||||
sizeWidth={node.size?.width}
|
// updated multiple times per second. If `NodeRenderer` were to update
|
||||||
sizeHeight={node.size?.height}
|
// every time, it would have to re-run the `nodes.map()` loop every
|
||||||
type={nodeType}
|
// time. This gets pricey with hundreds of nodes, especially if every
|
||||||
data={node.data}
|
// loop cycle does more than just rendering a JSX element!
|
||||||
sourcePosition={node.sourcePosition || Position.Bottom}
|
//
|
||||||
targetPosition={node.targetPosition || Position.Top}
|
// As a result of this choice, we took the following implementation
|
||||||
hidden={node.hidden}
|
// decisions:
|
||||||
xPos={posX}
|
// - NodeRenderer subscribes *only* to node IDs – and therefore
|
||||||
yPos={posY}
|
// rerender *only* when visible nodes are added or removed.
|
||||||
xPosOrigin={posOrigin.x}
|
// - NodeRenderer performs all operations the result of which can be
|
||||||
yPosOrigin={posOrigin.y}
|
// shared between nodes (such as creating the `ResizeObserver`
|
||||||
|
// instance, or subscribing to `selector`). This means extra prop
|
||||||
|
// drilling into `NodeComponentWrapper`, but it means we need to run
|
||||||
|
// these operations only once – instead of once per node.
|
||||||
|
// - Any operations that you’d normally write inside `nodes.map` are
|
||||||
|
// moved into `NodeComponentWrapper`. This ensures they are
|
||||||
|
// memorized – so if `NodeRenderer` *has* to rerender, it only
|
||||||
|
// needs to regenerate the list of nodes, nothing else.
|
||||||
|
<NodeWrapper
|
||||||
|
key={nodeId}
|
||||||
|
id={nodeId}
|
||||||
|
nodeTypes={props.nodeTypes}
|
||||||
|
nodeExtent={props.nodeExtent}
|
||||||
|
nodeOrigin={props.nodeOrigin}
|
||||||
onClick={props.onNodeClick}
|
onClick={props.onNodeClick}
|
||||||
onMouseEnter={props.onNodeMouseEnter}
|
onMouseEnter={props.onNodeMouseEnter}
|
||||||
onMouseMove={props.onNodeMouseMove}
|
onMouseMove={props.onNodeMouseMove}
|
||||||
onMouseLeave={props.onNodeMouseLeave}
|
onMouseLeave={props.onNodeMouseLeave}
|
||||||
onContextMenu={props.onNodeContextMenu}
|
onContextMenu={props.onNodeContextMenu}
|
||||||
onDoubleClick={props.onNodeDoubleClick}
|
onDoubleClick={props.onNodeDoubleClick}
|
||||||
selected={!!node.selected}
|
|
||||||
isDraggable={isDraggable}
|
|
||||||
isSelectable={isSelectable}
|
|
||||||
isConnectable={isConnectable}
|
|
||||||
isFocusable={isFocusable}
|
|
||||||
resizeObserver={resizeObserver}
|
|
||||||
dragHandle={node.dragHandle}
|
|
||||||
zIndex={node[internalsSymbol]?.z ?? 0}
|
|
||||||
isParent={!!node[internalsSymbol]?.isParent}
|
|
||||||
noDragClassName={props.noDragClassName}
|
noDragClassName={props.noDragClassName}
|
||||||
noPanClassName={props.noPanClassName}
|
noPanClassName={props.noPanClassName}
|
||||||
initialized={initialized}
|
|
||||||
rfId={props.rfId}
|
rfId={props.rfId}
|
||||||
disableKeyboardA11y={props.disableKeyboardA11y}
|
disableKeyboardA11y={props.disableKeyboardA11y}
|
||||||
ariaLabel={node.ariaLabel}
|
resizeObserver={resizeObserver}
|
||||||
|
nodesDraggable={nodesDraggable}
|
||||||
|
nodesConnectable={nodesConnectable}
|
||||||
|
nodesFocusable={nodesFocusable}
|
||||||
|
elementsSelectable={elementsSelectable}
|
||||||
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import { ReactFlowState } from '../../types';
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
|
||||||
|
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
|
||||||
|
|
||||||
|
export default function useResizeObserver() {
|
||||||
|
const updateNodeDimensions = useStore(selector);
|
||||||
|
const resizeObserverRef = useRef<ResizeObserver>();
|
||||||
|
|
||||||
|
const resizeObserver = useMemo(() => {
|
||||||
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||||
|
const updates = new Map();
|
||||||
|
|
||||||
|
entries.forEach((entry: ResizeObserverEntry) => {
|
||||||
|
const id = entry.target.getAttribute('data-id') as string;
|
||||||
|
updates.set(id, {
|
||||||
|
id,
|
||||||
|
nodeElement: entry.target as HTMLDivElement,
|
||||||
|
forceUpdate: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateNodeDimensions(updates);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserverRef.current = observer;
|
||||||
|
|
||||||
|
return observer;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
resizeObserverRef?.current?.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return resizeObserver;
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import type { ComponentType } from 'react';
|
|
||||||
import type { NodeProps } from '@xyflow/system';
|
|
||||||
|
|
||||||
import DefaultNode from '../../components/Nodes/DefaultNode';
|
|
||||||
import InputNode from '../../components/Nodes/InputNode';
|
|
||||||
import OutputNode from '../../components/Nodes/OutputNode';
|
|
||||||
import GroupNode from '../../components/Nodes/GroupNode';
|
|
||||||
import wrapNode from '../../components/Nodes/wrapNode';
|
|
||||||
import type { NodeTypes, NodeTypesWrapped } from '../../types';
|
|
||||||
|
|
||||||
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
|
|
||||||
|
|
||||||
export function createNodeTypes(nodeTypes: NodeTypes): NodeTypesWrapped {
|
|
||||||
const standardTypes: NodeTypesWrapped = {
|
|
||||||
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeProps>),
|
|
||||||
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeProps>),
|
|
||||||
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeProps>),
|
|
||||||
group: wrapNode((nodeTypes.group || GroupNode) as ComponentType<NodeProps>),
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrappedTypes = {} as NodeTypesWrapped;
|
|
||||||
const specialTypes: NodeTypesWrapped = Object.keys(nodeTypes)
|
|
||||||
.filter((k) => !['input', 'default', 'output', 'group'].includes(k))
|
|
||||||
.reduce((res, key) => {
|
|
||||||
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}, wrappedTypes);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...standardTypes,
|
|
||||||
...specialTypes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -12,32 +12,14 @@ import {
|
|||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import Attribution from '../../components/Attribution';
|
import Attribution from '../../components/Attribution';
|
||||||
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
|
|
||||||
import DefaultNode from '../../components/Nodes/DefaultNode';
|
|
||||||
import InputNode from '../../components/Nodes/InputNode';
|
|
||||||
import OutputNode from '../../components/Nodes/OutputNode';
|
|
||||||
import GroupNode from '../../components/Nodes/GroupNode';
|
|
||||||
import SelectionListener from '../../components/SelectionListener';
|
import SelectionListener from '../../components/SelectionListener';
|
||||||
import StoreUpdater from '../../components/StoreUpdater';
|
import StoreUpdater from '../../components/StoreUpdater';
|
||||||
import A11yDescriptions from '../../components/A11yDescriptions';
|
import A11yDescriptions from '../../components/A11yDescriptions';
|
||||||
import GraphView from '../GraphView';
|
import GraphView from '../GraphView';
|
||||||
import Wrapper from './Wrapper';
|
import Wrapper from './Wrapper';
|
||||||
import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types';
|
import type { ReactFlowProps, ReactFlowRefType } from '../../types';
|
||||||
|
import useColorModeClass from '../../hooks/useColorModeClass';
|
||||||
const defaultNodeTypes: NodeTypes = {
|
|
||||||
input: InputNode,
|
|
||||||
default: DefaultNode,
|
|
||||||
output: OutputNode,
|
|
||||||
group: GroupNode,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultEdgeTypes: EdgeTypes = {
|
|
||||||
default: BezierEdge,
|
|
||||||
straight: StraightEdge,
|
|
||||||
step: StepEdge,
|
|
||||||
smoothstep: SmoothStepEdge,
|
|
||||||
simplebezier: SimpleBezierEdge,
|
|
||||||
};
|
|
||||||
|
|
||||||
const initNodeOrigin: NodeOrigin = [0, 0];
|
const initNodeOrigin: NodeOrigin = [0, 0];
|
||||||
const initSnapGrid: [number, number] = [15, 15];
|
const initSnapGrid: [number, number] = [15, 15];
|
||||||
@@ -59,8 +41,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
defaultNodes,
|
defaultNodes,
|
||||||
defaultEdges,
|
defaultEdges,
|
||||||
className,
|
className,
|
||||||
nodeTypes = defaultNodeTypes,
|
nodeTypes,
|
||||||
edgeTypes = defaultEdgeTypes,
|
edgeTypes,
|
||||||
onNodeClick,
|
onNodeClick,
|
||||||
onEdgeClick,
|
onEdgeClick,
|
||||||
onInit,
|
onInit,
|
||||||
@@ -169,18 +151,20 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
onViewportChange,
|
onViewportChange,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
colorMode = 'light',
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const rfId = id || '1';
|
const rfId = id || '1';
|
||||||
|
const colorModeClassName = useColorModeClass(colorMode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...rest}
|
{...rest}
|
||||||
style={{ ...style, ...wrapperStyle }}
|
style={{ ...style, ...wrapperStyle }}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cc(['react-flow', className])}
|
className={cc(['react-flow', className, colorModeClassName])}
|
||||||
data-testid="rf__wrapper"
|
data-testid="rf__wrapper"
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { ColorMode, ColorModeClass } from '@xyflow/system';
|
||||||
|
|
||||||
|
function getMediaQuery() {
|
||||||
|
if (typeof window === 'undefined' || !window.matchMedia) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useColorModeClass(colorMode: ColorMode): ColorModeClass {
|
||||||
|
const [colorModeClass, setColorModeClass] = useState<ColorModeClass | null>(
|
||||||
|
colorMode === 'system' ? null : colorMode
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (colorMode !== 'system') {
|
||||||
|
setColorModeClass(colorMode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = getMediaQuery();
|
||||||
|
const updateColorModeClass = () => setColorModeClass(mediaQuery?.matches ? 'dark' : 'light');
|
||||||
|
|
||||||
|
updateColorModeClass();
|
||||||
|
mediaQuery?.addEventListener('change', updateColorModeClass);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery?.removeEventListener('change', updateColorModeClass);
|
||||||
|
};
|
||||||
|
}, [colorMode]);
|
||||||
|
|
||||||
|
return colorModeClass !== null ? colorModeClass : getMediaQuery()?.matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { Connection, HandleType, areConnectionMapsEqual, handleConnectionChange } from '@xyflow/system';
|
||||||
|
|
||||||
|
import { useStore } from './useStore';
|
||||||
|
import { useNodeId } from '../contexts/NodeIdContext';
|
||||||
|
|
||||||
|
type useHandleConnectionsParams = {
|
||||||
|
type: HandleType;
|
||||||
|
id?: string | null;
|
||||||
|
nodeId?: string;
|
||||||
|
onConnect?: (connections: Connection[]) => void;
|
||||||
|
onDisconnect?: (connections: Connection[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @param param.type - handle type 'source' or 'target'
|
||||||
|
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
|
||||||
|
* @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used
|
||||||
|
* @param param.onConnect - gets called when a connection is established
|
||||||
|
* @param param.onDisconnect - gets called when a connection is removed
|
||||||
|
* @returns an array with connections
|
||||||
|
*/
|
||||||
|
export function useHandleConnections({
|
||||||
|
type,
|
||||||
|
id = null,
|
||||||
|
nodeId,
|
||||||
|
onConnect,
|
||||||
|
onDisconnect,
|
||||||
|
}: useHandleConnectionsParams): Connection[] {
|
||||||
|
const _nodeId = useNodeId();
|
||||||
|
const prevConnections = useRef<Map<string, Connection> | null>(null);
|
||||||
|
const currentNodeId = nodeId || _nodeId;
|
||||||
|
|
||||||
|
const connections = useStore(
|
||||||
|
(state) => state.connectionLookup.get(`${currentNodeId}-${type}-${id}`),
|
||||||
|
areConnectionMapsEqual
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts
|
||||||
|
if (prevConnections.current && prevConnections.current !== connections) {
|
||||||
|
const _connections = connections ?? new Map();
|
||||||
|
handleConnectionChange(prevConnections.current, _connections, onDisconnect);
|
||||||
|
handleConnectionChange(_connections, prevConnections.current, onConnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevConnections.current = connections ?? new Map();
|
||||||
|
}, [connections, onConnect, onDisconnect]);
|
||||||
|
|
||||||
|
return useMemo(() => Array.from(connections?.values() ?? []), [connections]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
|
import { useStore } from '../hooks/useStore';
|
||||||
|
import type { Node } from '../types';
|
||||||
|
|
||||||
|
export function useNodesData<NodeType extends Node = Node>(nodeId: string): NodeType['data'] | null;
|
||||||
|
export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): NodeType['data'][];
|
||||||
|
export function useNodesData<NodeType extends Node = Node>(
|
||||||
|
nodeIds: string[],
|
||||||
|
guard: (node: Node) => node is NodeType
|
||||||
|
): NodeType['data'][];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function useNodesData(nodeIds: any): any {
|
||||||
|
const nodesData = useStore(
|
||||||
|
useCallback(
|
||||||
|
(s) => {
|
||||||
|
if (!Array.isArray(nodeIds)) {
|
||||||
|
return s.nodeLookup.get(nodeIds)?.data || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
for (const nodeId of nodeIds) {
|
||||||
|
const nodeData = s.nodeLookup.get(nodeId)?.data;
|
||||||
|
|
||||||
|
if (nodeData) {
|
||||||
|
data.push(nodeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
[nodeIds]
|
||||||
|
),
|
||||||
|
shallow
|
||||||
|
);
|
||||||
|
|
||||||
|
return nodesData;
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import useViewportHelper from './useViewportHelper';
|
import useViewportHelper from './useViewportHelper';
|
||||||
import { useStoreApi } from '../hooks/useStore';
|
import { useStoreApi } from './useStore';
|
||||||
import type {
|
import type {
|
||||||
ReactFlowInstance,
|
ReactFlowInstance,
|
||||||
Instance,
|
Instance,
|
||||||
@@ -24,6 +24,7 @@ import type {
|
|||||||
Node,
|
Node,
|
||||||
Edge,
|
Edge,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { isNode } from '../utils';
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData> {
|
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData> {
|
||||||
@@ -206,7 +207,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (nodes || store.getState().nodes).filter((n) => {
|
return (nodes || store.getState().nodes).filter((n) => {
|
||||||
if (!isRect && (n.id === node!.id || !n.positionAbsolute)) {
|
if (!isRect && (n.id === node!.id || !n.computed?.positionAbsolute)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +272,36 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
|||||||
return getOutgoersBase(node, nodes, edges);
|
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 useMemo(() => {
|
||||||
return {
|
return {
|
||||||
...viewportHelper,
|
...viewportHelper,
|
||||||
@@ -289,6 +320,8 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
|||||||
getConnectedEdges,
|
getConnectedEdges,
|
||||||
getIncomers,
|
getIncomers,
|
||||||
getOutgoers,
|
getOutgoers,
|
||||||
|
updateNode,
|
||||||
|
updateNodeData,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
viewportHelper,
|
viewportHelper,
|
||||||
|
|||||||
@@ -23,8 +23,11 @@ function useUpdateNodePositions() {
|
|||||||
const yDiff = params.y * yVelo * factor;
|
const yDiff = params.y * yVelo * factor;
|
||||||
|
|
||||||
const nodeUpdates = selectedNodes.map((node) => {
|
const nodeUpdates = selectedNodes.map((node) => {
|
||||||
if (node.positionAbsolute) {
|
if (node.computed?.positionAbsolute) {
|
||||||
let nextPosition = { x: node.positionAbsolute.x + xDiff, y: node.positionAbsolute.y + yDiff };
|
let nextPosition = {
|
||||||
|
x: node.computed?.positionAbsolute.x + xDiff,
|
||||||
|
y: node.computed?.positionAbsolute.y + yDiff,
|
||||||
|
};
|
||||||
|
|
||||||
if (snapToGrid) {
|
if (snapToGrid) {
|
||||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||||
@@ -40,7 +43,10 @@ function useUpdateNodePositions() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
node.position = position;
|
node.position = position;
|
||||||
node.positionAbsolute = positionAbsolute;
|
if (!node.computed) {
|
||||||
|
node.computed = {};
|
||||||
|
}
|
||||||
|
node.computed.positionAbsolute = positionAbsolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
import { getNodesInside } from '@xyflow/system';
|
import { getNodesInside } from '@xyflow/system';
|
||||||
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
import { useStore } from '../hooks/useStore';
|
import { useStore } from '../hooks/useStore';
|
||||||
import type { Node, ReactFlowState } from '../types';
|
import type { Node, ReactFlowState } from '../types';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
function useVisibleNodes(onlyRenderVisible: boolean) {
|
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
|
||||||
const nodes = useStore(
|
return onlyRenderVisible
|
||||||
useCallback(
|
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
|
||||||
(s: ReactFlowState) =>
|
(node) => node.id
|
||||||
onlyRenderVisible
|
)
|
||||||
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
|
: Array.from(s.nodeLookup.keys());
|
||||||
: s.nodes,
|
};
|
||||||
[onlyRenderVisible]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return nodes;
|
function useVisibleNodeIds(onlyRenderVisible: boolean) {
|
||||||
|
const nodeIds = useStore(useCallback(selector(onlyRenderVisible), [onlyRenderVisible]), shallow);
|
||||||
|
|
||||||
|
return nodeIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useVisibleNodes;
|
export default useVisibleNodeIds;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export { default as ReactFlow } from './container/ReactFlow';
|
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 { default as EdgeText } from './components/Edges/EdgeText';
|
||||||
export { default as StraightEdge } from './components/Edges/StraightEdge';
|
export { StraightEdge } from './components/Edges/StraightEdge';
|
||||||
export { default as StepEdge } from './components/Edges/StepEdge';
|
export { StepEdge } from './components/Edges/StepEdge';
|
||||||
export { default as BezierEdge } from './components/Edges/BezierEdge';
|
export { BezierEdge } from './components/Edges/BezierEdge';
|
||||||
export { default as SimpleBezierEdge, getSimpleBezierPath } from './components/Edges/SimpleBezierEdge';
|
export { SimpleBezierEdge, getSimpleBezierPath } from './components/Edges/SimpleBezierEdge';
|
||||||
export { default as SmoothStepEdge } from './components/Edges/SmoothStepEdge';
|
export { SmoothStepEdge } from './components/Edges/SmoothStepEdge';
|
||||||
export { default as BaseEdge } from './components/Edges/BaseEdge';
|
export { default as BaseEdge } from './components/Edges/BaseEdge';
|
||||||
export { default as ReactFlowProvider } from './components/ReactFlowProvider';
|
export { default as ReactFlowProvider } from './components/ReactFlowProvider';
|
||||||
export { default as Panel, type PanelProps } from './components/Panel';
|
export { default as Panel, type PanelProps } from './components/Panel';
|
||||||
@@ -22,6 +22,8 @@ export { useStore, useStoreApi } from './hooks/useStore';
|
|||||||
export { default as useOnViewportChange, type UseOnViewportChangeOptions } from './hooks/useOnViewportChange';
|
export { default as useOnViewportChange, type UseOnViewportChangeOptions } from './hooks/useOnViewportChange';
|
||||||
export { default as useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange';
|
export { default as useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange';
|
||||||
export { default as useNodesInitialized, type UseNodesInitializedOptions } from './hooks/useNodesInitialized';
|
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 { useNodeId } from './contexts/NodeIdContext';
|
||||||
|
|
||||||
export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes';
|
export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes';
|
||||||
@@ -73,6 +75,8 @@ export {
|
|||||||
type Box,
|
type Box,
|
||||||
type Transform,
|
type Transform,
|
||||||
type CoordinateExtent,
|
type CoordinateExtent,
|
||||||
|
type ColorMode,
|
||||||
|
type ColorModeClass,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
// system utils
|
// system utils
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
|||||||
import {
|
import {
|
||||||
clampPosition,
|
clampPosition,
|
||||||
fitView as fitViewSystem,
|
fitView as fitViewSystem,
|
||||||
updateNodes,
|
adoptUserProvidedNodes,
|
||||||
updateAbsolutePositions,
|
updateAbsolutePositions,
|
||||||
panBy as panBySystem,
|
panBy as panBySystem,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
updateNodeDimensions as updateNodeDimensionsSystem,
|
updateNodeDimensions as updateNodeDimensionsSystem,
|
||||||
|
updateConnectionLookup,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
||||||
@@ -42,15 +43,24 @@ const createRFStore = ({
|
|||||||
...getInitialState({ nodes, edges, width, height, fitView }),
|
...getInitialState({ nodes, edges, width, height, fitView }),
|
||||||
setNodes: (nodes: Node[]) => {
|
setNodes: (nodes: Node[]) => {
|
||||||
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
||||||
// Whenver new nodes are set, we need to calculate the absolute positions of the nodes
|
// setNodes() is called exclusively in response to user actions:
|
||||||
// and update the nodeLookup.
|
// - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
|
||||||
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
// - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
|
||||||
|
//
|
||||||
|
// When this happens, we take the note objects passed by the user and extend them with fields
|
||||||
|
// relevant for internal React Flow operations.
|
||||||
|
// TODO: consider updating the types to reflect the distinction between user-provided nodes and internal nodes.
|
||||||
|
const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||||
|
|
||||||
set({ nodes: nextNodes });
|
set({ nodes: nodesWithInternalData });
|
||||||
},
|
},
|
||||||
setEdges: (edges: Edge[]) => {
|
setEdges: (edges: Edge[]) => {
|
||||||
const { defaultEdgeOptions = {} } = get();
|
const { defaultEdgeOptions = {}, connectionLookup } = get();
|
||||||
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
|
const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e }));
|
||||||
|
|
||||||
|
updateConnectionLookup(connectionLookup, nextEdges);
|
||||||
|
|
||||||
|
set({ edges: nextEdges });
|
||||||
},
|
},
|
||||||
// when the user works with an uncontrolled flow,
|
// when the user works with an uncontrolled flow,
|
||||||
// we set a flag `hasDefaultNodes` / `hasDefaultEdges`
|
// we set a flag `hasDefaultNodes` / `hasDefaultEdges`
|
||||||
@@ -69,7 +79,8 @@ const createRFStore = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (hasDefaultNodes) {
|
if (hasDefaultNodes) {
|
||||||
nextState.nodes = updateNodes(nodes, new Map(), {
|
const { nodeLookup } = get();
|
||||||
|
nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||||
nodeOrigin: get().nodeOrigin,
|
nodeOrigin: get().nodeOrigin,
|
||||||
elevateNodesOnSelect: get().elevateNodesOnSelect,
|
elevateNodesOnSelect: get().elevateNodesOnSelect,
|
||||||
});
|
});
|
||||||
@@ -147,7 +158,7 @@ const createRFStore = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (positionChanged) {
|
if (positionChanged) {
|
||||||
change.positionAbsolute = node.positionAbsolute;
|
change.positionAbsolute = node.computed?.positionAbsolute;
|
||||||
change.position = node.position;
|
change.position = node.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +174,7 @@ const createRFStore = ({
|
|||||||
if (changes?.length) {
|
if (changes?.length) {
|
||||||
if (hasDefaultNodes) {
|
if (hasDefaultNodes) {
|
||||||
const updatedNodes = applyNodeChanges(changes, nodes);
|
const updatedNodes = applyNodeChanges(changes, nodes);
|
||||||
const nextNodes = updateNodes(updatedNodes, nodeLookup, {
|
const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, {
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
elevateNodesOnSelect,
|
elevateNodesOnSelect,
|
||||||
});
|
});
|
||||||
@@ -276,7 +287,10 @@ const createRFStore = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
positionAbsolute,
|
computed: {
|
||||||
|
...node.computed,
|
||||||
|
positionAbsolute,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -323,6 +337,7 @@ const createRFStore = ({
|
|||||||
|
|
||||||
set(currentConnection);
|
set(currentConnection);
|
||||||
},
|
},
|
||||||
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
// @todo: what should we do about this? Do we still need it?
|
// @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
|
// if you are on a SPA with multiple flows, we want to make sure that the store gets resetted
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
infiniteExtent,
|
infiniteExtent,
|
||||||
ConnectionMode,
|
ConnectionMode,
|
||||||
updateNodes,
|
adoptUserProvidedNodes,
|
||||||
getNodesBounds,
|
getNodesBounds,
|
||||||
getViewportForBounds,
|
getViewportForBounds,
|
||||||
Transform,
|
Transform,
|
||||||
|
updateConnectionLookup,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type { Edge, Node, ReactFlowStore } from '../types';
|
import type { Edge, Node, ReactFlowStore } from '../types';
|
||||||
@@ -22,17 +23,17 @@ const getInitialState = ({
|
|||||||
height?: number;
|
height?: number;
|
||||||
fitView?: boolean;
|
fitView?: boolean;
|
||||||
} = {}): ReactFlowStore => {
|
} = {}): ReactFlowStore => {
|
||||||
const nodeLookup = new Map<string, Node>();
|
const nodeLookup = new Map();
|
||||||
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
|
const connectionLookup = updateConnectionLookup(new Map(), edges);
|
||||||
|
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||||
|
nodeOrigin: [0, 0],
|
||||||
|
elevateNodesOnSelect: false,
|
||||||
|
});
|
||||||
|
|
||||||
let transform: Transform = [0, 0, 1];
|
let transform: Transform = [0, 0, 1];
|
||||||
|
|
||||||
if (fitView && width && height) {
|
if (fitView && width && height) {
|
||||||
const nodesWithDimensions = nextNodes.map((node) => ({
|
const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height);
|
||||||
...node,
|
|
||||||
width: node.size?.width,
|
|
||||||
height: node.size?.height,
|
|
||||||
}));
|
|
||||||
const bounds = getNodesBounds(nodesWithDimensions, [0, 0]);
|
const bounds = getNodesBounds(nodesWithDimensions, [0, 0]);
|
||||||
const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
|
const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
|
||||||
transform = [x, y, zoom];
|
transform = [x, y, zoom];
|
||||||
@@ -46,6 +47,7 @@ const getInitialState = ({
|
|||||||
nodes: nextNodes,
|
nodes: nextNodes,
|
||||||
nodeLookup,
|
nodeLookup,
|
||||||
edges: edges,
|
edges: edges,
|
||||||
|
connectionLookup,
|
||||||
onNodesChange: null,
|
onNodesChange: null,
|
||||||
onEdgesChange: null,
|
onEdgesChange: null,
|
||||||
hasDefaultNodes: false,
|
hasDefaultNodes: false,
|
||||||
@@ -65,7 +67,7 @@ const getInitialState = ({
|
|||||||
paneDragging: false,
|
paneDragging: false,
|
||||||
noPanClassName: 'nopan',
|
noPanClassName: 'nopan',
|
||||||
nodeOrigin: [0, 0],
|
nodeOrigin: [0, 0],
|
||||||
nodeDragThreshold: 0,
|
nodeDragThreshold: 1,
|
||||||
|
|
||||||
snapGrid: [15, 15],
|
snapGrid: [15, 15],
|
||||||
snapToGrid: false,
|
snapToGrid: false,
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
/* this will be exported as base.css and can be used for a basic styling */
|
/* this will be exported as base.css and can be used for a basic styling */
|
||||||
@import '../../../system/src/styles/init.css';
|
@import '../../../system/src/styles/init.css';
|
||||||
@import '../../../system/src/styles/base.css';
|
@import '../../../system/src/styles/base.css';
|
||||||
|
|
||||||
|
.react-flow {
|
||||||
|
--edge-label-background-color-default: #ffffff;
|
||||||
|
--edge-label-color-default: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow.dark {
|
||||||
|
--edge-label-background-color-default: #141414;
|
||||||
|
--edge-label-color-default: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__edge-textbg {
|
||||||
|
fill: var(--edge-label-background-color, var(--edge-label-background-color-default));
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__edge-text {
|
||||||
|
fill: var(--edge-label-color, var(--edge-label-color-default));
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,3 +2,21 @@
|
|||||||
@import '../../../system/src/styles/init.css';
|
@import '../../../system/src/styles/init.css';
|
||||||
@import '../../../system/src/styles/style.css';
|
@import '../../../system/src/styles/style.css';
|
||||||
@import '../../../system/src/styles/node-resizer.css';
|
@import '../../../system/src/styles/node-resizer.css';
|
||||||
|
|
||||||
|
.react-flow {
|
||||||
|
--edge-label-background-color-default: #ffffff;
|
||||||
|
--edge-label-color-default: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow.dark {
|
||||||
|
--edge-label-background-color-default: #141414;
|
||||||
|
--edge-label-color-default: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__edge-textbg {
|
||||||
|
fill: var(--edge-label-background-color, var(--edge-label-background-color-default));
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__edge-text {
|
||||||
|
fill: var(--edge-label-color, var(--edge-label-color-default));
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user