@@ -0,0 +1,13 @@
|
||||
# Examples for Testing & Development
|
||||
These examples are used for feature development and E2E testing.
|
||||
|
||||
## Overview
|
||||
SvelteKit App with Svelte Flow examples at [`examples/svelte`](./svelte)
|
||||
|
||||
Vite App with React Flow examples at [`examples/react`](./react)
|
||||
|
||||
Astro App used for SSR testing at [`examples/astro-xyflow`](./astro-xyflow)
|
||||
|
||||
## E2E
|
||||
|
||||
For furhter documentation of E2E tests have a look at [`/tests/playwright`](/tests/playwright).
|
||||
@@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = {
|
||||
left: 'auto',
|
||||
};
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
|
||||
const CustomNode: FC<NodeProps> = ({ data, positionAbsolute }) => {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
@@ -18,7 +18,7 @@ const CustomNode: FC<NodeProps> = ({ data, xPos, yPos }) => {
|
||||
<div>
|
||||
Position:{' '}
|
||||
<strong>
|
||||
{xPos.toFixed(2)},{yPos.toFixed(2)}
|
||||
{positionAbsolute.x.toFixed(2)},{positionAbsolute.y.toFixed(2)}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ const initialNodes: Node[] = [
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
size: nodeSize,
|
||||
...nodeSize,
|
||||
handles: [
|
||||
{
|
||||
type: 'source',
|
||||
@@ -41,23 +41,19 @@ const initialNodes: Node[] = [
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
size: nodeSize,
|
||||
...nodeSize,
|
||||
handles: [
|
||||
{
|
||||
type: 'source',
|
||||
position: Position.Bottom,
|
||||
x: nodeSize.width * 0.5,
|
||||
y: nodeSize.height,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
type: 'target',
|
||||
position: Position.Top,
|
||||
x: nodeSize.width * 0.5,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -65,23 +61,19 @@ const initialNodes: Node[] = [
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
size: nodeSize,
|
||||
...nodeSize,
|
||||
handles: [
|
||||
{
|
||||
type: 'source',
|
||||
position: Position.Bottom,
|
||||
x: nodeSize.width * 0.5,
|
||||
y: nodeSize.height,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
type: 'target',
|
||||
position: Position.Top,
|
||||
x: nodeSize.width * 0.5,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -11,10 +11,8 @@
|
||||
data: { label: 'Node 0' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
size: {
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
width: 100,
|
||||
height: 40,
|
||||
handles: [
|
||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||
@@ -26,10 +24,8 @@
|
||||
data: { label: 'A' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
size: {
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
width: 100,
|
||||
height: 40,
|
||||
handles: [
|
||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||
@@ -41,10 +37,8 @@
|
||||
data: { label: 'B' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
size: {
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
width: 100,
|
||||
height: 40,
|
||||
handles: [
|
||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||
@@ -56,10 +50,8 @@
|
||||
data: { label: 'C' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
size: {
|
||||
width: 100,
|
||||
height: 40,
|
||||
},
|
||||
width: 100,
|
||||
height: 40,
|
||||
handles: [
|
||||
{ type: 'source', x: 100, y: 20, position: Position.Right },
|
||||
{ type: 'target', x: 0, y: 20, position: Position.Left },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# React Flow examples
|
||||
|
||||
This Vite app is used internally to test the library.
|
||||
This Vite app is used internally to develop and test the library.
|
||||
|
||||
## Start local dev server
|
||||
|
||||
@@ -8,5 +8,15 @@ This Vite app is used internally to test the library.
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Adding new example
|
||||
|
||||
Development of the library is done against `src/examples`. Feel free to add new implementations for features that you develop.
|
||||
|
||||
1. Create a new folder & flow at `src/examples/`
|
||||
2. Register the new route in `src/App/routes.ts`
|
||||
|
||||
## Adding new E2E implementation
|
||||
E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under [`http://localhost:3000/tests/generic/$foldername/$filename`](http://localhost:5173/tests/generic/nodes/general). For further documentation visit [`xyflow/tests/playwright`](/tests/playwright).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Basic from '../examples/Basic';
|
||||
import Backgrounds from '../examples/Backgrounds';
|
||||
import ColorMode from '../examples/ColorMode';
|
||||
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
|
||||
import ControlledViewport from '../examples/ControlledViewport';
|
||||
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
||||
@@ -62,6 +63,11 @@ const routes: IRoute[] = [
|
||||
path: 'backgrounds',
|
||||
component: Backgrounds,
|
||||
},
|
||||
{
|
||||
name: 'Color Mode',
|
||||
path: 'color-mode',
|
||||
component: ColorMode,
|
||||
},
|
||||
{
|
||||
name: 'Cancel Connection',
|
||||
path: 'cancel-connection',
|
||||
|
||||
Vendored
+5
-5
@@ -2,10 +2,10 @@ import { BackgroundProps, ControlProps, Edge, MiniMapProps, Node, PanelProps, Re
|
||||
|
||||
declare global {
|
||||
interface FlowConfig {
|
||||
flowProps: Omit<ReactFlowProps, 'nodes' | 'edges'> & { nodes: Node[]; edges: Edge[] };
|
||||
panelProps: PanelProps;
|
||||
backgroundProps: BackgroundProps;
|
||||
controlsProps: ControlProps;
|
||||
minimapProps: MiniMapProps;
|
||||
flowProps?: Omit<ReactFlowProps, 'nodes' | 'edges'> & { nodes: Node[]; edges: Edge[] };
|
||||
panelProps?: PanelProps;
|
||||
backgroundProps?: BackgroundProps;
|
||||
controlsProps?: ControlProps;
|
||||
minimapProps?: MiniMapProps;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ChangeEventHandler, useCallback, useState } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
addEdge,
|
||||
Node,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
OnConnect,
|
||||
Edge,
|
||||
MiniMap,
|
||||
Background,
|
||||
Controls,
|
||||
Panel,
|
||||
ColorMode,
|
||||
Position,
|
||||
} from '@xyflow/react';
|
||||
|
||||
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;
|
||||
@@ -49,7 +49,7 @@ const DnDFlow = () => {
|
||||
|
||||
if (reactFlowInstance) {
|
||||
const type = event.dataTransfer.getData('application/reactflow');
|
||||
const position = reactFlowInstance.screenToFlowCoordinate({
|
||||
const position = reactFlowInstance.screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
@@ -66,7 +66,7 @@ const DnDFlow = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.dndflow}>
|
||||
<ReactFlowProvider nodes={initialNodes} edges={[]}>
|
||||
<ReactFlowProvider initialNodes={initialNodes} initialEdges={[]}>
|
||||
<div className={styles.wrapper}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
|
||||
@@ -5,7 +5,6 @@ import CustomNode from './CustomNode';
|
||||
import FloatingEdge from './FloatingEdge';
|
||||
import CustomConnectionLine from './CustomConnectionLine';
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import './style.css';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
|
||||
@@ -4,12 +4,9 @@ import { Node, Position, MarkerType, XYPosition } from '@xyflow/react';
|
||||
// of the line between the center of the intersectionNode and the target node
|
||||
function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
|
||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||
const {
|
||||
width: intersectionNodeWidth,
|
||||
height: intersectionNodeHeight,
|
||||
positionAbsolute: intersectionNodePosition,
|
||||
} = intersectionNode;
|
||||
const targetPosition = targetNode.positionAbsolute!;
|
||||
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode;
|
||||
const intersectionNodePosition = intersectionNode.computed?.positionAbsolute!;
|
||||
const targetPosition = targetNode.computed?.positionAbsolute!;
|
||||
|
||||
const w = intersectionNodeWidth! / 2;
|
||||
const h = intersectionNodeHeight! / 2;
|
||||
@@ -32,7 +29,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
|
||||
|
||||
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
||||
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
const n = { ...node.positionAbsolute, ...node };
|
||||
const n = { ...node.computed?.positionAbsolute, ...node };
|
||||
const nx = Math.round(n.x!);
|
||||
const ny = Math.round(n.y!);
|
||||
const px = Math.round(intersectionPoint.x);
|
||||
@@ -41,13 +38,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + n.width! - 1) {
|
||||
if (px >= nx + n.computed?.width! - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y! + n.height! - 1) {
|
||||
if (py >= n.y! + n.computed?.height! - 1) {
|
||||
return Position.Bottom;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ const EdgesFlow = () => {
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseMove={onEdgeMouseMove}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
onDelete={console.log}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + (n.width ?? 0) - 1) {
|
||||
if (px >= nx + (n.computed?.width ?? 0) - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y + (n.height ?? 0) - 1) {
|
||||
if (py >= n.y + (n.computed?.height ?? 0) - 1) {
|
||||
return Position.Bottom;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ import CustomResizer from './CustomResizer';
|
||||
import VerticalResizer from './VerticalResizer';
|
||||
import HorizontalResizer from './HorizontalResizer';
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
|
||||
const nodeTypes = {
|
||||
defaultResizer: DefaultResizer,
|
||||
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 onPaneClick = (event: ReactMouseEvent) => console.log('pane click', 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 onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
|
||||
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
|
||||
@@ -230,9 +230,9 @@ const OverviewFlow = () => {
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
onPaneMouseMove={onPaneMouseMove}
|
||||
>
|
||||
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
|
||||
<MiniMap nodeBorderRadius={2} />
|
||||
<Controls />
|
||||
<Background color="#aaa" gap={25} />
|
||||
<Background gap={25} />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
ReactFlowInstance,
|
||||
Edge,
|
||||
Node,
|
||||
NodeChange,
|
||||
@@ -18,11 +17,6 @@ import {
|
||||
|
||||
import { getNodesAndEdges } from './utils';
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
reactFlowInstance.fitView();
|
||||
console.log(reactFlowInstance.getNodes());
|
||||
};
|
||||
|
||||
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
|
||||
|
||||
const StressFlow = () => {
|
||||
@@ -64,11 +58,11 @@ const StressFlow = () => {
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onConnect={onConnect}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgeChange}
|
||||
minZoom={0.2}
|
||||
fitView
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
@@ -20,8 +20,6 @@ export function getNodesAndEdges(xElements = 10, yElements = 10): ElementsCollec
|
||||
style: { width: 50, height: 30, fontSize: 11 },
|
||||
data,
|
||||
position,
|
||||
width: 50,
|
||||
height: 30,
|
||||
};
|
||||
initialNodes.push(node);
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ const idStyle: CSSProperties = {
|
||||
left: 2,
|
||||
};
|
||||
|
||||
const DebugNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
|
||||
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsolute, id }) => {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<div style={idStyle}>{id}</div>
|
||||
<div style={infoStyle}>
|
||||
x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex}
|
||||
x:{Math.round(positionAbsolute.x)} y:{Math.round(positionAbsolute.y)} z:{zIndex}
|
||||
</div>
|
||||
<Handle type="source" position={Position.Bottom} />
|
||||
</>
|
||||
|
||||
@@ -18,8 +18,8 @@ type FlowProps = {
|
||||
};
|
||||
|
||||
export default ({ flowConfig }: FlowProps) => {
|
||||
const [nodes, setNodes] = useState(flowConfig.flowProps.nodes);
|
||||
const [edges, setEdges] = useState(flowConfig.flowProps.edges);
|
||||
const [nodes, setNodes] = useState(flowConfig.flowProps?.nodes);
|
||||
const [edges, setEdges] = useState(flowConfig.flowProps?.edges);
|
||||
const props = { ...flowConfig.flowProps, nodes, edges };
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback((changes) => setNodes((nds) => applyNodeChanges(changes, nds)), []);
|
||||
|
||||
@@ -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 { createRoot } from 'react-dom/client';
|
||||
|
||||
import App from './App';
|
||||
import App from './App/index';
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import './index.css';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Svelte Flow examples
|
||||
# Svelte Flow examples
|
||||
|
||||
This Vite app is used internally to test the library.
|
||||
|
||||
@@ -8,3 +8,13 @@ This Vite app is used internally to test the library.
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Adding new example
|
||||
|
||||
Development of the library is done against `src/routes/examples`. Feel free to add new implementations for features that you develop.
|
||||
|
||||
1. Create a new folder & flow at `src/routes/examples/`
|
||||
2. Register the new route in `src/components/Header/Header.svelte`
|
||||
|
||||
## Adding new E2E implementation
|
||||
|
||||
E2E testing is done against the flows implemented in `src/generic-tests`. Adding a new configuration file automatically adds a new route under [`http://localhost:5173/tests/generic/$foldername/$filename`](http://localhost:5173/tests/generic/nodes/general). For further documentation visit [`xyflow/tests/playwright`](/tests/playwright).
|
||||
|
||||
Vendored
+5
-5
@@ -20,11 +20,11 @@ declare global {
|
||||
}
|
||||
|
||||
interface FlowConfig {
|
||||
flowProps: Omit<SvelteFlowProps, 'nodes' | 'edges'> & { nodes: Node[]; edges: Edge[] };
|
||||
panelProps: PanelProps;
|
||||
backgroundProps: BackgroundProps;
|
||||
controlsProps: ControlsProps;
|
||||
minimapProps: MiniMapProps;
|
||||
flowProps?: Omit<SvelteFlowProps, 'nodes' | 'edges'> & { nodes: Node[]; edges: Edge[] };
|
||||
panelProps?: PanelProps;
|
||||
backgroundProps?: BackgroundProps;
|
||||
controlsProps?: ControlsProps;
|
||||
minimapProps?: MiniMapProps;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
const routes = [
|
||||
'add-node-on-drop',
|
||||
'colormode',
|
||||
'custom-connection-line',
|
||||
'customnode',
|
||||
'dagre',
|
||||
@@ -12,6 +13,7 @@
|
||||
'figma',
|
||||
'interaction',
|
||||
'intersections',
|
||||
'node-toolbar',
|
||||
'overview',
|
||||
'stress',
|
||||
'subflows',
|
||||
|
||||
@@ -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,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { SvelteFlow, useSvelteFlow } from '@xyflow/svelte';
|
||||
import type { Edge, Node } from '@xyflow/svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { SvelteFlow, useSvelteFlow, type Edge, type Node } from '@xyflow/svelte';
|
||||
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
|
||||
@@ -22,7 +21,7 @@
|
||||
let id = 1;
|
||||
const getId = () => `${id++}`;
|
||||
|
||||
const { screenToFlowCoordinate, flowToScreenCoordinate } = useSvelteFlow();
|
||||
const { screenToFlowPosition, flowToScreenPosition } = useSvelteFlow();
|
||||
|
||||
function handleConnectEnd({ detail: { event } }: { detail: { event: MouseEvent | TouchEvent } }) {
|
||||
// See of connection landed inside the flow pane
|
||||
@@ -34,7 +33,7 @@
|
||||
y: event.clientY
|
||||
};
|
||||
|
||||
const doubleTransformedPosition = flowToScreenCoordinate(screenToFlowCoordinate(position));
|
||||
const doubleTransformedPosition = flowToScreenPosition(screenToFlowPosition(position));
|
||||
console.log(
|
||||
'Is transforming in both directions (screen-flow, flow-screen) the same?',
|
||||
position.x === doubleTransformedPosition.x && position.y === doubleTransformedPosition.y
|
||||
@@ -43,8 +42,8 @@
|
||||
const newNode: Node = {
|
||||
id,
|
||||
data: { label: `Node ${id}` },
|
||||
// project the screen coordinates to pane coordinates
|
||||
position: screenToFlowCoordinate(position),
|
||||
// project the screen position to pane position
|
||||
position: screenToFlowPosition(position),
|
||||
// set the origin of the new node so it is centered
|
||||
origin: [0.5, 0.0]
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -174,6 +174,8 @@
|
||||
event.preventDefault();
|
||||
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
|
||||
autoPanOnNodeDrag
|
||||
connectionMode={ConnectionMode.Strict}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import { Handle, Position, type NodeProps, type XYPosition } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let data: { label: string } = { label: 'Node' };
|
||||
export let xPos: number = 0;
|
||||
export let yPos: number = 0;
|
||||
export let positionAbsolute: XYPosition = { x: 0, y: 0 };
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
<div>{data.label}</div>
|
||||
<div>{~~xPos}, {~~yPos}</div>
|
||||
<div>{~~positionAbsolute.x}, {~~positionAbsolute.y}</div>
|
||||
|
||||
<Handle type="target" position={Position.Top} />
|
||||
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
import { Handle, Position, type NodeProps, type XYPosition } from '@xyflow/svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: string;
|
||||
export let xPos: number = 0;
|
||||
export let yPos: number = 0;
|
||||
export let positionAbsolute: XYPosition = { x: 0, y: 0 };
|
||||
export let zIndex: number = 0;
|
||||
</script>
|
||||
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<div>{id}</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>
|
||||
<Handle type="source" position={Position.Bottom} />
|
||||
|
||||
<style>
|
||||
.custom {
|
||||
background: #ffcc00;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
export let flowConfig: FlowConfig;
|
||||
|
||||
// Create writables here so it is easier to create test cases
|
||||
const nodes = writable(flowConfig.flowProps.nodes);
|
||||
const edges = writable(flowConfig.flowProps.edges);
|
||||
const nodes = writable(flowConfig.flowProps?.nodes);
|
||||
const edges = writable(flowConfig.flowProps?.edges);
|
||||
|
||||
const flowProps = { ...flowConfig.flowProps, nodes, edges };
|
||||
</script>
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "turbo run dev --parallel --concurrency 12",
|
||||
"dev:svelte": "turbo run dev --filter=svelte --filter=system",
|
||||
"dev:react": "turbo run dev --filter=react",
|
||||
"dev:svelte": "turbo run dev --filter=svelte-examples...",
|
||||
"dev:react": "turbo run dev --filter=react-examples...",
|
||||
"test:svelte": "pnpm --filter=playwright run test:svelte",
|
||||
"test:svelte:ui": "pnpm --filter=playwright run test:svelte:ui",
|
||||
"test:react": "pnpm --filter=playwright run test:react",
|
||||
|
||||
@@ -26,6 +26,6 @@ type DotPatternProps = {
|
||||
|
||||
export function DotPattern({ radius, className }: DotPatternProps) {
|
||||
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/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 { shallow } from 'zustand/shallow';
|
||||
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({
|
||||
style,
|
||||
className,
|
||||
nodeStrokeColor = 'transparent',
|
||||
nodeColor = '#e2e2e2',
|
||||
nodeStrokeColor,
|
||||
nodeColor,
|
||||
nodeClassName = '',
|
||||
nodeBorderRadius = 5,
|
||||
nodeStrokeWidth = 2,
|
||||
nodeStrokeWidth,
|
||||
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
|
||||
// a component properly.
|
||||
nodeComponent,
|
||||
maskColor = 'rgb(240, 240, 240, 0.6)',
|
||||
maskColor,
|
||||
maskStrokeColor = 'none',
|
||||
maskStrokeWidth = 1,
|
||||
position = 'bottom-right',
|
||||
@@ -118,7 +118,7 @@ function MiniMap({
|
||||
|
||||
const onSvgNodeClick = onNodeClick
|
||||
? useCallback((event: MouseEvent, nodeId: string) => {
|
||||
const node = store.getState().nodes.find((n) => n.id === nodeId)!;
|
||||
const node = store.getState().nodeLookup.get(nodeId)!;
|
||||
onNodeClick(event, node);
|
||||
}, [])
|
||||
: undefined;
|
||||
@@ -126,7 +126,15 @@ function MiniMap({
|
||||
return (
|
||||
<Panel
|
||||
position={position}
|
||||
style={style}
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
'--minimap-mask-color-props': typeof maskColor === 'string' ? maskColor : undefined,
|
||||
'--minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined,
|
||||
'--minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined,
|
||||
'--minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined,
|
||||
} as CSSProperties
|
||||
}
|
||||
className={cc(['react-flow__minimap', className])}
|
||||
data-testid="rf__minimap"
|
||||
>
|
||||
@@ -153,7 +161,6 @@ function MiniMap({
|
||||
className="react-flow__minimap-mask"
|
||||
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`}
|
||||
fill={maskColor}
|
||||
fillRule="evenodd"
|
||||
stroke={maskStrokeColor}
|
||||
strokeWidth={maskStrokeWidth}
|
||||
|
||||
@@ -31,9 +31,11 @@ function MiniMapNode({
|
||||
ry={borderRadius}
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
style={{
|
||||
fill,
|
||||
stroke: strokeColor,
|
||||
strokeWidth,
|
||||
}}
|
||||
shapeRendering={shapeRendering}
|
||||
onClick={onClick ? (event) => onClick(event, id) : undefined}
|
||||
/>
|
||||
|
||||
@@ -12,15 +12,18 @@ import type { MiniMapNodes, GetMiniMapNodeAttribute } from './types';
|
||||
declare const window: any;
|
||||
|
||||
const selector = (s: ReactFlowState) => s.nodeOrigin;
|
||||
const selectorNodes = (s: ReactFlowState) => s.nodes.filter((node) => !node.hidden && node.width && node.height);
|
||||
const selectorNodes = (s: ReactFlowState) =>
|
||||
s.nodes.filter(
|
||||
(node) => !node.hidden && (node.computed?.width || node.width) && (node.computed?.height || node.height)
|
||||
);
|
||||
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
|
||||
|
||||
function MiniMapNodes({
|
||||
nodeStrokeColor = 'transparent',
|
||||
nodeColor = '#e2e2e2',
|
||||
nodeStrokeColor,
|
||||
nodeColor,
|
||||
nodeClassName = '',
|
||||
nodeBorderRadius = 5,
|
||||
nodeStrokeWidth = 2,
|
||||
nodeStrokeWidth,
|
||||
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
|
||||
// a component properly.
|
||||
nodeComponent: NodeComponent = MiniMapNode,
|
||||
@@ -38,20 +41,22 @@ function MiniMapNodes({
|
||||
<>
|
||||
{nodes.map((node) => {
|
||||
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
||||
const color = nodeColor === undefined ? undefined : nodeColorFunc(node);
|
||||
const strokeColor = nodeStrokeColor === undefined ? undefined : nodeStrokeColorFunc(node);
|
||||
|
||||
return (
|
||||
<NodeComponent
|
||||
key={node.id}
|
||||
x={x}
|
||||
y={y}
|
||||
width={node.width!}
|
||||
height={node.height!}
|
||||
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)}
|
||||
color={color}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
strokeColor={strokeColor}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
shapeRendering={shapeRendering}
|
||||
onClick={onClick}
|
||||
|
||||
@@ -42,10 +42,10 @@ export type MiniMapNodeProps = {
|
||||
height: number;
|
||||
borderRadius: number;
|
||||
className: string;
|
||||
color: string;
|
||||
color?: string;
|
||||
shapeRendering: string;
|
||||
strokeColor: string;
|
||||
strokeWidth: number;
|
||||
strokeColor?: string;
|
||||
strokeWidth?: number;
|
||||
style?: CSSProperties;
|
||||
selected: boolean;
|
||||
onClick?: (event: MouseEvent, id: string) => void;
|
||||
|
||||
@@ -65,13 +65,13 @@ function ResizeControl({
|
||||
|
||||
const dragHandler = drag<HTMLDivElement, unknown>()
|
||||
.on('start', (event: ResizeDragEvent) => {
|
||||
const { nodes, transform, snapGrid, snapToGrid } = store.getState();
|
||||
const node = nodes.find((n) => n.id === id);
|
||||
const { nodeLookup, transform, snapGrid, snapToGrid } = store.getState();
|
||||
const node = nodeLookup.get(id);
|
||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
|
||||
prevValues.current = {
|
||||
width: node?.width ?? 0,
|
||||
height: node?.height ?? 0,
|
||||
width: node?.computed?.width ?? 0,
|
||||
height: node?.computed?.height ?? 0,
|
||||
x: node?.position.x ?? 0,
|
||||
y: node?.position.y ?? 0,
|
||||
};
|
||||
@@ -86,9 +86,9 @@ function ResizeControl({
|
||||
onResizeStart?.(event, { ...prevValues.current });
|
||||
})
|
||||
.on('drag', (event: ResizeDragEvent) => {
|
||||
const { nodes, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState();
|
||||
const { nodeLookup, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState();
|
||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
const node = nodes.find((n) => n.id === id);
|
||||
const node = nodeLookup.get(id);
|
||||
|
||||
if (node) {
|
||||
const changes: NodeChange[] = [];
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useCallback, CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
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 { useStore } from '../../hooks/useStore';
|
||||
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||
import NodeToolbarPortal from './NodeToolbarPortal';
|
||||
import { Align, NodeToolbarProps } from './types';
|
||||
import { NodeToolbarProps } from './types';
|
||||
|
||||
const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) =>
|
||||
a?.positionAbsolute?.x === b?.positionAbsolute?.x &&
|
||||
a?.positionAbsolute?.y === b?.positionAbsolute?.y &&
|
||||
a?.computed?.positionAbsolute?.x === b?.computed?.positionAbsolute?.x &&
|
||||
a?.computed?.positionAbsolute?.y === b?.computed?.positionAbsolute?.y &&
|
||||
a?.width === b?.width &&
|
||||
a?.height === b?.height &&
|
||||
a?.selected === b?.selected &&
|
||||
@@ -22,53 +22,15 @@ const nodesEqualityFn = (a: Node[], b: Node[]) => {
|
||||
};
|
||||
|
||||
const storeSelector = (state: ReactFlowState) => ({
|
||||
transform: state.transform,
|
||||
viewport: {
|
||||
x: state.transform[0],
|
||||
y: state.transform[1],
|
||||
zoom: state.transform[2],
|
||||
},
|
||||
nodeOrigin: state.nodeOrigin,
|
||||
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({
|
||||
nodeId,
|
||||
children,
|
||||
@@ -87,17 +49,19 @@ function NodeToolbar({
|
||||
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
|
||||
|
||||
return nodeIds.reduce<Node[]>((acc, id) => {
|
||||
const node = state.nodes.find((n) => n.id === id);
|
||||
const node = state.nodeLookup.get(id);
|
||||
if (node) {
|
||||
acc.push(node);
|
||||
}
|
||||
return acc;
|
||||
}, [] as Node[]);
|
||||
}, []);
|
||||
},
|
||||
[nodeId, contextNodeId]
|
||||
);
|
||||
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 =
|
||||
typeof isVisible === 'boolean' ? isVisible : nodes.length === 1 && nodes[0].selected && selectedNodesCount === 1;
|
||||
|
||||
@@ -110,14 +74,19 @@ function NodeToolbar({
|
||||
|
||||
const wrapperStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
transform: getTransform(nodeRect, transform, position, offset, align),
|
||||
transform: getNodeToolbarTransform(nodeRect, viewport, position, offset, align),
|
||||
zIndex,
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<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}
|
||||
</div>
|
||||
</NodeToolbarPortal>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type { Position } from '@xyflow/system';
|
||||
import type { Position, Align } from '@xyflow/system';
|
||||
|
||||
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
|
||||
nodeId?: string | string[];
|
||||
@@ -8,5 +8,3 @@ export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
|
||||
offset?: number;
|
||||
align?: Align;
|
||||
};
|
||||
|
||||
export type Align = 'center' | 'start' | 'end';
|
||||
|
||||
@@ -43,7 +43,7 @@ const ConnectionLine = ({
|
||||
const { fromNode, handleId, toX, toY, connectionMode } = useStore(
|
||||
useCallback(
|
||||
(s: ReactFlowStore) => ({
|
||||
fromNode: s.nodes.find((n) => n.id === nodeId),
|
||||
fromNode: s.nodeLookup.get(nodeId),
|
||||
handleId: s.connectionStartHandle?.handleId,
|
||||
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
|
||||
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
|
||||
@@ -65,10 +65,10 @@ const ConnectionLine = ({
|
||||
}
|
||||
|
||||
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
|
||||
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2;
|
||||
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0;
|
||||
const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX;
|
||||
const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY;
|
||||
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed?.width ?? 0) / 2;
|
||||
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0;
|
||||
const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX;
|
||||
const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY;
|
||||
const fromPosition = fromHandle?.position;
|
||||
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { isNumeric } from '@xyflow/system';
|
||||
|
||||
import type { BaseEdgeProps } from '../../types';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
|
||||
const BaseEdge = ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react';
|
||||
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';
|
||||
@@ -53,26 +53,30 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
const [updateHover, setUpdateHover] = useState<boolean>(false);
|
||||
const [updating, setUpdating] = useState<boolean>(false);
|
||||
const store = useStoreApi();
|
||||
const edgePosition = useStore((state) => {
|
||||
const sourceNode = state.nodes.find((n) => n.id === source);
|
||||
const targetNode = state.nodes.find((n) => n.id === target);
|
||||
const edgePosition = useStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const sourceNode = state.nodeLookup.get(source);
|
||||
const targetNode = state.nodeLookup.get(target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pos = getEdgePosition({
|
||||
id,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
sourceHandle: sourceHandleId || null,
|
||||
targetHandle: targetHandleId || null,
|
||||
connectionMode: state.connectionMode,
|
||||
onError: state.onError,
|
||||
});
|
||||
|
||||
return pos;
|
||||
}, shallow);
|
||||
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]);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* The Handle component is used to connect nodes. When the user mousedowns a handle, we start the connection process.
|
||||
* The user can then drag the connection to another handle or node. When the user releases the mouse, we check if the
|
||||
* connection is valid and if so, we call the onConnect callback.
|
||||
*/
|
||||
import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
@@ -12,7 +12,7 @@ export function getMouseHandler(
|
||||
return handler === undefined
|
||||
? handler
|
||||
: (event: MouseEvent) => {
|
||||
const node = getState().nodes.find((n) => n.id === id)!;
|
||||
const node = getState().nodeLookup.get(id)!;
|
||||
handler(event, { ...node });
|
||||
};
|
||||
}
|
||||
@@ -35,8 +35,8 @@ export function handleNodeClick({
|
||||
unselect?: boolean;
|
||||
nodeRef?: RefObject<HTMLDivElement>;
|
||||
}) {
|
||||
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodes, onError } = store.getState();
|
||||
const node = nodes.find((n) => n.id === id)!;
|
||||
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeLookup, onError } = store.getState();
|
||||
const node = nodeLookup.get(id);
|
||||
|
||||
if (!node) {
|
||||
onError?.('012', errorMessages['error012'](id));
|
||||
|
||||
@@ -52,8 +52,9 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
||||
disableKeyboardA11y,
|
||||
ariaLabel,
|
||||
rfId,
|
||||
sizeWidth,
|
||||
sizeHeight,
|
||||
positionAbsolute,
|
||||
width,
|
||||
height,
|
||||
}: WrapNodeProps) => {
|
||||
const store = useStoreApi();
|
||||
const nodeRef = useRef<HTMLDivElement>(null);
|
||||
@@ -146,7 +147,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
||||
if (targetPosChanged) {
|
||||
prevTargetPosition.current = targetPosition;
|
||||
}
|
||||
store.getState().updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]);
|
||||
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
|
||||
}
|
||||
}, [id, type, sourcePosition, targetPosition]);
|
||||
|
||||
@@ -186,8 +187,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
||||
transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`,
|
||||
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
||||
visibility: initialized ? 'visible' : 'hidden',
|
||||
width: sizeWidth,
|
||||
height: sizeHeight,
|
||||
width,
|
||||
height,
|
||||
...style,
|
||||
}}
|
||||
data-id={id}
|
||||
@@ -209,8 +210,9 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
||||
id={id}
|
||||
data={data}
|
||||
type={type}
|
||||
xPos={xPos}
|
||||
yPos={yPos}
|
||||
width={width}
|
||||
height={height}
|
||||
positionAbsolute={positionAbsolute}
|
||||
selected={selected}
|
||||
isConnectable={isConnectable}
|
||||
sourcePosition={sourcePosition}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* This is a helper component for calling the onSelectionChange listener.
|
||||
* It will only be mounted if the user has passed an onSelectionChange listener
|
||||
* or is using the useOnSelectionChange hook.
|
||||
* @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component?
|
||||
*/
|
||||
import { memo, useEffect } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
@@ -24,8 +30,6 @@ function areEqual(a: SelectorSlice, b: SelectorSlice) {
|
||||
);
|
||||
}
|
||||
|
||||
// This is just a helper component for calling the onSelectionChange listener.
|
||||
// @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component?
|
||||
const SelectionListener = memo(({ onSelectionChange }: SelectionListenerProps) => {
|
||||
const store = useStoreApi();
|
||||
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* This component helps us to update the store with the vlues coming from the user.
|
||||
* We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`)
|
||||
* and values that have a dedicated setter function in the store (like `setNodes`).
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { StoreApi } from 'zustand';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
@@ -38,6 +43,7 @@ type StoreUpdaterProps = Pick<
|
||||
| 'fitViewOptions'
|
||||
| 'onNodesDelete'
|
||||
| 'onEdgesDelete'
|
||||
| 'onDelete'
|
||||
| 'onNodeDragStart'
|
||||
| 'onNodeDrag'
|
||||
| 'onNodeDragStop'
|
||||
@@ -70,10 +76,10 @@ const selector = (s: ReactFlowState) => ({
|
||||
reset: s.reset,
|
||||
});
|
||||
|
||||
function useStoreUpdater<T>(value: T | undefined, setStoreState: (param: T) => void) {
|
||||
function useStoreUpdater<T>(value: T | undefined, setStoreAction: (param: T) => void) {
|
||||
useEffect(() => {
|
||||
if (typeof value !== 'undefined') {
|
||||
setStoreState(value);
|
||||
setStoreAction(value);
|
||||
}
|
||||
}, [value]);
|
||||
}
|
||||
@@ -123,6 +129,7 @@ const StoreUpdater = ({
|
||||
fitViewOptions,
|
||||
onNodesDelete,
|
||||
onEdgesDelete,
|
||||
onDelete,
|
||||
onNodeDrag,
|
||||
onNodeDragStart,
|
||||
onNodeDragStop,
|
||||
@@ -187,6 +194,7 @@ const StoreUpdater = ({
|
||||
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);
|
||||
|
||||
@@ -63,6 +63,8 @@ const EdgeRenderer = ({
|
||||
children,
|
||||
}: EdgeRendererProps) => {
|
||||
const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
|
||||
// we are grouping edges by zIndex here in order to be able to render them in the correct order
|
||||
// each zIndex gets its own svg element
|
||||
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
|
||||
|
||||
return (
|
||||
@@ -70,7 +72,7 @@ const EdgeRenderer = ({
|
||||
{edgeTree.map(({ level, edges, isMaxLevel }) => (
|
||||
<svg key={level} style={{ zIndex: level }} className="react-flow__edges react-flow__container">
|
||||
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
|
||||
<g>
|
||||
<>
|
||||
{edges.map((edge) => {
|
||||
let edgeType = edge.type || 'default';
|
||||
|
||||
@@ -132,7 +134,7 @@ const EdgeRenderer = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</>
|
||||
</svg>
|
||||
))}
|
||||
{children}
|
||||
|
||||
@@ -48,11 +48,16 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
const updates = entries.map((entry: ResizeObserverEntry) => ({
|
||||
id: entry.target.getAttribute('data-id') as string,
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
forceUpdate: true,
|
||||
}));
|
||||
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);
|
||||
});
|
||||
@@ -86,19 +91,19 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
||||
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
|
||||
|
||||
const clampedPosition = props.nodeExtent
|
||||
? clampPosition(node.positionAbsolute, props.nodeExtent)
|
||||
: node.positionAbsolute;
|
||||
? clampPosition(node.computed?.positionAbsolute, props.nodeExtent)
|
||||
: node.computed?.positionAbsolute;
|
||||
|
||||
const posX = clampedPosition?.x ?? 0;
|
||||
const posY = clampedPosition?.y ?? 0;
|
||||
const posOrigin = getPositionWithOrigin({
|
||||
x: posX,
|
||||
y: posY,
|
||||
width: node.width ?? 0,
|
||||
height: node.height ?? 0,
|
||||
width: node.computed?.width ?? node.width ?? 0,
|
||||
height: node.computed?.height ?? node.height ?? 0,
|
||||
origin: node.origin || props.nodeOrigin,
|
||||
});
|
||||
const initialized = (!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height);
|
||||
const initialized = (!!node.computed?.width && !!node.computed?.height) || (!!node.width && !!node.height);
|
||||
|
||||
return (
|
||||
<NodeComponent
|
||||
@@ -106,8 +111,8 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
||||
id={node.id}
|
||||
className={node.className}
|
||||
style={node.style}
|
||||
sizeWidth={node.size?.width}
|
||||
sizeHeight={node.size?.height}
|
||||
width={node.width ?? undefined}
|
||||
height={node.height ?? undefined}
|
||||
type={nodeType}
|
||||
data={node.data}
|
||||
sourcePosition={node.sourcePosition || Position.Bottom}
|
||||
@@ -117,6 +122,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
||||
yPos={posY}
|
||||
xPosOrigin={posOrigin.x}
|
||||
yPosOrigin={posOrigin.y}
|
||||
positionAbsolute={clampedPosition || { x: 0, y: 0 }}
|
||||
onClick={props.onNodeClick}
|
||||
onMouseEnter={props.onNodeMouseEnter}
|
||||
onMouseMove={props.onNodeMouseMove}
|
||||
|
||||
@@ -23,6 +23,7 @@ import A11yDescriptions from '../../components/A11yDescriptions';
|
||||
import GraphView from '../GraphView';
|
||||
import Wrapper from './Wrapper';
|
||||
import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types';
|
||||
import useColorModeClass from '../../hooks/useColorModeClass';
|
||||
|
||||
const defaultNodeTypes: NodeTypes = {
|
||||
input: InputNode,
|
||||
@@ -82,6 +83,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
onNodeDragStop,
|
||||
onNodesDelete,
|
||||
onEdgesDelete,
|
||||
onDelete,
|
||||
onSelectionChange,
|
||||
onSelectionDragStart,
|
||||
onSelectionDrag,
|
||||
@@ -168,18 +170,20 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
onViewportChange,
|
||||
width,
|
||||
height,
|
||||
colorMode = 'light',
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const rfId = id || '1';
|
||||
const colorModeClassName = useColorModeClass(colorMode);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
style={{ ...style, ...wrapperStyle }}
|
||||
ref={ref}
|
||||
className={cc(['react-flow', className])}
|
||||
className={cc(['react-flow', className, colorModeClassName])}
|
||||
data-testid="rf__wrapper"
|
||||
id={id}
|
||||
>
|
||||
@@ -281,6 +285,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
fitViewOptions={fitViewOptions}
|
||||
onNodesDelete={onNodesDelete}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
onDelete={onDelete}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDrag={onNodeDrag}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
}, []);
|
||||
|
||||
const getNode = useCallback<Instance.GetNode<NodeData>>((id) => {
|
||||
return store.getState().nodes.find((n) => n.id === id);
|
||||
return store.getState().nodeLookup.get(id);
|
||||
}, []);
|
||||
|
||||
const getEdges = useCallback<Instance.GetEdges<EdgeData>>(() => {
|
||||
@@ -127,6 +127,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
onEdgesDelete,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
onDelete,
|
||||
} = store.getState();
|
||||
const { matchingNodes, matchingEdges } = getElementsToRemove<Node, Edge>({
|
||||
nodesToRemove: nodesDeleted || [],
|
||||
@@ -171,6 +172,8 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
onNodesChange(nodeChanges);
|
||||
}
|
||||
}
|
||||
|
||||
onDelete?.({ nodes: matchingNodes, edges: matchingEdges });
|
||||
}
|
||||
|
||||
return { deletedNodes: matchingNodes, deletedEdges: matchingEdges };
|
||||
@@ -181,7 +184,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
nodeOrRect: Node<NodeData> | { id: Node['id'] } | Rect
|
||||
): [Rect | null, Node<NodeData> | null | undefined, boolean] => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const node = isRect ? null : store.getState().nodes.find((n) => n.id === nodeOrRect.id);
|
||||
const node = isRect ? null : store.getState().nodeLookup.get(nodeOrRect.id);
|
||||
|
||||
if (!isRect && !node) {
|
||||
[null, null, isRect];
|
||||
@@ -203,7 +206,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,17 +8,16 @@ function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
|
||||
return useCallback<UpdateNodeInternals>((id: string | string[]) => {
|
||||
const { domNode, updateNodeDimensions } = store.getState();
|
||||
|
||||
const updateIds = Array.isArray(id) ? id : [id];
|
||||
const updates = updateIds.reduce<NodeDimensionUpdate[]>((res, updateId) => {
|
||||
const updates = new Map<string, NodeDimensionUpdate>();
|
||||
|
||||
updateIds.forEach((updateId) => {
|
||||
const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement;
|
||||
|
||||
if (nodeElement) {
|
||||
res.push({ id: updateId, nodeElement, forceUpdate: true });
|
||||
updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true });
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => updateNodeDimensions(updates));
|
||||
}, []);
|
||||
|
||||
@@ -23,8 +23,11 @@ function useUpdateNodePositions() {
|
||||
const yDiff = params.y * yVelo * factor;
|
||||
|
||||
const nodeUpdates = selectedNodes.map((node) => {
|
||||
if (node.positionAbsolute) {
|
||||
let nextPosition = { x: node.positionAbsolute.x + xDiff, y: node.positionAbsolute.y + yDiff };
|
||||
if (node.computed?.positionAbsolute) {
|
||||
let nextPosition = {
|
||||
x: node.computed?.positionAbsolute.x + xDiff,
|
||||
y: node.computed?.positionAbsolute.y + yDiff,
|
||||
};
|
||||
|
||||
if (snapToGrid) {
|
||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||
@@ -40,7 +43,10 @@ function useUpdateNodePositions() {
|
||||
);
|
||||
|
||||
node.position = position;
|
||||
node.positionAbsolute = positionAbsolute;
|
||||
if (!node.computed) {
|
||||
node.computed = {};
|
||||
}
|
||||
node.computed.positionAbsolute = positionAbsolute;
|
||||
}
|
||||
|
||||
return node;
|
||||
|
||||
@@ -12,8 +12,8 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole
|
||||
const visibleEdges =
|
||||
onlyRenderVisible && s.width && s.height
|
||||
? s.edges.filter((e) => {
|
||||
const sourceNode = s.nodes.find((n) => n.id === e.source);
|
||||
const targetNode = s.nodes.find((n) => n.id === e.target);
|
||||
const sourceNode = s.nodeLookup.get(e.source);
|
||||
const targetNode = s.nodeLookup.get(e.target);
|
||||
|
||||
return (
|
||||
sourceNode &&
|
||||
@@ -29,7 +29,7 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole
|
||||
})
|
||||
: s.edges;
|
||||
|
||||
return groupEdgesByZLevel(visibleEdges, s.nodes, elevateEdgesOnSelect);
|
||||
return groupEdgesByZLevel(visibleEdges, s.nodeLookup, elevateEdgesOnSelect);
|
||||
},
|
||||
[onlyRenderVisible, elevateEdgesOnSelect]
|
||||
),
|
||||
|
||||
@@ -73,6 +73,8 @@ export {
|
||||
type Box,
|
||||
type Transform,
|
||||
type CoordinateExtent,
|
||||
type ColorMode,
|
||||
type ColorModeClass,
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -41,18 +41,19 @@ const createRFStore = ({
|
||||
(set, get) => ({
|
||||
...getInitialState({ nodes, edges, width, height, fitView }),
|
||||
setNodes: (nodes: Node[]) => {
|
||||
const { nodes: storeNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
const nextNodes = updateNodes(nodes, storeNodes, { nodeOrigin, elevateNodesOnSelect });
|
||||
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
// Whenver new nodes are set, we need to calculate the absolute positions of the nodes
|
||||
// and update the nodeLookup.
|
||||
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||
|
||||
set({ nodes: nextNodes });
|
||||
},
|
||||
getNodes: () => {
|
||||
return get().nodes;
|
||||
},
|
||||
setEdges: (edges: Edge[]) => {
|
||||
const { defaultEdgeOptions = {} } = get();
|
||||
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
|
||||
},
|
||||
// when the user works with an uncontrolled flow,
|
||||
// we set a flag `hasDefaultNodes` / `hasDefaultEdges`
|
||||
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => {
|
||||
const hasDefaultNodes = typeof nodes !== 'undefined';
|
||||
const hasDefaultEdges = typeof edges !== 'undefined';
|
||||
@@ -68,7 +69,7 @@ const createRFStore = ({
|
||||
};
|
||||
|
||||
if (hasDefaultNodes) {
|
||||
nextState.nodes = updateNodes(nodes, [], {
|
||||
nextState.nodes = updateNodes(nodes, new Map(), {
|
||||
nodeOrigin: get().nodeOrigin,
|
||||
elevateNodesOnSelect: get().elevateNodesOnSelect,
|
||||
});
|
||||
@@ -79,14 +80,27 @@ const createRFStore = ({
|
||||
|
||||
set(nextState);
|
||||
},
|
||||
// Every node gets registerd at a ResizeObserver. Whenever a node
|
||||
// changes its dimensions, this function is called to measure the
|
||||
// new dimensions and update the nodes.
|
||||
updateNodeDimensions: (updates) => {
|
||||
const { onNodesChange, fitView, nodes, fitViewOnInit, fitViewDone, fitViewOnInitOptions, domNode, nodeOrigin } =
|
||||
get();
|
||||
const {
|
||||
onNodesChange,
|
||||
fitView,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
fitViewOnInit,
|
||||
fitViewDone,
|
||||
fitViewOnInitOptions,
|
||||
domNode,
|
||||
nodeOrigin,
|
||||
} = get();
|
||||
const changes: NodeDimensionChange[] = [];
|
||||
|
||||
const updatedNodes = updateNodeDimensionsSystem(
|
||||
updates,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
domNode,
|
||||
nodeOrigin,
|
||||
(id: string, dimensions: Dimensions) => {
|
||||
@@ -102,8 +116,9 @@ const createRFStore = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const nextNodes = updateAbsolutePositions(updatedNodes, nodeOrigin);
|
||||
const nextNodes = updateAbsolutePositions(updatedNodes, nodeLookup, nodeOrigin);
|
||||
|
||||
// we call fitView once initially after all dimensions are set
|
||||
let nextFitViewDone = fitViewDone;
|
||||
if (!fitViewDone && fitViewOnInit) {
|
||||
nextFitViewDone = fitView(nextNodes, {
|
||||
@@ -112,6 +127,11 @@ const createRFStore = ({
|
||||
});
|
||||
}
|
||||
|
||||
// here we are cirmumventing the onNodesChange handler
|
||||
// in order to be able to display nodes even if the user
|
||||
// has not provided an onNodesChange handler.
|
||||
// Nodes are only rendered if they have a width and height
|
||||
// attribute which they get from this handler.
|
||||
set({ nodes: nextNodes, fitViewDone: nextFitViewDone });
|
||||
|
||||
if (changes?.length > 0) {
|
||||
@@ -127,7 +147,7 @@ const createRFStore = ({
|
||||
};
|
||||
|
||||
if (positionChanged) {
|
||||
change.positionAbsolute = node.positionAbsolute;
|
||||
change.positionAbsolute = node.computed?.positionAbsolute;
|
||||
change.position = node.position;
|
||||
}
|
||||
|
||||
@@ -138,12 +158,12 @@ const createRFStore = ({
|
||||
},
|
||||
|
||||
triggerNodeChanges: (changes) => {
|
||||
const { onNodesChange, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
const { onNodesChange, nodeLookup, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
|
||||
if (changes?.length) {
|
||||
if (hasDefaultNodes) {
|
||||
const updatedNodes = applyNodeChanges(changes, nodes);
|
||||
const nextNodes = updateNodes(updatedNodes, nodes, {
|
||||
const nextNodes = updateNodes(updatedNodes, nodeLookup, {
|
||||
nodeOrigin,
|
||||
elevateNodesOnSelect,
|
||||
});
|
||||
@@ -256,7 +276,10 @@ const createRFStore = ({
|
||||
|
||||
return {
|
||||
...node,
|
||||
positionAbsolute,
|
||||
computed: {
|
||||
...node.computed,
|
||||
positionAbsolute,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -22,16 +22,13 @@ const getInitialState = ({
|
||||
height?: number;
|
||||
fitView?: boolean;
|
||||
} = {}): ReactFlowStore => {
|
||||
const nextNodes = updateNodes(nodes, [], { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
|
||||
const nodeLookup = new Map<string, Node>();
|
||||
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
|
||||
|
||||
let transform: Transform = [0, 0, 1];
|
||||
|
||||
if (fitView && width && height) {
|
||||
const nodesWithDimensions = nextNodes.map((node) => ({
|
||||
...node,
|
||||
width: node.size?.width,
|
||||
height: node.size?.height,
|
||||
}));
|
||||
const nodesWithDimensions = nextNodes.filter((node) => node.width && node.height);
|
||||
const bounds = getNodesBounds(nodesWithDimensions, [0, 0]);
|
||||
const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
|
||||
transform = [x, y, zoom];
|
||||
@@ -43,6 +40,7 @@ const getInitialState = ({
|
||||
height: 0,
|
||||
transform,
|
||||
nodes: nextNodes,
|
||||
nodeLookup,
|
||||
edges: edges,
|
||||
onNodesChange: null,
|
||||
onEdgesChange: null,
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/* 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/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/style.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));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
SelectionMode,
|
||||
OnError,
|
||||
IsValidConnection,
|
||||
ColorMode,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -34,6 +35,7 @@ import type {
|
||||
FitViewOptions,
|
||||
OnNodesDelete,
|
||||
OnEdgesDelete,
|
||||
OnDelete,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
NodeDragHandler,
|
||||
@@ -70,6 +72,7 @@ export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
|
||||
onEdgesChange?: OnEdgesChange;
|
||||
onNodesDelete?: OnNodesDelete;
|
||||
onEdgesDelete?: OnEdgesDelete;
|
||||
onDelete?: OnDelete;
|
||||
onSelectionDragStart?: SelectionDragHandler;
|
||||
onSelectionDrag?: SelectionDragHandler;
|
||||
onSelectionDragStop?: SelectionDragHandler;
|
||||
@@ -153,6 +156,7 @@ export type ReactFlowProps = Omit<HTMLAttributes<HTMLDivElement>, 'onError'> & {
|
||||
nodeDragThreshold?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
colorMode?: ColorMode;
|
||||
};
|
||||
|
||||
export type ReactFlowRefType = HTMLDivElement;
|
||||
|
||||
@@ -21,6 +21,7 @@ export type OnEdgesChange = (changes: EdgeChange[]) => void;
|
||||
|
||||
export type OnNodesDelete = (nodes: Node[]) => void;
|
||||
export type OnEdgesDelete = (edges: Edge[]) => void;
|
||||
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
|
||||
|
||||
export type NodeTypes = { [key: string]: ComponentType<NodeProps> };
|
||||
export type NodeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapNodeProps>> };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { NodeBase } from '@xyflow/system';
|
||||
import type { NodeBase, XYPosition } from '@xyflow/system';
|
||||
|
||||
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
|
||||
NodeData,
|
||||
@@ -24,6 +24,7 @@ export type WrapNodeProps<NodeData = any> = Pick<
|
||||
yPos: number;
|
||||
xPosOrigin: number;
|
||||
yPosOrigin: number;
|
||||
positionAbsolute: XYPosition;
|
||||
initialized: boolean;
|
||||
isSelectable: boolean;
|
||||
isDraggable: boolean;
|
||||
@@ -40,6 +41,6 @@ export type WrapNodeProps<NodeData = any> = Pick<
|
||||
noPanClassName: string;
|
||||
rfId: string;
|
||||
disableKeyboardA11y: boolean;
|
||||
sizeWidth?: number;
|
||||
sizeHeight?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
OnEdgesDelete,
|
||||
OnSelectionChangeFunc,
|
||||
UnselectNodesAndEdgesParams,
|
||||
OnDelete,
|
||||
} from '.';
|
||||
|
||||
export type ReactFlowStore = {
|
||||
@@ -46,6 +47,7 @@ export type ReactFlowStore = {
|
||||
height: number;
|
||||
transform: Transform;
|
||||
nodes: Node[];
|
||||
nodeLookup: Map<string, Node>;
|
||||
edges: Edge[];
|
||||
onNodesChange: OnNodesChange | null;
|
||||
onEdgesChange: OnEdgesChange | null;
|
||||
@@ -117,6 +119,7 @@ export type ReactFlowStore = {
|
||||
|
||||
onNodesDelete?: OnNodesDelete;
|
||||
onEdgesDelete?: OnEdgesDelete;
|
||||
onDelete?: OnDelete;
|
||||
onError?: OnError;
|
||||
|
||||
// event handlers
|
||||
@@ -138,10 +141,9 @@ export type ReactFlowStore = {
|
||||
|
||||
export type ReactFlowActions = {
|
||||
setNodes: (nodes: Node[]) => void;
|
||||
getNodes: () => Node[];
|
||||
setEdges: (edges: Edge[]) => void;
|
||||
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
|
||||
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
||||
updateNodeDimensions: (updates: Map<string, NodeDimensionUpdate>) => void;
|
||||
updateNodePositions: UpdateNodePositions;
|
||||
resetSelectedElements: () => void;
|
||||
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;
|
||||
|
||||
@@ -5,14 +5,17 @@ export function handleParentExpand(res: any[], updateItem: any) {
|
||||
const parent = res.find((e) => e.id === updateItem.parentNode);
|
||||
|
||||
if (parent) {
|
||||
const extendWidth = updateItem.position.x + updateItem.width - parent.width;
|
||||
const extendHeight = updateItem.position.y + updateItem.height - parent.height;
|
||||
if (!parent.computed) {
|
||||
parent.computed = {};
|
||||
}
|
||||
const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width;
|
||||
const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height;
|
||||
|
||||
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
|
||||
parent.style = { ...parent.style } || {};
|
||||
|
||||
parent.style.width = parent.style.width ?? parent.width;
|
||||
parent.style.height = parent.style.height ?? parent.height;
|
||||
parent.style.width = parent.style.width ?? parent.computed.width;
|
||||
parent.style.height = parent.style.height ?? parent.computed.height;
|
||||
|
||||
if (extendWidth > 0) {
|
||||
parent.style.width += extendWidth;
|
||||
@@ -36,30 +39,45 @@ export function handleParentExpand(res: any[], updateItem: any) {
|
||||
updateItem.position.y = 0;
|
||||
}
|
||||
|
||||
parent.width = parent.style.width;
|
||||
parent.height = parent.style.height;
|
||||
parent.computed.width = parent.style.width;
|
||||
parent.computed.height = parent.style.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function applies changes to nodes or edges that are triggered by React Flow internally.
|
||||
// When you drag a node for example, React Flow will send a position change update.
|
||||
// This function then applies the changes and returns the updated elements.
|
||||
function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
// we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows
|
||||
if (changes.some((c) => c.type === 'reset')) {
|
||||
return changes.filter((c) => c.type === 'reset').map((c) => c.item);
|
||||
}
|
||||
|
||||
let remainingChanges = changes;
|
||||
const initElements: any[] = changes.filter((c) => c.type === 'add').map((c) => c.item);
|
||||
|
||||
return elements.reduce((res: any[], item: any) => {
|
||||
const currentChanges = changes.filter((c) => c.id === item.id);
|
||||
const nextChanges: any[] = [];
|
||||
const _remainingChanges: any[] = [];
|
||||
|
||||
if (currentChanges.length === 0) {
|
||||
remainingChanges.forEach((c) => {
|
||||
if (c.id === item.id) {
|
||||
nextChanges.push(c);
|
||||
} else {
|
||||
_remainingChanges.push(c);
|
||||
}
|
||||
});
|
||||
remainingChanges = _remainingChanges;
|
||||
|
||||
if (nextChanges.length === 0) {
|
||||
res.push(item);
|
||||
return res;
|
||||
}
|
||||
|
||||
const updateItem = { ...item };
|
||||
|
||||
for (const currentChange of currentChanges) {
|
||||
for (const currentChange of nextChanges) {
|
||||
if (currentChange) {
|
||||
switch (currentChange.type) {
|
||||
case 'select': {
|
||||
@@ -72,7 +90,10 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
if (typeof currentChange.positionAbsolute !== 'undefined') {
|
||||
updateItem.positionAbsolute = currentChange.positionAbsolute;
|
||||
if (!updateItem.computed) {
|
||||
updateItem.computed = {};
|
||||
}
|
||||
updateItem.computed.positionAbsolute = currentChange.positionAbsolute;
|
||||
}
|
||||
|
||||
if (typeof currentChange.dragging !== 'undefined') {
|
||||
@@ -86,8 +107,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
case 'dimensions': {
|
||||
if (typeof currentChange.dimensions !== 'undefined') {
|
||||
updateItem.width = currentChange.dimensions.width;
|
||||
updateItem.height = currentChange.dimensions.height;
|
||||
if (!updateItem.computed) {
|
||||
updateItem.computed = {};
|
||||
}
|
||||
updateItem.computed.width = currentChange.dimensions.width;
|
||||
updateItem.computed.height = currentChange.dimensions.height;
|
||||
}
|
||||
|
||||
if (typeof currentChange.updateStyle !== 'undefined') {
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
## 0.0.28
|
||||
|
||||
There are some breaking changes again (sorry!) but we are very close to the final API for Svelte Flow 1.0.0. The biggest change is that we group node attriubutes (`width`, `height`, `positionAbsolute`) that are added by the library under `node.computed`. This makes it easier to understand, that this stuff comes from the library itself. `node.width` and `node.height` is still an optional node option and can be used to set certain dimensions for SSR or on the client.
|
||||
|
||||
- add `<NodeToolbar />` component
|
||||
- add `on:selectionclick` and `on:selectioncontextmenu` event handlers
|
||||
- add `ondelete({ nodes, edges })` handler
|
||||
- add `zoomActivationKey` prop
|
||||
- add `width` and `height` prop to custom `NodeProps` type
|
||||
- ⚠️ replace `xPos` and `yPos` with `positionAbsolute` prop to custom `NodeProps` type
|
||||
- ⚠️ `node.width/height` and `node.positionAbsolute` can now be found under `node.computed.width/height/positionAbsolute`
|
||||
- ⚠️ `node.width/height` is still optional an can be used for forcing certain dimensions and SSR
|
||||
- refactor keys: you can now disable keys by setting them to `null` (e.g. `selectionKey={null}`)
|
||||
- performance optimization with internal node lookup
|
||||
|
||||
## 0.0.27
|
||||
|
||||
- add `selectionOnDrag` prop - can be used to create figma-like controls in combination with `panOnDrag={false}` / `panOnDrag={[1, 2]}` + `panOnScroll={true}`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/svelte",
|
||||
"version": "0.0.27",
|
||||
"version": "0.0.28",
|
||||
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
||||
"keywords": [
|
||||
"svelte",
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
|
||||
|
||||
return {
|
||||
nodes: get(store.nodes),
|
||||
nodeLookup: get(store.nodeLookup),
|
||||
edges: get(store.edges),
|
||||
nodeExtent: get(store.nodeExtent),
|
||||
snapGrid: snapGrid ? snapGrid : [0, 0],
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
export default function (node: Element, target = 'body') {
|
||||
const targetEl = document.querySelector(target);
|
||||
type PortalOptions = {
|
||||
target?: string;
|
||||
domNode: Element | null;
|
||||
};
|
||||
|
||||
function tryToMount(node: Element, domNode: Element | null, target: string | undefined) {
|
||||
if (!domNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEl = target ? domNode.querySelector(target) : domNode;
|
||||
|
||||
if (targetEl) {
|
||||
targetEl.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
export default function (node: Element, { target, domNode }: PortalOptions) {
|
||||
tryToMount(node, domNode, target);
|
||||
|
||||
return {
|
||||
async update({ target, domNode }: PortalOptions) {
|
||||
tryToMount(node, domNode, target);
|
||||
},
|
||||
destroy() {
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
</script>
|
||||
|
||||
<path
|
||||
{id}
|
||||
d={path}
|
||||
class={cc(['svelte-flow__edge-path', className])}
|
||||
marker-start={markerStart}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import portal from '$lib/actions/portal';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
type $$Props = {};
|
||||
|
||||
const { domNode } = useStore();
|
||||
</script>
|
||||
|
||||
<div use:portal={'.svelte-flow__edgelabel-renderer'}>
|
||||
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: $domNode }}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '@svelte-put/shortcut';
|
||||
import { shortcut, type ShortcutModifierDefinition } from '@svelte-put/shortcut';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import type { KeyHandlerProps } from './types';
|
||||
@@ -12,40 +12,66 @@
|
||||
export let multiSelectionKey: $$Props['multiSelectionKey'] = isMacOs() ? 'Meta' : 'Control';
|
||||
export let deleteKey: $$Props['deleteKey'] = 'Backspace';
|
||||
export let panActivationKey: $$Props['panActivationKey'] = ' ';
|
||||
export let zoomActivationKey: $$Props['zoomActivationKey'] = isMacOs() ? 'Meta' : 'Control';
|
||||
|
||||
const {
|
||||
selectionKeyPressed,
|
||||
multiselectionKeyPressed,
|
||||
deleteKeyPressed,
|
||||
panActivationKeyPressed
|
||||
panActivationKeyPressed,
|
||||
zoomActivationKeyPressed
|
||||
} = useStore();
|
||||
|
||||
function isKeyObject(key?: KeyDefinition): key is KeyDefinitionObject {
|
||||
function isKeyObject(key?: KeyDefinition | null): key is KeyDefinitionObject {
|
||||
return typeof key === 'object';
|
||||
}
|
||||
|
||||
$: selectionKeyString = typeof selectionKey === 'string' ? selectionKey : selectionKey!.key;
|
||||
$: selectionKeyModifier = isKeyObject(selectionKey) ? selectionKey?.modifier : [];
|
||||
function getModifier(key?: KeyDefinition | null): ShortcutModifierDefinition {
|
||||
return isKeyObject(key) ? key.modifier || [] : [];
|
||||
}
|
||||
|
||||
$: multiSelectionKeyString =
|
||||
typeof multiSelectionKey === 'string' ? multiSelectionKey : multiSelectionKey!.key;
|
||||
$: multiSelectionKeyModifier = isKeyObject(multiSelectionKey) ? multiSelectionKey?.modifier : [];
|
||||
function getKeyString(key?: KeyDefinition | null): string {
|
||||
if (key === null || key === undefined) {
|
||||
// this is a workaround to check if a key is set
|
||||
// if not we won't call the callback
|
||||
return '';
|
||||
}
|
||||
|
||||
$: deleteKeyString = typeof deleteKey === 'string' ? deleteKey : deleteKey!.key;
|
||||
$: deleteKeyModifier = isKeyObject(deleteKey) ? deleteKey?.modifier : [];
|
||||
return isKeyObject(key) ? key.key : key;
|
||||
}
|
||||
|
||||
$: panActivationKeyString =
|
||||
typeof panActivationKey === 'string' ? panActivationKey : panActivationKey!.key;
|
||||
$: panActivationKeyModifier = isKeyObject(panActivationKey) ? panActivationKey?.modifier : [];
|
||||
$: selectionKeyDefinition = {
|
||||
key: getKeyString(selectionKey),
|
||||
modifier: getModifier(selectionKey)
|
||||
};
|
||||
|
||||
$: multiSelectionKeyDefinition = {
|
||||
key: getKeyString(multiSelectionKey),
|
||||
modifier: getModifier(multiSelectionKey)
|
||||
};
|
||||
|
||||
$: deleteKeyDefinition = {
|
||||
key: getKeyString(deleteKey),
|
||||
modifier: getModifier(deleteKey)
|
||||
};
|
||||
|
||||
$: panActivationKeyDefinition = {
|
||||
key: getKeyString(panActivationKey),
|
||||
modifier: getModifier(panActivationKey)
|
||||
};
|
||||
|
||||
$: zoomActivationKeyDefinition = {
|
||||
key: getKeyString(zoomActivationKey),
|
||||
modifier: getModifier(zoomActivationKey)
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
key: selectionKeyString,
|
||||
modifier: selectionKeyModifier,
|
||||
callback: () => selectionKeyPressed.set(true)
|
||||
...selectionKeyDefinition,
|
||||
callback: () => selectionKeyDefinition.key && selectionKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -53,9 +79,8 @@
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
key: selectionKeyString,
|
||||
modifier: selectionKeyModifier,
|
||||
callback: () => selectionKeyPressed.set(false)
|
||||
...selectionKeyDefinition,
|
||||
callback: () => selectionKeyDefinition.key && selectionKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -63,9 +88,8 @@
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
key: multiSelectionKeyString,
|
||||
modifier: multiSelectionKeyModifier,
|
||||
callback: () => multiselectionKeyPressed.set(true)
|
||||
...multiSelectionKeyDefinition,
|
||||
callback: () => multiSelectionKeyDefinition.key && multiselectionKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -73,9 +97,8 @@
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
key: multiSelectionKeyString,
|
||||
modifier: multiSelectionKeyModifier,
|
||||
callback: () => multiselectionKeyPressed.set(false)
|
||||
...multiSelectionKeyDefinition,
|
||||
callback: () => multiSelectionKeyDefinition.key && multiselectionKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -83,11 +106,8 @@
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
key: deleteKeyString,
|
||||
modifier: deleteKeyModifier,
|
||||
callback: () => {
|
||||
deleteKeyPressed.set(true);
|
||||
}
|
||||
...deleteKeyDefinition,
|
||||
callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -95,9 +115,8 @@
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
key: deleteKeyString,
|
||||
modifier: deleteKeyModifier,
|
||||
callback: () => deleteKeyPressed.set(false)
|
||||
...deleteKeyDefinition,
|
||||
callback: () => deleteKeyDefinition.key && deleteKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
@@ -105,9 +124,8 @@
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
key: panActivationKeyString,
|
||||
modifier: panActivationKeyModifier,
|
||||
callback: () => panActivationKeyPressed.set(true)
|
||||
...panActivationKeyDefinition,
|
||||
callback: () => panActivationKeyDefinition.key && panActivationKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
@@ -115,9 +133,26 @@
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
key: panActivationKeyString,
|
||||
modifier: panActivationKeyModifier,
|
||||
callback: () => panActivationKeyPressed.set(false)
|
||||
...panActivationKeyDefinition,
|
||||
callback: () => panActivationKeyDefinition.key && panActivationKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
...zoomActivationKeyDefinition,
|
||||
callback: () => zoomActivationKeyDefinition.key && zoomActivationKeyPressed.set(true)
|
||||
}
|
||||
],
|
||||
type: 'keydown'
|
||||
}}
|
||||
use:shortcut={{
|
||||
trigger: [
|
||||
{
|
||||
...zoomActivationKeyDefinition,
|
||||
callback: () => zoomActivationKeyDefinition.key && zoomActivationKeyPressed.set(false)
|
||||
}
|
||||
],
|
||||
type: 'keyup'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { KeyDefinition } from '$lib/types';
|
||||
|
||||
export type KeyHandlerProps = {
|
||||
selectionKey?: KeyDefinition;
|
||||
multiSelectionKey?: KeyDefinition;
|
||||
deleteKey?: KeyDefinition;
|
||||
panActivationKey?: KeyDefinition;
|
||||
selectionKey?: KeyDefinition | null;
|
||||
multiSelectionKey?: KeyDefinition | null;
|
||||
deleteKey?: KeyDefinition | null;
|
||||
panActivationKey?: KeyDefinition | null;
|
||||
zoomActivationKey?: KeyDefinition | null;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { getNodesBounds } from '@xyflow/system';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import { Selection } from '$lib/components/Selection';
|
||||
import drag from '$lib/actions/drag';
|
||||
import type { Node } from '$lib/types';
|
||||
|
||||
const store = useStore();
|
||||
const { selectionRectMode, nodes } = store;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
selectioncontextmenu: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
selectionclick: { nodes: Node[]; event: MouseEvent | TouchEvent };
|
||||
}>();
|
||||
|
||||
$: selectedNodes = $nodes.filter((n) => n.selected);
|
||||
$: bounds = getNodesBounds(selectedNodes);
|
||||
|
||||
function onContextMenu(event: MouseEvent | TouchEvent) {
|
||||
dispatch('selectioncontextmenu', { nodes: selectedNodes, event });
|
||||
}
|
||||
|
||||
function onClick(event: MouseEvent | TouchEvent) {
|
||||
dispatch('selectionclick', { nodes: selectedNodes, event });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if selectedNodes && $selectionRectMode === 'nodes'}
|
||||
@@ -17,6 +32,8 @@
|
||||
class="selection-wrapper nopan"
|
||||
style="width: {bounds.width}px; height: {bounds.height}px; transform: translate({bounds.x}px, {bounds.y}px)"
|
||||
use:drag={{ disabled: false, store }}
|
||||
on:contextmenu={onContextMenu}
|
||||
on:click={onClick}
|
||||
>
|
||||
<Selection width="100%" height="100%" x={0} y={0} />
|
||||
</div>
|
||||
|
||||
@@ -27,11 +27,15 @@
|
||||
export let style: NodeWrapperProps['style'] = undefined;
|
||||
export let type: NodeWrapperProps['type'] = 'default';
|
||||
export let isParent: NodeWrapperProps['isParent'] = false;
|
||||
export let positionAbsolute: NodeWrapperProps['positionAbsolute'] = undefined;
|
||||
export let positionOrigin: NodeWrapperProps['positionOrigin'] = undefined;
|
||||
export let positionX: NodeWrapperProps['positionX'];
|
||||
export let positionY: NodeWrapperProps['positionY'];
|
||||
export let positionOriginX: NodeWrapperProps['positionOriginX'];
|
||||
export let positionOriginY: NodeWrapperProps['positionOriginY'];
|
||||
export let sourcePosition: NodeWrapperProps['sourcePosition'] = undefined;
|
||||
export let targetPosition: NodeWrapperProps['targetPosition'] = undefined;
|
||||
export let zIndex: NodeWrapperProps['zIndex'];
|
||||
export let width: NodeWrapperProps['width'] = undefined;
|
||||
export let height: NodeWrapperProps['height'] = undefined;
|
||||
export let dragHandle: NodeWrapperProps['dragHandle'] = undefined;
|
||||
export let initialized: NodeWrapperProps['initialized'] = false;
|
||||
let className: string = '';
|
||||
@@ -87,13 +91,18 @@
|
||||
|
||||
if (doUpdate) {
|
||||
requestAnimationFrame(() =>
|
||||
updateNodeDimensions([
|
||||
{
|
||||
id,
|
||||
nodeElement: nodeRef,
|
||||
forceUpdate: true
|
||||
}
|
||||
])
|
||||
updateNodeDimensions(
|
||||
new Map([
|
||||
[
|
||||
id,
|
||||
{
|
||||
id,
|
||||
nodeElement: nodeRef,
|
||||
forceUpdate: true
|
||||
}
|
||||
]
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,8 +133,6 @@
|
||||
|
||||
dispatch('nodeclick', { node, event });
|
||||
}
|
||||
|
||||
// @todo: add selectable state
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@@ -161,11 +168,11 @@
|
||||
class:nopan={draggable}
|
||||
class:parent={isParent}
|
||||
style:z-index={zIndex}
|
||||
style:transform="translate({positionOrigin?.x ?? 0}px, {positionOrigin?.y ?? 0}px)"
|
||||
style:transform="translate({positionOriginX}px, {positionOriginY}px)"
|
||||
style:visibility={initialized ? 'visible' : 'hidden'}
|
||||
style="{style} {node.size?.width ? `;width=${node.size?.width}px` : ''} {node.size?.height
|
||||
? `;height=${node.size?.height}px;`
|
||||
: ''}"
|
||||
style="{style ?? ''}; {!width ? '' : `width:${width}px;`} {!height
|
||||
? ''
|
||||
: `height:${height}px;`}"
|
||||
on:click={onSelectNodeHandler}
|
||||
on:mouseenter={(event) => dispatch('nodemouseenter', { node, event })}
|
||||
on:mouseleave={(event) => dispatch('nodemouseleave', { node, event })}
|
||||
@@ -184,8 +191,9 @@
|
||||
{dragging}
|
||||
{dragHandle}
|
||||
isConnectable={connectable}
|
||||
xPos={positionAbsolute?.x ?? 0}
|
||||
yPos={positionAbsolute?.y ?? 0}
|
||||
positionAbsolute={{ x: positionX, y: positionY }}
|
||||
{width}
|
||||
{height}
|
||||
on:connectstart
|
||||
on:connect
|
||||
on:connectend
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { XYPosition } from '@xyflow/system';
|
||||
import type { Node } from '$lib/types';
|
||||
|
||||
export type NodeWrapperProps = Pick<
|
||||
@@ -9,19 +8,22 @@ export type NodeWrapperProps = Pick<
|
||||
| 'data'
|
||||
| 'draggable'
|
||||
| 'dragging'
|
||||
| 'positionAbsolute'
|
||||
| 'selected'
|
||||
| 'selectable'
|
||||
| 'style'
|
||||
| 'type'
|
||||
| 'width'
|
||||
| 'height'
|
||||
| 'sourcePosition'
|
||||
| 'targetPosition'
|
||||
| 'dragHandle'
|
||||
| 'hidden'
|
||||
> & {
|
||||
positionOrigin?: XYPosition;
|
||||
width?: number;
|
||||
height?: number;
|
||||
type: string;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
positionOriginX: number;
|
||||
positionOriginY: number;
|
||||
'on:nodeclick'?: (event: MouseEvent) => void;
|
||||
resizeObserver?: ResizeObserver | null;
|
||||
isParent?: boolean;
|
||||
|
||||
@@ -11,24 +11,29 @@
|
||||
|
||||
// unused props - we need to list them here in order to prevent warnings
|
||||
export let id: $$Props['id'] = '';
|
||||
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 xPos: $$Props['xPos'] = 0;
|
||||
export let yPos: $$Props['yPos'] = 0;
|
||||
export let positionAbsolute: $$Props['positionAbsolute'] = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
id;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
xPos;
|
||||
yPos;
|
||||
positionAbsolute;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
// unused props - we need to list them here in order to prevent warnings
|
||||
export let id: $$Props['id'] = '';
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let data: $$Props['data'] = {};
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let sourcePosition: $$Props['sourcePosition'] = undefined;
|
||||
@@ -13,12 +15,16 @@
|
||||
export let zIndex: $$Props['zIndex'] = undefined;
|
||||
export let dragging: $$Props['dragging'] = false;
|
||||
export let dragHandle: $$Props['dragHandle'] = undefined;
|
||||
export let xPos: $$Props['xPos'] = 0;
|
||||
export let yPos: $$Props['yPos'] = 0;
|
||||
export let positionAbsolute: $$Props['positionAbsolute'] = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
id;
|
||||
width;
|
||||
height;
|
||||
data;
|
||||
selected;
|
||||
sourcePosition;
|
||||
@@ -27,7 +33,6 @@
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
xPos;
|
||||
yPos;
|
||||
positionAbsolute;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
@@ -10,26 +10,31 @@
|
||||
|
||||
// unused props - we need to list them here in order to prevent warnings
|
||||
export let id: $$Props['id'] = '';
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let targetPosition: $$Props['targetPosition'] = 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 xPos: $$Props['xPos'] = 0;
|
||||
export let yPos: $$Props['yPos'] = 0;
|
||||
export let positionAbsolute: $$Props['positionAbsolute'] = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
id;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
targetPosition;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
xPos;
|
||||
yPos;
|
||||
positionAbsolute;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,26 +10,28 @@
|
||||
|
||||
// unused props - we need to list them here in order to prevent warnings
|
||||
export let id: $$Props['id'] = '';
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let selected: $$Props['selected'] = undefined;
|
||||
export let sourcePosition: $$Props['sourcePosition'] = 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 xPos: $$Props['xPos'] = 0;
|
||||
export let yPos: $$Props['yPos'] = 0;
|
||||
export let positionAbsolute: $$Props['positionAbsolute'] = { x: 0, y: 0 };
|
||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
||||
|
||||
// @todo: there must be a better way to do this
|
||||
id;
|
||||
width;
|
||||
height;
|
||||
selected;
|
||||
sourcePosition;
|
||||
type;
|
||||
zIndex;
|
||||
dragging;
|
||||
dragHandle;
|
||||
xPos;
|
||||
yPos;
|
||||
positionAbsolute;
|
||||
isConnectable;
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,11 +17,18 @@
|
||||
typeof ResizeObserver === 'undefined'
|
||||
? null
|
||||
: new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
const updates = entries.map((entry: ResizeObserverEntry) => ({
|
||||
id: entry.target.getAttribute('data-id') as string,
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
forceUpdate: true
|
||||
}));
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -33,10 +40,10 @@
|
||||
<div class="svelte-flow__nodes">
|
||||
{#each $visibleNodes as node (node.id)}
|
||||
{@const posOrigin = getPositionWithOrigin({
|
||||
x: node.positionAbsolute?.x ?? 0,
|
||||
y: node.positionAbsolute?.y ?? 0,
|
||||
width: (node.size?.width || node.width) ?? 0,
|
||||
height: (node.size?.height || node.height) ?? 0,
|
||||
x: node.computed?.positionAbsolute?.x ?? 0,
|
||||
y: node.computed?.positionAbsolute?.y ?? 0,
|
||||
width: node.computed?.width ?? node.width ?? 0,
|
||||
height: node.computed?.height ?? node.height ?? 0,
|
||||
origin: node.origin
|
||||
})}
|
||||
<NodeWrapper
|
||||
@@ -54,18 +61,23 @@
|
||||
node.connectable ||
|
||||
($nodesConnectable && typeof node.connectable === 'undefined')
|
||||
)}
|
||||
positionAbsolute={node.positionAbsolute}
|
||||
positionOrigin={posOrigin}
|
||||
positionX={node.computed?.positionAbsolute?.x ?? 0}
|
||||
positionY={node.computed?.positionAbsolute?.y ?? 0}
|
||||
positionOriginX={posOrigin.x ?? 0}
|
||||
positionOriginY={posOrigin.y ?? 0}
|
||||
isParent={!!node[internalsSymbol]?.isParent}
|
||||
style={node.style}
|
||||
class={node.class}
|
||||
type={node.type}
|
||||
type={node.type ?? 'default'}
|
||||
sourcePosition={node.sourcePosition}
|
||||
targetPosition={node.targetPosition}
|
||||
dragging={node.dragging}
|
||||
zIndex={node[internalsSymbol]?.z ?? 0}
|
||||
dragHandle={node.dragHandle}
|
||||
initialized={(!!node.width && !!node.height) || (!!node.size?.width && !!node.size?.height)}
|
||||
width={node.width ?? undefined}
|
||||
height={node.height ?? undefined}
|
||||
initialized={(!!node.computed?.width && !!node.computed?.height) ||
|
||||
(!!node.width && !!node.height)}
|
||||
{resizeObserver}
|
||||
on:nodeclick
|
||||
on:nodemouseenter
|
||||
|
||||
@@ -15,8 +15,14 @@
|
||||
import { Attribution } from '$lib/components/Attribution';
|
||||
import { key, useStore, createStoreContext } from '$lib/store';
|
||||
import type { SvelteFlowProps } from './types';
|
||||
import { updateStore, updateStoreByKeys, type UpdatableStoreProps } from './utils';
|
||||
import {
|
||||
updateStore,
|
||||
updateStoreByKeys,
|
||||
type UpdatableStoreProps,
|
||||
getColorModeClass
|
||||
} from './utils';
|
||||
import { get } from 'svelte/store';
|
||||
import { useColorModeClass } from '$lib/hooks/useColorModeClass';
|
||||
|
||||
type $$Props = SvelteFlowProps;
|
||||
|
||||
@@ -34,6 +40,8 @@
|
||||
export let selectionKey: $$Props['selectionKey'] = undefined;
|
||||
export let selectionMode: $$Props['selectionMode'] = undefined;
|
||||
export let panActivationKey: $$Props['panActivationKey'] = undefined;
|
||||
export let multiSelectionKey: $$Props['multiSelectionKey'] = undefined;
|
||||
export let zoomActivationKey: $$Props['zoomActivationKey'] = undefined;
|
||||
export let nodesDraggable: $$Props['nodesDraggable'] = undefined;
|
||||
export let nodesConnectable: $$Props['nodesConnectable'] = undefined;
|
||||
export let nodeDragThreshold: $$Props['nodeDragThreshold'] = undefined;
|
||||
@@ -61,12 +69,14 @@
|
||||
export let selectionOnDrag: $$Props['selectionOnDrag'] = undefined;
|
||||
export let autoPanOnConnect: $$Props['autoPanOnConnect'] = true;
|
||||
export let autoPanOnNodeDrag: $$Props['autoPanOnNodeDrag'] = true;
|
||||
export let onError: $$Props['onError'] = undefined;
|
||||
export let onerror: $$Props['onerror'] = undefined;
|
||||
export let ondelete: $$Props['ondelete'] = undefined;
|
||||
export let attributionPosition: $$Props['attributionPosition'] = undefined;
|
||||
export let proOptions: $$Props['proOptions'] = undefined;
|
||||
export let defaultEdgeOptions: $$Props['defaultEdgeOptions'] = undefined;
|
||||
export let width: $$Props['width'] = undefined;
|
||||
export let height: $$Props['height'] = undefined;
|
||||
export let colorMode: $$Props['colorMode'] = 'light';
|
||||
|
||||
export let defaultMarkerColor = '#b1b1b7';
|
||||
|
||||
@@ -137,7 +147,8 @@
|
||||
isValidConnection,
|
||||
autoPanOnConnect,
|
||||
autoPanOnNodeDrag,
|
||||
onError,
|
||||
onerror,
|
||||
ondelete,
|
||||
connectionMode,
|
||||
nodeDragThreshold
|
||||
};
|
||||
@@ -152,6 +163,8 @@
|
||||
maxZoom,
|
||||
translateExtent
|
||||
});
|
||||
|
||||
$: colorModeClass = useColorModeClass(colorMode);
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -159,14 +172,20 @@
|
||||
bind:clientWidth
|
||||
bind:clientHeight
|
||||
{style}
|
||||
class={cc(['svelte-flow', className])}
|
||||
class={cc(['svelte-flow', className, $colorModeClass])}
|
||||
data-testid="svelte-flow__wrapper"
|
||||
on:dragover
|
||||
on:drop
|
||||
{...$$restProps}
|
||||
role="application"
|
||||
>
|
||||
<KeyHandler {selectionKey} {deleteKey} {panActivationKey} />
|
||||
<KeyHandler
|
||||
{selectionKey}
|
||||
{deleteKey}
|
||||
{panActivationKey}
|
||||
{multiSelectionKey}
|
||||
{zoomActivationKey}
|
||||
/>
|
||||
<Zoom
|
||||
{initialViewport}
|
||||
{onMoveStart}
|
||||
@@ -204,7 +223,7 @@
|
||||
on:nodedragstop
|
||||
on:nodecontextmenu
|
||||
/>
|
||||
<NodeSelection />
|
||||
<NodeSelection on:selectionclick on:selectioncontextmenu />
|
||||
</ViewportComponent>
|
||||
<UserSelection />
|
||||
</Pane>
|
||||
|
||||
@@ -14,7 +14,8 @@ import type {
|
||||
OnError,
|
||||
ConnectionMode,
|
||||
PanelPosition,
|
||||
ProOptions
|
||||
ProOptions,
|
||||
ColorMode
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -24,7 +25,8 @@ import type {
|
||||
KeyDefinition,
|
||||
EdgeTypes,
|
||||
DefaultEdgeOptions,
|
||||
FitViewOptions
|
||||
FitViewOptions,
|
||||
OnDelete
|
||||
} from '$lib/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
@@ -34,9 +36,11 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
||||
edges: Writable<Edge[]>;
|
||||
nodeTypes?: NodeTypes;
|
||||
edgeTypes?: EdgeTypes;
|
||||
selectionKey?: KeyDefinition;
|
||||
panActivationKey?: KeyDefinition;
|
||||
deleteKey?: KeyDefinition;
|
||||
selectionKey?: KeyDefinition | null;
|
||||
panActivationKey?: KeyDefinition | null;
|
||||
deleteKey?: KeyDefinition | null;
|
||||
multiSelectionKey?: KeyDefinition | null;
|
||||
zoomActivationKey?: KeyDefinition | null;
|
||||
fitView?: boolean;
|
||||
fitViewOptions?: FitViewOptions;
|
||||
nodeOrigin?: NodeOrigin;
|
||||
@@ -72,6 +76,7 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
||||
defaultEdgeOptions?: DefaultEdgeOptions;
|
||||
width?: number;
|
||||
height?: number;
|
||||
colorMode?: ColorMode;
|
||||
|
||||
class?: string;
|
||||
style?: string;
|
||||
@@ -82,5 +87,6 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
||||
onMoveStart?: OnMoveStart;
|
||||
onMove?: OnMove;
|
||||
onMoveEnd?: OnMoveEnd;
|
||||
onError?: OnError;
|
||||
onerror?: OnError;
|
||||
ondelete?: OnDelete;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SvelteFlowStore } from '$lib/store/types';
|
||||
import type { EdgeTypes, NodeTypes } from '$lib/types';
|
||||
import type { CoordinateExtent } from '@xyflow/system';
|
||||
import type { ColorMode, CoordinateExtent } from '@xyflow/system';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
// this is helper function for updating the store
|
||||
@@ -62,7 +62,8 @@ export type UpdatableStoreProps = {
|
||||
autoPanOnConnect?: UnwrapWritable<SvelteFlowStore['autoPanOnConnect']>;
|
||||
autoPanOnNodeDrag?: UnwrapWritable<SvelteFlowStore['autoPanOnNodeDrag']>;
|
||||
connectionMode?: UnwrapWritable<SvelteFlowStore['connectionMode']>;
|
||||
onError?: UnwrapWritable<SvelteFlowStore['onError']>;
|
||||
onerror?: UnwrapWritable<SvelteFlowStore['onerror']>;
|
||||
ondelete?: UnwrapWritable<SvelteFlowStore['ondelete']>;
|
||||
nodeDragThreshold?: UnwrapWritable<SvelteFlowStore['nodeDragThreshold']>;
|
||||
};
|
||||
|
||||
@@ -76,3 +77,15 @@ export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStorePr
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getColorModeClass(colorMode?: ColorMode) {
|
||||
if (colorMode !== 'system') {
|
||||
return colorMode;
|
||||
}
|
||||
|
||||
if (!colorMode || typeof window === 'undefined' || !window.matchMedia) {
|
||||
return 'light';
|
||||
}
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
dragging,
|
||||
translateExtent,
|
||||
lib,
|
||||
panActivationKeyPressed
|
||||
panActivationKeyPressed,
|
||||
zoomActivationKeyPressed
|
||||
} = useStore();
|
||||
|
||||
$: viewPort = initialViewport || { x: 0, y: 0, zoom: 1 };
|
||||
@@ -55,7 +56,7 @@
|
||||
panOnDrag: _panOnDrag,
|
||||
panOnScrollSpeed: 0.5,
|
||||
panOnScrollMode: panOnScrollMode || PanOnScrollMode.Free,
|
||||
zoomActivationKeyPressed: false,
|
||||
zoomActivationKeyPressed: $zoomActivationKeyPressed,
|
||||
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
|
||||
noPanClassName: 'nopan',
|
||||
noWheelClassName: 'nowheel',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ColorMode, ColorModeClass } from '@xyflow/system';
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
|
||||
function getMediaQuery() {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)');
|
||||
}
|
||||
|
||||
export function useColorModeClass(colorMode: ColorMode = 'light'): Readable<ColorModeClass> {
|
||||
const colorModeClass = readable<ColorModeClass>('light', (set) => {
|
||||
if (colorMode !== 'system') {
|
||||
set(colorMode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = getMediaQuery();
|
||||
const updateColorModeClass = () => set(mediaQuery?.matches ? 'dark' : 'light');
|
||||
|
||||
set(mediaQuery?.matches ? 'dark' : 'light');
|
||||
mediaQuery?.addEventListener('change', updateColorModeClass);
|
||||
|
||||
return () => {
|
||||
mediaQuery?.removeEventListener('change', updateColorModeClass);
|
||||
};
|
||||
});
|
||||
|
||||
return colorModeClass;
|
||||
}
|
||||
@@ -141,7 +141,7 @@ export function useSvelteFlow(): {
|
||||
}
|
||||
|
||||
return (nodesToIntersect || get(nodes)).filter((n) => {
|
||||
if (!isRect && (n.id === node.id || !n.positionAbsolute)) {
|
||||
if (!isRect && (n.id === node.id || !n.computed?.positionAbsolute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { get } from 'svelte/store';
|
||||
import type { UpdateNodeInternals, NodeDimensionUpdate } from '@xyflow/system';
|
||||
import type { UpdateNodeInternals } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
@@ -9,17 +9,17 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
// @todo: do we want to add this to system?
|
||||
const updateInternals = (id: string | string[]) => {
|
||||
const updateIds = Array.isArray(id) ? id : [id];
|
||||
const updates = updateIds.reduce<NodeDimensionUpdate[]>((res, updateId) => {
|
||||
const updates = new Map();
|
||||
|
||||
updateIds.forEach((updateId) => {
|
||||
const nodeElement = get(domNode)?.querySelector(
|
||||
`.svelte-flow__node[data-id="${updateId}"]`
|
||||
) as HTMLDivElement;
|
||||
|
||||
if (nodeElement) {
|
||||
res.push({ id: updateId, nodeElement, forceUpdate: true });
|
||||
updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true });
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => updateNodeDimensions(updates));
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from '$lib/components/Handle';
|
||||
export * from '$lib/plugins/Controls';
|
||||
export * from '$lib/plugins/Background';
|
||||
export * from '$lib/plugins/Minimap';
|
||||
export * from '$lib/plugins/NodeToolbar';
|
||||
|
||||
// store
|
||||
export { useStore } from '$lib/store';
|
||||
@@ -74,7 +75,9 @@ export {
|
||||
type Rect,
|
||||
type Box,
|
||||
type Transform,
|
||||
type CoordinateExtent
|
||||
type CoordinateExtent,
|
||||
type ColorMode,
|
||||
type ColorModeClass
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
export let position: $$Props['position'] = 'bottom-right';
|
||||
export let ariaLabel: $$Props['ariaLabel'] = 'Mini map';
|
||||
export let nodeStrokeColor: $$Props['nodeStrokeColor'] = 'transparent';
|
||||
export let nodeColor: $$Props['nodeColor'] = '#e2e2e2';
|
||||
export let nodeColor: $$Props['nodeColor'] = undefined;
|
||||
export let nodeClass: $$Props['nodeClass'] = '';
|
||||
export let nodeBorderRadius: $$Props['nodeBorderRadius'] = 5;
|
||||
export let nodeStrokeWidth: $$Props['nodeStrokeWidth'] = 2;
|
||||
@@ -51,7 +51,7 @@
|
||||
translateExtent
|
||||
} = useStore();
|
||||
|
||||
const nodeColorFunc = getAttrFunction(nodeColor);
|
||||
const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor);
|
||||
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
||||
const nodeClassFunc = getAttrFunction(nodeClass);
|
||||
const shapeRendering =
|
||||
@@ -114,15 +114,15 @@
|
||||
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
|
||||
|
||||
{#each $nodes as node (node.id)}
|
||||
{#if node.width && node.height}
|
||||
{#if (node.computed?.width || node?.width) && (node.computed?.height || node.height)}
|
||||
{@const pos = getNodePositionWithOrigin(node).positionAbsolute}
|
||||
<MinimapNode
|
||||
x={pos.x}
|
||||
y={pos.y}
|
||||
width={node.width}
|
||||
height={node.height}
|
||||
width={node.computed?.width ?? node.width ?? 0}
|
||||
height={node.computed?.height ?? node.height ?? 0}
|
||||
selected={node.selected}
|
||||
color={nodeColorFunc(node)}
|
||||
color={nodeColorFunc?.(node)}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
@@ -142,27 +142,3 @@
|
||||
</svg>
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
background-color: var(
|
||||
--minimap-background-color-props,
|
||||
var(--minimap-background-color, var(--minimap-background-color-default))
|
||||
);
|
||||
}
|
||||
|
||||
.svelte-flow__minimap-mask {
|
||||
fill: var(
|
||||
--minimap-mask-color-props,
|
||||
var(--minimap-mask-color, var(--minimap-mask-color-default))
|
||||
);
|
||||
stroke: var(
|
||||
--minimap-mask-stroke-color-props,
|
||||
var(--minimap-mask-stroke-color, var(--minimap-mask-stroke-color-default))
|
||||
);
|
||||
stroke-width: var(
|
||||
--minimap-mask-stroke-width-props,
|
||||
var(--minimap-mask-stroke-width, var(--minimap-mask-stroke-width-default))
|
||||
);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
export let width: number = 0;
|
||||
export let height: number = 0;
|
||||
export let borderRadius: number = 5;
|
||||
export let color: string;
|
||||
export let color: string | undefined = undefined;
|
||||
export let shapeRendering: string;
|
||||
export let strokeColor: string;
|
||||
export let strokeColor: string | undefined = undefined;
|
||||
export let strokeWidth: number = 2;
|
||||
export let selected: boolean = false;
|
||||
let className: string = '';
|
||||
@@ -24,8 +24,8 @@
|
||||
ry={borderRadius}
|
||||
{width}
|
||||
{height}
|
||||
fill={color}
|
||||
stroke={strokeColor}
|
||||
stroke-width={strokeWidth}
|
||||
style={`${color ? `fill: ${color};` : ''}${strokeColor ? `stroke: ${strokeColor};` : ''}${
|
||||
strokeWidth ? `stroke-width: ${strokeWidth};` : ''
|
||||
}`}
|
||||
shape-rendering={shapeRendering}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import {
|
||||
getNodesBounds,
|
||||
Position,
|
||||
type Rect,
|
||||
internalsSymbol,
|
||||
getNodeToolbarTransform
|
||||
} from '@xyflow/system';
|
||||
import portal from '$lib/actions/portal';
|
||||
import type { Node } from '$lib/types';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
import type { NodeToolbarProps } from './types';
|
||||
|
||||
type $$Props = NodeToolbarProps;
|
||||
|
||||
export let nodeId: $$Props['nodeId'] = undefined;
|
||||
export let position: $$Props['position'] = undefined;
|
||||
export let align: $$Props['align'] = undefined;
|
||||
export let offset: $$Props['offset'] = undefined;
|
||||
export let isVisible: $$Props['isVisible'] = undefined;
|
||||
|
||||
const { domNode, viewport, nodeLookup, nodes, nodeOrigin } = useStore();
|
||||
const contextNodeId = getContext<string>('svelteflow__node_id');
|
||||
|
||||
let transform: string;
|
||||
let toolbarNodes: Node[] = [];
|
||||
let _offset = offset !== undefined ? offset : 10;
|
||||
let _position = position !== undefined ? position : Position.Top;
|
||||
let _align = align !== undefined ? align : 'center';
|
||||
|
||||
$: {
|
||||
// $nodes only needed to trigger updates, $nodeLookup is just a helper that does not trigger any updates
|
||||
if ($nodes) {
|
||||
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId];
|
||||
|
||||
toolbarNodes = nodeIds.reduce<Node[]>((res, nodeId) => {
|
||||
const node = $nodeLookup.get(nodeId);
|
||||
|
||||
if (node) {
|
||||
res.push(node);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
let nodeRect: Rect | undefined = undefined;
|
||||
|
||||
if (toolbarNodes.length === 1) {
|
||||
const toolbarNode = toolbarNodes[0];
|
||||
nodeRect = {
|
||||
...toolbarNode.position,
|
||||
width: toolbarNode.computed?.width ?? toolbarNode.width ?? 0,
|
||||
height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0
|
||||
};
|
||||
} else if (toolbarNodes.length > 1) {
|
||||
nodeRect = getNodesBounds(toolbarNodes, $nodeOrigin);
|
||||
}
|
||||
|
||||
if (nodeRect) {
|
||||
transform = getNodeToolbarTransform(nodeRect, $viewport, _position, _offset, _align);
|
||||
}
|
||||
}
|
||||
|
||||
$: zIndex =
|
||||
toolbarNodes.length === 0
|
||||
? 1
|
||||
: Math.max(...toolbarNodes.map((node) => (node[internalsSymbol]?.z || 5) + 1));
|
||||
|
||||
//FIXME: Possible performance bottleneck
|
||||
$: selectedNodesCount = $nodes.filter((node) => node.selected).length;
|
||||
|
||||
// if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected
|
||||
$: isActive =
|
||||
typeof isVisible === 'boolean'
|
||||
? isVisible
|
||||
: toolbarNodes.length === 1 && toolbarNodes[0].selected && selectedNodesCount === 1;
|
||||
</script>
|
||||
|
||||
{#if $domNode && isActive && toolbarNodes}
|
||||
<div
|
||||
data-id={toolbarNodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
|
||||
class="svelte-flow__node-toolbar"
|
||||
use:portal={{ domNode: $domNode }}
|
||||
style:position="absolute"
|
||||
style:transform
|
||||
style:z-index={zIndex}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as NodeToolbar } from './NodeToolbar.svelte';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Position, Align } from '@xyflow/system';
|
||||
|
||||
export type NodeToolbarProps = {
|
||||
nodeId?: string | string[];
|
||||
position?: Position;
|
||||
align?: Align;
|
||||
offset?: number;
|
||||
isVisible?: boolean;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user