Merge branch 'next' into feat/changesets

This commit is contained in:
moklick
2024-07-01 11:54:05 +02:00
106 changed files with 9369 additions and 7461 deletions
+38 -42
View File
@@ -4,47 +4,57 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thank you for reporting an issue :pray:. Hello! :sparkles: You found the place to submit a bug or issue that you discovered in the xyflow library, thank you for helping us out!
**Please only file issues for v11. Will will no longer fix issues for v10.** Having trouble making something work in xyflow, but its not a bug with the library itself? Youll get the fastest response by asking in [github discussions](https://github.com/xyflow/xyflow/discussions) or our [discord server](https://discord.gg/RVmnytFmGW).
This issue tracker is for reporting bugs found in react-flow (https://github.com/wbkd/react-flow) **Search our [discussions](https://github.com/xyflow/xyflow/discussions?discussions_q=) and [issues](https://github.com/xyflow/xyflow/issues?q=)** to see if your problem has already been reported and you can add more context, or see if someone else has already solved your issue. And of course, check our [docs](https://reactflow.dev/learn) if you havent already.
If you have a question about how to achieve something and are struggling, please post a question
inside of react-flow's Discussion's tab: https://github.com/wbkd/react-flow/discussions
If it's an issue about the docs please go to the react-flow-docs (https://github.com/wbkd/react-flow-docs)
Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already: **Issue with the xyflow Docs?** Submit an issue in our [web repo](https://github.com/xyflow/web/issues) instead.
- react-flow's Open Issue's tab: https://github.com/wbkd/react-flow/issues?q=is%3Aissue+sort%3Aupdated-desc+position
- react-flow's Closed Issues tab: https://github.com/wbkd/react-flow/issues?q=is%3Aissue+sort%3Aupdated-desc+position+is%3Aclosed
- react-flow's Discussion's tab: https://github.com/wbkd/react-flow/discussions
The more information you fill in, the better the community can help you. With that out of the way, lets get started :bug:
- type: textarea - type: textarea
id: description id: platform
attributes: attributes:
label: Describe the Bug label: What platform were you using when you found the bug?
description: Provide a clear and concise description of the challenge you are running into. description: We only address issues for the most recent xyflow major release or upcoming releases.
value: |
- React Flow / Svelte Flow version:
- Browser and version:
- OS and version:
validations: validations:
required: true required: true
- type: input - type: input
id: link id: link
attributes: attributes:
label: Your Example Website or App label: Live code example
description: | description: |
Which website or app were you using when the bug happened? This is the most important part of the bug report!
Note:
- Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the `react-flow-renderer` npm package. React Flow codesandbox starters
- To create a shareable code example you can use Codesandbox (https://codesandbox.io/s/react-flow-starter-ou8or) or Stackblitz (https://stackblitz.com/). - js: https://new.reactflow.dev/js
- Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. - ts: https://new.reactflow.dev/ts
placeholder: |
e.g. https://stackblitz.com/edit/...... OR Github Repo Svelte Flow stackblitz starters
- js: https://new.svelteflow.dev/js
- ts: https://new.svelteflow.dev/ts
Avoid dependencies in your example for easier debugging.
placeholder: https://stackblitz.com/edit/... or Github Repo
validations: validations:
required: false required: false
- type: textarea
id: description
attributes:
label: Describe the Bug
description: What exactly is happening when you encounter the bug?
validations:
required: true
- type: textarea - type: textarea
id: steps id: steps
attributes: attributes:
label: Steps to Reproduce the Bug or Issue label: Steps to reproduce the bug or issue
description: Describe the steps we have to take to reproduce the behavior. description: How can we see the reported behavior in the code example you provided? The more detail, the better!
placeholder: | placeholder: |
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
@@ -56,9 +66,8 @@ body:
id: expected id: expected
attributes: attributes:
label: Expected behavior label: Expected behavior
description: Provide a clear and concise description of what you expected to happen. description: What would you expect to happen if there was no bug or issue?
placeholder: | placeholder: As a user, I expected ___ behavior, but instead I am seeing ___
As a user, I expected ___ behavior but i am seeing ___
validations: validations:
required: true required: true
- type: textarea - type: textarea
@@ -67,22 +76,9 @@ body:
label: Screenshots or Videos label: Screenshots or Videos
description: | description: |
If applicable, add screenshots or a video to help explain your problem. If applicable, add screenshots or a video to help explain your problem.
For more information on the supported file image/file types and the file size limits, please refer Supported image/file types and file size limits- https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files placeholder: Drag your video or image files inside of this editor
placeholder: |
You can drag your video or image files inside of this editor ↓
- type: textarea
id: platform
attributes:
label: Platform
value: |
- OS: [e.g. macOS, Windows, Linux]
- Browser: [e.g. Chrome, Safari, Firefox]
- Version: [e.g. 91.1]
validations:
required: true
- type: textarea - type: textarea
id: additional id: additional
attributes: attributes:
label: Additional context label: Additional context
description: Add any other context about the problem here.
+4 -4
View File
@@ -34,7 +34,7 @@ import Subflow from '../examples/Subflow';
import SwitchFlow from '../examples/Switch'; import SwitchFlow from '../examples/Switch';
import TouchDevice from '../examples/TouchDevice'; import TouchDevice from '../examples/TouchDevice';
import Undirectional from '../examples/Undirectional'; import Undirectional from '../examples/Undirectional';
import UpdatableEdge from '../examples/UpdatableEdge'; import ReconnectEdge from '../examples/ReconnectEdge';
import UpdateNode from '../examples/UpdateNode'; import UpdateNode from '../examples/UpdateNode';
import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals'; import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals';
import UseReactFlow from '../examples/UseReactFlow'; import UseReactFlow from '../examples/UseReactFlow';
@@ -271,9 +271,9 @@ const routes: IRoute[] = [
component: Undirectional, component: Undirectional,
}, },
{ {
name: 'Updatable Edge', name: 'Reconnect Edge',
path: 'updatable-edge', path: 'reconnect-edge',
component: UpdatableEdge, component: ReconnectEdge,
}, },
{ {
name: 'Update Node', name: 'Update Node',
@@ -25,6 +25,7 @@ const defaultNodes: Node[] = [
}, },
{ {
id: '3', id: '3',
type: 'output',
data: { label: 'Node 3' }, data: { label: 'Node 3' },
position: { x: 400, y: 100 }, position: { x: 400, y: 100 },
className: 'light', className: 'light',
@@ -77,6 +77,7 @@ const DnDFlow = () => {
onInit={onInit} onInit={onInit}
onDrop={onDrop} onDrop={onDrop}
onDragOver={onDragOver} onDragOver={onDragOver}
nodeOrigin={nodeOrigin}
> >
<Controls /> <Controls />
</ReactFlow> </ReactFlow>
@@ -28,10 +28,15 @@ for (let i = 0; i < 100; i++) {
}); });
} }
const initEdges: Edge[] = []; const initEdges: Edge[] = initNodes.reduce<Edge[]>((res, node, index) => {
if (index > 0) {
res.push({ id: `${index - 1}-${index}`, source: (index - 1).toString(), target: index.toString() });
}
return res;
}, []);
const CustomNodeFlow = () => { const CustomNodeFlow = () => {
const { setNodes, updateNodeData } = useReactFlow(); const { setNodes, updateNodeData, updateEdge } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState(initNodes); const [nodes, , onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
@@ -55,6 +60,10 @@ const CustomNodeFlow = () => {
nodes.forEach((node) => updateNodeData(node.id, { label: 'node update' })); nodes.forEach((node) => updateNodeData(node.id, { label: 'node update' }));
}; };
const multiUpdateEdges = () => {
edges.forEach((edge) => updateEdge(edge.id, { label: 'edge update' }));
};
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -70,6 +79,7 @@ const CustomNodeFlow = () => {
<Panel> <Panel>
<button onClick={multiSetNodes}>set nodes</button> <button onClick={multiSetNodes}>set nodes</button>
<button onClick={multiUpdateNodes}>update nodes</button> <button onClick={multiUpdateNodes}>update nodes</button>
<button onClick={multiUpdateEdges}>update edges</button>
</Panel> </Panel>
</ReactFlow> </ReactFlow>
); );
@@ -37,6 +37,7 @@ const initialNodes: Node[] = [
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'default resizer' }, data: { label: 'default resizer' },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
origin: [1, 1],
style: { ...nodeStyle }, style: { ...nodeStyle },
}, },
{ {
@@ -127,7 +128,7 @@ const initialNodes: Node[] = [
data: { label: 'Parent', keepAspectRatio: true }, data: { label: 'Parent', keepAspectRatio: true },
position: { x: 700, y: 0 }, position: { x: 700, y: 0 },
width: 300, width: 300,
height: 400, height: 300,
style: { ...nodeStyle }, style: { ...nodeStyle },
}, },
{ {
@@ -147,7 +148,9 @@ const initialNodes: Node[] = [
id: '5b', id: '5b',
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'Child with expandParent' }, data: { label: 'Child with expandParent' },
position: { x: 150, y: 100 }, position: { x: 100, y: 100 },
width: 100,
height: 100,
parentId: '5', parentId: '5',
expandParent: true, expandParent: true,
style: { ...nodeStyle }, style: { ...nodeStyle },
@@ -155,8 +158,10 @@ const initialNodes: Node[] = [
{ {
id: '5c', id: '5c',
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true }, data: { label: 'Child with expandParent & keepAspectRatio' },
position: { x: 25, y: 200 }, position: { x: 250, y: 200 },
height: 100,
width: 100,
parentId: '5', parentId: '5',
expandParent: true, expandParent: true,
style: { ...nodeStyle }, style: { ...nodeStyle },
@@ -28,6 +28,14 @@ const initialNodes: Node[] = [
data: { label: 'A Node' }, data: { label: 'A Node' },
position: { x: 250, y: 0 }, position: { x: 250, y: 0 },
}, },
{
id: '3',
type: 'b',
sourcePosition: Position.Right,
targetPosition: Position.Left,
data: { label: 'B Node' },
position: { x: 350, y: 0 },
},
]; ];
const buttonStyle: CSSProperties = { const buttonStyle: CSSProperties = {
@@ -78,6 +86,7 @@ const NodeTypeChangeFlow = () => {
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
nodeTypes={nodeTypesObjects[nodeTypesId]} nodeTypes={nodeTypesObjects[nodeTypesId]}
fitView
> >
<button onClick={changeType} style={buttonStyle}> <button onClick={changeType} style={buttonStyle}>
change type change type
@@ -2,7 +2,7 @@ import { useState, useCallback, MouseEvent as ReactMouseEvent } from 'react';
import { import {
ReactFlow, ReactFlow,
Controls, Controls,
updateEdge, reconnectEdge,
addEdge, addEdge,
applyNodeChanges, applyNodeChanges,
applyEdgeChanges, applyEdgeChanges,
@@ -91,21 +91,21 @@ const initialNodes: Node[] = [
]; ];
const initialEdges: Edge[] = [ const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', updatable: 'source' }, { id: 'e1-3', source: '1', target: '3', label: 'This edge can only be updated from source', reconnectable: 'source' },
{ id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', updatable: 'target' }, { id: 'e2-4', source: '2', target: '4', label: 'This edge can only be updated from target', reconnectable: 'target' },
{ id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' }, { id: 'e5-6', source: '5', target: '6', label: 'This edge can be updated from both sides' },
]; ];
const onEdgeUpdateStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) => const onReconnectStart = (_: ReactMouseEvent, edge: Edge, handleType: HandleType) =>
console.log(`start update ${handleType} handle`, edge); console.log(`start update ${handleType} handle`, edge);
const onEdgeUpdateEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => const onReconnectEnd = (_: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) =>
console.log(`end update ${handleType} handle`, edge); console.log(`end update ${handleType} handle`, edge);
const UpdatableEdge = () => { const ReconnectEdge = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes); const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => const onReconnect = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els)); const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
const onNodesChange = useCallback((changes: NodeChange[]) => { const onNodesChange = useCallback((changes: NodeChange[]) => {
@@ -123,10 +123,10 @@ const UpdatableEdge = () => {
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
snapToGrid={true} snapToGrid={true}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onConnect={onConnect} onConnect={onConnect}
onEdgeUpdateStart={onEdgeUpdateStart} onReconnectStart={onReconnectStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onReconnectEnd={onReconnectEnd}
fitView fitView
> >
<Controls /> <Controls />
@@ -134,4 +134,4 @@ const UpdatableEdge = () => {
); );
}; };
export default UpdatableEdge; export default ReconnectEdge;
@@ -1,6 +1,6 @@
import React, { memo, FC, CSSProperties } from 'react'; import React, { memo, FC, CSSProperties } from 'react';
import { Handle, NodeProps, Position } from '@xyflow/react'; import { Handle, NodeProps, Position, useInternalNode } from '@xyflow/react';
const infoStyle: CSSProperties = { fontSize: 11 }; const infoStyle: CSSProperties = { fontSize: 11 };
const idStyle: CSSProperties = { const idStyle: CSSProperties = {
@@ -12,6 +12,8 @@ const idStyle: CSSProperties = {
}; };
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => { const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
const node = useInternalNode(id)!;
return ( return (
<> <>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
@@ -19,6 +21,9 @@ const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY
<div style={infoStyle}> <div style={infoStyle}>
x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex} x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
</div> </div>
<div style={infoStyle}>
x:{Math.round(node.position.x)} y:{Math.round(node.position.y)}
</div>
<Handle type="source" position={Position.Bottom} /> <Handle type="source" position={Position.Bottom} />
</> </>
); );
+11 -2
View File
@@ -14,6 +14,8 @@ import {
Background, Background,
Panel, Panel,
NodeOrigin, NodeOrigin,
useUpdateNodeInternals,
ReactFlowProvider,
} from '@xyflow/react'; } from '@xyflow/react';
import DebugNode from './DebugNode'; import DebugNode from './DebugNode';
@@ -104,7 +106,7 @@ const initialNodes: Node[] = [
{ {
id: '5b', id: '5b',
data: { label: 'Node 5b' }, data: { label: 'Node 5b' },
position: { x: 225, y: 50 }, position: { x: 200, y: 200 },
className: 'light', className: 'light',
parentId: '5', parentId: '5',
expandParent: true, expandParent: true,
@@ -151,6 +153,7 @@ const nodeTypes = {
const Subflow = () => { const Subflow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null); const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const updateNodeInternals = useUpdateNodeInternals();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -216,6 +219,7 @@ const Subflow = () => {
onlyRenderVisibleElements={false} onlyRenderVisibleElements={false}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
fitView fitView
nodeOrigin={[0, 0]}
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
@@ -228,9 +232,14 @@ const Subflow = () => {
<button onClick={toggleChildNodes}>toggleChildNodes</button> <button onClick={toggleChildNodes}>toggleChildNodes</button>
<button onClick={logToObject}>toObject</button> <button onClick={logToObject}>toObject</button>
<button onClick={() => setNodes(initialNodes)}>setNodes</button> <button onClick={() => setNodes(initialNodes)}>setNodes</button>
<button onClick={() => updateNodeInternals(nodes.map((node) => node.id))}>updateNodeInternals</button>
</Panel> </Panel>
</ReactFlow> </ReactFlow>
); );
}; };
export default Subflow; export default () => (
<ReactFlowProvider>
<Subflow />
</ReactFlowProvider>
);
@@ -10,7 +10,7 @@ import {
Edge, Edge,
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
updateEdge, reconnectEdge,
useNodesState, useNodesState,
useEdgesState, useEdgesState,
} from '@xyflow/react'; } from '@xyflow/react';
@@ -22,55 +22,55 @@ const initialNodes: Node[] = [
id: '00', id: '00',
type: 'custom', type: 'custom',
position: { x: 300, y: 250 }, position: { x: 300, y: 250 },
data: null, data: {},
}, },
{ {
id: '01', id: '01',
type: 'custom', type: 'custom',
position: { x: 100, y: 50 }, position: { x: 100, y: 50 },
data: null, data: {},
}, },
{ {
id: '02', id: '02',
type: 'custom', type: 'custom',
position: { x: 500, y: 50 }, position: { x: 500, y: 50 },
data: null, data: {},
}, },
{ {
id: '03', id: '03',
type: 'custom', type: 'custom',
position: { x: 500, y: 500 }, position: { x: 500, y: 500 },
data: null, data: {},
}, },
{ {
id: '04', id: '04',
type: 'custom', type: 'custom',
position: { x: 100, y: 500 }, position: { x: 100, y: 500 },
data: null, data: {},
}, },
{ {
id: '10', id: '10',
type: 'custom', type: 'custom',
position: { x: 300, y: 5 }, position: { x: 300, y: 5 },
data: null, data: {},
}, },
{ {
id: '20', id: '20',
type: 'custom', type: 'custom',
position: { x: 600, y: 250 }, position: { x: 600, y: 250 },
data: null, data: {},
}, },
{ {
id: '30', id: '30',
type: 'custom', type: 'custom',
position: { x: 300, y: 600 }, position: { x: 300, y: 600 },
data: null, data: {},
}, },
{ {
id: '40', id: '40',
type: 'custom', type: 'custom',
position: { x: 5, y: 250 }, position: { x: 5, y: 250 },
data: null, data: {},
}, },
]; ];
@@ -186,8 +186,8 @@ const UpdateNodeInternalsFlow = () => {
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]); const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
const onEdgeUpdate = useCallback( const onReconnect = useCallback(
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)), (oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
[] []
); );
@@ -215,7 +215,7 @@ const UpdateNodeInternalsFlow = () => {
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
connectionLineType={ConnectionLineType.Bezier} connectionLineType={ConnectionLineType.Bezier}
connectionMode={ConnectionMode.Loose} connectionMode={ConnectionMode.Loose}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
/> />
); );
}; };
@@ -1,9 +1,12 @@
import { useKeyPress } from '@xyflow/react'; import { useKeyPress } from '@xyflow/react';
import { useEffect } from 'react';
const UseKeyPressComponent = () => { const UseKeyPressComponent = () => {
const metaPressed = useKeyPress(['Meta']); const metaPressed = useKeyPress(['Meta']);
console.log({ metaPressed }); useEffect(() => {
console.log({ metaPressed });
}, [metaPressed]);
return <div />; return <div />;
}; };
@@ -13,7 +13,7 @@ import {
OnConnectStart, OnConnectStart,
OnConnectEnd, OnConnectEnd,
OnConnect, OnConnect,
updateEdge, reconnectEdge,
Edge, Edge,
IsValidConnection, IsValidConnection,
OnBeforeDelete, OnBeforeDelete,
@@ -24,10 +24,10 @@ import ConnectionStatus from './ConnectionStatus';
import styles from './validation.module.css'; import styles from './validation.module.css';
const initialNodes: Node[] = [ const initialNodes: Node[] = [
{ id: '0', type: 'custominput', position: { x: 0, y: 150 }, data: null }, { id: '0', type: 'custominput', position: { x: 0, y: 150 }, data: {} },
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 }, data: null }, { id: 'A', type: 'customnode', position: { x: 250, y: 0 }, data: {} },
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 }, data: null }, { id: 'B', type: 'customnode', position: { x: 250, y: 150 }, data: {} },
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 }, data: null }, { id: 'C', type: 'customnode', position: { x: 250, y: 300 }, data: {} },
]; ];
const isValidConnection: IsValidConnection = (connection) => connection.target === 'B'; const isValidConnection: IsValidConnection = (connection) => connection.target === 'B';
@@ -41,7 +41,7 @@ const CustomInput: FC<NodeProps> = () => (
const CustomNode: FC<NodeProps> = ({ id }) => ( const CustomNode: FC<NodeProps> = ({ id }) => (
<> <>
<Handle type="target" position={Position.Left} isConnectableStart={false} /> <Handle type="target" position={Position.Top} isConnectableStart={false} />
<div>{id}</div> <div>{id}</div>
<Handle type="source" position={Position.Right} /> <Handle type="source" position={Position.Right} />
</> </>
@@ -81,8 +81,8 @@ const ValidationFlow = () => {
[value] [value]
); );
const onEdgeUpdate = useCallback( const onReconnect = useCallback(
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)), (oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),
[setEdges] [setEdges]
); );
@@ -102,7 +102,7 @@ const ValidationFlow = () => {
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onConnectStart={onConnectStart} onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd} onConnectEnd={onConnectEnd}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
isValidConnection={isValidConnection} isValidConnection={isValidConnection}
onBeforeDelete={onBeforeDelete} onBeforeDelete={onBeforeDelete}
fitView fitView
@@ -149,7 +149,7 @@ export default {
// source: '9', // source: '9',
// target: '11', // target: '11',
// label: 'focusable', // label: 'focusable',
// updatable: true // reconnectable: true
// }, // },
// { // {
// id: 'not-focusable', // id: 'not-focusable',
@@ -146,7 +146,7 @@ export default {
// source: '9', // source: '9',
// target: '11', // target: '11',
// label: 'focusable', // label: 'focusable',
// updatable: true // reconnectable: true
// }, // },
// { // {
// id: 'not-focusable', // id: 'not-focusable',
@@ -1,9 +1,24 @@
<script lang="ts"> <script lang="ts">
import { useConnection } from '@xyflow/svelte'; import { getBezierPath, useConnection } from '@xyflow/svelte';
const connection = useConnection(); const connection = useConnection();
let path: string | null = null;
$: if ($connection.inProgress) {
const { from, to, fromPosition, toPosition } = $connection;
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition
};
[path] = getBezierPath(pathParams);
}
</script> </script>
{#if $connection.path} {#if $connection.inProgress}
<path d={$connection.path} fill="none" stroke={$connection.startHandle?.handleId} /> <path d={path} fill="none" stroke={$connection.fromHandle.id} />
{/if} {/if}
@@ -24,8 +24,8 @@
data: { label: 'only connectable with B' }, data: { label: 'only connectable with B' },
...nodeDefaults ...nodeDefaults
}, },
{ id: 'A', position: { x: 250, y: 0 }, data: { label: 'A' }, ...nodeDefaults }, { id: 'A', position: { x: 250, y: 0 }, data: { label: 'A' } },
{ id: 'B', position: { x: 250, y: 150 }, data: { label: 'B' }, ...nodeDefaults }, { id: 'B', position: { x: 250, y: 150 }, data: { label: 'B' } },
{ id: 'C', position: { x: 250, y: 300 }, data: { label: 'C' }, ...nodeDefaults } { id: 'C', position: { x: 250, y: 300 }, data: { label: 'C' }, ...nodeDefaults }
]); ]);
+8 -7
View File
@@ -14,10 +14,10 @@
"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",
"test:react:ui": "pnpm --filter=playwright run test:react:ui", "test:react:ui": "pnpm --filter=playwright run test:react:ui",
"build": "turbo run build", "build:all": "turbo run build",
"test": "turbo run test", "build": "turbo run build --filter=./packages/**",
"lint": "turbo run lint", "lint": "turbo run lint --filter=./packages/**",
"typecheck": "turbo run typecheck", "typecheck": "turbo run typecheck --filter=./packages/**",
"release": "changeset publish", "release": "changeset publish",
"clean": "pnpm -r --parallel exec rimraf dist .turbo node_modules" "clean": "pnpm -r --parallel exec rimraf dist .turbo node_modules"
}, },
@@ -35,8 +35,9 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^3.23.0", "rollup": "^4.18.0",
"turbo": "^1.10.0", "turbo": "^2.0.3",
"typescript": "5.1.3" "typescript": "5.1.3"
} },
"packageManager": "pnpm@9.2.0"
} }
+34
View File
@@ -1,5 +1,39 @@
# @xyflow/react # @xyflow/react
## 12.0.0-next.25
- `useConnection` returns `ConnectionState` with `toNode` and `toHandle`
- add `toNode` and `toHandle` to custom connection line props
- node origin is part of position absolute
- refactor connection handling
## 12.0.0-next.24
- fix `window` bug for SSR for real
## 12.0.0-next.23
- fix `window` bug for SSR
## 12.0.0-next.22
- ⚠️ rename `updateEdge` to `reconnectEdge` and realted APIs [#4373](https://github.com/xyflow/xyflow/pull/4373)
- revise selection usability (capture while dragging out of the flow)
- use correct end handle position when drawing a connection lines
- determine correct end positions for connection lines
## 12.0.0-next.21
- fix node origin bug
## 12.0.0-next.20
- add `updateEdge` and `updateEdgeData` helpers to `useReactFlow`
- enable dynamic edge label updates
- prevent zooming on mobile if zoomOnPinch is false
- add straight edge to path built-in-types
- abort drag when multiple touches are detected
## 12.0.0-next.19 ## 12.0.0-next.19
- update internals on node resizer updates - update internals on node resizer updates
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/react", "name": "@xyflow/react",
"version": "12.0.0-next.19", "version": "12.0.0-next.24",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [ "keywords": [
"react", "react",
@@ -25,12 +25,8 @@ const selector = (s: ReactFlowState) => {
return { return {
viewBB, viewBB,
boundingRect: boundingRect: s.nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup), viewBB) : viewBB,
s.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { nodeOrigin: s.nodeOrigin }), viewBB)
: viewBB,
rfId: s.rfId, rfId: s.rfId,
nodeOrigin: s.nodeOrigin,
panZoom: s.panZoom, panZoom: s.panZoom,
translateExtent: s.translateExtent, translateExtent: s.translateExtent,
flowWidth: s.width, flowWidth: s.width,
@@ -1,7 +1,7 @@
/* 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 { ComponentType, memo } from 'react'; import { ComponentType, memo } from 'react';
import { NodeOrigin, getNodeDimensions, getNodePositionWithOrigin, nodeHasDimensions } from '@xyflow/system'; import { getNodeDimensions, nodeHasDimensions } from '@xyflow/system';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
@@ -11,7 +11,6 @@ import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMa
declare const window: any; declare const window: any;
const selector = (s: ReactFlowState) => s.nodeOrigin;
const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id); const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id);
const getAttrFunction = <NodeType extends Node>(func: any): GetMiniMapNodeAttribute<NodeType> => const getAttrFunction = <NodeType extends Node>(func: any): GetMiniMapNodeAttribute<NodeType> =>
func instanceof Function ? func : () => func; func instanceof Function ? func : () => func;
@@ -28,7 +27,6 @@ function MiniMapNodes<NodeType extends Node>({
onClick, onClick,
}: MiniMapNodesProps<NodeType>) { }: MiniMapNodesProps<NodeType>) {
const nodeIds = useStore(selectorNodeIds, shallow); const nodeIds = useStore(selectorNodeIds, shallow);
const nodeOrigin = useStore(selector);
const nodeColorFunc = getAttrFunction<NodeType>(nodeColor); const nodeColorFunc = getAttrFunction<NodeType>(nodeColor);
const nodeStrokeColorFunc = getAttrFunction<NodeType>(nodeStrokeColor); const nodeStrokeColorFunc = getAttrFunction<NodeType>(nodeStrokeColor);
const nodeClassNameFunc = getAttrFunction<NodeType>(nodeClassName); const nodeClassNameFunc = getAttrFunction<NodeType>(nodeClassName);
@@ -46,7 +44,6 @@ function MiniMapNodes<NodeType extends Node>({
<NodeComponentWrapper<NodeType> <NodeComponentWrapper<NodeType>
key={nodeId} key={nodeId}
id={nodeId} id={nodeId}
nodeOrigin={nodeOrigin}
nodeColorFunc={nodeColorFunc} nodeColorFunc={nodeColorFunc}
nodeStrokeColorFunc={nodeStrokeColorFunc} nodeStrokeColorFunc={nodeStrokeColorFunc}
nodeClassNameFunc={nodeClassNameFunc} nodeClassNameFunc={nodeClassNameFunc}
@@ -63,7 +60,6 @@ function MiniMapNodes<NodeType extends Node>({
function NodeComponentWrapperInner<NodeType extends Node>({ function NodeComponentWrapperInner<NodeType extends Node>({
id, id,
nodeOrigin,
nodeColorFunc, nodeColorFunc,
nodeStrokeColorFunc, nodeStrokeColorFunc,
nodeClassNameFunc, nodeClassNameFunc,
@@ -74,7 +70,6 @@ function NodeComponentWrapperInner<NodeType extends Node>({
onClick, onClick,
}: { }: {
id: string; id: string;
nodeOrigin: NodeOrigin;
nodeColorFunc: GetMiniMapNodeAttribute<NodeType>; nodeColorFunc: GetMiniMapNodeAttribute<NodeType>;
nodeStrokeColorFunc: GetMiniMapNodeAttribute<NodeType>; nodeStrokeColorFunc: GetMiniMapNodeAttribute<NodeType>;
nodeClassNameFunc: GetMiniMapNodeAttribute<NodeType>; nodeClassNameFunc: GetMiniMapNodeAttribute<NodeType>;
@@ -86,7 +81,7 @@ function NodeComponentWrapperInner<NodeType extends Node>({
}) { }) {
const { node, x, y } = useStore((s) => { const { node, x, y } = useStore((s) => {
const node = s.nodeLookup.get(id) as InternalNode<NodeType>; const node = s.nodeLookup.get(id) as InternalNode<NodeType>;
const { x, y } = getNodePositionWithOrigin(node, nodeOrigin).positionAbsolute; const { x, y } = node.internals.positionAbsolute;
return { return {
node, node,
@@ -67,26 +67,30 @@ function ResizeControl({
}, },
onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
const { triggerNodeChanges, nodeLookup, parentLookup, nodeOrigin } = store.getState(); const { triggerNodeChanges, nodeLookup, parentLookup, nodeOrigin } = store.getState();
const changes: NodeChange[] = []; const changes: NodeChange[] = [];
const nextPosition = { x: change.x, y: change.y }; const nextPosition = { x: change.x, y: change.y };
const node = nodeLookup.get(id); const node = nodeLookup.get(id);
if (node && node.expandParent && node.parentId) { if (node && node.expandParent && node.parentId) {
const origin = node.origin ?? nodeOrigin;
const width = change.width ?? node.measured.width!;
const height = change.height ?? node.measured.height!;
const child: ParentExpandChild = { const child: ParentExpandChild = {
id: node.id, id: node.id,
parentId: node.parentId, parentId: node.parentId,
rect: { rect: {
width: change.width ?? node.measured.width!, width,
height: change.height ?? node.measured.height!, height,
...evaluateAbsolutePosition( ...evaluateAbsolutePosition(
{ {
x: change.x ?? node.position.x, x: change.x ?? node.position.x,
y: change.y ?? node.position.y, y: change.y ?? node.position.y,
}, },
{ width, height },
node.parentId, node.parentId,
nodeLookup, nodeLookup,
node.origin ?? nodeOrigin origin
), ),
}, },
}; };
@@ -94,9 +98,10 @@ function ResizeControl({
const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin); const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges); changes.push(...parentExpandChanges);
// when the parent was expanded by the child node, its position will be clamped at 0,0 // when the parent was expanded by the child node, its position will be clamped at
nextPosition.x = change.x ? Math.max(0, change.x) : undefined; // 0,0 when node origin is 0,0 and to width, height if it's 1,1
nextPosition.y = change.y ? Math.max(0, change.y) : undefined; nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined;
nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined;
} }
if (nextPosition.x !== undefined && nextPosition.y !== undefined) { if (nextPosition.x !== undefined && nextPosition.y !== undefined) {
@@ -1,7 +1,7 @@
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 { Rect, Position, getNodeToolbarTransform, getNodesBounds } from '@xyflow/system'; import { Position, getNodeToolbarTransform, getInternalNodesBounds, NodeLookup } from '@xyflow/system';
import { InternalNode, ReactFlowState } from '../../types'; import { InternalNode, ReactFlowState } from '../../types';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
@@ -17,21 +17,24 @@ const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) =>
a?.selected !== b?.selected || a?.selected !== b?.selected ||
a?.internals.z !== b?.internals.z; a?.internals.z !== b?.internals.z;
const nodesEqualityFn = (a: InternalNode[], b: InternalNode[]) => { const nodesEqualityFn = (a: NodeLookup, b: NodeLookup) => {
if (a.length !== b.length) { if (a.size !== b.size) {
return false; return false;
} }
return !a.some((node, i) => nodeEqualityFn(node, b[i])); for (const [key, node] of a) {
if (nodeEqualityFn(node, b.get(key))) {
return false;
}
}
return true;
}; };
const storeSelector = (state: ReactFlowState) => ({ const storeSelector = (state: ReactFlowState) => ({
viewport: { x: state.transform[0],
x: state.transform[0], y: state.transform[1],
y: state.transform[1], zoom: state.transform[2],
zoom: state.transform[2],
},
nodeOrigin: state.nodeOrigin,
selectedNodesCount: state.nodes.filter((node) => node.selected).length, selectedNodesCount: state.nodes.filter((node) => node.selected).length,
}); });
@@ -49,36 +52,41 @@ export function NodeToolbar({
const contextNodeId = useNodeId(); const contextNodeId = useNodeId();
const nodesSelector = useCallback( const nodesSelector = useCallback(
(state: ReactFlowState): InternalNode[] => { (state: ReactFlowState): NodeLookup => {
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || '']; const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
const internalNodes = nodeIds.reduce<NodeLookup>((res, id) => {
return nodeIds.reduce<InternalNode[]>((acc, id) => {
const node = state.nodeLookup.get(id); const node = state.nodeLookup.get(id);
if (node) { if (node) {
acc.push(node); res.set(node.id, node);
} }
return acc;
}, []); return res;
}, new Map());
return internalNodes;
}, },
[nodeId, contextNodeId] [nodeId, contextNodeId]
); );
const nodes = useStore(nodesSelector, nodesEqualityFn); const nodes = useStore(nodesSelector, nodesEqualityFn);
const { viewport, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow); const { x, y, zoom, 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 // 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.size === 1 && nodes.values().next().value.selected && selectedNodesCount === 1;
if (!isActive || !nodes.length) { if (!isActive || !nodes.size) {
return null; return null;
} }
const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin }); const nodeRect = getInternalNodesBounds(nodes);
const zIndex: number = Math.max(...nodes.map((node) => node.internals.z + 1)); const nodesArray = Array.from(nodes.values());
const zIndex = Math.max(...nodesArray.map((node) => node.internals.z + 1));
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
position: 'absolute', position: 'absolute',
transform: getNodeToolbarTransform(nodeRect, viewport, position, offset, align), transform: getNodeToolbarTransform(nodeRect, { x, y, zoom }, position, offset, align),
zIndex, zIndex,
...style, ...style,
}; };
@@ -89,7 +97,8 @@ export function NodeToolbar({
style={wrapperStyle} style={wrapperStyle}
className={cc(['react-flow__node-toolbar', className])} className={cc(['react-flow__node-toolbar', className])}
{...rest} {...rest}
data-id={nodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()} // @todo: check if we could only do this for non-prod envs
data-id={nodesArray.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
> >
{children} {children}
</div> </div>
@@ -1,129 +1,18 @@
import { CSSProperties, useCallback } from 'react'; import { CSSProperties } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { import {
Position,
ConnectionLineType, ConnectionLineType,
ConnectionMode,
getBezierPath, getBezierPath,
getSmoothStepPath, getSmoothStepPath,
type ConnectionStatus, getConnectionStatus,
type HandleType, getStraightPath,
} from '@xyflow/system'; } from '@xyflow/system';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import type { ConnectionLineComponent, ReactFlowState, ReactFlowStore } from '../../types'; import type { ConnectionLineComponent, ReactFlowState } from '../../types';
import { useConnection } from '../../hooks/useConnection';
type ConnectionLineProps = {
nodeId: string;
handleType: HandleType;
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
connectionStatus: ConnectionStatus | null;
};
const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top,
};
const ConnectionLine = ({
nodeId,
handleType,
style,
type = ConnectionLineType.Bezier,
CustomComponent,
connectionStatus,
}: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode } = useStore(
useCallback(
(s: ReactFlowStore) => ({
fromNode: s.nodeLookup.get(nodeId),
handleId: s.connectionStartHandle?.handleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode,
}),
[nodeId]
),
shallow
);
const fromHandleBounds = fromNode?.internals.handleBounds;
let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) {
handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source'];
}
if (!fromNode || !handleBounds) {
return null;
}
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0;
const fromX = fromNode.internals.positionAbsolute.x + fromHandleX;
const fromY = fromNode.internals.positionAbsolute.y + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
if (!fromPosition || !toPosition) {
return null;
}
if (CustomComponent) {
return (
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={fromX}
fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
connectionStatus={connectionStatus}
/>
);
}
let dAttr = '';
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetPosition: toPosition,
};
if (type === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[dAttr] = getBezierPath(pathParams);
} else if (type === ConnectionLineType.Step) {
[dAttr] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
} else if (type === ConnectionLineType.SmoothStep) {
[dAttr] = getSmoothStepPath(pathParams);
} else if (type === ConnectionLineType.SimpleBezier) {
[dAttr] = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`;
}
return <path d={dAttr} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';
type ConnectionLineWrapperProps = { type ConnectionLineWrapperProps = {
type: ConnectionLineType; type: ConnectionLineType;
@@ -133,19 +22,18 @@ type ConnectionLineWrapperProps = {
}; };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
nodeId: s.connectionStartHandle?.nodeId,
handleType: s.connectionStartHandle?.type,
nodesConnectable: s.nodesConnectable, nodesConnectable: s.nodesConnectable,
connectionStatus: s.connectionStatus, isValid: s.connection.isValid,
inProgress: s.connection.inProgress,
width: s.width, width: s.width,
height: s.height, height: s.height,
}); });
export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) { export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
const { nodeId, handleType, nodesConnectable, width, height, connectionStatus } = useStore(selector, shallow); const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
const isValid = !!(nodeId && handleType && width && nodesConnectable); const renderConnection = !!(width && nodesConnectable && inProgress);
if (!isValid) { if (!renderConnection) {
return null; return null;
} }
@@ -156,16 +44,79 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
height={height} height={height}
className="react-flow__connectionline react-flow__container" className="react-flow__connectionline react-flow__container"
> >
<g className={cc(['react-flow__connection', connectionStatus])}> <g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
<ConnectionLine <ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} />
nodeId={nodeId}
handleType={handleType}
style={style}
type={type}
CustomComponent={component}
connectionStatus={connectionStatus}
/>
</g> </g>
</svg> </svg>
); );
} }
type ConnectionLineProps = {
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
isValid: boolean | null;
};
const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, isValid }: ConnectionLineProps) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } = useConnection();
if (!inProgress) {
return;
}
if (CustomComponent) {
return (
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={from.x}
fromY={from.y}
toX={to.x}
toY={to.y}
fromPosition={fromPosition}
toPosition={toPosition}
connectionStatus={getConnectionStatus(isValid)}
toNode={toNode}
toHandle={toHandle}
/>
);
}
let path = '';
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition,
};
switch (type) {
case ConnectionLineType.Bezier:
[path] = getBezierPath(pathParams);
break;
case ConnectionLineType.SimpleBezier:
[path] = getSimpleBezierPath(pathParams);
break;
case ConnectionLineType.Step:
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
break;
case ConnectionLineType.SmoothStep:
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
}
return <path d={path} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';
@@ -1,4 +1,4 @@
// Updatable edges have a anchors around their handles to update the edge. // Reconnectable edges have a anchors around their handles to reconnect the edge.
import { XYHandle, type Connection, EdgePosition } from '@xyflow/system'; import { XYHandle, type Connection, EdgePosition } from '@xyflow/system';
import { EdgeAnchor } from '../Edges/EdgeAnchor'; import { EdgeAnchor } from '../Edges/EdgeAnchor';
@@ -7,20 +7,20 @@ import { useStoreApi } from '../../hooks/useStore';
type EdgeUpdateAnchorsProps<EdgeType extends Edge = Edge> = { type EdgeUpdateAnchorsProps<EdgeType extends Edge = Edge> = {
edge: EdgeType; edge: EdgeType;
isUpdatable: boolean | 'source' | 'target'; isReconnectable: boolean | 'source' | 'target';
edgeUpdaterRadius: EdgeWrapperProps['edgeUpdaterRadius']; reconnectRadius: EdgeWrapperProps['reconnectRadius'];
sourceHandleId: Edge['sourceHandle']; sourceHandleId: Edge['sourceHandle'];
targetHandleId: Edge['targetHandle']; targetHandleId: Edge['targetHandle'];
onEdgeUpdate: EdgeWrapperProps<EdgeType>['onEdgeUpdate']; onReconnect: EdgeWrapperProps<EdgeType>['onReconnect'];
onEdgeUpdateStart: EdgeWrapperProps<EdgeType>['onEdgeUpdateStart']; onReconnectStart: EdgeWrapperProps<EdgeType>['onReconnectStart'];
onEdgeUpdateEnd: EdgeWrapperProps<EdgeType>['onEdgeUpdateEnd']; onReconnectEnd: EdgeWrapperProps<EdgeType>['onReconnectEnd'];
setUpdateHover: (hover: boolean) => void; setUpdateHover: (hover: boolean) => void;
setUpdating: (updating: boolean) => void; setReconnecting: (updating: boolean) => void;
} & EdgePosition; } & EdgePosition;
export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
isUpdatable, isReconnectable,
edgeUpdaterRadius, reconnectRadius,
edge, edge,
targetHandleId, targetHandleId,
sourceHandleId, sourceHandleId,
@@ -30,10 +30,10 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
targetY, targetY,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
onEdgeUpdate, onReconnect,
onEdgeUpdateStart, onReconnectStart,
onEdgeUpdateEnd, onReconnectEnd,
setUpdating, setReconnecting,
setUpdateHover, setUpdateHover,
}: EdgeUpdateAnchorsProps<EdgeType>) { }: EdgeUpdateAnchorsProps<EdgeType>) {
const store = useStoreApi(); const store = useStoreApi();
@@ -65,15 +65,15 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
const isTarget = isSourceHandle; const isTarget = isSourceHandle;
setUpdating(true); setReconnecting(true);
onEdgeUpdateStart?.(event, edge, handleType); onReconnectStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => { const _onReconnectEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false); setReconnecting(false);
onEdgeUpdateEnd?.(evt, edge, handleType); onReconnectEnd?.(evt, edge, handleType);
}; };
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection); const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
XYHandle.onPointerDown(event.nativeEvent, { XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect, autoPanOnConnect,
@@ -93,43 +93,43 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
onConnect: onConnectEdge, onConnect: onConnectEdge,
onConnectStart, onConnectStart,
onConnectEnd, onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd, onReconnectEnd: _onReconnectEnd,
updateConnection, updateConnection,
getTransform: () => store.getState().transform, getTransform: () => store.getState().transform,
getConnectionStartHandle: () => store.getState().connectionStartHandle, getFromHandle: () => store.getState().connection.fromHandle,
}); });
}; };
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => const onReconnectSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true); handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => const onReconnectTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false); handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdateHover(true); const onReconnectMouseEnter = () => setUpdateHover(true);
const onEdgeUpdaterMouseOut = () => setUpdateHover(false); const onReconnectMouseOut = () => setUpdateHover(false);
return ( return (
<> <>
{(isUpdatable === 'source' || isUpdatable === true) && ( {(isReconnectable === 'source' || isReconnectable === true) && (
<EdgeAnchor <EdgeAnchor
position={sourcePosition} position={sourcePosition}
centerX={sourceX} centerX={sourceX}
centerY={sourceY} centerY={sourceY}
radius={edgeUpdaterRadius} radius={reconnectRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown} onMouseDown={onReconnectSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter} onMouseEnter={onReconnectMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut} onMouseOut={onReconnectMouseOut}
type="source" type="source"
/> />
)} )}
{(isUpdatable === 'target' || isUpdatable === true) && ( {(isReconnectable === 'target' || isReconnectable === true) && (
<EdgeAnchor <EdgeAnchor
position={targetPosition} position={targetPosition}
centerX={targetX} centerX={targetX}
centerY={targetY} centerY={targetY}
radius={edgeUpdaterRadius} radius={reconnectRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown} onMouseDown={onReconnectTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter} onMouseEnter={onReconnectMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut} onMouseOut={onReconnectMouseOut}
type="target" type="target"
/> />
)} )}
@@ -18,7 +18,7 @@ import type { Edge, EdgeWrapperProps } from '../../types';
export function EdgeWrapper<EdgeType extends Edge = Edge>({ export function EdgeWrapper<EdgeType extends Edge = Edge>({
id, id,
edgesFocusable, edgesFocusable,
edgesUpdatable, edgesReconnectable,
elementsSelectable, elementsSelectable,
onClick, onClick,
onDoubleClick, onDoubleClick,
@@ -26,10 +26,10 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
onMouseEnter, onMouseEnter,
onMouseMove, onMouseMove,
onMouseLeave, onMouseLeave,
edgeUpdaterRadius, reconnectRadius,
onEdgeUpdate, onReconnect,
onEdgeUpdateStart, onReconnectStart,
onEdgeUpdateEnd, onReconnectEnd,
rfId, rfId,
edgeTypes, edgeTypes,
noPanClassName, noPanClassName,
@@ -50,14 +50,14 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
} }
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isUpdatable = const isReconnectable =
typeof onEdgeUpdate !== 'undefined' && typeof onReconnect !== 'undefined' &&
(edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined')); (edge.reconnectable || (edgesReconnectable && typeof edge.reconnectable === 'undefined'));
const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined')); const isSelectable = !!(edge.selectable || (elementsSelectable && typeof edge.selectable === 'undefined'));
const edgeRef = useRef<SVGGElement>(null); const edgeRef = useRef<SVGGElement>(null);
const [updateHover, setUpdateHover] = useState<boolean>(false); const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false); const [reconnecting, setReconnecting] = useState<boolean>(false);
const store = useStoreApi(); const store = useStoreApi();
const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore( const { zIndex, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = useStore(
@@ -207,7 +207,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined} aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef} ref={edgeRef}
> >
{!updating && ( {!reconnecting && (
<EdgeComponent <EdgeComponent
id={id} id={id}
source={edge.source} source={edge.source}
@@ -239,14 +239,14 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
interactionWidth={edge.interactionWidth} interactionWidth={edge.interactionWidth}
/> />
)} )}
{isUpdatable && ( {isReconnectable && (
<EdgeUpdateAnchors<EdgeType> <EdgeUpdateAnchors<EdgeType>
edge={edge} edge={edge}
isUpdatable={isUpdatable} isReconnectable={isReconnectable}
edgeUpdaterRadius={edgeUpdaterRadius} reconnectRadius={reconnectRadius}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onEdgeUpdateStart={onEdgeUpdateStart} onReconnectStart={onReconnectStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onReconnectEnd={onReconnectEnd}
sourceX={sourceX} sourceX={sourceX}
sourceY={sourceY} sourceY={sourceY}
targetX={targetX} targetX={targetX}
@@ -254,7 +254,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
sourcePosition={sourcePosition} sourcePosition={sourcePosition}
targetPosition={targetPosition} targetPosition={targetPosition}
setUpdateHover={setUpdateHover} setUpdateHover={setUpdateHover}
setUpdating={setUpdating} setReconnecting={setReconnecting}
sourceHandleId={edge.sourceHandle} sourceHandleId={edge.sourceHandle}
targetHandleId={edge.targetHandle} targetHandleId={edge.targetHandle}
/> />
@@ -1,4 +1,4 @@
import { memo, useState, useCallback } from 'react'; import { memo, useState, useEffect, useRef } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import type { Rect } from '@xyflow/system'; import type { Rect } from '@xyflow/system';
@@ -19,19 +19,20 @@ function EdgeTextComponent({
}: EdgeTextProps) { }: EdgeTextProps) {
const [edgeTextBbox, setEdgeTextBbox] = useState<Rect>({ x: 1, y: 0, width: 0, height: 0 }); const [edgeTextBbox, setEdgeTextBbox] = useState<Rect>({ x: 1, y: 0, width: 0, height: 0 });
const edgeTextClasses = cc(['react-flow__edge-textwrapper', className]); const edgeTextClasses = cc(['react-flow__edge-textwrapper', className]);
const edgeTextRef = useRef<SVGTextElement | null>(null);
const onEdgeTextRefChange = useCallback((edgeRef: SVGTextElement) => { useEffect(() => {
if (edgeRef === null) return; if (edgeTextRef.current) {
const textBbox = edgeTextRef.current.getBBox();
const textBbox = edgeRef.getBBox(); setEdgeTextBbox({
x: textBbox.x,
setEdgeTextBbox({ y: textBbox.y,
x: textBbox.x, width: textBbox.width,
y: textBbox.y, height: textBbox.height,
width: textBbox.width, });
height: textBbox.height, }
}); }, [label]);
}, []);
if (typeof label === 'undefined' || !label) { if (typeof label === 'undefined' || !label) {
return null; return null;
@@ -60,7 +61,7 @@ function EdgeTextComponent({
className="react-flow__edge-text" className="react-flow__edge-text"
y={edgeTextBbox.height / 2} y={edgeTextBbox.height / 2}
dy="0.3em" dy="0.3em"
ref={onEdgeTextRefChange} ref={edgeTextRef}
style={labelStyle} style={labelStyle}
> >
{label} {label}
+13 -19
View File
@@ -39,28 +39,22 @@ const selector = (s: ReactFlowState) => ({
const connectingSelector = const connectingSelector =
(nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => { (nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => {
const { const { connectionClickStartHandle: clickHandle, connectionMode, connection } = state;
connectionStartHandle: startHandle,
connectionEndHandle: endHandle,
connectionClickStartHandle: clickHandle,
connectionMode,
connectionStatus,
} = state;
const connectingTo = endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type; const { fromHandle, toHandle, isValid } = connection;
const connectingTo = toHandle?.nodeId === nodeId && toHandle?.id === handleId && toHandle?.type === type;
return { return {
connectingFrom: connectingFrom: fromHandle?.nodeId === nodeId && fromHandle?.id === handleId && fromHandle?.type === type,
startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.type === type,
connectingTo, connectingTo,
clickConnecting: clickConnecting: clickHandle?.nodeId === nodeId && clickHandle?.id === handleId && clickHandle?.type === type,
clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type,
isPossibleEndHandle: isPossibleEndHandle:
connectionMode === ConnectionMode.Strict connectionMode === ConnectionMode.Strict
? startHandle?.type !== type ? fromHandle?.type !== type
: nodeId !== startHandle?.nodeId || handleId !== startHandle?.handleId, : nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id,
connectionInProcess: !!startHandle, connectionInProcess: !!fromHandle,
valid: connectingTo && connectionStatus === 'valid', valid: connectingTo && isValid,
}; };
}; };
@@ -144,7 +138,7 @@ function HandleComponent(
onConnect: onConnectExtended, onConnect: onConnectExtended,
isValidConnection: isValidConnection || currentStore.isValidConnection, isValidConnection: isValidConnection || currentStore.isValidConnection,
getTransform: () => store.getState().transform, getTransform: () => store.getState().transform,
getConnectionStartHandle: () => store.getState().connectionStartHandle, getFromHandle: () => store.getState().connection.fromHandle,
}); });
} }
@@ -172,7 +166,7 @@ function HandleComponent(
if (!connectionClickStartHandle) { if (!connectionClickStartHandle) {
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type }); onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } }); store.setState({ connectionClickStartHandle: { nodeId, type, id: handleId } });
return; return;
} }
@@ -186,7 +180,7 @@ function HandleComponent(
}, },
connectionMode, connectionMode,
fromNodeId: connectionClickStartHandle.nodeId, fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null, fromHandleId: connectionClickStartHandle.id || null,
fromType: connectionClickStartHandle.type, fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler, isValidConnection: isValidConnectionHandler,
flowId, flowId,
@@ -6,7 +6,6 @@ import {
elementSelectionKeys, elementSelectionKeys,
errorMessages, errorMessages,
getNodeDimensions, getNodeDimensions,
getPositionWithOrigin,
isInputDOMNode, isInputDOMNode,
nodeHasDimensions, nodeHasDimensions,
} from '@xyflow/system'; } from '@xyflow/system';
@@ -40,7 +39,6 @@ export function NodeWrapper<NodeType extends Node>({
rfId, rfId,
nodeTypes, nodeTypes,
nodeExtent, nodeExtent,
nodeOrigin,
onError, onError,
}: NodeWrapperProps<NodeType>) { }: NodeWrapperProps<NodeType>) {
const { node, internals, isParent } = useStore((s) => { const { node, internals, isParent } = useStore((s) => {
@@ -87,15 +85,11 @@ export function NodeWrapper<NodeType extends Node>({
const nodeDimensions = getNodeDimensions(node); const nodeDimensions = getNodeDimensions(node);
const inlineDimensions = getNodeInlineStyleDimensions(node); const inlineDimensions = getNodeInlineStyleDimensions(node);
// TODO: clamping should happen earlier
const clampedPosition = nodeExtent const clampedPosition = nodeExtent
? clampPosition(internals.positionAbsolute, nodeExtent) ? clampPosition(internals.positionAbsolute, nodeExtent)
: internals.positionAbsolute; : internals.positionAbsolute;
const positionWithOrigin = getPositionWithOrigin({
...clampedPosition,
...nodeDimensions,
origin: node.origin || nodeOrigin,
});
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler = onMouseEnter const onMouseEnterHandler = onMouseEnter
@@ -181,7 +175,7 @@ export function NodeWrapper<NodeType extends Node>({
ref={nodeRef} ref={nodeRef}
style={{ style={{
zIndex: internals.z, zIndex: internals.z,
transform: `translate(${positionWithOrigin.x}px,${positionWithOrigin.y}px)`, transform: `translate(${clampedPosition.x}px,${clampedPosition.y}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none', pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: hasDimensions ? 'visible' : 'hidden', visibility: hasDimensions ? 'visible' : 'hidden',
...node.style, ...node.style,
@@ -21,7 +21,6 @@ export type NodesSelectionProps<NodeType> = {
const selector = (s: ReactFlowState) => { const selector = (s: ReactFlowState) => {
const { width, height, x, y } = getInternalNodesBounds(s.nodeLookup, { const { width, height, x, y } = getInternalNodesBounds(s.nodeLookup, {
nodeOrigin: s.nodeOrigin,
filter: (node) => !!node.selected, filter: (node) => !!node.selected,
}); });
@@ -4,6 +4,7 @@ import { Provider } from '../../contexts/StoreContext';
import { createStore } from '../../store'; import { createStore } from '../../store';
import { BatchProvider } from '../BatchProvider'; import { BatchProvider } from '../BatchProvider';
import type { Node, Edge } from '../../types'; import type { Node, Edge } from '../../types';
import { NodeOrigin } from '@xyflow/system';
export type ReactFlowProviderProps = { export type ReactFlowProviderProps = {
initialNodes?: Node[]; initialNodes?: Node[];
@@ -13,6 +14,7 @@ export type ReactFlowProviderProps = {
initialWidth?: number; initialWidth?: number;
initialHeight?: number; initialHeight?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
children: ReactNode; children: ReactNode;
}; };
@@ -24,6 +26,7 @@ export function ReactFlowProvider({
initialWidth: width, initialWidth: width,
initialHeight: height, initialHeight: height,
fitView, fitView,
nodeOrigin,
children, children,
}: ReactFlowProviderProps) { }: ReactFlowProviderProps) {
const [store] = useState(() => const [store] = useState(() =>
@@ -35,6 +38,7 @@ export function ReactFlowProvider({
width, width,
height, height,
fitView, fitView,
nodeOrigin,
}) })
); );
@@ -26,7 +26,7 @@ const reactFlowFieldsToTrack = [
'nodesConnectable', 'nodesConnectable',
'nodesFocusable', 'nodesFocusable',
'edgesFocusable', 'edgesFocusable',
'edgesUpdatable', 'edgesReconnectable',
'elevateNodesOnSelect', 'elevateNodesOnSelect',
'elevateEdgesOnSelect', 'elevateEdgesOnSelect',
'minZoom', 'minZoom',
@@ -14,14 +14,14 @@ type EdgeRendererProps<EdgeType extends Edge = Edge> = Pick<
| 'onEdgeDoubleClick' | 'onEdgeDoubleClick'
| 'defaultMarkerColor' | 'defaultMarkerColor'
| 'onlyRenderVisibleElements' | 'onlyRenderVisibleElements'
| 'onEdgeUpdate' | 'onReconnect'
| 'onEdgeContextMenu' | 'onEdgeContextMenu'
| 'onEdgeMouseEnter' | 'onEdgeMouseEnter'
| 'onEdgeMouseMove' | 'onEdgeMouseMove'
| 'onEdgeMouseLeave' | 'onEdgeMouseLeave'
| 'onEdgeUpdateStart' | 'onReconnectStart'
| 'onEdgeUpdateEnd' | 'onReconnectEnd'
| 'edgeUpdaterRadius' | 'reconnectRadius'
| 'noPanClassName' | 'noPanClassName'
| 'rfId' | 'rfId'
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
@@ -34,7 +34,7 @@ const selector = (s: ReactFlowState) => ({
width: s.width, width: s.width,
height: s.height, height: s.height,
edgesFocusable: s.edgesFocusable, edgesFocusable: s.edgesFocusable,
edgesUpdatable: s.edgesUpdatable, edgesReconnectable: s.edgesReconnectable,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
onError: s.onError, onError: s.onError,
@@ -46,19 +46,19 @@ function EdgeRendererComponent<EdgeType extends Edge = Edge>({
rfId, rfId,
edgeTypes, edgeTypes,
noPanClassName, noPanClassName,
onEdgeUpdate, onReconnect,
onEdgeContextMenu, onEdgeContextMenu,
onEdgeMouseEnter, onEdgeMouseEnter,
onEdgeMouseMove, onEdgeMouseMove,
onEdgeMouseLeave, onEdgeMouseLeave,
onEdgeClick, onEdgeClick,
edgeUpdaterRadius, reconnectRadius,
onEdgeDoubleClick, onEdgeDoubleClick,
onEdgeUpdateStart, onReconnectStart,
onEdgeUpdateEnd, onReconnectEnd,
disableKeyboardA11y, disableKeyboardA11y,
}: EdgeRendererProps<EdgeType>) { }: EdgeRendererProps<EdgeType>) {
const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow); const { edgesFocusable, edgesReconnectable, elementsSelectable, onError } = useStore(selector, shallow);
const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements); const edgeIds = useVisibleEdgeIds(onlyRenderVisibleElements);
return ( return (
@@ -71,19 +71,19 @@ function EdgeRendererComponent<EdgeType extends Edge = Edge>({
key={id} key={id}
id={id} id={id}
edgesFocusable={edgesFocusable} edgesFocusable={edgesFocusable}
edgesUpdatable={edgesUpdatable} edgesReconnectable={edgesReconnectable}
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onContextMenu={onEdgeContextMenu} onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter} onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove} onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave} onMouseLeave={onEdgeMouseLeave}
onClick={onEdgeClick} onClick={onEdgeClick}
edgeUpdaterRadius={edgeUpdaterRadius} reconnectRadius={reconnectRadius}
onDoubleClick={onEdgeDoubleClick} onDoubleClick={onEdgeDoubleClick}
onEdgeUpdateStart={onEdgeUpdateStart} onReconnectStart={onReconnectStart}
onEdgeUpdateEnd={onEdgeUpdateEnd} onReconnectEnd={onReconnectEnd}
rfId={rfId} rfId={rfId}
onError={onError} onError={onError}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
@@ -22,12 +22,13 @@ export type FlowRendererProps<NodeType extends Node = Node> = Omit<
| 'selectNodesOnDrag' | 'selectNodesOnDrag'
| 'defaultMarkerColor' | 'defaultMarkerColor'
| 'rfId' | 'rfId'
| 'nodeOrigin'
> & { > & {
isControlledViewport: boolean; isControlledViewport: boolean;
children: ReactNode; children: ReactNode;
}; };
const win = typeof window !== 'undefined' ? window : undefined;
const selector = (s: ReactFlowState) => { const selector = (s: ReactFlowState) => {
return { nodesSelectionActive: s.nodesSelectionActive, userSelectionActive: s.userSelectionActive }; return { nodesSelectionActive: s.nodesSelectionActive, userSelectionActive: s.userSelectionActive };
}; };
@@ -70,8 +71,8 @@ function FlowRendererComponent<NodeType extends Node = Node>({
isControlledViewport, isControlledViewport,
}: FlowRendererProps<NodeType>) { }: FlowRendererProps<NodeType>) {
const { nodesSelectionActive, userSelectionActive } = useStore(selector); const { nodesSelectionActive, userSelectionActive } = useStore(selector);
const selectionKeyPressed = useKeyPress(selectionKeyCode); const selectionKeyPressed = useKeyPress(selectionKeyCode, { target: win });
const panActivationKeyPressed = useKeyPress(panActivationKeyCode); const panActivationKeyPressed = useKeyPress(panActivationKeyCode, { target: win });
const panOnDrag = panActivationKeyPressed || _panOnDrag; const panOnDrag = panActivationKeyPressed || _panOnDrag;
const panOnScroll = panActivationKeyPressed || _panOnScroll; const panOnScroll = panActivationKeyPressed || _panOnScroll;
@@ -113,6 +114,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
panOnDrag={panOnDrag} panOnDrag={panOnDrag}
isSelecting={!!isSelecting} isSelecting={!!isSelecting}
selectionMode={selectionMode} selectionMode={selectionMode}
selectionKeyPressed={selectionKeyPressed}
> >
{children} {children}
{nodesSelectionActive && ( {nodesSelectionActive && (
@@ -32,7 +32,6 @@ export type GraphViewProps<NodeType extends Node = Node, EdgeType extends Edge =
| 'noPanClassName' | 'noPanClassName'
| 'defaultViewport' | 'defaultViewport'
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
| 'nodeOrigin'
> >
> & { > & {
rfId: string; rfId: string;
@@ -85,19 +84,18 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
onPaneMouseLeave, onPaneMouseLeave,
onPaneScroll, onPaneScroll,
onPaneContextMenu, onPaneContextMenu,
onEdgeUpdate,
onEdgeContextMenu, onEdgeContextMenu,
onEdgeMouseEnter, onEdgeMouseEnter,
onEdgeMouseMove, onEdgeMouseMove,
onEdgeMouseLeave, onEdgeMouseLeave,
edgeUpdaterRadius, reconnectRadius,
onEdgeUpdateStart, onReconnect,
onEdgeUpdateEnd, onReconnectStart,
onReconnectEnd,
noDragClassName, noDragClassName,
noWheelClassName, noWheelClassName,
noPanClassName, noPanClassName,
disableKeyboardA11y, disableKeyboardA11y,
nodeOrigin,
nodeExtent, nodeExtent,
rfId, rfId,
viewport, viewport,
@@ -153,15 +151,15 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick} onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onlyRenderVisibleElements={onlyRenderVisibleElements} onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu} onEdgeContextMenu={onEdgeContextMenu}
onEdgeMouseEnter={onEdgeMouseEnter} onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave} onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart} reconnectRadius={reconnectRadius}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor} defaultMarkerColor={defaultMarkerColor}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
@@ -186,7 +184,6 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
noDragClassName={noDragClassName} noDragClassName={noDragClassName}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
nodeOrigin={nodeOrigin}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
rfId={rfId} rfId={rfId}
/> />
@@ -22,7 +22,6 @@ export type NodeRendererProps<NodeType extends Node> = Pick<
| 'noDragClassName' | 'noDragClassName'
| 'rfId' | 'rfId'
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
| 'nodeOrigin'
| 'nodeExtent' | 'nodeExtent'
| 'nodeTypes' | 'nodeTypes'
>; >;
@@ -72,7 +71,6 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
id={nodeId} id={nodeId}
nodeTypes={props.nodeTypes} nodeTypes={props.nodeTypes}
nodeExtent={props.nodeExtent} nodeExtent={props.nodeExtent}
nodeOrigin={props.nodeOrigin}
onClick={props.onNodeClick} onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter} onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove} onMouseMove={props.onNodeMouseMove}
+37 -22
View File
@@ -2,7 +2,12 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift * The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/ */
import { useRef, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react'; import {
useRef,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
type ReactNode,
} from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { getNodesInside, getEventPosition, SelectionMode, type NodeChange, type EdgeChange } from '@xyflow/system'; import { getNodesInside, getEventPosition, SelectionMode, type NodeChange, type EdgeChange } from '@xyflow/system';
@@ -15,6 +20,7 @@ import type { ReactFlowProps, ReactFlowState } from '../../types';
type PaneProps = { type PaneProps = {
isSelecting: boolean; isSelecting: boolean;
selectionKeyPressed: boolean;
children: ReactNode; children: ReactNode;
} & Partial< } & Partial<
Pick< Pick<
@@ -52,6 +58,7 @@ const selector = (s: ReactFlowState) => ({
export function Pane({ export function Pane({
isSelecting, isSelecting,
selectionKeyPressed,
selectionMode = SelectionMode.Full, selectionMode = SelectionMode.Full,
panOnDrag, panOnDrag,
onSelectionStart, onSelectionStart,
@@ -72,6 +79,10 @@ export function Pane({
const edgeIdLookup = useRef<Map<string, Set<string>>>(new Map()); const edgeIdLookup = useRef<Map<string, Set<string>>>(new Map());
const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow); const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow);
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
// Used to prevent click events when the user lets go of the selectionKey during a selection
const selectionInProgress = useRef<boolean>(false);
const resetUserSelection = () => { const resetUserSelection = () => {
store.setState({ userSelectionActive: false, userSelectionRect: null }); store.setState({ userSelectionActive: false, userSelectionRect: null });
@@ -81,6 +92,12 @@ export function Pane({
}; };
const onClick = (event: ReactMouseEvent) => { const onClick = (event: ReactMouseEvent) => {
// We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress.current) {
selectionInProgress.current = false;
return;
}
onPaneClick?.(event); onPaneClick?.(event);
store.getState().resetSelectedElements(); store.getState().resetSelectedElements();
store.setState({ nodesSelectionActive: false }); store.setState({ nodesSelectionActive: false });
@@ -97,9 +114,10 @@ export function Pane({
const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined; const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
const onMouseDown = (event: ReactMouseEvent): void => { const onPointerDown = (event: ReactPointerEvent): void => {
const { resetSelectedElements, domNode, edgeLookup } = store.getState(); const { resetSelectedElements, domNode, edgeLookup } = store.getState();
containerBounds.current = domNode?.getBoundingClientRect(); containerBounds.current = domNode?.getBoundingClientRect();
container.current?.setPointerCapture(event.pointerId);
if ( if (
!elementsSelectable || !elementsSelectable ||
@@ -136,14 +154,16 @@ export function Pane({
onSelectionStart?.(event); onSelectionStart?.(event);
}; };
const onMouseMove = (event: ReactMouseEvent): void => { const onPointerMove = (event: ReactPointerEvent): void => {
const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = const { userSelectionRect, edgeLookup, transform, nodeLookup, triggerNodeChanges, triggerEdgeChanges } =
store.getState(); store.getState();
if (!isSelecting || !containerBounds.current || !userSelectionRect) { if (!containerBounds.current || !userSelectionRect) {
return; return;
} }
selectionInProgress.current = true;
const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current); const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current);
const { startX, startY } = userSelectionRect; const { startX, startY } = userSelectionRect;
@@ -161,8 +181,7 @@ export function Pane({
nextUserSelectRect, nextUserSelectRect,
transform, transform,
selectionMode === SelectionMode.Partial, selectionMode === SelectionMode.Partial,
true, true
nodeOrigin
); );
const selectedEdgeIds = new Set<string>(); const selectedEdgeIds = new Set<string>();
@@ -199,10 +218,11 @@ export function Pane({
}); });
}; };
const onMouseUp = (event: ReactMouseEvent) => { const onPointerUp = (event: ReactPointerEvent) => {
if (event.button !== 0) { if (event.button !== 0) {
return; return;
} }
container.current?.releasePointerCapture(event.pointerId);
const { userSelectionRect } = store.getState(); const { userSelectionRect } = store.getState();
// We only want to trigger click functions when in selection mode if // We only want to trigger click functions when in selection mode if
// the user did not move the mouse. // the user did not move the mouse.
@@ -214,30 +234,25 @@ export function Pane({
resetUserSelection(); resetUserSelection();
onSelectionEnd?.(event); onSelectionEnd?.(event);
};
const onMouseLeave = (event: ReactMouseEvent) => { // If the user kept holding the selectionKey during the selection,
if (userSelectionActive) { // we need to reset the selectionInProgress, so the next click event is not prevented
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); if (selectionKeyPressed) {
onSelectionEnd?.(event); selectionInProgress.current = false;
} }
resetUserSelection();
}; };
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
return ( return (
<div <div
className={cc(['react-flow__pane', { draggable: panOnDrag, dragging, selection: isSelecting }])} className={cc(['react-flow__pane', { draggable: panOnDrag, dragging, selection: isSelecting }])}
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)} onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onContextMenu={wrapHandler(onContextMenu, container)} onContextMenu={wrapHandler(onContextMenu, container)}
onWheel={wrapHandler(onWheel, container)} onWheel={wrapHandler(onWheel, container)}
onMouseEnter={hasActiveSelection ? undefined : onPaneMouseEnter} onPointerEnter={hasActiveSelection ? undefined : onPaneMouseEnter}
onMouseDown={hasActiveSelection ? onMouseDown : undefined} onPointerDown={hasActiveSelection ? onPointerDown : onPaneMouseMove}
onMouseMove={hasActiveSelection ? onMouseMove : onPaneMouseMove} onPointerMove={hasActiveSelection ? onPointerMove : onPaneMouseMove}
onMouseUp={hasActiveSelection ? onMouseUp : undefined} onPointerUp={hasActiveSelection ? onPointerUp : undefined}
onMouseLeave={hasActiveSelection ? onMouseLeave : onPaneMouseLeave} onPointerLeave={onPaneMouseLeave}
ref={container} ref={container}
style={containerStyle} style={containerStyle}
> >
@@ -3,6 +3,7 @@ import { useContext, type ReactNode } from 'react';
import StoreContext from '../../contexts/StoreContext'; import StoreContext from '../../contexts/StoreContext';
import { ReactFlowProvider } from '../../components/ReactFlowProvider'; import { ReactFlowProvider } from '../../components/ReactFlowProvider';
import type { Node, Edge } from '../../types'; import type { Node, Edge } from '../../types';
import { NodeOrigin } from '@xyflow/system';
export function Wrapper({ export function Wrapper({
children, children,
@@ -13,6 +14,7 @@ export function Wrapper({
width, width,
height, height,
fitView, fitView,
nodeOrigin,
}: { }: {
children: ReactNode; children: ReactNode;
nodes?: Node[]; nodes?: Node[];
@@ -22,6 +24,7 @@ export function Wrapper({
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}) { }) {
const isWrapped = useContext(StoreContext); const isWrapped = useContext(StoreContext);
@@ -40,6 +43,7 @@ export function Wrapper({
initialWidth={width} initialWidth={width}
initialHeight={height} initialHeight={height}
fitView={fitView} fitView={fitView}
nodeOrigin={nodeOrigin}
> >
{children} {children}
</ReactFlowProvider> </ReactFlowProvider>
@@ -81,7 +81,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
nodesFocusable, nodesFocusable,
nodeOrigin = defaultNodeOrigin, nodeOrigin = defaultNodeOrigin,
edgesFocusable, edgesFocusable,
edgesUpdatable, edgesReconnectable,
elementsSelectable = true, elementsSelectable = true,
defaultViewport = initViewport, defaultViewport = initViewport,
minZoom = 0.5, minZoom = 0.5,
@@ -104,15 +104,15 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onPaneScroll, onPaneScroll,
onPaneContextMenu, onPaneContextMenu,
children, children,
onEdgeUpdate, onReconnect,
onReconnectStart,
onReconnectEnd,
onEdgeContextMenu, onEdgeContextMenu,
onEdgeDoubleClick, onEdgeDoubleClick,
onEdgeMouseEnter, onEdgeMouseEnter,
onEdgeMouseMove, onEdgeMouseMove,
onEdgeMouseLeave, onEdgeMouseLeave,
onEdgeUpdateStart, reconnectRadius = 10,
onEdgeUpdateEnd,
edgeUpdaterRadius = 10,
onNodesChange, onNodesChange,
onEdgesChange, onEdgesChange,
noDragClassName = 'nodrag', noDragClassName = 'nodrag',
@@ -157,7 +157,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
data-testid="rf__wrapper" data-testid="rf__wrapper"
id={id} id={id}
> >
<Wrapper nodes={nodes} edges={edges} width={width} height={height} fitView={fitView}> <Wrapper nodes={nodes} edges={edges} width={width} height={height} fitView={fitView} nodeOrigin={nodeOrigin}>
<GraphView<NodeType, EdgeType> <GraphView<NodeType, EdgeType>
onInit={onInit} onInit={onInit}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
@@ -202,22 +202,21 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onSelectionContextMenu={onSelectionContextMenu} onSelectionContextMenu={onSelectionContextMenu}
onSelectionStart={onSelectionStart} onSelectionStart={onSelectionStart}
onSelectionEnd={onSelectionEnd} onSelectionEnd={onSelectionEnd}
onEdgeUpdate={onEdgeUpdate} onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onEdgeContextMenu={onEdgeContextMenu} onEdgeContextMenu={onEdgeContextMenu}
onEdgeDoubleClick={onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeMouseEnter={onEdgeMouseEnter} onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave} onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart} reconnectRadius={reconnectRadius}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor} defaultMarkerColor={defaultMarkerColor}
noDragClassName={noDragClassName} noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName} noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
rfId={rfId} rfId={rfId}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
nodeOrigin={nodeOrigin}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
viewport={viewport} viewport={viewport}
onViewportChange={onViewportChange} onViewportChange={onViewportChange}
@@ -236,7 +235,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
nodesConnectable={nodesConnectable} nodesConnectable={nodesConnectable}
nodesFocusable={nodesFocusable} nodesFocusable={nodesFocusable}
edgesFocusable={edgesFocusable} edgesFocusable={edgesFocusable}
edgesUpdatable={edgesUpdatable} edgesReconnectable={edgesReconnectable}
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
elevateNodesOnSelect={elevateNodesOnSelect} elevateNodesOnSelect={elevateNodesOnSelect}
elevateEdgesOnSelect={elevateEdgesOnSelect} elevateEdgesOnSelect={elevateEdgesOnSelect}
+9 -23
View File
@@ -2,33 +2,19 @@ import { shallow } from 'zustand/shallow';
import { useStore } from './useStore'; import { useStore } from './useStore';
import type { ReactFlowStore } from '../types/store'; import type { ReactFlowStore } from '../types/store';
import { ConnectionState, pointToRendererPoint } from '@xyflow/system';
const selector = (s: ReactFlowStore) => ({ const selector = (s: ReactFlowStore): ConnectionState => {
startHandle: s.connectionStartHandle, return s.connection.inProgress
endHandle: s.connectionEndHandle, ? { ...s.connection, to: pointToRendererPoint(s.connection.to, s.transform) }
status: s.connectionStatus, : { ...s.connection };
position: s.connectionStartHandle ? s.connectionPosition : null,
});
type UseConnectionResult = {
/** The start handle where the user interaction started or null */
startHandle: ReactFlowStore['connectionStartHandle'];
/** The target handle that's inside the connection radius or null */
endHandle: ReactFlowStore['connectionEndHandle'];
/** The current connection status 'valid', 'invalid' or null*/
status: ReactFlowStore['connectionStatus'];
/** The current connection position or null */
position: ReactFlowStore['connectionPosition'] | null;
}; };
/** /**
* Hook for accessing the ongoing connection. * Hook for accessing the connection state.
* *
* @public * @public
* @returns ongoing connection * @returns ConnectionState
*/ */
export function useConnection(): UseConnectionResult { export function useConnection(): ConnectionState {
const ongoingConnection = useStore(selector, shallow); return useStore(selector, shallow);
return ongoingConnection;
} }
@@ -9,6 +9,7 @@ import { Edge, Node } from '../types';
const selected = (item: Node | Edge) => item.selected; const selected = (item: Node | Edge) => item.selected;
const deleteKeyOptions: UseKeyPressOptions = { actInsideInputWithModifier: false }; const deleteKeyOptions: UseKeyPressOptions = { actInsideInputWithModifier: false };
const win = typeof window !== 'undefined' ? window : undefined;
/** /**
* Hook for handling global key events. * Hook for handling global key events.
@@ -26,7 +27,7 @@ export function useGlobalKeyHandler({
const { deleteElements } = useReactFlow(); const { deleteElements } = useReactFlow();
const deleteKeyPressed = useKeyPress(deleteKeyCode, deleteKeyOptions); const deleteKeyPressed = useKeyPress(deleteKeyCode, deleteKeyOptions);
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode, { target: win });
useEffect(() => { useEffect(() => {
if (deleteKeyPressed) { if (deleteKeyPressed) {
+35 -5
View File
@@ -13,7 +13,7 @@ import {
import useViewportHelper from './useViewportHelper'; import useViewportHelper from './useViewportHelper';
import { useStore, useStoreApi } from './useStore'; import { useStore, useStoreApi } from './useStore';
import { useBatchContext } from '../components/BatchProvider'; import { useBatchContext } from '../components/BatchProvider';
import { elementToRemoveChange, isNode } from '../utils'; import { elementToRemoveChange, isEdge, isNode } from '../utils';
import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, GeneralHelpers } from '../types'; import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, GeneralHelpers } from '../types';
const selector = (s: ReactFlowState) => !!s.panZoom; const selector = (s: ReactFlowState) => !!s.panZoom;
@@ -41,12 +41,16 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
batchContext.nodeQueue.push(payload as NodeType[]); batchContext.nodeQueue.push(payload as NodeType[]);
}; };
const setEdges: GeneralHelpers<NodeType, EdgeType>['setEdges'] = (payload) => {
batchContext.edgeQueue.push(payload as EdgeType[]);
};
const getNodeRect = (node: NodeType | { id: string }): Rect | null => { const getNodeRect = (node: NodeType | { id: string }): Rect | null => {
const { nodeLookup, nodeOrigin } = store.getState(); const { nodeLookup, nodeOrigin } = store.getState();
const nodeToUse = isNode<NodeType>(node) ? node : nodeLookup.get(node.id)!; const nodeToUse = isNode<NodeType>(node) ? node : nodeLookup.get(node.id)!;
const position = nodeToUse.parentId const position = nodeToUse.parentId
? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.parentId, nodeLookup, nodeOrigin) ? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.measured, nodeToUse.parentId, nodeLookup, nodeOrigin)
: nodeToUse.position; : nodeToUse.position;
const nodeWithPosition = { const nodeWithPosition = {
@@ -77,6 +81,23 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
); );
}; };
const updateEdge: GeneralHelpers<NodeType, EdgeType>['updateEdge'] = (
id,
edgeUpdate,
options = { replace: false }
) => {
setEdges((prevEdges) =>
prevEdges.map((edge) => {
if (edge.id === id) {
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge as EdgeType) : edgeUpdate;
return options.replace && isEdge(nextEdge) ? (nextEdge as EdgeType) : { ...edge, ...nextEdge };
}
return edge;
})
);
};
return { return {
getNodes: () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[], getNodes: () => store.getState().nodes.map((n) => ({ ...n })) as NodeType[],
getNode: (id) => getInternalNode(id)?.internals.userNode as NodeType, getNode: (id) => getInternalNode(id)?.internals.userNode as NodeType,
@@ -87,9 +108,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
}, },
getEdge: (id) => store.getState().edgeLookup.get(id) as EdgeType, getEdge: (id) => store.getState().edgeLookup.get(id) as EdgeType,
setNodes, setNodes,
setEdges: (payload) => { setEdges,
batchContext.edgeQueue.push(payload as EdgeType[]);
},
addNodes: (payload) => { addNodes: (payload) => {
const newNodes = Array.isArray(payload) ? payload : [payload]; const newNodes = Array.isArray(payload) ? payload : [payload];
batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]); batchContext.nodeQueue.push((nodes) => [...nodes, ...newNodes]);
@@ -200,6 +219,17 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
options options
); );
}, },
updateEdge,
updateEdgeData: (id, dataUpdate, options = { replace: false }) => {
updateEdge(
id,
(edge) => {
const nextData = typeof dataUpdate === 'function' ? dataUpdate(edge) : dataUpdate;
return options.replace ? { ...edge, data: nextData } : { ...edge, data: { ...edge.data, ...nextData } };
},
options
);
},
}; };
}, []); }, []);
+16 -15
View File
@@ -45,22 +45,23 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return { x, y, zoom }; return { x, y, zoom };
}, },
fitView: (options) => { fitView: (options) => {
const { nodeLookup, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState(); const { nodeLookup, width, height, minZoom, maxZoom, panZoom } = store.getState();
return panZoom if (!panZoom) {
? fitView( return false;
{ }
nodeLookup,
width, return fitView(
height, {
nodeOrigin, nodeLookup,
minZoom, width,
maxZoom, height,
panZoom, minZoom,
}, maxZoom,
options panZoom,
) },
: false; options
);
}, },
setCenter: (x, y, options) => { setCenter: (x, y, options) => {
const { width, height, maxZoom, panZoom } = store.getState(); const { width, height, maxZoom, panZoom } = store.getState();
+1 -2
View File
@@ -49,7 +49,6 @@ export {
type OnMoveStart, type OnMoveStart,
type OnMoveEnd, type OnMoveEnd,
type Connection, type Connection,
type ConnectionStatus,
ConnectionMode, ConnectionMode,
type OnConnectStartParams, type OnConnectStartParams,
type OnConnectStart, type OnConnectStart,
@@ -116,6 +115,6 @@ export {
getIncomers, getIncomers,
getOutgoers, getOutgoers,
addEdge, addEdge,
updateEdge, reconnectEdge,
getConnectedEdges, getConnectedEdges,
} from '@xyflow/system'; } from '@xyflow/system';
+15 -20
View File
@@ -12,6 +12,8 @@ import {
EdgeSelectionChange, EdgeSelectionChange,
NodeSelectionChange, NodeSelectionChange,
ParentExpandChild, ParentExpandChild,
initialConnection,
NodeOrigin,
} from '@xyflow/system'; } from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -26,6 +28,7 @@ const createStore = ({
width, width,
height, height,
fitView, fitView,
nodeOrigin,
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
@@ -34,10 +37,11 @@ const createStore = ({
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}) => }) =>
createWithEqualityFn<ReactFlowState>( createWithEqualityFn<ReactFlowState>(
(set, get) => ({ (set, get) => ({
...getInitialState({ nodes, edges, width, height, fitView, defaultNodes, defaultEdges }), ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, defaultNodes, defaultEdges }),
setNodes: (nodes: Node[]) => { setNodes: (nodes: Node[]) => {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get();
// setNodes() is called exclusively in response to user actions: // setNodes() is called exclusively in response to user actions:
@@ -98,7 +102,7 @@ const createStore = ({
return; return;
} }
updateAbsolutePositions(nodeLookup, { nodeOrigin }); updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin });
// we call fitView once initially after all dimensions are set // we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone; let nextFitViewDone = fitViewDone;
@@ -155,8 +159,8 @@ const createStore = ({
} }
if (parentExpandChildren.length > 0) { if (parentExpandChildren.length > 0) {
const { nodeLookup, parentLookup } = get(); const { nodeLookup, parentLookup, nodeOrigin } = get();
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup); const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges); changes.push(...parentExpandChanges);
} }
@@ -288,7 +292,7 @@ const createStore = ({
return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
}, },
fitView: (options?: FitViewOptions): boolean => { fitView: (options?: FitViewOptions): boolean => {
const { panZoom, width, height, minZoom, maxZoom, nodeOrigin, nodeLookup } = get(); const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();
if (!panZoom) { if (!panZoom) {
return false; return false;
@@ -302,26 +306,17 @@ const createStore = ({
panZoom, panZoom,
minZoom, minZoom,
maxZoom, maxZoom,
nodeOrigin,
}, },
options options
); );
}, },
cancelConnection: () => cancelConnection: () => {
set({ set({
connectionStatus: null, connection: { ...initialConnection },
connectionStartHandle: null, });
connectionEndHandle: null, },
}), updateConnection: (connection) => {
updateConnection: (params) => { set({ connection });
const { connectionPosition } = get();
const currentConnection = {
...params,
connectionPosition: params.connectionPosition ?? connectionPosition,
};
set(currentConnection);
}, },
reset: () => set({ ...getInitialState() }), reset: () => set({ ...getInitialState() }),
+10 -9
View File
@@ -7,6 +7,8 @@ import {
updateConnectionLookup, updateConnectionLookup,
devWarn, devWarn,
getInternalNodesBounds, getInternalNodesBounds,
NodeOrigin,
initialConnection,
} from '@xyflow/system'; } from '@xyflow/system';
import type { Edge, InternalNode, Node, ReactFlowStore } from '../types'; import type { Edge, InternalNode, Node, ReactFlowStore } from '../types';
@@ -19,6 +21,7 @@ const getInitialState = ({
width, width,
height, height,
fitView, fitView,
nodeOrigin,
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
@@ -27,6 +30,7 @@ const getInitialState = ({
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
} = {}): ReactFlowStore => { } = {}): ReactFlowStore => {
const nodeLookup = new Map<string, InternalNode>(); const nodeLookup = new Map<string, InternalNode>();
const parentLookup = new Map(); const parentLookup = new Map();
@@ -34,21 +38,21 @@ const getInitialState = ({
const edgeLookup = new Map(); const edgeLookup = new Map();
const storeEdges = defaultEdges ?? edges ?? []; const storeEdges = defaultEdges ?? edges ?? [];
const storeNodes = defaultNodes ?? nodes ?? []; const storeNodes = defaultNodes ?? nodes ?? [];
const storeNodeOrigin = nodeOrigin ?? [0, 0];
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
adoptUserNodes(storeNodes, nodeLookup, parentLookup, { adoptUserNodes(storeNodes, nodeLookup, parentLookup, {
nodeOrigin: [0, 0], nodeOrigin: storeNodeOrigin,
elevateNodesOnSelect: false, elevateNodesOnSelect: false,
}); });
let transform: Transform = [0, 0, 1]; let transform: Transform = [0, 0, 1];
if (fitView && width && height) { if (fitView && width && height) {
// @todo users nodeOrigin should be used here
const bounds = getInternalNodesBounds(nodeLookup, { const bounds = getInternalNodesBounds(nodeLookup, {
nodeOrigin: [0, 0],
filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight)), filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight)),
}); });
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];
} }
@@ -76,13 +80,11 @@ const getInitialState = ({
nodesSelectionActive: false, nodesSelectionActive: false,
userSelectionActive: false, userSelectionActive: false,
userSelectionRect: null, userSelectionRect: null,
connectionPosition: { x: 0, y: 0 },
connectionStatus: null,
connectionMode: ConnectionMode.Strict, connectionMode: ConnectionMode.Strict,
domNode: null, domNode: null,
paneDragging: false, paneDragging: false,
noPanClassName: 'nopan', noPanClassName: 'nopan',
nodeOrigin: [0, 0], nodeOrigin: storeNodeOrigin,
nodeDragThreshold: 1, nodeDragThreshold: 1,
snapGrid: [15, 15], snapGrid: [15, 15],
@@ -92,7 +94,7 @@ const getInitialState = ({
nodesConnectable: true, nodesConnectable: true,
nodesFocusable: true, nodesFocusable: true,
edgesFocusable: true, edgesFocusable: true,
edgesUpdatable: true, edgesReconnectable: true,
elementsSelectable: true, elementsSelectable: true,
elevateNodesOnSelect: true, elevateNodesOnSelect: true,
elevateEdgesOnSelect: false, elevateEdgesOnSelect: false,
@@ -103,8 +105,7 @@ const getInitialState = ({
multiSelectionActive: false, multiSelectionActive: false,
connectionStartHandle: null, connection: { ...initialConnection },
connectionEndHandle: null,
connectionClickStartHandle: null, connectionClickStartHandle: null,
connectOnClick: true, connectOnClick: true,
+6 -6
View File
@@ -29,7 +29,7 @@ import type {
Node, Node,
Edge, Edge,
ConnectionLineComponent, ConnectionLineComponent,
OnEdgeUpdateFunc, OnReconnect,
OnInit, OnInit,
DefaultEdgeOptions, DefaultEdgeOptions,
FitViewOptions, FitViewOptions,
@@ -129,9 +129,9 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
onEdgeMouseLeave?: EdgeMouseHandler<EdgeType>; onEdgeMouseLeave?: EdgeMouseHandler<EdgeType>;
/** This event handler is called when a user double clicks on an edge */ /** This event handler is called when a user double clicks on an edge */
onEdgeDoubleClick?: EdgeMouseHandler<EdgeType>; onEdgeDoubleClick?: EdgeMouseHandler<EdgeType>;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void; onReconnect?: OnReconnect<EdgeType>;
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
onEdgeUpdate?: OnEdgeUpdateFunc<EdgeType>; onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
/** This event handler is called when a Node is updated /** This event handler is called when a Node is updated
* @example // Use NodesState hook to create edges and get onNodesChange handler * @example // Use NodesState hook to create edges and get onNodesChange handler
* import ReactFlow, { useNodesState } from '@xyflow/react'; * import ReactFlow, { useNodesState } from '@xyflow/react';
@@ -330,7 +330,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
/** Controls if all edges should be updateable /** Controls if all edges should be updateable
* @default true * @default true
*/ */
edgesUpdatable?: boolean; edgesReconnectable?: boolean;
/** Controls if all elements should (nodes & edges) be selectable /** Controls if all elements should (nodes & edges) be selectable
* @default true * @default true
*/ */
@@ -413,7 +413,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
panOnScrollMode?: PanOnScrollMode; panOnScrollMode?: PanOnScrollMode;
/** Controls if the viewport should zoom by double clicking somewhere on the flow */ /** Controls if the viewport should zoom by double clicking somewhere on the flow */
zoomOnDoubleClick?: boolean; zoomOnDoubleClick?: boolean;
edgeUpdaterRadius?: number; reconnectRadius?: number;
noDragClassName?: string; noDragClassName?: string;
noWheelClassName?: string; noWheelClassName?: string;
noPanClassName?: string; noPanClassName?: string;
+16 -15
View File
@@ -10,8 +10,7 @@ import type {
HandleType, HandleType,
Connection, Connection,
ConnectionLineType, ConnectionLineType,
HandleElement, Handle,
ConnectionStatus,
EdgePosition, EdgePosition,
StepPathOptions, StepPathOptions,
OnError, OnError,
@@ -28,8 +27,6 @@ export type EdgeLabelOptions = {
labelBgBorderRadius?: number; labelBgBorderRadius?: number;
}; };
export type EdgeUpdatable = boolean | HandleType;
/** /**
* The Edge type is mainly used for the `edges` that get passed to the ReactFlow component * The Edge type is mainly used for the `edges` that get passed to the ReactFlow component
* @public * @public
@@ -41,7 +38,7 @@ export type Edge<
EdgeLabelOptions & { EdgeLabelOptions & {
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
updatable?: EdgeUpdatable; reconnectable?: boolean | HandleType;
focusable?: boolean; focusable?: boolean;
}; };
@@ -60,26 +57,28 @@ type StepEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>
pathOptions?: StepPathOptions; pathOptions?: StepPathOptions;
}; };
export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge; type StraightEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>> = Edge<EdgeData, 'straight'>;
export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge | StraightEdge;
export type EdgeMouseHandler<EdgeType extends Edge = Edge> = (event: ReactMouseEvent, edge: EdgeType) => void; export type EdgeMouseHandler<EdgeType extends Edge = Edge> = (event: ReactMouseEvent, edge: EdgeType) => void;
export type EdgeWrapperProps<EdgeType extends Edge = Edge> = { export type EdgeWrapperProps<EdgeType extends Edge = Edge> = {
id: string; id: string;
edgesFocusable: boolean; edgesFocusable: boolean;
edgesUpdatable: boolean; edgesReconnectable: boolean;
elementsSelectable: boolean; elementsSelectable: boolean;
noPanClassName: string; noPanClassName: string;
onClick?: EdgeMouseHandler<EdgeType>; onClick?: EdgeMouseHandler<EdgeType>;
onDoubleClick?: EdgeMouseHandler<EdgeType>; onDoubleClick?: EdgeMouseHandler<EdgeType>;
onEdgeUpdate?: OnEdgeUpdateFunc<EdgeType>; onReconnect?: OnReconnect<EdgeType>;
onContextMenu?: EdgeMouseHandler<EdgeType>; onContextMenu?: EdgeMouseHandler<EdgeType>;
onMouseEnter?: EdgeMouseHandler<EdgeType>; onMouseEnter?: EdgeMouseHandler<EdgeType>;
onMouseMove?: EdgeMouseHandler<EdgeType>; onMouseMove?: EdgeMouseHandler<EdgeType>;
onMouseLeave?: EdgeMouseHandler<EdgeType>; onMouseLeave?: EdgeMouseHandler<EdgeType>;
edgeUpdaterRadius?: number; reconnectRadius?: number;
onEdgeUpdateStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
rfId?: string; rfId?: string;
edgeTypes?: EdgeTypes; edgeTypes?: EdgeTypes;
onError?: OnError; onError?: OnError;
@@ -189,20 +188,22 @@ export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'tar
*/ */
export type SimpleBezierEdgeProps = EdgeComponentProps; export type SimpleBezierEdgeProps = EdgeComponentProps;
export type OnEdgeUpdateFunc<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newConnection: Connection) => void; export type OnReconnect<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newConnection: Connection) => void;
export type ConnectionLineComponentProps = { export type ConnectionLineComponentProps = {
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
fromNode?: Node; fromNode: Node;
fromHandle?: HandleElement; fromHandle: Handle;
fromX: number; fromX: number;
fromY: number; fromY: number;
toX: number; toX: number;
toY: number; toY: number;
fromPosition: Position; fromPosition: Position;
toPosition: Position; toPosition: Position;
connectionStatus: ConnectionStatus | null; connectionStatus: 'valid' | 'invalid' | null;
toNode: Node | null;
toHandle: Handle | null;
}; };
export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>; export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>;
+30
View File
@@ -143,6 +143,36 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
dataUpdate: Partial<NodeType['data']> | ((node: NodeType) => Partial<NodeType['data']>), dataUpdate: Partial<NodeType['data']> | ((node: NodeType) => Partial<NodeType['data']>),
options?: { replace: boolean } options?: { replace: boolean }
) => void; ) => void;
/**
* Updates an edge.
*
* @param id - id of the edge to update
* @param edgeUpdate - the edge update as an object or a function that receives the current edge and returns the edge update
* @param options.replace - if true, the edge is replaced with the edge update, otherwise the changes get merged
*
* @example
* updateEdge('edge-1', (edge) => ({ label: 'A new label' }));
*/
updateEdge: (
id: string,
edgeUpdate: Partial<EdgeType> | ((edge: EdgeType) => Partial<EdgeType>),
options?: { replace: boolean }
) => void;
/**
* Updates the data attribute of a edge.
*
* @param id - id of the edge to update
* @param dataUpdate - the data update as an object or a function that receives the current data and returns the data update
* @param options.replace - if true, the data is replaced with the data update, otherwise the changes get merged
*
* @example
* updateEdgeData('edge-1', { label: 'A new label' });
*/
updateEdgeData: (
id: string,
dataUpdate: Partial<EdgeType['data']> | ((edge: EdgeType) => Partial<EdgeType['data']>),
options?: { replace: boolean }
) => void;
}; };
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers< export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers<
+1 -9
View File
@@ -1,12 +1,5 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system';
CoordinateExtent,
NodeBase,
NodeOrigin,
OnError,
NodeProps as NodePropsBase,
InternalNodeBase,
} from '@xyflow/system';
import { NodeTypes } from './general'; import { NodeTypes } from './general';
@@ -59,7 +52,6 @@ export type NodeWrapperProps<NodeType extends Node> = {
disableKeyboardA11y: boolean; disableKeyboardA11y: boolean;
nodeTypes?: NodeTypes; nodeTypes?: NodeTypes;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
nodeOrigin: NodeOrigin;
onError?: OnError; onError?: OnError;
}; };
+9 -13
View File
@@ -1,6 +1,6 @@
import { import {
ConnectionMode, ConnectionMode,
type ConnectionStatus, type ConnectionState,
type CoordinateExtent, type CoordinateExtent,
type InternalNodeUpdate, type InternalNodeUpdate,
type UpdateNodePositions, type UpdateNodePositions,
@@ -10,9 +10,8 @@ import {
type OnViewportChange, type OnViewportChange,
type SelectionRect, type SelectionRect,
type SnapGrid, type SnapGrid,
type ConnectingHandle, type Handle,
type Transform, type Transform,
type XYPosition,
type PanZoomInstance, type PanZoomInstance,
type PanBy, type PanBy,
type OnConnectStart, type OnConnectStart,
@@ -25,8 +24,9 @@ import {
type EdgeLookup, type EdgeLookup,
type ConnectionLookup, type ConnectionLookup,
type NodeLookup, type NodeLookup,
NodeChange, type NodeChange,
EdgeChange, type EdgeChange,
type ParentLookup,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -54,7 +54,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
transform: Transform; transform: Transform;
nodes: NodeType[]; nodes: NodeType[];
nodeLookup: NodeLookup<InternalNode<NodeType>>; nodeLookup: NodeLookup<InternalNode<NodeType>>;
parentLookup: Map<string, InternalNode<NodeType>[]>; parentLookup: ParentLookup<InternalNode<NodeType>>;
edges: Edge[]; edges: Edge[];
edgeLookup: EdgeLookup<EdgeType>; edgeLookup: EdgeLookup<EdgeType>;
connectionLookup: ConnectionLookup; connectionLookup: ConnectionLookup;
@@ -78,9 +78,9 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
userSelectionActive: boolean; userSelectionActive: boolean;
userSelectionRect: SelectionRect | null; userSelectionRect: SelectionRect | null;
connectionPosition: XYPosition; connection: ConnectionState;
connectionStatus: ConnectionStatus | null;
connectionMode: ConnectionMode; connectionMode: ConnectionMode;
connectionClickStartHandle: (Pick<Handle, 'nodeId' | 'id'> & Required<Pick<Handle, 'type'>>) | null;
snapToGrid: boolean; snapToGrid: boolean;
snapGrid: SnapGrid; snapGrid: SnapGrid;
@@ -89,7 +89,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
nodesConnectable: boolean; nodesConnectable: boolean;
nodesFocusable: boolean; nodesFocusable: boolean;
edgesFocusable: boolean; edgesFocusable: boolean;
edgesUpdatable: boolean; edgesReconnectable: boolean;
elementsSelectable: boolean; elementsSelectable: boolean;
elevateNodesOnSelect: boolean; elevateNodesOnSelect: boolean;
elevateEdgesOnSelect: boolean; elevateEdgesOnSelect: boolean;
@@ -97,10 +97,6 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
multiSelectionActive: boolean; multiSelectionActive: boolean;
connectionStartHandle: ConnectingHandle | null;
connectionEndHandle: ConnectingHandle | null;
connectionClickStartHandle: ConnectingHandle | null;
onNodeDragStart?: OnNodeDrag<NodeType>; onNodeDragStart?: OnNodeDrag<NodeType>;
onNodeDrag?: OnNodeDrag<NodeType>; onNodeDrag?: OnNodeDrag<NodeType>;
onNodeDragStop?: OnNodeDrag<NodeType>; onNodeDragStop?: OnNodeDrag<NodeType>;
+4 -1
View File
@@ -2,5 +2,8 @@
"extends": "@xyflow/tsconfig/react.json", "extends": "@xyflow/tsconfig/react.json",
"display": "@xyflow/react", "display": "@xyflow/react",
"include": ["**/*.ts", "**/*.tsx"], "include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"],
"compilerOptions": {
"outDir": "dist"
}
} }
+28 -4
View File
@@ -1,5 +1,29 @@
# @xyflow/svelte # @xyflow/svelte
## 0.1.8
- `useConnection` returns `ConnectionState` with `toNode` and `toHandle`
- node origin is part of position absolute
- refactor connection handling
## 0.1.7
- revise selection usability (capture while dragging out of the flow)
- only prevent shift scrolling when selection is actually in progress
- use correct end handle position when drawing a connection lines
- determine correct end positions for connection lines
## 0.1.6
- fix node origin bug
## 0.1.5
- prevent zooming on mobile if zoomOnPinch is false
- add straight edge to path built-in-types
- abort drag when multiple touches are detected
- fix merge_styles error
## 0.1.4 ## 0.1.4
- add `selectable`, `deletable` and `draggable` to node and edge props - add `selectable`, `deletable` and `draggable` to node and edge props
@@ -7,7 +31,7 @@
- add `on:edgemouseenter` and `on:edgemouseleave` event handler - add `on:edgemouseenter` and `on:edgemouseleave` event handler
- fix deselection of edges - fix deselection of edges
- remove pointer events from panel when user selection is active - remove pointer events from panel when user selection is active
- fix viewport initialization with user viewport - fix viewport initialization with user viewport
- fix parent node lookup in `evaluateAbsolutePosition`- thanks @lcsfort - fix parent node lookup in `evaluateAbsolutePosition`- thanks @lcsfort
## 0.1.3 ## 0.1.3
@@ -31,16 +55,16 @@ This is a bigger update for Svelte Flow to keep up with the latest changes we ma
- rename `node.computed` to `node.measured` - this attribute only includes `width` and `height` and no `positionAbsolute` anymore. For this we added the helpers `getInternalNode` and `useInternalNode` - rename `node.computed` to `node.measured` - this attribute only includes `width` and `height` and no `positionAbsolute` anymore. For this we added the helpers `getInternalNode` and `useInternalNode`
- rename `node.parentNode` to `node.parentId` - rename `node.parentNode` to `node.parentId`
### More updates: ### More updates:
- add `isValidConnection` for `<Handle />` component - add `isValidConnection` for `<Handle />` component
- add `fitViewOptions` for `<Controls />` component - add `fitViewOptions` for `<Controls />` component
- add `getInternalNode` to `useSvelteFlow` - add `getInternalNode` to `useSvelteFlow`
- add `useInternalNode` hook - add `useInternalNode` hook
- don't reset nodes and edges when svelte flow unmounts - thanks @darabos - don't reset nodes and edges when svelte flow unmounts - thanks @darabos
- fix node event types - thanks @RedPhoenixQ - fix node event types - thanks @RedPhoenixQ
- make handleId and isTarget reactive - thanks @darabos - make handleId and isTarget reactive - thanks @darabos
- fix MiniMap interaction for touch devices - fix MiniMap interaction for touch devices
- fix pane: pinch zoom on windows - fix pane: pinch zoom on windows
- fix nodes: return user node in node event handlers - fix nodes: return user node in node event handlers
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/svelte", "name": "@xyflow/svelte",
"version": "0.1.4", "version": "0.1.7",
"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",
@@ -18,8 +18,8 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"css": "postcss src/styles/{base,style}.css --config ./../../tooling/postcss-config --dir dist", "css": "postcss src/styles/{base,style}.css --config ./../../tooling/postcss-config --dir dist",
"css-watch": "pnpm css --watch", "css-watch": "pnpm css --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .", "lint": "prettier --check . && eslint ./src",
"format": "prettier --plugin-search-dir . --write .", "format": "prettier --write .",
"typecheck": "pnpm check" "typecheck": "pnpm check"
}, },
"type": "module", "type": "module",
@@ -34,7 +34,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
nodeExtent: get(store.nodeExtent), nodeExtent: get(store.nodeExtent),
snapGrid: snapGrid ? snapGrid : [0, 0], snapGrid: snapGrid ? snapGrid : [0, 0],
snapToGrid: !!snapGrid, snapToGrid: !!snapGrid,
nodeOrigin: [0, 0], nodeOrigin: get(store.nodeOrigin),
multiSelectionActive: get(store.multiselectionKeyPressed), multiSelectionActive: get(store.multiselectionKeyPressed),
domNode: get(store.domNode), domNode: get(store.domNode),
transform: [vp.x, vp.y, vp.zoom], transform: [vp.x, vp.y, vp.zoom],
@@ -2,21 +2,59 @@
import cc from 'classcat'; import cc from 'classcat';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import {
ConnectionLineType,
getBezierPath,
getConnectionStatus,
getSmoothStepPath,
getStraightPath
} from '@xyflow/system';
export let containerStyle: string = ''; export let containerStyle: string = '';
export let style: string = ''; export let style: string = '';
export let isCustomComponent: boolean = false; export let isCustomComponent: boolean = false;
const { width, height, connection } = useStore(); const { width, height, connection, connectionLineType } = useStore();
let path: string | null = null;
$: if ($connection.inProgress && !isCustomComponent) {
const { from, to, fromPosition, toPosition } = $connection;
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition
};
switch ($connectionLineType) {
case ConnectionLineType.Bezier:
[path] = getBezierPath(pathParams);
break;
case ConnectionLineType.Step:
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
});
break;
case ConnectionLineType.SmoothStep:
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
}
}
</script> </script>
{#if $connection.path} {#if $connection.inProgress}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}> <svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}>
<g class={cc(['svelte-flow__connection', $connection.status])}> <g class={cc(['svelte-flow__connection', getConnectionStatus($connection.isValid)])}>
<slot name="connectionLine" /> <slot name="connectionLine" />
<!-- slot fallbacks do not work if slots are forwarded in parent --> <!-- slot fallbacks do not work if slots are forwarded in parent -->
{#if !isCustomComponent} {#if !isCustomComponent}
<path d={$connection.path} {style} fill="none" class="svelte-flow__connection-path" /> <path d={path} {style} fill="none" class="svelte-flow__connection-path" />
{/if} {/if}
</g> </g>
</svg> </svg>
@@ -103,7 +103,7 @@
$onConnectEndAction?.(event); $onConnectEndAction?.(event);
}, },
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom], getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
getConnectionStartHandle: () => $connection.startHandle getFromHandle: () => $connection.fromHandle
}); });
} }
} }
@@ -128,21 +128,20 @@
prevConnections = connections ?? new Map(); prevConnections = connections ?? new Map();
} }
$: connectionInProcess = !!$connection.startHandle; $: connectionInProcess = !!$connection.fromHandle;
$: connectingFrom = $: connectingFrom =
$connection.startHandle?.nodeId === nodeId && $connection.fromHandle?.nodeId === nodeId &&
$connection.startHandle?.type === type && $connection.fromHandle?.type === type &&
$connection.startHandle?.handleId === handleId; $connection.fromHandle?.id === handleId;
$: connectingTo = $: connectingTo =
$connection.endHandle?.nodeId === nodeId && $connection.toHandle?.nodeId === nodeId &&
$connection.endHandle?.type === type && $connection.toHandle?.type === type &&
$connection.endHandle?.handleId === handleId; $connection.toHandle?.id === handleId;
$: isPossibleEndHandle = $: isPossibleEndHandle =
$connectionMode === ConnectionMode.Strict $connectionMode === ConnectionMode.Strict
? $connection.startHandle?.type !== type ? $connection.fromHandle?.type !== type
: nodeId !== $connection.startHandle?.nodeId || : nodeId !== $connection.fromHandle?.nodeId || handleId !== $connection.fromHandle?.id;
handleId !== $connection.startHandle?.handleId; $: valid = connectingTo && $connection.isValid;
$: valid = connectingTo && $connection.status === 'valid';
</script> </script>
<!-- <!--
@@ -31,8 +31,6 @@
export let isParent: $$Props['isParent'] = false; export let isParent: $$Props['isParent'] = false;
export let positionX: $$Props['positionX']; export let positionX: $$Props['positionX'];
export let positionY: $$Props['positionY']; export let positionY: $$Props['positionY'];
export let positionOriginX: $$Props['positionOriginX'];
export let positionOriginY: $$Props['positionOriginY'];
export let sourcePosition: $$Props['sourcePosition'] = undefined; export let sourcePosition: $$Props['sourcePosition'] = undefined;
export let targetPosition: $$Props['targetPosition'] = undefined; export let targetPosition: $$Props['targetPosition'] = undefined;
export let zIndex: $$Props['zIndex']; export let zIndex: $$Props['zIndex'];
@@ -181,7 +179,7 @@
class:nopan={draggable} class:nopan={draggable}
class:parent={isParent} class:parent={isParent}
style:z-index={zIndex} style:z-index={zIndex}
style:transform="translate({positionOriginX}px, {positionOriginY}px)" style:transform="translate({positionX}px, {positionY}px)"
style:visibility={initialized ? 'visible' : 'hidden'} style:visibility={initialized ? 'visible' : 'hidden'}
style="{style ?? ''};{inlineStyleDimensions.width}{inlineStyleDimensions.height}" style="{style ?? ''};{inlineStyleDimensions.width}{inlineStyleDimensions.height}"
on:click={onSelectNodeHandler} on:click={onSelectNodeHandler}
@@ -28,8 +28,6 @@ export type NodeWrapperProps = Pick<
type: string; type: string;
positionX: number; positionX: number;
positionY: 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,12 +11,14 @@
export let initialWidth: $$Props['initialWidth'] = undefined; export let initialWidth: $$Props['initialWidth'] = undefined;
export let initialHeight: $$Props['initialHeight'] = undefined; export let initialHeight: $$Props['initialHeight'] = undefined;
export let fitView: $$Props['fitView'] = undefined; export let fitView: $$Props['fitView'] = undefined;
export let nodeOrigin: $$Props['nodeOrigin'] = undefined;
const store = createStore({ const store = createStore({
nodes: initialNodes, nodes: initialNodes,
edges: initialEdges, edges: initialEdges,
width: initialWidth, width: initialWidth,
height: initialHeight, height: initialHeight,
nodeOrigin,
fitView fitView
}); });
@@ -1,4 +1,5 @@
import type { Edge, Node } from '$lib/types'; import type { Edge, Node } from '$lib/types';
import type { NodeOrigin } from '@xyflow/system';
export type SvelteFlowProviderProps = { export type SvelteFlowProviderProps = {
initialNodes?: Node[]; initialNodes?: Node[];
@@ -6,4 +7,5 @@ export type SvelteFlowProviderProps = {
initialWidth?: number; initialWidth?: number;
initialHeight?: number; initialHeight?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}; };
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { NodeProps } from '$lib/types'; import type { NodeProps } from '$lib/types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface $$Props extends NodeProps {} interface $$Props extends NodeProps {}
// this is a workaround for suppressing the warning about unused props // this is a workaround for suppressing the warning about unused props
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { getPositionWithOrigin, getNodeDimensions, nodeHasDimensions } from '@xyflow/system'; import { nodeHasDimensions } from '@xyflow/system';
import { NodeWrapper } from '$lib/components/NodeWrapper'; import { NodeWrapper } from '$lib/components/NodeWrapper';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
@@ -40,13 +40,6 @@
<div class="svelte-flow__nodes"> <div class="svelte-flow__nodes">
{#each $visibleNodes as node (node.id)} {#each $visibleNodes as node (node.id)}
{@const nodeDimesions = getNodeDimensions(node)}
{@const posOrigin = getPositionWithOrigin({
x: node.internals.positionAbsolute.x,
y: node.internals.positionAbsolute.y,
...nodeDimesions,
origin: node.origin
})}
<NodeWrapper <NodeWrapper
{node} {node}
id={node.id} id={node.id}
@@ -65,8 +58,6 @@
deletable={node.deletable ?? true} deletable={node.deletable ?? true}
positionX={node.internals.positionAbsolute.x} positionX={node.internals.positionAbsolute.x}
positionY={node.internals.positionAbsolute.y} positionY={node.internals.positionAbsolute.y}
positionOriginX={posOrigin.x ?? 0}
positionOriginY={posOrigin.y ?? 0}
isParent={$parentLookup.has(node.id)} isParent={$parentLookup.has(node.id)}
style={node.style} style={node.style}
class={node.class} class={node.class}
@@ -75,15 +75,24 @@
$selectionKeyPressed || $selectionRect || (selectionOnDrag && _panOnDrag !== true); $selectionKeyPressed || $selectionRect || (selectionOnDrag && _panOnDrag !== true);
$: hasActiveSelection = $elementsSelectable && (isSelecting || $selectionRectMode === 'user'); $: hasActiveSelection = $elementsSelectable && (isSelecting || $selectionRectMode === 'user');
function onClick(event: MouseEvent | TouchEvent) { // Used to prevent click events when the user lets go of the selectionKey during a selection
dispatch('paneclick', { event }); let selectionInProgress = false;
function onClick(event: MouseEvent | TouchEvent) {
// We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress) {
selectionInProgress = false;
return;
}
dispatch('paneclick', { event });
unselectNodesAndEdges(); unselectNodesAndEdges();
selectionRectMode.set(null); selectionRectMode.set(null);
} }
function onMouseDown(event: MouseEvent) { function onPointerDown(event: PointerEvent) {
containerBounds = container.getBoundingClientRect(); containerBounds = container.getBoundingClientRect();
container.setPointerCapture(event.pointerId);
if ( if (
!elementsSelectable || !elementsSelectable ||
@@ -111,10 +120,13 @@
// onSelectionStart?.(event); // onSelectionStart?.(event);
} }
function onMouseMove(event: MouseEvent) { function onPointerMove(event: PointerEvent) {
if (!isSelecting || !containerBounds || !$selectionRect) { if (!isSelecting || !containerBounds || !$selectionRect) {
return; return;
} }
selectionInProgress = true;
const mousePos = getEventPosition(event, containerBounds); const mousePos = getEventPosition(event, containerBounds);
const startX = $selectionRect.startX ?? 0; const startX = $selectionRect.startX ?? 0;
const startY = $selectionRect.startY ?? 0; const startY = $selectionRect.startY ?? 0;
@@ -157,11 +169,13 @@
selectionRect.set(nextUserSelectRect); selectionRect.set(nextUserSelectRect);
} }
function onMouseUp(event: MouseEvent) { function onPointerUp(event: PointerEvent) {
if (event.button !== 0) { if (event.button !== 0) {
return; return;
} }
container.releasePointerCapture(event.pointerId);
// We only want to trigger click functions when in selection mode if // We only want to trigger click functions when in selection mode if
// the user did not move the mouse. // the user did not move the mouse.
if (!isSelecting && $selectionRectMode === 'user' && event.target === container) { if (!isSelecting && $selectionRectMode === 'user' && event.target === container) {
@@ -173,17 +187,14 @@
$selectionRectMode = 'nodes'; $selectionRectMode = 'nodes';
} }
// onSelectionEnd?.(event); // If the user kept holding the selectionKey during the selection,
} // we need to reset the selectionInProgress, so the next click event is not prevented
if ($selectionKeyPressed) {
const onMouseLeave = () => { selectionInProgress = false;
if ($selectionRectMode === 'user') {
selectionRectMode.set(selectedNodes.length > 0 ? 'nodes' : null);
// onSelectionEnd?.(event);
} }
selectionRect.set(null); // onSelectionEnd?.(event);
}; }
const onContextMenu = (event: MouseEvent) => { const onContextMenu = (event: MouseEvent) => {
if (Array.isArray(_panOnDrag) && _panOnDrag?.includes(2)) { if (Array.isArray(_panOnDrag) && _panOnDrag?.includes(2)) {
@@ -204,10 +215,9 @@
class:dragging={$dragging} class:dragging={$dragging}
class:selection={isSelecting} class:selection={isSelecting}
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)} on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
on:mousedown={hasActiveSelection ? onMouseDown : undefined} on:pointerdown={hasActiveSelection ? onPointerDown : undefined}
on:mousemove={hasActiveSelection ? onMouseMove : undefined} on:pointermove={hasActiveSelection ? onPointerMove : undefined}
on:mouseup={hasActiveSelection ? onMouseUp : undefined} on:pointerup={hasActiveSelection ? onPointerUp : undefined}
on:mouseleave={hasActiveSelection ? onMouseLeave : undefined}
on:contextmenu={wrapHandler(onContextMenu, container)} on:contextmenu={wrapHandler(onContextMenu, container)}
> >
<slot /> <slot />
@@ -19,7 +19,7 @@
<div <div
class={cc(['svelte-flow__panel', className, ...positionClasses])} class={cc(['svelte-flow__panel', className, ...positionClasses])}
{style} {style}
style:pointer-events={$selectionRectMode ? 'none' : undefined} style:pointer-events={$selectionRectMode ? 'none' : ''}
{...$$restProps} {...$$restProps}
> >
<slot /> <slot />
@@ -78,6 +78,7 @@
export let onconnectend: $$Props['onconnectend'] = undefined; export let onconnectend: $$Props['onconnectend'] = undefined;
export let onbeforedelete: $$Props['onbeforedelete'] = undefined; export let onbeforedelete: $$Props['onbeforedelete'] = undefined;
export let oninit: $$Props['oninit'] = undefined; export let oninit: $$Props['oninit'] = undefined;
export let nodeOrigin: $$Props['nodeOrigin'] = undefined;
export let defaultMarkerColor = '#b1b1b7'; export let defaultMarkerColor = '#b1b1b7';
@@ -93,7 +94,14 @@
const store = hasContext(key) const store = hasContext(key)
? useStore() ? useStore()
: createStoreContext({ nodes: get(nodes), edges: get(edges), width, height, fitView }); : createStoreContext({
nodes: get(nodes),
edges: get(edges),
width,
height,
fitView,
nodeOrigin
});
onMount(() => { onMount(() => {
store.width.set(clientWidth); store.width.set(clientWidth);
@@ -168,7 +176,8 @@
onconnect, onconnect,
onconnectstart, onconnectstart,
onconnectend, onconnectend,
onbeforedelete onbeforedelete,
nodeOrigin
}; };
updateStoreByKeys(store, updatableProps); updateStoreByKeys(store, updatableProps);
@@ -70,6 +70,7 @@ export type UpdatableStoreProps = {
onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>; onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>;
onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>; onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>;
onbeforedelete?: UnwrapWritable<SvelteFlowStore['onbeforedelete']>; onbeforedelete?: UnwrapWritable<SvelteFlowStore['onbeforedelete']>;
nodeOrigin?: UnwrapWritable<SvelteFlowStore['nodeOrigin']>;
}; };
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) { export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
@@ -23,7 +23,7 @@
const { const {
viewport, viewport,
panZoom, panZoom,
selectionKeyPressed, selectionRect,
minZoom, minZoom,
maxZoom, maxZoom,
dragging, dragging,
@@ -66,7 +66,7 @@
preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true, preventScrolling: typeof preventScrolling === 'boolean' ? preventScrolling : true,
noPanClassName: 'nopan', noPanClassName: 'nopan',
noWheelClassName: 'nowheel', noWheelClassName: 'nowheel',
userSelectionActive: $selectionKeyPressed, userSelectionActive: !!$selectionRect,
translateExtent: $translateExtent, translateExtent: $translateExtent,
lib: $lib lib: $lib
}} }}
@@ -1,7 +1,7 @@
import type { Readable } from 'svelte/store'; import type { Readable } from 'svelte/store';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { ConnectionProps } from '$lib/store/derived-connection-props';
import type { ConnectionState } from '@xyflow/system';
/** /**
* Hook for receiving the current connection. * Hook for receiving the current connection.
@@ -9,7 +9,7 @@ import type { ConnectionProps } from '$lib/store/derived-connection-props';
* @public * @public
* @returns current connection as a readable store * @returns current connection as a readable store
*/ */
export function useConnection(): Readable<ConnectionProps> { export function useConnection(): Readable<ConnectionState> {
const { connection } = useStore(); const { connection } = useStore();
return connection; return connection;
+1 -3
View File
@@ -70,7 +70,6 @@ export {
type OnMoveStart, type OnMoveStart,
type OnMoveEnd, type OnMoveEnd,
type Connection, type Connection,
type ConnectionStatus,
ConnectionMode, ConnectionMode,
type OnConnectStartParams, type OnConnectStartParams,
type OnConnectStart, type OnConnectStart,
@@ -124,6 +123,5 @@ export {
getIncomers, getIncomers,
getOutgoers, getOutgoers,
getConnectedEdges, getConnectedEdges,
addEdge, addEdge
updateEdge
} from '@xyflow/system'; } from '@xyflow/system';
@@ -11,7 +11,6 @@
getBoundsOfRects, getBoundsOfRects,
getInternalNodesBounds, getInternalNodesBounds,
getNodeDimensions, getNodeDimensions,
getNodePositionWithOrigin,
nodeHasDimensions, nodeHasDimensions,
type Rect type Rect
} from '@xyflow/system'; } from '@xyflow/system';
@@ -134,11 +133,10 @@
{#each $nodes as userNode (userNode.id)} {#each $nodes as userNode (userNode.id)}
{@const node = $nodeLookup.get(userNode.id)} {@const node = $nodeLookup.get(userNode.id)}
{#if node && nodeHasDimensions(node)} {#if node && nodeHasDimensions(node)}
{@const pos = getNodePositionWithOrigin(node).positionAbsolute}
{@const nodeDimesions = getNodeDimensions(node)} {@const nodeDimesions = getNodeDimensions(node)}
<MinimapNode <MinimapNode
x={pos.x} x={node.internals.positionAbsolute.x}
y={pos.y} y={node.internals.positionAbsolute.y}
{...nodeDimesions} {...nodeDimesions}
selected={node.selected} selected={node.selected}
color={nodeColorFunc?.(node)} color={nodeColorFunc?.(node)}
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { getNodesBounds, Position, type Rect, getNodeToolbarTransform } from '@xyflow/system'; import { getNodesBounds, Position, getNodeToolbarTransform } from '@xyflow/system';
import portal from '$lib/actions/portal'; import portal from '$lib/actions/portal';
import type { InternalNode } from '$lib/types'; import type { InternalNode } from '$lib/types';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
@@ -1,128 +0,0 @@
import { derived, type Writable } from 'svelte/store';
import {
getBezierPath,
getSmoothStepPath,
getStraightPath,
ConnectionLineType,
ConnectionMode,
Position,
type HandleElement
} from '@xyflow/system';
import type { SvelteFlowStoreState } from './types';
import type { ConnectionData } from '$lib/types';
export type ConnectionProps = {
path: string | null;
sourceX: number | null;
sourceY: number | null;
sourcePosition: Position | undefined | null;
targetX: number | null;
targetY: number | null;
targetPosition: Position | undefined | null;
pointerPosition: ConnectionData['connectionPosition'] | null;
startHandle: ConnectionData['connectionStartHandle'] | null;
endHandle: ConnectionData['connectionEndHandle'] | null;
status: ConnectionData['connectionStatus'] | null;
};
export const initConnectionProps = {
path: null,
sourceX: null,
sourceY: null,
sourcePosition: null,
targetX: null,
targetY: null,
targetPosition: null,
pointerPosition: null,
startHandle: null,
endHandle: null,
status: null
};
const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top
};
export function getDerivedConnectionProps(
store: SvelteFlowStoreState,
currentConnection: Writable<ConnectionData>
) {
return derived(
[
currentConnection,
store.connectionLineType,
store.connectionMode,
store.nodeLookup,
store.viewport
],
([connection, connectionLineType, connectionMode, nodeLookup, viewport]) => {
if (!connection.connectionStartHandle?.nodeId) {
return initConnectionProps;
}
// TODO: it should bail out if the node is not found
const fromNode = nodeLookup.get(connection.connectionStartHandle?.nodeId);
const fromHandleBounds = fromNode?.internals.handleBounds;
const handleBoundsStrict =
fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || [];
const handleBoundsLoose: HandleElement[] | undefined | null = handleBoundsStrict
? handleBoundsStrict
: fromHandleBounds?.[
connection?.connectionStartHandle?.type === 'source' ? 'target' : 'source'
];
const handleBounds =
connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = connection.connectionStartHandle?.handleId
? handleBounds?.find((d) => d.id === connection.connectionStartHandle?.handleId)
: handleBounds?.[0];
const fromHandleX = fromHandle
? fromHandle.x + fromHandle.width / 2
: (fromNode?.measured.width ?? 0) / 2;
const fromHandleY = fromHandle
? fromHandle.y + fromHandle.height / 2
: fromNode?.measured.height ?? 0;
const fromX = (fromNode?.internals.positionAbsolute.x ?? 0) + fromHandleX;
const fromY = (fromNode?.internals.positionAbsolute.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined;
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: ((connection.connectionPosition?.x ?? 0) - viewport.x) / viewport.zoom,
targetY: ((connection.connectionPosition?.y ?? 0) - viewport.y) / viewport.zoom,
targetPosition: toPosition
};
let path = '';
if (connectionLineType === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[path] = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) {
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
});
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
[path] = getSmoothStepPath(pathParams);
} else {
[path] = getStraightPath(pathParams);
}
return {
path,
...pathParams,
pointerPosition: connection.connectionPosition,
startHandle: connection.connectionStartHandle,
endHandle: connection.connectionEndHandle,
status: connection.connectionStatus
};
}
);
}
+35 -23
View File
@@ -7,6 +7,9 @@ import {
panBy as panBySystem, panBy as panBySystem,
updateNodeInternals as updateNodeInternalsSystem, updateNodeInternals as updateNodeInternalsSystem,
addEdge as addEdgeUtil, addEdge as addEdgeUtil,
initialConnection,
errorMessages,
pointToRendererPoint,
type UpdateNodePositions, type UpdateNodePositions,
type InternalNodeUpdate, type InternalNodeUpdate,
type ViewportHelperFunctionOptions, type ViewportHelperFunctionOptions,
@@ -14,16 +17,16 @@ import {
type XYPosition, type XYPosition,
type CoordinateExtent, type CoordinateExtent,
type UpdateConnection, type UpdateConnection,
errorMessages type ConnectionState,
type NodeOrigin
} from '@xyflow/system'; } from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData } from '$lib/types'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
import type { SvelteFlowStore } from './types'; import type { SvelteFlowStore } from './types';
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils'; import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
import { getVisibleEdges } from './visible-edges'; import { getVisibleEdges } from './visible-edges';
import { getVisibleNodes } from './visible-nodes'; import { getVisibleNodes } from './visible-nodes';
import { getDerivedConnectionProps } from './derived-connection-props';
export const key = Symbol(); export const key = Symbol();
@@ -32,15 +35,24 @@ export function createStore({
edges, edges,
width, width,
height, height,
fitView: fitViewOnCreate fitView: fitViewOnCreate,
nodeOrigin
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}): SvelteFlowStore { }): SvelteFlowStore {
const store = getInitialStore({ nodes, edges, width, height, fitView: fitViewOnCreate }); const store = getInitialStore({
nodes,
edges,
width,
height,
fitView: fitViewOnCreate,
nodeOrigin
});
function setNodeTypes(nodeTypes: NodeTypes) { function setNodeTypes(nodeTypes: NodeTypes) {
store.nodeTypes.set({ store.nodeTypes.set({
@@ -122,6 +134,7 @@ export function createStore({
} }
case 'position': case 'position':
node.position = change.position ?? node.position; node.position = change.position ?? node.position;
break; break;
} }
} }
@@ -147,8 +160,7 @@ export function createStore({
height: get(store.height), height: get(store.height),
minZoom: get(store.minZoom), minZoom: get(store.minZoom),
maxZoom: get(store.maxZoom), maxZoom: get(store.maxZoom),
panZoom, panZoom
nodeOrigin: get(store.nodeOrigin)
}, },
options options
); );
@@ -330,22 +342,13 @@ export function createStore({
}); });
} }
const initConnectionUpdateData = { const _connection = writable<ConnectionState>(initialConnection);
connectionStartHandle: null, const updateConnection: UpdateConnection = (newConnection: ConnectionState) => {
connectionEndHandle: null, _connection.set({ ...newConnection });
connectionPosition: null,
connectionStatus: null
};
// by creating an internal, unexposed store and using a derived store
// we prevent using slow get() calls
const currentConnection = writable<ConnectionData>(initConnectionUpdateData);
const updateConnection: UpdateConnection = (newConnection: ConnectionData) => {
currentConnection.set(newConnection);
}; };
function cancelConnection() { function cancelConnection() {
updateConnection(initConnectionUpdateData); _connection.set(initialConnection);
} }
function reset() { function reset() {
@@ -364,9 +367,16 @@ export function createStore({
...store, ...store,
// derived state // derived state
connection: getDerivedConnectionProps(store, currentConnection),
visibleEdges: getVisibleEdges(store), visibleEdges: getVisibleEdges(store),
visibleNodes: getVisibleNodes(store), visibleNodes: getVisibleNodes(store),
connection: derived([_connection, store.viewport], ([connection, viewport]) => {
return connection.inProgress
? {
...connection,
to: pointToRendererPoint(connection.to, [viewport.x, viewport.y, viewport.zoom])
}
: { ...connection };
}),
markers: derived( markers: derived(
[store.edges, store.defaultMarkerColor, store.flowId], [store.edges, store.defaultMarkerColor, store.flowId],
([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id }) ([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id })
@@ -438,15 +448,17 @@ export function createStoreContext({
edges, edges,
width, width,
height, height,
fitView fitView,
nodeOrigin
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}) { }) {
const store = createStore({ nodes, edges, width, height, fitView }); const store = createStore({ nodes, edges, width, height, fitView, nodeOrigin });
setContext(key, { setContext(key, {
getStore: () => store getStore: () => store
+17 -15
View File
@@ -6,9 +6,9 @@ import {
ConnectionLineType, ConnectionLineType,
devWarn, devWarn,
adoptUserNodes, adoptUserNodes,
getNodesBounds,
getViewportForBounds, getViewportForBounds,
updateConnectionLookup, updateConnectionLookup,
initialConnection,
type SelectionRect, type SelectionRect,
type SnapGrid, type SnapGrid,
type MarkerProps, type MarkerProps,
@@ -22,7 +22,10 @@ import {
type OnConnectStart, type OnConnectStart,
type OnConnectEnd, type OnConnectEnd,
type NodeLookup, type NodeLookup,
type EdgeLookup type EdgeLookup,
type ConnectionState,
type ParentLookup,
getInternalNodesBounds
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -51,7 +54,6 @@ import type {
InternalNode InternalNode
} from '$lib/types'; } from '$lib/types';
import { createNodesStore, createEdgesStore } from './utils'; import { createNodesStore, createEdgesStore } from './utils';
import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
export const initialNodeTypes = { export const initialNodeTypes = {
input: InputNode, input: InputNode,
@@ -72,18 +74,21 @@ export const getInitialStore = ({
edges = [], edges = [],
width, width,
height, height,
fitView fitView,
nodeOrigin
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}) => { }) => {
const nodeLookup: NodeLookup = new Map(); const nodeLookup: NodeLookup = new Map();
const parentLookup = new Map(); const parentLookup = new Map();
const storeNodeOrigin = nodeOrigin ?? [0, 0];
adoptUserNodes(nodes, nodeLookup, parentLookup, { adoptUserNodes(nodes, nodeLookup, parentLookup, {
nodeOrigin: [0, 0], nodeOrigin: storeNodeOrigin,
elevateNodesOnSelect: false, elevateNodesOnSelect: false,
checkEquality: false checkEquality: false
}); });
@@ -94,20 +99,17 @@ export const getInitialStore = ({
let viewport: Viewport = { x: 0, y: 0, zoom: 1 }; let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
if (fitView && width && height) { if (fitView && width && height) {
const nodesWithDimensions = nodes.filter( const bounds = getInternalNodesBounds(nodeLookup, {
(node) => (node.width && node.height) || (node.initialWidth && node.initialHeight) filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight))
); });
// @todo users nodeOrigin should be used here
const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] });
viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
} }
return { return {
flowId: writable<string | null>(null), flowId: writable<string | null>(null),
nodes: createNodesStore(nodes, nodeLookup, parentLookup), nodes: createNodesStore(nodes, nodeLookup, parentLookup, storeNodeOrigin),
nodeLookup: readable<NodeLookup<InternalNode>>(nodeLookup), nodeLookup: readable<NodeLookup<InternalNode>>(nodeLookup),
parentLookup: readable<Map<string, InternalNode[]>>(parentLookup), parentLookup: readable<ParentLookup<InternalNode>>(parentLookup),
edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup), edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup),
visibleNodes: readable<InternalNode[]>([]), visibleNodes: readable<InternalNode[]>([]),
edges: createEdgesStore(edges, connectionLookup, edgeLookup), edges: createEdgesStore(edges, connectionLookup, edgeLookup),
@@ -117,7 +119,7 @@ export const getInitialStore = ({
width: writable<number>(500), width: writable<number>(500),
minZoom: writable<number>(0.5), minZoom: writable<number>(0.5),
maxZoom: writable<number>(2), maxZoom: writable<number>(2),
nodeOrigin: writable<NodeOrigin>([0, 0]), nodeOrigin: writable<NodeOrigin>(storeNodeOrigin),
nodeDragThreshold: writable<number>(1), nodeDragThreshold: writable<number>(1),
nodeExtent: writable<CoordinateExtent>(infiniteExtent), nodeExtent: writable<CoordinateExtent>(infiniteExtent),
translateExtent: writable<CoordinateExtent>(infiniteExtent), translateExtent: writable<CoordinateExtent>(infiniteExtent),
@@ -142,7 +144,7 @@ export const getInitialStore = ({
viewport: writable<Viewport>(viewport), viewport: writable<Viewport>(viewport),
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict), connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
domNode: writable<HTMLDivElement | null>(null), domNode: writable<HTMLDivElement | null>(null),
connection: readable<ConnectionProps>(initConnectionProps), connection: readable<ConnectionState>(initialConnection),
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier), connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier),
connectionRadius: writable<number>(20), connectionRadius: writable<number>(20),
isValidConnection: writable<IsValidConnection>(() => true), isValidConnection: writable<IsValidConnection>(() => true),
+6 -2
View File
@@ -13,7 +13,9 @@ import {
type PanZoomInstance, type PanZoomInstance,
type ConnectionLookup, type ConnectionLookup,
type EdgeLookup, type EdgeLookup,
type NodeLookup type NodeLookup,
type ParentLookup,
type NodeOrigin
} from '@xyflow/system'; } from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, InternalNode, Node } from '$lib/types'; import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, InternalNode, Node } from '$lib/types';
@@ -128,7 +130,8 @@ export type NodeStoreOptions = {
export const createNodesStore = ( export const createNodesStore = (
nodes: Node[], nodes: Node[],
nodeLookup: NodeLookup<InternalNode>, nodeLookup: NodeLookup<InternalNode>,
parentLookup: Map<string, InternalNode[]> parentLookup: ParentLookup<InternalNode>,
nodeOrigin: NodeOrigin = [0, 0]
): { ): {
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber; subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
update: (this: void, updater: Updater<Node[]>) => void; update: (this: void, updater: Updater<Node[]>) => void;
@@ -144,6 +147,7 @@ export const createNodesStore = (
const _set = (nds: Node[]): Node[] => { const _set = (nds: Node[]): Node[] => {
adoptUserNodes(nds, nodeLookup, parentLookup, { adoptUserNodes(nds, nodeLookup, parentLookup, {
elevateNodesOnSelect, elevateNodesOnSelect,
nodeOrigin,
defaults, defaults,
checkEquality: false checkEquality: false
}); });
+6 -1
View File
@@ -44,7 +44,12 @@ type StepEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>
pathOptions?: StepPathOptions; pathOptions?: StepPathOptions;
}; };
export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge; type StraightEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>> = Edge<
EdgeData,
'straight'
>;
export type BuiltInEdge = SmoothStepEdge | BezierEdge | StepEdge | StraightEdge;
/** /**
* Custom edge component props. * Custom edge component props.
+3 -3
View File
@@ -2,7 +2,7 @@ import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
import type { import type {
FitViewOptionsBase, FitViewOptionsBase,
XYPosition, XYPosition,
ConnectingHandle, Handle,
Connection, Connection,
OnBeforeDeleteBase, OnBeforeDeleteBase,
HandleProps as HandlePropsSystem HandleProps as HandlePropsSystem
@@ -17,8 +17,8 @@ export type KeyDefinition = string | KeyDefinitionObject;
export type ConnectionData = { export type ConnectionData = {
connectionPosition: XYPosition | null; connectionPosition: XYPosition | null;
connectionStartHandle: ConnectingHandle | null; connectionStartHandle: Handle | null;
connectionEndHandle: ConnectingHandle | null; connectionEndHandle: Handle | null;
connectionStatus: string | null; connectionStatus: string | null;
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.27", "version": "0.0.30",
"description": "xyflow core system that powers React Flow and Svelte Flow.", "description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [ "keywords": [
"node-based UI", "node-based UI",
+49 -12
View File
@@ -6,9 +6,9 @@ import type { ZoomBehavior } from 'd3-zoom';
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { Transition } from 'd3-transition'; import type { Transition } from 'd3-transition';
import type { XYPosition, Rect } from './utils'; import type { XYPosition, Rect, Position } from './utils';
import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes'; import type { InternalNodeBase, NodeBase, NodeDragItem } from './nodes';
import type { ConnectingHandle, HandleType } from './handles'; import type { Handle, HandleType } from './handles';
import { PanZoomInstance } from './panzoom'; import { PanZoomInstance } from './panzoom';
import { EdgeBase } from '..'; import { EdgeBase } from '..';
@@ -37,8 +37,6 @@ export type HandleConnection = Connection & {
edgeId: string; edgeId: string;
}; };
export type ConnectionStatus = 'valid' | 'invalid';
export enum ConnectionMode { export enum ConnectionMode {
Strict = 'strict', Strict = 'strict',
Loose = 'loose', Loose = 'loose',
@@ -63,7 +61,6 @@ export type FitViewParamsBase<NodeType extends NodeBase> = {
panZoom: PanZoomInstance; panZoom: PanZoomInstance;
minZoom: number; minZoom: number;
maxZoom: number; maxZoom: number;
nodeOrigin?: NodeOrigin;
}; };
export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = { export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
@@ -135,12 +132,52 @@ export type OnError = (id: string, message: string) => void;
export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void; export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void;
export type PanBy = (delta: XYPosition) => boolean; export type PanBy = (delta: XYPosition) => boolean;
export type UpdateConnection = (params: { export const initialConnection: NoConnection = {
connectionPosition: XYPosition | null; inProgress: false,
connectionStatus: ConnectionStatus | null; isValid: null,
connectionStartHandle: ConnectingHandle | null; from: null,
connectionEndHandle: ConnectingHandle | null; fromHandle: null,
}) => void; fromPosition: null,
fromNode: null,
to: null,
toHandle: null,
toPosition: null,
toNode: null,
};
export type NoConnection = {
inProgress: false;
isValid: null;
from: null;
fromHandle: null;
fromPosition: null;
fromNode: null;
to: null;
toHandle: null;
toPosition: null;
toNode: null;
};
export type ConnectionInProgress = {
inProgress: true;
isValid: boolean | null;
from: XYPosition;
fromHandle: Handle;
fromPosition: Position;
fromNode: NodeBase;
to: XYPosition;
toHandle: Handle | null;
toPosition: Position;
toNode: NodeBase | null;
};
export type ConnectionState = ConnectionInProgress | NoConnection;
export type UpdateConnection = (params: ConnectionState) => void;
export type ColorModeClass = 'light' | 'dark'; export type ColorModeClass = 'light' | 'dark';
export type ColorMode = ColorModeClass | 'system'; export type ColorMode = ColorModeClass | 'system';
+4 -17
View File
@@ -2,28 +2,15 @@ import type { Position, IsValidConnection } from '.';
export type HandleType = 'source' | 'target'; export type HandleType = 'source' | 'target';
export type HandleElement = { export type Handle = {
id?: string | null; id?: string | null;
nodeId: string;
x: number; x: number;
y: number; y: number;
position: Position;
type: HandleType;
width: number; width: number;
height: number; height: number;
position: Position;
type?: HandleType;
};
export type ConnectingHandle = {
nodeId: string;
type: HandleType;
handleId?: string | null;
};
export type ConnectionHandle = {
id: string | null;
type: HandleType;
nodeId: string;
x: number;
y: number;
}; };
export type HandleProps = { export type HandleProps = {
+6 -5
View File
@@ -1,4 +1,4 @@
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.'; import type { XYPosition, Position, CoordinateExtent, Handle } from '.';
import { Optional } from '../utils/types'; import { Optional } from '../utils/types';
/** /**
@@ -77,6 +77,7 @@ export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType &
* Used as an optimization to avoid certain operations. */ * Used as an optimization to avoid certain operations. */
userNode: NodeType; userNode: NodeType;
handleBounds?: NodeHandleBounds; handleBounds?: NodeHandleBounds;
bounds?: NodeBounds;
}; };
}; };
@@ -110,8 +111,8 @@ export type NodeProps<NodeType extends NodeBase> = Pick<
}; };
export type NodeHandleBounds = { export type NodeHandleBounds = {
source: HandleElement[] | null; source: Handle[] | null;
target: HandleElement[] | null; target: Handle[] | null;
}; };
export type InternalNodeUpdate = { export type InternalNodeUpdate = {
@@ -148,9 +149,9 @@ export type NodeOrigin = [number, number];
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void; export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>; export type NodeHandle = Omit<Optional<Handle, 'width' | 'height'>, 'nodeId'>;
export type Align = 'center' | 'start' | 'end'; export type Align = 'center' | 'start' | 'end';
export type NodeLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType>; export type NodeLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType>;
export type ParentLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType[]>; export type ParentLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, Map<string, NodeType>>;
+7
View File
@@ -5,6 +5,13 @@ export enum Position {
Bottom = 'bottom', Bottom = 'bottom',
} }
export const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top,
};
export type XYPosition = { export type XYPosition = {
x: number; x: number;
y: number; y: number;
+4
View File
@@ -51,3 +51,7 @@ export function handleConnectionChange(
cb(diff); cb(diff);
} }
} }
export function getConnectionStatus(isValid: boolean | null) {
return isValid === null ? null : isValid ? 'valid' : 'invalid';
}
+12 -18
View File
@@ -1,4 +1,4 @@
import type { Transform, XYPosition, SnapGrid, Dimensions, NodeOrigin, HandleElement, Position } from '../types'; import type { Transform, XYPosition, SnapGrid, Dimensions, Position, Handle } from '../types';
import { snapPosition, pointToRendererPoint } from './general'; import { snapPosition, pointToRendererPoint } from './general';
export type GetPointerPositionParams = { export type GetPointerPositionParams = {
@@ -59,35 +59,29 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec
// We store them in the internals object of the node in order to avoid // We store them in the internals object of the node in order to avoid
// unnecessary recalculations. // unnecessary recalculations.
export const getHandleBounds = ( export const getHandleBounds = (
selector: string, type: 'source' | 'target',
nodeElement: HTMLDivElement, nodeElement: HTMLDivElement,
nodeBounds: DOMRect,
zoom: number, zoom: number,
nodeOrigin: NodeOrigin = [0, 0] nodeId: string
): HandleElement[] | null => { ): Handle[] | null => {
const handles = nodeElement.querySelectorAll(selector); const handles = nodeElement.querySelectorAll(`.${type}`);
if (!handles || !handles.length) { if (!handles || !handles.length) {
return null; return null;
} }
const handlesArray = Array.from(handles) as HTMLDivElement[]; return Array.from(handles).map((handle): Handle => {
// @todo can't we use the node dimensions here?
const nodeBounds = nodeElement.getBoundingClientRect();
const nodeOffset = {
x: nodeBounds.width * nodeOrigin[0],
y: nodeBounds.height * nodeOrigin[1],
};
return handlesArray.map((handle): HandleElement => {
const handleBounds = handle.getBoundingClientRect(); const handleBounds = handle.getBoundingClientRect();
return { return {
id: handle.getAttribute('data-handleid'), id: handle.getAttribute('data-handleid'),
type,
nodeId,
position: handle.getAttribute('data-handlepos') as unknown as Position, position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (handleBounds.left - nodeBounds.left - nodeOffset.x) / zoom, x: (handleBounds.left - nodeBounds.left) / zoom,
y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom, y: (handleBounds.top - nodeBounds.top) / zoom,
...getDimensions(handle), ...getDimensions(handle as HTMLDivElement),
}; };
}); });
}; };
+4 -4
View File
@@ -132,23 +132,23 @@ export const addEdge = <EdgeType extends EdgeBase>(
return edges.concat(edge); return edges.concat(edge);
}; };
export type UpdateEdgeOptions = { export type ReconnectEdgeOptions = {
shouldReplaceId?: boolean; shouldReplaceId?: boolean;
}; };
/** /**
* A handy utility to update an existing Edge with new properties * A handy utility to reconnect an existing edge with new properties
* @param oldEdge - The edge you want to update * @param oldEdge - The edge you want to update
* @param newConnection - The new connection you want to update the edge with * @param newConnection - The new connection you want to update the edge with
* @param edges - The array of all current edges * @param edges - The array of all current edges
* @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id * @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id
* @returns the updated edges array * @returns the updated edges array
*/ */
export const updateEdge = <EdgeType extends EdgeBase>( export const reconnectEdge = <EdgeType extends EdgeBase>(
oldEdge: EdgeType, oldEdge: EdgeType,
newConnection: Connection, newConnection: Connection,
edges: EdgeType[], edges: EdgeType[],
options: UpdateEdgeOptions = { shouldReplaceId: true } options: ReconnectEdgeOptions = { shouldReplaceId: true }
): EdgeType[] => { ): EdgeType[] => {
const { id: oldEdgeId, ...rest } = oldEdge; const { id: oldEdgeId, ...rest } = oldEdge;
+29 -18
View File
@@ -1,9 +1,9 @@
import { EdgePosition } from '../../types/edges'; import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general'; import { ConnectionMode, OnError } from '../../types/general';
import { InternalNodeBase, NodeHandle } from '../../types/nodes'; import { InternalNodeBase, NodeHandle } from '../../types/nodes';
import { Position } from '../../types/utils'; import { Position, XYPosition } from '../../types/utils';
import { errorMessages } from '../../constants'; import { errorMessages } from '../../constants';
import { HandleElement } from '../../types'; import { Handle } from '../../types';
import { getNodeDimensions } from '../general'; import { getNodeDimensions } from '../general';
export type GetEdgePositionParams = { export type GetEdgePositionParams = {
@@ -42,8 +42,6 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
: (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []), : (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []),
params.targetHandle params.targetHandle
); );
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle || !targetHandle) { if (!sourceHandle || !targetHandle) {
params.onError?.( params.onError?.(
@@ -58,14 +56,16 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
return null; return null;
} }
const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle); const sourcePosition = sourceHandle?.position || Position.Bottom;
const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle); const targetPosition = targetHandle?.position || Position.Top;
const source = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
const target = getHandlePosition(targetNode, targetHandle, targetPosition);
return { return {
sourceX, sourceX: source.x,
sourceY, sourceY: source.y,
targetX, targetX: target.x,
targetY, targetY: target.y,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
}; };
@@ -84,9 +84,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
handle.height = handle.height ?? 1; handle.height = handle.height ?? 1;
if (handle.type === 'source') { if (handle.type === 'source') {
source.push(handle as HandleElement); source.push(handle as Handle);
} else if (handle.type === 'target') { } else if (handle.type === 'target') {
target.push(handle as HandleElement); target.push(handle as Handle);
} }
} }
@@ -96,24 +96,35 @@ function toHandleBounds(handles?: NodeHandle[]) {
}; };
} }
function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] { export function getHandlePosition(
node: InternalNodeBase,
handle: Handle | null,
fallbackPosition: Position = Position.Left,
center = false
): XYPosition {
const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x; const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x;
const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y; const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y;
const { width, height } = handle ?? getNodeDimensions(node); const { width, height } = handle ?? getNodeDimensions(node);
if (center) {
return { x: x + width / 2, y: y + height / 2 };
}
const position = handle?.position ?? fallbackPosition;
switch (position) { switch (position) {
case Position.Top: case Position.Top:
return [x + width / 2, y]; return { x: x + width / 2, y };
case Position.Right: case Position.Right:
return [x + width, y + height / 2]; return { x: x + width, y: y + height / 2 };
case Position.Bottom: case Position.Bottom:
return [x + width / 2, y + height]; return { x: x + width / 2, y: y + height };
case Position.Left: case Position.Left:
return [x, y + height / 2]; return { x, y: y + height / 2 };
} }
} }
function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null { function getHandle(bounds: Handle[], handleId?: string | null): Handle | null {
if (!bounds) { if (!bounds) {
return null; return null;
} }
+15 -35
View File
@@ -12,7 +12,7 @@ import type {
NodeLookup, NodeLookup,
} from '../types'; } from '../types';
import { type Viewport } from '../types'; import { type Viewport } from '../types';
import { getNodePositionWithOrigin } from './graph'; import { getNodePositionWithOrigin, isInternalNodeBase } from './graph';
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max); export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
@@ -68,24 +68,28 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
}); });
export const nodeToRect = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => { export const nodeToRect = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
const { x, y } = getNodePositionWithOrigin(node, nodeOrigin).positionAbsolute; const { x, y } = isInternalNodeBase(node)
? node.internals.positionAbsolute
: getNodePositionWithOrigin(node, nodeOrigin);
return { return {
x, x,
y, y,
width: node.measured?.width ?? node.width ?? 0, width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
height: node.measured?.height ?? node.height ?? 0, height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,
}; };
}; };
export const nodeToBox = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => { export const nodeToBox = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
const { x, y } = getNodePositionWithOrigin(node, nodeOrigin).positionAbsolute; const { x, y } = isInternalNodeBase(node)
? node.internals.positionAbsolute
: getNodePositionWithOrigin(node, nodeOrigin);
return { return {
x, x,
y, y,
x2: x + (node.measured?.width ?? node.width ?? 0), x2: x + (node.measured?.width ?? node.width ?? node.initialWidth ?? 0),
y2: y + (node.measured?.height ?? node.height ?? 0), y2: y + (node.measured?.height ?? node.height ?? node.initialHeight ?? 0),
}; };
}; };
@@ -114,29 +118,6 @@ export const devWarn = (id: string, message: string) => {
} }
}; };
export const getPositionWithOrigin = ({
x,
y,
width,
height,
origin = [0, 0],
}: {
x: number;
y: number;
width: number;
height: number;
origin?: NodeOrigin;
}): XYPosition => {
if (!width || !height || origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
return { x, y };
}
return {
x: x - width * origin[0],
y: y - height * origin[1],
};
};
export const snapPosition = (position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition => { export const snapPosition = (position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition => {
return { return {
x: snapGrid[0] * Math.round(position.x / snapGrid[0]), x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
@@ -239,9 +220,10 @@ export function nodeHasDimensions<NodeType extends NodeBase = NodeBase>(node: No
*/ */
export function evaluateAbsolutePosition( export function evaluateAbsolutePosition(
position: XYPosition, position: XYPosition,
dimensions: { width?: number; height?: number } = { width: 0, height: 0 },
parentId: string, parentId: string,
nodeLookup: NodeLookup, nodeLookup: NodeLookup,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin
): XYPosition { ): XYPosition {
let nextParentId: string | undefined = parentId; let nextParentId: string | undefined = parentId;
const positionAbsolute = { ...position }; const positionAbsolute = { ...position };
@@ -252,10 +234,8 @@ export function evaluateAbsolutePosition(
if (parent) { if (parent) {
const origin = parent.origin || nodeOrigin; const origin = parent.origin || nodeOrigin;
const xOffset = (parent.measured.width ?? 0) * origin[0]; positionAbsolute.x += parent.internals.positionAbsolute.x - (dimensions.width ?? 0) * origin[0];
const yOffset = (parent.measured.height ?? 0) * origin[1]; positionAbsolute.y += parent.internals.positionAbsolute.y - (dimensions.height ?? 0) * origin[1];
positionAbsolute.x += parent.position.x - xOffset;
positionAbsolute.y += parent.position.y - yOffset;
} }
} }
+21 -39
View File
@@ -106,25 +106,15 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
return nodes.filter((n) => incomersIds.has(n.id)); return nodes.filter((n) => incomersIds.has(n.id));
}; };
export const getNodePositionWithOrigin = ( export const getNodePositionWithOrigin = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): XYPosition => {
node: InternalNodeBase | NodeBase,
nodeOrigin: NodeOrigin = [0, 0]
): { position: XYPosition; positionAbsolute: XYPosition } => {
const { width, height } = getNodeDimensions(node); const { width, height } = getNodeDimensions(node);
const positionAbsolute = 'internals' in node ? node.internals.positionAbsolute : node.position; const origin = node.origin ?? nodeOrigin;
const origin = node.origin || nodeOrigin;
const offsetX = width * origin[0]; const offsetX = width * origin[0];
const offsetY = height * origin[1]; const offsetY = height * origin[1];
return { return {
position: { x: node.position.x - offsetX,
x: node.position.x - offsetX, y: node.position.y - offsetY,
y: node.position.y - offsetY,
},
positionAbsolute: {
x: positionAbsolute.x - offsetX,
y: positionAbsolute.y - offsetY,
},
}; };
}; };
@@ -157,7 +147,6 @@ export const getNodesBounds = (nodes: NodeBase[], params: GetNodesBoundsParams =
}; };
export type GetInternalNodesBoundsParams<NodeType> = { export type GetInternalNodesBoundsParams<NodeType> = {
nodeOrigin?: NodeOrigin;
useRelativePosition?: boolean; useRelativePosition?: boolean;
filter?: (node: NodeType) => boolean; filter?: (node: NodeType) => boolean;
}; };
@@ -168,9 +157,7 @@ export type GetInternalNodesBoundsParams<NodeType> = {
*/ */
export const getInternalNodesBounds = <NodeType extends InternalNodeBase | NodeDragItem>( export const getInternalNodesBounds = <NodeType extends InternalNodeBase | NodeDragItem>(
nodeLookup: Map<string, NodeType>, nodeLookup: Map<string, NodeType>,
params: GetInternalNodesBoundsParams<NodeType> = { params: GetInternalNodesBoundsParams<NodeType> = {}
nodeOrigin: [0, 0],
}
): Rect => { ): Rect => {
if (nodeLookup.size === 0) { if (nodeLookup.size === 0) {
return { x: 0, y: 0, width: 0, height: 0 }; return { x: 0, y: 0, width: 0, height: 0 };
@@ -179,8 +166,8 @@ export const getInternalNodesBounds = <NodeType extends InternalNodeBase | NodeD
let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }; let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity };
nodeLookup.forEach((node) => { nodeLookup.forEach((node) => {
if (params.filter == undefined || params.filter(node)) { if (params.filter === undefined || params.filter(node)) {
const nodeBox = nodeToBox(node as InternalNodeBase, params.nodeOrigin); const nodeBox = nodeToBox(node as InternalNodeBase);
box = getBoundsOfBoxes(box, nodeBox); box = getBoundsOfBoxes(box, nodeBox);
} }
}); });
@@ -194,8 +181,7 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
[tx, ty, tScale]: Transform = [0, 0, 1], [tx, ty, tScale]: Transform = [0, 0, 1],
partially = false, partially = false,
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute // set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
excludeNonSelectableNodes = false, excludeNonSelectableNodes = false
nodeOrigin: NodeOrigin = [0, 0]
): InternalNodeBase<NodeType>[] => { ): InternalNodeBase<NodeType>[] => {
const paneRect = { const paneRect = {
...pointToRendererPoint(rect, [tx, ty, tScale]), ...pointToRendererPoint(rect, [tx, ty, tScale]),
@@ -214,7 +200,7 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
continue; continue;
} }
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin)); const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node));
const notInitialized = width === null || height === null; const notInitialized = width === null || height === null;
const partiallyVisible = partially && overlappingArea > 0; const partiallyVisible = partially && overlappingArea > 0;
@@ -248,22 +234,22 @@ export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType
}; };
export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>( export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
{ nodeLookup, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params, { nodeLookup, width, height, panZoom, minZoom, maxZoom }: Params,
options?: Options options?: Options
) { ) {
const filteredNodes: InternalNodeBase[] = []; const filteredNodes: Map<string, InternalNodeBase> = new Map();
const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null; const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null;
nodeLookup.forEach((n) => { nodeLookup.forEach((n) => {
const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden); const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden);
if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) { if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) {
filteredNodes.push(n); filteredNodes.set(n.id, n);
} }
}); });
if (filteredNodes.length > 0) { if (filteredNodes.size > 0) {
const bounds = getNodesBounds(filteredNodes, { nodeOrigin }); const bounds = getInternalNodesBounds(filteredNodes);
const viewport = getViewportForBounds( const viewport = getViewportForBounds(
bounds, bounds,
@@ -323,9 +309,8 @@ export function calculateNodePosition<NodeType extends NodeBase>({
}): { position: XYPosition; positionAbsolute: XYPosition } { }): { position: XYPosition; positionAbsolute: XYPosition } {
const node = nodeLookup.get(nodeId)!; const node = nodeLookup.get(nodeId)!;
const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined; const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined;
const { x: parentX, y: parentY } = parentNode const { x: parentX, y: parentY } = parentNode ? parentNode.internals.positionAbsolute : { x: 0, y: 0 };
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute const origin = node.origin ?? nodeOrigin;
: { x: 0, y: 0 };
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent); let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
@@ -339,13 +324,9 @@ export function calculateNodePosition<NodeType extends NodeBase>({
const parentHeight = parentNode.measured.height; const parentHeight = parentNode.measured.height;
if (nodeWidth && nodeHeight && parentWidth && parentHeight) { if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
const currNodeOrigin = node.origin || nodeOrigin;
const extentX = parentX + nodeWidth * currNodeOrigin[0];
const extentY = parentY + nodeHeight * currNodeOrigin[1];
currentExtent = [ currentExtent = [
[extentX, extentY], [parentX, parentY],
[extentX + parentWidth - nodeWidth, extentY + parentHeight - nodeHeight], [parentX + parentWidth - nodeWidth, parentY + parentHeight - nodeHeight],
]; ];
} }
} }
@@ -362,8 +343,9 @@ export function calculateNodePosition<NodeType extends NodeBase>({
return { return {
position: { position: {
x: positionAbsolute.x - parentX, // TODO: is there a better way to do this?
y: positionAbsolute.y - parentY, x: positionAbsolute.x - parentX + node.measured.width! * origin[0],
y: positionAbsolute.y - parentY + node.measured.height! * origin[1],
}, },
positionAbsolute, positionAbsolute,
}; };
+121 -103
View File
@@ -6,7 +6,6 @@ import {
PanZoomInstance, PanZoomInstance,
Transform, Transform,
XYPosition, XYPosition,
XYZPosition,
ConnectionLookup, ConnectionLookup,
EdgeBase, EdgeBase,
EdgeLookup, EdgeLookup,
@@ -22,48 +21,28 @@ import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './ge
import { getNodePositionWithOrigin } from './graph'; import { getNodePositionWithOrigin } from './graph';
import { ParentExpandChild } from './types'; import { ParentExpandChild } from './types';
const defaultOptions = {
nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true,
defaults: {},
};
const adoptUserNodesDefaultOptions = {
...defaultOptions,
checkEquality: true,
};
export function updateAbsolutePositions<NodeType extends NodeBase>( export function updateAbsolutePositions<NodeType extends NodeBase>(
nodeLookup: Map<string, InternalNodeBase<NodeType>>, nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
options: UpdateNodesOptions<NodeType> = { parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
nodeOrigin: [0, 0] as NodeOrigin, options?: UpdateNodesOptions<NodeType>
elevateNodesOnSelect: true,
defaults: {},
}
) { ) {
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; const _options = { ...defaultOptions, ...options };
for (const node of nodeLookup.values()) {
for (const [, node] of nodeLookup) { if (!node.parentId) {
const parentId = node.parentId;
if (!parentId) {
continue; continue;
} }
if (!nodeLookup.has(parentId)) { updateChildPosition(node, nodeLookup, parentLookup, _options);
throw new Error(`Parent node ${parentId} not found`);
}
const parentNode = nodeLookup.get(parentId);
const { x, y, z } = calculateXYZPosition(
node,
nodeLookup,
{
...node.position,
z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0),
},
parentNode?.origin ?? options.nodeOrigin
);
const currPosition = node.internals.positionAbsolute;
const positionChanged = x !== currPosition.x || y !== currPosition.y;
if (positionChanged || z !== node.internals.z) {
node.internals = {
...node.internals,
positionAbsolute: positionChanged ? { x, y } : currPosition,
z,
};
}
} }
} }
@@ -76,38 +55,33 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
export function adoptUserNodes<NodeType extends NodeBase>( export function adoptUserNodes<NodeType extends NodeBase>(
nodes: NodeType[], nodes: NodeType[],
nodeLookup: Map<string, InternalNodeBase<NodeType>>, nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
parentLookup: Map<string, InternalNodeBase<NodeType>[]>, parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
options: UpdateNodesOptions<NodeType> = { options?: UpdateNodesOptions<NodeType>
nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true,
defaults: {},
checkEquality: true,
}
) { ) {
const _options = { ...adoptUserNodesDefaultOptions, ...options };
const tmpLookup = new Map(nodeLookup); const tmpLookup = new Map(nodeLookup);
nodeLookup.clear(); nodeLookup.clear();
parentLookup.clear(); parentLookup.clear();
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
nodes.forEach((userNode) => { for (const userNode of nodes) {
let internalNode = tmpLookup.get(userNode.id); let internalNode = tmpLookup.get(userNode.id);
if (_options.checkEquality && userNode === internalNode?.internals.userNode) {
if (options.checkEquality && userNode === internalNode?.internals.userNode) {
nodeLookup.set(userNode.id, internalNode); nodeLookup.set(userNode.id, internalNode);
} else { } else {
internalNode = { internalNode = {
...options.defaults, ..._options.defaults,
...userNode, ...userNode,
measured: { measured: {
width: userNode.measured?.width, width: userNode.measured?.width,
height: userNode.measured?.height, height: userNode.measured?.height,
}, },
internals: { internals: {
positionAbsolute: userNode.position, positionAbsolute: getNodePositionWithOrigin(userNode, _options.nodeOrigin),
handleBounds: internalNode?.internals.handleBounds, handleBounds: internalNode?.internals.handleBounds,
z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0), z: calculateZ(userNode, selectedNodeZ),
userNode, userNode,
}, },
}; };
@@ -115,50 +89,75 @@ export function adoptUserNodes<NodeType extends NodeBase>(
} }
if (userNode.parentId) { if (userNode.parentId) {
const childNodes = parentLookup.get(userNode.parentId); updateChildPosition(internalNode, nodeLookup, parentLookup, options);
if (childNodes) {
childNodes.push(internalNode);
} else {
parentLookup.set(userNode.parentId, [internalNode]);
}
} }
});
if (parentLookup.size > 0) {
updateAbsolutePositions(nodeLookup, options);
} }
} }
function calculateXYZPosition<NodeType extends NodeBase>( function updateChildPosition<NodeType extends NodeBase>(
node: NodeType, node: InternalNodeBase<NodeType>,
nodeLookup: Map<string, InternalNodeBase<NodeType>>, nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
result: XYZPosition, parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
nodeOrigin: NodeOrigin = [0, 0] options?: UpdateNodesOptions<NodeType>
): XYZPosition { ) {
if (!node.parentId) { const _options = { ...defaultOptions, ...options };
return result;
const parentId = node.parentId!;
const parentNode = nodeLookup.get(parentId);
if (!parentNode) {
throw new Error(`Parent node ${parentId} not found`);
} }
const parent = nodeLookup.get(node.parentId)!; // update the parentLookup
const parentPosition = getNodePositionWithOrigin(parent, nodeOrigin).position; const childNodes = parentLookup.get(parentId);
if (childNodes) {
childNodes.set(node.id, node);
} else {
parentLookup.set(parentId, new Map([[node.id, node]]));
}
return calculateXYZPosition( const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
parent,
nodeLookup, const { x, y, z } = calculateChildXYZ(node, parentNode, _options.nodeOrigin!, selectedNodeZ);
{
x: (result.x ?? 0) + parentPosition.x, const currPosition = node.internals.positionAbsolute;
y: (result.y ?? 0) + parentPosition.y, const positionChanged = x !== currPosition.x || y !== currPosition.y;
z: (parent.internals.z ?? 0) > (result.z ?? 0) ? parent.internals.z ?? 0 : result.z ?? 0,
}, if (positionChanged || z !== node.internals.z) {
parent.origin || nodeOrigin node.internals = {
); ...node.internals,
positionAbsolute: positionChanged ? { x, y } : currPosition,
z,
};
}
}
function calculateZ(node: NodeBase, selectedNodeZ: number) {
return (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0);
}
function calculateChildXYZ<NodeType extends NodeBase>(
childNode: InternalNodeBase<NodeType>,
parentNode: InternalNodeBase<NodeType>,
nodeOrigin: NodeOrigin,
selectedNodeZ: number
) {
const position = getNodePositionWithOrigin(childNode, nodeOrigin);
const childZ = calculateZ(childNode, selectedNodeZ);
const parentZ = parentNode.internals.z ?? 0;
return {
x: parentNode.internals.positionAbsolute.x + position.x,
y: parentNode.internals.positionAbsolute.y + position.y,
z: parentZ > childZ ? parentZ : childZ,
};
} }
export function handleExpandParent( export function handleExpandParent(
children: ParentExpandChild[], children: ParentExpandChild[],
nodeLookup: NodeLookup, nodeLookup: NodeLookup,
parentLookup: ParentLookup, parentLookup: ParentLookup,
nodeOrigin?: NodeOrigin nodeOrigin: NodeOrigin = [0, 0]
): (NodeDimensionChange | NodePositionChange)[] { ): (NodeDimensionChange | NodePositionChange)[] {
const changes: (NodeDimensionChange | NodePositionChange)[] = []; const changes: (NodeDimensionChange | NodePositionChange)[] = [];
const parentExpansions = new Map<string, { expandedRect: Rect; parent: InternalNodeBase }>(); const parentExpansions = new Map<string, { expandedRect: Rect; parent: InternalNodeBase }>();
@@ -170,36 +169,45 @@ export function handleExpandParent(
continue; continue;
} }
const parentRect = const parentRect = parentExpansions.get(child.parentId)?.expandedRect ?? nodeToRect(parent);
parentExpansions.get(child.parentId)?.expandedRect ?? nodeToRect(parent, parent.origin ?? nodeOrigin);
const expandedRect = getBoundsOfRects(parentRect, child.rect); const expandedRect = getBoundsOfRects(parentRect, child.rect);
parentExpansions.set(child.parentId, { expandedRect, parent }); parentExpansions.set(child.parentId, { expandedRect, parent });
} }
if (parentExpansions.size > 0) { if (parentExpansions.size > 0) {
parentExpansions.forEach(({ expandedRect, parent }, parentId) => { parentExpansions.forEach(({ expandedRect, parent }, parentId) => {
// determine the position & dimensions of the parent // determine the position & dimensions of the parent
const { position } = getNodePositionWithOrigin(parent, parent.origin); const positionAbsolute = parent.internals.positionAbsolute;
const dimensions = getNodeDimensions(parent); const dimensions = getNodeDimensions(parent);
const origin = parent.origin ?? nodeOrigin;
// determine how much the parent expands by moving the position // determine how much the parent expands in width and position
const xChange = expandedRect.x < position.x ? Math.round(Math.abs(position.x - expandedRect.x)) : 0; const xChange =
const yChange = expandedRect.y < position.y ? Math.round(Math.abs(position.y - expandedRect.y)) : 0; expandedRect.x < positionAbsolute.x ? Math.round(Math.abs(positionAbsolute.x - expandedRect.x)) : 0;
const yChange =
expandedRect.y < positionAbsolute.y ? Math.round(Math.abs(positionAbsolute.y - expandedRect.y)) : 0;
if (xChange > 0 || yChange > 0) { const newWidth = Math.max(dimensions.width, Math.round(expandedRect.width));
const newHeight = Math.max(dimensions.height, Math.round(expandedRect.height));
const widthChange = (newWidth - dimensions.width) * origin[0];
const heightChange = (newHeight - dimensions.height) * origin[1];
// We need to correct the position of the parent node if the origin is not [0,0]
if (xChange > 0 || yChange > 0 || widthChange || heightChange) {
changes.push({ changes.push({
id: parentId, id: parentId,
type: 'position', type: 'position',
position: { position: {
x: position.x - xChange, x: parent.position.x - xChange + widthChange,
y: position.y - yChange, y: parent.position.y - yChange + heightChange,
}, },
}); });
// We move all child nodes in the oppsite direction // We move all child nodes in the oppsite direction
// so the x,y changes of the parent do not move the children // so the x,y changes of the parent do not move the children
const childNodes = parentLookup.get(parentId); parentLookup.get(parentId)?.forEach((childNode) => {
childNodes?.forEach((childNode) => {
if (!children.some((child) => child.id === childNode.id)) { if (!children.some((child) => child.id === childNode.id)) {
changes.push({ changes.push({
id: childNode.id, id: childNode.id,
@@ -213,14 +221,15 @@ export function handleExpandParent(
}); });
} }
if (dimensions.width < expandedRect.width || dimensions.height < expandedRect.height) { // We need to correct the dimensions of the parent node if the origin is not [0,0]
if (dimensions.width < expandedRect.width || dimensions.height < expandedRect.height || xChange || yChange) {
changes.push({ changes.push({
id: parentId, id: parentId,
type: 'dimensions', type: 'dimensions',
setAttributes: true, setAttributes: true,
dimensions: { dimensions: {
width: Math.max(dimensions.width, Math.round(expandedRect.width)), width: newWidth + (xChange ? origin[0] * xChange - widthChange : 0),
height: Math.max(dimensions.height, Math.round(expandedRect.height)), height: newHeight + (yChange ? origin[1] * yChange - heightChange : 0),
}, },
}); });
} }
@@ -250,16 +259,19 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
// in this array we collect nodes, that might trigger changes (like expanding parent) // in this array we collect nodes, that might trigger changes (like expanding parent)
const parentExpandChildren: ParentExpandChild[] = []; const parentExpandChildren: ParentExpandChild[] = [];
updates.forEach((update) => { for (const update of updates.values()) {
const node = nodeLookup.get(update.id); const node = nodeLookup.get(update.id);
if (!node) {
continue;
}
if (node?.hidden) { if (node.hidden) {
node.internals = { node.internals = {
...node.internals, ...node.internals,
handleBounds: undefined, handleBounds: undefined,
}; };
updatedInternals = true; updatedInternals = true;
} else if (node) { } else {
const dimensions = getDimensions(update.nodeElement); const dimensions = getDimensions(update.nodeElement);
const dimensionChanged = node.measured.width !== dimensions.width || node.measured.height !== dimensions.height; const dimensionChanged = node.measured.width !== dimensions.width || node.measured.height !== dimensions.height;
const doUpdate = !!( const doUpdate = !!(
@@ -269,14 +281,20 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
); );
if (doUpdate) { if (doUpdate) {
const nodeBounds = update.nodeElement.getBoundingClientRect();
node.measured = dimensions; node.measured = dimensions;
node.internals = { node.internals = {
...node.internals, ...node.internals,
positionAbsolute: getNodePositionWithOrigin(node, nodeOrigin),
handleBounds: { handleBounds: {
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin), source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id),
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin), target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id),
}, },
}; };
if (node.parentId) {
updateChildPosition(node, nodeLookup, parentLookup, { nodeOrigin });
}
updatedInternals = true; updatedInternals = true;
@@ -297,7 +315,7 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
} }
} }
} }
}); }
if (parentExpandChildren.length > 0) { if (parentExpandChildren.length > 0) {
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin); const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin);
+13 -3
View File
@@ -100,6 +100,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
let containerBounds: DOMRect | null = null; let containerBounds: DOMRect | null = null;
let dragStarted = false; let dragStarted = false;
let d3Selection: Selection<Element, unknown, null, undefined> | null = null; let d3Selection: Selection<Element, unknown, null, undefined> | null = null;
let abortDrag = false; // prevents unintentional dragging on multitouch
// public functions // public functions
function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }: DragUpdateParams) { function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }: DragUpdateParams) {
@@ -123,13 +124,12 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 }; let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 };
if (dragItems.size > 1 && nodeExtent) { if (dragItems.size > 1 && nodeExtent) {
const rect = getInternalNodesBounds(dragItems, { nodeOrigin }); const rect = getInternalNodesBounds(dragItems);
nodesBox = rectToBox(rect); nodesBox = rectToBox(rect);
} }
for (const [id, dragItem] of dragItems) { for (const [id, dragItem] of dragItems) {
let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y }; let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y };
if (snapToGrid) { if (snapToGrid) {
nextPosition = snapPosition(nextPosition, snapGrid); nextPosition = snapPosition(nextPosition, snapGrid);
} }
@@ -264,6 +264,8 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
.on('start', (event: UseDragEvent) => { .on('start', (event: UseDragEvent) => {
const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems(); const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems();
abortDrag = false;
if (nodeDragThreshold === 0) { if (nodeDragThreshold === 0) {
startDrag(event); startDrag(event);
} }
@@ -277,6 +279,14 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems(); const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems();
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
if (event.sourceEvent.type === 'touchmove' && event.sourceEvent.touches.length > 1) {
abortDrag = true;
}
if (abortDrag) {
return;
}
if (!autoPanStarted && autoPanOnNodeDrag && dragStarted) { if (!autoPanStarted && autoPanOnNodeDrag && dragStarted) {
autoPanStarted = true; autoPanStarted = true;
autoPan(); autoPan();
@@ -301,7 +311,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
} }
}) })
.on('end', (event: UseDragEvent) => { .on('end', (event: UseDragEvent) => {
if (!dragStarted) { if (!dragStarted || abortDrag) {
return; return;
} }
+92 -107
View File
@@ -1,74 +1,25 @@
import { pointToRendererPoint, rendererPointToPoint, getHostForElement, calcAutoPan, getEventPosition } from '../utils'; import {
pointToRendererPoint,
getHostForElement,
calcAutoPan,
getEventPosition,
getHandlePosition,
rendererPointToPoint,
} from '../utils';
import { import {
ConnectionMode, ConnectionMode,
type OnConnect, Position,
type OnConnectStart, oppositePosition,
type HandleType, ConnectionInProgress,
type Handle,
type Connection, type Connection,
type PanBy,
type Transform,
type ConnectingHandle,
type OnConnectEnd,
type UpdateConnection,
type IsValidConnection,
type ConnectionHandle,
NodeLookup,
} from '../types'; } from '../types';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils'; import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils';
import { IsValidParams, OnPointerDownParams, Result, XYHandleInstance } from './types';
export type OnPointerDownParams = {
autoPanOnConnect: boolean;
connectionMode: ConnectionMode;
connectionRadius: number;
domNode: HTMLDivElement | null;
handleId: string | null;
nodeId: string;
isTarget: boolean;
nodeLookup: NodeLookup;
lib: string;
flowId: string | null;
edgeUpdaterType?: HandleType;
updateConnection: UpdateConnection;
panBy: PanBy;
cancelConnection: () => void;
onConnectStart?: OnConnectStart;
onConnect?: OnConnect;
onConnectEnd?: OnConnectEnd;
isValidConnection?: IsValidConnection;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
getTransform: () => Transform;
getConnectionStartHandle: () => ConnectingHandle | null;
};
export type IsValidParams = {
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null;
connectionMode: ConnectionMode;
fromNodeId: string;
fromHandleId: string | null;
fromType: HandleType;
isValidConnection?: IsValidConnection;
doc: Document | ShadowRoot;
lib: string;
flowId: string | null;
};
export type XYHandleInstance = {
onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void;
isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result;
};
type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection | null;
endHandle: ConnectingHandle | null;
};
const alwaysValid = () => true; const alwaysValid = () => true;
let connectionStartHandle: ConnectingHandle | null = null;
function onPointerDown( function onPointerDown(
event: MouseEvent | TouchEvent, event: MouseEvent | TouchEvent,
{ {
@@ -89,16 +40,16 @@ function onPointerDown(
onConnect, onConnect,
onConnectEnd, onConnectEnd,
isValidConnection = alwaysValid, isValidConnection = alwaysValid,
onEdgeUpdateEnd, onReconnectEnd,
updateConnection, updateConnection,
getTransform, getTransform,
getConnectionStartHandle, getFromHandle,
}: OnPointerDownParams }: OnPointerDownParams
) { ) {
// when xyflow is used inside a shadow root we can't use document // when xyflow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
let autoPanId = 0; let autoPanId = 0;
let closestHandle: ConnectionHandle | null; let closestHandle: Handle | null;
const { x, y } = getEventPosition(event); const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y); const clickedHandle = doc?.elementFromPoint(x, y);
@@ -109,13 +60,13 @@ function onPointerDown(
return; return;
} }
let connectionPosition = getEventPosition(event, containerBounds); let position = getEventPosition(event, containerBounds);
let autoPanStarted = false; let autoPanStarted = false;
let connection: Connection | null = null; let connection: Connection | null = null;
let isValid = false; let isValid: boolean | null = false;
let handleDomNode: Element | null = null; let handleDomNode: Element | null = null;
const handleLookup = getHandleLookup({ const [handleLookup, fromHandleInternal] = getHandleLookup({
nodeLookup, nodeLookup,
nodeId, nodeId,
handleId, handleId,
@@ -127,38 +78,54 @@ function onPointerDown(
if (!autoPanOnConnect || !containerBounds) { if (!autoPanOnConnect || !containerBounds) {
return; return;
} }
const [x, y] = calcAutoPan(connectionPosition, containerBounds); const [x, y] = calcAutoPan(position, containerBounds);
panBy({ x, y }); panBy({ x, y });
autoPanId = requestAnimationFrame(autoPan); autoPanId = requestAnimationFrame(autoPan);
} }
// Stays the same for all consecutive pointermove events // Stays the same for all consecutive pointermove events
connectionStartHandle = { const fromHandle: Handle = {
...fromHandleInternal,
nodeId, nodeId,
handleId,
type: handleType, type: handleType,
position: fromHandleInternal.position,
}; };
updateConnection({ const fromNodeInternal = nodeLookup.get(nodeId)!;
connectionPosition,
connectionStatus: null, const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
// connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
connectionStartHandle, const newConnection: ConnectionInProgress = {
connectionEndHandle: null, inProgress: true,
}); isValid: null,
from,
fromHandle,
fromPosition: fromHandle.position,
fromNode: fromNodeInternal.internals.userNode,
to: position,
toHandle: null,
toPosition: oppositePosition[fromHandle.position],
toNode: null,
};
updateConnection(newConnection);
let previousConnection: ConnectionInProgress = newConnection;
onConnectStart?.(event, { nodeId, handleId, handleType }); onConnectStart?.(event, { nodeId, handleId, handleType });
function onPointerMove(event: MouseEvent | TouchEvent) { function onPointerMove(event: MouseEvent | TouchEvent) {
if (!getConnectionStartHandle()) { if (!getFromHandle() || !fromHandle) {
onPointerUp(event); onPointerUp(event);
return;
} }
const transform = getTransform(); const transform = getTransform();
connectionPosition = getEventPosition(event, containerBounds); position = getEventPosition(event, containerBounds);
closestHandle = getClosestHandle( closestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]), pointToRendererPoint(position, transform, false, [1, 1]),
connectionRadius, connectionRadius,
handleLookup handleLookup
); );
@@ -178,27 +145,41 @@ function onPointerDown(
doc, doc,
lib, lib,
flowId, flowId,
handleLookup,
}); });
handleDomNode = result.handleDomNode; handleDomNode = result.handleDomNode;
connection = result.connection; connection = result.connection;
isValid = result.isValid; isValid = isConnectionValid(!!closestHandle, result.isValid);
updateConnection({ const newConnection: ConnectionInProgress = {
connectionStartHandle, // from stays the same
connectionPosition: ...previousConnection,
isValid,
to:
closestHandle && isValid closestHandle && isValid
? rendererPointToPoint( ? rendererPointToPoint({ x: closestHandle.x, y: closestHandle.y }, transform)
{ : position,
x: closestHandle.x, toHandle: result.toHandle,
y: closestHandle.y, toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
}, toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)!.internals.userNode : null,
transform };
)
: connectionPosition, // we don't want to trigger an update when the connection
connectionStatus: getConnectionStatus(!!closestHandle, isValid), // is snapped to the same handle as before
connectionEndHandle: result.endHandle, if (
}); isValid &&
closestHandle &&
previousConnection.toHandle &&
newConnection.toHandle &&
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
previousConnection.toHandle.id === newConnection.toHandle.id
) {
return;
}
updateConnection(newConnection);
previousConnection = newConnection;
} }
function onPointerUp(event: MouseEvent | TouchEvent) { function onPointerUp(event: MouseEvent | TouchEvent) {
@@ -211,7 +192,7 @@ function onPointerDown(
onConnectEnd?.(event); onConnectEnd?.(event);
if (edgeUpdaterType) { if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event); onReconnectEnd?.(event);
} }
cancelConnection(); cancelConnection();
@@ -220,7 +201,6 @@ function onPointerDown(
isValid = false; isValid = false;
connection = null; connection = null;
handleDomNode = null; handleDomNode = null;
connectionStartHandle = null;
doc.removeEventListener('mousemove', onPointerMove as EventListener); doc.removeEventListener('mousemove', onPointerMove as EventListener);
doc.removeEventListener('mouseup', onPointerUp as EventListener); doc.removeEventListener('mouseup', onPointerUp as EventListener);
@@ -249,6 +229,7 @@ function isValidHandle(
lib, lib,
flowId, flowId,
isValidConnection = alwaysValid, isValidConnection = alwaysValid,
handleLookup,
}: IsValidParams }: IsValidParams
) { ) {
const isTarget = fromType === 'target'; const isTarget = fromType === 'target';
@@ -266,7 +247,7 @@ function isValidHandle(
handleDomNode: handleToCheck, handleDomNode: handleToCheck,
isValid: false, isValid: false,
connection: null, connection: null,
endHandle: null, toHandle: null,
}; };
if (handleToCheck) { if (handleToCheck) {
@@ -297,14 +278,18 @@ function isValidHandle(
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target') ? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
: handleNodeId !== fromNodeId || handleId !== fromHandleId); : handleNodeId !== fromNodeId || handleId !== fromHandleId);
if (isValid) { result.isValid = isValid && isValidConnection(connection);
result.endHandle = {
nodeId: handleNodeId as string,
handleId,
type: handleType as HandleType,
};
result.isValid = isValidConnection(connection); if (handleLookup) {
const toHandle = handleLookup.find(
(h) => h.id === handleId && h.nodeId === handleNodeId && h.type === handleType
);
if (toHandle) {
result.toHandle = {
...toHandle,
};
}
} }
} }
+63
View File
@@ -0,0 +1,63 @@
import {
ConnectionMode,
type Connection,
type OnConnect,
type OnConnectStart,
type HandleType,
type PanBy,
type Transform,
type Handle,
type OnConnectEnd,
type UpdateConnection,
type IsValidConnection,
NodeLookup,
} from '../types';
export type OnPointerDownParams = {
autoPanOnConnect: boolean;
connectionMode: ConnectionMode;
connectionRadius: number;
domNode: HTMLDivElement | null;
handleId: string | null;
nodeId: string;
isTarget: boolean;
nodeLookup: NodeLookup;
lib: string;
flowId: string | null;
edgeUpdaterType?: HandleType;
updateConnection: UpdateConnection;
panBy: PanBy;
cancelConnection: () => void;
onConnectStart?: OnConnectStart;
onConnect?: OnConnect;
onConnectEnd?: OnConnectEnd;
isValidConnection?: IsValidConnection;
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
getTransform: () => Transform;
getFromHandle: () => Handle | null;
};
export type IsValidParams = {
handle: Pick<Handle, 'nodeId' | 'id' | 'type'> | null;
connectionMode: ConnectionMode;
fromNodeId: string;
fromHandleId: string | null;
fromType: HandleType;
isValidConnection?: IsValidConnection;
doc: Document | ShadowRoot;
lib: string;
flowId: string | null;
handleLookup?: Handle[];
};
export type XYHandleInstance = {
onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void;
isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result;
};
export type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection | null;
toHandle: Handle | null;
};
+30 -33
View File
@@ -1,41 +1,36 @@
import { getHandlePosition } from '../utils';
import { import {
ConnectionStatus,
type HandleType, type HandleType,
type NodeHandleBounds, type NodeHandleBounds,
type XYPosition, type XYPosition,
type ConnectionHandle, type Handle,
InternalNodeBase, InternalNodeBase,
NodeLookup, NodeLookup,
} from '../types'; } from '../types';
// this functions collects all handles and adds an absolute position // this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position // so that we can later find the closest handle to the mouse position
export function getHandles( function getHandles(
node: InternalNodeBase, node: InternalNodeBase,
handleBounds: NodeHandleBounds, handleBounds: NodeHandleBounds,
type: HandleType, type: HandleType,
currentHandle: string currentHandle: { nodeId: string; handleId: string | null; handleType: HandleType }
): ConnectionHandle[] { ): [Handle[], Handle | null] {
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => { let excludedHandle = null;
if (`${node.id}-${h.id}-${type}` !== currentHandle) { const handles = (handleBounds[type] || []).reduce<Handle[]>((res, handle) => {
res.push({ if (node.id === currentHandle.nodeId && type === currentHandle.handleType && handle.id === currentHandle.handleId) {
id: h.id || null, excludedHandle = handle;
type, } else {
nodeId: node.id, const handleXY = getHandlePosition(node, handle, handle.position, true);
x: node.internals.positionAbsolute.x + h.x + h.width / 2, res.push({ ...handle, ...handleXY });
y: node.internals.positionAbsolute.y + h.y + h.height / 2,
});
} }
return res; return res;
}, []); }, []);
return [handles, excludedHandle];
} }
export function getClosestHandle( export function getClosestHandle(pos: XYPosition, connectionRadius: number, handles: Handle[]): Handle | null {
pos: XYPosition, let closestHandles: Handle[] = [];
connectionRadius: number,
handles: ConnectionHandle[]
): ConnectionHandle | null {
let closestHandles: ConnectionHandle[] = [];
let minDistance = Infinity; let minDistance = Infinity;
for (const handle of handles) { for (const handle of handles) {
@@ -65,7 +60,7 @@ type GetHandleLookupParams = {
nodeLookup: NodeLookup; nodeLookup: NodeLookup;
nodeId: string; nodeId: string;
handleId: string | null; handleId: string | null;
handleType: string; handleType: HandleType;
}; };
export function getHandleLookup({ export function getHandleLookup({
@@ -73,19 +68,21 @@ export function getHandleLookup({
nodeId, nodeId,
handleId, handleId,
handleType, handleType,
}: GetHandleLookupParams): ConnectionHandle[] { }: GetHandleLookupParams): [Handle[], Handle] {
const connectionHandles: ConnectionHandle[] = []; const connectionHandles: Handle[] = [];
const currentHandle = { nodeId, handleId, handleType };
let excludedHandle: Handle | null = null;
for (const [, node] of nodeLookup) { for (const node of nodeLookup.values()) {
if (node.internals.handleBounds) { if (node.internals.handleBounds) {
const id = `${nodeId}-${handleId}-${handleType}`; const [sourceHandles, excludedSource] = getHandles(node, node.internals.handleBounds, 'source', currentHandle);
const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id); const [targetHandles, excludedTarget] = getHandles(node, node.internals.handleBounds, 'target', currentHandle);
const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id); excludedHandle = excludedHandle ? excludedHandle : excludedSource ?? excludedTarget;
connectionHandles.push(...sourceHandles, ...targetHandles); connectionHandles.push(...sourceHandles, ...targetHandles);
} }
} }
return connectionHandles; return [connectionHandles, excludedHandle!];
} }
export function getHandleType( export function getHandleType(
@@ -103,14 +100,14 @@ export function getHandleType(
return null; return null;
} }
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) { export function isConnectionValid(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
let connectionStatus = null; let isValid: boolean | null = null;
if (isHandleValid) { if (isHandleValid) {
connectionStatus = 'valid'; isValid = true;
} else if (isInsideConnectionRadius && !isHandleValid) { } else if (isInsideConnectionRadius && !isHandleValid) {
connectionStatus = 'invalid'; isValid = false;
} }
return connectionStatus as ConnectionStatus; return isValid;
} }
+5
View File
@@ -38,6 +38,7 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
zoomable = true, zoomable = true,
inversePan = false, inversePan = false,
}: XYMinimapUpdate) { }: XYMinimapUpdate) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const zoomHandler = (event: D3ZoomEvent<SVGSVGElement, any>) => { const zoomHandler = (event: D3ZoomEvent<SVGSVGElement, any>) => {
const transform = getTransform(); const transform = getTransform();
@@ -55,6 +56,7 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
}; };
let panStart = [0, 0]; let panStart = [0, 0];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const panStartHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => { const panStartHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (event.sourceEvent.type === 'mousedown' || event.sourceEvent.type === 'touchstart') { if (event.sourceEvent.type === 'mousedown' || event.sourceEvent.type === 'touchstart') {
panStart = [ panStart = [
@@ -64,6 +66,7 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
} }
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const panHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => { const panHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => {
const transform = getTransform(); const transform = getTransform();
@@ -101,8 +104,10 @@ export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMi
const zoomAndPanHandler = zoom() const zoomAndPanHandler = zoom()
.on('start', panStartHandler) .on('start', panStartHandler)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
.on('zoom', pannable ? panHandler : null) .on('zoom', pannable ? panHandler : null)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
.on('zoom.wheel', zoomable ? zoomHandler : null); .on('zoom.wheel', zoomable ? zoomHandler : null);
+5
View File
@@ -65,6 +65,11 @@ export function createFilter({
return false; return false;
} }
if (!zoomOnPinch && event.type === 'touchstart' && event.touches?.length > 1) {
event.preventDefault(); // if you manage to start with 2 touches, we prevent native zoom
return false;
}
// when there is no scroll handling enabled, we prevent all wheel events // when there is no scroll handling enabled, we prevent all wheel events
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') { if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
return false; return false;
+36 -25
View File
@@ -3,7 +3,15 @@ import { select } from 'd3-selection';
import { getControlDirection, getDimensionsAfterResize, getResizeDirection } from './utils'; import { getControlDirection, getDimensionsAfterResize, getResizeDirection } from './utils';
import { getPointerPosition } from '../utils'; import { getPointerPosition } from '../utils';
import type { CoordinateExtent, NodeBase, NodeLookup, NodeOrigin, Transform, XYPosition } from '../types'; import type {
CoordinateExtent,
InternalNodeBase,
NodeBase,
NodeLookup,
NodeOrigin,
Transform,
XYPosition,
} from '../types';
import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types'; import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types';
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 }; const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
@@ -100,9 +108,9 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
const controlDirection = getControlDirection(controlPosition); const controlDirection = getControlDirection(controlPosition);
let node: NodeBase | undefined = undefined; let node: InternalNodeBase | undefined = undefined;
let childNodes: XYResizerChildChange[] = []; let childNodes: XYResizerChildChange[] = [];
let parentNode: NodeBase | undefined = undefined; // Needed to fix expandParent let parentNode: InternalNodeBase | undefined = undefined; // Needed to fix expandParent
let parentExtent: CoordinateExtent | undefined = undefined; let parentExtent: CoordinateExtent | undefined = undefined;
let childExtent: CoordinateExtent | undefined = undefined; let childExtent: CoordinateExtent | undefined = undefined;
@@ -118,8 +126,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
prevValues = { prevValues = {
width: node.measured?.width ?? 0, width: node.measured.width ?? 0,
height: node.measured?.height ?? 0, height: node.measured.height ?? 0,
x: node.position.x ?? 0, x: node.position.x ?? 0,
y: node.position.y ?? 0, y: node.position.y ?? 0,
}; };
@@ -132,11 +140,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
}; };
parentNode = undefined; parentNode = undefined;
if (node.extent === 'parent' || node.expandParent) {
parentNode = nodeLookup.get(node.parentId!); if (node.parentId && (node.extent === 'parent' || node.expandParent)) {
if (parentNode && node.extent === 'parent') { parentNode = nodeLookup.get(node.parentId);
parentExtent = nodeToParentExtent(parentNode); parentExtent = parentNode && node.extent === 'parent' ? nodeToParentExtent(parentNode) : undefined;
}
} }
// Collect all child nodes to correct their relative positions when top/left changes // Collect all child nodes to correct their relative positions when top/left changes
@@ -153,7 +160,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
}); });
if (child.extent === 'parent' || child.expandParent) { if (child.extent === 'parent' || child.expandParent) {
const extent = nodeToChildExtent(child, node!, child.origin ?? nodeOrigin); const extent = nodeToChildExtent(child, node, child.origin ?? nodeOrigin);
if (childExtent) { if (childExtent) {
childExtent = [ childExtent = [
@@ -202,26 +209,15 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
return; return;
} }
if (isXPosChange || isYPosChange || nodeOrigin[0] === 1 || nodeOrigin[1] == 1) { if (isXPosChange || isYPosChange || nodeOrigin[0] === 1 || nodeOrigin[1] === 1) {
change.x = isXPosChange ? x : prevValues.x; change.x = isXPosChange ? x : prevValues.x;
change.y = isYPosChange ? y : prevValues.y; change.y = isYPosChange ? y : prevValues.y;
prevValues.x = change.x; prevValues.x = change.x;
prevValues.y = change.y; prevValues.y = change.y;
// Fix expandParent when resizing from top/left // when top/left changes, correct the relative positions of child nodes
if (parentNode && node.expandParent) { // so that they stay in the same position
if (change.x && change.x < 0) {
prevValues.x = 0;
startValues.x = startValues.x - change.x;
}
if (change.y && change.y < 0) {
prevValues.y = 0;
startValues.y = startValues.y - change.y;
}
}
if (childNodes.length > 0) { if (childNodes.length > 0) {
const xChange = x - prevX; const xChange = x - prevX;
const yChange = y - prevY; const yChange = y - prevY;
@@ -243,6 +239,21 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
prevValues.height = change.height; prevValues.height = change.height;
} }
// Fix expandParent when resizing from top/left
if (parentNode && node.expandParent) {
const xLimit = nodeOrigin[0] * (change.width ?? 0);
if (change.x && change.x < xLimit) {
prevValues.x = xLimit;
startValues.x = startValues.x - (change.x - xLimit);
}
const yLimit = nodeOrigin[1] * (change.height ?? 0);
if (change.y && change.y < yLimit) {
prevValues.y = yLimit;
startValues.y = startValues.y - (change.y - yLimit);
}
}
const direction = getResizeDirection({ const direction = getResizeDirection({
width: prevValues.width, width: prevValues.width,
prevWidth, prevWidth,
+7358 -5849
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -32,14 +32,14 @@ test.describe('Node Toolbar', async () => {
}); });
test('all toolbars are positioned correctly', async ({ page }) => { test('all toolbars are positioned correctly', async ({ page }) => {
permutations.forEach(async (permutation) => { const tests = permutations.map((permutation) => async () => {
const toolbar = page const toolbar = page
.locator(`[data-id="${permutation.id}"]`) .locator(`[data-id="${permutation.id}"]`)
.and(page.locator(`.${FRAMEWORK}-flow__node-toolbar`)); .and(page.locator(`.${FRAMEWORK}-flow__node-toolbar`));
const node = page.locator(`[data-id="${permutation.id}"]`).and(page.locator(`.${FRAMEWORK}-flow__node`)); const node = page.locator(`[data-id="${permutation.id}"]`).and(page.locator(`.${FRAMEWORK}-flow__node`));
await expect(toolbar).toBeAttached(); await expect(toolbar).toBeAttached({ timeout: 5000 });
await expect(node).toBeAttached(); await expect(node).toBeAttached({ timeout: 5000 });
const toolbarBox = await toolbar.boundingBox(); const toolbarBox = await toolbar.boundingBox();
const nodeBox = await node.boundingBox(); const nodeBox = await node.boundingBox();
@@ -78,6 +78,7 @@ test.describe('Node Toolbar', async () => {
break; break;
} }
}); });
await Promise.all(tests.map((t) => t()));
}); });
test('toolbar default behaviour', async ({ page }) => { test('toolbar default behaviour', async ({ page }) => {

Some files were not shown because too many files have changed in this diff Show More