@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.
Files changed (48) hide show
  1. package/dist/esm/index.js +2176 -0
  2. package/dist/esm/index.mjs +266 -146
  3. package/dist/esm/types/edges.d.ts +2 -2
  4. package/dist/esm/types/edges.d.ts.map +1 -1
  5. package/dist/esm/types/general.d.ts +7 -0
  6. package/dist/esm/types/general.d.ts.map +1 -1
  7. package/dist/esm/types/nodes.d.ts +7 -2
  8. package/dist/esm/types/nodes.d.ts.map +1 -1
  9. package/dist/esm/utils/edges/bezier-edge.d.ts +23 -0
  10. package/dist/esm/utils/edges/bezier-edge.d.ts.map +1 -1
  11. package/dist/esm/utils/edges/general.d.ts +23 -5
  12. package/dist/esm/utils/edges/general.d.ts.map +1 -1
  13. package/dist/esm/utils/edges/positions.d.ts.map +1 -1
  14. package/dist/esm/utils/edges/smoothstep-edge.d.ts +22 -0
  15. package/dist/esm/utils/edges/smoothstep-edge.d.ts.map +1 -1
  16. package/dist/esm/utils/edges/straight-edge.d.ts +20 -0
  17. package/dist/esm/utils/edges/straight-edge.d.ts.map +1 -1
  18. package/dist/esm/utils/general.d.ts +19 -2
  19. package/dist/esm/utils/general.d.ts.map +1 -1
  20. package/dist/esm/utils/graph.d.ts +61 -6
  21. package/dist/esm/utils/graph.d.ts.map +1 -1
  22. package/dist/esm/utils/store.d.ts +4 -4
  23. package/dist/esm/utils/store.d.ts.map +1 -1
  24. package/dist/esm/xyminimap/index.d.ts.map +1 -1
  25. package/dist/umd/index.js +1 -1
  26. package/dist/umd/types/edges.d.ts +2 -2
  27. package/dist/umd/types/edges.d.ts.map +1 -1
  28. package/dist/umd/types/general.d.ts +7 -0
  29. package/dist/umd/types/general.d.ts.map +1 -1
  30. package/dist/umd/types/nodes.d.ts +7 -2
  31. package/dist/umd/types/nodes.d.ts.map +1 -1
  32. package/dist/umd/utils/edges/bezier-edge.d.ts +23 -0
  33. package/dist/umd/utils/edges/bezier-edge.d.ts.map +1 -1
  34. package/dist/umd/utils/edges/general.d.ts +23 -5
  35. package/dist/umd/utils/edges/general.d.ts.map +1 -1
  36. package/dist/umd/utils/edges/positions.d.ts.map +1 -1
  37. package/dist/umd/utils/edges/smoothstep-edge.d.ts +22 -0
  38. package/dist/umd/utils/edges/smoothstep-edge.d.ts.map +1 -1
  39. package/dist/umd/utils/edges/straight-edge.d.ts +20 -0
  40. package/dist/umd/utils/edges/straight-edge.d.ts.map +1 -1
  41. package/dist/umd/utils/general.d.ts +19 -2
  42. package/dist/umd/utils/general.d.ts.map +1 -1
  43. package/dist/umd/utils/graph.d.ts +61 -6
  44. package/dist/umd/utils/graph.d.ts.map +1 -1
  45. package/dist/umd/utils/store.d.ts +4 -4
  46. package/dist/umd/utils/store.d.ts.map +1 -1
  47. package/dist/umd/xyminimap/index.d.ts.map +1 -1
  48. 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 };