Merge branch 'next' into absolute-origin

This commit is contained in:
Moritz Klack
2024-06-24 12:57:24 +02:00
committed by GitHub
91 changed files with 8822 additions and 7060 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',
@@ -60,12 +60,13 @@ const ControlledUncontrolled = () => {
const updateNodePositions = () => { const updateNodePositions = () => {
instance.setNodes((nodes) => instance.setNodes((nodes) =>
nodes.map((node) => { nodes.map((node) => {
node.position = { return {
x: Math.random() * 400, ...node,
y: Math.random() * 400, position: {
x: Math.random() * 400,
y: Math.random() * 400,
},
}; };
return node;
}) })
); );
}; };
@@ -73,11 +74,12 @@ const ControlledUncontrolled = () => {
const updateEdgeColors = () => { const updateEdgeColors = () => {
instance.setEdges((edges) => instance.setEdges((edges) =>
edges.map((edge) => { edges.map((edge) => {
edge.style = { return {
stroke: '#ff5050', ...edge,
style: {
stroke: '#ff5050',
},
}; };
return edge;
}) })
); );
}; };
@@ -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>
@@ -73,6 +73,8 @@ const HiddenFlow = () => {
setEdges(setHidden(isHidden)); setEdges(setHidden(isHidden));
}, [isHidden, setEdges, setNodes]); }, [isHidden, setEdges, setNodes]);
console.log(nodes);
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -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>
); );
@@ -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;
@@ -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',
+1 -1
View File
@@ -10,7 +10,7 @@
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", "moduleResolution": "Node16",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
+4 -4
View File
@@ -12,8 +12,8 @@
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1", "@sveltejs/adapter-auto": "^3.2.1",
"@sveltejs/kit": "^2.5.2", "@sveltejs/kit": "^2.5.10",
"@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0", "@typescript-eslint/parser": "^6.10.0",
"eslint": "^8.53.0", "eslint": "^8.53.0",
@@ -25,12 +25,12 @@
"svelte-check": "^3.6.6", "svelte-check": "^3.6.6",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0" "vite": "^5.2.12"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.0.4", "@dagrejs/dagre": "^1.0.4",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.1.1",
"@xyflow/svelte": "workspace:^" "@xyflow/svelte": "workspace:^"
} }
} }
@@ -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',
@@ -159,6 +159,8 @@
on:nodemouseenter={(event) => console.log('on node enter', event)} on:nodemouseenter={(event) => console.log('on node enter', event)}
on:nodemouseleave={(event) => console.log('on node leave', event)} on:nodemouseleave={(event) => console.log('on node leave', event)}
on:edgeclick={(event) => console.log('edge click', event)} on:edgeclick={(event) => console.log('edge click', event)}
on:edgemouseenter={(event) => console.log('edge enter', event)}
on:edgemouseleave={(event) => console.log('edge leave', event)}
onconnectstart={(event) => console.log('on connect start', event)} onconnectstart={(event) => console.log('on connect start', event)}
onconnect={(event) => console.log('on connect', event)} onconnect={(event) => console.log('on connect', event)}
onconnectend={(event) => console.log('on connect end', event)} onconnectend={(event) => console.log('on connect end', event)}
@@ -21,7 +21,7 @@
{ id: 'B', position: { x: 0, y: 100 }, data: { label: 'B' } } { id: 'B', position: { x: 0, y: 100 }, data: { label: 'B' } }
]); ]);
const edges = writable([{ id: 'ab', source: 'A', target: 'B' }]); const edges = writable([{ id: 'ab', source: 'A', target: 'B' }]);
const viewport = writable<Viewport>({ x: 0, y: 10, zoom: 1.25 }); const viewport = writable<Viewport>({ x: 100, y: 100, zoom: 1.25 });
const { fitView } = useSvelteFlow(); const { fitView } = useSvelteFlow();
@@ -35,7 +35,7 @@
}); });
</script> </script>
<SvelteFlow {nodes} {edges} fitView {viewport}> <SvelteFlow {nodes} {edges} {viewport}>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
@@ -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"
} }
+36
View File
@@ -1,5 +1,41 @@
# @xyflow/react # @xyflow/react
## 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
- update internals on node resizer updates
- re-observe node when `node.hidden` is toggled
- update `updateNodeData` argument type - thanks @ogroppo
- add `selectable`, `deletable` and `draggable` to node and edge props
- add `parentId` to node props
- fix parent node lookup in `evaluateAbsolutePosition`- thanks @lcsfort
## 12.0.0-next.18 ## 12.0.0-next.18
- don't show nodeTypes warning if not necessary you've created a new nodeTypes or edgeTypes - don't show nodeTypes warning if not necessary you've created a new nodeTypes or edgeTypes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/react", "name": "@xyflow/react",
"version": "12.0.0-next.18", "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",
@@ -39,7 +39,7 @@ const ConnectionLine = ({
CustomComponent, CustomComponent,
connectionStatus, connectionStatus,
}: ConnectionLineProps) => { }: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode } = useStore( const { fromNode, handleId, toX, toY, connectionMode, endPosition, isValid } = useStore(
useCallback( useCallback(
(s: ReactFlowStore) => ({ (s: ReactFlowStore) => ({
fromNode: s.nodeLookup.get(nodeId), fromNode: s.nodeLookup.get(nodeId),
@@ -47,11 +47,14 @@ const ConnectionLine = ({
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
endPosition: s.connectionEndHandle?.position,
isValid: s.connectionStatus === 'valid',
}), }),
[nodeId] [nodeId]
), ),
shallow shallow
); );
const fromHandleBounds = fromNode?.internals.handleBounds; const fromHandleBounds = fromNode?.internals.handleBounds;
let handleBounds = fromHandleBounds?.[handleType]; let handleBounds = fromHandleBounds?.[handleType];
@@ -69,7 +72,7 @@ const ConnectionLine = ({
const fromX = fromNode.internals.positionAbsolute.x + fromHandleX; const fromX = fromNode.internals.positionAbsolute.x + fromHandleX;
const fromY = fromNode.internals.positionAbsolute.y + fromHandleY; const fromY = fromNode.internals.positionAbsolute.y + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null; const toPosition = isValid && endPosition ? endPosition : fromPosition ? oppositePosition[fromPosition] : null;
if (!fromPosition || !toPosition) { if (!fromPosition || !toPosition) {
return null; return null;
@@ -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, getConnectionStartHandle: () => store.getState().connectionStartHandle,
}); });
}; };
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}
@@ -215,6 +215,8 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
type={edge.type} type={edge.type}
selected={edge.selected} selected={edge.selected}
animated={edge.animated} animated={edge.animated}
selectable={isSelectable}
deletable={edge.deletable ?? true}
label={edge.label} label={edge.label}
labelStyle={edge.labelStyle} labelStyle={edge.labelStyle}
labelShowBg={edge.labelShowBg} labelShowBg={edge.labelShowBg}
@@ -237,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}
@@ -252,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}
@@ -202,12 +202,16 @@ export function NodeWrapper<NodeType extends Node>({
positionAbsoluteX={clampedPosition.x} positionAbsoluteX={clampedPosition.x}
positionAbsoluteY={clampedPosition.y} positionAbsoluteY={clampedPosition.y}
selected={node.selected} selected={node.selected}
selectable={isSelectable}
draggable={isDraggable}
deletable={node.deletable ?? true}
isConnectable={isConnectable} isConnectable={isConnectable}
sourcePosition={node.sourcePosition} sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition} targetPosition={node.targetPosition}
dragging={dragging} dragging={dragging}
dragHandle={node.dragHandle} dragHandle={node.dragHandle}
zIndex={internals.z} zIndex={internals.z}
parentId={node.parentId}
{...nodeDimensions} {...nodeDimensions}
/> />
</Provider> </Provider>
@@ -26,17 +26,17 @@ export function useNodeObserver({
const prevSourcePosition = useRef(node.sourcePosition); const prevSourcePosition = useRef(node.sourcePosition);
const prevTargetPosition = useRef(node.targetPosition); const prevTargetPosition = useRef(node.targetPosition);
const prevType = useRef(nodeType); const prevType = useRef(nodeType);
const isInitialized = hasDimensions && !!node.internals.handleBounds && !node.hidden; const isInitialized = hasDimensions && !!node.internals.handleBounds;
useEffect(() => { useEffect(() => {
if (nodeRef.current && (!isInitialized || observedNode.current !== nodeRef.current)) { if (nodeRef.current && !node.hidden && (!isInitialized || observedNode.current !== nodeRef.current)) {
if (observedNode.current) { if (observedNode.current) {
resizeObserver?.unobserve(observedNode.current); resizeObserver?.unobserve(observedNode.current);
} }
resizeObserver?.observe(nodeRef.current); resizeObserver?.observe(nodeRef.current);
observedNode.current = nodeRef.current; observedNode.current = nodeRef.current;
} }
}, [isInitialized]); }, [isInitialized, node.hidden]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -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}
@@ -28,6 +28,8 @@ export type FlowRendererProps<NodeType extends Node = Node> = Omit<
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 +72,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 +115,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 && (
@@ -85,14 +85,14 @@ 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,
@@ -153,15 +153,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}
@@ -20,6 +20,7 @@ export function useResizeObserver() {
updates.set(id, { updates.set(id, {
id, id,
nodeElement: entry.target as HTMLDivElement, nodeElement: entry.target as HTMLDivElement,
force: true,
}); });
}); });
+35 -19
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, nodeOrigin, 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;
@@ -199,10 +219,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 +235,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}
> >
@@ -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',
@@ -202,15 +202,15 @@ 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}
@@ -236,7 +236,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,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) {
+34 -4
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,6 +41,10 @@ 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();
@@ -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
);
},
}; };
}, []); }, []);
+1 -1
View File
@@ -116,6 +116,6 @@ export {
getIncomers, getIncomers,
getOutgoers, getOutgoers,
addEdge, addEdge,
updateEdge, reconnectEdge,
getConnectedEdges, getConnectedEdges,
} from '@xyflow/system'; } from '@xyflow/system';
+1 -1
View File
@@ -92,7 +92,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,
+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;
+11 -11
View File
@@ -28,8 +28,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 +39,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 +58,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;
@@ -100,7 +100,7 @@ export type EdgeTextProps = HTMLAttributes<SVGElement> &
*/ */
export type EdgeProps<EdgeType extends Edge = Edge> = Pick< export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
EdgeType, EdgeType,
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' | 'selectable' | 'deletable'
> & > &
EdgePosition & EdgePosition &
EdgeLabelOptions & { EdgeLabelOptions & {
@@ -189,7 +189,7 @@ 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;
+31 -1
View File
@@ -140,7 +140,37 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
*/ */
updateNodeData: ( updateNodeData: (
id: string, id: string,
dataUpdate: object | ((node: NodeType) => object), dataUpdate: Partial<NodeType['data']> | ((node: NodeType) => Partial<NodeType['data']>),
options?: { replace: boolean }
) => 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 } options?: { replace: boolean }
) => void; ) => void;
}; };
+1 -1
View File
@@ -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;
+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
View File
@@ -1,5 +1,33 @@
# @xyflow/svelte # @xyflow/svelte
## 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
- add `selectable`, `deletable` and `draggable` to node and edge props
- add `parentId` to node props
- add `on:edgemouseenter` and `on:edgemouseleave` event handler
- fix deselection of edges
- remove pointer events from panel when user selection is active
- fix viewport initialization with user viewport
- fix parent node lookup in `evaluateAbsolutePosition`- thanks @lcsfort
## 0.1.3 ## 0.1.3
- fix `NodeToolbar` for subflows - fix `NodeToolbar` for subflows
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/svelte", "name": "@xyflow/svelte",
"version": "0.1.3", "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",
@@ -23,6 +23,7 @@
export let animated: $$Props['animated'] = false; export let animated: $$Props['animated'] = false;
export let selected: $$Props['selected'] = false; export let selected: $$Props['selected'] = false;
export let selectable: $$Props['selectable'] = undefined; export let selectable: $$Props['selectable'] = undefined;
export let deletable: $$Props['deletable'] = undefined;
export let hidden: $$Props['hidden'] = false; export let hidden: $$Props['hidden'] = false;
export let label: $$Props['label'] = undefined; export let label: $$Props['label'] = undefined;
export let labelStyle: $$Props['labelStyle'] = undefined; export let labelStyle: $$Props['labelStyle'] = undefined;
@@ -49,13 +50,15 @@
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
edgeclick: { edge: Edge; event: MouseEvent | TouchEvent }; edgeclick: { edge: Edge; event: MouseEvent | TouchEvent };
edgecontextmenu: { edge: Edge; event: MouseEvent }; edgecontextmenu: { edge: Edge; event: MouseEvent };
edgemouseenter: { edge: Edge; event: MouseEvent };
edgemouseleave: { edge: Edge; event: MouseEvent };
}>(); }>();
$: edgeType = type || 'default'; $: edgeType = type || 'default';
$: edgeComponent = $edgeTypes[edgeType] || BezierEdgeInternal; $: edgeComponent = $edgeTypes[edgeType] || BezierEdgeInternal;
$: markerStartUrl = markerStart ? `url('#${getMarkerId(markerStart, $flowId)}')` : undefined; $: markerStartUrl = markerStart ? `url('#${getMarkerId(markerStart, $flowId)}')` : undefined;
$: markerEndUrl = markerEnd ? `url('#${getMarkerId(markerEnd, $flowId)}')` : undefined; $: markerEndUrl = markerEnd ? `url('#${getMarkerId(markerEnd, $flowId)}')` : undefined;
$: isSelectable = selectable || ($elementsSelectable && typeof selectable === 'undefined'); $: isSelectable = selectable ?? $elementsSelectable;
const handleEdgeSelect = useHandleEdgeSelect(); const handleEdgeSelect = useHandleEdgeSelect();
@@ -68,11 +71,12 @@
} }
} }
function onContextMenu(event: MouseEvent) { type EdgeMouseEvent = 'edgecontextmenu' | 'edgemouseenter' | 'edgemouseleave';
function onMouseEvent(event: MouseEvent, type: EdgeMouseEvent) {
const edge = $edgeLookup.get(id); const edge = $edgeLookup.get(id);
if (edge) { if (edge) {
dispatch('edgecontextmenu', { event, edge }); dispatch(type, { event, edge });
} }
} }
</script> </script>
@@ -88,7 +92,15 @@
class:selectable={isSelectable} class:selectable={isSelectable}
data-id={id} data-id={id}
on:click={onClick} on:click={onClick}
on:contextmenu={onContextMenu} on:contextmenu={(e) => {
onMouseEvent(e, 'edgecontextmenu');
}}
on:mouseenter={(e) => {
onMouseEvent(e, 'edgemouseenter');
}}
on:mouseleave={(e) => {
onMouseEvent(e, 'edgemouseleave');
}}
aria-label={ariaLabel === null aria-label={ariaLabel === null
? undefined ? undefined
: ariaLabel : ariaLabel
@@ -114,6 +126,8 @@
{data} {data}
{style} {style}
{interactionWidth} {interactionWidth}
selectable={isSelectable}
deletable={deletable ?? true}
type={edgeType} type={edgeType}
sourceHandleId={sourceHandle} sourceHandleId={sourceHandle}
targetHandleId={targetHandle} targetHandleId={targetHandle}
@@ -22,6 +22,7 @@
export let draggable: $$Props['draggable'] = undefined; export let draggable: $$Props['draggable'] = undefined;
export let selectable: $$Props['selectable'] = undefined; export let selectable: $$Props['selectable'] = undefined;
export let connectable: $$Props['connectable'] = true; export let connectable: $$Props['connectable'] = true;
export let deletable: $$Props['deletable'] = true;
export let hidden: $$Props['hidden'] = false; export let hidden: $$Props['hidden'] = false;
export let dragging: boolean = false; export let dragging: boolean = false;
export let resizeObserver: $$Props['resizeObserver'] = null; export let resizeObserver: $$Props['resizeObserver'] = null;
@@ -43,6 +44,8 @@
export let height: $$Props['height'] = undefined; export let height: $$Props['height'] = undefined;
export let dragHandle: $$Props['dragHandle'] = undefined; export let dragHandle: $$Props['dragHandle'] = undefined;
export let initialized: $$Props['initialized'] = false; export let initialized: $$Props['initialized'] = false;
export let parentId: $$Props['parentId'] = undefined;
let className: string = ''; let className: string = '';
export { className as class }; export { className as class };
@@ -192,11 +195,15 @@
{data} {data}
{id} {id}
{selected} {selected}
{selectable}
{deletable}
{sourcePosition} {sourcePosition}
{targetPosition} {targetPosition}
{zIndex} {zIndex}
{dragging} {dragging}
{draggable}
{dragHandle} {dragHandle}
{parentId}
type={nodeType} type={nodeType}
isConnectable={$connectableStore} isConnectable={$connectableStore}
positionAbsoluteX={positionX} positionAbsoluteX={positionX}
@@ -10,6 +10,7 @@ export type NodeWrapperProps = Pick<
| 'dragging' | 'dragging'
| 'selected' | 'selected'
| 'selectable' | 'selectable'
| 'deletable'
| 'style' | 'style'
| 'type' | 'type'
| 'sourcePosition' | 'sourcePosition'
@@ -20,6 +21,7 @@ export type NodeWrapperProps = Pick<
| 'height' | 'height'
| 'initialWidth' | 'initialWidth'
| 'initialHeight' | 'initialHeight'
| 'parentId'
> & { > & {
measuredWidth?: number; measuredWidth?: number;
measuredHeight?: number; measuredHeight?: number;
@@ -6,16 +6,8 @@
type $$Props = EdgeProps; type $$Props = EdgeProps;
// these props are not used in this edge, but passed to every custom edge component
export let id: $$Props['id'] = '';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'default';
export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined;
export let label: $$Props['label'] = undefined; export let label: $$Props['label'] = undefined;
export let labelStyle: $$Props['labelStyle'] = undefined; export let labelStyle: $$Props['labelStyle'] = undefined;
export let data: $$Props['data'] = undefined;
export let style: $$Props['style'] = undefined; export let style: $$Props['style'] = undefined;
export let markerStart: $$Props['markerStart'] = undefined; export let markerStart: $$Props['markerStart'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined; export let markerEnd: $$Props['markerEnd'] = undefined;
@@ -24,12 +16,10 @@
export let sourceX: $$Props['sourceX']; export let sourceX: $$Props['sourceX'];
export let sourceY: $$Props['sourceY']; export let sourceY: $$Props['sourceY'];
export let sourcePosition: $$Props['sourcePosition']; export let sourcePosition: $$Props['sourcePosition'];
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
export let targetX: $$Props['targetX']; export let targetX: $$Props['targetX'];
export let targetY: $$Props['targetY']; export let targetY: $$Props['targetY'];
export let targetPosition: $$Props['targetPosition']; export let targetPosition: $$Props['targetPosition'];
export let targetHandleId: $$Props['targetHandleId'] = undefined;
$: [path, labelX, labelY] = getBezierPath({ $: [path, labelX, labelY] = getBezierPath({
sourceX, sourceX,
@@ -40,16 +30,8 @@
targetPosition targetPosition
}); });
// hopefully with Svelte5, we don't need this kind of workaround anymore // this is a workaround for suppressing the warning about unused props
id; $$restProps;
source;
target;
type;
animated;
selected;
data;
sourceHandleId;
targetHandleId;
</script> </script>
<BaseEdge <BaseEdge
@@ -6,17 +6,8 @@
type $$Props = EdgeProps; type $$Props = EdgeProps;
// these props are not used in this edge, but passed to every custom edge component
export let id: $$Props['id'] = '';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'smoothstep';
export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined;
export let label: $$Props['label'] = undefined; export let label: $$Props['label'] = undefined;
export let labelStyle: $$Props['labelStyle'] = undefined; export let labelStyle: $$Props['labelStyle'] = undefined;
export let data: $$Props['data'] = undefined;
export let style: $$Props['style'] = undefined; export let style: $$Props['style'] = undefined;
export let markerStart: $$Props['markerStart'] = undefined; export let markerStart: $$Props['markerStart'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined; export let markerEnd: $$Props['markerEnd'] = undefined;
@@ -25,12 +16,10 @@
export let sourceX: $$Props['sourceX']; export let sourceX: $$Props['sourceX'];
export let sourceY: $$Props['sourceY']; export let sourceY: $$Props['sourceY'];
export let sourcePosition: $$Props['sourcePosition']; export let sourcePosition: $$Props['sourcePosition'];
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
export let targetX: $$Props['targetX']; export let targetX: $$Props['targetX'];
export let targetY: $$Props['targetY']; export let targetY: $$Props['targetY'];
export let targetPosition: $$Props['targetPosition']; export let targetPosition: $$Props['targetPosition'];
export let targetHandleId: $$Props['targetHandleId'] = undefined;
$: [path, labelX, labelY] = getSmoothStepPath({ $: [path, labelX, labelY] = getSmoothStepPath({
sourceX, sourceX,
@@ -41,15 +30,8 @@
targetPosition targetPosition
}); });
id; // this is a workaround for suppressing the warning about unused props
source; $$restProps;
target;
type;
animated;
selected;
data;
sourceHandleId;
targetHandleId;
</script> </script>
<BaseEdge <BaseEdge
@@ -6,16 +6,7 @@
type $$Props = EdgeProps; type $$Props = EdgeProps;
// these props are not used in this edge, but passed to every custom edge component
export let id: $$Props['id'] = '';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'step';
export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined;
export let label: $$Props['label'] = undefined; export let label: $$Props['label'] = undefined;
export let data: $$Props['data'] = undefined;
export let labelStyle: $$Props['labelStyle'] = undefined; export let labelStyle: $$Props['labelStyle'] = undefined;
export let style: $$Props['style'] = undefined; export let style: $$Props['style'] = undefined;
export let markerStart: $$Props['markerStart'] = undefined; export let markerStart: $$Props['markerStart'] = undefined;
@@ -25,12 +16,10 @@
export let sourceX: $$Props['sourceX']; export let sourceX: $$Props['sourceX'];
export let sourceY: $$Props['sourceY']; export let sourceY: $$Props['sourceY'];
export let sourcePosition: $$Props['sourcePosition']; export let sourcePosition: $$Props['sourcePosition'];
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
export let targetX: $$Props['targetX']; export let targetX: $$Props['targetX'];
export let targetY: $$Props['targetY']; export let targetY: $$Props['targetY'];
export let targetPosition: $$Props['targetPosition']; export let targetPosition: $$Props['targetPosition'];
export let targetHandleId: $$Props['targetHandleId'] = undefined;
$: [path, labelX, labelY] = getSmoothStepPath({ $: [path, labelX, labelY] = getSmoothStepPath({
sourceX, sourceX,
@@ -42,15 +31,8 @@
borderRadius: 0 borderRadius: 0
}); });
id; // this is a workaround for suppressing the warning about unused props
source; $$restProps;
target;
type;
animated;
selected;
data;
sourceHandleId;
targetHandleId;
</script> </script>
<BaseEdge <BaseEdge
@@ -6,17 +6,8 @@
type $$Props = EdgeProps; type $$Props = EdgeProps;
// these props are not used in this edge, but passed to every custom edge component
export let id: $$Props['id'] = '';
export let source: $$Props['source'] = '';
export let target: $$Props['target'] = '';
export let type: $$Props['type'] = 'straight';
export let animated: $$Props['animated'] = undefined;
export let selected: $$Props['selected'] = undefined;
export let label: $$Props['label'] = undefined; export let label: $$Props['label'] = undefined;
export let labelStyle: $$Props['labelStyle'] = undefined; export let labelStyle: $$Props['labelStyle'] = undefined;
export let data: $$Props['data'] = undefined;
export let style: $$Props['style'] = undefined; export let style: $$Props['style'] = undefined;
export let markerStart: $$Props['markerStart'] = undefined; export let markerStart: $$Props['markerStart'] = undefined;
export let markerEnd: $$Props['markerEnd'] = undefined; export let markerEnd: $$Props['markerEnd'] = undefined;
@@ -24,13 +15,9 @@
export let sourceX: $$Props['sourceX']; export let sourceX: $$Props['sourceX'];
export let sourceY: $$Props['sourceY']; export let sourceY: $$Props['sourceY'];
export let sourcePosition: $$Props['sourcePosition'];
export let sourceHandleId: $$Props['sourceHandleId'] = undefined;
export let targetX: $$Props['targetX']; export let targetX: $$Props['targetX'];
export let targetY: $$Props['targetY']; export let targetY: $$Props['targetY'];
export let targetPosition: $$Props['targetPosition'];
export let targetHandleId: $$Props['targetHandleId'] = undefined;
$: [path, labelX, labelY] = getStraightPath({ $: [path, labelX, labelY] = getStraightPath({
sourceX, sourceX,
@@ -39,17 +26,8 @@
targetY targetY
}); });
id; // this is a workaround for suppressing the warning about unused props
source; $$restProps;
target;
type;
animated;
selected;
data;
sourcePosition;
targetPosition;
sourceHandleId;
targetHandleId;
</script> </script>
<BaseEdge <BaseEdge
@@ -7,36 +7,13 @@
interface $$Props extends NodeProps {} interface $$Props extends NodeProps {}
export let data: $$Props['data'] = { label: 'Node' }; export let data: $$Props['data'] = { label: 'Node' };
export let targetPosition: $$Props['targetPosition'] = Position.Top; export let targetPosition: $$Props['targetPosition'] = undefined;
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom; export let sourcePosition: $$Props['sourcePosition'] = undefined;
// unused props - we need to list them here in order to prevent warnings // this is a workaround for suppressing the warning about unused props
export let id: $$Props['id'] = ''; $$restProps;
export let width: $$Props['width'] = undefined;
export let height: $$Props['height'] = undefined;
export let selected: $$Props['selected'] = undefined;
export let type: $$Props['type'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this
id;
width;
height;
selected;
type;
zIndex;
dragging;
dragHandle;
positionAbsoluteX;
positionAbsoluteY;
isConnectable;
</script> </script>
<Handle type="target" position={targetPosition} /> <Handle type="target" position={targetPosition ?? Position.Top} />
{data?.label} {data?.label}
<Handle type="source" position={sourcePosition} /> <Handle type="source" position={sourcePosition ?? Position.Bottom} />
@@ -1,37 +1,9 @@
<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 {}
// unused props - we need to list them here in order to prevent warnings // this is a workaround for suppressing the warning about unused props
export let id: $$Props['id'] = ''; $$restProps;
export let width: $$Props['width'] = undefined;
export let height: $$Props['height'] = undefined;
export let data: $$Props['data'] = {};
export let selected: $$Props['selected'] = undefined;
export let sourcePosition: $$Props['sourcePosition'] = undefined;
export let targetPosition: $$Props['targetPosition'] = undefined;
export let type: $$Props['type'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this
id;
width;
height;
data;
selected;
sourcePosition;
targetPosition;
type;
zIndex;
dragging;
dragHandle;
positionAbsoluteX;
positionAbsoluteY;
isConnectable;
</script> </script>
@@ -7,36 +7,11 @@
interface $$Props extends NodeProps {} interface $$Props extends NodeProps {}
export let data: $$Props['data'] = { label: 'Node' }; export let data: $$Props['data'] = { label: 'Node' };
export let sourcePosition: $$Props['sourcePosition'] = Position.Bottom; export let sourcePosition: $$Props['sourcePosition'] = undefined;
// unused props - we need to list them here in order to prevent warnings // this is a workaround for suppressing the warning about unused props
export let id: $$Props['id'] = ''; $$restProps;
export let width: $$Props['width'] = undefined;
export let height: $$Props['height'] = undefined;
export let selected: $$Props['selected'] = undefined;
export let targetPosition: $$Props['targetPosition'] = undefined;
export let type: $$Props['type'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this
id;
width;
height;
selected;
targetPosition;
type;
zIndex;
dragging;
dragHandle;
positionAbsoluteX;
positionAbsoluteY;
isConnectable;
</script> </script>
{data?.label} {data?.label}
<Handle type="source" position={sourcePosition} /> <Handle type="source" position={sourcePosition ?? Position.Bottom} />
@@ -7,36 +7,11 @@
interface $$Props extends NodeProps {} interface $$Props extends NodeProps {}
export let data: $$Props['data'] = { label: 'Node' }; export let data: $$Props['data'] = { label: 'Node' };
export let targetPosition: $$Props['targetPosition'] = Position.Top; export let targetPosition: $$Props['targetPosition'] = undefined;
// unused props - we need to list them here in order to prevent warnings // this is a workaround for suppressing the warning about unused props
export let id: $$Props['id'] = ''; $$restProps;
export let width: $$Props['width'] = undefined;
export let height: $$Props['height'] = undefined;
export let selected: $$Props['selected'] = undefined;
export let sourcePosition: $$Props['sourcePosition'] = undefined;
export let type: $$Props['type'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'];
export let zIndex: $$Props['zIndex'];
// @todo: there must be a better way to do this
id;
width;
height;
selected;
sourcePosition;
type;
zIndex;
dragging;
dragHandle;
positionAbsoluteX;
positionAbsoluteY;
isConnectable;
</script> </script>
{data?.label} {data?.label}
<Handle type="target" position={targetPosition} /> <Handle type="target" position={targetPosition ?? Position.Top} />
@@ -11,7 +11,8 @@
const { const {
visibleEdges, visibleEdges,
edgesInitialized, edgesInitialized,
edges: { setDefaultOptions } edges: { setDefaultOptions },
elementsSelectable
} = useStore(); } = useStore();
onMount(() => { onMount(() => {
@@ -33,7 +34,8 @@
style={edge.style} style={edge.style}
animated={edge.animated} animated={edge.animated}
selected={edge.selected} selected={edge.selected}
selectable={edge.selectable} selectable={edge.selectable ?? $elementsSelectable}
deletable={edge.deletable}
hidden={edge.hidden} hidden={edge.hidden}
label={edge.label} label={edge.label}
labelStyle={edge.labelStyle} labelStyle={edge.labelStyle}
@@ -54,6 +56,8 @@
zIndex={edge.zIndex} zIndex={edge.zIndex}
on:edgeclick on:edgeclick
on:edgecontextmenu on:edgecontextmenu
on:edgemouseenter
on:edgemouseleave
/> />
{/each} {/each}
@@ -62,6 +62,7 @@
node.connectable || node.connectable ||
($nodesConnectable && typeof node.connectable === 'undefined') ($nodesConnectable && typeof node.connectable === 'undefined')
)} )}
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} positionOriginX={posOrigin.x ?? 0}
@@ -82,6 +83,7 @@
initialHeight={node.initialHeight} initialHeight={node.initialHeight}
measuredWidth={node.measured.width} measuredWidth={node.measured.width}
measuredHeight={node.measured.height} measuredHeight={node.measured.height}
parentId={node.parentId}
{resizeObserver} {resizeObserver}
on:nodeclick on:nodeclick
on:nodemouseenter on:nodemouseenter
@@ -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 />
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import cc from 'classcat'; import cc from 'classcat';
import type { PanelProps } from './types'; import type { PanelProps } from './types';
import { useStore } from '$lib/store';
type $$Props = PanelProps; type $$Props = PanelProps;
@@ -10,9 +11,16 @@
let className: $$Props['class'] = undefined; let className: $$Props['class'] = undefined;
export { className as class }; export { className as class };
const { selectionRectMode } = useStore();
$: positionClasses = `${position}`.split('-'); $: positionClasses = `${position}`.split('-');
</script> </script>
<div class={cc(['svelte-flow__panel', className, ...positionClasses])} {style} {...$$restProps}> <div
class={cc(['svelte-flow__panel', className, ...positionClasses])}
{style}
style:pointer-events={$selectionRectMode ? 'none' : ''}
{...$$restProps}
>
<slot /> <slot />
</div> </div>
@@ -2,7 +2,7 @@
import { onMount, hasContext } from 'svelte'; import { onMount, hasContext } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import cc from 'classcat'; import cc from 'classcat';
import { ConnectionMode, PanOnScrollMode, type Viewport } from '@xyflow/system'; import { ConnectionMode, PanOnScrollMode } from '@xyflow/system';
import { Zoom } from '$lib/container/Zoom'; import { Zoom } from '$lib/container/Zoom';
import { Pane } from '$lib/container/Pane'; import { Pane } from '$lib/container/Pane';
@@ -28,7 +28,7 @@
export let fitViewOptions: $$Props['fitViewOptions'] = undefined; export let fitViewOptions: $$Props['fitViewOptions'] = undefined;
export let minZoom: $$Props['minZoom'] = undefined; export let minZoom: $$Props['minZoom'] = undefined;
export let maxZoom: $$Props['maxZoom'] = undefined; export let maxZoom: $$Props['maxZoom'] = undefined;
export let initialViewport: Viewport = { x: 0, y: 0, zoom: 1 }; export let initialViewport: $$Props['initialViewport'] = undefined;
export let viewport: $$Props['viewport'] = undefined; export let viewport: $$Props['viewport'] = undefined;
export let nodeTypes: $$Props['nodeTypes'] = undefined; export let nodeTypes: $$Props['nodeTypes'] = undefined;
export let edgeTypes: $$Props['edgeTypes'] = undefined; export let edgeTypes: $$Props['edgeTypes'] = undefined;
@@ -89,6 +89,8 @@
let clientWidth: number; let clientWidth: number;
let clientHeight: number; let clientHeight: number;
const initViewport = $viewport || initialViewport;
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 });
@@ -203,7 +205,7 @@
{zoomActivationKey} {zoomActivationKey}
/> />
<Zoom <Zoom
{initialViewport} initialViewport={initViewport}
{onMoveStart} {onMoveStart}
{onMove} {onMove}
{onMoveEnd} {onMoveEnd}
@@ -222,7 +224,13 @@
{selectionOnDrag} {selectionOnDrag}
> >
<ViewportComponent> <ViewportComponent>
<EdgeRenderer on:edgeclick on:edgecontextmenu {defaultEdgeOptions} /> <EdgeRenderer
on:edgeclick
on:edgecontextmenu
on:edgemouseenter
on:edgemouseleave
{defaultEdgeOptions}
/>
<ConnectionLine <ConnectionLine
containerStyle={connectionLineContainerStyle} containerStyle={connectionLineContainerStyle}
style={connectionLineStyle} style={connectionLineStyle}
@@ -8,7 +8,7 @@
type $$Props = ZoomProps; type $$Props = ZoomProps;
export let initialViewport: $$Props['initialViewport']; export let initialViewport: $$Props['initialViewport'] = undefined;
export let onMoveStart: $$Props['onMoveStart'] = undefined; export let onMoveStart: $$Props['onMoveStart'] = undefined;
export let onMove: $$Props['onMove'] = undefined; export let onMove: $$Props['onMove'] = undefined;
export let onMoveEnd: $$Props['onMoveEnd'] = undefined; export let onMoveEnd: $$Props['onMoveEnd'] = undefined;
@@ -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 { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode, Viewport } from '@xyflow/system'; import type { OnMoveStart, OnMove, OnMoveEnd, PanOnScrollMode, Viewport } from '@xyflow/system';
export type ZoomProps = { export type ZoomProps = {
initialViewport: Viewport; initialViewport?: Viewport;
panOnScrollMode: PanOnScrollMode; panOnScrollMode: PanOnScrollMode;
onMove?: OnMove; onMove?: OnMove;
onMoveStart?: OnMoveStart; onMoveStart?: OnMoveStart;
+1 -2
View File
@@ -124,6 +124,5 @@ export {
getIncomers, getIncomers,
getOutgoers, getOutgoers,
getConnectedEdges, getConnectedEdges,
addEdge, addEdge
updateEdge
} from '@xyflow/system'; } from '@xyflow/system';
@@ -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';
@@ -88,7 +88,9 @@ export function getDerivedConnectionProps(
const fromX = (fromNode?.internals.positionAbsolute.x ?? 0) + fromHandleX; const fromX = (fromNode?.internals.positionAbsolute.x ?? 0) + fromHandleX;
const fromY = (fromNode?.internals.positionAbsolute.y ?? 0) + fromHandleY; const fromY = (fromNode?.internals.positionAbsolute.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined; const toPosition =
connection.connectionEndHandle?.position ??
(fromPosition ? oppositePosition[fromPosition] : undefined);
const pathParams = { const pathParams = {
sourceX: fromX, sourceX: fromX,
+2 -2
View File
@@ -210,10 +210,10 @@ export function createStore({
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) { function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
const resetNodes = resetSelectedElements(params?.nodes || get(store.nodes)); const resetNodes = resetSelectedElements(params?.nodes || get(store.nodes));
if (resetNodes) store.nodes.update((nds) => nds); if (resetNodes) store.nodes.set(get(store.nodes));
const resetEdges = resetSelectedElements(params?.edges || get(store.edges)); const resetEdges = resetSelectedElements(params?.edges || get(store.edges));
if (resetEdges) store.edges.update((nds) => nds); if (resetEdges) store.edges.set(get(store.edges));
} }
store.deleteKeyPressed.subscribe(async (deleteKeyPressed) => { store.deleteKeyPressed.subscribe(async (deleteKeyPressed) => {
+7 -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.
@@ -133,6 +138,7 @@ export type EdgeLayouted = Pick<
| 'animated' | 'animated'
| 'selected' | 'selected'
| 'selectable' | 'selectable'
| 'deletable'
| 'label' | 'label'
| 'labelStyle' | 'labelStyle'
| 'interactionWidth' | 'interactionWidth'
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.26", "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",
@@ -42,10 +42,10 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@types/d3": "^7.4.0", "@types/d3-drag": "^3.0.7",
"@types/d3-drag": "^3.0.1", "@types/d3-selection": "^3.0.10",
"@types/d3-selection": "^3.0.3", "@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.1", "@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0", "d3-drag": "^3.0.0",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0" "d3-zoom": "^3.0.0"
+6 -1
View File
@@ -1,5 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3'; import type { Selection as D3Selection } from 'd3-selection';
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
import type { ZoomBehavior } from 'd3-zoom';
// this is needed for the Selection type to include the transition function :/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { Transition } from 'd3-transition';
import type { XYPosition, Rect } from './utils'; import type { XYPosition, Rect } from './utils';
import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes'; import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes';
+1
View File
@@ -16,6 +16,7 @@ export type ConnectingHandle = {
nodeId: string; nodeId: string;
type: HandleType; type: HandleType;
handleId?: string | null; handleId?: string | null;
position?: Position | null;
}; };
export type ConnectionHandle = { export type ConnectionHandle = {
+12 -1
View File
@@ -88,7 +88,18 @@ export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType &
*/ */
export type NodeProps<NodeType extends NodeBase> = Pick< export type NodeProps<NodeType extends NodeBase> = Pick<
NodeType, NodeType,
'id' | 'data' | 'width' | 'height' | 'sourcePosition' | 'targetPosition' | 'selected' | 'dragHandle' | 'id'
| 'data'
| 'width'
| 'height'
| 'sourcePosition'
| 'targetPosition'
| 'selected'
| 'dragHandle'
| 'selectable'
| 'deletable'
| 'draggable'
| 'parentId'
> & > &
Required<Pick<NodeType, 'type' | 'dragging' | 'zIndex'>> & { Required<Pick<NodeType, 'type' | 'dragging' | 'zIndex'>> & {
/** whether a node is connectable or not */ /** whether a node is connectable or not */
+5 -6
View File
@@ -71,6 +71,7 @@ export const getHandleBounds = (
selector: string, selector: string,
node: InternalNodeBase, node: InternalNodeBase,
nodeElement: HTMLDivElement, nodeElement: HTMLDivElement,
nodeBounds: DOMRect,
zoom: number, zoom: number,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0]
): HandleElement[] | null => { ): HandleElement[] | null => {
@@ -82,11 +83,9 @@ export const getHandleBounds = (
const handlesArray = Array.from(handles) as HTMLDivElement[]; const handlesArray = Array.from(handles) as HTMLDivElement[];
// @todo can't we use the node dimensions here?
const nodeBounds = nodeElement.getBoundingClientRect();
const nodeOffset = { const nodeOffset = {
x: nodeBounds.width * nodeOrigin[0], x: nodeBounds.left + nodeBounds.width * nodeOrigin[0],
y: nodeBounds.height * nodeOrigin[1], y: nodeBounds.top + nodeBounds.height * nodeOrigin[1],
}; };
return handlesArray.map((handle): HandleElement => { return handlesArray.map((handle): HandleElement => {
@@ -95,8 +94,8 @@ export const getHandleBounds = (
return { return {
id: handle.getAttribute('data-handleid'), id: handle.getAttribute('data-handleid'),
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 - nodeOffset.x) / zoom,
y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom, y: (handleBounds.top - nodeOffset.y) / zoom,
...getDimensions(handle), ...getDimensions(handle),
}; };
}); });
+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;
+10 -5
View File
@@ -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,8 +56,10 @@ 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 [sourceX, sourceY] = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
const [targetX, targetY] = getHandlePosition(targetNode, targetHandle, targetPosition);
return { return {
sourceX, sourceX,
@@ -96,10 +96,15 @@ function toHandleBounds(handles?: NodeHandle[]) {
}; };
} }
function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] { export function getHandlePosition(
node: InternalNodeBase,
handle: HandleElement | null,
fallbackPosition: Position = Position.Left
): number[] {
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);
const position = handle?.position ?? fallbackPosition;
switch (position) { switch (position) {
case Position.Top: case Position.Top:
+11 -4
View File
@@ -250,10 +250,17 @@ export function evaluateAbsolutePosition(
): XYPosition { ): XYPosition {
const positionAbsolute = { ...position }; const positionAbsolute = { ...position };
const parent = nodeLookup.get(parentId); while (nextParentId) {
if (parent) { const parent = nodeLookup.get(nextParentId);
positionAbsolute.x += parent.internals.positionAbsolute.x - dimensions.width * nodeOrigin[0]; nextParentId = parent?.parentId;
positionAbsolute.y += parent.internals.positionAbsolute.y - dimensions.height * nodeOrigin[1];
if (parent) {
const origin = parent.origin || nodeOrigin;
const xOffset = (parent.measured.width ?? 0) * origin[0];
const yOffset = (parent.measured.height ?? 0) * origin[1];
positionAbsolute.x += parent.internals.positionAbsolute.x - dimensions.width * nodeOrigin[0];
positionAbsolute.y += parent.internals.positionAbsolute.y - dimensions.height * nodeOrigin[1];
}
} }
return positionAbsolute; return positionAbsolute;
+5 -4
View File
@@ -282,14 +282,15 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
); );
if (doUpdate) { if (doUpdate) {
const positionAbsolute = getNodePositionWithOrigin(node, nodeOrigin); const nodeBounds = update.nodeElement.getBoundingClientRect();
node.measured = dimensions; node.measured = dimensions;
node.internals = { node.internals = {
...node.internals, ...node.internals,
positionAbsolute: getNodePositionWithOrigin(node, nodeOrigin), positionAbsolute,
handleBounds: { handleBounds: {
source: getHandleBounds('.source', node, update.nodeElement, zoom), source: getHandleBounds('.source', update.nodeElement, nodeBounds, zoom),
target: getHandleBounds('.target', node, update.nodeElement, zoom), target: getHandleBounds('.target', update.nodeElement, nodeBounds, zoom),
}, },
}; };
if (node.parentId) { if (node.parentId) {
+12 -1
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) {
@@ -263,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);
} }
@@ -276,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();
@@ -300,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;
} }
+12 -12
View File
@@ -13,6 +13,7 @@ import {
type IsValidConnection, type IsValidConnection,
type ConnectionHandle, type ConnectionHandle,
NodeLookup, NodeLookup,
Position,
} from '../types'; } from '../types';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils'; import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils';
@@ -36,7 +37,7 @@ export type OnPointerDownParams = {
onConnect?: OnConnect; onConnect?: OnConnect;
onConnectEnd?: OnConnectEnd; onConnectEnd?: OnConnectEnd;
isValidConnection?: IsValidConnection; isValidConnection?: IsValidConnection;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void; onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
getTransform: () => Transform; getTransform: () => Transform;
getConnectionStartHandle: () => ConnectingHandle | null; getConnectionStartHandle: () => ConnectingHandle | null;
}; };
@@ -89,7 +90,7 @@ function onPointerDown(
onConnect, onConnect,
onConnectEnd, onConnectEnd,
isValidConnection = alwaysValid, isValidConnection = alwaysValid,
onEdgeUpdateEnd, onReconnectEnd,
updateConnection, updateConnection,
getTransform, getTransform,
getConnectionStartHandle, getConnectionStartHandle,
@@ -138,12 +139,12 @@ function onPointerDown(
nodeId, nodeId,
handleId, handleId,
type: handleType, type: handleType,
position: (clickedHandle?.getAttribute('data-handlepos') as Position) || Position.Top,
}; };
updateConnection({ updateConnection({
connectionPosition, connectionPosition,
connectionStatus: null, connectionStatus: null,
// connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
connectionStartHandle, connectionStartHandle,
connectionEndHandle: null, connectionEndHandle: null,
}); });
@@ -211,7 +212,7 @@ function onPointerDown(
onConnectEnd?.(event); onConnectEnd?.(event);
if (edgeUpdaterType) { if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event); onReconnectEnd?.(event);
} }
cancelConnection(); cancelConnection();
@@ -297,15 +298,14 @@ 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); result.endHandle = {
} nodeId: handleNodeId as string,
handleId,
type: handleType as HandleType,
position: handleToCheck.getAttribute('data-handlepos') as Position,
};
} }
return result; return result;
+7 -5
View File
@@ -1,3 +1,4 @@
import { getHandlePosition } from '../utils';
import { import {
ConnectionStatus, ConnectionStatus,
type HandleType, type HandleType,
@@ -16,14 +17,15 @@ export function getHandles(
type: HandleType, type: HandleType,
currentHandle: string currentHandle: string
): ConnectionHandle[] { ): ConnectionHandle[] {
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => { return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, handle) => {
if (`${node.id}-${h.id}-${type}` !== currentHandle) { if (`${node.id}-${handle.id}-${type}` !== currentHandle) {
const [x, y] = getHandlePosition(node, handle);
res.push({ res.push({
id: h.id || null, id: handle.id || null,
type, type,
nodeId: node.id, nodeId: node.id,
x: node.internals.positionAbsolute.x + h.x + h.width / 2, x,
y: node.internals.positionAbsolute.y + h.y + h.height / 2, y,
}); });
} }
return res; return res;
+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;
+7496 -6107
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 }) => {
+622 -372
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -13,15 +13,15 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.42.1", "@playwright/test": "^1.44.1",
"@types/node": "^18.7.16" "@types/node": "^20.14.6"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-react": "^1.42.1", "@playwright/experimental-ct-react": "^1.44.1",
"@types/react": "^18.2.31", "@types/react": "^18.3.3",
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.3.0",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.3.1",
"typescript": "^5.2.2" "typescript": "^5.4.5"
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@ export function sharedConfigWithPort({ port, framework }: ConfigParams): Playwri
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: process.env.CI ? 'dot' : 'list',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
+2 -2
View File
@@ -7,7 +7,7 @@
"devDependencies": { "devDependencies": {
"eslint": "^8.22.0", "eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-config-turbo": "latest", "eslint-config-turbo": "^2.0.3",
"eslint-plugin-react": "latest" "eslint-plugin-react": "^7.33.2"
} }
} }
+6 -6
View File
@@ -7,12 +7,12 @@
"main": "src/index.js", "main": "src/index.js",
"module": "src/index.js", "module": "src/index.js",
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^25.0.0", "@rollup/plugin-commonjs": "^25.0.8",
"@rollup/plugin-node-resolve": "^15.1.0", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.6",
"@rollup/plugin-terser": "^0.4.3", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "11.0.0", "@rollup/plugin-typescript": "11.1.6",
"rollup": "^3.23.0", "rollup": "^4.18.0",
"rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-peer-deps-external": "^2.2.4",
"typescript": "^5.1.3" "typescript": "^5.1.3"
} }
+3 -3
View File
@@ -1,6 +1,7 @@
{ {
"$schema": "https://turborepo.org/schema.json", "$schema": "https://turborepo.org/schema.json",
"pipeline": { "globalEnv": ["NODE_ENV"],
"tasks": {
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"outputs": ["dist/**"] "outputs": ["dist/**"]
@@ -20,6 +21,5 @@
"dependsOn": ["^build"], "dependsOn": ["^build"],
"outputs": [] "outputs": []
} }
}, }
"globalEnv": ["NODE_ENV"]
} }