Merge branch 'main' into chore/svelte-typed-context

This commit is contained in:
peterkogo
2025-12-03 11:06:23 +01:00
31 changed files with 369 additions and 39 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Prevent unnecessary re-render in `FlowRenderer`
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/system': patch
---
Allow custom `getEdgeId` function in `addEdge` and `reconnectEdge` options to enable custom edge ID schemes.
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Always create a new measured object in apply changes.
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/react': minor
---
Add `experimental_useOnNodesChangeMiddleware` hook
-7
View File
@@ -1,7 +0,0 @@
---
'@xyflow/react': minor
'@xyflow/svelte': minor
'@xyflow/system': patch
---
Pass current pointer position to connection
+7
View File
@@ -0,0 +1,7 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
'@xyflow/system': patch
---
Update an ongoing connection when user moves node with keyboard.
+1 -1
View File
@@ -156,7 +156,7 @@ For releasing packages we are using [changesets](https://github.com/changesets/c
1. create PRs for new features, updates and fixes (with a changeset if relevant for changelog) 1. create PRs for new features, updates and fixes (with a changeset if relevant for changelog)
2. merge into main 2. merge into main
3. changset creates a PR that bumps all packages based on the changesets 3. changeset creates a PR that bumps all packages based on the changesets
4. merge changeset PR if you want to release to Github and npm 4. merge changeset PR if you want to release to Github and npm
## Built by [xyflow](https://xyflow.com) ## Built by [xyflow](https://xyflow.com)
+6
View File
@@ -60,6 +60,7 @@ import DevTools from '../examples/DevTools';
import Redux from '../examples/Redux'; import Redux from '../examples/Redux';
import MovingHandles from '../examples/MovingHandles'; import MovingHandles from '../examples/MovingHandles';
import DetachedHandle from '../examples/DetachedHandle'; import DetachedHandle from '../examples/DetachedHandle';
import Middlewares from '../examples/Middlewares';
export interface IRoute { export interface IRoute {
name: string; name: string;
@@ -238,6 +239,11 @@ const routes: IRoute[] = [
path: 'layouting', path: 'layouting',
component: Layouting, component: Layouting,
}, },
{
name: 'Middlewares',
path: 'middlewares',
component: Middlewares,
},
{ {
name: 'Multi setNodes', name: 'Multi setNodes',
path: 'multi-setnodes', path: 'multi-setnodes',
@@ -0,0 +1,54 @@
import { NodeChange, experimental_useOnNodesChangeMiddleware } from '@xyflow/react';
import { useCallback, useState } from 'react';
export function RestrictExtent({
label = 'Restrict Extent',
minX = -Infinity,
minY = -Infinity,
maxX = Infinity,
maxY = Infinity,
}: {
label?: string;
minX?: number;
minY?: number;
maxX?: number;
maxY?: number;
}) {
const [isEnabled, setIsEnabled] = useState(false);
experimental_useOnNodesChangeMiddleware(
useCallback(
(changes: NodeChange[]) => {
if (!isEnabled) return changes;
return changes.map((change) => {
const { type } = change;
if (type === 'position') {
const { position } = change;
if (position) {
position.x = Math.min(Math.max(position.x, minX), maxX);
position.y = Math.min(Math.max(position.y, minY), maxY);
change.position = position;
}
} else if (type === 'add' || type === 'replace') {
const { item } = change;
if (item) {
item.position.x = Math.min(Math.max(item.position.x, minX), maxX);
item.position.y = Math.min(Math.max(item.position.y, minY), maxY);
change.item = item;
}
}
return change;
});
},
[minX, minY, maxX, maxY, isEnabled]
)
);
return (
<div>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input type="checkbox" checked={isEnabled} onChange={(e) => setIsEnabled(e.target.checked)} />
{label}
</label>
</div>
);
}
@@ -0,0 +1,85 @@
import { useCallback } from 'react';
import {
ReactFlow,
MiniMap,
Background,
BackgroundVariant,
Controls,
ReactFlowProvider,
Edge,
useReactFlow,
Panel,
useNodesState,
useEdgesState,
addEdge,
Connection,
} from '@xyflow/react';
import { initialNodes, initialEdges } from '../CancelConnection/data';
import { RestrictExtent } from './RestrictExtent';
const a = { id: 'a', data: { label: 'A' }, position: { x: 250, y: 5 } };
const b = { id: 'b', data: { label: 'B' }, position: { x: 100, y: 100 } };
const c = { id: 'c', data: { label: 'C' }, position: { x: 400, y: 100 } };
const SetNotesBatchingFlow = () => {
const { setNodes, updateNode } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const triggerMultipleSetNodes = useCallback(() => {
setNodes([a]);
setNodes((nodes) => [...nodes, b]);
setNodes((nodes) => [...nodes, c]);
setNodes((nodes) =>
nodes.map((node) =>
node.id === 'a' ? { ...node, position: { x: node.position.x + 2000, y: node.position.y + 20 } } : node
)
);
}, []);
const triggerMultipleUpdateNodes = useCallback(() => {
triggerMultipleSetNodes();
updateNode('a', (a) => ({ position: { x: a.position.x + 20, y: a.position.y + 20 } }));
updateNode('b', (b) => ({ position: { x: b.position.x + 20, y: b.position.y + 20 } }));
updateNode('c', (c) => ({ position: { x: c.position.x + 20, y: c.position.y + 20 } }));
updateNode('a', (a) => ({ data: { ...a.data, label: `A ${Date.now()}` } }));
updateNode('b', (b) => ({ data: { ...b.data, label: `B ${Date.now()}` } }));
updateNode('c', (c) => ({ data: { ...c.data, label: `C ${Date.now()}` } }));
}, []);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
fitView
>
<Panel position="top-left">
<RestrictExtent minX={0} maxX={500} label="Restrict X" />
<RestrictExtent minY={-100} maxY={500} label="Restrict Y" />
</Panel>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
<Panel position="top-right">
<button onClick={triggerMultipleSetNodes}>queue multiple setNodes calls</button>
<button onClick={triggerMultipleUpdateNodes}>queue multiple updateNode calls</button>
</Panel>
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<SetNotesBatchingFlow />
</ReactFlowProvider>
);
}
+11
View File
@@ -1,5 +1,16 @@
# @xyflow/react # @xyflow/react
## 12.9.3
### Patch Changes
- [#5621](https://github.com/xyflow/xyflow/pull/5621) [`c1304dba7`](https://github.com/xyflow/xyflow/commit/c1304dba7a20bb8d74c7aceb23cd80b56e4c0482) Thanks [@moklick](https://github.com/moklick)! - Set `paneClickDistance` default value to `1`.
- [#5578](https://github.com/xyflow/xyflow/pull/5578) [`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773) Thanks [@peterkogo](https://github.com/peterkogo)! - Pass current pointer position to connection
- Updated dependencies [[`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773)]:
- @xyflow/system@0.0.73
## 12.9.2 ## 12.9.2
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/react", "name": "@xyflow/react",
"version": "12.9.2", "version": "12.9.3",
"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",
@@ -28,7 +28,15 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
const store = useStoreApi<NodeType, EdgeType>(); const store = useStoreApi<NodeType, EdgeType>();
const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => { const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => {
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup, fitViewQueued } = store.getState(); const {
nodes = [],
setNodes,
hasDefaultNodes,
onNodesChange,
nodeLookup,
fitViewQueued,
onNodesChangeMiddlewareMap,
} = store.getState();
/* /*
* This is essentially an `Array.reduce` in imperative clothing. Processing * This is essentially an `Array.reduce` in imperative clothing. Processing
@@ -40,11 +48,15 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
next = typeof payload === 'function' ? payload(next) : payload; next = typeof payload === 'function' ? payload(next) : payload;
} }
const changes = getElementsDiffChanges({ let changes = getElementsDiffChanges({
items: next, items: next,
lookup: nodeLookup, lookup: nodeLookup,
}) as NodeChange<NodeType>[]; }) as NodeChange<NodeType>[];
for (const middleware of onNodesChangeMiddlewareMap.values()) {
changes = middleware(changes);
}
if (hasDefaultNodes) { if (hasDefaultNodes) {
setNodes(next); setNodes(next);
} }
@@ -1,4 +1,5 @@
import { memo, type ReactNode } from 'react'; import { memo, type ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { useGlobalKeyHandler } from '../../hooks/useGlobalKeyHandler'; import { useGlobalKeyHandler } from '../../hooks/useGlobalKeyHandler';
@@ -72,7 +73,7 @@ function FlowRendererComponent<NodeType extends Node = Node>({
onViewportChange, onViewportChange,
isControlledViewport, isControlledViewport,
}: FlowRendererProps<NodeType>) { }: FlowRendererProps<NodeType>) {
const { nodesSelectionActive, userSelectionActive } = useStore(selector); const { nodesSelectionActive, userSelectionActive } = useStore(selector, shallow);
const selectionKeyPressed = useKeyPress(selectionKeyCode, { target: win }); const selectionKeyPressed = useKeyPress(selectionKeyCode, { target: win });
const panActivationKeyPressed = useKeyPress(panActivationKeyCode, { target: win }); const panActivationKeyPressed = useKeyPress(panActivationKeyCode, { target: win });
@@ -104,7 +104,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onPaneMouseLeave, onPaneMouseLeave,
onPaneScroll, onPaneScroll,
onPaneContextMenu, onPaneContextMenu,
paneClickDistance = 0, paneClickDistance = 1,
nodeClickDistance = 0, nodeClickDistance = 0,
children, children,
onReconnect, onReconnect,
@@ -0,0 +1,30 @@
import { useEffect, useState } from 'react';
import type { EdgeChange } from '@xyflow/system';
import { useStoreApi } from './useStore';
import type { Edge, Node } from '../types';
/**
* Registers a middleware function to transform edge changes.
*
* @public
* @param fn - Middleware function. Should be memoized with useCallback to avoid re-registration.
*/
export function experimental_useOnEdgesChangeMiddleware<EdgeType extends Edge = Edge>(
fn: (changes: EdgeChange<EdgeType>[]) => EdgeChange<EdgeType>[]
) {
const store = useStoreApi<Node, EdgeType>();
const [symbol] = useState(() => Symbol());
useEffect(() => {
const { onEdgesChangeMiddlewareMap } = store.getState();
onEdgesChangeMiddlewareMap.set(symbol, fn);
}, [fn]);
useEffect(() => {
const { onEdgesChangeMiddlewareMap } = store.getState();
return () => {
onEdgesChangeMiddlewareMap.delete(symbol);
};
}, []);
}
@@ -0,0 +1,30 @@
import { useEffect, useState } from 'react';
import type { NodeChange } from '@xyflow/system';
import { useStoreApi } from './useStore';
import type { Edge, Node } from '../types';
/**
* Registers a middleware function to transform node changes.
*
* @public
* @param fn - Middleware function. Should be memoized with useCallback to avoid re-registration.
*/
export function experimental_useOnNodesChangeMiddleware<NodeType extends Node = Node>(
fn: (changes: NodeChange<NodeType>[]) => NodeChange<NodeType>[]
) {
const store = useStoreApi<NodeType, Edge>();
const [symbol] = useState(() => Symbol());
useEffect(() => {
const { onNodesChangeMiddlewareMap } = store.getState();
onNodesChangeMiddlewareMap.set(symbol, fn);
}, [fn]);
useEffect(() => {
const { onNodesChangeMiddlewareMap } = store.getState();
return () => {
onNodesChangeMiddlewareMap.delete(symbol);
};
}, []);
}
+3
View File
@@ -30,6 +30,9 @@ export { useConnection } from './hooks/useConnection';
export { useInternalNode } from './hooks/useInternalNode'; export { useInternalNode } from './hooks/useInternalNode';
export { useNodeId } from './contexts/NodeIdContext'; export { useNodeId } from './contexts/NodeIdContext';
export { experimental_useOnNodesChangeMiddleware } from './hooks/useOnNodesChangeMiddleware';
export { experimental_useOnEdgesChangeMiddleware } from './hooks/useOnEdgesChangeMiddleware';
export { applyNodeChanges, applyEdgeChanges } from './utils/changes'; export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
export { isNode, isEdge } from './utils/general'; export { isNode, isEdge } from './utils/general';
+13 -2
View File
@@ -14,6 +14,8 @@ import {
NodeOrigin, NodeOrigin,
CoordinateExtent, CoordinateExtent,
fitViewport, fitViewport,
getHandlePosition,
Position,
} from '@xyflow/system'; } from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -175,8 +177,8 @@ const createStore = ({
}, },
updateNodePositions: (nodeDragItems, dragging = false) => { updateNodePositions: (nodeDragItems, dragging = false) => {
const parentExpandChildren: ParentExpandChild[] = []; const parentExpandChildren: ParentExpandChild[] = [];
const changes = []; let changes = [];
const { nodeLookup, triggerNodeChanges } = get(); const { nodeLookup, triggerNodeChanges, connection, updateConnection, onNodesChangeMiddlewareMap } = get();
for (const [id, dragItem] of nodeDragItems) { for (const [id, dragItem] of nodeDragItems) {
// we are using the nodelookup to be sure to use the current expandParent and parentId value // we are using the nodelookup to be sure to use the current expandParent and parentId value
@@ -195,6 +197,11 @@ const createStore = ({
dragging, dragging,
}; };
if (node && connection.inProgress && connection.fromNode.id === node.id) {
const updatedFrom = getHandlePosition(node, connection.fromHandle, Position.Left, true);
updateConnection({ ...connection, from: updatedFrom });
}
if (expandParent && node.parentId) { if (expandParent && node.parentId) {
parentExpandChildren.push({ parentExpandChildren.push({
id, id,
@@ -216,6 +223,10 @@ const createStore = ({
changes.push(...parentExpandChanges); changes.push(...parentExpandChanges);
} }
for (const middleware of onNodesChangeMiddlewareMap.values()) {
changes = middleware(changes);
}
triggerNodeChanges(changes); triggerNodeChanges(changes);
}, },
triggerNodeChanges: (changes) => { triggerNodeChanges: (changes) => {
+3
View File
@@ -146,6 +146,9 @@ const getInitialState = ({
lib: 'react', lib: 'react',
debug: false, debug: false,
ariaLabelConfig: defaultAriaLabelConfig, ariaLabelConfig: defaultAriaLabelConfig,
onNodesChangeMiddlewareMap: new Map(),
onEdgesChangeMiddlewareMap: new Map(),
}; };
}; };
+3
View File
@@ -152,6 +152,9 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
lib: string; lib: string;
debug: boolean; debug: boolean;
ariaLabelConfig: AriaLabelConfig; ariaLabelConfig: AriaLabelConfig;
onNodesChangeMiddlewareMap: Map<symbol, (changes: NodeChange<NodeType>[]) => NodeChange<NodeType>[]>;
onEdgesChangeMiddlewareMap: Map<symbol, (changes: EdgeChange<EdgeType>[]) => EdgeChange<EdgeType>[]>;
}; };
export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = { export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
+3 -3
View File
@@ -125,9 +125,9 @@ function applyChange(change: any, element: any): any {
case 'dimensions': { case 'dimensions': {
if (typeof change.dimensions !== 'undefined') { if (typeof change.dimensions !== 'undefined') {
element.measured ??= {}; element.measured = {
element.measured.width = change.dimensions.width; ...change.dimensions,
element.measured.height = change.dimensions.height; };
if (change.setAttributes) { if (change.setAttributes) {
if (change.setAttributes === true || change.setAttributes === 'width') { if (change.setAttributes === true || change.setAttributes === 'width') {
+13
View File
@@ -1,5 +1,18 @@
# @xyflow/svelte # @xyflow/svelte
## 1.4.2
### Patch Changes
- [#5603](https://github.com/xyflow/xyflow/pull/5603) [`17a175791`](https://github.com/xyflow/xyflow/commit/17a175791b2420b32d55a8fcbbb35649862b85cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix onPaneClick always firing when moving the viewport
- [#5615](https://github.com/xyflow/xyflow/pull/5615) [`8b35c77eb`](https://github.com/xyflow/xyflow/commit/8b35c77ebe5a4f6cba33c597d94eba1ead64fdfc) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix wrong positions when resizing nodes with a non-default node-origin
- [#5578](https://github.com/xyflow/xyflow/pull/5578) [`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773) Thanks [@peterkogo](https://github.com/peterkogo)! - Pass current pointer position to connection
- Updated dependencies [[`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773)]:
- @xyflow/system@0.0.73
## 1.4.1 ## 1.4.1
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/svelte", "name": "@xyflow/svelte",
"version": "1.4.1", "version": "1.4.2",
"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",
@@ -61,7 +61,7 @@
panOnScroll = false, panOnScroll = false,
panOnScrollSpeed = 0.5, panOnScrollSpeed = 0.5,
panOnDrag = true, panOnDrag = true,
selectionOnDrag = true, selectionOnDrag = false,
connectionLineComponent, connectionLineComponent,
connectionLineStyle, connectionLineStyle,
connectionLineContainerStyle, connectionLineContainerStyle,
@@ -75,14 +75,11 @@
}, },
onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
// eslint-disable-next-line svelte/prefer-svelte-reactivity // eslint-disable-next-line svelte/prefer-svelte-reactivity
const changes = new Map<string, Partial<Node>>(); const changes = new Map<string, XYResizerChange>();
let position = change.x && change.y ? { x: change.x, y: change.y } : undefined; changes.set(id, change);
changes.set(id, { ...change, position });
for (const childChange of childChanges) { for (const childChange of childChanges) {
changes.set(childChange.id, { changes.set(childChange.id, {x: childChange.position.x, y: childChange.position.y });
position: childChange.position
});
} }
store.nodes = store.nodes.map((node) => { store.nodes = store.nodes.map((node) => {
@@ -91,11 +88,11 @@
const vertical = !resizeDirection || resizeDirection === 'vertical'; const vertical = !resizeDirection || resizeDirection === 'vertical';
if (change) { if (change) {
return { return {
...node, ...node,
position: { position: {
x: horizontal ? (change.position?.x ?? node.position.x) : node.position.x, x: horizontal ? (change.x ?? node.position.x) : node.position.x,
y: vertical ? (change.position?.y ?? node.position.y) : node.position.y y: vertical ? (change.y ?? node.position.y) : node.position.y
}, },
width: horizontal ? (change.width ?? node.width) : node.width, width: horizontal ? (change.width ?? node.width) : node.width,
height: vertical ? (change.height ?? node.height) : node.height height: vertical ? (change.height ?? node.height) : node.height
+12 -1
View File
@@ -15,7 +15,9 @@ import {
updateAbsolutePositions, updateAbsolutePositions,
snapPosition, snapPosition,
calculateNodePosition, calculateNodePosition,
type SetCenterOptions type SetCenterOptions,
getHandlePosition,
Position
} from '@xyflow/system'; } from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
@@ -51,6 +53,15 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => { const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
store.nodes = store.nodes.map((node) => { store.nodes = store.nodes.map((node) => {
if (store.connection.inProgress && store.connection.fromNode.id === node.id) {
const internalNode = store.nodeLookup.get(node.id);
if (internalNode) {
store.connection = {
...store.connection,
from: getHandlePosition(internalNode, store.connection.fromHandle, Position.Left, true)
};
}
}
const dragItem = nodeDragItems.get(node.id); const dragItem = nodeDragItems.get(node.id);
return dragItem ? { ...node, position: dragItem.position, dragging } : node; return dragItem ? { ...node, position: dragItem.position, dragging } : node;
}); });
+6
View File
@@ -1,5 +1,11 @@
# @xyflow/system # @xyflow/system
## 0.0.73
### Patch Changes
- [#5578](https://github.com/xyflow/xyflow/pull/5578) [`00bcb9f5f`](https://github.com/xyflow/xyflow/commit/00bcb9f5f45f49814b9ac19b3f55cfe069ee3773) Thanks [@peterkogo](https://github.com/peterkogo)! - Pass current pointer position to connection
## 0.0.72 ## 0.0.72
### Patch Changes ### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.72", "version": "0.0.73",
"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",
+33 -4
View File
@@ -84,7 +84,19 @@ export function isEdgeVisible({ sourceNode, targetNode, width, height, transform
return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0; return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0;
} }
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | EdgeBase): string => /**
* Type for a custom edge ID generator function.
* @public
*/
export type GetEdgeId = (params: Connection | EdgeBase) => string;
/**
* The default edge ID generator function. Generates an ID based on the source, target, and handles.
* @public
* @param params - The connection or edge to generate an ID for.
* @returns The generated edge ID.
*/
export const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | EdgeBase): string =>
`xy-edge__${source}${sourceHandle || ''}-${target}${targetHandle || ''}`; `xy-edge__${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => { const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
@@ -97,11 +109,19 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
); );
}; };
export type AddEdgeOptions = {
/**
* Custom function to generate edge IDs. If not provided, the default `getEdgeId` function is used.
*/
getEdgeId?: GetEdgeId;
};
/** /**
* This util is a convenience function to add a new Edge to an array of edges. It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one. * This util is a convenience function to add a new Edge to an array of edges. It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one.
* @public * @public
* @param edgeParams - Either an `Edge` or a `Connection` you want to add. * @param edgeParams - Either an `Edge` or a `Connection` you want to add.
* @param edges - The array of all current edges. * @param edges - The array of all current edges.
* @param options - Optional configuration object.
* @returns A new array of edges with the new edge added. * @returns A new array of edges with the new edge added.
* *
* @remarks If an edge with the same `target` and `source` already exists (and the same * @remarks If an edge with the same `target` and `source` already exists (and the same
@@ -111,7 +131,8 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => {
*/ */
export const addEdge = <EdgeType extends EdgeBase>( export const addEdge = <EdgeType extends EdgeBase>(
edgeParams: EdgeType | Connection, edgeParams: EdgeType | Connection,
edges: EdgeType[] edges: EdgeType[],
options: AddEdgeOptions = {}
): EdgeType[] => { ): EdgeType[] => {
if (!edgeParams.source || !edgeParams.target) { if (!edgeParams.source || !edgeParams.target) {
devWarn('006', errorMessages['error006']()); devWarn('006', errorMessages['error006']());
@@ -119,13 +140,15 @@ export const addEdge = <EdgeType extends EdgeBase>(
return edges; return edges;
} }
const edgeIdGenerator = options.getEdgeId || getEdgeId;
let edge: EdgeType; let edge: EdgeType;
if (isEdgeBase(edgeParams)) { if (isEdgeBase(edgeParams)) {
edge = { ...edgeParams }; edge = { ...edgeParams };
} else { } else {
edge = { edge = {
...edgeParams, ...edgeParams,
id: getEdgeId(edgeParams), id: edgeIdGenerator(edgeParams),
} as EdgeType; } as EdgeType;
} }
@@ -150,6 +173,10 @@ export type ReconnectEdgeOptions = {
* @default true * @default true
*/ */
shouldReplaceId?: boolean; shouldReplaceId?: boolean;
/**
* Custom function to generate edge IDs. If not provided, the default `getEdgeId` function is used.
*/
getEdgeId?: GetEdgeId;
}; };
/** /**
@@ -190,10 +217,12 @@ export const reconnectEdge = <EdgeType extends EdgeBase>(
return edges; return edges;
} }
const edgeIdGenerator = options.getEdgeId || getEdgeId;
// Remove old edge and create the new edge with parameters of old edge. // Remove old edge and create the new edge with parameters of old edge.
const edge = { const edge = {
...rest, ...rest,
id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId, id: options.shouldReplaceId ? edgeIdGenerator(newConnection) : oldEdgeId,
source: newConnection.source, source: newConnection.source,
target: newConnection.target, target: newConnection.target,
sourceHandle: newConnection.sourceHandle, sourceHandle: newConnection.sourceHandle,
+9 -4
View File
@@ -93,8 +93,8 @@ function onPointerDown(
position: fromHandleInternal.position, position: fromHandleInternal.position,
}; };
const fromNodeInternal = nodeLookup.get(nodeId)!; const fromInternalNode = nodeLookup.get(nodeId)!;
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true); const from = getHandlePosition(fromInternalNode, fromHandle, Position.Left, true);
let previousConnection: ConnectionInProgress = { let previousConnection: ConnectionInProgress = {
inProgress: true, inProgress: true,
@@ -103,7 +103,7 @@ function onPointerDown(
from, from,
fromHandle, fromHandle,
fromPosition: fromHandle.position, fromPosition: fromHandle.position,
fromNode: fromNodeInternal, fromNode: fromInternalNode,
to: position, to: position,
toHandle: null, toHandle: null,
@@ -172,9 +172,14 @@ function onPointerDown(
connection = result.connection; connection = result.connection;
isValid = isConnectionValid(!!closestHandle, result.isValid); isValid = isConnectionValid(!!closestHandle, result.isValid);
const fromInternalNode = nodeLookup.get(nodeId);
const from = fromInternalNode
? getHandlePosition(fromInternalNode, fromHandle, Position.Left, true)
: previousConnection.from;
const newConnection: ConnectionInProgress = { const newConnection: ConnectionInProgress = {
// from stays the same
...previousConnection, ...previousConnection,
from,
isValid, isValid,
to: to:
result.toHandle && isValid result.toHandle && isValid