Merge pull request #3786 from xyflow/next

React Flow 12.0.0-next.6, Svelte Flow 0.0.33
This commit is contained in:
Moritz Klack
2024-01-11 17:39:38 +01:00
committed by GitHub
27 changed files with 468 additions and 194 deletions
@@ -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]);
}); });
}); });
+6
View File
@@ -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',
+36 -6
View File
@@ -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();
+9
View File
@@ -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 -1
View File
@@ -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]);
+53 -33
View File
@@ -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);
+9 -6
View File
@@ -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 -2
View File
@@ -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 {
+150 -85
View File
@@ -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;
}
+12
View File
@@ -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 -1
View File
@@ -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",
+10 -13
View File
@@ -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;
}; };
+12 -23
View File
@@ -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[]>([]),