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