@xyflow/system 0.0.1

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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/dist/esm/constants.d.ts +22 -0
  4. package/dist/esm/constants.d.ts.map +1 -0
  5. package/dist/esm/index.d.ts +8 -0
  6. package/dist/esm/index.d.ts.map +1 -0
  7. package/dist/esm/index.js +1741 -0
  8. package/dist/esm/types/edges.d.ts +62 -0
  9. package/dist/esm/types/edges.d.ts.map +1 -0
  10. package/dist/esm/types/general.d.ts +106 -0
  11. package/dist/esm/types/general.d.ts.map +1 -0
  12. package/dist/esm/types/handles.d.ts +29 -0
  13. package/dist/esm/types/handles.d.ts.map +1 -0
  14. package/dist/esm/types/index.d.ts +7 -0
  15. package/dist/esm/types/index.d.ts.map +1 -0
  16. package/dist/esm/types/nodes.d.ts +76 -0
  17. package/dist/esm/types/nodes.d.ts.map +1 -0
  18. package/dist/esm/types/panzoom.d.ts +48 -0
  19. package/dist/esm/types/panzoom.d.ts.map +1 -0
  20. package/dist/esm/types/utils.d.ts +25 -0
  21. package/dist/esm/types/utils.d.ts.map +1 -0
  22. package/dist/esm/utils/dom.d.ts +20 -0
  23. package/dist/esm/utils/dom.d.ts.map +1 -0
  24. package/dist/esm/utils/edges/bezier-edge.d.ts +30 -0
  25. package/dist/esm/utils/edges/bezier-edge.d.ts.map +1 -0
  26. package/dist/esm/utils/edges/general.d.ts +29 -0
  27. package/dist/esm/utils/edges/general.d.ts.map +1 -0
  28. package/dist/esm/utils/edges/index.d.ts +6 -0
  29. package/dist/esm/utils/edges/index.d.ts.map +1 -0
  30. package/dist/esm/utils/edges/positions.d.ts +14 -0
  31. package/dist/esm/utils/edges/positions.d.ts.map +1 -0
  32. package/dist/esm/utils/edges/smoothstep-edge.d.ts +15 -0
  33. package/dist/esm/utils/edges/smoothstep-edge.d.ts.map +1 -0
  34. package/dist/esm/utils/edges/straight-edge.d.ts +8 -0
  35. package/dist/esm/utils/edges/straight-edge.d.ts.map +1 -0
  36. package/dist/esm/utils/general.d.ts +29 -0
  37. package/dist/esm/utils/general.d.ts.map +1 -0
  38. package/dist/esm/utils/graph.d.ts +26 -0
  39. package/dist/esm/utils/graph.d.ts.map +1 -0
  40. package/dist/esm/utils/index.d.ts +7 -0
  41. package/dist/esm/utils/index.d.ts.map +1 -0
  42. package/dist/esm/utils/marker.d.ts +7 -0
  43. package/dist/esm/utils/marker.d.ts.map +1 -0
  44. package/dist/esm/utils/store.d.ts +20 -0
  45. package/dist/esm/utils/store.d.ts.map +1 -0
  46. package/dist/esm/utils/utils.d.ts +29 -0
  47. package/dist/esm/utils/utils.d.ts.map +1 -0
  48. package/dist/esm/xydrag/XYDrag.d.ts +48 -0
  49. package/dist/esm/xydrag/XYDrag.d.ts.map +1 -0
  50. package/dist/esm/xydrag/index.d.ts +2 -0
  51. package/dist/esm/xydrag/index.d.ts.map +1 -0
  52. package/dist/esm/xydrag/utils.d.ts +11 -0
  53. package/dist/esm/xydrag/utils.d.ts.map +1 -0
  54. package/dist/esm/xyhandle/XYHandle.d.ts +45 -0
  55. package/dist/esm/xyhandle/XYHandle.d.ts.map +1 -0
  56. package/dist/esm/xyhandle/index.d.ts +2 -0
  57. package/dist/esm/xyhandle/index.d.ts.map +1 -0
  58. package/dist/esm/xyhandle/utils.d.ts +15 -0
  59. package/dist/esm/xyhandle/utils.d.ts.map +1 -0
  60. package/dist/esm/xyminimap/index.d.ts +28 -0
  61. package/dist/esm/xyminimap/index.d.ts.map +1 -0
  62. package/dist/esm/xypanzoom/XYPanZoom.d.ts +10 -0
  63. package/dist/esm/xypanzoom/XYPanZoom.d.ts.map +1 -0
  64. package/dist/esm/xypanzoom/eventhandler.d.ts +48 -0
  65. package/dist/esm/xypanzoom/eventhandler.d.ts.map +1 -0
  66. package/dist/esm/xypanzoom/filter.d.ts +14 -0
  67. package/dist/esm/xypanzoom/filter.d.ts.map +1 -0
  68. package/dist/esm/xypanzoom/index.d.ts +2 -0
  69. package/dist/esm/xypanzoom/index.d.ts.map +1 -0
  70. package/dist/esm/xypanzoom/utils.d.ts +9 -0
  71. package/dist/esm/xypanzoom/utils.d.ts.map +1 -0
  72. package/dist/umd/constants.d.ts +22 -0
  73. package/dist/umd/constants.d.ts.map +1 -0
  74. package/dist/umd/index.d.ts +8 -0
  75. package/dist/umd/index.d.ts.map +1 -0
  76. package/dist/umd/index.js +1 -0
  77. package/dist/umd/types/edges.d.ts +62 -0
  78. package/dist/umd/types/edges.d.ts.map +1 -0
  79. package/dist/umd/types/general.d.ts +106 -0
  80. package/dist/umd/types/general.d.ts.map +1 -0
  81. package/dist/umd/types/handles.d.ts +29 -0
  82. package/dist/umd/types/handles.d.ts.map +1 -0
  83. package/dist/umd/types/index.d.ts +7 -0
  84. package/dist/umd/types/index.d.ts.map +1 -0
  85. package/dist/umd/types/nodes.d.ts +76 -0
  86. package/dist/umd/types/nodes.d.ts.map +1 -0
  87. package/dist/umd/types/panzoom.d.ts +48 -0
  88. package/dist/umd/types/panzoom.d.ts.map +1 -0
  89. package/dist/umd/types/utils.d.ts +25 -0
  90. package/dist/umd/types/utils.d.ts.map +1 -0
  91. package/dist/umd/utils/dom.d.ts +20 -0
  92. package/dist/umd/utils/dom.d.ts.map +1 -0
  93. package/dist/umd/utils/edges/bezier-edge.d.ts +30 -0
  94. package/dist/umd/utils/edges/bezier-edge.d.ts.map +1 -0
  95. package/dist/umd/utils/edges/general.d.ts +29 -0
  96. package/dist/umd/utils/edges/general.d.ts.map +1 -0
  97. package/dist/umd/utils/edges/index.d.ts +6 -0
  98. package/dist/umd/utils/edges/index.d.ts.map +1 -0
  99. package/dist/umd/utils/edges/positions.d.ts +14 -0
  100. package/dist/umd/utils/edges/positions.d.ts.map +1 -0
  101. package/dist/umd/utils/edges/smoothstep-edge.d.ts +15 -0
  102. package/dist/umd/utils/edges/smoothstep-edge.d.ts.map +1 -0
  103. package/dist/umd/utils/edges/straight-edge.d.ts +8 -0
  104. package/dist/umd/utils/edges/straight-edge.d.ts.map +1 -0
  105. package/dist/umd/utils/general.d.ts +29 -0
  106. package/dist/umd/utils/general.d.ts.map +1 -0
  107. package/dist/umd/utils/graph.d.ts +26 -0
  108. package/dist/umd/utils/graph.d.ts.map +1 -0
  109. package/dist/umd/utils/index.d.ts +7 -0
  110. package/dist/umd/utils/index.d.ts.map +1 -0
  111. package/dist/umd/utils/marker.d.ts +7 -0
  112. package/dist/umd/utils/marker.d.ts.map +1 -0
  113. package/dist/umd/utils/store.d.ts +20 -0
  114. package/dist/umd/utils/store.d.ts.map +1 -0
  115. package/dist/umd/utils/utils.d.ts +45 -0
  116. package/dist/umd/utils/utils.d.ts.map +1 -0
  117. package/dist/umd/xydrag/XYDrag.d.ts +48 -0
  118. package/dist/umd/xydrag/XYDrag.d.ts.map +1 -0
  119. package/dist/umd/xydrag/index.d.ts +2 -0
  120. package/dist/umd/xydrag/index.d.ts.map +1 -0
  121. package/dist/umd/xydrag/utils.d.ts +11 -0
  122. package/dist/umd/xydrag/utils.d.ts.map +1 -0
  123. package/dist/umd/xyhandle/XYHandle.d.ts +45 -0
  124. package/dist/umd/xyhandle/XYHandle.d.ts.map +1 -0
  125. package/dist/umd/xyhandle/index.d.ts +2 -0
  126. package/dist/umd/xyhandle/index.d.ts.map +1 -0
  127. package/dist/umd/xyhandle/utils.d.ts +15 -0
  128. package/dist/umd/xyhandle/utils.d.ts.map +1 -0
  129. package/dist/umd/xyminimap/index.d.ts +28 -0
  130. package/dist/umd/xyminimap/index.d.ts.map +1 -0
  131. package/dist/umd/xypanzoom/XYPanZoom.d.ts +10 -0
  132. package/dist/umd/xypanzoom/XYPanZoom.d.ts.map +1 -0
  133. package/dist/umd/xypanzoom/eventhandler.d.ts +48 -0
  134. package/dist/umd/xypanzoom/eventhandler.d.ts.map +1 -0
  135. package/dist/umd/xypanzoom/filter.d.ts +14 -0
  136. package/dist/umd/xypanzoom/filter.d.ts.map +1 -0
  137. package/dist/umd/xypanzoom/index.d.ts +2 -0
  138. package/dist/umd/xypanzoom/index.d.ts.map +1 -0
  139. package/dist/umd/xypanzoom/utils.d.ts +9 -0
  140. package/dist/umd/xypanzoom/utils.d.ts.map +1 -0
  141. package/package.json +62 -0
@@ -0,0 +1,1741 @@
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
+ };
19
+ const internalsSymbol = Symbol.for('internals');
20
+ const infiniteExtent = [
21
+ [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
22
+ [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
23
+ ];
24
+ const elementSelectionKeys = ['Enter', ' ', 'Escape'];
25
+
26
+ var ConnectionMode;
27
+ (function (ConnectionMode) {
28
+ ConnectionMode["Strict"] = "strict";
29
+ ConnectionMode["Loose"] = "loose";
30
+ })(ConnectionMode || (ConnectionMode = {}));
31
+ var PanOnScrollMode;
32
+ (function (PanOnScrollMode) {
33
+ PanOnScrollMode["Free"] = "free";
34
+ PanOnScrollMode["Vertical"] = "vertical";
35
+ PanOnScrollMode["Horizontal"] = "horizontal";
36
+ })(PanOnScrollMode || (PanOnScrollMode = {}));
37
+ var SelectionMode;
38
+ (function (SelectionMode) {
39
+ SelectionMode["Partial"] = "partial";
40
+ SelectionMode["Full"] = "full";
41
+ })(SelectionMode || (SelectionMode = {}));
42
+
43
+ var ConnectionLineType;
44
+ (function (ConnectionLineType) {
45
+ ConnectionLineType["Bezier"] = "default";
46
+ ConnectionLineType["Straight"] = "straight";
47
+ ConnectionLineType["Step"] = "step";
48
+ ConnectionLineType["SmoothStep"] = "smoothstep";
49
+ ConnectionLineType["SimpleBezier"] = "simplebezier";
50
+ })(ConnectionLineType || (ConnectionLineType = {}));
51
+ var MarkerType;
52
+ (function (MarkerType) {
53
+ MarkerType["Arrow"] = "arrow";
54
+ MarkerType["ArrowClosed"] = "arrowclosed";
55
+ })(MarkerType || (MarkerType = {}));
56
+
57
+ var Position;
58
+ (function (Position) {
59
+ Position["Left"] = "left";
60
+ Position["Top"] = "top";
61
+ Position["Right"] = "right";
62
+ Position["Bottom"] = "bottom";
63
+ })(Position || (Position = {}));
64
+
65
+ /* eslint-disable @typescript-eslint/no-explicit-any */
66
+ const isEdgeBase = (element) => 'id' in element && 'source' in element && 'target' in element;
67
+ const isNodeBase = (element) => 'id' in element && !('source' in element) && !('target' in element);
68
+ const getOutgoersBase = (node, nodes, edges) => {
69
+ if (!isNodeBase(node)) {
70
+ return [];
71
+ }
72
+ const outgoerIds = edges.filter((e) => e.source === node.id).map((e) => e.target);
73
+ return nodes.filter((n) => outgoerIds.includes(n.id));
74
+ };
75
+ const getIncomersBase = (node, nodes, edges) => {
76
+ if (!isNodeBase(node)) {
77
+ return [];
78
+ }
79
+ const incomersIds = edges.filter((e) => e.target === node.id).map((e) => e.source);
80
+ return nodes.filter((n) => incomersIds.includes(n.id));
81
+ };
82
+ const getNodePositionWithOrigin = (node, nodeOrigin = [0, 0]) => {
83
+ if (!node) {
84
+ return {
85
+ x: 0,
86
+ y: 0,
87
+ positionAbsolute: {
88
+ x: 0,
89
+ y: 0,
90
+ },
91
+ };
92
+ }
93
+ const offsetX = (node.width ?? 0) * nodeOrigin[0];
94
+ const offsetY = (node.height ?? 0) * nodeOrigin[1];
95
+ const position = {
96
+ x: node.position.x - offsetX,
97
+ y: node.position.y - offsetY,
98
+ };
99
+ return {
100
+ ...position,
101
+ positionAbsolute: node.positionAbsolute
102
+ ? {
103
+ x: node.positionAbsolute.x - offsetX,
104
+ y: node.positionAbsolute.y - offsetY,
105
+ }
106
+ : position,
107
+ };
108
+ };
109
+ const getRectOfNodes = (nodes, nodeOrigin = [0, 0]) => {
110
+ if (nodes.length === 0) {
111
+ return { x: 0, y: 0, width: 0, height: 0 };
112
+ }
113
+ const box = nodes.reduce((currBox, node) => {
114
+ const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
115
+ return getBoundsOfBoxes(currBox, rectToBox({
116
+ x,
117
+ y,
118
+ width: node.width || 0,
119
+ height: node.height || 0,
120
+ }));
121
+ }, { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity });
122
+ return boxToRect(box);
123
+ };
124
+ const getNodesInside = (nodes, rect, [tx, ty, tScale] = [0, 0, 1], partially = false,
125
+ // set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
126
+ excludeNonSelectableNodes = false, nodeOrigin = [0, 0]) => {
127
+ const paneRect = {
128
+ ...pointToRendererPoint(rect, [tx, ty, tScale]),
129
+ width: rect.width / tScale,
130
+ height: rect.height / tScale,
131
+ };
132
+ const visibleNodes = nodes.reduce((res, node) => {
133
+ const { width, height, selectable = true, hidden = false } = node;
134
+ if ((excludeNonSelectableNodes && !selectable) || hidden) {
135
+ return res;
136
+ }
137
+ const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
138
+ const notInitialized = width === undefined || height === undefined || width === null || height === null;
139
+ const partiallyVisible = partially && overlappingArea > 0;
140
+ const area = (width || 0) * (height || 0);
141
+ const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
142
+ if (isVisible || node.dragging) {
143
+ res.push(node);
144
+ }
145
+ return res;
146
+ }, []);
147
+ return visibleNodes;
148
+ };
149
+ const getConnectedEdgesBase = (nodes, edges) => {
150
+ const nodeIds = nodes.map((node) => node.id);
151
+ return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target));
152
+ };
153
+ function fitView({ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }, options) {
154
+ const filteredNodes = nodes.filter((n) => {
155
+ const isVisible = options?.includeHiddenNodes ? n.width && n.height : !n.hidden;
156
+ if (options?.nodes?.length) {
157
+ return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id);
158
+ }
159
+ return isVisible;
160
+ });
161
+ const nodesInitialized = filteredNodes.every((n) => n.width && n.height);
162
+ if (nodes.length > 0 && nodesInitialized) {
163
+ const bounds = getRectOfNodes(nodes, nodeOrigin);
164
+ const [x, y, zoom] = getTransformForBounds(bounds, width, height, options?.minZoom ?? minZoom, options?.maxZoom ?? maxZoom, options?.padding ?? 0.1);
165
+ panZoom.setViewport({ x, y, zoom }, { duration: options?.duration });
166
+ return true;
167
+ }
168
+ return false;
169
+ }
170
+ function calcNextPosition(node, nextPosition, nodes, nodeExtent, nodeOrigin = [0, 0], onError) {
171
+ let currentExtent = node.extent || nodeExtent;
172
+ let parentNode = null;
173
+ let parentPos = { x: 0, y: 0 };
174
+ if (node.parentNode) {
175
+ parentNode = nodes.find((n) => n.id === node.parentNode) || null;
176
+ parentPos = parentNode
177
+ ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
178
+ : parentPos;
179
+ }
180
+ if (node.extent === 'parent') {
181
+ if (node.parentNode && node.width && node.height) {
182
+ const currNodeOrigin = node.origin || nodeOrigin;
183
+ currentExtent =
184
+ parentNode && isNumeric(parentNode.width) && isNumeric(parentNode.height)
185
+ ? [
186
+ [parentPos.x + node.width * currNodeOrigin[0], parentPos.y + node.height * currNodeOrigin[1]],
187
+ [
188
+ parentPos.x + parentNode.width - node.width + node.width * currNodeOrigin[0],
189
+ parentPos.y + parentNode.height - node.height + node.height * currNodeOrigin[1],
190
+ ],
191
+ ]
192
+ : currentExtent;
193
+ }
194
+ else {
195
+ onError?.('005', errorMessages['error005']());
196
+ currentExtent = nodeExtent;
197
+ }
198
+ }
199
+ else if (node.extent && node.parentNode) {
200
+ currentExtent = [
201
+ [node.extent[0][0] + parentPos.x, node.extent[0][1] + parentPos.y],
202
+ [node.extent[1][0] + parentPos.x, node.extent[1][1] + parentPos.y],
203
+ ];
204
+ }
205
+ const positionAbsolute = currentExtent
206
+ ? clampPosition(nextPosition, currentExtent)
207
+ : nextPosition;
208
+ return {
209
+ position: {
210
+ x: positionAbsolute.x - parentPos.x,
211
+ y: positionAbsolute.y - parentPos.y,
212
+ },
213
+ positionAbsolute,
214
+ };
215
+ }
216
+ // helper function to get arrays of nodes and edges that can be deleted
217
+ // you can pass in a list of nodes and edges that should be deleted
218
+ // and the function only returns elements that are deletable and also handles connected nodes and child nodes
219
+ function getElementsToRemove({ nodesToRemove, edgesToRemove, nodes, edges, }) {
220
+ const nodeIds = nodesToRemove.map((node) => node.id);
221
+ const edgeIds = edgesToRemove.map((edge) => edge.id);
222
+ const matchingNodes = nodes.reduce((res, node) => {
223
+ const parentHit = !nodeIds.includes(node.id) && node.parentNode && res.find((n) => n.id === node.parentNode);
224
+ const deletable = typeof node.deletable === 'boolean' ? node.deletable : true;
225
+ if (deletable && (nodeIds.includes(node.id) || parentHit)) {
226
+ res.push(node);
227
+ }
228
+ return res;
229
+ }, []);
230
+ const deletableEdges = edges.filter((e) => (typeof e.deletable === 'boolean' ? e.deletable : true));
231
+ const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
232
+ const connectedEdges = getConnectedEdgesBase(matchingNodes, deletableEdges);
233
+ const matchingEdges = [...initialHitEdges, ...connectedEdges];
234
+ return {
235
+ matchingEdges,
236
+ matchingNodes,
237
+ };
238
+ }
239
+
240
+ const clamp = (val, min = 0, max = 1) => Math.min(Math.max(val, min), max);
241
+ const clampPosition = (position = { x: 0, y: 0 }, extent) => ({
242
+ x: clamp(position.x, extent[0][0], extent[1][0]),
243
+ y: clamp(position.y, extent[0][1], extent[1][1]),
244
+ });
245
+ // returns a number between 0 and 1 that represents the velocity of the movement
246
+ // when the mouse is close to the edge of the canvas
247
+ const calcAutoPanVelocity = (value, min, max) => {
248
+ if (value < min) {
249
+ return clamp(Math.abs(value - min), 1, 50) / 50;
250
+ }
251
+ else if (value > max) {
252
+ return -clamp(Math.abs(value - max), 1, 50) / 50;
253
+ }
254
+ return 0;
255
+ };
256
+ const calcAutoPan = (pos, bounds) => {
257
+ const xMovement = calcAutoPanVelocity(pos.x, 35, bounds.width - 35) * 20;
258
+ const yMovement = calcAutoPanVelocity(pos.y, 35, bounds.height - 35) * 20;
259
+ return [xMovement, yMovement];
260
+ };
261
+ const getBoundsOfBoxes = (box1, box2) => ({
262
+ x: Math.min(box1.x, box2.x),
263
+ y: Math.min(box1.y, box2.y),
264
+ x2: Math.max(box1.x2, box2.x2),
265
+ y2: Math.max(box1.y2, box2.y2),
266
+ });
267
+ const rectToBox = ({ x, y, width, height }) => ({
268
+ x,
269
+ y,
270
+ x2: x + width,
271
+ y2: y + height,
272
+ });
273
+ const boxToRect = ({ x, y, x2, y2 }) => ({
274
+ x,
275
+ y,
276
+ width: x2 - x,
277
+ height: y2 - y,
278
+ });
279
+ const nodeToRect = (node, nodeOrigin = [0, 0]) => {
280
+ const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
281
+ return {
282
+ ...positionAbsolute,
283
+ width: node.width || 0,
284
+ height: node.height || 0,
285
+ };
286
+ };
287
+ const nodeToBox = (node, nodeOrigin = [0, 0]) => {
288
+ const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
289
+ return {
290
+ ...positionAbsolute,
291
+ x2: positionAbsolute.x + (node.width || 0),
292
+ y2: positionAbsolute.y + (node.height || 0),
293
+ };
294
+ };
295
+ const getBoundsOfRects = (rect1, rect2) => boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
296
+ const getOverlappingArea = (rectA, rectB) => {
297
+ const xOverlap = Math.max(0, Math.min(rectA.x + rectA.width, rectB.x + rectB.width) - Math.max(rectA.x, rectB.x));
298
+ const yOverlap = Math.max(0, Math.min(rectA.y + rectA.height, rectB.y + rectB.height) - Math.max(rectA.y, rectB.y));
299
+ return Math.ceil(xOverlap * yOverlap);
300
+ };
301
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
+ const isRectObject = (obj) => isNumeric(obj.width) && isNumeric(obj.height) && isNumeric(obj.x) && isNumeric(obj.y);
303
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
304
+ const isNumeric = (n) => !isNaN(n) && isFinite(n);
305
+ // used for a11y key board controls for nodes and edges
306
+ const devWarn = (id, message) => {
307
+ if (process.env.NODE_ENV === 'development') {
308
+ console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`);
309
+ }
310
+ };
311
+ const getPositionWithOrigin = ({ x, y, width, height, origin = [0, 0], }) => {
312
+ if (!width || !height || origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
313
+ return { x, y };
314
+ }
315
+ return {
316
+ x: x - width * origin[0],
317
+ y: y - height * origin[1],
318
+ };
319
+ };
320
+ function snapPosition(position, snapGrid = [1, 1]) {
321
+ return {
322
+ x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
323
+ y: snapGrid[1] * Math.round(position.y / snapGrid[1]),
324
+ };
325
+ }
326
+ const pointToRendererPoint = ({ x, y }, [tx, ty, tScale], snapToGrid = false, snapGrid = [1, 1]) => {
327
+ const position = {
328
+ x: (x - tx) / tScale,
329
+ y: (y - ty) / tScale,
330
+ };
331
+ return snapToGrid ? snapPosition(position, snapGrid) : position;
332
+ };
333
+ const rendererPointToPoint = ({ x, y }, [tx, ty, tScale]) => {
334
+ return {
335
+ x: x * tScale + tx,
336
+ y: y * tScale + ty,
337
+ };
338
+ };
339
+ const getTransformForBounds = (bounds, width, height, minZoom, maxZoom, padding = 0.1) => {
340
+ const xZoom = width / (bounds.width * (1 + padding));
341
+ const yZoom = height / (bounds.height * (1 + padding));
342
+ const zoom = Math.min(xZoom, yZoom);
343
+ const clampedZoom = clamp(zoom, minZoom, maxZoom);
344
+ const boundsCenterX = bounds.x + bounds.width / 2;
345
+ const boundsCenterY = bounds.y + bounds.height / 2;
346
+ const x = width / 2 - boundsCenterX * clampedZoom;
347
+ const y = height / 2 - boundsCenterY * clampedZoom;
348
+ return [x, y, clampedZoom];
349
+ };
350
+
351
+ function getPointerPosition(event, { snapGrid = [0, 0], snapToGrid = false, transform }) {
352
+ const { x, y } = getEventPosition(event);
353
+ const pointerPos = pointToRendererPoint({ x, y }, transform);
354
+ const { x: xSnapped, y: ySnapped } = snapToGrid ? snapPosition(pointerPos, snapGrid) : pointerPos;
355
+ // we need the snapped position in order to be able to skip unnecessary drag events
356
+ return {
357
+ xSnapped,
358
+ ySnapped,
359
+ ...pointerPos,
360
+ };
361
+ }
362
+ const getDimensions = (node) => ({
363
+ width: node.offsetWidth,
364
+ height: node.offsetHeight,
365
+ });
366
+ const getHostForElement = (element) => element.getRootNode?.() || window?.document;
367
+ const inputTags = ['INPUT', 'SELECT', 'TEXTAREA'];
368
+ function isInputDOMNode(event) {
369
+ // using composed path for handling shadow dom
370
+ const target = (event.composedPath?.()?.[0] || event.target);
371
+ const isInput = inputTags.includes(target?.nodeName) || target?.hasAttribute('contenteditable');
372
+ // we want to be able to do a multi selection event if we are in an input field
373
+ const isModifierKey = event.ctrlKey || event.metaKey || event.shiftKey;
374
+ // when an input field is focused we don't want to trigger deletion or movement of nodes
375
+ return (isInput && !isModifierKey) || !!target?.closest('.nokey');
376
+ }
377
+ const isMouseEvent = (event) => 'clientX' in event;
378
+ const getEventPosition = (event, bounds) => {
379
+ const isMouse = isMouseEvent(event);
380
+ const evtX = isMouse ? event.clientX : event.touches?.[0].clientX;
381
+ const evtY = isMouse ? event.clientY : event.touches?.[0].clientY;
382
+ return {
383
+ x: evtX - (bounds?.left ?? 0),
384
+ y: evtY - (bounds?.top ?? 0),
385
+ };
386
+ };
387
+ const getHandleBounds = (selector, nodeElement, zoom, nodeOrigin = [0, 0]) => {
388
+ const handles = nodeElement.querySelectorAll(selector);
389
+ if (!handles || !handles.length) {
390
+ return null;
391
+ }
392
+ const handlesArray = Array.from(handles);
393
+ const nodeBounds = nodeElement.getBoundingClientRect();
394
+ const nodeOffset = {
395
+ x: nodeBounds.width * nodeOrigin[0],
396
+ y: nodeBounds.height * nodeOrigin[1],
397
+ };
398
+ return handlesArray.map((handle) => {
399
+ const handleBounds = handle.getBoundingClientRect();
400
+ return {
401
+ id: handle.getAttribute('data-handleid'),
402
+ position: handle.getAttribute('data-handlepos'),
403
+ x: (handleBounds.left - nodeBounds.left - nodeOffset.x) / zoom,
404
+ y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom,
405
+ ...getDimensions(handle),
406
+ };
407
+ });
408
+ };
409
+
410
+ function getBezierEdgeCenter({ sourceX, sourceY, targetX, targetY, sourceControlX, sourceControlY, targetControlX, targetControlY, }) {
411
+ // cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
412
+ // https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
413
+ const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
414
+ const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
415
+ const offsetX = Math.abs(centerX - sourceX);
416
+ const offsetY = Math.abs(centerY - sourceY);
417
+ return [centerX, centerY, offsetX, offsetY];
418
+ }
419
+ function calculateControlOffset(distance, curvature) {
420
+ if (distance >= 0) {
421
+ return 0.5 * distance;
422
+ }
423
+ return curvature * 25 * Math.sqrt(-distance);
424
+ }
425
+ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }) {
426
+ switch (pos) {
427
+ case Position.Left:
428
+ return [x1 - calculateControlOffset(x1 - x2, c), y1];
429
+ case Position.Right:
430
+ return [x1 + calculateControlOffset(x2 - x1, c), y1];
431
+ case Position.Top:
432
+ return [x1, y1 - calculateControlOffset(y1 - y2, c)];
433
+ case Position.Bottom:
434
+ return [x1, y1 + calculateControlOffset(y2 - y1, c)];
435
+ }
436
+ }
437
+ function getBezierPath({ sourceX, sourceY, sourcePosition = Position.Bottom, targetX, targetY, targetPosition = Position.Top, curvature = 0.25, }) {
438
+ const [sourceControlX, sourceControlY] = getControlWithCurvature({
439
+ pos: sourcePosition,
440
+ x1: sourceX,
441
+ y1: sourceY,
442
+ x2: targetX,
443
+ y2: targetY,
444
+ c: curvature,
445
+ });
446
+ const [targetControlX, targetControlY] = getControlWithCurvature({
447
+ pos: targetPosition,
448
+ x1: targetX,
449
+ y1: targetY,
450
+ x2: sourceX,
451
+ y2: sourceY,
452
+ c: curvature,
453
+ });
454
+ const [labelX, labelY, offsetX, offsetY] = getBezierEdgeCenter({
455
+ sourceX,
456
+ sourceY,
457
+ targetX,
458
+ targetY,
459
+ sourceControlX,
460
+ sourceControlY,
461
+ targetControlX,
462
+ targetControlY,
463
+ });
464
+ return [
465
+ `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
466
+ labelX,
467
+ labelY,
468
+ offsetX,
469
+ offsetY,
470
+ ];
471
+ }
472
+
473
+ // this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
474
+ function getEdgeCenter({ sourceX, sourceY, targetX, targetY, }) {
475
+ const xOffset = Math.abs(targetX - sourceX) / 2;
476
+ const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
477
+ const yOffset = Math.abs(targetY - sourceY) / 2;
478
+ const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
479
+ return [centerX, centerY, xOffset, yOffset];
480
+ }
481
+ const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
482
+ function groupEdgesByZLevel(edges, nodes, elevateEdgesOnSelect = false) {
483
+ let maxLevel = -1;
484
+ const levelLookup = edges.reduce((tree, edge) => {
485
+ const hasZIndex = isNumeric(edge.zIndex);
486
+ let z = hasZIndex ? edge.zIndex : 0;
487
+ if (elevateEdgesOnSelect) {
488
+ z = hasZIndex
489
+ ? edge.zIndex
490
+ : Math.max(nodes.find((n) => n.id === edge.source)?.[internalsSymbol]?.z || 0, nodes.find((n) => n.id === edge.target)?.[internalsSymbol]?.z || 0);
491
+ }
492
+ if (tree[z]) {
493
+ tree[z].push(edge);
494
+ }
495
+ else {
496
+ tree[z] = [edge];
497
+ }
498
+ maxLevel = z > maxLevel ? z : maxLevel;
499
+ return tree;
500
+ }, {});
501
+ const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
502
+ const level = +key;
503
+ return {
504
+ edges,
505
+ level,
506
+ isMaxLevel: level === maxLevel,
507
+ };
508
+ });
509
+ if (edgeTree.length === 0) {
510
+ return defaultEdgeTree;
511
+ }
512
+ return edgeTree;
513
+ }
514
+ function isEdgeVisible({ sourceNode, targetNode, width, height, transform }) {
515
+ const edgeBox = getBoundsOfBoxes(nodeToBox(sourceNode), nodeToBox(targetNode));
516
+ if (edgeBox.x === edgeBox.x2) {
517
+ edgeBox.x2 += 1;
518
+ }
519
+ if (edgeBox.y === edgeBox.y2) {
520
+ edgeBox.y2 += 1;
521
+ }
522
+ const viewRect = {
523
+ x: -transform[0] / transform[2],
524
+ y: -transform[1] / transform[2],
525
+ width: width / transform[2],
526
+ height: height / transform[2],
527
+ };
528
+ return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0;
529
+ }
530
+ const getEdgeId = ({ source, sourceHandle, target, targetHandle }) => `xyflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
531
+ const connectionExists = (edge, edges) => {
532
+ return edges.some((el) => el.source === edge.source &&
533
+ el.target === edge.target &&
534
+ (el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
535
+ (el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle)));
536
+ };
537
+ const addEdgeBase = (edgeParams, edges) => {
538
+ if (!edgeParams.source || !edgeParams.target) {
539
+ devWarn('006', errorMessages['error006']());
540
+ return edges;
541
+ }
542
+ let edge;
543
+ if (isEdgeBase(edgeParams)) {
544
+ edge = { ...edgeParams };
545
+ }
546
+ else {
547
+ edge = {
548
+ ...edgeParams,
549
+ id: getEdgeId(edgeParams),
550
+ };
551
+ }
552
+ if (connectionExists(edge, edges)) {
553
+ return edges;
554
+ }
555
+ return edges.concat(edge);
556
+ };
557
+ const updateEdgeBase = (oldEdge, newConnection, edges, options = { shouldReplaceId: true }) => {
558
+ const { id: oldEdgeId, ...rest } = oldEdge;
559
+ if (!newConnection.source || !newConnection.target) {
560
+ devWarn('006', errorMessages['error006']());
561
+ return edges;
562
+ }
563
+ const foundEdge = edges.find((e) => e.id === oldEdge.id);
564
+ if (!foundEdge) {
565
+ devWarn('007', errorMessages['error007'](oldEdgeId));
566
+ return edges;
567
+ }
568
+ // Remove old edge and create the new edge with parameters of old edge.
569
+ const edge = {
570
+ ...rest,
571
+ id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId,
572
+ source: newConnection.source,
573
+ target: newConnection.target,
574
+ sourceHandle: newConnection.sourceHandle,
575
+ targetHandle: newConnection.targetHandle,
576
+ };
577
+ return edges.filter((e) => e.id !== oldEdgeId).concat(edge);
578
+ };
579
+
580
+ function getStraightPath({ sourceX, sourceY, targetX, targetY, }) {
581
+ const [labelX, labelY, offsetX, offsetY] = getEdgeCenter({
582
+ sourceX,
583
+ sourceY,
584
+ targetX,
585
+ targetY,
586
+ });
587
+ return [`M ${sourceX},${sourceY}L ${targetX},${targetY}`, labelX, labelY, offsetX, offsetY];
588
+ }
589
+
590
+ const handleDirections = {
591
+ [Position.Left]: { x: -1, y: 0 },
592
+ [Position.Right]: { x: 1, y: 0 },
593
+ [Position.Top]: { x: 0, y: -1 },
594
+ [Position.Bottom]: { x: 0, y: 1 },
595
+ };
596
+ const getDirection = ({ source, sourcePosition = Position.Bottom, target, }) => {
597
+ if (sourcePosition === Position.Left || sourcePosition === Position.Right) {
598
+ return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 };
599
+ }
600
+ return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 };
601
+ };
602
+ const distance = (a, b) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
603
+ // ith this function we try to mimic a orthogonal edge routing behaviour
604
+ // 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
605
+ function getPoints({ source, sourcePosition = Position.Bottom, target, targetPosition = Position.Top, center, offset, }) {
606
+ const sourceDir = handleDirections[sourcePosition];
607
+ const targetDir = handleDirections[targetPosition];
608
+ const sourceGapped = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset };
609
+ const targetGapped = { x: target.x + targetDir.x * offset, y: target.y + targetDir.y * offset };
610
+ const dir = getDirection({
611
+ source: sourceGapped,
612
+ sourcePosition,
613
+ target: targetGapped,
614
+ });
615
+ const dirAccessor = dir.x !== 0 ? 'x' : 'y';
616
+ const currDir = dir[dirAccessor];
617
+ let points = [];
618
+ let centerX, centerY;
619
+ const [defaultCenterX, defaultCenterY, defaultOffsetX, defaultOffsetY] = getEdgeCenter({
620
+ sourceX: source.x,
621
+ sourceY: source.y,
622
+ targetX: target.x,
623
+ targetY: target.y,
624
+ });
625
+ // opposite handle positions, default case
626
+ if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
627
+ centerX = center.x || defaultCenterX;
628
+ centerY = center.y || defaultCenterY;
629
+ // --->
630
+ // |
631
+ // >---
632
+ const verticalSplit = [
633
+ { x: centerX, y: sourceGapped.y },
634
+ { x: centerX, y: targetGapped.y },
635
+ ];
636
+ // |
637
+ // ---
638
+ // |
639
+ const horizontalSplit = [
640
+ { x: sourceGapped.x, y: centerY },
641
+ { x: targetGapped.x, y: centerY },
642
+ ];
643
+ if (sourceDir[dirAccessor] === currDir) {
644
+ points = dirAccessor === 'x' ? verticalSplit : horizontalSplit;
645
+ }
646
+ else {
647
+ points = dirAccessor === 'x' ? horizontalSplit : verticalSplit;
648
+ }
649
+ }
650
+ else {
651
+ // sourceTarget means we take x from source and y from target, targetSource is the opposite
652
+ const sourceTarget = [{ x: sourceGapped.x, y: targetGapped.y }];
653
+ const targetSource = [{ x: targetGapped.x, y: sourceGapped.y }];
654
+ // this handles edges with same handle positions
655
+ if (dirAccessor === 'x') {
656
+ points = sourceDir.x === currDir ? targetSource : sourceTarget;
657
+ }
658
+ else {
659
+ points = sourceDir.y === currDir ? sourceTarget : targetSource;
660
+ }
661
+ // these are conditions for handling mixed handle positions like Right -> Bottom for example
662
+ if (sourcePosition !== targetPosition) {
663
+ const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x';
664
+ const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite];
665
+ const sourceGtTargetOppo = sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite];
666
+ const sourceLtTargetOppo = sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite];
667
+ const flipSourceTarget = (sourceDir[dirAccessor] === 1 && ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) ||
668
+ (sourceDir[dirAccessor] !== 1 && ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo)));
669
+ if (flipSourceTarget) {
670
+ points = dirAccessor === 'x' ? sourceTarget : targetSource;
671
+ }
672
+ }
673
+ centerX = points[0].x;
674
+ centerY = points[0].y;
675
+ }
676
+ const pathPoints = [source, sourceGapped, ...points, targetGapped, target];
677
+ return [pathPoints, centerX, centerY, defaultOffsetX, defaultOffsetY];
678
+ }
679
+ function getBend(a, b, c, size) {
680
+ const bendSize = Math.min(distance(a, b) / 2, distance(b, c) / 2, size);
681
+ const { x, y } = b;
682
+ // no bend
683
+ if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {
684
+ return `L${x} ${y}`;
685
+ }
686
+ // first segment is horizontal
687
+ if (a.y === y) {
688
+ const xDir = a.x < c.x ? -1 : 1;
689
+ const yDir = a.y < c.y ? 1 : -1;
690
+ return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`;
691
+ }
692
+ const xDir = a.x < c.x ? 1 : -1;
693
+ const yDir = a.y < c.y ? -1 : 1;
694
+ return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
695
+ }
696
+ function getSmoothStepPath({ sourceX, sourceY, sourcePosition = Position.Bottom, targetX, targetY, targetPosition = Position.Top, borderRadius = 5, centerX, centerY, offset = 20, }) {
697
+ const [points, labelX, labelY, offsetX, offsetY] = getPoints({
698
+ source: { x: sourceX, y: sourceY },
699
+ sourcePosition,
700
+ target: { x: targetX, y: targetY },
701
+ targetPosition,
702
+ center: { x: centerX, y: centerY },
703
+ offset,
704
+ });
705
+ const path = points.reduce((res, p, i) => {
706
+ let segment = '';
707
+ if (i > 0 && i < points.length - 1) {
708
+ segment = getBend(points[i - 1], p, points[i + 1], borderRadius);
709
+ }
710
+ else {
711
+ segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`;
712
+ }
713
+ res += segment;
714
+ return res;
715
+ }, '');
716
+ return [path, labelX, labelY, offsetX, offsetY];
717
+ }
718
+
719
+ function getEdgePosition(params) {
720
+ const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getHandleDataByNode(params.sourceNode);
721
+ const [targetNodeRect, targetHandleBounds, targetIsValid] = getHandleDataByNode(params.targetNode);
722
+ if (!sourceIsValid || !targetIsValid) {
723
+ return null;
724
+ }
725
+ // when connection type is loose we can define all handles as sources and connect source -> source
726
+ const targetNodeHandles = params.connectionMode === ConnectionMode.Strict
727
+ ? targetHandleBounds.target
728
+ : (targetHandleBounds.target ?? []).concat(targetHandleBounds.source ?? []);
729
+ const sourceHandle = getHandle(sourceHandleBounds.source, params.sourceHandle);
730
+ const targetHandle = getHandle(targetNodeHandles, params.targetHandle);
731
+ const sourcePosition = sourceHandle?.position || Position.Bottom;
732
+ const targetPosition = targetHandle?.position || Position.Top;
733
+ if (!sourceHandle || !targetHandle) {
734
+ params.onError?.('008', errorMessages['error008'](!sourceHandle ? 'source' : 'target', {
735
+ id: params.id,
736
+ sourceHandle: params.sourceHandle,
737
+ targetHandle: params.targetHandle,
738
+ }));
739
+ return null;
740
+ }
741
+ const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
742
+ const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
743
+ return {
744
+ sourceX,
745
+ sourceY,
746
+ targetX,
747
+ targetY,
748
+ sourcePosition,
749
+ targetPosition,
750
+ };
751
+ }
752
+ function getHandleDataByNode(node) {
753
+ const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
754
+ const isValid = handleBounds &&
755
+ node?.width &&
756
+ node?.height &&
757
+ typeof node?.positionAbsolute?.x !== 'undefined' &&
758
+ typeof node?.positionAbsolute?.y !== 'undefined';
759
+ return [
760
+ {
761
+ x: node?.positionAbsolute?.x || 0,
762
+ y: node?.positionAbsolute?.y || 0,
763
+ width: node?.width || 0,
764
+ height: node?.height || 0,
765
+ },
766
+ handleBounds,
767
+ !!isValid,
768
+ ];
769
+ }
770
+ function getHandlePosition(position, nodeRect, handle = null) {
771
+ const x = (handle?.x || 0) + nodeRect.x;
772
+ const y = (handle?.y || 0) + nodeRect.y;
773
+ const width = handle?.width || nodeRect.width;
774
+ const height = handle?.height || nodeRect.height;
775
+ switch (position) {
776
+ case Position.Top:
777
+ return {
778
+ x: x + width / 2,
779
+ y,
780
+ };
781
+ case Position.Right:
782
+ return {
783
+ x: x + width,
784
+ y: y + height / 2,
785
+ };
786
+ case Position.Bottom:
787
+ return {
788
+ x: x + width / 2,
789
+ y: y + height,
790
+ };
791
+ case Position.Left:
792
+ return {
793
+ x,
794
+ y: y + height / 2,
795
+ };
796
+ }
797
+ }
798
+ function getHandle(bounds, handleId) {
799
+ if (!bounds) {
800
+ return null;
801
+ }
802
+ if (bounds.length === 1 || !handleId) {
803
+ return bounds[0];
804
+ }
805
+ else if (handleId) {
806
+ return bounds.find((d) => d.id === handleId) || null;
807
+ }
808
+ return null;
809
+ }
810
+
811
+ function getMarkerId(marker, id) {
812
+ if (!marker) {
813
+ return '';
814
+ }
815
+ if (typeof marker === 'string') {
816
+ return marker;
817
+ }
818
+ const idPrefix = id ? `${id}__` : '';
819
+ return `${idPrefix}${Object.keys(marker)
820
+ .sort()
821
+ .map((key) => `${key}=${marker[key]}`)
822
+ .join('&')}`;
823
+ }
824
+ function createMarkerIds(edges, { id, defaultColor }) {
825
+ const ids = [];
826
+ return edges
827
+ .reduce((markers, edge) => {
828
+ [edge.markerStart, edge.markerEnd].forEach((marker) => {
829
+ if (marker && typeof marker === 'object') {
830
+ const markerId = getMarkerId(marker, id);
831
+ if (!ids.includes(markerId)) {
832
+ markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
833
+ ids.push(markerId);
834
+ }
835
+ }
836
+ });
837
+ return markers;
838
+ }, [])
839
+ .sort((a, b) => a.id.localeCompare(b.id));
840
+ }
841
+
842
+ function updateAbsolutePositions(nodes, nodeOrigin = [0, 0], parentNodes) {
843
+ return nodes.map((node) => {
844
+ if (node.parentNode && !nodes.find((n) => n.id === node.parentNode)) {
845
+ throw new Error(`Parent node ${node.parentNode} not found`);
846
+ }
847
+ if (node.parentNode || parentNodes?.[node.id]) {
848
+ const parentNode = node.parentNode ? nodes.find((n) => n.id === node.parentNode) : null;
849
+ const { x, y, z } = calculateXYZPosition(node, nodes, {
850
+ ...node.position,
851
+ z: node[internalsSymbol]?.z ?? 0,
852
+ }, parentNode?.origin || nodeOrigin);
853
+ node.positionAbsolute = {
854
+ x,
855
+ y,
856
+ };
857
+ node[internalsSymbol].z = z;
858
+ if (parentNodes?.[node.id]) {
859
+ node[internalsSymbol].isParent = true;
860
+ }
861
+ }
862
+ return node;
863
+ });
864
+ }
865
+ function updateNodes(nodes, storeNodes, options = {
866
+ nodeOrigin: [0, 0],
867
+ elevateNodesOnSelect: true,
868
+ defaults: {},
869
+ }) {
870
+ const parentNodes = {};
871
+ const selectedNodeZ = options?.elevateNodesOnSelect ? 1000 : 0;
872
+ const nextNodes = nodes.map((n) => {
873
+ const currentStoreNode = storeNodes.find((storeNode) => n.id === storeNode.id);
874
+ const node = {
875
+ ...options.defaults,
876
+ ...n,
877
+ positionAbsolute: n.position,
878
+ width: n.width || currentStoreNode?.width,
879
+ height: n.height || currentStoreNode?.height,
880
+ };
881
+ const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);
882
+ const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol];
883
+ if (node.parentNode) {
884
+ parentNodes[node.parentNode] = true;
885
+ }
886
+ Object.defineProperty(node, internalsSymbol, {
887
+ enumerable: false,
888
+ value: {
889
+ handleBounds: currInternals?.handleBounds,
890
+ z,
891
+ },
892
+ });
893
+ return node;
894
+ });
895
+ const nodesWithPositions = updateAbsolutePositions(nextNodes, options.nodeOrigin, parentNodes);
896
+ return nodesWithPositions;
897
+ }
898
+ function calculateXYZPosition(node, nodes, result, nodeOrigin) {
899
+ if (!node.parentNode) {
900
+ return result;
901
+ }
902
+ const parentNode = nodes.find((n) => n.id === node.parentNode);
903
+ const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
904
+ return calculateXYZPosition(parentNode, nodes, {
905
+ x: (result.x ?? 0) + parentNodePosition.x,
906
+ y: (result.y ?? 0) + parentNodePosition.y,
907
+ z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0,
908
+ }, parentNode.origin || nodeOrigin);
909
+ }
910
+ function updateNodeDimensions(updates, nodes, domNode, nodeOrigin, onUpdate) {
911
+ const viewportNode = domNode?.querySelector('.xyflow__viewport');
912
+ if (!viewportNode) {
913
+ return null;
914
+ }
915
+ const style = window.getComputedStyle(viewportNode);
916
+ const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
917
+ const nextNodes = nodes.map((node) => {
918
+ const update = updates.find((u) => u.id === node.id);
919
+ if (update) {
920
+ const dimensions = getDimensions(update.nodeElement);
921
+ const doUpdate = !!(dimensions.width &&
922
+ dimensions.height &&
923
+ (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate));
924
+ if (doUpdate) {
925
+ onUpdate?.(node.id, dimensions);
926
+ return {
927
+ ...node,
928
+ ...dimensions,
929
+ [internalsSymbol]: {
930
+ ...node[internalsSymbol],
931
+ handleBounds: {
932
+ source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin),
933
+ target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin),
934
+ },
935
+ },
936
+ };
937
+ }
938
+ }
939
+ return node;
940
+ });
941
+ return nextNodes;
942
+ }
943
+ function panBy({ delta, panZoom, transform, translateExtent, width, height, }) {
944
+ if (!panZoom || (!delta.x && !delta.y)) {
945
+ return false;
946
+ }
947
+ const nextViewport = panZoom.setViewportConstrained({
948
+ x: transform[0] + delta.x,
949
+ y: transform[1] + delta.y,
950
+ zoom: transform[2],
951
+ }, [
952
+ [0, 0],
953
+ [width, height],
954
+ ], translateExtent);
955
+ const transformChanged = !!nextViewport &&
956
+ (nextViewport.x !== transform[0] || nextViewport.y !== transform[1] || nextViewport.k !== transform[2]);
957
+ return transformChanged;
958
+ }
959
+
960
+ function wrapSelectionDragFunc(selectionFunc) {
961
+ return (event, _, nodes) => selectionFunc?.(event, nodes);
962
+ }
963
+ function isParentSelected(node, nodes) {
964
+ if (!node.parentNode) {
965
+ return false;
966
+ }
967
+ const parentNode = nodes.find((node) => node.id === node.parentNode);
968
+ if (!parentNode) {
969
+ return false;
970
+ }
971
+ if (parentNode.selected) {
972
+ return true;
973
+ }
974
+ return isParentSelected(parentNode, nodes);
975
+ }
976
+ function hasSelector(target, selector, domNode) {
977
+ let current = target;
978
+ do {
979
+ if (current?.matches(selector))
980
+ return true;
981
+ if (current === domNode)
982
+ return false;
983
+ current = current.parentElement;
984
+ } while (current);
985
+ return false;
986
+ }
987
+ // looks for all selected nodes and created a NodeDragItem for each of them
988
+ function getDragItems(nodes, nodesDraggable, mousePos, nodeId) {
989
+ return nodes
990
+ .filter((n) => (n.selected || n.id === nodeId) &&
991
+ (!n.parentNode || !isParentSelected(n, nodes)) &&
992
+ (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')))
993
+ .map((n) => ({
994
+ id: n.id,
995
+ position: n.position || { x: 0, y: 0 },
996
+ positionAbsolute: n.positionAbsolute || { x: 0, y: 0 },
997
+ distance: {
998
+ x: mousePos.x - (n.positionAbsolute?.x ?? 0),
999
+ y: mousePos.y - (n.positionAbsolute?.y ?? 0),
1000
+ },
1001
+ delta: {
1002
+ x: 0,
1003
+ y: 0,
1004
+ },
1005
+ extent: n.extent,
1006
+ parentNode: n.parentNode,
1007
+ width: n.width,
1008
+ height: n.height,
1009
+ origin: n.origin,
1010
+ }));
1011
+ }
1012
+ // returns two params:
1013
+ // 1. the dragged node (or the first of the list, if we are dragging a node selection)
1014
+ // 2. array of selected nodes (for multi selections)
1015
+ function getEventHandlerParams({ nodeId, dragItems, nodes, }) {
1016
+ const extentedDragItems = dragItems.map((n) => {
1017
+ const node = nodes.find((node) => node.id === n.id);
1018
+ return {
1019
+ ...node,
1020
+ position: n.position,
1021
+ positionAbsolute: n.positionAbsolute,
1022
+ };
1023
+ });
1024
+ return [nodeId ? extentedDragItems.find((n) => n.id === nodeId) : extentedDragItems[0], extentedDragItems];
1025
+ }
1026
+
1027
+ function XYDrag({ domNode, onNodeClick, getStoreItems, onDragStart, onDrag, onDragStop, }) {
1028
+ let lastPos = { x: null, y: null };
1029
+ let autoPanId = 0;
1030
+ let dragItems = [];
1031
+ let autoPanStarted = false;
1032
+ let mousePosition = { x: 0, y: 0 };
1033
+ let dragEvent = null;
1034
+ let containerBounds = null;
1035
+ const d3Selection = select(domNode);
1036
+ // public functions
1037
+ function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }) {
1038
+ function updateNodes({ x, y }) {
1039
+ const { nodes, nodeExtent, snapGrid, snapToGrid, nodeOrigin, onNodeDrag, onSelectionDrag, onError, updateNodePositions, } = getStoreItems();
1040
+ lastPos = { x, y };
1041
+ let hasChange = false;
1042
+ dragItems = dragItems.map((n) => {
1043
+ let nextPosition = { x: x - n.distance.x, y: y - n.distance.y };
1044
+ if (snapToGrid) {
1045
+ nextPosition = snapPosition(nextPosition, snapGrid);
1046
+ }
1047
+ const updatedPos = calcNextPosition(n, nextPosition, nodes, nodeExtent, nodeOrigin, onError);
1048
+ // we want to make sure that we only fire a change event when there is a changes
1049
+ hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
1050
+ n.position = updatedPos.position;
1051
+ n.positionAbsolute = updatedPos.positionAbsolute;
1052
+ return n;
1053
+ });
1054
+ if (!hasChange) {
1055
+ return;
1056
+ }
1057
+ updateNodePositions(dragItems, true, true);
1058
+ const onNodeOrSelectionDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
1059
+ if (dragEvent) {
1060
+ const [currentNode, currentNodes] = getEventHandlerParams({
1061
+ nodeId,
1062
+ dragItems,
1063
+ nodes,
1064
+ });
1065
+ onDrag?.(dragEvent, dragItems, currentNode, currentNodes);
1066
+ onNodeOrSelectionDrag?.(dragEvent, currentNode, currentNodes);
1067
+ }
1068
+ }
1069
+ function autoPan() {
1070
+ if (!containerBounds) {
1071
+ return;
1072
+ }
1073
+ const [xMovement, yMovement] = calcAutoPan(mousePosition, containerBounds);
1074
+ if (xMovement !== 0 || yMovement !== 0) {
1075
+ const { transform, panBy } = getStoreItems();
1076
+ lastPos.x = (lastPos.x ?? 0) - xMovement / transform[2];
1077
+ lastPos.y = (lastPos.y ?? 0) - yMovement / transform[2];
1078
+ if (panBy({ x: xMovement, y: yMovement })) {
1079
+ updateNodes(lastPos);
1080
+ }
1081
+ }
1082
+ autoPanId = requestAnimationFrame(autoPan);
1083
+ }
1084
+ const d3DragInstance = drag()
1085
+ .on('start', (event) => {
1086
+ const { nodes, multiSelectionActive, domNode, nodesDraggable, transform, snapGrid, snapToGrid, selectNodesOnDrag, onNodeDragStart, onSelectionDragStart, unselectNodesAndEdges, } = getStoreItems();
1087
+ if (!selectNodesOnDrag && !multiSelectionActive && nodeId) {
1088
+ if (!nodes.find((n) => n.id === nodeId)?.selected) {
1089
+ // we need to reset selected nodes when selectNodesOnDrag=false
1090
+ unselectNodesAndEdges();
1091
+ }
1092
+ }
1093
+ if (isSelectable && selectNodesOnDrag) {
1094
+ onNodeClick?.();
1095
+ }
1096
+ const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
1097
+ lastPos = pointerPos;
1098
+ dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId);
1099
+ const onNodeOrSelectionDragStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart);
1100
+ if (dragItems) {
1101
+ const [currentNode, currentNodes] = getEventHandlerParams({
1102
+ nodeId,
1103
+ dragItems,
1104
+ nodes,
1105
+ });
1106
+ onDragStart?.(event.sourceEvent, dragItems, currentNode, currentNodes);
1107
+ onNodeOrSelectionDragStart?.(event.sourceEvent, currentNode, currentNodes);
1108
+ }
1109
+ containerBounds = domNode?.getBoundingClientRect() || null;
1110
+ mousePosition = getEventPosition(event.sourceEvent, containerBounds);
1111
+ })
1112
+ .on('drag', (event) => {
1113
+ const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid } = getStoreItems();
1114
+ const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
1115
+ if (!autoPanStarted && autoPanOnNodeDrag) {
1116
+ autoPanStarted = true;
1117
+ autoPan();
1118
+ }
1119
+ // skip events without movement
1120
+ if ((lastPos.x !== pointerPos.xSnapped || lastPos.y !== pointerPos.ySnapped) && dragItems) {
1121
+ dragEvent = event.sourceEvent;
1122
+ mousePosition = getEventPosition(event.sourceEvent, containerBounds);
1123
+ updateNodes(pointerPos);
1124
+ }
1125
+ })
1126
+ .on('end', (event) => {
1127
+ autoPanStarted = false;
1128
+ cancelAnimationFrame(autoPanId);
1129
+ if (dragItems) {
1130
+ const { nodes, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
1131
+ const onNodeOrSelectionDragStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop);
1132
+ updateNodePositions(dragItems, false, false);
1133
+ const [currentNode, currentNodes] = getEventHandlerParams({
1134
+ nodeId,
1135
+ dragItems,
1136
+ nodes,
1137
+ });
1138
+ onDragStop?.(event.sourceEvent, dragItems, currentNode, currentNodes);
1139
+ onNodeOrSelectionDragStop?.(event.sourceEvent, currentNode, currentNodes);
1140
+ }
1141
+ })
1142
+ .filter((event) => {
1143
+ const target = event.target;
1144
+ const isDraggable = !event.button &&
1145
+ (!noDragClassName || !hasSelector(target, `.${noDragClassName}`, domNode)) &&
1146
+ (!handleSelector || hasSelector(target, handleSelector, domNode));
1147
+ return isDraggable;
1148
+ });
1149
+ d3Selection.call(d3DragInstance);
1150
+ }
1151
+ function destroy() {
1152
+ d3Selection.on('.drag', null);
1153
+ }
1154
+ return {
1155
+ update,
1156
+ destroy,
1157
+ };
1158
+ }
1159
+
1160
+ // this functions collects all handles and adds an absolute position
1161
+ // so that we can later find the closest handle to the mouse position
1162
+ function getHandles(node, handleBounds, type, currentHandle) {
1163
+ return (handleBounds[type] || []).reduce((res, h) => {
1164
+ if (`${node.id}-${h.id}-${type}` !== currentHandle) {
1165
+ res.push({
1166
+ id: h.id || null,
1167
+ type,
1168
+ nodeId: node.id,
1169
+ x: (node.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
1170
+ y: (node.positionAbsolute?.y ?? 0) + h.y + h.height / 2,
1171
+ });
1172
+ }
1173
+ return res;
1174
+ }, []);
1175
+ }
1176
+ function getClosestHandle(pos, connectionRadius, handles) {
1177
+ let closestHandles = [];
1178
+ let minDistance = Infinity;
1179
+ handles.forEach((handle) => {
1180
+ const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
1181
+ if (distance <= connectionRadius) {
1182
+ if (distance < minDistance) {
1183
+ closestHandles = [handle];
1184
+ }
1185
+ else if (distance === minDistance) {
1186
+ // when multiple handles are on the same distance we collect all of them
1187
+ closestHandles.push(handle);
1188
+ }
1189
+ minDistance = distance;
1190
+ }
1191
+ });
1192
+ if (!closestHandles.length) {
1193
+ return null;
1194
+ }
1195
+ return closestHandles.length === 1
1196
+ ? closestHandles[0]
1197
+ : // 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
1198
+ closestHandles.find((handle) => handle.type === 'target') || closestHandles[0];
1199
+ }
1200
+ function getHandleLookup({ nodes, nodeId, handleId, handleType }) {
1201
+ return nodes.reduce((res, node) => {
1202
+ if (node[internalsSymbol]) {
1203
+ const { handleBounds } = node[internalsSymbol];
1204
+ let sourceHandles = [];
1205
+ let targetHandles = [];
1206
+ if (handleBounds) {
1207
+ sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`);
1208
+ targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`);
1209
+ }
1210
+ res.push(...sourceHandles, ...targetHandles);
1211
+ }
1212
+ return res;
1213
+ }, []);
1214
+ }
1215
+ function getHandleType(edgeUpdaterType, handleDomNode) {
1216
+ if (edgeUpdaterType) {
1217
+ return edgeUpdaterType;
1218
+ }
1219
+ else if (handleDomNode?.classList.contains('target')) {
1220
+ return 'target';
1221
+ }
1222
+ else if (handleDomNode?.classList.contains('source')) {
1223
+ return 'source';
1224
+ }
1225
+ return null;
1226
+ }
1227
+ function resetRecentHandle(handleDomNode, lib) {
1228
+ handleDomNode?.classList.remove('valid', 'connecting', `${lib}-flow__handle-valid`, `${lib}-flow__handle-connecting`);
1229
+ }
1230
+ function getConnectionStatus(isInsideConnectionRadius, isHandleValid) {
1231
+ let connectionStatus = null;
1232
+ if (isHandleValid) {
1233
+ connectionStatus = 'valid';
1234
+ }
1235
+ else if (isInsideConnectionRadius && !isHandleValid) {
1236
+ connectionStatus = 'invalid';
1237
+ }
1238
+ return connectionStatus;
1239
+ }
1240
+
1241
+ const nullConnection = { source: null, target: null, sourceHandle: null, targetHandle: null };
1242
+ const alwaysValid = () => true;
1243
+ function onPointerDown(event, { connectionMode, connectionRadius, handleId, nodeId, edgeUpdaterType, isTarget, domNode, nodes, lib, autoPanOnConnect, panBy, cancelConnection, onConnectStart, onConnect, onConnectEnd, isValidConnection = alwaysValid, onEdgeUpdateEnd, updateConnection, getTransform, }) {
1244
+ // when xyflow is used inside a shadow root we can't use document
1245
+ const doc = getHostForElement(event.target);
1246
+ let autoPanId = 0;
1247
+ let closestHandle;
1248
+ const { x, y } = getEventPosition(event);
1249
+ const clickedHandle = doc?.elementFromPoint(x, y);
1250
+ const handleType = getHandleType(edgeUpdaterType, clickedHandle);
1251
+ const containerBounds = domNode?.getBoundingClientRect();
1252
+ if (!containerBounds || !handleType) {
1253
+ return;
1254
+ }
1255
+ let prevActiveHandle;
1256
+ let connectionPosition = getEventPosition(event, containerBounds);
1257
+ let autoPanStarted = false;
1258
+ let connection = null;
1259
+ let isValid = false;
1260
+ let handleDomNode = null;
1261
+ const handleLookup = getHandleLookup({
1262
+ nodes,
1263
+ nodeId,
1264
+ handleId,
1265
+ handleType,
1266
+ });
1267
+ // when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
1268
+ function autoPan() {
1269
+ if (!autoPanOnConnect || !containerBounds) {
1270
+ return;
1271
+ }
1272
+ const [x, y] = calcAutoPan(connectionPosition, containerBounds);
1273
+ panBy({ x, y });
1274
+ autoPanId = requestAnimationFrame(autoPan);
1275
+ }
1276
+ updateConnection({
1277
+ connectionPosition,
1278
+ connectionStatus: null,
1279
+ // connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
1280
+ connectionStartHandle: {
1281
+ nodeId,
1282
+ handleId,
1283
+ type: handleType,
1284
+ },
1285
+ connectionEndHandle: null,
1286
+ });
1287
+ onConnectStart?.(event, { nodeId, handleId, handleType });
1288
+ function onPointerMove(event) {
1289
+ const transform = getTransform();
1290
+ connectionPosition = getEventPosition(event, containerBounds);
1291
+ closestHandle = getClosestHandle(pointToRendererPoint(connectionPosition, transform, false, [1, 1]), connectionRadius, handleLookup);
1292
+ if (!autoPanStarted) {
1293
+ autoPan();
1294
+ autoPanStarted = true;
1295
+ }
1296
+ const result = isValidHandle(event, {
1297
+ handle: closestHandle,
1298
+ connectionMode,
1299
+ fromNodeId: nodeId,
1300
+ fromHandleId: handleId,
1301
+ fromType: isTarget ? 'target' : 'source',
1302
+ isValidConnection,
1303
+ doc,
1304
+ lib,
1305
+ });
1306
+ handleDomNode = result.handleDomNode;
1307
+ connection = result.connection;
1308
+ isValid = result.isValid;
1309
+ updateConnection({
1310
+ connectionPosition: closestHandle && isValid
1311
+ ? rendererPointToPoint({
1312
+ x: closestHandle.x,
1313
+ y: closestHandle.y,
1314
+ }, transform)
1315
+ : connectionPosition,
1316
+ connectionStatus: getConnectionStatus(!!closestHandle, isValid),
1317
+ connectionEndHandle: result.endHandle,
1318
+ });
1319
+ if (!closestHandle && !isValid && !handleDomNode) {
1320
+ return resetRecentHandle(prevActiveHandle, lib);
1321
+ }
1322
+ if (connection.source !== connection.target && handleDomNode) {
1323
+ resetRecentHandle(prevActiveHandle, lib);
1324
+ prevActiveHandle = handleDomNode;
1325
+ handleDomNode.classList.add('connecting', `${lib}-flow__handle-connecting`);
1326
+ handleDomNode.classList.toggle('valid', isValid);
1327
+ handleDomNode.classList.toggle(`${lib}-flow__handle-valid`, isValid);
1328
+ }
1329
+ }
1330
+ function onPointerUp(event) {
1331
+ if ((closestHandle || handleDomNode) && connection && isValid) {
1332
+ onConnect?.(connection);
1333
+ }
1334
+ // it's important to get a fresh reference from the store here
1335
+ // in order to get the latest state of onConnectEnd
1336
+ onConnectEnd?.(event);
1337
+ if (edgeUpdaterType) {
1338
+ onEdgeUpdateEnd?.(event);
1339
+ }
1340
+ resetRecentHandle(prevActiveHandle, lib);
1341
+ cancelConnection();
1342
+ cancelAnimationFrame(autoPanId);
1343
+ autoPanStarted = false;
1344
+ isValid = false;
1345
+ connection = null;
1346
+ handleDomNode = null;
1347
+ doc.removeEventListener('mousemove', onPointerMove);
1348
+ doc.removeEventListener('mouseup', onPointerUp);
1349
+ doc.removeEventListener('touchmove', onPointerMove);
1350
+ doc.removeEventListener('touchend', onPointerUp);
1351
+ }
1352
+ doc.addEventListener('mousemove', onPointerMove);
1353
+ doc.addEventListener('mouseup', onPointerUp);
1354
+ doc.addEventListener('touchmove', onPointerMove);
1355
+ doc.addEventListener('touchend', onPointerUp);
1356
+ }
1357
+ // checks if and returns connection in fom of an object { source: 123, target: 312 }
1358
+ function isValidHandle(event, { handle, connectionMode, fromNodeId, fromHandleId, fromType, doc, lib, isValidConnection = alwaysValid, }) {
1359
+ const isTarget = fromType === 'target';
1360
+ const handleDomNode = doc.querySelector(`.${lib}-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`);
1361
+ const { x, y } = getEventPosition(event);
1362
+ const handleBelow = doc.elementFromPoint(x, y);
1363
+ // we always want to prioritize the handle below the mouse cursor over the closest distance handle,
1364
+ // because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
1365
+ const handleToCheck = handleBelow?.classList.contains(`${lib}-flow__handle`) ? handleBelow : handleDomNode;
1366
+ const result = {
1367
+ handleDomNode: handleToCheck,
1368
+ isValid: false,
1369
+ connection: nullConnection,
1370
+ endHandle: null,
1371
+ };
1372
+ if (handleToCheck) {
1373
+ const handleType = getHandleType(undefined, handleToCheck);
1374
+ const handleNodeId = handleToCheck.getAttribute('data-nodeid');
1375
+ const handleId = handleToCheck.getAttribute('data-handleid');
1376
+ const connectable = handleToCheck.classList.contains('connectable');
1377
+ const connectableEnd = handleToCheck.classList.contains('connectableend');
1378
+ const connection = {
1379
+ source: isTarget ? handleNodeId : fromNodeId,
1380
+ sourceHandle: isTarget ? handleId : fromHandleId,
1381
+ target: isTarget ? fromNodeId : handleNodeId,
1382
+ targetHandle: isTarget ? fromHandleId : handleId,
1383
+ };
1384
+ result.connection = connection;
1385
+ const isConnectable = connectable && connectableEnd;
1386
+ // in strict mode we don't allow target to target or source to source connections
1387
+ const isValid = isConnectable &&
1388
+ (connectionMode === ConnectionMode.Strict
1389
+ ? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
1390
+ : handleNodeId !== fromNodeId || handleId !== fromHandleId);
1391
+ if (isValid) {
1392
+ result.endHandle = {
1393
+ nodeId: handleNodeId,
1394
+ handleId,
1395
+ type: handleType,
1396
+ };
1397
+ result.isValid = isValidConnection(connection);
1398
+ }
1399
+ }
1400
+ return result;
1401
+ }
1402
+ const XYHandle = {
1403
+ onPointerDown,
1404
+ isValid: isValidHandle,
1405
+ };
1406
+
1407
+ function XYMinimap({ domNode, panZoom, getTransform, getViewScale }) {
1408
+ const selection = select(domNode);
1409
+ function update({ translateExtent, width, height, zoomStep = 10, pannable = true, zoomable = true, inversePan = false, }) {
1410
+ const zoomHandler = (event) => {
1411
+ const transform = getTransform();
1412
+ if (event.sourceEvent.type !== 'wheel' || !panZoom) {
1413
+ return;
1414
+ }
1415
+ const pinchDelta = -event.sourceEvent.deltaY *
1416
+ (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) *
1417
+ zoomStep;
1418
+ const nextZoom = transform[2] * Math.pow(2, pinchDelta);
1419
+ panZoom.scaleTo(nextZoom);
1420
+ };
1421
+ const panHandler = (event) => {
1422
+ const transform = getTransform();
1423
+ if (event.sourceEvent.type !== 'mousemove' || !panZoom) {
1424
+ return;
1425
+ }
1426
+ // @TODO: how to calculate the correct next position? Math.max(1, transform[2]) is a workaround.
1427
+ const moveScale = getViewScale() * Math.max(1, transform[2]) * (inversePan ? -1 : 1);
1428
+ const position = {
1429
+ x: transform[0] - event.sourceEvent.movementX * moveScale,
1430
+ y: transform[1] - event.sourceEvent.movementY * moveScale,
1431
+ };
1432
+ const extent = [
1433
+ [0, 0],
1434
+ [width, height],
1435
+ ];
1436
+ panZoom.setViewportConstrained({
1437
+ x: position.x,
1438
+ y: position.y,
1439
+ zoom: transform[2],
1440
+ }, extent, translateExtent);
1441
+ };
1442
+ const zoomAndPanHandler = zoom()
1443
+ // @ts-ignore
1444
+ .on('zoom', pannable ? panHandler : null)
1445
+ // @ts-ignore
1446
+ .on('zoom.wheel', zoomable ? zoomHandler : null);
1447
+ selection.call(zoomAndPanHandler, {});
1448
+ }
1449
+ function destroy() {
1450
+ selection.on('zoom', null);
1451
+ }
1452
+ return {
1453
+ update,
1454
+ destroy,
1455
+ pointer,
1456
+ };
1457
+ }
1458
+
1459
+ const viewChanged = (prevViewport, eventViewport) => prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k;
1460
+ const transformToViewport = (transform) => ({
1461
+ x: transform.x,
1462
+ y: transform.y,
1463
+ zoom: transform.k,
1464
+ });
1465
+ const viewportToTransform = ({ x, y, zoom }) => zoomIdentity.translate(x, y).scale(zoom);
1466
+ const isWrappedWithClass = (event, className) => event.target.closest(`.${className}`);
1467
+ const isRightClickPan = (panOnDrag, usedButton) => usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2);
1468
+ const getD3Transition = (selection, duration = 0) => typeof duration === 'number' && duration > 0 ? selection.transition().duration(duration) : selection;
1469
+
1470
+ function createPanOnScrollHandler({ noWheelClassName, d3Selection, d3Zoom, panOnScrollMode, panOnScrollSpeed, zoomOnPinch, }) {
1471
+ return (event) => {
1472
+ if (isWrappedWithClass(event, noWheelClassName)) {
1473
+ return false;
1474
+ }
1475
+ event.preventDefault();
1476
+ event.stopImmediatePropagation();
1477
+ const currentZoom = d3Selection.property('__zoom').k || 1;
1478
+ if (event.ctrlKey && zoomOnPinch) {
1479
+ const point = pointer(event);
1480
+ // taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
1481
+ const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10;
1482
+ const zoom = currentZoom * Math.pow(2, pinchDelta);
1483
+ d3Zoom.scaleTo(d3Selection, zoom, point);
1484
+ return;
1485
+ }
1486
+ // increase scroll speed in firefox
1487
+ // firefox: deltaMode === 1; chrome: deltaMode === 0
1488
+ const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
1489
+ const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
1490
+ const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
1491
+ d3Zoom.translateBy(d3Selection, -(deltaX / currentZoom) * panOnScrollSpeed, -(deltaY / currentZoom) * panOnScrollSpeed);
1492
+ };
1493
+ }
1494
+ function createZoomOnScrollHandler({ noWheelClassName, preventScrolling, d3ZoomHandler }) {
1495
+ return function (event, d) {
1496
+ if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
1497
+ return null;
1498
+ }
1499
+ event.preventDefault();
1500
+ d3ZoomHandler.call(this, event, d);
1501
+ };
1502
+ }
1503
+ function createPanZoomStartHandler({ zoomPanValues, onDraggingChange, onPanZoomStart }) {
1504
+ return (event) => {
1505
+ // we need to remember it here, because it's always 0 in the "zoom" event
1506
+ zoomPanValues.mouseButton = event.sourceEvent?.button || 0;
1507
+ zoomPanValues.isZoomingOrPanning = true;
1508
+ if (event.sourceEvent?.type === 'mousedown') {
1509
+ onDraggingChange(true);
1510
+ }
1511
+ if (onPanZoomStart) {
1512
+ const viewport = transformToViewport(event.transform);
1513
+ zoomPanValues.prevViewport = viewport;
1514
+ onPanZoomStart?.(event.sourceEvent, viewport);
1515
+ }
1516
+ };
1517
+ }
1518
+ function createPanZoomHandler({ zoomPanValues, panOnDrag, onPaneContextMenu, onTransformChange, onPanZoom, }) {
1519
+ return (event) => {
1520
+ zoomPanValues.usedRightMouseButton = !!(onPaneContextMenu && isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0));
1521
+ onTransformChange([event.transform.x, event.transform.y, event.transform.k]);
1522
+ if (onPanZoom) {
1523
+ onPanZoom?.(event.sourceEvent, transformToViewport(event.transform));
1524
+ }
1525
+ };
1526
+ }
1527
+ function createPanZoomEndHandler({ zoomPanValues, panOnDrag, panOnScroll, onDraggingChange, onPanZoomEnd, onPaneContextMenu, }) {
1528
+ return (event) => {
1529
+ zoomPanValues.isZoomingOrPanning = false;
1530
+ if (onPaneContextMenu &&
1531
+ isRightClickPan(panOnDrag, zoomPanValues.mouseButton ?? 0) &&
1532
+ !zoomPanValues.usedRightMouseButton &&
1533
+ event.sourceEvent) {
1534
+ onPaneContextMenu(event.sourceEvent);
1535
+ }
1536
+ zoomPanValues.usedRightMouseButton = false;
1537
+ onDraggingChange(false);
1538
+ if (onPanZoomEnd && viewChanged(zoomPanValues.prevViewport, event.transform)) {
1539
+ const viewport = transformToViewport(event.transform);
1540
+ zoomPanValues.prevViewport = viewport;
1541
+ clearTimeout(zoomPanValues.timerId);
1542
+ zoomPanValues.timerId = setTimeout(() => {
1543
+ onPanZoomEnd?.(event.sourceEvent, viewport);
1544
+ },
1545
+ // we need a setTimeout for panOnScroll to supress multiple end events fired during scroll
1546
+ panOnScroll ? 150 : 0);
1547
+ }
1548
+ };
1549
+ }
1550
+
1551
+ function createFilter({ zoomActivationKeyPressed, zoomOnScroll, zoomOnPinch, panOnDrag, panOnScroll, zoomOnDoubleClick, userSelectionActive, noWheelClassName, noPanClassName, lib, }) {
1552
+ return (event) => {
1553
+ const zoomScroll = zoomActivationKeyPressed || zoomOnScroll;
1554
+ const pinchZoom = zoomOnPinch && event.ctrlKey;
1555
+ if (event.button === 1 &&
1556
+ event.type === 'mousedown' &&
1557
+ (isWrappedWithClass(event, `${lib}-flow__node`) || isWrappedWithClass(event, `${lib}-flow__edge`))) {
1558
+ return true;
1559
+ }
1560
+ // if all interactions are disabled, we prevent all zoom events
1561
+ if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) {
1562
+ return false;
1563
+ }
1564
+ // during a selection we prevent all other interactions
1565
+ if (userSelectionActive) {
1566
+ return false;
1567
+ }
1568
+ // if zoom on double click is disabled, we prevent the double click event
1569
+ if (!zoomOnDoubleClick && event.type === 'dblclick') {
1570
+ return false;
1571
+ }
1572
+ // if the target element is inside an element with the nowheel class, we prevent zooming
1573
+ if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
1574
+ return false;
1575
+ }
1576
+ // if the target element is inside an element with the nopan class, we prevent panning
1577
+ if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') {
1578
+ return false;
1579
+ }
1580
+ if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
1581
+ return false;
1582
+ }
1583
+ // when there is no scroll handling enabled, we prevent all wheel events
1584
+ if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
1585
+ return false;
1586
+ }
1587
+ // if the pane is not movable, we prevent dragging it with mousestart or touchstart
1588
+ if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) {
1589
+ return false;
1590
+ }
1591
+ // if the pane is only movable using allowed clicks
1592
+ if (Array.isArray(panOnDrag) &&
1593
+ !panOnDrag.includes(event.button) &&
1594
+ (event.type === 'mousedown' || event.type === 'touchstart')) {
1595
+ return false;
1596
+ }
1597
+ // We only allow right clicks if pan on drag is set to right click
1598
+ const buttonAllowed = (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || !event.button || event.button <= 1;
1599
+ // default filter for d3-zoom
1600
+ return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed;
1601
+ };
1602
+ }
1603
+
1604
+ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPanZoom, onPanZoomStart, onPanZoomEnd, onTransformChange, onDraggingChange, }) {
1605
+ const zoomPanValues = {
1606
+ isZoomingOrPanning: false,
1607
+ usedRightMouseButton: false,
1608
+ prevViewport: { x: 0, y: 0, zoom: 0 },
1609
+ mouseButton: 0,
1610
+ timerId: undefined,
1611
+ };
1612
+ const bbox = domNode.getBoundingClientRect();
1613
+ const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
1614
+ const d3Selection = select(domNode).call(d3ZoomInstance);
1615
+ setViewportConstrained({
1616
+ x: viewport.x,
1617
+ y: viewport.y,
1618
+ zoom: clamp(viewport.zoom, minZoom, maxZoom),
1619
+ }, [
1620
+ [0, 0],
1621
+ [bbox.width, bbox.height],
1622
+ ], translateExtent);
1623
+ const d3ZoomHandler = d3Selection.on('wheel.zoom');
1624
+ function setTransform(transform, options) {
1625
+ if (d3Selection) {
1626
+ d3ZoomInstance?.transform(getD3Transition(d3Selection, options?.duration), transform);
1627
+ }
1628
+ }
1629
+ // public functions
1630
+ function update({ noWheelClassName, noPanClassName, onPaneContextMenu, userSelectionActive, panOnScroll, panOnDrag, panOnScrollMode, panOnScrollSpeed, preventScrolling, zoomOnPinch, zoomOnScroll, zoomOnDoubleClick, zoomActivationKeyPressed, lib, }) {
1631
+ if (userSelectionActive && !zoomPanValues.isZoomingOrPanning) {
1632
+ destroy();
1633
+ }
1634
+ const isPanOnScroll = panOnScroll && !zoomActivationKeyPressed && !userSelectionActive;
1635
+ const wheelHandler = isPanOnScroll
1636
+ ? createPanOnScrollHandler({
1637
+ noWheelClassName,
1638
+ d3Selection,
1639
+ d3Zoom: d3ZoomInstance,
1640
+ panOnScrollMode,
1641
+ panOnScrollSpeed,
1642
+ zoomOnPinch,
1643
+ })
1644
+ : createZoomOnScrollHandler({
1645
+ noWheelClassName,
1646
+ preventScrolling,
1647
+ d3ZoomHandler,
1648
+ });
1649
+ d3Selection.on('wheel.zoom', wheelHandler, { passive: false });
1650
+ if (!userSelectionActive) {
1651
+ // pan zoom start
1652
+ const startHandler = createPanZoomStartHandler({
1653
+ zoomPanValues,
1654
+ onDraggingChange,
1655
+ onPanZoomStart,
1656
+ });
1657
+ d3ZoomInstance.on('start', startHandler);
1658
+ // pan zoom
1659
+ const panZoomHandler = createPanZoomHandler({
1660
+ zoomPanValues,
1661
+ panOnDrag,
1662
+ onPaneContextMenu: !!onPaneContextMenu,
1663
+ onPanZoom,
1664
+ onTransformChange,
1665
+ });
1666
+ d3ZoomInstance.on('zoom', panZoomHandler);
1667
+ // pan zoom end
1668
+ const panZoomEndHandler = createPanZoomEndHandler({
1669
+ zoomPanValues,
1670
+ panOnDrag,
1671
+ panOnScroll,
1672
+ onPaneContextMenu,
1673
+ onPanZoomEnd,
1674
+ onDraggingChange,
1675
+ });
1676
+ d3ZoomInstance.on('end', panZoomEndHandler);
1677
+ }
1678
+ const filter = createFilter({
1679
+ zoomActivationKeyPressed,
1680
+ panOnDrag,
1681
+ zoomOnScroll,
1682
+ panOnScroll,
1683
+ zoomOnDoubleClick,
1684
+ zoomOnPinch,
1685
+ userSelectionActive,
1686
+ noPanClassName,
1687
+ noWheelClassName,
1688
+ lib,
1689
+ });
1690
+ d3ZoomInstance.filter(filter);
1691
+ }
1692
+ function destroy() {
1693
+ d3ZoomInstance.on('zoom', null);
1694
+ }
1695
+ function setViewportConstrained(viewport, extent, translateExtent) {
1696
+ const nextTransform = viewportToTransform(viewport);
1697
+ const contrainedTransform = d3ZoomInstance?.constrain()(nextTransform, extent, translateExtent);
1698
+ if (contrainedTransform) {
1699
+ setTransform(contrainedTransform);
1700
+ }
1701
+ return contrainedTransform;
1702
+ }
1703
+ function setViewport(viewport, options) {
1704
+ const nextTransform = viewportToTransform(viewport);
1705
+ setTransform(nextTransform, options);
1706
+ return nextTransform;
1707
+ }
1708
+ function getViewport() {
1709
+ const transform = d3Selection ? zoomTransform(d3Selection.node()) : { x: 0, y: 0, k: 1 };
1710
+ return { x: transform.x, y: transform.y, zoom: transform.k };
1711
+ }
1712
+ function scaleTo(zoom, options) {
1713
+ if (d3Selection) {
1714
+ d3ZoomInstance?.scaleTo(getD3Transition(d3Selection, options?.duration), zoom);
1715
+ }
1716
+ }
1717
+ function scaleBy(factor, options) {
1718
+ if (d3Selection) {
1719
+ d3ZoomInstance?.scaleBy(getD3Transition(d3Selection, options?.duration), factor);
1720
+ }
1721
+ }
1722
+ function setScaleExtent(scaleExtent) {
1723
+ d3ZoomInstance?.scaleExtent(scaleExtent);
1724
+ }
1725
+ function setTranslateExtent(translateExtent) {
1726
+ d3ZoomInstance?.translateExtent(translateExtent);
1727
+ }
1728
+ return {
1729
+ update,
1730
+ destroy,
1731
+ setViewport,
1732
+ setViewportConstrained,
1733
+ getViewport,
1734
+ scaleTo,
1735
+ scaleBy,
1736
+ setScaleExtent,
1737
+ setTranslateExtent,
1738
+ };
1739
+ }
1740
+
1741
+ export { ConnectionLineType, ConnectionMode, MarkerType, PanOnScrollMode, Position, SelectionMode, XYDrag, XYHandle, XYMinimap, XYPanZoom, addEdgeBase, boxToRect, calcAutoPan, calcNextPosition, clamp, clampPosition, createMarkerIds, devWarn, elementSelectionKeys, errorMessages, fitView, getBezierEdgeCenter, getBezierPath, getBoundsOfBoxes, getBoundsOfRects, getConnectedEdgesBase, getDimensions, getEdgeCenter, getEdgePosition, getElementsToRemove, getEventPosition, getHandleBounds, getHostForElement, getIncomersBase, getMarkerId, getNodePositionWithOrigin, getNodesInside, getOutgoersBase, getOverlappingArea, getPointerPosition, getPositionWithOrigin, getRectOfNodes, getSmoothStepPath, getStraightPath, getTransformForBounds, groupEdgesByZLevel, infiniteExtent, internalsSymbol, isEdgeBase, isEdgeVisible, isInputDOMNode, isMouseEvent, isNodeBase, isNumeric, isRectObject, nodeToBox, nodeToRect, panBy, pointToRendererPoint, rectToBox, rendererPointToPoint, snapPosition, updateAbsolutePositions, updateEdgeBase, updateNodeDimensions, updateNodes };