@xyflow/system 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.js +2176 -0
- package/dist/esm/index.mjs +266 -146
- package/dist/esm/types/edges.d.ts +2 -2
- package/dist/esm/types/edges.d.ts.map +1 -1
- package/dist/esm/types/general.d.ts +7 -0
- package/dist/esm/types/general.d.ts.map +1 -1
- package/dist/esm/types/nodes.d.ts +7 -2
- package/dist/esm/types/nodes.d.ts.map +1 -1
- package/dist/esm/utils/edges/bezier-edge.d.ts +23 -0
- package/dist/esm/utils/edges/bezier-edge.d.ts.map +1 -1
- package/dist/esm/utils/edges/general.d.ts +23 -5
- package/dist/esm/utils/edges/general.d.ts.map +1 -1
- package/dist/esm/utils/edges/positions.d.ts.map +1 -1
- package/dist/esm/utils/edges/smoothstep-edge.d.ts +22 -0
- package/dist/esm/utils/edges/smoothstep-edge.d.ts.map +1 -1
- package/dist/esm/utils/edges/straight-edge.d.ts +20 -0
- package/dist/esm/utils/edges/straight-edge.d.ts.map +1 -1
- package/dist/esm/utils/general.d.ts +19 -2
- package/dist/esm/utils/general.d.ts.map +1 -1
- package/dist/esm/utils/graph.d.ts +61 -6
- package/dist/esm/utils/graph.d.ts.map +1 -1
- package/dist/esm/utils/store.d.ts +4 -4
- package/dist/esm/utils/store.d.ts.map +1 -1
- package/dist/esm/xyminimap/index.d.ts.map +1 -1
- package/dist/umd/index.js +1 -1
- package/dist/umd/types/edges.d.ts +2 -2
- package/dist/umd/types/edges.d.ts.map +1 -1
- package/dist/umd/types/general.d.ts +7 -0
- package/dist/umd/types/general.d.ts.map +1 -1
- package/dist/umd/types/nodes.d.ts +7 -2
- package/dist/umd/types/nodes.d.ts.map +1 -1
- package/dist/umd/utils/edges/bezier-edge.d.ts +23 -0
- package/dist/umd/utils/edges/bezier-edge.d.ts.map +1 -1
- package/dist/umd/utils/edges/general.d.ts +23 -5
- package/dist/umd/utils/edges/general.d.ts.map +1 -1
- package/dist/umd/utils/edges/positions.d.ts.map +1 -1
- package/dist/umd/utils/edges/smoothstep-edge.d.ts +22 -0
- package/dist/umd/utils/edges/smoothstep-edge.d.ts.map +1 -1
- package/dist/umd/utils/edges/straight-edge.d.ts +20 -0
- package/dist/umd/utils/edges/straight-edge.d.ts.map +1 -1
- package/dist/umd/utils/general.d.ts +19 -2
- package/dist/umd/utils/general.d.ts.map +1 -1
- package/dist/umd/utils/graph.d.ts +61 -6
- package/dist/umd/utils/graph.d.ts.map +1 -1
- package/dist/umd/utils/store.d.ts +4 -4
- package/dist/umd/utils/store.d.ts.map +1 -1
- package/dist/umd/xyminimap/index.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,2176 @@
|
|
|
1
|
+
import { drag } from 'd3-drag';
|
|
2
|
+
import { select, pointer } from 'd3-selection';
|
|
3
|
+
import { zoom, zoomIdentity, zoomTransform } from 'd3-zoom';
|
|
4
|
+
|
|
5
|
+
// @todo: update URLs to xyflow
|
|
6
|
+
const errorMessages = {
|
|
7
|
+
error001: () => '[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001',
|
|
8
|
+
error002: () => "It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",
|
|
9
|
+
error003: (nodeType) => `Node type "${nodeType}" not found. Using fallback type "default".`,
|
|
10
|
+
error004: () => 'The React Flow parent container needs a width and a height to render the graph.',
|
|
11
|
+
error005: () => 'Only child nodes can use a parent extent.',
|
|
12
|
+
error006: () => "Can't create edge. An edge needs a source and a target.",
|
|
13
|
+
error007: (id) => `The old edge with id=${id} does not exist.`,
|
|
14
|
+
error009: (type) => `Marker type "${type}" doesn't exist.`,
|
|
15
|
+
error008: (handleType, { id, sourceHandle, targetHandle }) => `Couldn't create edge for ${handleType} handle id: "${!sourceHandle ? sourceHandle : targetHandle}", edge id: ${id}.`,
|
|
16
|
+
error010: () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.',
|
|
17
|
+
error011: (edgeType) => `Edge type "${edgeType}" not found. Using fallback type "default".`,
|
|
18
|
+
error012: (id) => `Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,
|
|
19
|
+
};
|
|
20
|
+
const internalsSymbol = Symbol.for('internals');
|
|
21
|
+
const infiniteExtent = [
|
|
22
|
+
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
|
23
|
+
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
|
24
|
+
];
|
|
25
|
+
const elementSelectionKeys = ['Enter', ' ', 'Escape'];
|
|
26
|
+
|
|
27
|
+
var ConnectionMode;
|
|
28
|
+
(function (ConnectionMode) {
|
|
29
|
+
ConnectionMode["Strict"] = "strict";
|
|
30
|
+
ConnectionMode["Loose"] = "loose";
|
|
31
|
+
})(ConnectionMode || (ConnectionMode = {}));
|
|
32
|
+
var PanOnScrollMode;
|
|
33
|
+
(function (PanOnScrollMode) {
|
|
34
|
+
PanOnScrollMode["Free"] = "free";
|
|
35
|
+
PanOnScrollMode["Vertical"] = "vertical";
|
|
36
|
+
PanOnScrollMode["Horizontal"] = "horizontal";
|
|
37
|
+
})(PanOnScrollMode || (PanOnScrollMode = {}));
|
|
38
|
+
var SelectionMode;
|
|
39
|
+
(function (SelectionMode) {
|
|
40
|
+
SelectionMode["Partial"] = "partial";
|
|
41
|
+
SelectionMode["Full"] = "full";
|
|
42
|
+
})(SelectionMode || (SelectionMode = {}));
|
|
43
|
+
|
|
44
|
+
var ConnectionLineType;
|
|
45
|
+
(function (ConnectionLineType) {
|
|
46
|
+
ConnectionLineType["Bezier"] = "default";
|
|
47
|
+
ConnectionLineType["Straight"] = "straight";
|
|
48
|
+
ConnectionLineType["Step"] = "step";
|
|
49
|
+
ConnectionLineType["SmoothStep"] = "smoothstep";
|
|
50
|
+
ConnectionLineType["SimpleBezier"] = "simplebezier";
|
|
51
|
+
})(ConnectionLineType || (ConnectionLineType = {}));
|
|
52
|
+
var MarkerType;
|
|
53
|
+
(function (MarkerType) {
|
|
54
|
+
MarkerType["Arrow"] = "arrow";
|
|
55
|
+
MarkerType["ArrowClosed"] = "arrowclosed";
|
|
56
|
+
})(MarkerType || (MarkerType = {}));
|
|
57
|
+
|
|
58
|
+
var Position;
|
|
59
|
+
(function (Position) {
|
|
60
|
+
Position["Left"] = "left";
|
|
61
|
+
Position["Top"] = "top";
|
|
62
|
+
Position["Right"] = "right";
|
|
63
|
+
Position["Bottom"] = "bottom";
|
|
64
|
+
})(Position || (Position = {}));
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
function areConnectionMapsEqual(a, b) {
|
|
70
|
+
if (!a && !b) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (!a || !b || a.size !== b.size) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (!a.size && !b.size) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
for (const key of a.keys()) {
|
|
80
|
+
if (!b.has(key)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* We call the callback for all connections in a that are not in b
|
|
88
|
+
*
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
function handleConnectionChange(a, b, cb) {
|
|
92
|
+
if (!cb) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const diff = [];
|
|
96
|
+
a.forEach((connection, key) => {
|
|
97
|
+
if (!b?.has(key)) {
|
|
98
|
+
diff.push(connection);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
if (diff.length) {
|
|
102
|
+
cb(diff);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
107
|
+
/**
|
|
108
|
+
* Test whether an object is useable as an Edge
|
|
109
|
+
* @public
|
|
110
|
+
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
|
|
111
|
+
* @param element - The element to test
|
|
112
|
+
* @returns A boolean indicating whether the element is an Edge
|
|
113
|
+
*/
|
|
114
|
+
const isEdgeBase = (element) => 'id' in element && 'source' in element && 'target' in element;
|
|
115
|
+
/**
|
|
116
|
+
* Test whether an object is useable as a Node
|
|
117
|
+
* @public
|
|
118
|
+
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
|
|
119
|
+
* @param element - The element to test
|
|
120
|
+
* @returns A boolean indicating whether the element is an Node
|
|
121
|
+
*/
|
|
122
|
+
const isNodeBase = (element) => 'id' in element && !('source' in element) && !('target' in element);
|
|
123
|
+
/**
|
|
124
|
+
* Pass in a node, and get connected nodes where edge.source === node.id
|
|
125
|
+
* @public
|
|
126
|
+
* @param node - The node to get the connected nodes from
|
|
127
|
+
* @param nodes - The array of all nodes
|
|
128
|
+
* @param edges - The array of all edges
|
|
129
|
+
* @returns An array of nodes that are connected over eges where the source is the given node
|
|
130
|
+
*/
|
|
131
|
+
const getOutgoersBase = (node, nodes, edges) => {
|
|
132
|
+
if (!node.id) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const outgoerIds = new Set();
|
|
136
|
+
edges.forEach((edge) => {
|
|
137
|
+
if (edge.source === node.id) {
|
|
138
|
+
outgoerIds.add(edge.target);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return nodes.filter((n) => outgoerIds.has(n.id));
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Pass in a node, and get connected nodes where edge.target === node.id
|
|
145
|
+
* @public
|
|
146
|
+
* @param node - The node to get the connected nodes from
|
|
147
|
+
* @param nodes - The array of all nodes
|
|
148
|
+
* @param edges - The array of all edges
|
|
149
|
+
* @returns An array of nodes that are connected over eges where the target is the given node
|
|
150
|
+
*/
|
|
151
|
+
const getIncomersBase = (node, nodes, edges) => {
|
|
152
|
+
if (!node.id) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const incomersIds = new Set();
|
|
156
|
+
edges.forEach((edge) => {
|
|
157
|
+
if (edge.target === node.id) {
|
|
158
|
+
incomersIds.add(edge.source);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return nodes.filter((n) => incomersIds.has(n.id));
|
|
162
|
+
};
|
|
163
|
+
const getNodePositionWithOrigin = (node, nodeOrigin = [0, 0]) => {
|
|
164
|
+
if (!node) {
|
|
165
|
+
return {
|
|
166
|
+
x: 0,
|
|
167
|
+
y: 0,
|
|
168
|
+
positionAbsolute: {
|
|
169
|
+
x: 0,
|
|
170
|
+
y: 0,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const offsetX = (node.computed?.width ?? node.width ?? 0) * nodeOrigin[0];
|
|
175
|
+
const offsetY = (node.computed?.height ?? node.height ?? 0) * nodeOrigin[1];
|
|
176
|
+
const position = {
|
|
177
|
+
x: node.position.x - offsetX,
|
|
178
|
+
y: node.position.y - offsetY,
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
...position,
|
|
182
|
+
positionAbsolute: node.computed?.positionAbsolute
|
|
183
|
+
? {
|
|
184
|
+
x: node.computed.positionAbsolute.x - offsetX,
|
|
185
|
+
y: node.computed.positionAbsolute.y - offsetY,
|
|
186
|
+
}
|
|
187
|
+
: position,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Determines a bounding box that contains all given nodes in an array
|
|
192
|
+
* @public
|
|
193
|
+
* @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport.
|
|
194
|
+
* @param nodes - Nodes to calculate the bounds for
|
|
195
|
+
* @param nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
|
|
196
|
+
* @returns Bounding box enclosing all nodes
|
|
197
|
+
*/
|
|
198
|
+
const getNodesBounds = (nodes, nodeOrigin = [0, 0]) => {
|
|
199
|
+
if (nodes.length === 0) {
|
|
200
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
201
|
+
}
|
|
202
|
+
const box = nodes.reduce((currBox, node) => {
|
|
203
|
+
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
|
204
|
+
return getBoundsOfBoxes(currBox, rectToBox({
|
|
205
|
+
x,
|
|
206
|
+
y,
|
|
207
|
+
width: node.computed?.width ?? node.width ?? 0,
|
|
208
|
+
height: node.computed?.height ?? node.height ?? 0,
|
|
209
|
+
}));
|
|
210
|
+
}, { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity });
|
|
211
|
+
return boxToRect(box);
|
|
212
|
+
};
|
|
213
|
+
const getNodesInside = (nodes, rect, [tx, ty, tScale] = [0, 0, 1], partially = false,
|
|
214
|
+
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
|
|
215
|
+
excludeNonSelectableNodes = false, nodeOrigin = [0, 0]) => {
|
|
216
|
+
const paneRect = {
|
|
217
|
+
...pointToRendererPoint(rect, [tx, ty, tScale]),
|
|
218
|
+
width: rect.width / tScale,
|
|
219
|
+
height: rect.height / tScale,
|
|
220
|
+
};
|
|
221
|
+
const visibleNodes = nodes.reduce((res, node) => {
|
|
222
|
+
const { computed, selectable = true, hidden = false } = node;
|
|
223
|
+
const width = computed?.width ?? node.width ?? null;
|
|
224
|
+
const height = computed?.height ?? node.height ?? null;
|
|
225
|
+
if ((excludeNonSelectableNodes && !selectable) || hidden) {
|
|
226
|
+
return res;
|
|
227
|
+
}
|
|
228
|
+
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
|
|
229
|
+
const notInitialized = width === null || height === null;
|
|
230
|
+
const partiallyVisible = partially && overlappingArea > 0;
|
|
231
|
+
const area = (width ?? 0) * (height ?? 0);
|
|
232
|
+
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
|
|
233
|
+
if (isVisible || node.dragging) {
|
|
234
|
+
res.push(node);
|
|
235
|
+
}
|
|
236
|
+
return res;
|
|
237
|
+
}, []);
|
|
238
|
+
return visibleNodes;
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Get all connecting edges for a given set of nodes
|
|
242
|
+
* @param nodes - Nodes you want to get the connected edges for
|
|
243
|
+
* @param edges - All edges
|
|
244
|
+
* @returns Array of edges that connect any of the given nodes with each other
|
|
245
|
+
*/
|
|
246
|
+
const getConnectedEdgesBase = (nodes, edges) => {
|
|
247
|
+
const nodeIds = new Set();
|
|
248
|
+
nodes.forEach((node) => {
|
|
249
|
+
nodeIds.add(node.id);
|
|
250
|
+
});
|
|
251
|
+
return edges.filter((edge) => nodeIds.has(edge.source) || nodeIds.has(edge.target));
|
|
252
|
+
};
|
|
253
|
+
function fitView({ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }, options) {
|
|
254
|
+
const filteredNodes = nodes.filter((n) => {
|
|
255
|
+
const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden);
|
|
256
|
+
if (options?.nodes?.length) {
|
|
257
|
+
return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id);
|
|
258
|
+
}
|
|
259
|
+
return isVisible;
|
|
260
|
+
});
|
|
261
|
+
if (filteredNodes.length > 0) {
|
|
262
|
+
const bounds = getNodesBounds(filteredNodes, nodeOrigin);
|
|
263
|
+
const viewport = getViewportForBounds(bounds, width, height, options?.minZoom ?? minZoom, options?.maxZoom ?? maxZoom, options?.padding ?? 0.1);
|
|
264
|
+
panZoom.setViewport(viewport, { duration: options?.duration });
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
function clampNodeExtent(node, extent) {
|
|
270
|
+
if (!extent || extent === 'parent') {
|
|
271
|
+
return extent;
|
|
272
|
+
}
|
|
273
|
+
return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]];
|
|
274
|
+
}
|
|
275
|
+
function calcNextPosition(node, nextPosition, nodes, nodeExtent, nodeOrigin = [0, 0], onError) {
|
|
276
|
+
const clampedNodeExtent = clampNodeExtent(node, node.extent || nodeExtent);
|
|
277
|
+
let currentExtent = clampedNodeExtent;
|
|
278
|
+
let parentNode = null;
|
|
279
|
+
let parentPos = { x: 0, y: 0 };
|
|
280
|
+
if (node.parentNode) {
|
|
281
|
+
parentNode = nodes.find((n) => n.id === node.parentNode) || null;
|
|
282
|
+
parentPos = parentNode
|
|
283
|
+
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
|
|
284
|
+
: parentPos;
|
|
285
|
+
}
|
|
286
|
+
if (node.extent === 'parent' && !node.expandParent) {
|
|
287
|
+
const nodeWidth = node.computed?.width;
|
|
288
|
+
const nodeHeight = node.computed?.height;
|
|
289
|
+
if (node.parentNode && nodeWidth && nodeHeight) {
|
|
290
|
+
const currNodeOrigin = node.origin || nodeOrigin;
|
|
291
|
+
currentExtent =
|
|
292
|
+
parentNode && isNumeric(parentNode.computed?.width) && isNumeric(parentNode.computed?.height)
|
|
293
|
+
? [
|
|
294
|
+
[parentPos.x + nodeWidth * currNodeOrigin[0], parentPos.y + nodeHeight * currNodeOrigin[1]],
|
|
295
|
+
[
|
|
296
|
+
parentPos.x + (parentNode.computed?.width ?? 0) - nodeWidth + nodeWidth * currNodeOrigin[0],
|
|
297
|
+
parentPos.y + (parentNode.computed?.height ?? 0) - nodeHeight + nodeHeight * currNodeOrigin[1],
|
|
298
|
+
],
|
|
299
|
+
]
|
|
300
|
+
: currentExtent;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
onError?.('005', errorMessages['error005']());
|
|
304
|
+
currentExtent = clampedNodeExtent;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else if (node.extent && node.parentNode && node.extent !== 'parent') {
|
|
308
|
+
currentExtent = [
|
|
309
|
+
[node.extent[0][0] + parentPos.x, node.extent[0][1] + parentPos.y],
|
|
310
|
+
[node.extent[1][0] + parentPos.x, node.extent[1][1] + parentPos.y],
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
const positionAbsolute = currentExtent && currentExtent !== 'parent'
|
|
314
|
+
? clampPosition(nextPosition, currentExtent)
|
|
315
|
+
: nextPosition;
|
|
316
|
+
return {
|
|
317
|
+
position: {
|
|
318
|
+
x: positionAbsolute.x - parentPos.x,
|
|
319
|
+
y: positionAbsolute.y - parentPos.y,
|
|
320
|
+
},
|
|
321
|
+
positionAbsolute,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Pass in nodes & edges to delete, get arrays of nodes and edges that actually can be deleted
|
|
326
|
+
* @internal
|
|
327
|
+
* @param param.nodesToRemove - The nodes to remove
|
|
328
|
+
* @param param.edgesToRemove - The edges to remove
|
|
329
|
+
* @param param.nodes - All nodes
|
|
330
|
+
* @param param.edges - All edges
|
|
331
|
+
* @param param.onBeforeDelete - Callback to check which nodes and edges can be deleted
|
|
332
|
+
* @returns nodes: nodes that can be deleted, edges: edges that can be deleted
|
|
333
|
+
*/
|
|
334
|
+
async function getElementsToRemove({ nodesToRemove = [], edgesToRemove = [], nodes, edges, onBeforeDelete, }) {
|
|
335
|
+
const nodeIds = nodesToRemove.map((node) => node.id);
|
|
336
|
+
const matchingNodes = [];
|
|
337
|
+
for (const node of nodes) {
|
|
338
|
+
if (node.deletable === false) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const isIncluded = nodeIds.includes(node.id);
|
|
342
|
+
const parentHit = !isIncluded && node.parentNode && matchingNodes.find((n) => n.id === node.parentNode);
|
|
343
|
+
if (isIncluded || parentHit) {
|
|
344
|
+
matchingNodes.push(node);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const edgeIds = edgesToRemove.map((edge) => edge.id);
|
|
348
|
+
const deletableEdges = edges.filter((edge) => edge.deletable !== false);
|
|
349
|
+
const connectedEdges = getConnectedEdgesBase(matchingNodes, deletableEdges);
|
|
350
|
+
const matchingEdges = connectedEdges;
|
|
351
|
+
for (const edge of deletableEdges) {
|
|
352
|
+
const isIncluded = edgeIds.includes(edge.id);
|
|
353
|
+
if (isIncluded && !matchingEdges.find((e) => e.id === edge.id)) {
|
|
354
|
+
matchingEdges.push(edge);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (!onBeforeDelete) {
|
|
358
|
+
return {
|
|
359
|
+
edges: matchingEdges,
|
|
360
|
+
nodes: matchingNodes,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const onBeforeDeleteResult = await onBeforeDelete({
|
|
364
|
+
nodes: matchingNodes,
|
|
365
|
+
edges: matchingEdges,
|
|
366
|
+
});
|
|
367
|
+
if (typeof onBeforeDeleteResult === 'boolean') {
|
|
368
|
+
return onBeforeDeleteResult ? { edges: matchingEdges, nodes: matchingNodes } : { edges: [], nodes: [] };
|
|
369
|
+
}
|
|
370
|
+
return onBeforeDeleteResult;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const clamp = (val, min = 0, max = 1) => Math.min(Math.max(val, min), max);
|
|
374
|
+
const clampPosition = (position = { x: 0, y: 0 }, extent) => ({
|
|
375
|
+
x: clamp(position.x, extent[0][0], extent[1][0]),
|
|
376
|
+
y: clamp(position.y, extent[0][1], extent[1][1]),
|
|
377
|
+
});
|
|
378
|
+
/**
|
|
379
|
+
* Calculates the velocity of panning when the mouse is close to the edge of the canvas
|
|
380
|
+
* @internal
|
|
381
|
+
* @param value - One dimensional poition of the mouse (x or y)
|
|
382
|
+
* @param min - Minimal position on canvas before panning starts
|
|
383
|
+
* @param max - Maximal position on canvas before panning starts
|
|
384
|
+
* @returns - A number between 0 and 1 that represents the velocity of panning
|
|
385
|
+
*/
|
|
386
|
+
const calcAutoPanVelocity = (value, min, max) => {
|
|
387
|
+
if (value < min) {
|
|
388
|
+
return clamp(Math.abs(value - min), 1, 50) / 50;
|
|
389
|
+
}
|
|
390
|
+
else if (value > max) {
|
|
391
|
+
return -clamp(Math.abs(value - max), 1, 50) / 50;
|
|
392
|
+
}
|
|
393
|
+
return 0;
|
|
394
|
+
};
|
|
395
|
+
const calcAutoPan = (pos, bounds) => {
|
|
396
|
+
const xMovement = calcAutoPanVelocity(pos.x, 35, bounds.width - 35) * 20;
|
|
397
|
+
const yMovement = calcAutoPanVelocity(pos.y, 35, bounds.height - 35) * 20;
|
|
398
|
+
return [xMovement, yMovement];
|
|
399
|
+
};
|
|
400
|
+
const getBoundsOfBoxes = (box1, box2) => ({
|
|
401
|
+
x: Math.min(box1.x, box2.x),
|
|
402
|
+
y: Math.min(box1.y, box2.y),
|
|
403
|
+
x2: Math.max(box1.x2, box2.x2),
|
|
404
|
+
y2: Math.max(box1.y2, box2.y2),
|
|
405
|
+
});
|
|
406
|
+
const rectToBox = ({ x, y, width, height }) => ({
|
|
407
|
+
x,
|
|
408
|
+
y,
|
|
409
|
+
x2: x + width,
|
|
410
|
+
y2: y + height,
|
|
411
|
+
});
|
|
412
|
+
const boxToRect = ({ x, y, x2, y2 }) => ({
|
|
413
|
+
x,
|
|
414
|
+
y,
|
|
415
|
+
width: x2 - x,
|
|
416
|
+
height: y2 - y,
|
|
417
|
+
});
|
|
418
|
+
const nodeToRect = (node, nodeOrigin = [0, 0]) => {
|
|
419
|
+
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
|
420
|
+
return {
|
|
421
|
+
...positionAbsolute,
|
|
422
|
+
width: node.computed?.width ?? node.width ?? 0,
|
|
423
|
+
height: node.computed?.height ?? node.height ?? 0,
|
|
424
|
+
};
|
|
425
|
+
};
|
|
426
|
+
const nodeToBox = (node, nodeOrigin = [0, 0]) => {
|
|
427
|
+
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
|
428
|
+
return {
|
|
429
|
+
...positionAbsolute,
|
|
430
|
+
x2: positionAbsolute.x + (node.computed?.width ?? node.width ?? 0),
|
|
431
|
+
y2: positionAbsolute.y + (node.computed?.height ?? node.height ?? 0),
|
|
432
|
+
};
|
|
433
|
+
};
|
|
434
|
+
const getBoundsOfRects = (rect1, rect2) => boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
|
|
435
|
+
const getOverlappingArea = (rectA, rectB) => {
|
|
436
|
+
const xOverlap = Math.max(0, Math.min(rectA.x + rectA.width, rectB.x + rectB.width) - Math.max(rectA.x, rectB.x));
|
|
437
|
+
const yOverlap = Math.max(0, Math.min(rectA.y + rectA.height, rectB.y + rectB.height) - Math.max(rectA.y, rectB.y));
|
|
438
|
+
return Math.ceil(xOverlap * yOverlap);
|
|
439
|
+
};
|
|
440
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
441
|
+
const isRectObject = (obj) => isNumeric(obj.width) && isNumeric(obj.height) && isNumeric(obj.x) && isNumeric(obj.y);
|
|
442
|
+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
443
|
+
const isNumeric = (n) => !isNaN(n) && isFinite(n);
|
|
444
|
+
// used for a11y key board controls for nodes and edges
|
|
445
|
+
const devWarn = (id, message) => {
|
|
446
|
+
if (process.env.NODE_ENV === 'development') {
|
|
447
|
+
console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
const getPositionWithOrigin = ({ x, y, width, height, origin = [0, 0], }) => {
|
|
451
|
+
if (!width || !height || origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
|
|
452
|
+
return { x, y };
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
x: x - width * origin[0],
|
|
456
|
+
y: y - height * origin[1],
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
const snapPosition = (position, snapGrid = [1, 1]) => {
|
|
460
|
+
return {
|
|
461
|
+
x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
|
|
462
|
+
y: snapGrid[1] * Math.round(position.y / snapGrid[1]),
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
const pointToRendererPoint = ({ x, y }, [tx, ty, tScale], snapToGrid = false, snapGrid = [1, 1]) => {
|
|
466
|
+
const position = {
|
|
467
|
+
x: (x - tx) / tScale,
|
|
468
|
+
y: (y - ty) / tScale,
|
|
469
|
+
};
|
|
470
|
+
return snapToGrid ? snapPosition(position, snapGrid) : position;
|
|
471
|
+
};
|
|
472
|
+
const rendererPointToPoint = ({ x, y }, [tx, ty, tScale]) => {
|
|
473
|
+
return {
|
|
474
|
+
x: x * tScale + tx,
|
|
475
|
+
y: y * tScale + ty,
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
/**
|
|
479
|
+
* Returns a viewport that encloses the given bounds with optional padding.
|
|
480
|
+
* @public
|
|
481
|
+
* @remarks You can determine bounds of nodes with {@link getNodesBounds} and {@link getBoundsOfRects}
|
|
482
|
+
* @param bounds - Bounds to fit inside viewport
|
|
483
|
+
* @param width - Width of the viewport
|
|
484
|
+
* @param height - Height of the viewport
|
|
485
|
+
* @param minZoom - Minimum zoom level of the resulting viewport
|
|
486
|
+
* @param maxZoom - Maximum zoom level of the resulting viewport
|
|
487
|
+
* @param padding - Optional padding around the bounds
|
|
488
|
+
* @returns A transforned {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport}
|
|
489
|
+
* @example
|
|
490
|
+
* const { x, y, zoom } = getViewportForBounds(
|
|
491
|
+
{ x: 0, y: 0, width: 100, height: 100},
|
|
492
|
+
1200, 800, 0.5, 2);
|
|
493
|
+
*/
|
|
494
|
+
const getViewportForBounds = (bounds, width, height, minZoom, maxZoom, padding) => {
|
|
495
|
+
const xZoom = width / (bounds.width * (1 + padding));
|
|
496
|
+
const yZoom = height / (bounds.height * (1 + padding));
|
|
497
|
+
const zoom = Math.min(xZoom, yZoom);
|
|
498
|
+
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
|
499
|
+
const boundsCenterX = bounds.x + bounds.width / 2;
|
|
500
|
+
const boundsCenterY = bounds.y + bounds.height / 2;
|
|
501
|
+
const x = width / 2 - boundsCenterX * clampedZoom;
|
|
502
|
+
const y = height / 2 - boundsCenterY * clampedZoom;
|
|
503
|
+
return { x, y, zoom: clampedZoom };
|
|
504
|
+
};
|
|
505
|
+
const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0;
|
|
506
|
+
|
|
507
|
+
function getPointerPosition(event, { snapGrid = [0, 0], snapToGrid = false, transform }) {
|
|
508
|
+
const { x, y } = getEventPosition(event);
|
|
509
|
+
const pointerPos = pointToRendererPoint({ x, y }, transform);
|
|
510
|
+
const { x: xSnapped, y: ySnapped } = snapToGrid ? snapPosition(pointerPos, snapGrid) : pointerPos;
|
|
511
|
+
// we need the snapped position in order to be able to skip unnecessary drag events
|
|
512
|
+
return {
|
|
513
|
+
xSnapped,
|
|
514
|
+
ySnapped,
|
|
515
|
+
...pointerPos,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const getDimensions = (node) => ({
|
|
519
|
+
width: node.offsetWidth,
|
|
520
|
+
height: node.offsetHeight,
|
|
521
|
+
});
|
|
522
|
+
const getHostForElement = (element) => element.getRootNode?.() || window?.document;
|
|
523
|
+
const inputTags = ['INPUT', 'SELECT', 'TEXTAREA'];
|
|
524
|
+
function isInputDOMNode(event) {
|
|
525
|
+
// using composed path for handling shadow dom
|
|
526
|
+
const target = (event.composedPath?.()?.[0] || event.target);
|
|
527
|
+
const isInput = inputTags.includes(target?.nodeName) || target?.hasAttribute('contenteditable');
|
|
528
|
+
// we want to be able to do a multi selection event if we are in an input field
|
|
529
|
+
const isModifierKey = event.ctrlKey || event.metaKey || event.shiftKey;
|
|
530
|
+
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
|
531
|
+
return (isInput && !isModifierKey) || !!target?.closest('.nokey');
|
|
532
|
+
}
|
|
533
|
+
const isMouseEvent = (event) => 'clientX' in event;
|
|
534
|
+
const getEventPosition = (event, bounds) => {
|
|
535
|
+
const isMouse = isMouseEvent(event);
|
|
536
|
+
const evtX = isMouse ? event.clientX : event.touches?.[0].clientX;
|
|
537
|
+
const evtY = isMouse ? event.clientY : event.touches?.[0].clientY;
|
|
538
|
+
return {
|
|
539
|
+
x: evtX - (bounds?.left ?? 0),
|
|
540
|
+
y: evtY - (bounds?.top ?? 0),
|
|
541
|
+
};
|
|
542
|
+
};
|
|
543
|
+
// The handle bounds are calculated relative to the node element.
|
|
544
|
+
// We store them in the internals object of the node in order to avoid
|
|
545
|
+
// unnecessary recalculations.
|
|
546
|
+
const getHandleBounds = (selector, nodeElement, zoom, nodeOrigin = [0, 0]) => {
|
|
547
|
+
const handles = nodeElement.querySelectorAll(selector);
|
|
548
|
+
if (!handles || !handles.length) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const handlesArray = Array.from(handles);
|
|
552
|
+
// @todo can't we use the node dimensions here?
|
|
553
|
+
const nodeBounds = nodeElement.getBoundingClientRect();
|
|
554
|
+
const nodeOffset = {
|
|
555
|
+
x: nodeBounds.width * nodeOrigin[0],
|
|
556
|
+
y: nodeBounds.height * nodeOrigin[1],
|
|
557
|
+
};
|
|
558
|
+
return handlesArray.map((handle) => {
|
|
559
|
+
const handleBounds = handle.getBoundingClientRect();
|
|
560
|
+
return {
|
|
561
|
+
id: handle.getAttribute('data-handleid'),
|
|
562
|
+
position: handle.getAttribute('data-handlepos'),
|
|
563
|
+
x: (handleBounds.left - nodeBounds.left - nodeOffset.x) / zoom,
|
|
564
|
+
y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom,
|
|
565
|
+
...getDimensions(handle),
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
function getBezierEdgeCenter({ sourceX, sourceY, targetX, targetY, sourceControlX, sourceControlY, targetControlX, targetControlY, }) {
|
|
571
|
+
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
|
|
572
|
+
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
|
|
573
|
+
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
|
|
574
|
+
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
|
|
575
|
+
const offsetX = Math.abs(centerX - sourceX);
|
|
576
|
+
const offsetY = Math.abs(centerY - sourceY);
|
|
577
|
+
return [centerX, centerY, offsetX, offsetY];
|
|
578
|
+
}
|
|
579
|
+
function calculateControlOffset(distance, curvature) {
|
|
580
|
+
if (distance >= 0) {
|
|
581
|
+
return 0.5 * distance;
|
|
582
|
+
}
|
|
583
|
+
return curvature * 25 * Math.sqrt(-distance);
|
|
584
|
+
}
|
|
585
|
+
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }) {
|
|
586
|
+
switch (pos) {
|
|
587
|
+
case Position.Left:
|
|
588
|
+
return [x1 - calculateControlOffset(x1 - x2, c), y1];
|
|
589
|
+
case Position.Right:
|
|
590
|
+
return [x1 + calculateControlOffset(x2 - x1, c), y1];
|
|
591
|
+
case Position.Top:
|
|
592
|
+
return [x1, y1 - calculateControlOffset(y1 - y2, c)];
|
|
593
|
+
case Position.Bottom:
|
|
594
|
+
return [x1, y1 + calculateControlOffset(y2 - y1, c)];
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Get a bezier path from source to target handle
|
|
599
|
+
* @param params.sourceX - The x position of the source handle
|
|
600
|
+
* @param params.sourceY - The y position of the source handle
|
|
601
|
+
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
|
|
602
|
+
* @param params.targetX - The x position of the target handle
|
|
603
|
+
* @param params.targetY - The y position of the target handle
|
|
604
|
+
* @param params.targetPosition - The position of the target handle (default: Position.Top)
|
|
605
|
+
* @param params.curvature - The curvature of the bezier edge
|
|
606
|
+
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
|
|
607
|
+
* @example
|
|
608
|
+
* const source = { x: 0, y: 20 };
|
|
609
|
+
const target = { x: 150, y: 100 };
|
|
610
|
+
|
|
611
|
+
const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
|
|
612
|
+
sourceX: source.x,
|
|
613
|
+
sourceY: source.y,
|
|
614
|
+
sourcePosition: Position.Right,
|
|
615
|
+
targetX: target.x,
|
|
616
|
+
targetY: target.y,
|
|
617
|
+
targetPosition: Position.Left,
|
|
618
|
+
});
|
|
619
|
+
*/
|
|
620
|
+
function getBezierPath({ sourceX, sourceY, sourcePosition = Position.Bottom, targetX, targetY, targetPosition = Position.Top, curvature = 0.25, }) {
|
|
621
|
+
const [sourceControlX, sourceControlY] = getControlWithCurvature({
|
|
622
|
+
pos: sourcePosition,
|
|
623
|
+
x1: sourceX,
|
|
624
|
+
y1: sourceY,
|
|
625
|
+
x2: targetX,
|
|
626
|
+
y2: targetY,
|
|
627
|
+
c: curvature,
|
|
628
|
+
});
|
|
629
|
+
const [targetControlX, targetControlY] = getControlWithCurvature({
|
|
630
|
+
pos: targetPosition,
|
|
631
|
+
x1: targetX,
|
|
632
|
+
y1: targetY,
|
|
633
|
+
x2: sourceX,
|
|
634
|
+
y2: sourceY,
|
|
635
|
+
c: curvature,
|
|
636
|
+
});
|
|
637
|
+
const [labelX, labelY, offsetX, offsetY] = getBezierEdgeCenter({
|
|
638
|
+
sourceX,
|
|
639
|
+
sourceY,
|
|
640
|
+
targetX,
|
|
641
|
+
targetY,
|
|
642
|
+
sourceControlX,
|
|
643
|
+
sourceControlY,
|
|
644
|
+
targetControlX,
|
|
645
|
+
targetControlY,
|
|
646
|
+
});
|
|
647
|
+
return [
|
|
648
|
+
`M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
|
|
649
|
+
labelX,
|
|
650
|
+
labelY,
|
|
651
|
+
offsetX,
|
|
652
|
+
offsetY,
|
|
653
|
+
];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
|
|
657
|
+
function getEdgeCenter({ sourceX, sourceY, targetX, targetY, }) {
|
|
658
|
+
const xOffset = Math.abs(targetX - sourceX) / 2;
|
|
659
|
+
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
|
|
660
|
+
const yOffset = Math.abs(targetY - sourceY) / 2;
|
|
661
|
+
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
|
662
|
+
return [centerX, centerY, xOffset, yOffset];
|
|
663
|
+
}
|
|
664
|
+
function getElevatedEdgeZIndex({ sourceNode, targetNode, selected = false, zIndex = 0, elevateOnSelect = false, }) {
|
|
665
|
+
if (!elevateOnSelect) {
|
|
666
|
+
return zIndex;
|
|
667
|
+
}
|
|
668
|
+
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
|
|
669
|
+
const selectedZIndex = Math.max(sourceNode[internalsSymbol]?.z || 0, targetNode[internalsSymbol]?.z || 0, 1000);
|
|
670
|
+
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
|
|
671
|
+
}
|
|
672
|
+
function isEdgeVisible({ sourceNode, targetNode, width, height, transform }) {
|
|
673
|
+
const edgeBox = getBoundsOfBoxes(nodeToBox(sourceNode), nodeToBox(targetNode));
|
|
674
|
+
if (edgeBox.x === edgeBox.x2) {
|
|
675
|
+
edgeBox.x2 += 1;
|
|
676
|
+
}
|
|
677
|
+
if (edgeBox.y === edgeBox.y2) {
|
|
678
|
+
edgeBox.y2 += 1;
|
|
679
|
+
}
|
|
680
|
+
const viewRect = {
|
|
681
|
+
x: -transform[0] / transform[2],
|
|
682
|
+
y: -transform[1] / transform[2],
|
|
683
|
+
width: width / transform[2],
|
|
684
|
+
height: height / transform[2],
|
|
685
|
+
};
|
|
686
|
+
return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0;
|
|
687
|
+
}
|
|
688
|
+
const getEdgeId = ({ source, sourceHandle, target, targetHandle }) => `xy-edge__${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
|
|
689
|
+
const connectionExists = (edge, edges) => {
|
|
690
|
+
return edges.some((el) => el.source === edge.source &&
|
|
691
|
+
el.target === edge.target &&
|
|
692
|
+
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
|
|
693
|
+
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle)));
|
|
694
|
+
};
|
|
695
|
+
/**
|
|
696
|
+
* This util is a convenience function to add a new Edge to an array of edges
|
|
697
|
+
* @remarks It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one.
|
|
698
|
+
* @public
|
|
699
|
+
* @param edgeParams - Either an Edge or a Connection you want to add
|
|
700
|
+
* @param edges - The array of all current edges
|
|
701
|
+
* @returns A new array of edges with the new edge added
|
|
702
|
+
*/
|
|
703
|
+
const addEdgeBase = (edgeParams, edges) => {
|
|
704
|
+
if (!edgeParams.source || !edgeParams.target) {
|
|
705
|
+
devWarn('006', errorMessages['error006']());
|
|
706
|
+
return edges;
|
|
707
|
+
}
|
|
708
|
+
let edge;
|
|
709
|
+
if (isEdgeBase(edgeParams)) {
|
|
710
|
+
edge = { ...edgeParams };
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
edge = {
|
|
714
|
+
...edgeParams,
|
|
715
|
+
id: getEdgeId(edgeParams),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
if (connectionExists(edge, edges)) {
|
|
719
|
+
return edges;
|
|
720
|
+
}
|
|
721
|
+
if (edge.sourceHandle === null) {
|
|
722
|
+
delete edge.sourceHandle;
|
|
723
|
+
}
|
|
724
|
+
if (edge.targetHandle === null) {
|
|
725
|
+
delete edge.targetHandle;
|
|
726
|
+
}
|
|
727
|
+
return edges.concat(edge);
|
|
728
|
+
};
|
|
729
|
+
/**
|
|
730
|
+
* A handy utility to update an existing Edge with new properties
|
|
731
|
+
* @param oldEdge - The edge you want to update
|
|
732
|
+
* @param newConnection - The new connection you want to update the edge with
|
|
733
|
+
* @param edges - The array of all current edges
|
|
734
|
+
* @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id
|
|
735
|
+
* @returns the updated edges array
|
|
736
|
+
*/
|
|
737
|
+
const updateEdgeBase = (oldEdge, newConnection, edges, options = { shouldReplaceId: true }) => {
|
|
738
|
+
const { id: oldEdgeId, ...rest } = oldEdge;
|
|
739
|
+
if (!newConnection.source || !newConnection.target) {
|
|
740
|
+
devWarn('006', errorMessages['error006']());
|
|
741
|
+
return edges;
|
|
742
|
+
}
|
|
743
|
+
const foundEdge = edges.find((e) => e.id === oldEdge.id);
|
|
744
|
+
if (!foundEdge) {
|
|
745
|
+
devWarn('007', errorMessages['error007'](oldEdgeId));
|
|
746
|
+
return edges;
|
|
747
|
+
}
|
|
748
|
+
// Remove old edge and create the new edge with parameters of old edge.
|
|
749
|
+
const edge = {
|
|
750
|
+
...rest,
|
|
751
|
+
id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId,
|
|
752
|
+
source: newConnection.source,
|
|
753
|
+
target: newConnection.target,
|
|
754
|
+
sourceHandle: newConnection.sourceHandle,
|
|
755
|
+
targetHandle: newConnection.targetHandle,
|
|
756
|
+
};
|
|
757
|
+
return edges.filter((e) => e.id !== oldEdgeId).concat(edge);
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Get a straight path from source to target handle
|
|
762
|
+
* @param params.sourceX - The x position of the source handle
|
|
763
|
+
* @param params.sourceY - The y position of the source handle
|
|
764
|
+
* @param params.targetX - The x position of the target handle
|
|
765
|
+
* @param params.targetY - The y position of the target handle
|
|
766
|
+
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
|
|
767
|
+
* @example
|
|
768
|
+
* const source = { x: 0, y: 20 };
|
|
769
|
+
const target = { x: 150, y: 100 };
|
|
770
|
+
|
|
771
|
+
const [path, labelX, labelY, offsetX, offsetY] = getStraightPath({
|
|
772
|
+
sourceX: source.x,
|
|
773
|
+
sourceY: source.y,
|
|
774
|
+
sourcePosition: Position.Right,
|
|
775
|
+
targetX: target.x,
|
|
776
|
+
targetY: target.y,
|
|
777
|
+
targetPosition: Position.Left,
|
|
778
|
+
});
|
|
779
|
+
*/
|
|
780
|
+
function getStraightPath({ sourceX, sourceY, targetX, targetY, }) {
|
|
781
|
+
const [labelX, labelY, offsetX, offsetY] = getEdgeCenter({
|
|
782
|
+
sourceX,
|
|
783
|
+
sourceY,
|
|
784
|
+
targetX,
|
|
785
|
+
targetY,
|
|
786
|
+
});
|
|
787
|
+
return [`M ${sourceX},${sourceY}L ${targetX},${targetY}`, labelX, labelY, offsetX, offsetY];
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const handleDirections = {
|
|
791
|
+
[Position.Left]: { x: -1, y: 0 },
|
|
792
|
+
[Position.Right]: { x: 1, y: 0 },
|
|
793
|
+
[Position.Top]: { x: 0, y: -1 },
|
|
794
|
+
[Position.Bottom]: { x: 0, y: 1 },
|
|
795
|
+
};
|
|
796
|
+
const getDirection = ({ source, sourcePosition = Position.Bottom, target, }) => {
|
|
797
|
+
if (sourcePosition === Position.Left || sourcePosition === Position.Right) {
|
|
798
|
+
return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 };
|
|
799
|
+
}
|
|
800
|
+
return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 };
|
|
801
|
+
};
|
|
802
|
+
const distance = (a, b) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
|
|
803
|
+
// ith this function we try to mimic a orthogonal edge routing behaviour
|
|
804
|
+
// It's not as good as a real orthogonal edge routing but it's faster and good enough as a default for step and smooth step edges
|
|
805
|
+
function getPoints({ source, sourcePosition = Position.Bottom, target, targetPosition = Position.Top, center, offset, }) {
|
|
806
|
+
const sourceDir = handleDirections[sourcePosition];
|
|
807
|
+
const targetDir = handleDirections[targetPosition];
|
|
808
|
+
const sourceGapped = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset };
|
|
809
|
+
const targetGapped = { x: target.x + targetDir.x * offset, y: target.y + targetDir.y * offset };
|
|
810
|
+
const dir = getDirection({
|
|
811
|
+
source: sourceGapped,
|
|
812
|
+
sourcePosition,
|
|
813
|
+
target: targetGapped,
|
|
814
|
+
});
|
|
815
|
+
const dirAccessor = dir.x !== 0 ? 'x' : 'y';
|
|
816
|
+
const currDir = dir[dirAccessor];
|
|
817
|
+
let points = [];
|
|
818
|
+
let centerX, centerY;
|
|
819
|
+
const sourceGapOffset = { x: 0, y: 0 };
|
|
820
|
+
const targetGapOffset = { x: 0, y: 0 };
|
|
821
|
+
const [defaultCenterX, defaultCenterY, defaultOffsetX, defaultOffsetY] = getEdgeCenter({
|
|
822
|
+
sourceX: source.x,
|
|
823
|
+
sourceY: source.y,
|
|
824
|
+
targetX: target.x,
|
|
825
|
+
targetY: target.y,
|
|
826
|
+
});
|
|
827
|
+
// opposite handle positions, default case
|
|
828
|
+
if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
|
|
829
|
+
centerX = center.x || defaultCenterX;
|
|
830
|
+
centerY = center.y || defaultCenterY;
|
|
831
|
+
// --->
|
|
832
|
+
// |
|
|
833
|
+
// >---
|
|
834
|
+
const verticalSplit = [
|
|
835
|
+
{ x: centerX, y: sourceGapped.y },
|
|
836
|
+
{ x: centerX, y: targetGapped.y },
|
|
837
|
+
];
|
|
838
|
+
// |
|
|
839
|
+
// ---
|
|
840
|
+
// |
|
|
841
|
+
const horizontalSplit = [
|
|
842
|
+
{ x: sourceGapped.x, y: centerY },
|
|
843
|
+
{ x: targetGapped.x, y: centerY },
|
|
844
|
+
];
|
|
845
|
+
if (sourceDir[dirAccessor] === currDir) {
|
|
846
|
+
points = dirAccessor === 'x' ? verticalSplit : horizontalSplit;
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
points = dirAccessor === 'x' ? horizontalSplit : verticalSplit;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
// sourceTarget means we take x from source and y from target, targetSource is the opposite
|
|
854
|
+
const sourceTarget = [{ x: sourceGapped.x, y: targetGapped.y }];
|
|
855
|
+
const targetSource = [{ x: targetGapped.x, y: sourceGapped.y }];
|
|
856
|
+
// this handles edges with same handle positions
|
|
857
|
+
if (dirAccessor === 'x') {
|
|
858
|
+
points = sourceDir.x === currDir ? targetSource : sourceTarget;
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
points = sourceDir.y === currDir ? sourceTarget : targetSource;
|
|
862
|
+
}
|
|
863
|
+
if (sourcePosition === targetPosition) {
|
|
864
|
+
const diff = Math.abs(source[dirAccessor] - target[dirAccessor]);
|
|
865
|
+
// if an edge goes from right to right for example (sourcePosition === targetPosition) and the distance between source.x and target.x is less than the offset, the added point and the gapped source/target will overlap. This leads to a weird edge path. To avoid this we add a gapOffset to the source/target
|
|
866
|
+
if (diff <= offset) {
|
|
867
|
+
const gapOffset = Math.min(offset - 1, offset - diff);
|
|
868
|
+
if (sourceDir[dirAccessor] === currDir) {
|
|
869
|
+
sourceGapOffset[dirAccessor] = (sourceGapped[dirAccessor] > source[dirAccessor] ? -1 : 1) * gapOffset;
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
targetGapOffset[dirAccessor] = (targetGapped[dirAccessor] > target[dirAccessor] ? -1 : 1) * gapOffset;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// these are conditions for handling mixed handle positions like Right -> Bottom for example
|
|
877
|
+
if (sourcePosition !== targetPosition) {
|
|
878
|
+
const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x';
|
|
879
|
+
const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite];
|
|
880
|
+
const sourceGtTargetOppo = sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite];
|
|
881
|
+
const sourceLtTargetOppo = sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite];
|
|
882
|
+
const flipSourceTarget = (sourceDir[dirAccessor] === 1 && ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) ||
|
|
883
|
+
(sourceDir[dirAccessor] !== 1 && ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo)));
|
|
884
|
+
if (flipSourceTarget) {
|
|
885
|
+
points = dirAccessor === 'x' ? sourceTarget : targetSource;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const sourceGapPoint = { x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y };
|
|
889
|
+
const targetGapPoint = { x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y };
|
|
890
|
+
const maxXDistance = Math.max(Math.abs(sourceGapPoint.x - points[0].x), Math.abs(targetGapPoint.x - points[0].x));
|
|
891
|
+
const maxYDistance = Math.max(Math.abs(sourceGapPoint.y - points[0].y), Math.abs(targetGapPoint.y - points[0].y));
|
|
892
|
+
// we want to place the label on the longest segment of the edge
|
|
893
|
+
if (maxXDistance >= maxYDistance) {
|
|
894
|
+
centerX = (sourceGapPoint.x + targetGapPoint.x) / 2;
|
|
895
|
+
centerY = points[0].y;
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
centerX = points[0].x;
|
|
899
|
+
centerY = (sourceGapPoint.y + targetGapPoint.y) / 2;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const pathPoints = [
|
|
903
|
+
source,
|
|
904
|
+
{ x: sourceGapped.x + sourceGapOffset.x, y: sourceGapped.y + sourceGapOffset.y },
|
|
905
|
+
...points,
|
|
906
|
+
{ x: targetGapped.x + targetGapOffset.x, y: targetGapped.y + targetGapOffset.y },
|
|
907
|
+
target,
|
|
908
|
+
];
|
|
909
|
+
return [pathPoints, centerX, centerY, defaultOffsetX, defaultOffsetY];
|
|
910
|
+
}
|
|
911
|
+
function getBend(a, b, c, size) {
|
|
912
|
+
const bendSize = Math.min(distance(a, b) / 2, distance(b, c) / 2, size);
|
|
913
|
+
const { x, y } = b;
|
|
914
|
+
// no bend
|
|
915
|
+
if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {
|
|
916
|
+
return `L${x} ${y}`;
|
|
917
|
+
}
|
|
918
|
+
// first segment is horizontal
|
|
919
|
+
if (a.y === y) {
|
|
920
|
+
const xDir = a.x < c.x ? -1 : 1;
|
|
921
|
+
const yDir = a.y < c.y ? 1 : -1;
|
|
922
|
+
return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`;
|
|
923
|
+
}
|
|
924
|
+
const xDir = a.x < c.x ? 1 : -1;
|
|
925
|
+
const yDir = a.y < c.y ? -1 : 1;
|
|
926
|
+
return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Get a smooth step path from source to target handle
|
|
930
|
+
* @param params.sourceX - The x position of the source handle
|
|
931
|
+
* @param params.sourceY - The y position of the source handle
|
|
932
|
+
* @param params.sourcePosition - The position of the source handle (default: Position.Bottom)
|
|
933
|
+
* @param params.targetX - The x position of the target handle
|
|
934
|
+
* @param params.targetY - The y position of the target handle
|
|
935
|
+
* @param params.targetPosition - The position of the target handle (default: Position.Top)
|
|
936
|
+
* @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label
|
|
937
|
+
* @example
|
|
938
|
+
* const source = { x: 0, y: 20 };
|
|
939
|
+
const target = { x: 150, y: 100 };
|
|
940
|
+
|
|
941
|
+
const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({
|
|
942
|
+
sourceX: source.x,
|
|
943
|
+
sourceY: source.y,
|
|
944
|
+
sourcePosition: Position.Right,
|
|
945
|
+
targetX: target.x,
|
|
946
|
+
targetY: target.y,
|
|
947
|
+
targetPosition: Position.Left,
|
|
948
|
+
});
|
|
949
|
+
*/
|
|
950
|
+
function getSmoothStepPath({ sourceX, sourceY, sourcePosition = Position.Bottom, targetX, targetY, targetPosition = Position.Top, borderRadius = 5, centerX, centerY, offset = 20, }) {
|
|
951
|
+
const [points, labelX, labelY, offsetX, offsetY] = getPoints({
|
|
952
|
+
source: { x: sourceX, y: sourceY },
|
|
953
|
+
sourcePosition,
|
|
954
|
+
target: { x: targetX, y: targetY },
|
|
955
|
+
targetPosition,
|
|
956
|
+
center: { x: centerX, y: centerY },
|
|
957
|
+
offset,
|
|
958
|
+
});
|
|
959
|
+
const path = points.reduce((res, p, i) => {
|
|
960
|
+
let segment = '';
|
|
961
|
+
if (i > 0 && i < points.length - 1) {
|
|
962
|
+
segment = getBend(points[i - 1], p, points[i + 1], borderRadius);
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`;
|
|
966
|
+
}
|
|
967
|
+
res += segment;
|
|
968
|
+
return res;
|
|
969
|
+
}, '');
|
|
970
|
+
return [path, labelX, labelY, offsetX, offsetY];
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function isNodeInitialized(node) {
|
|
974
|
+
return !!(node?.[internalsSymbol]?.handleBounds || node?.handles?.length) && !!(node?.computed?.width || node?.width);
|
|
975
|
+
}
|
|
976
|
+
function getEdgePosition(params) {
|
|
977
|
+
const { sourceNode, targetNode } = params;
|
|
978
|
+
if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
|
|
982
|
+
const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles);
|
|
983
|
+
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
|
|
984
|
+
const targetHandle = getHandle(
|
|
985
|
+
// when connection type is loose we can define all handles as sources and connect source -> source
|
|
986
|
+
params.connectionMode === ConnectionMode.Strict
|
|
987
|
+
? targetHandleBounds?.target ?? []
|
|
988
|
+
: (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []), params.targetHandle);
|
|
989
|
+
const sourcePosition = sourceHandle?.position || Position.Bottom;
|
|
990
|
+
const targetPosition = targetHandle?.position || Position.Top;
|
|
991
|
+
if (!sourceHandle || !targetHandle) {
|
|
992
|
+
params.onError?.('008', errorMessages['error008'](!sourceHandle ? 'source' : 'target', {
|
|
993
|
+
id: params.id,
|
|
994
|
+
sourceHandle: params.sourceHandle,
|
|
995
|
+
targetHandle: params.targetHandle,
|
|
996
|
+
}));
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
|
|
1000
|
+
const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle);
|
|
1001
|
+
return {
|
|
1002
|
+
sourceX,
|
|
1003
|
+
sourceY,
|
|
1004
|
+
targetX,
|
|
1005
|
+
targetY,
|
|
1006
|
+
sourcePosition,
|
|
1007
|
+
targetPosition,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function toHandleBounds(handles) {
|
|
1011
|
+
if (!handles) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
const source = [];
|
|
1015
|
+
const target = [];
|
|
1016
|
+
for (const handle of handles) {
|
|
1017
|
+
handle.width = handle.width || 1;
|
|
1018
|
+
handle.height = handle.height || 1;
|
|
1019
|
+
if (handle.type === 'source') {
|
|
1020
|
+
source.push(handle);
|
|
1021
|
+
}
|
|
1022
|
+
else if (handle.type === 'target') {
|
|
1023
|
+
target.push(handle);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
source,
|
|
1028
|
+
target,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function getHandlePosition(position, node, handle = null) {
|
|
1032
|
+
const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
|
|
1033
|
+
const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
|
|
1034
|
+
const width = handle?.width || (node?.computed?.width ?? node?.width ?? 0);
|
|
1035
|
+
const height = handle?.height || (node?.computed?.height ?? node?.height ?? 0);
|
|
1036
|
+
switch (position) {
|
|
1037
|
+
case Position.Top:
|
|
1038
|
+
return [x + width / 2, y];
|
|
1039
|
+
case Position.Right:
|
|
1040
|
+
return [x + width, y + height / 2];
|
|
1041
|
+
case Position.Bottom:
|
|
1042
|
+
return [x + width / 2, y + height];
|
|
1043
|
+
case Position.Left:
|
|
1044
|
+
return [x, y + height / 2];
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function getHandle(bounds, handleId) {
|
|
1048
|
+
if (!bounds) {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
if (bounds.length === 1 || !handleId) {
|
|
1052
|
+
return bounds[0];
|
|
1053
|
+
}
|
|
1054
|
+
else if (handleId) {
|
|
1055
|
+
return bounds.find((d) => d.id === handleId) || null;
|
|
1056
|
+
}
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function getMarkerId(marker, id) {
|
|
1061
|
+
if (!marker) {
|
|
1062
|
+
return '';
|
|
1063
|
+
}
|
|
1064
|
+
if (typeof marker === 'string') {
|
|
1065
|
+
return marker;
|
|
1066
|
+
}
|
|
1067
|
+
const idPrefix = id ? `${id}__` : '';
|
|
1068
|
+
return `${idPrefix}${Object.keys(marker)
|
|
1069
|
+
.sort()
|
|
1070
|
+
.map((key) => `${key}=${marker[key]}`)
|
|
1071
|
+
.join('&')}`;
|
|
1072
|
+
}
|
|
1073
|
+
function createMarkerIds(edges, { id, defaultColor }) {
|
|
1074
|
+
const ids = [];
|
|
1075
|
+
return edges
|
|
1076
|
+
.reduce((markers, edge) => {
|
|
1077
|
+
[edge.markerStart, edge.markerEnd].forEach((marker) => {
|
|
1078
|
+
if (marker && typeof marker === 'object') {
|
|
1079
|
+
const markerId = getMarkerId(marker, id);
|
|
1080
|
+
if (!ids.includes(markerId)) {
|
|
1081
|
+
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
|
|
1082
|
+
ids.push(markerId);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
return markers;
|
|
1087
|
+
}, [])
|
|
1088
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function getNodeToolbarTransform(nodeRect, viewport, position, offset, align) {
|
|
1092
|
+
let alignmentOffset = 0.5;
|
|
1093
|
+
if (align === 'start') {
|
|
1094
|
+
alignmentOffset = 0;
|
|
1095
|
+
}
|
|
1096
|
+
else if (align === 'end') {
|
|
1097
|
+
alignmentOffset = 1;
|
|
1098
|
+
}
|
|
1099
|
+
// position === Position.Top
|
|
1100
|
+
// we set the x any y position of the toolbar based on the nodes position
|
|
1101
|
+
let pos = [
|
|
1102
|
+
(nodeRect.x + nodeRect.width * alignmentOffset) * viewport.zoom + viewport.x,
|
|
1103
|
+
nodeRect.y * viewport.zoom + viewport.y - offset,
|
|
1104
|
+
];
|
|
1105
|
+
// and than shift it based on the alignment. The shift values are in %.
|
|
1106
|
+
let shift = [-100 * alignmentOffset, -100];
|
|
1107
|
+
switch (position) {
|
|
1108
|
+
case Position.Right:
|
|
1109
|
+
pos = [
|
|
1110
|
+
(nodeRect.x + nodeRect.width) * viewport.zoom + viewport.x + offset,
|
|
1111
|
+
(nodeRect.y + nodeRect.height * alignmentOffset) * viewport.zoom + viewport.y,
|
|
1112
|
+
];
|
|
1113
|
+
shift = [0, -100 * alignmentOffset];
|
|
1114
|
+
break;
|
|
1115
|
+
case Position.Bottom:
|
|
1116
|
+
pos[1] = (nodeRect.y + nodeRect.height) * viewport.zoom + viewport.y + offset;
|
|
1117
|
+
shift[1] = 0;
|
|
1118
|
+
break;
|
|
1119
|
+
case Position.Left:
|
|
1120
|
+
pos = [
|
|
1121
|
+
nodeRect.x * viewport.zoom + viewport.x - offset,
|
|
1122
|
+
(nodeRect.y + nodeRect.height * alignmentOffset) * viewport.zoom + viewport.y,
|
|
1123
|
+
];
|
|
1124
|
+
shift = [-100, -100 * alignmentOffset];
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
return `translate(${pos[0]}px, ${pos[1]}px) translate(${shift[0]}%, ${shift[1]}%)`;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function updateAbsolutePositions(nodes, nodeLookup, nodeOrigin = [0, 0], parentNodes) {
|
|
1131
|
+
return nodes.map((node) => {
|
|
1132
|
+
if (node.parentNode && !nodeLookup.has(node.parentNode)) {
|
|
1133
|
+
throw new Error(`Parent node ${node.parentNode} not found`);
|
|
1134
|
+
}
|
|
1135
|
+
if (node.parentNode || parentNodes?.[node.id]) {
|
|
1136
|
+
const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null;
|
|
1137
|
+
const { x, y, z } = calculateXYZPosition(node, nodes, nodeLookup, {
|
|
1138
|
+
...node.position,
|
|
1139
|
+
z: node[internalsSymbol]?.z ?? 0,
|
|
1140
|
+
}, parentNode?.origin || nodeOrigin);
|
|
1141
|
+
const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
|
|
1142
|
+
node.computed.positionAbsolute = positionChanged
|
|
1143
|
+
? {
|
|
1144
|
+
x,
|
|
1145
|
+
y,
|
|
1146
|
+
}
|
|
1147
|
+
: node.computed?.positionAbsolute;
|
|
1148
|
+
node[internalsSymbol].z = z;
|
|
1149
|
+
if (parentNodes?.[node.id]) {
|
|
1150
|
+
node[internalsSymbol].isParent = true;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return node;
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
function adoptUserProvidedNodes(nodes, nodeLookup, options = {
|
|
1157
|
+
nodeOrigin: [0, 0],
|
|
1158
|
+
elevateNodesOnSelect: true,
|
|
1159
|
+
defaults: {},
|
|
1160
|
+
}) {
|
|
1161
|
+
const tmpLookup = new Map(nodeLookup);
|
|
1162
|
+
nodeLookup.clear();
|
|
1163
|
+
const parentNodes = {};
|
|
1164
|
+
const selectedNodeZ = options?.elevateNodesOnSelect ? 1000 : 0;
|
|
1165
|
+
const nextNodes = nodes.map((n) => {
|
|
1166
|
+
const currentStoreNode = tmpLookup.get(n.id);
|
|
1167
|
+
if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) {
|
|
1168
|
+
nodeLookup.set(n.id, currentStoreNode);
|
|
1169
|
+
return currentStoreNode;
|
|
1170
|
+
}
|
|
1171
|
+
const node = {
|
|
1172
|
+
...options.defaults,
|
|
1173
|
+
...n,
|
|
1174
|
+
computed: {
|
|
1175
|
+
positionAbsolute: n.position,
|
|
1176
|
+
width: n.computed?.width || currentStoreNode?.computed?.width,
|
|
1177
|
+
height: n.computed?.height || currentStoreNode?.computed?.height,
|
|
1178
|
+
},
|
|
1179
|
+
};
|
|
1180
|
+
const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);
|
|
1181
|
+
const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol];
|
|
1182
|
+
if (node.parentNode) {
|
|
1183
|
+
parentNodes[node.parentNode] = true;
|
|
1184
|
+
}
|
|
1185
|
+
Object.defineProperty(node, internalsSymbol, {
|
|
1186
|
+
enumerable: false,
|
|
1187
|
+
value: {
|
|
1188
|
+
handleBounds: currInternals?.handleBounds,
|
|
1189
|
+
z,
|
|
1190
|
+
userProvidedNode: n,
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
nodeLookup.set(node.id, node);
|
|
1194
|
+
return node;
|
|
1195
|
+
});
|
|
1196
|
+
const nodesWithPositions = updateAbsolutePositions(nextNodes, nodeLookup, options.nodeOrigin, parentNodes);
|
|
1197
|
+
return nodesWithPositions;
|
|
1198
|
+
}
|
|
1199
|
+
function calculateXYZPosition(node, nodes, nodeLookup, result, nodeOrigin) {
|
|
1200
|
+
if (!node.parentNode) {
|
|
1201
|
+
return result;
|
|
1202
|
+
}
|
|
1203
|
+
const parentNode = nodeLookup.get(node.parentNode);
|
|
1204
|
+
const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
|
|
1205
|
+
return calculateXYZPosition(parentNode, nodes, nodeLookup, {
|
|
1206
|
+
x: (result.x ?? 0) + parentNodePosition.x,
|
|
1207
|
+
y: (result.y ?? 0) + parentNodePosition.y,
|
|
1208
|
+
z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0,
|
|
1209
|
+
}, parentNode.origin || nodeOrigin);
|
|
1210
|
+
}
|
|
1211
|
+
function updateNodeDimensions(updates, nodes, nodeLookup, domNode, nodeOrigin, onUpdate) {
|
|
1212
|
+
const viewportNode = domNode?.querySelector('.xyflow__viewport');
|
|
1213
|
+
if (!viewportNode) {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
const style = window.getComputedStyle(viewportNode);
|
|
1217
|
+
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
|
|
1218
|
+
const nextNodes = nodes.map((node) => {
|
|
1219
|
+
const update = updates.get(node.id);
|
|
1220
|
+
if (update) {
|
|
1221
|
+
const dimensions = getDimensions(update.nodeElement);
|
|
1222
|
+
const doUpdate = !!(dimensions.width &&
|
|
1223
|
+
dimensions.height &&
|
|
1224
|
+
(node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.forceUpdate));
|
|
1225
|
+
if (doUpdate) {
|
|
1226
|
+
onUpdate?.(node.id, dimensions);
|
|
1227
|
+
const newNode = {
|
|
1228
|
+
...node,
|
|
1229
|
+
computed: {
|
|
1230
|
+
...node.computed,
|
|
1231
|
+
...dimensions,
|
|
1232
|
+
},
|
|
1233
|
+
[internalsSymbol]: {
|
|
1234
|
+
...node[internalsSymbol],
|
|
1235
|
+
handleBounds: {
|
|
1236
|
+
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin),
|
|
1237
|
+
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin),
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
};
|
|
1241
|
+
nodeLookup.set(node.id, newNode);
|
|
1242
|
+
return newNode;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return node;
|
|
1246
|
+
});
|
|
1247
|
+
return nextNodes;
|
|
1248
|
+
}
|
|
1249
|
+
function panBy({ delta, panZoom, transform, translateExtent, width, height, }) {
|
|
1250
|
+
if (!panZoom || (!delta.x && !delta.y)) {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
const nextViewport = panZoom.setViewportConstrained({
|
|
1254
|
+
x: transform[0] + delta.x,
|
|
1255
|
+
y: transform[1] + delta.y,
|
|
1256
|
+
zoom: transform[2],
|
|
1257
|
+
}, [
|
|
1258
|
+
[0, 0],
|
|
1259
|
+
[width, height],
|
|
1260
|
+
], translateExtent);
|
|
1261
|
+
const transformChanged = !!nextViewport &&
|
|
1262
|
+
(nextViewport.x !== transform[0] || nextViewport.y !== transform[1] || nextViewport.k !== transform[2]);
|
|
1263
|
+
return transformChanged;
|
|
1264
|
+
}
|
|
1265
|
+
function updateConnectionLookup(connectionLookup, edgeLookup, edges) {
|
|
1266
|
+
connectionLookup.clear();
|
|
1267
|
+
edgeLookup.clear();
|
|
1268
|
+
for (const edge of edges) {
|
|
1269
|
+
const { source, target, sourceHandle = null, targetHandle = null } = edge;
|
|
1270
|
+
const sourceKey = `${source}-source-${sourceHandle}`;
|
|
1271
|
+
const targetKey = `${target}-target-${targetHandle}`;
|
|
1272
|
+
const prevSource = connectionLookup.get(sourceKey) || new Map();
|
|
1273
|
+
const prevTarget = connectionLookup.get(targetKey) || new Map();
|
|
1274
|
+
const connection = { source, target, sourceHandle, targetHandle };
|
|
1275
|
+
edgeLookup.set(edge.id, edge);
|
|
1276
|
+
connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
|
|
1277
|
+
connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function wrapSelectionDragFunc(selectionFunc) {
|
|
1282
|
+
return (event, _, nodes) => selectionFunc?.(event, nodes);
|
|
1283
|
+
}
|
|
1284
|
+
function isParentSelected(node, nodes) {
|
|
1285
|
+
if (!node.parentNode) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
const parentNode = nodes.find((node) => node.id === node.parentNode);
|
|
1289
|
+
if (!parentNode) {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
if (parentNode.selected) {
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
return isParentSelected(parentNode, nodes);
|
|
1296
|
+
}
|
|
1297
|
+
function hasSelector(target, selector, domNode) {
|
|
1298
|
+
let current = target;
|
|
1299
|
+
do {
|
|
1300
|
+
if (current?.matches(selector))
|
|
1301
|
+
return true;
|
|
1302
|
+
if (current === domNode)
|
|
1303
|
+
return false;
|
|
1304
|
+
current = current.parentElement;
|
|
1305
|
+
} while (current);
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
// looks for all selected nodes and created a NodeDragItem for each of them
|
|
1309
|
+
function getDragItems(nodes, nodesDraggable, mousePos, nodeId) {
|
|
1310
|
+
return nodes
|
|
1311
|
+
.filter((n) => (n.selected || n.id === nodeId) &&
|
|
1312
|
+
(!n.parentNode || !isParentSelected(n, nodes)) &&
|
|
1313
|
+
(n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')))
|
|
1314
|
+
.map((n) => ({
|
|
1315
|
+
id: n.id,
|
|
1316
|
+
position: n.position || { x: 0, y: 0 },
|
|
1317
|
+
distance: {
|
|
1318
|
+
x: mousePos.x - (n.computed?.positionAbsolute?.x ?? 0),
|
|
1319
|
+
y: mousePos.y - (n.computed?.positionAbsolute?.y ?? 0),
|
|
1320
|
+
},
|
|
1321
|
+
delta: {
|
|
1322
|
+
x: 0,
|
|
1323
|
+
y: 0,
|
|
1324
|
+
},
|
|
1325
|
+
extent: n.extent,
|
|
1326
|
+
parentNode: n.parentNode,
|
|
1327
|
+
origin: n.origin,
|
|
1328
|
+
expandParent: n.expandParent,
|
|
1329
|
+
computed: {
|
|
1330
|
+
positionAbsolute: n.computed?.positionAbsolute || { x: 0, y: 0 },
|
|
1331
|
+
width: n.computed?.width || 0,
|
|
1332
|
+
height: n.computed?.height || 0,
|
|
1333
|
+
},
|
|
1334
|
+
}));
|
|
1335
|
+
}
|
|
1336
|
+
// returns two params:
|
|
1337
|
+
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
|
|
1338
|
+
// 2. array of selected nodes (for multi selections)
|
|
1339
|
+
function getEventHandlerParams({ nodeId, dragItems, nodeLookup, }) {
|
|
1340
|
+
const nodesFromDragItems = dragItems.map((n) => {
|
|
1341
|
+
const node = nodeLookup.get(n.id);
|
|
1342
|
+
return {
|
|
1343
|
+
...node,
|
|
1344
|
+
position: n.position,
|
|
1345
|
+
computed: {
|
|
1346
|
+
...n.computed,
|
|
1347
|
+
positionAbsolute: n.computed.positionAbsolute,
|
|
1348
|
+
},
|
|
1349
|
+
};
|
|
1350
|
+
});
|
|
1351
|
+
return [nodeId ? nodesFromDragItems.find((n) => n.id === nodeId) : nodesFromDragItems[0], nodesFromDragItems];
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function XYDrag({ domNode, onNodeMouseDown, getStoreItems, onDragStart, onDrag, onDragStop, }) {
|
|
1355
|
+
let lastPos = { x: null, y: null };
|
|
1356
|
+
let autoPanId = 0;
|
|
1357
|
+
let dragItems = [];
|
|
1358
|
+
let autoPanStarted = false;
|
|
1359
|
+
let mousePosition = { x: 0, y: 0 };
|
|
1360
|
+
let dragEvent = null;
|
|
1361
|
+
let containerBounds = null;
|
|
1362
|
+
let dragStarted = false;
|
|
1363
|
+
const d3Selection = select(domNode);
|
|
1364
|
+
// public functions
|
|
1365
|
+
function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }) {
|
|
1366
|
+
function updateNodes({ x, y }) {
|
|
1367
|
+
const { nodes, nodeLookup, nodeExtent, snapGrid, snapToGrid, nodeOrigin, onNodeDrag, onSelectionDrag, onError, updateNodePositions, } = getStoreItems();
|
|
1368
|
+
lastPos = { x, y };
|
|
1369
|
+
let hasChange = false;
|
|
1370
|
+
let nodesBox = { x: 0, y: 0, x2: 0, y2: 0 };
|
|
1371
|
+
if (dragItems.length > 1 && nodeExtent) {
|
|
1372
|
+
const rect = getNodesBounds(dragItems, nodeOrigin);
|
|
1373
|
+
nodesBox = rectToBox(rect);
|
|
1374
|
+
}
|
|
1375
|
+
dragItems = dragItems.map((n) => {
|
|
1376
|
+
let nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
|
|
1377
|
+
if (snapToGrid) {
|
|
1378
|
+
nextPosition = snapPosition(nextPosition, snapGrid);
|
|
1379
|
+
}
|
|
1380
|
+
// if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
|
|
1381
|
+
// based on its position so that the node stays at it's position relative to the selection.
|
|
1382
|
+
const adjustedNodeExtent = [
|
|
1383
|
+
[nodeExtent[0][0], nodeExtent[0][1]],
|
|
1384
|
+
[nodeExtent[1][0], nodeExtent[1][1]],
|
|
1385
|
+
];
|
|
1386
|
+
if (dragItems.length > 1 && nodeExtent && !n.extent) {
|
|
1387
|
+
adjustedNodeExtent[0][0] = n.computed.positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
|
|
1388
|
+
adjustedNodeExtent[1][0] =
|
|
1389
|
+
n.computed.positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
|
|
1390
|
+
adjustedNodeExtent[0][1] = n.computed.positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
|
|
1391
|
+
adjustedNodeExtent[1][1] =
|
|
1392
|
+
n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
|
|
1393
|
+
}
|
|
1394
|
+
const updatedPos = calcNextPosition(n, nextPosition, nodes, adjustedNodeExtent, nodeOrigin, onError);
|
|
1395
|
+
// we want to make sure that we only fire a change event when there is a change
|
|
1396
|
+
hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
|
|
1397
|
+
n.position = updatedPos.position;
|
|
1398
|
+
n.computed.positionAbsolute = updatedPos.positionAbsolute;
|
|
1399
|
+
return n;
|
|
1400
|
+
});
|
|
1401
|
+
if (!hasChange) {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
updateNodePositions(dragItems, true, true);
|
|
1405
|
+
const onNodeOrSelectionDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
|
|
1406
|
+
if (dragEvent && (onDrag || onNodeOrSelectionDrag)) {
|
|
1407
|
+
const [currentNode, currentNodes] = getEventHandlerParams({
|
|
1408
|
+
nodeId,
|
|
1409
|
+
dragItems,
|
|
1410
|
+
nodeLookup,
|
|
1411
|
+
});
|
|
1412
|
+
onDrag?.(dragEvent, dragItems, currentNode, currentNodes);
|
|
1413
|
+
onNodeOrSelectionDrag?.(dragEvent, currentNode, currentNodes);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function autoPan() {
|
|
1417
|
+
if (!containerBounds) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
const [xMovement, yMovement] = calcAutoPan(mousePosition, containerBounds);
|
|
1421
|
+
if (xMovement !== 0 || yMovement !== 0) {
|
|
1422
|
+
const { transform, panBy } = getStoreItems();
|
|
1423
|
+
lastPos.x = (lastPos.x ?? 0) - xMovement / transform[2];
|
|
1424
|
+
lastPos.y = (lastPos.y ?? 0) - yMovement / transform[2];
|
|
1425
|
+
if (panBy({ x: xMovement, y: yMovement })) {
|
|
1426
|
+
updateNodes(lastPos);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
autoPanId = requestAnimationFrame(autoPan);
|
|
1430
|
+
}
|
|
1431
|
+
function startDrag(event) {
|
|
1432
|
+
const { nodes, nodeLookup, multiSelectionActive, nodesDraggable, transform, snapGrid, snapToGrid, selectNodesOnDrag, onNodeDragStart, onSelectionDragStart, unselectNodesAndEdges, } = getStoreItems();
|
|
1433
|
+
dragStarted = true;
|
|
1434
|
+
if ((!selectNodesOnDrag || !isSelectable) && !multiSelectionActive && nodeId) {
|
|
1435
|
+
if (!nodeLookup.get(nodeId)?.selected) {
|
|
1436
|
+
// we need to reset selected nodes when selectNodesOnDrag=false
|
|
1437
|
+
unselectNodesAndEdges();
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
if (isSelectable && selectNodesOnDrag && nodeId) {
|
|
1441
|
+
onNodeMouseDown?.(nodeId);
|
|
1442
|
+
}
|
|
1443
|
+
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
|
1444
|
+
lastPos = pointerPos;
|
|
1445
|
+
dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId);
|
|
1446
|
+
const onNodeOrSelectionDragStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart);
|
|
1447
|
+
if (dragItems && (onDragStart || onNodeOrSelectionDragStart)) {
|
|
1448
|
+
const [currentNode, currentNodes] = getEventHandlerParams({
|
|
1449
|
+
nodeId,
|
|
1450
|
+
dragItems,
|
|
1451
|
+
nodeLookup,
|
|
1452
|
+
});
|
|
1453
|
+
onDragStart?.(event.sourceEvent, dragItems, currentNode, currentNodes);
|
|
1454
|
+
onNodeOrSelectionDragStart?.(event.sourceEvent, currentNode, currentNodes);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const d3DragInstance = drag()
|
|
1458
|
+
.on('start', (event) => {
|
|
1459
|
+
const { domNode, nodeDragThreshold, transform, snapGrid, snapToGrid } = getStoreItems();
|
|
1460
|
+
if (nodeDragThreshold === 0) {
|
|
1461
|
+
startDrag(event);
|
|
1462
|
+
}
|
|
1463
|
+
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
|
1464
|
+
lastPos = pointerPos;
|
|
1465
|
+
containerBounds = domNode?.getBoundingClientRect() || null;
|
|
1466
|
+
mousePosition = getEventPosition(event.sourceEvent, containerBounds);
|
|
1467
|
+
})
|
|
1468
|
+
.on('drag', (event) => {
|
|
1469
|
+
const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems();
|
|
1470
|
+
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
|
1471
|
+
if (!autoPanStarted && autoPanOnNodeDrag && dragStarted) {
|
|
1472
|
+
autoPanStarted = true;
|
|
1473
|
+
autoPan();
|
|
1474
|
+
}
|
|
1475
|
+
if (!dragStarted) {
|
|
1476
|
+
const x = pointerPos.xSnapped - (lastPos.x ?? 0);
|
|
1477
|
+
const y = pointerPos.ySnapped - (lastPos.y ?? 0);
|
|
1478
|
+
const distance = Math.sqrt(x * x + y * y);
|
|
1479
|
+
if (distance > nodeDragThreshold) {
|
|
1480
|
+
startDrag(event);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
// skip events without movement
|
|
1484
|
+
if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems && dragStarted) {
|
|
1485
|
+
dragEvent = event.sourceEvent;
|
|
1486
|
+
mousePosition = getEventPosition(event.sourceEvent, containerBounds);
|
|
1487
|
+
updateNodes(pointerPos);
|
|
1488
|
+
}
|
|
1489
|
+
})
|
|
1490
|
+
.on('end', (event) => {
|
|
1491
|
+
if (!dragStarted) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
autoPanStarted = false;
|
|
1495
|
+
dragStarted = false;
|
|
1496
|
+
cancelAnimationFrame(autoPanId);
|
|
1497
|
+
if (dragItems) {
|
|
1498
|
+
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
|
|
1499
|
+
const onNodeOrSelectionDragStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop);
|
|
1500
|
+
updateNodePositions(dragItems, false, false);
|
|
1501
|
+
if (onDragStop || onNodeOrSelectionDragStop) {
|
|
1502
|
+
const [currentNode, currentNodes] = getEventHandlerParams({
|
|
1503
|
+
nodeId,
|
|
1504
|
+
dragItems,
|
|
1505
|
+
nodeLookup,
|
|
1506
|
+
});
|
|
1507
|
+
onDragStop?.(event.sourceEvent, dragItems, currentNode, currentNodes);
|
|
1508
|
+
onNodeOrSelectionDragStop?.(event.sourceEvent, currentNode, currentNodes);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
})
|
|
1512
|
+
.filter((event) => {
|
|
1513
|
+
const target = event.target;
|
|
1514
|
+
const isDraggable = !event.button &&
|
|
1515
|
+
(!noDragClassName || !hasSelector(target, `.${noDragClassName}`, domNode)) &&
|
|
1516
|
+
(!handleSelector || hasSelector(target, handleSelector, domNode));
|
|
1517
|
+
return isDraggable;
|
|
1518
|
+
});
|
|
1519
|
+
d3Selection.call(d3DragInstance);
|
|
1520
|
+
}
|
|
1521
|
+
function destroy() {
|
|
1522
|
+
d3Selection.on('.drag', null);
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
update,
|
|
1526
|
+
destroy,
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// this functions collects all handles and adds an absolute position
|
|
1531
|
+
// so that we can later find the closest handle to the mouse position
|
|
1532
|
+
function getHandles(node, handleBounds, type, currentHandle) {
|
|
1533
|
+
return (handleBounds[type] || []).reduce((res, h) => {
|
|
1534
|
+
if (`${node.id}-${h.id}-${type}` !== currentHandle) {
|
|
1535
|
+
res.push({
|
|
1536
|
+
id: h.id || null,
|
|
1537
|
+
type,
|
|
1538
|
+
nodeId: node.id,
|
|
1539
|
+
x: (node.computed?.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
|
|
1540
|
+
y: (node.computed?.positionAbsolute?.y ?? 0) + h.y + h.height / 2,
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
return res;
|
|
1544
|
+
}, []);
|
|
1545
|
+
}
|
|
1546
|
+
function getClosestHandle(pos, connectionRadius, handles) {
|
|
1547
|
+
let closestHandles = [];
|
|
1548
|
+
let minDistance = Infinity;
|
|
1549
|
+
handles.forEach((handle) => {
|
|
1550
|
+
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
|
|
1551
|
+
if (distance <= connectionRadius) {
|
|
1552
|
+
if (distance < minDistance) {
|
|
1553
|
+
closestHandles = [handle];
|
|
1554
|
+
}
|
|
1555
|
+
else if (distance === minDistance) {
|
|
1556
|
+
// when multiple handles are on the same distance we collect all of them
|
|
1557
|
+
closestHandles.push(handle);
|
|
1558
|
+
}
|
|
1559
|
+
minDistance = distance;
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
if (!closestHandles.length) {
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
return closestHandles.length === 1
|
|
1566
|
+
? closestHandles[0]
|
|
1567
|
+
: // if multiple handles are layouted on top of each other we take the one with type = target because it's more likely that the user wants to connect to this one
|
|
1568
|
+
closestHandles.find((handle) => handle.type === 'target') || closestHandles[0];
|
|
1569
|
+
}
|
|
1570
|
+
function getHandleLookup({ nodes, nodeId, handleId, handleType }) {
|
|
1571
|
+
return nodes.reduce((res, node) => {
|
|
1572
|
+
if (node[internalsSymbol]) {
|
|
1573
|
+
const { handleBounds } = node[internalsSymbol];
|
|
1574
|
+
let sourceHandles = [];
|
|
1575
|
+
let targetHandles = [];
|
|
1576
|
+
if (handleBounds) {
|
|
1577
|
+
sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`);
|
|
1578
|
+
targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`);
|
|
1579
|
+
}
|
|
1580
|
+
res.push(...sourceHandles, ...targetHandles);
|
|
1581
|
+
}
|
|
1582
|
+
return res;
|
|
1583
|
+
}, []);
|
|
1584
|
+
}
|
|
1585
|
+
function getHandleType(edgeUpdaterType, handleDomNode) {
|
|
1586
|
+
if (edgeUpdaterType) {
|
|
1587
|
+
return edgeUpdaterType;
|
|
1588
|
+
}
|
|
1589
|
+
else if (handleDomNode?.classList.contains('target')) {
|
|
1590
|
+
return 'target';
|
|
1591
|
+
}
|
|
1592
|
+
else if (handleDomNode?.classList.contains('source')) {
|
|
1593
|
+
return 'source';
|
|
1594
|
+
}
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
function resetRecentHandle(handleDomNode, lib) {
|
|
1598
|
+
handleDomNode?.classList.remove('valid', 'connecting', `${lib}-flow__handle-valid`, `${lib}-flow__handle-connecting`);
|
|
1599
|
+
}
|
|
1600
|
+
function getConnectionStatus(isInsideConnectionRadius, isHandleValid) {
|
|
1601
|
+
let connectionStatus = null;
|
|
1602
|
+
if (isHandleValid) {
|
|
1603
|
+
connectionStatus = 'valid';
|
|
1604
|
+
}
|
|
1605
|
+
else if (isInsideConnectionRadius && !isHandleValid) {
|
|
1606
|
+
connectionStatus = 'invalid';
|
|
1607
|
+
}
|
|
1608
|
+
return connectionStatus;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const alwaysValid = () => true;
|
|
1612
|
+
let connectionStartHandle = null;
|
|
1613
|
+
function onPointerDown(event, { connectionMode, connectionRadius, handleId, nodeId, edgeUpdaterType, isTarget, domNode, nodes, lib, autoPanOnConnect, panBy, cancelConnection, onConnectStart, onConnect, onConnectEnd, isValidConnection = alwaysValid, onEdgeUpdateEnd, updateConnection, getTransform, }) {
|
|
1614
|
+
// when xyflow is used inside a shadow root we can't use document
|
|
1615
|
+
const doc = getHostForElement(event.target);
|
|
1616
|
+
let autoPanId = 0;
|
|
1617
|
+
let closestHandle;
|
|
1618
|
+
const { x, y } = getEventPosition(event);
|
|
1619
|
+
const clickedHandle = doc?.elementFromPoint(x, y);
|
|
1620
|
+
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
|
|
1621
|
+
const containerBounds = domNode?.getBoundingClientRect();
|
|
1622
|
+
if (!containerBounds || !handleType) {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
let prevActiveHandle;
|
|
1626
|
+
let connectionPosition = getEventPosition(event, containerBounds);
|
|
1627
|
+
let autoPanStarted = false;
|
|
1628
|
+
let connection = null;
|
|
1629
|
+
let isValid = false;
|
|
1630
|
+
let handleDomNode = null;
|
|
1631
|
+
const handleLookup = getHandleLookup({
|
|
1632
|
+
nodes,
|
|
1633
|
+
nodeId,
|
|
1634
|
+
handleId,
|
|
1635
|
+
handleType,
|
|
1636
|
+
});
|
|
1637
|
+
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
|
|
1638
|
+
function autoPan() {
|
|
1639
|
+
if (!autoPanOnConnect || !containerBounds) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
const [x, y] = calcAutoPan(connectionPosition, containerBounds);
|
|
1643
|
+
panBy({ x, y });
|
|
1644
|
+
autoPanId = requestAnimationFrame(autoPan);
|
|
1645
|
+
}
|
|
1646
|
+
// Stays the same for all consecutive pointermove events
|
|
1647
|
+
connectionStartHandle = {
|
|
1648
|
+
nodeId,
|
|
1649
|
+
handleId,
|
|
1650
|
+
type: handleType,
|
|
1651
|
+
};
|
|
1652
|
+
updateConnection({
|
|
1653
|
+
connectionPosition,
|
|
1654
|
+
connectionStatus: null,
|
|
1655
|
+
// connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
|
|
1656
|
+
connectionStartHandle,
|
|
1657
|
+
connectionEndHandle: null,
|
|
1658
|
+
});
|
|
1659
|
+
onConnectStart?.(event, { nodeId, handleId, handleType });
|
|
1660
|
+
function onPointerMove(event) {
|
|
1661
|
+
const transform = getTransform();
|
|
1662
|
+
connectionPosition = getEventPosition(event, containerBounds);
|
|
1663
|
+
closestHandle = getClosestHandle(pointToRendererPoint(connectionPosition, transform, false, [1, 1]), connectionRadius, handleLookup);
|
|
1664
|
+
if (!autoPanStarted) {
|
|
1665
|
+
autoPan();
|
|
1666
|
+
autoPanStarted = true;
|
|
1667
|
+
}
|
|
1668
|
+
const result = isValidHandle(event, {
|
|
1669
|
+
handle: closestHandle,
|
|
1670
|
+
connectionMode,
|
|
1671
|
+
fromNodeId: nodeId,
|
|
1672
|
+
fromHandleId: handleId,
|
|
1673
|
+
fromType: isTarget ? 'target' : 'source',
|
|
1674
|
+
isValidConnection,
|
|
1675
|
+
doc,
|
|
1676
|
+
lib,
|
|
1677
|
+
});
|
|
1678
|
+
handleDomNode = result.handleDomNode;
|
|
1679
|
+
connection = result.connection;
|
|
1680
|
+
isValid = result.isValid;
|
|
1681
|
+
updateConnection({
|
|
1682
|
+
connectionStartHandle,
|
|
1683
|
+
connectionPosition: closestHandle && isValid
|
|
1684
|
+
? rendererPointToPoint({
|
|
1685
|
+
x: closestHandle.x,
|
|
1686
|
+
y: closestHandle.y,
|
|
1687
|
+
}, transform)
|
|
1688
|
+
: connectionPosition,
|
|
1689
|
+
connectionStatus: getConnectionStatus(!!closestHandle, isValid),
|
|
1690
|
+
connectionEndHandle: result.endHandle,
|
|
1691
|
+
});
|
|
1692
|
+
if (!closestHandle && !isValid && !handleDomNode) {
|
|
1693
|
+
return resetRecentHandle(prevActiveHandle, lib);
|
|
1694
|
+
}
|
|
1695
|
+
if (connection?.source !== connection?.target && handleDomNode) {
|
|
1696
|
+
resetRecentHandle(prevActiveHandle, lib);
|
|
1697
|
+
prevActiveHandle = handleDomNode;
|
|
1698
|
+
handleDomNode.classList.add('connecting', `${lib}-flow__handle-connecting`);
|
|
1699
|
+
handleDomNode.classList.toggle('valid', isValid);
|
|
1700
|
+
handleDomNode.classList.toggle(`${lib}-flow__handle-valid`, isValid);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
function onPointerUp(event) {
|
|
1704
|
+
if ((closestHandle || handleDomNode) && connection && isValid) {
|
|
1705
|
+
onConnect?.(connection);
|
|
1706
|
+
}
|
|
1707
|
+
// it's important to get a fresh reference from the store here
|
|
1708
|
+
// in order to get the latest state of onConnectEnd
|
|
1709
|
+
onConnectEnd?.(event);
|
|
1710
|
+
if (edgeUpdaterType) {
|
|
1711
|
+
onEdgeUpdateEnd?.(event);
|
|
1712
|
+
}
|
|
1713
|
+
resetRecentHandle(prevActiveHandle, lib);
|
|
1714
|
+
cancelConnection();
|
|
1715
|
+
cancelAnimationFrame(autoPanId);
|
|
1716
|
+
autoPanStarted = false;
|
|
1717
|
+
isValid = false;
|
|
1718
|
+
connection = null;
|
|
1719
|
+
handleDomNode = null;
|
|
1720
|
+
connectionStartHandle = null;
|
|
1721
|
+
doc.removeEventListener('mousemove', onPointerMove);
|
|
1722
|
+
doc.removeEventListener('mouseup', onPointerUp);
|
|
1723
|
+
doc.removeEventListener('touchmove', onPointerMove);
|
|
1724
|
+
doc.removeEventListener('touchend', onPointerUp);
|
|
1725
|
+
}
|
|
1726
|
+
doc.addEventListener('mousemove', onPointerMove);
|
|
1727
|
+
doc.addEventListener('mouseup', onPointerUp);
|
|
1728
|
+
doc.addEventListener('touchmove', onPointerMove);
|
|
1729
|
+
doc.addEventListener('touchend', onPointerUp);
|
|
1730
|
+
}
|
|
1731
|
+
// checks if and returns connection in fom of an object { source: 123, target: 312 }
|
|
1732
|
+
function isValidHandle(event, { handle, connectionMode, fromNodeId, fromHandleId, fromType, doc, lib, isValidConnection = alwaysValid, }) {
|
|
1733
|
+
const isTarget = fromType === 'target';
|
|
1734
|
+
const handleDomNode = doc.querySelector(`.${lib}-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`);
|
|
1735
|
+
const { x, y } = getEventPosition(event);
|
|
1736
|
+
const handleBelow = doc.elementFromPoint(x, y);
|
|
1737
|
+
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
|
|
1738
|
+
// because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
|
|
1739
|
+
const handleToCheck = handleBelow?.classList.contains(`${lib}-flow__handle`) ? handleBelow : handleDomNode;
|
|
1740
|
+
const result = {
|
|
1741
|
+
handleDomNode: handleToCheck,
|
|
1742
|
+
isValid: false,
|
|
1743
|
+
connection: null,
|
|
1744
|
+
endHandle: null,
|
|
1745
|
+
};
|
|
1746
|
+
if (handleToCheck) {
|
|
1747
|
+
const handleType = getHandleType(undefined, handleToCheck);
|
|
1748
|
+
const handleNodeId = handleToCheck.getAttribute('data-nodeid');
|
|
1749
|
+
const handleId = handleToCheck.getAttribute('data-handleid');
|
|
1750
|
+
const connectable = handleToCheck.classList.contains('connectable');
|
|
1751
|
+
const connectableEnd = handleToCheck.classList.contains('connectableend');
|
|
1752
|
+
if (!handleNodeId) {
|
|
1753
|
+
return result;
|
|
1754
|
+
}
|
|
1755
|
+
const connection = {
|
|
1756
|
+
source: isTarget ? handleNodeId : fromNodeId,
|
|
1757
|
+
sourceHandle: isTarget ? handleId : fromHandleId,
|
|
1758
|
+
target: isTarget ? fromNodeId : handleNodeId,
|
|
1759
|
+
targetHandle: isTarget ? fromHandleId : handleId,
|
|
1760
|
+
};
|
|
1761
|
+
result.connection = connection;
|
|
1762
|
+
const isConnectable = connectable && connectableEnd;
|
|
1763
|
+
// in strict mode we don't allow target to target or source to source connections
|
|
1764
|
+
const isValid = isConnectable &&
|
|
1765
|
+
(connectionMode === ConnectionMode.Strict
|
|
1766
|
+
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
|
|
1767
|
+
: handleNodeId !== fromNodeId || handleId !== fromHandleId);
|
|
1768
|
+
if (isValid) {
|
|
1769
|
+
result.endHandle = {
|
|
1770
|
+
nodeId: handleNodeId,
|
|
1771
|
+
handleId,
|
|
1772
|
+
type: handleType,
|
|
1773
|
+
};
|
|
1774
|
+
result.isValid = isValidConnection(connection);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return result;
|
|
1778
|
+
}
|
|
1779
|
+
const XYHandle = {
|
|
1780
|
+
onPointerDown,
|
|
1781
|
+
isValid: isValidHandle,
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
function XYMinimap({ domNode, panZoom, getTransform, getViewScale }) {
|
|
1785
|
+
const selection = select(domNode);
|
|
1786
|
+
function update({ translateExtent, width, height, zoomStep = 10, pannable = true, zoomable = true, inversePan = false, }) {
|
|
1787
|
+
const zoomHandler = (event) => {
|
|
1788
|
+
const transform = getTransform();
|
|
1789
|
+
if (event.sourceEvent.type !== 'wheel' || !panZoom) {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const pinchDelta = -event.sourceEvent.deltaY *
|
|
1793
|
+
(event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) *
|
|
1794
|
+
zoomStep;
|
|
1795
|
+
const nextZoom = transform[2] * Math.pow(2, pinchDelta);
|
|
1796
|
+
panZoom.scaleTo(nextZoom);
|
|
1797
|
+
};
|
|
1798
|
+
const panHandler = (event) => {
|
|
1799
|
+
const transform = getTransform();
|
|
1800
|
+
if (event.sourceEvent.type !== 'mousemove' || !panZoom) {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
const moveScale = getViewScale() * Math.max(transform[2], Math.log(transform[2])) * (inversePan ? -1 : 1);
|
|
1804
|
+
const position = {
|
|
1805
|
+
x: transform[0] - event.sourceEvent.movementX * moveScale,
|
|
1806
|
+
y: transform[1] - event.sourceEvent.movementY * moveScale,
|
|
1807
|
+
};
|
|
1808
|
+
const extent = [
|
|
1809
|
+
[0, 0],
|
|
1810
|
+
[width, height],
|
|
1811
|
+
];
|
|
1812
|
+
panZoom.setViewportConstrained({
|
|
1813
|
+
x: position.x,
|
|
1814
|
+
y: position.y,
|
|
1815
|
+
zoom: transform[2],
|
|
1816
|
+
}, extent, translateExtent);
|
|
1817
|
+
};
|
|
1818
|
+
const zoomAndPanHandler = zoom()
|
|
1819
|
+
// @ts-ignore
|
|
1820
|
+
.on('zoom', pannable ? panHandler : null)
|
|
1821
|
+
// @ts-ignore
|
|
1822
|
+
.on('zoom.wheel', zoomable ? zoomHandler : null);
|
|
1823
|
+
selection.call(zoomAndPanHandler, {});
|
|
1824
|
+
}
|
|
1825
|
+
function destroy() {
|
|
1826
|
+
selection.on('zoom', null);
|
|
1827
|
+
}
|
|
1828
|
+
return {
|
|
1829
|
+
update,
|
|
1830
|
+
destroy,
|
|
1831
|
+
pointer,
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const viewChanged = (prevViewport, eventViewport) => prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k;
|
|
1836
|
+
const transformToViewport = (transform) => ({
|
|
1837
|
+
x: transform.x,
|
|
1838
|
+
y: transform.y,
|
|
1839
|
+
zoom: transform.k,
|
|
1840
|
+
});
|
|
1841
|
+
const viewportToTransform = ({ x, y, zoom }) => zoomIdentity.translate(x, y).scale(zoom);
|
|
1842
|
+
const isWrappedWithClass = (event, className) => event.target.closest(`.${className}`);
|
|
1843
|
+
const isRightClickPan = (panOnDrag, usedButton) => usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2);
|
|
1844
|
+
const getD3Transition = (selection, duration = 0) => typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration) : selection;
|
|
1845
|
+
const wheelDelta = (event) => {
|
|
1846
|
+
const factor = event.ctrlKey && isMacOs() ? 10 : 1;
|
|
1847
|
+
return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * factor;
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
function createPanOnScrollHandler({ zoomPanValues, noWheelClassName, d3Selection, d3Zoom, panOnScrollMode, panOnScrollSpeed, zoomOnPinch, onPanZoomStart, onPanZoom, onPanZoomEnd, }) {
|
|
1851
|
+
return (event) => {
|
|
1852
|
+
if (isWrappedWithClass(event, noWheelClassName)) {
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
event.preventDefault();
|
|
1856
|
+
event.stopImmediatePropagation();
|
|
1857
|
+
const currentZoom = d3Selection.property('__zoom').k || 1;
|
|
1858
|
+
const _isMacOs = isMacOs();
|
|
1859
|
+
// macos sets ctrlKey=true for pinch gesture on a trackpad
|
|
1860
|
+
if (event.ctrlKey && zoomOnPinch && _isMacOs) {
|
|
1861
|
+
const point = pointer(event);
|
|
1862
|
+
const pinchDelta = wheelDelta(event);
|
|
1863
|
+
const zoom = currentZoom * Math.pow(2, pinchDelta);
|
|
1864
|
+
// @ts-ignore
|
|
1865
|
+
d3Zoom.scaleTo(d3Selection, zoom, point, event);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
// increase scroll speed in firefox
|
|
1869
|
+
// firefox: deltaMode === 1; chrome: deltaMode === 0
|
|
1870
|
+
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
|
|
1871
|
+
let deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
|
|
1872
|
+
let deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
|
|
1873
|
+
// this enables vertical scrolling with shift + scroll on windows
|
|
1874
|
+
if (!_isMacOs && event.shiftKey && panOnScrollMode !== PanOnScrollMode.Vertical) {
|
|
1875
|
+
deltaX = event.deltaY * deltaNormalize;
|
|
1876
|
+
deltaY = 0;
|
|
1877
|
+
}
|
|
1878
|
+
d3Zoom.translateBy(d3Selection, -(deltaX / currentZoom) * panOnScrollSpeed, -(deltaY / currentZoom) * panOnScrollSpeed,
|
|
1879
|
+
// @ts-ignore
|
|
1880
|
+
{ internal: true });
|
|
1881
|
+
const nextViewport = transformToViewport(d3Selection.property('__zoom'));
|
|
1882
|
+
clearTimeout(zoomPanValues.panScrollTimeout);
|
|
1883
|
+
// for pan on scroll we need to handle the event calls on our own
|
|
1884
|
+
// we can't use the start, zoom and end events from d3-zoom
|
|
1885
|
+
// because start and move gets called on every scroll event and not once at the beginning
|
|
1886
|
+
if (!zoomPanValues.isPanScrolling) {
|
|
1887
|
+
zoomPanValues.isPanScrolling = true;
|
|
1888
|
+
onPanZoomStart?.(event, nextViewport);
|
|
1889
|
+
}
|
|
1890
|
+
if (zoomPanValues.isPanScrolling) {
|
|
1891
|
+
onPanZoom?.(event, nextViewport);
|
|
1892
|
+
zoomPanValues.panScrollTimeout = setTimeout(() => {
|
|
1893
|
+
onPanZoomEnd?.(event, nextViewport);
|
|
1894
|
+
zoomPanValues.isPanScrolling = false;
|
|
1895
|
+
}, 150);
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
function createZoomOnScrollHandler({ noWheelClassName, preventScrolling, d3ZoomHandler }) {
|
|
1900
|
+
return function (event, d) {
|
|
1901
|
+
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
event.preventDefault();
|
|
1905
|
+
d3ZoomHandler.call(this, event, d);
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
function createPanZoomStartHandler({ zoomPanValues, onDraggingChange, onPanZoomStart }) {
|
|
1909
|
+
return (event) => {
|
|
1910
|
+
if (event.sourceEvent?.internal) {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const viewport = transformToViewport(event.transform);
|
|
1914
|
+
// we need to remember it here, because it's always 0 in the "zoom" event
|
|
1915
|
+
zoomPanValues.mouseButton = event.sourceEvent?.button || 0;
|
|
1916
|
+
zoomPanValues.isZoomingOrPanning = true;
|
|
1917
|
+
zoomPanValues.prevViewport = viewport;
|
|
1918
|
+
if (event.sourceEvent?.type === 'mousedown') {
|
|
1919
|
+
onDraggingChange(true);
|
|
1920
|
+
}
|
|
1921
|
+
if (onPanZoomStart) {
|
|
1922
|
+
onPanZoomStart?.(event.sourceEvent, viewport);
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
function createPanZoomHandler({ zoomPanValues, panOnDrag, onPaneContextMenu, onTransformChange, onPanZoom, }) {
|
|
1927
|
+
return (event) => {
|
|
1928
|
+
zoomPanValues.usedRightMouseButton = !!(onPaneContextMenu && isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0));
|
|
1929
|
+
if (!event.sourceEvent?.sync) {
|
|
1930
|
+
onTransformChange([event.transform.x, event.transform.y, event.transform.k]);
|
|
1931
|
+
}
|
|
1932
|
+
if (onPanZoom && !event.sourceEvent?.internal) {
|
|
1933
|
+
onPanZoom?.(event.sourceEvent, transformToViewport(event.transform));
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function createPanZoomEndHandler({ zoomPanValues, panOnDrag, panOnScroll, onDraggingChange, onPanZoomEnd, onPaneContextMenu, }) {
|
|
1938
|
+
return (event) => {
|
|
1939
|
+
if (event.sourceEvent?.internal) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
zoomPanValues.isZoomingOrPanning = false;
|
|
1943
|
+
if (onPaneContextMenu &&
|
|
1944
|
+
isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0) &&
|
|
1945
|
+
!zoomPanValues.usedRightMouseButton &&
|
|
1946
|
+
event.sourceEvent) {
|
|
1947
|
+
onPaneContextMenu(event.sourceEvent);
|
|
1948
|
+
}
|
|
1949
|
+
zoomPanValues.usedRightMouseButton = false;
|
|
1950
|
+
onDraggingChange(false);
|
|
1951
|
+
if (onPanZoomEnd && viewChanged(zoomPanValues.prevViewport, event.transform)) {
|
|
1952
|
+
const viewport = transformToViewport(event.transform);
|
|
1953
|
+
zoomPanValues.prevViewport = viewport;
|
|
1954
|
+
clearTimeout(zoomPanValues.timerId);
|
|
1955
|
+
zoomPanValues.timerId = setTimeout(() => {
|
|
1956
|
+
onPanZoomEnd?.(event.sourceEvent, viewport);
|
|
1957
|
+
},
|
|
1958
|
+
// we need a setTimeout for panOnScroll to supress multiple end events fired during scroll
|
|
1959
|
+
panOnScroll ? 150 : 0);
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1965
|
+
function createFilter({ zoomActivationKeyPressed, zoomOnScroll, zoomOnPinch, panOnDrag, panOnScroll, zoomOnDoubleClick, userSelectionActive, noWheelClassName, noPanClassName, lib, }) {
|
|
1966
|
+
return (event) => {
|
|
1967
|
+
const zoomScroll = zoomActivationKeyPressed || zoomOnScroll;
|
|
1968
|
+
const pinchZoom = zoomOnPinch && event.ctrlKey;
|
|
1969
|
+
if (event.button === 1 &&
|
|
1970
|
+
event.type === 'mousedown' &&
|
|
1971
|
+
(isWrappedWithClass(event, `${lib}-flow__node`) || isWrappedWithClass(event, `${lib}-flow__edge`))) {
|
|
1972
|
+
return true;
|
|
1973
|
+
}
|
|
1974
|
+
// if all interactions are disabled, we prevent all zoom events
|
|
1975
|
+
if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) {
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
// during a selection we prevent all other interactions
|
|
1979
|
+
if (userSelectionActive) {
|
|
1980
|
+
return false;
|
|
1981
|
+
}
|
|
1982
|
+
// if zoom on double click is disabled, we prevent the double click event
|
|
1983
|
+
if (!zoomOnDoubleClick && event.type === 'dblclick') {
|
|
1984
|
+
return false;
|
|
1985
|
+
}
|
|
1986
|
+
// if the target element is inside an element with the nowheel class, we prevent zooming
|
|
1987
|
+
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
// if the target element is inside an element with the nopan class, we prevent panning
|
|
1991
|
+
if (isWrappedWithClass(event, noPanClassName) &&
|
|
1992
|
+
(event.type !== 'wheel' || (panOnScroll && event.type === 'wheel' && !zoomActivationKeyPressed))) {
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
|
|
1996
|
+
return false;
|
|
1997
|
+
}
|
|
1998
|
+
// when there is no scroll handling enabled, we prevent all wheel events
|
|
1999
|
+
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
|
|
2000
|
+
return false;
|
|
2001
|
+
}
|
|
2002
|
+
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
|
|
2003
|
+
if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) {
|
|
2004
|
+
return false;
|
|
2005
|
+
}
|
|
2006
|
+
// if the pane is only movable using allowed clicks
|
|
2007
|
+
if (Array.isArray(panOnDrag) &&
|
|
2008
|
+
!panOnDrag.includes(event.button) &&
|
|
2009
|
+
(event.type === 'mousedown' || event.type === 'touchstart')) {
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2012
|
+
// We only allow right clicks if pan on drag is set to right click
|
|
2013
|
+
const buttonAllowed = (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || !event.button || event.button <= 1;
|
|
2014
|
+
// default filter for d3-zoom
|
|
2015
|
+
return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed;
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPanZoom, onPanZoomStart, onPanZoomEnd, onTransformChange, onDraggingChange, }) {
|
|
2020
|
+
const zoomPanValues = {
|
|
2021
|
+
isZoomingOrPanning: false,
|
|
2022
|
+
usedRightMouseButton: false,
|
|
2023
|
+
prevViewport: { x: 0, y: 0, zoom: 0 },
|
|
2024
|
+
mouseButton: 0,
|
|
2025
|
+
timerId: undefined,
|
|
2026
|
+
panScrollTimeout: undefined,
|
|
2027
|
+
isPanScrolling: false,
|
|
2028
|
+
};
|
|
2029
|
+
const bbox = domNode.getBoundingClientRect();
|
|
2030
|
+
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
|
|
2031
|
+
const d3Selection = select(domNode).call(d3ZoomInstance);
|
|
2032
|
+
setViewportConstrained({
|
|
2033
|
+
x: viewport.x,
|
|
2034
|
+
y: viewport.y,
|
|
2035
|
+
zoom: clamp(viewport.zoom, minZoom, maxZoom),
|
|
2036
|
+
}, [
|
|
2037
|
+
[0, 0],
|
|
2038
|
+
[bbox.width, bbox.height],
|
|
2039
|
+
], translateExtent);
|
|
2040
|
+
const d3ZoomHandler = d3Selection.on('wheel.zoom');
|
|
2041
|
+
d3ZoomInstance.wheelDelta(wheelDelta);
|
|
2042
|
+
function setTransform(transform, options) {
|
|
2043
|
+
if (d3Selection) {
|
|
2044
|
+
d3ZoomInstance?.transform(getD3Transition(d3Selection, options?.duration), transform);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
// public functions
|
|
2048
|
+
function update({ noWheelClassName, noPanClassName, onPaneContextMenu, userSelectionActive, panOnScroll, panOnDrag, panOnScrollMode, panOnScrollSpeed, preventScrolling, zoomOnPinch, zoomOnScroll, zoomOnDoubleClick, zoomActivationKeyPressed, lib, }) {
|
|
2049
|
+
if (userSelectionActive && !zoomPanValues.isZoomingOrPanning) {
|
|
2050
|
+
destroy();
|
|
2051
|
+
}
|
|
2052
|
+
const isPanOnScroll = panOnScroll && !zoomActivationKeyPressed && !userSelectionActive;
|
|
2053
|
+
const wheelHandler = isPanOnScroll
|
|
2054
|
+
? createPanOnScrollHandler({
|
|
2055
|
+
zoomPanValues,
|
|
2056
|
+
noWheelClassName,
|
|
2057
|
+
d3Selection,
|
|
2058
|
+
d3Zoom: d3ZoomInstance,
|
|
2059
|
+
panOnScrollMode,
|
|
2060
|
+
panOnScrollSpeed,
|
|
2061
|
+
zoomOnPinch,
|
|
2062
|
+
onPanZoomStart,
|
|
2063
|
+
onPanZoom,
|
|
2064
|
+
onPanZoomEnd,
|
|
2065
|
+
})
|
|
2066
|
+
: createZoomOnScrollHandler({
|
|
2067
|
+
noWheelClassName,
|
|
2068
|
+
preventScrolling,
|
|
2069
|
+
d3ZoomHandler,
|
|
2070
|
+
});
|
|
2071
|
+
d3Selection.on('wheel.zoom', wheelHandler, { passive: false });
|
|
2072
|
+
if (!userSelectionActive) {
|
|
2073
|
+
// pan zoom start
|
|
2074
|
+
const startHandler = createPanZoomStartHandler({
|
|
2075
|
+
zoomPanValues,
|
|
2076
|
+
onDraggingChange,
|
|
2077
|
+
onPanZoomStart,
|
|
2078
|
+
});
|
|
2079
|
+
d3ZoomInstance.on('start', startHandler);
|
|
2080
|
+
// pan zoom
|
|
2081
|
+
const panZoomHandler = createPanZoomHandler({
|
|
2082
|
+
zoomPanValues,
|
|
2083
|
+
panOnDrag,
|
|
2084
|
+
onPaneContextMenu: !!onPaneContextMenu,
|
|
2085
|
+
onPanZoom,
|
|
2086
|
+
onTransformChange,
|
|
2087
|
+
});
|
|
2088
|
+
d3ZoomInstance.on('zoom', panZoomHandler);
|
|
2089
|
+
// pan zoom end
|
|
2090
|
+
const panZoomEndHandler = createPanZoomEndHandler({
|
|
2091
|
+
zoomPanValues,
|
|
2092
|
+
panOnDrag,
|
|
2093
|
+
panOnScroll,
|
|
2094
|
+
onPaneContextMenu,
|
|
2095
|
+
onPanZoomEnd,
|
|
2096
|
+
onDraggingChange,
|
|
2097
|
+
});
|
|
2098
|
+
d3ZoomInstance.on('end', panZoomEndHandler);
|
|
2099
|
+
}
|
|
2100
|
+
const filter = createFilter({
|
|
2101
|
+
zoomActivationKeyPressed,
|
|
2102
|
+
panOnDrag,
|
|
2103
|
+
zoomOnScroll,
|
|
2104
|
+
panOnScroll,
|
|
2105
|
+
zoomOnDoubleClick,
|
|
2106
|
+
zoomOnPinch,
|
|
2107
|
+
userSelectionActive,
|
|
2108
|
+
noPanClassName,
|
|
2109
|
+
noWheelClassName,
|
|
2110
|
+
lib,
|
|
2111
|
+
});
|
|
2112
|
+
d3ZoomInstance.filter(filter);
|
|
2113
|
+
}
|
|
2114
|
+
function destroy() {
|
|
2115
|
+
d3ZoomInstance.on('zoom', null);
|
|
2116
|
+
}
|
|
2117
|
+
function setViewportConstrained(viewport, extent, translateExtent) {
|
|
2118
|
+
const nextTransform = viewportToTransform(viewport);
|
|
2119
|
+
const contrainedTransform = d3ZoomInstance?.constrain()(nextTransform, extent, translateExtent);
|
|
2120
|
+
if (contrainedTransform) {
|
|
2121
|
+
setTransform(contrainedTransform);
|
|
2122
|
+
}
|
|
2123
|
+
return contrainedTransform;
|
|
2124
|
+
}
|
|
2125
|
+
function setViewport(viewport, options) {
|
|
2126
|
+
const nextTransform = viewportToTransform(viewport);
|
|
2127
|
+
setTransform(nextTransform, options);
|
|
2128
|
+
return nextTransform;
|
|
2129
|
+
}
|
|
2130
|
+
function syncViewport(viewport) {
|
|
2131
|
+
if (d3Selection) {
|
|
2132
|
+
const nextTransform = viewportToTransform(viewport);
|
|
2133
|
+
const currentTransform = d3Selection.property('__zoom');
|
|
2134
|
+
if (currentTransform.k !== viewport.zoom ||
|
|
2135
|
+
currentTransform.x !== viewport.x ||
|
|
2136
|
+
currentTransform.y !== viewport.y) {
|
|
2137
|
+
// @ts-ignore
|
|
2138
|
+
d3ZoomInstance?.transform(d3Selection, nextTransform, null, { sync: true });
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
function getViewport() {
|
|
2143
|
+
const transform = d3Selection ? zoomTransform(d3Selection.node()) : { x: 0, y: 0, k: 1 };
|
|
2144
|
+
return { x: transform.x, y: transform.y, zoom: transform.k };
|
|
2145
|
+
}
|
|
2146
|
+
function scaleTo(zoom, options) {
|
|
2147
|
+
if (d3Selection) {
|
|
2148
|
+
d3ZoomInstance?.scaleTo(getD3Transition(d3Selection, options?.duration), zoom);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function scaleBy(factor, options) {
|
|
2152
|
+
if (d3Selection) {
|
|
2153
|
+
d3ZoomInstance?.scaleBy(getD3Transition(d3Selection, options?.duration), factor);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
function setScaleExtent(scaleExtent) {
|
|
2157
|
+
d3ZoomInstance?.scaleExtent(scaleExtent);
|
|
2158
|
+
}
|
|
2159
|
+
function setTranslateExtent(translateExtent) {
|
|
2160
|
+
d3ZoomInstance?.translateExtent(translateExtent);
|
|
2161
|
+
}
|
|
2162
|
+
return {
|
|
2163
|
+
update,
|
|
2164
|
+
destroy,
|
|
2165
|
+
setViewport,
|
|
2166
|
+
setViewportConstrained,
|
|
2167
|
+
getViewport,
|
|
2168
|
+
scaleTo,
|
|
2169
|
+
scaleBy,
|
|
2170
|
+
setScaleExtent,
|
|
2171
|
+
setTranslateExtent,
|
|
2172
|
+
syncViewport,
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
export { ConnectionLineType, ConnectionMode, MarkerType, PanOnScrollMode, Position, SelectionMode, XYDrag, XYHandle, XYMinimap, XYPanZoom, addEdgeBase, adoptUserProvidedNodes, areConnectionMapsEqual, boxToRect, calcAutoPan, calcNextPosition, clamp, clampPosition, createMarkerIds, devWarn, elementSelectionKeys, errorMessages, fitView, getBezierEdgeCenter, getBezierPath, getBoundsOfBoxes, getBoundsOfRects, getConnectedEdgesBase, getDimensions, getEdgeCenter, getEdgePosition, getElementsToRemove, getElevatedEdgeZIndex, getEventPosition, getHandleBounds, getHostForElement, getIncomersBase, getMarkerId, getNodePositionWithOrigin, getNodeToolbarTransform, getNodesBounds, getNodesInside, getOutgoersBase, getOverlappingArea, getPointerPosition, getPositionWithOrigin, getSmoothStepPath, getStraightPath, getViewportForBounds, handleConnectionChange, infiniteExtent, internalsSymbol, isEdgeBase, isEdgeVisible, isInputDOMNode, isMacOs, isMouseEvent, isNodeBase, isNumeric, isRectObject, nodeToBox, nodeToRect, panBy, pointToRendererPoint, rectToBox, rendererPointToPoint, snapPosition, updateAbsolutePositions, updateConnectionLookup, updateEdgeBase, updateNodeDimensions };
|