Merge pull request #3786 from xyflow/next
React Flow 12.0.0-next.6, Svelte Flow 0.0.33
This commit is contained in:
@@ -13,7 +13,6 @@ describe('useEdges.cy.tsx', () => {
|
|||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
|
||||||
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
|
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ const initialNodes: Node[] = nodes.map((n) => ({
|
|||||||
|
|
||||||
const expectedNodes: Node[] = initialNodes.map((n) => ({
|
const expectedNodes: Node[] = initialNodes.map((n) => ({
|
||||||
...n,
|
...n,
|
||||||
positionAbsolute: n.position,
|
computed: {
|
||||||
...nodeDimensions,
|
positionAbsolute: n.position,
|
||||||
|
...nodeDimensions,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('useNodes.cy.tsx', () => {
|
describe('useNodes.cy.tsx', () => {
|
||||||
@@ -29,7 +31,6 @@ describe('useNodes.cy.tsx', () => {
|
|||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
|
||||||
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
|
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -156,20 +156,22 @@ describe('applyChanges Testing', () => {
|
|||||||
expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight });
|
expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resets nodes/edges', () => {
|
it('replaces nodes/edges', () => {
|
||||||
const nodesLength = nodes.length;
|
const nodesLength = nodes.length;
|
||||||
const nodeChanges: NodeChange[] = [{ type: 'reset', item: nodes[0] }];
|
const nodeChanges: NodeChange[] = [{ type: 'replace', id: nodes[0].id, item: nodes[1] }];
|
||||||
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||||
|
|
||||||
expect(nodes.length).length.to.be.equal(nodesLength);
|
expect(nodes.length).length.to.be.equal(nodesLength);
|
||||||
expect(nextNodes.length).to.be.equal(nodeChanges.length);
|
expect(nextNodes.length).to.be.equal(nodesLength);
|
||||||
|
expect(nextNodes[0]).to.be.deep.equal(nodes[1]);
|
||||||
|
|
||||||
const edgesLength = edges.length;
|
const edgesLength = edges.length;
|
||||||
const edgeChange: EdgeChange[] = [{ type: 'reset', item: edges[0] }];
|
const edgeChange: EdgeChange[] = [{ type: 'replace', id: edges[0].id, item: edges[1] }];
|
||||||
const nextEdges = applyEdgeChanges(edgeChange, edges);
|
const nextEdges = applyEdgeChanges(edgeChange, edges);
|
||||||
|
|
||||||
expect(edges.length).length.to.be.equal(edgesLength);
|
expect(edges.length).length.to.be.equal(edgesLength);
|
||||||
expect(nextEdges.length).to.be.equal(edgeChange.length);
|
expect(nextEdges.length).to.be.equal(edgesLength);
|
||||||
|
expect(nextEdges[0]).to.be.deep.equal(edges[1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import Interaction from '../examples/Interaction';
|
|||||||
import Intersection from '../examples/Intersection';
|
import Intersection from '../examples/Intersection';
|
||||||
import Layouting from '../examples/Layouting';
|
import Layouting from '../examples/Layouting';
|
||||||
import MultiFlows from '../examples/MultiFlows';
|
import MultiFlows from '../examples/MultiFlows';
|
||||||
|
import MultiSetNodes from '../examples/MultiSetNodes';
|
||||||
import NodeResizer from '../examples/NodeResizer';
|
import NodeResizer from '../examples/NodeResizer';
|
||||||
import NodeTypeChange from '../examples/NodeTypeChange';
|
import NodeTypeChange from '../examples/NodeTypeChange';
|
||||||
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
||||||
@@ -180,6 +181,11 @@ const routes: IRoute[] = [
|
|||||||
path: 'layouting',
|
path: 'layouting',
|
||||||
component: Layouting,
|
component: Layouting,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Multi setNodes',
|
||||||
|
path: 'multi-setnodes',
|
||||||
|
component: MultiSetNodes,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Multi Flows',
|
name: 'Multi Flows',
|
||||||
path: 'multiflows',
|
path: 'multiflows',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MouseEvent } from 'react';
|
import { MouseEvent, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
@@ -53,10 +53,11 @@ const initialEdges: Edge[] = [
|
|||||||
const defaultEdgeOptions = {};
|
const defaultEdgeOptions = {};
|
||||||
|
|
||||||
const BasicFlow = () => {
|
const BasicFlow = () => {
|
||||||
const instance = useReactFlow();
|
const { setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
|
||||||
|
useReactFlow();
|
||||||
|
|
||||||
const updatePos = () => {
|
const updatePos = () => {
|
||||||
instance.setNodes((nodes) =>
|
setNodes((nodes) =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
node.position = {
|
node.position = {
|
||||||
x: Math.random() * 400,
|
x: Math.random() * 400,
|
||||||
@@ -68,11 +69,11 @@ const BasicFlow = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logToObject = () => console.log(instance.toObject());
|
const logToObject = () => console.log(toObject());
|
||||||
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
|
const resetTransform = () => setViewport({ x: 0, y: 0, zoom: 1 });
|
||||||
|
|
||||||
const toggleClassnames = () => {
|
const toggleClassnames = () => {
|
||||||
instance.setNodes((nodes) =>
|
setNodes((nodes) =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
node.className = node.className === 'light' ? 'dark' : 'light';
|
node.className = node.className === 'light' ? 'dark' : 'light';
|
||||||
|
|
||||||
@@ -81,6 +82,30 @@ const BasicFlow = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteSelectedElements = useCallback(() => {
|
||||||
|
const selectedNodes = getNodes().filter((node) => node.selected);
|
||||||
|
const selectedEdges = getEdges().filter((edge) => edge.selected);
|
||||||
|
deleteElements({ nodes: selectedNodes, edges: selectedEdges });
|
||||||
|
}, [deleteElements]);
|
||||||
|
|
||||||
|
const deleteSomeElements = useCallback(() => {
|
||||||
|
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSetNodes = () => {
|
||||||
|
setNodes([
|
||||||
|
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
|
||||||
|
{ id: 'b', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateNode = () => {
|
||||||
|
updateNodeData('1', { label: 'update' });
|
||||||
|
updateNodeData('2', { label: 'update' });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
defaultNodes={initialNodes}
|
defaultNodes={initialNodes}
|
||||||
@@ -106,6 +131,11 @@ const BasicFlow = () => {
|
|||||||
<button onClick={updatePos}>change pos</button>
|
<button onClick={updatePos}>change pos</button>
|
||||||
<button onClick={toggleClassnames}>toggle classnames</button>
|
<button onClick={toggleClassnames}>toggle classnames</button>
|
||||||
<button onClick={logToObject}>toObject</button>
|
<button onClick={logToObject}>toObject</button>
|
||||||
|
|
||||||
|
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
|
||||||
|
<button onClick={deleteSomeElements}>deleteSomeElements</button>
|
||||||
|
<button onClick={onSetNodes}>setNodes</button>
|
||||||
|
<button onClick={onUpdateNode}>updateNode</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Controls,
|
||||||
|
addEdge,
|
||||||
|
Connection,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
Background,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
Panel,
|
||||||
|
useReactFlow,
|
||||||
|
ReactFlowProvider,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
const initNodes: Node[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
initNodes.push({
|
||||||
|
id: i.toString(),
|
||||||
|
data: {
|
||||||
|
label: `node ${i + 1}`,
|
||||||
|
},
|
||||||
|
position: { x: (i % 10) * 60, y: Math.floor(i / 10) * 60 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const initEdges: Edge[] = [];
|
||||||
|
|
||||||
|
const CustomNodeFlow = () => {
|
||||||
|
const { setNodes, updateNodeData } = useReactFlow();
|
||||||
|
const [nodes, , onNodesChange] = useNodesState(initNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
|
||||||
|
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||||
|
|
||||||
|
const multiSetNodes = () => {
|
||||||
|
nodes.forEach((node) =>
|
||||||
|
setNodes((nds) =>
|
||||||
|
nds.map((n) => {
|
||||||
|
if (n.id === node.id) {
|
||||||
|
return { ...n, data: { label: 'node set' } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return n;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const multiUpdateNodes = () => {
|
||||||
|
nodes.forEach((node) => updateNodeData(node.id, { label: 'node update' }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
fitView
|
||||||
|
className="multiset"
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
<Background />
|
||||||
|
<Panel>
|
||||||
|
<button onClick={multiSetNodes}>set nodes</button>
|
||||||
|
<button onClick={multiUpdateNodes}>update nodes</button>
|
||||||
|
</Panel>
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<CustomNodeFlow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.multiset .react-flow__node {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
import { memo, ChangeEventHandler } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react';
|
import { Position, NodeProps, Handle, useReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
function TextNode({ id, data }: NodeProps) {
|
function TextNode({ id, data }: NodeProps) {
|
||||||
const { updateNodeData } = useReactFlow();
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const [text, setText] = useState(data.text);
|
||||||
const onChange: ChangeEventHandler<HTMLInputElement> = (evt) => updateNodeData(id, { text: evt.target.value });
|
const updateText = (text: string) => {
|
||||||
|
// avoid jumping caret with a synchronous update
|
||||||
|
setText(text);
|
||||||
|
// update actual node data
|
||||||
|
updateNodeData(id, { text });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
<div style={{ background: '#eee', color: '#222', padding: 10, fontSize: 12, borderRadius: 10 }}>
|
||||||
<div>node {id}</div>
|
<div>node {id}</div>
|
||||||
<div>
|
<div>
|
||||||
<input onChange={onChange} value={data.text} />
|
<input onChange={(evt) => updateText(evt.target.value)} value={text} />
|
||||||
</div>
|
</div>
|
||||||
<Handle type="source" position={Position.Right} />
|
<Handle type="source" position={Position.Right} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const initEdges: Edge[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const CustomNodeFlow = () => {
|
const CustomNodeFlow = () => {
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
|
const [nodes, , onNodesChange] = useNodesState(initNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
|
||||||
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, MouseEvent, useEffect } from 'react';
|
import { useCallback, MouseEvent, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Background,
|
Background,
|
||||||
@@ -68,6 +68,7 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
getNodes,
|
getNodes,
|
||||||
getEdges,
|
getEdges,
|
||||||
deleteElements,
|
deleteElements,
|
||||||
|
updateNodeData,
|
||||||
} = useReactFlow();
|
} = useReactFlow();
|
||||||
|
|
||||||
const onPaneClick = useCallback(
|
const onPaneClick = useCallback(
|
||||||
@@ -125,12 +126,31 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
|
deleteElements({ nodes: [{ id: '2' }], edges: [{ id: 'e1-3' }] });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const edgeAdded = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addEdges({ id: 'e3-4', source: '3', target: '4' });
|
if (!edgeAdded.current) {
|
||||||
|
addEdges({ id: 'e3-4', source: '3', target: '4' });
|
||||||
|
edgeAdded.current = true;
|
||||||
|
}
|
||||||
}, [addEdges]);
|
}, [addEdges]);
|
||||||
|
|
||||||
const onResetNodes = useCallback(() => setNodesHook(initialNodes), [setNodesHook]);
|
const onResetNodes = useCallback(() => setNodesHook(initialNodes), [setNodesHook]);
|
||||||
|
|
||||||
|
const onSetNodes = () => {
|
||||||
|
setNodes([
|
||||||
|
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Node a' } },
|
||||||
|
{ id: 'b', position: { x: 0, y: 150 }, data: { label: 'Node b' } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateNode = () => {
|
||||||
|
updateNodeData('1', { label: 'update' });
|
||||||
|
updateNodeData('2', { label: 'update' });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -153,6 +173,8 @@ const UseZoomPanHelperFlow = () => {
|
|||||||
<button onClick={logNodes}>useNodes</button>
|
<button onClick={logNodes}>useNodes</button>
|
||||||
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
|
<button onClick={deleteSelectedElements}>deleteSelectedElements</button>
|
||||||
<button onClick={deleteSomeElements}>deleteSomeElements</button>
|
<button onClick={deleteSomeElements}>deleteSomeElements</button>
|
||||||
|
<button onClick={onSetNodes}>setNodes</button>
|
||||||
|
<button onClick={onUpdateNode}>updateNode</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Background />
|
<Background />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useNodes, useSvelteFlow } from '@xyflow/svelte';
|
import { useEdges, useNodes, useSvelteFlow } from '@xyflow/svelte';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
zoomIn,
|
zoomIn,
|
||||||
@@ -10,10 +10,17 @@
|
|||||||
setViewport,
|
setViewport,
|
||||||
getViewport,
|
getViewport,
|
||||||
viewport,
|
viewport,
|
||||||
toObject
|
toObject,
|
||||||
|
deleteElements
|
||||||
} = useSvelteFlow();
|
} = useSvelteFlow();
|
||||||
|
|
||||||
const nodes = useNodes();
|
const nodes = useNodes();
|
||||||
|
const edges = useEdges();
|
||||||
|
|
||||||
|
const deleteNode = () => {
|
||||||
|
$nodes.shift();
|
||||||
|
$nodes = $nodes;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside>
|
<aside>
|
||||||
@@ -25,6 +32,15 @@
|
|||||||
<button on:click={() => setCenter(0, 0)}>setCenter 0, 0</button>
|
<button on:click={() => setCenter(0, 0)}>setCenter 0, 0</button>
|
||||||
<button on:click={() => setViewport({ x: 100, y: 100, zoom: 2 })}>setViewport</button>
|
<button on:click={() => setViewport({ x: 100, y: 100, zoom: 2 })}>setViewport</button>
|
||||||
<button on:click={() => console.log(getViewport())}>getViewport</button>
|
<button on:click={() => console.log(getViewport())}>getViewport</button>
|
||||||
|
|
||||||
|
<button on:click={() => deleteElements({ edges: $edges.map((edge) => ({ id: edge.id })) })}
|
||||||
|
>delete edges</button
|
||||||
|
>
|
||||||
|
<button on:click={() => deleteElements({ nodes: [{ id: $nodes[0].id }] })}>delete node</button>
|
||||||
|
<button on:click={() => deleteElements({ nodes: $nodes.map((node) => ({ id: node.id })) })}
|
||||||
|
>deleteElements</button
|
||||||
|
>
|
||||||
|
<button on:click={() => deleteNode()}>delete via store</button>
|
||||||
<button
|
<button
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
const { nodes, edges, viewport } = toObject();
|
const { nodes, edges, viewport } = toObject();
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# @xyflow/react
|
# @xyflow/react
|
||||||
|
|
||||||
|
## 12.0.0-next.6
|
||||||
|
|
||||||
|
### Minor changes
|
||||||
|
|
||||||
|
- fix `deleteElements`
|
||||||
|
- refactor internal `applyChanges`
|
||||||
|
- batch `setNodes` and `setEdges` from `useReactFlow`
|
||||||
|
- add `aria-label` prop for `<Controls />`
|
||||||
|
|
||||||
## 12.0.0-next.5
|
## 12.0.0-next.5
|
||||||
|
|
||||||
### Minor changes
|
### Minor changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/react",
|
"name": "@xyflow/react",
|
||||||
"version": "12.0.0-next.5",
|
"version": "12.0.0-next.6",
|
||||||
"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",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ function ControlsComponent({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
position = 'bottom-left',
|
position = 'bottom-left',
|
||||||
|
'aria-label': ariaLabel = 'React Flow controls',
|
||||||
}: ControlProps) {
|
}: ControlProps) {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow);
|
const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow);
|
||||||
@@ -70,6 +71,7 @@ function ControlsComponent({
|
|||||||
position={position}
|
position={position}
|
||||||
style={style}
|
style={style}
|
||||||
data-testid="rf__controls"
|
data-testid="rf__controls"
|
||||||
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
{showZoom && (
|
{showZoom && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react';
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
import type { PanelPosition } from '@xyflow/system';
|
import type { PanelPosition } from '@xyflow/system';
|
||||||
|
|
||||||
import type { FitViewOptions } from '../../types';
|
import type { FitViewOptions } from '../../types';
|
||||||
|
|
||||||
export type ControlProps = HTMLAttributes<HTMLDivElement> & {
|
export type ControlProps = {
|
||||||
showZoom?: boolean;
|
showZoom?: boolean;
|
||||||
showFitView?: boolean;
|
showFitView?: boolean;
|
||||||
showInteractive?: boolean;
|
showInteractive?: boolean;
|
||||||
@@ -14,6 +14,9 @@ export type ControlProps = HTMLAttributes<HTMLDivElement> & {
|
|||||||
onInteractiveChange?: (interactiveStatus: boolean) => void;
|
onInteractiveChange?: (interactiveStatus: boolean) => void;
|
||||||
position?: PanelPosition;
|
position?: PanelPosition;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
'aria-label'?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ControlButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
export type ControlButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export function useGlobalKeyHandler({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deleteKeyPressed) {
|
if (deleteKeyPressed) {
|
||||||
const { edges, nodes, onBeforeDelete } = store.getState();
|
const { edges, nodes } = store.getState();
|
||||||
deleteElements({ nodes: nodes.filter(selected), edges: edges.filter(selected), onBeforeDelete });
|
deleteElements({ nodes: nodes.filter(selected), edges: edges.filter(selected) });
|
||||||
store.setState({ nodesSelectionActive: false });
|
store.setState({ nodesSelectionActive: false });
|
||||||
}
|
}
|
||||||
}, [deleteKeyPressed]);
|
}, [deleteKeyPressed]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
|
import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
|
||||||
|
|
||||||
import useViewportHelper from './useViewportHelper';
|
import useViewportHelper from './useViewportHelper';
|
||||||
@@ -8,14 +8,12 @@ import type {
|
|||||||
Instance,
|
Instance,
|
||||||
NodeAddChange,
|
NodeAddChange,
|
||||||
EdgeAddChange,
|
EdgeAddChange,
|
||||||
NodeResetChange,
|
|
||||||
EdgeResetChange,
|
|
||||||
NodeRemoveChange,
|
|
||||||
EdgeRemoveChange,
|
|
||||||
Node,
|
Node,
|
||||||
Edge,
|
Edge,
|
||||||
|
NodeChange,
|
||||||
|
EdgeChange,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { isNode } from '../utils';
|
import { getElementsDiffChanges, isNode } from '../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for accessing the ReactFlow instance.
|
* Hook for accessing the ReactFlow instance.
|
||||||
@@ -48,34 +46,57 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
return edges.find((e) => e.id === id) as EdgeType;
|
return edges.find((e) => e.id === id) as EdgeType;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// this is used to handle multiple syncronous setNodes calls
|
||||||
|
const setNodesData = useRef<Node[]>();
|
||||||
|
const setNodesTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
const setNodes = useCallback<Instance.SetNodes<NodeType>>((payload) => {
|
const setNodes = useCallback<Instance.SetNodes<NodeType>>((payload) => {
|
||||||
const { nodes, setNodes, hasDefaultNodes, onNodesChange } = store.getState();
|
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState();
|
||||||
const nextNodes = typeof payload === 'function' ? payload(nodes as NodeType[]) : payload;
|
const nextNodes = typeof payload === 'function' ? payload((setNodesData.current as NodeType[]) || nodes) : payload;
|
||||||
|
|
||||||
if (hasDefaultNodes) {
|
setNodesData.current = nextNodes;
|
||||||
setNodes(nextNodes);
|
|
||||||
} else if (onNodesChange) {
|
if (setNodesTimeout.current) {
|
||||||
const changes =
|
clearTimeout(setNodesTimeout.current);
|
||||||
nextNodes.length === 0
|
|
||||||
? nodes.map((node) => ({ type: 'remove', id: node.id } as NodeRemoveChange))
|
|
||||||
: nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange<NodeType>));
|
|
||||||
onNodesChange(changes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if there are multiple synchronous setNodes calls, we only want to call onNodesChange once
|
||||||
|
// for this, we use a timeout to wait for the last call and store updated nodes in setNodesData
|
||||||
|
// this is not perfect, but should work in most cases
|
||||||
|
setNodesTimeout.current = setTimeout(() => {
|
||||||
|
if (hasDefaultNodes) {
|
||||||
|
setNodes(nextNodes);
|
||||||
|
} else if (onNodesChange) {
|
||||||
|
const changes: NodeChange[] = getElementsDiffChanges({ items: setNodesData.current, lookup: nodeLookup });
|
||||||
|
onNodesChange(changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
setNodesData.current = undefined;
|
||||||
|
}, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// this is used to handle multiple syncronous setEdges calls
|
||||||
|
const setEdgesData = useRef<Edge[]>();
|
||||||
|
const setEdgesTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
const setEdges = useCallback<Instance.SetEdges<EdgeType>>((payload) => {
|
const setEdges = useCallback<Instance.SetEdges<EdgeType>>((payload) => {
|
||||||
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
|
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState();
|
||||||
const nextEdges = typeof payload === 'function' ? payload(edges as EdgeType[]) : payload;
|
const nextEdges = typeof payload === 'function' ? payload((setEdgesData.current as EdgeType[]) || edges) : payload;
|
||||||
|
|
||||||
if (hasDefaultEdges) {
|
setEdgesData.current = nextEdges;
|
||||||
setEdges(nextEdges);
|
|
||||||
} else if (onEdgesChange) {
|
if (setEdgesTimeout.current) {
|
||||||
const changes =
|
clearTimeout(setEdgesTimeout.current);
|
||||||
nextEdges.length === 0
|
|
||||||
? edges.map((edge) => ({ type: 'remove', id: edge.id } as EdgeRemoveChange))
|
|
||||||
: nextEdges.map((edge) => ({ item: edge, type: 'reset' } as EdgeResetChange<EdgeType>));
|
|
||||||
onEdgesChange(changes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEdgesTimeout.current = setTimeout(() => {
|
||||||
|
if (hasDefaultEdges) {
|
||||||
|
setEdges(nextEdges);
|
||||||
|
} else if (onEdgesChange) {
|
||||||
|
const changes: EdgeChange[] = getElementsDiffChanges({ items: nextEdges, lookup: edgeLookup });
|
||||||
|
onEdgesChange(changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEdgesData.current = undefined;
|
||||||
|
}, 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addNodes = useCallback<Instance.AddNodes<NodeType>>((payload) => {
|
const addNodes = useCallback<Instance.AddNodes<NodeType>>((payload) => {
|
||||||
@@ -118,7 +139,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const deleteElements = useCallback<Instance.DeleteElements>(
|
const deleteElements = useCallback<Instance.DeleteElements>(
|
||||||
async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [], onBeforeDelete }) => {
|
async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => {
|
||||||
const {
|
const {
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
@@ -129,6 +150,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
onNodesChange,
|
onNodesChange,
|
||||||
onEdgesChange,
|
onEdgesChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onBeforeDelete,
|
||||||
} = store.getState();
|
} = store.getState();
|
||||||
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
|
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
|
||||||
nodesToRemove,
|
nodesToRemove,
|
||||||
@@ -143,9 +165,8 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
|
|
||||||
if (hasMatchingEdges) {
|
if (hasMatchingEdges) {
|
||||||
if (hasDefaultEdges) {
|
if (hasDefaultEdges) {
|
||||||
store.setState({
|
const nextEdges = edges.filter((e) => !matchingEdges.some((mE) => mE.id === e.id));
|
||||||
edges: edges.filter((e) => !matchingEdges.some((mE) => mE.id === e.id)),
|
store.getState().setEdges(nextEdges);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEdgesDelete?.(matchingEdges);
|
onEdgesDelete?.(matchingEdges);
|
||||||
@@ -159,9 +180,8 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
|
|
||||||
if (hasMatchingNodes) {
|
if (hasMatchingNodes) {
|
||||||
if (hasDefaultNodes) {
|
if (hasDefaultNodes) {
|
||||||
store.setState({
|
const nextNodes = nodes.filter((n) => !matchingNodes.some((mN) => mN.id === n.id));
|
||||||
nodes: nodes.filter((n) => !matchingNodes.some((mN) => mN.id === n.id)),
|
store.getState().setNodes(nextNodes);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNodesDelete?.(matchingNodes);
|
onNodesDelete?.(matchingNodes);
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ export type NodeAddChange<NodeType extends Node = Node> = {
|
|||||||
type: 'add';
|
type: 'add';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeResetChange<NodeType extends Node = Node> = {
|
export type NodeReplaceChange<NodeType extends Node = Node> = {
|
||||||
|
id: string;
|
||||||
item: NodeType;
|
item: NodeType;
|
||||||
type: 'reset';
|
type: 'replace';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +50,7 @@ export type NodeChange =
|
|||||||
| NodeSelectionChange
|
| NodeSelectionChange
|
||||||
| NodeRemoveChange
|
| NodeRemoveChange
|
||||||
| NodeAddChange
|
| NodeAddChange
|
||||||
| NodeResetChange;
|
| NodeReplaceChange;
|
||||||
|
|
||||||
export type EdgeSelectionChange = NodeSelectionChange;
|
export type EdgeSelectionChange = NodeSelectionChange;
|
||||||
export type EdgeRemoveChange = NodeRemoveChange;
|
export type EdgeRemoveChange = NodeRemoveChange;
|
||||||
@@ -57,8 +58,10 @@ export type EdgeAddChange<EdgeType extends Edge = Edge> = {
|
|||||||
item: EdgeType;
|
item: EdgeType;
|
||||||
type: 'add';
|
type: 'add';
|
||||||
};
|
};
|
||||||
export type EdgeResetChange<EdgeType extends Edge = Edge> = {
|
|
||||||
|
export type EdgeReplaceChange<EdgeType extends Edge = Edge> = {
|
||||||
|
id: string;
|
||||||
item: EdgeType;
|
item: EdgeType;
|
||||||
type: 'reset';
|
type: 'replace';
|
||||||
};
|
};
|
||||||
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange;
|
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeReplaceChange;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
import type { OnBeforeDelete, Rect, Viewport } from '@xyflow/system';
|
import type { Rect, Viewport } from '@xyflow/system';
|
||||||
import type { Node, Edge, ViewportHelperFunctions } from '.';
|
import type { Node, Edge, ViewportHelperFunctions } from '.';
|
||||||
|
|
||||||
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||||
@@ -12,7 +12,6 @@ export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends E
|
|||||||
export type DeleteElementsOptions = {
|
export type DeleteElementsOptions = {
|
||||||
nodes?: (Node | { id: Node['id'] })[];
|
nodes?: (Node | { id: Node['id'] })[];
|
||||||
edges?: (Edge | { id: Edge['id'] })[];
|
edges?: (Edge | { id: Edge['id'] })[];
|
||||||
onBeforeDelete?: OnBeforeDelete;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export namespace Instance {
|
export namespace Instance {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { EdgeLookup, NodeLookup } from '@xyflow/system';
|
||||||
import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types';
|
import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types';
|
||||||
|
|
||||||
export function handleParentExpand(updatedElements: any[], updateItem: any) {
|
export function handleParentExpand(updatedElements: any[], updateItem: any) {
|
||||||
@@ -53,112 +54,120 @@ export function handleParentExpand(updatedElements: any[], updateItem: any) {
|
|||||||
// When you drag a node for example, React Flow will send a position change update.
|
// When you drag a node for example, React Flow will send a position change update.
|
||||||
// This function then applies the changes and returns the updated elements.
|
// This function then applies the changes and returns the updated elements.
|
||||||
function applyChanges(changes: any[], elements: any[]): any[] {
|
function applyChanges(changes: any[], elements: any[]): any[] {
|
||||||
// we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows
|
|
||||||
if (changes.some((c) => c.type === 'reset')) {
|
|
||||||
return changes.filter((c) => c.type === 'reset').map((c) => c.item);
|
|
||||||
}
|
|
||||||
|
|
||||||
let remainingChanges = [];
|
|
||||||
const updatedElements: any[] = [];
|
const updatedElements: any[] = [];
|
||||||
|
// By storing a map of changes for each element, we can a quick lookup as we
|
||||||
|
// iterate over the elements array!
|
||||||
|
const changesMap = new Map<any, any[]>();
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
if (change.type === 'add') {
|
if (change.type === 'add') {
|
||||||
updatedElements.push(change.item);
|
updatedElements.push(change.item);
|
||||||
|
continue;
|
||||||
|
} else if (change.type === 'remove' || change.type === 'replace') {
|
||||||
|
// For a 'remove' change we can safely ignore any other changes queued for
|
||||||
|
// the same element, it's going to be removed anyway!
|
||||||
|
changesMap.set(change.id, [change]);
|
||||||
} else {
|
} else {
|
||||||
remainingChanges.push(change);
|
const elementChanges = changesMap.get(change.id);
|
||||||
|
|
||||||
|
if (elementChanges) {
|
||||||
|
// If we have some changes queued already, we can do a mutable update of
|
||||||
|
// that array and save ourselves some copying.
|
||||||
|
elementChanges.push(change);
|
||||||
|
} else {
|
||||||
|
changesMap.set(change.id, [change]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of elements) {
|
for (const element of elements) {
|
||||||
const nextChanges: any[] = [];
|
const changes = changesMap.get(element.id);
|
||||||
const _remainingChanges: any[] = [];
|
|
||||||
|
|
||||||
for (const change of remainingChanges) {
|
// When there are no changes for an element we can just push it unmodified,
|
||||||
if (change.id === item.id) {
|
// no need to copy it.
|
||||||
nextChanges.push(change);
|
if (!changes) {
|
||||||
} else {
|
updatedElements.push(element);
|
||||||
_remainingChanges.push(change);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingChanges = _remainingChanges;
|
|
||||||
|
|
||||||
if (nextChanges.length === 0) {
|
|
||||||
updatedElements.push(item);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateItem = { ...item };
|
// If we have a 'remove' change queued, it'll be the only change in the array
|
||||||
let isDeletion = false;
|
if (changes[0].type === 'remove') {
|
||||||
|
continue;
|
||||||
for (const currentChange of nextChanges) {
|
|
||||||
if (currentChange) {
|
|
||||||
switch (currentChange.type) {
|
|
||||||
case 'select': {
|
|
||||||
updateItem.selected = currentChange.selected;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'position': {
|
|
||||||
if (typeof currentChange.position !== 'undefined') {
|
|
||||||
updateItem.position = currentChange.position;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof currentChange.positionAbsolute !== 'undefined') {
|
|
||||||
if (!updateItem.computed) {
|
|
||||||
updateItem.computed = {};
|
|
||||||
}
|
|
||||||
updateItem.computed.positionAbsolute = currentChange.positionAbsolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof currentChange.dragging !== 'undefined') {
|
|
||||||
updateItem.dragging = currentChange.dragging;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateItem.expandParent) {
|
|
||||||
handleParentExpand(updatedElements, updateItem);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'dimensions': {
|
|
||||||
if (typeof currentChange.dimensions !== 'undefined') {
|
|
||||||
if (!updateItem.computed) {
|
|
||||||
updateItem.computed = {};
|
|
||||||
}
|
|
||||||
updateItem.computed.width = currentChange.dimensions.width;
|
|
||||||
updateItem.computed.height = currentChange.dimensions.height;
|
|
||||||
|
|
||||||
// this is needed for the node resizer to work
|
|
||||||
if (currentChange.resizing) {
|
|
||||||
updateItem.width = currentChange.dimensions.width;
|
|
||||||
updateItem.height = currentChange.dimensions.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof currentChange.resizing === 'boolean') {
|
|
||||||
updateItem.resizing = currentChange.resizing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateItem.expandParent) {
|
|
||||||
handleParentExpand(updatedElements, updateItem);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'remove': {
|
|
||||||
isDeletion = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDeletion) {
|
if (changes[0].type === 'replace') {
|
||||||
updatedElements.push(updateItem);
|
updatedElements.push({ ...changes[0].item });
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For other types of changes, we want to start with a shallow copy of the
|
||||||
|
// object so React knows this element has changed. Sequential changes will
|
||||||
|
/// each _mutate_ this object, so there's only ever one copy.
|
||||||
|
const updatedElement = { ...element };
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
applyChange(change, updatedElement, elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedElements.push(updatedElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedElements;
|
return updatedElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Applies a single change to an element. This is a *mutable* update.
|
||||||
|
function applyChange(change: any, element: any, elements: any[] = []): any {
|
||||||
|
switch (change.type) {
|
||||||
|
case 'select': {
|
||||||
|
element.selected = change.selected;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'position': {
|
||||||
|
if (typeof change.position !== 'undefined') {
|
||||||
|
element.position = change.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof change.positionAbsolute !== 'undefined') {
|
||||||
|
element.computed ??= {};
|
||||||
|
element.computed.positionAbsolute = change.positionAbsolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof change.dragging !== 'undefined') {
|
||||||
|
element.dragging = change.dragging;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.expandParent) {
|
||||||
|
handleParentExpand(elements, element);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'dimensions': {
|
||||||
|
if (typeof change.dimensions !== 'undefined') {
|
||||||
|
element.computed ??= {};
|
||||||
|
element.computed.width = change.dimensions.width;
|
||||||
|
element.computed.height = change.dimensions.height;
|
||||||
|
|
||||||
|
if (change.resizing) {
|
||||||
|
element.width = change.dimensions.width;
|
||||||
|
element.height = change.dimensions.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof change.resizing === 'boolean') {
|
||||||
|
element.resizing = change.resizing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.expandParent) {
|
||||||
|
handleParentExpand(elements, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop in function that applies node changes to an array of nodes.
|
* Drop in function that applies node changes to an array of nodes.
|
||||||
* @public
|
* @public
|
||||||
@@ -237,3 +246,59 @@ export function getSelectionChanges(
|
|||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used to find the changes between two sets of elements.
|
||||||
|
* It is used to determine which nodes or edges have been added, removed or replaced.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @param params.items = the next set of elements (nodes or edges)
|
||||||
|
* @param params.lookup = a lookup map of the current store elements
|
||||||
|
* @returns an array of changes
|
||||||
|
*/
|
||||||
|
export function getElementsDiffChanges({
|
||||||
|
items,
|
||||||
|
lookup,
|
||||||
|
}: {
|
||||||
|
items: Node[] | undefined;
|
||||||
|
lookup: NodeLookup;
|
||||||
|
}): NodeChange[];
|
||||||
|
export function getElementsDiffChanges({
|
||||||
|
items,
|
||||||
|
lookup,
|
||||||
|
}: {
|
||||||
|
items: Edge[] | undefined;
|
||||||
|
lookup: EdgeLookup;
|
||||||
|
}): EdgeChange[];
|
||||||
|
export function getElementsDiffChanges({
|
||||||
|
items = [],
|
||||||
|
lookup,
|
||||||
|
}: {
|
||||||
|
items: any[] | undefined;
|
||||||
|
lookup: Map<string, any>;
|
||||||
|
}): any[] {
|
||||||
|
const changes: any[] = [];
|
||||||
|
const itemsLookup = new Map<string, any>(items.map((item) => [item.id, item]));
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const storeItem = lookup.get(item.id);
|
||||||
|
|
||||||
|
if (storeItem !== undefined && storeItem !== item) {
|
||||||
|
changes.push({ id: item.id, item: item, type: 'replace' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storeItem === undefined) {
|
||||||
|
changes.push({ item: item, type: 'add' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id] of lookup) {
|
||||||
|
const nextNode = itemsLookup.get(id);
|
||||||
|
|
||||||
|
if (nextNode === undefined) {
|
||||||
|
changes.push({ id, type: 'remove' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# @xyflow/svelte
|
# @xyflow/svelte
|
||||||
|
|
||||||
|
## 0.0.33
|
||||||
|
|
||||||
|
### Bugfix
|
||||||
|
|
||||||
|
- fix `deleteElements`
|
||||||
|
- fix dragging when `draggable=false`
|
||||||
|
- add `ariaLabel` prop for `<Controls />`
|
||||||
|
|
||||||
|
### ⚠️ Breaking
|
||||||
|
|
||||||
|
- `deleteElements` function now takes one object as an argument `{ nodes: [], edges: [] }` instead of two `(nodes, edges)`
|
||||||
|
|
||||||
## 0.0.32
|
## 0.0.32
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/svelte",
|
"name": "@xyflow/svelte",
|
||||||
"version": "0.0.32",
|
"version": "0.0.33",
|
||||||
"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",
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
type Rect,
|
type Rect,
|
||||||
getViewportForBounds,
|
getViewportForBounds,
|
||||||
getElementsToRemove,
|
getElementsToRemove,
|
||||||
rendererPointToPoint,
|
rendererPointToPoint
|
||||||
type OnBeforeDelete
|
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
@@ -47,11 +46,13 @@ export function useSvelteFlow(): {
|
|||||||
partially?: boolean
|
partially?: boolean
|
||||||
) => boolean;
|
) => boolean;
|
||||||
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void;
|
fitBounds: (bounds: Rect, options?: FitBoundsOptions) => void;
|
||||||
deleteElements: (
|
deleteElements: ({
|
||||||
nodesToRemove?: (Node | { id: Node['id'] })[],
|
nodes,
|
||||||
edgesToRemove?: (Edge | { id: Edge['id'] })[],
|
edges
|
||||||
onBeforeDelete?: OnBeforeDelete
|
}: {
|
||||||
) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[] }>;
|
nodes?: (Node | { id: Node['id'] })[];
|
||||||
|
edges?: (Edge | { id: Edge['id'] })[];
|
||||||
|
}) => Promise<{ deletedNodes: Node[]; deletedEdges: Edge[] }>;
|
||||||
screenToFlowPosition: (position: XYPosition) => XYPosition;
|
screenToFlowPosition: (position: XYPosition) => XYPosition;
|
||||||
flowToScreenPosition: (position: XYPosition) => XYPosition;
|
flowToScreenPosition: (position: XYPosition) => XYPosition;
|
||||||
viewport: Writable<Viewport>;
|
viewport: Writable<Viewport>;
|
||||||
@@ -201,17 +202,13 @@ export function useSvelteFlow(): {
|
|||||||
|
|
||||||
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
|
||||||
},
|
},
|
||||||
deleteElements: async (
|
deleteElements: async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => {
|
||||||
nodesToRemove: (Node | { id: Node['id'] })[] = [],
|
|
||||||
edgesToRemove: (Edge | { id: Edge['id'] })[] = [],
|
|
||||||
onBeforeDelete?: OnBeforeDelete
|
|
||||||
) => {
|
|
||||||
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
|
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
|
||||||
nodesToRemove,
|
nodesToRemove,
|
||||||
edgesToRemove,
|
edgesToRemove,
|
||||||
nodes: get(nodes),
|
nodes: get(nodes),
|
||||||
edges: get(edges),
|
edges: get(edges),
|
||||||
onBeforeDelete
|
onBeforeDelete: get(onbeforedelete)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matchingNodes) {
|
if (matchingNodes) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
export let buttonColor: $$Props['buttonColor'] = undefined;
|
export let buttonColor: $$Props['buttonColor'] = undefined;
|
||||||
export let buttonColorHover: $$Props['buttonColorHover'] = undefined;
|
export let buttonColorHover: $$Props['buttonColorHover'] = undefined;
|
||||||
export let buttonBorderColor: $$Props['buttonColorHover'] = undefined;
|
export let buttonBorderColor: $$Props['buttonColorHover'] = undefined;
|
||||||
|
export let ariaLabel: $$Props['aria-label'] = undefined;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
zoomIn,
|
zoomIn,
|
||||||
@@ -67,7 +68,12 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel class="svelte-flow__controls" {position} data-testid="svelte-flow__controls">
|
<Panel
|
||||||
|
class="svelte-flow__controls"
|
||||||
|
{position}
|
||||||
|
data-testid="svelte-flow__controls"
|
||||||
|
aria-label={ariaLabel ?? 'Svelte Flow controls'}
|
||||||
|
>
|
||||||
{#if showZoom}
|
{#if showZoom}
|
||||||
<ControlButton
|
<ControlButton
|
||||||
on:click={onZoomInHandler}
|
on:click={onZoomInHandler}
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ export type ControlsProps = {
|
|||||||
buttonBgColorHover?: string;
|
buttonBgColorHover?: string;
|
||||||
buttonColor?: string;
|
buttonColor?: string;
|
||||||
buttonColorHover?: string;
|
buttonColorHover?: string;
|
||||||
|
'aria-label'?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -178,34 +178,23 @@ export function createStore({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSelectedItem<T extends Node | Edge>(ids: string[]) {
|
function resetSelectedElements(elements: Node[] | Edge[]) {
|
||||||
return (item: T) => {
|
let elementsChanged = false;
|
||||||
if (item.selected && ids.includes(item.id)) {
|
elements.forEach((element) => {
|
||||||
return {
|
if (element.selected) {
|
||||||
...item,
|
element.selected = false;
|
||||||
selected: false
|
elementsChanged = true;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return item;
|
return elementsChanged;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
|
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
|
||||||
const selectedNodeIds = (params?.nodes ? params.nodes : get(store.nodes))
|
const resetNodes = resetSelectedElements(params?.nodes || get(store.nodes));
|
||||||
.filter((node) => node.selected)
|
if (resetNodes) store.nodes.set(get(store.nodes));
|
||||||
.map((node) => node.id);
|
|
||||||
const selectedEdgeIds = (params?.edges ? params.edges : get(store.edges))
|
|
||||||
.filter((edge) => edge.selected)
|
|
||||||
.map((edge) => edge.id);
|
|
||||||
|
|
||||||
if (selectedNodeIds.length) {
|
const resetEdges = resetSelectedElements(params?.edges || get(store.edges));
|
||||||
store.nodes.update((ns) => ns.map(resetSelectedItem(selectedNodeIds)));
|
if (resetEdges) store.edges.set(get(store.edges));
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedEdgeIds.length) {
|
|
||||||
store.edges.update((es) => es.map(resetSelectedItem(selectedEdgeIds)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
store.deleteKeyPressed.subscribe(async (deleteKeyPressed) => {
|
store.deleteKeyPressed.subscribe(async (deleteKeyPressed) => {
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import {
|
|||||||
type OnConnectStart,
|
type OnConnectStart,
|
||||||
type OnConnectEnd,
|
type OnConnectEnd,
|
||||||
type NodeLookup,
|
type NodeLookup,
|
||||||
type OnBeforeDelete
|
type OnBeforeDelete,
|
||||||
|
type EdgeLookup
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||||
@@ -99,6 +100,7 @@ export const getInitialStore = ({
|
|||||||
flowId: writable<string | null>(null),
|
flowId: writable<string | null>(null),
|
||||||
nodes: createNodesStore(nextNodes, nodeLookup),
|
nodes: createNodesStore(nextNodes, nodeLookup),
|
||||||
nodeLookup: readable<NodeLookup>(nodeLookup),
|
nodeLookup: readable<NodeLookup>(nodeLookup),
|
||||||
|
edgeLookup: readable<EdgeLookup>(edgeLookup),
|
||||||
visibleNodes: readable<Node[]>([]),
|
visibleNodes: readable<Node[]>([]),
|
||||||
edges: createEdgesStore(edges, connectionLookup, edgeLookup),
|
edges: createEdgesStore(edges, connectionLookup, edgeLookup),
|
||||||
visibleEdges: readable<EdgeLayouted[]>([]),
|
visibleEdges: readable<EdgeLayouted[]>([]),
|
||||||
|
|||||||
Reference in New Issue
Block a user