@xyflow/system 0.0.11 → 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 (62) hide show
  1. package/dist/esm/index.js +2176 -0
  2. package/dist/esm/index.mjs +322 -137
  3. package/dist/esm/types/edges.d.ts +5 -2
  4. package/dist/esm/types/edges.d.ts.map +1 -1
  5. package/dist/esm/types/general.d.ts +10 -2
  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/types/utils.d.ts.map +1 -1
  10. package/dist/esm/utils/connections.d.ts +12 -0
  11. package/dist/esm/utils/connections.d.ts.map +1 -0
  12. package/dist/esm/utils/edges/bezier-edge.d.ts +23 -0
  13. package/dist/esm/utils/edges/bezier-edge.d.ts.map +1 -1
  14. package/dist/esm/utils/edges/general.d.ts +23 -5
  15. package/dist/esm/utils/edges/general.d.ts.map +1 -1
  16. package/dist/esm/utils/edges/positions.d.ts.map +1 -1
  17. package/dist/esm/utils/edges/smoothstep-edge.d.ts +22 -0
  18. package/dist/esm/utils/edges/smoothstep-edge.d.ts.map +1 -1
  19. package/dist/esm/utils/edges/straight-edge.d.ts +20 -0
  20. package/dist/esm/utils/edges/straight-edge.d.ts.map +1 -1
  21. package/dist/esm/utils/general.d.ts +19 -2
  22. package/dist/esm/utils/general.d.ts.map +1 -1
  23. package/dist/esm/utils/graph.d.ts +63 -8
  24. package/dist/esm/utils/graph.d.ts.map +1 -1
  25. package/dist/esm/utils/index.d.ts +2 -0
  26. package/dist/esm/utils/index.d.ts.map +1 -1
  27. package/dist/esm/utils/store.d.ts +4 -3
  28. package/dist/esm/utils/store.d.ts.map +1 -1
  29. package/dist/esm/xyhandle/XYHandle.d.ts +1 -1
  30. package/dist/esm/xyhandle/XYHandle.d.ts.map +1 -1
  31. package/dist/esm/xyminimap/index.d.ts.map +1 -1
  32. package/dist/umd/index.js +1 -1
  33. package/dist/umd/types/edges.d.ts +5 -2
  34. package/dist/umd/types/edges.d.ts.map +1 -1
  35. package/dist/umd/types/general.d.ts +10 -2
  36. package/dist/umd/types/general.d.ts.map +1 -1
  37. package/dist/umd/types/nodes.d.ts +7 -2
  38. package/dist/umd/types/nodes.d.ts.map +1 -1
  39. package/dist/umd/types/utils.d.ts.map +1 -1
  40. package/dist/umd/utils/connections.d.ts +12 -0
  41. package/dist/umd/utils/connections.d.ts.map +1 -0
  42. package/dist/umd/utils/edges/bezier-edge.d.ts +23 -0
  43. package/dist/umd/utils/edges/bezier-edge.d.ts.map +1 -1
  44. package/dist/umd/utils/edges/general.d.ts +23 -5
  45. package/dist/umd/utils/edges/general.d.ts.map +1 -1
  46. package/dist/umd/utils/edges/positions.d.ts.map +1 -1
  47. package/dist/umd/utils/edges/smoothstep-edge.d.ts +22 -0
  48. package/dist/umd/utils/edges/smoothstep-edge.d.ts.map +1 -1
  49. package/dist/umd/utils/edges/straight-edge.d.ts +20 -0
  50. package/dist/umd/utils/edges/straight-edge.d.ts.map +1 -1
  51. package/dist/umd/utils/general.d.ts +19 -2
  52. package/dist/umd/utils/general.d.ts.map +1 -1
  53. package/dist/umd/utils/graph.d.ts +63 -8
  54. package/dist/umd/utils/graph.d.ts.map +1 -1
  55. package/dist/umd/utils/index.d.ts +2 -0
  56. package/dist/umd/utils/index.d.ts.map +1 -1
  57. package/dist/umd/utils/store.d.ts +4 -3
  58. package/dist/umd/utils/store.d.ts.map +1 -1
  59. package/dist/umd/xyhandle/XYHandle.d.ts +1 -1
  60. package/dist/umd/xyhandle/XYHandle.d.ts.map +1 -1
  61. package/dist/umd/xyminimap/index.d.ts.map +1 -1
  62. package/package.json +5 -5
@@ -1,4 +1,3 @@
1
- "use client"
2
1
  import { drag } from 'd3-drag';
3
2
  import { select, pointer } from 'd3-selection';
4
3
  import { zoom, zoomIdentity, zoomTransform } from 'd3-zoom';
@@ -64,9 +63,71 @@ var Position;
64
63
  Position["Bottom"] = "bottom";
65
64
  })(Position || (Position = {}));
66
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
+
67
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
+ */
68
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
+ */
69
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
+ */
70
131
  const getOutgoersBase = (node, nodes, edges) => {
71
132
  if (!node.id) {
72
133
  return [];
@@ -79,6 +140,14 @@ const getOutgoersBase = (node, nodes, edges) => {
79
140
  });
80
141
  return nodes.filter((n) => outgoerIds.has(n.id));
81
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
+ */
82
151
  const getIncomersBase = (node, nodes, edges) => {
83
152
  if (!node.id) {
84
153
  return [];
@@ -118,6 +187,14 @@ const getNodePositionWithOrigin = (node, nodeOrigin = [0, 0]) => {
118
187
  : position,
119
188
  };
120
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
+ */
121
198
  const getNodesBounds = (nodes, nodeOrigin = [0, 0]) => {
122
199
  if (nodes.length === 0) {
123
200
  return { x: 0, y: 0, width: 0, height: 0 };
@@ -160,6 +237,12 @@ excludeNonSelectableNodes = false, nodeOrigin = [0, 0]) => {
160
237
  }, []);
161
238
  return visibleNodes;
162
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
+ */
163
246
  const getConnectedEdgesBase = (nodes, edges) => {
164
247
  const nodeIds = new Set();
165
248
  nodes.forEach((node) => {
@@ -238,33 +321,53 @@ function calcNextPosition(node, nextPosition, nodes, nodeExtent, nodeOrigin = [0
238
321
  positionAbsolute,
239
322
  };
240
323
  }
241
- // helper function to get arrays of nodes and edges that can be deleted
242
- // you can pass in a list of nodes and edges that should be deleted
243
- // and the function only returns elements that are deletable and also handles connected nodes and child nodes
244
- function getElementsToRemove({ nodesToRemove, edgesToRemove, nodes, edges, }) {
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, }) {
245
335
  const nodeIds = nodesToRemove.map((node) => node.id);
246
- const edgeIds = edgesToRemove.map((edge) => edge.id);
247
- const matchingNodes = nodes.reduce((res, node) => {
248
- const parentHit = !nodeIds.includes(node.id) && node.parentNode && res.find((n) => n.id === node.parentNode);
249
- const deletable = typeof node.deletable === 'boolean' ? node.deletable : true;
250
- if (deletable && (nodeIds.includes(node.id) || parentHit)) {
251
- res.push(node);
336
+ const matchingNodes = [];
337
+ for (const node of nodes) {
338
+ if (node.deletable === false) {
339
+ continue;
252
340
  }
253
- return res;
254
- }, []);
255
- const deletableEdges = edges.filter((e) => (typeof e.deletable === 'boolean' ? e.deletable : true));
256
- const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
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);
257
349
  const connectedEdges = getConnectedEdgesBase(matchingNodes, deletableEdges);
258
- const matchingEdges = connectedEdges.reduce((res, edge) => {
259
- if (!res.find((e) => e.id === edge.id)) {
260
- res.push(edge);
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);
261
355
  }
262
- return res;
263
- }, initialHitEdges);
264
- return {
265
- matchingEdges,
266
- matchingNodes,
267
- };
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;
268
371
  }
269
372
 
270
373
  const clamp = (val, min = 0, max = 1) => Math.min(Math.max(val, min), max);
@@ -272,8 +375,14 @@ const clampPosition = (position = { x: 0, y: 0 }, extent) => ({
272
375
  x: clamp(position.x, extent[0][0], extent[1][0]),
273
376
  y: clamp(position.y, extent[0][1], extent[1][1]),
274
377
  });
275
- // returns a number between 0 and 1 that represents the velocity of the movement
276
- // when the mouse is close to the edge of the canvas
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
+ */
277
386
  const calcAutoPanVelocity = (value, min, max) => {
278
387
  if (value < min) {
279
388
  return clamp(Math.abs(value - min), 1, 50) / 50;
@@ -347,12 +456,12 @@ const getPositionWithOrigin = ({ x, y, width, height, origin = [0, 0], }) => {
347
456
  y: y - height * origin[1],
348
457
  };
349
458
  };
350
- function snapPosition(position, snapGrid = [1, 1]) {
459
+ const snapPosition = (position, snapGrid = [1, 1]) => {
351
460
  return {
352
461
  x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
353
462
  y: snapGrid[1] * Math.round(position.y / snapGrid[1]),
354
463
  };
355
- }
464
+ };
356
465
  const pointToRendererPoint = ({ x, y }, [tx, ty, tScale], snapToGrid = false, snapGrid = [1, 1]) => {
357
466
  const position = {
358
467
  x: (x - tx) / tScale,
@@ -366,6 +475,22 @@ const rendererPointToPoint = ({ x, y }, [tx, ty, tScale]) => {
366
475
  y: y * tScale + ty,
367
476
  };
368
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
+ */
369
494
  const getViewportForBounds = (bounds, width, height, minZoom, maxZoom, padding) => {
370
495
  const xZoom = width / (bounds.width * (1 + padding));
371
496
  const yZoom = height / (bounds.height * (1 + padding));
@@ -469,6 +594,29 @@ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }) {
469
594
  return [x1, y1 + calculateControlOffset(y2 - y1, c)];
470
595
  }
471
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
+ */
472
620
  function getBezierPath({ sourceX, sourceY, sourcePosition = Position.Bottom, targetX, targetY, targetPosition = Position.Top, curvature = 0.25, }) {
473
621
  const [sourceControlX, sourceControlY] = getControlWithCurvature({
474
622
  pos: sourcePosition,
@@ -513,40 +661,13 @@ function getEdgeCenter({ sourceX, sourceY, targetX, targetY, }) {
513
661
  const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
514
662
  return [centerX, centerY, xOffset, yOffset];
515
663
  }
516
- const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
517
- function groupEdgesByZLevel(edges, nodeLookup, elevateEdgesOnSelect = false) {
518
- let maxLevel = -1;
519
- const levelLookup = edges.reduce((tree, edge) => {
520
- const hasZIndex = isNumeric(edge.zIndex);
521
- let z = hasZIndex ? edge.zIndex : 0;
522
- if (elevateEdgesOnSelect) {
523
- const targetNode = nodeLookup.get(edge.target);
524
- const sourceNode = nodeLookup.get(edge.source);
525
- const edgeOrConnectedNodeSelected = edge.selected || targetNode?.selected || sourceNode?.selected;
526
- const selectedZIndex = Math.max(sourceNode?.[internalsSymbol]?.z || 0, targetNode?.[internalsSymbol]?.z || 0, 1000);
527
- z = (hasZIndex ? edge.zIndex : 0) + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
528
- }
529
- if (tree[z]) {
530
- tree[z].push(edge);
531
- }
532
- else {
533
- tree[z] = [edge];
534
- }
535
- maxLevel = z > maxLevel ? z : maxLevel;
536
- return tree;
537
- }, {});
538
- const edgeTree = Object.entries(levelLookup).map(([key, edges]) => {
539
- const level = +key;
540
- return {
541
- edges,
542
- level,
543
- isMaxLevel: level === maxLevel,
544
- };
545
- });
546
- if (edgeTree.length === 0) {
547
- return defaultEdgeTree;
664
+ function getElevatedEdgeZIndex({ sourceNode, targetNode, selected = false, zIndex = 0, elevateOnSelect = false, }) {
665
+ if (!elevateOnSelect) {
666
+ return zIndex;
548
667
  }
549
- return edgeTree;
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);
550
671
  }
551
672
  function isEdgeVisible({ sourceNode, targetNode, width, height, transform }) {
552
673
  const edgeBox = getBoundsOfBoxes(nodeToBox(sourceNode), nodeToBox(targetNode));
@@ -564,13 +685,21 @@ function isEdgeVisible({ sourceNode, targetNode, width, height, transform }) {
564
685
  };
565
686
  return getOverlappingArea(viewRect, boxToRect(edgeBox)) > 0;
566
687
  }
567
- const getEdgeId = ({ source, sourceHandle, target, targetHandle }) => `xyflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
688
+ const getEdgeId = ({ source, sourceHandle, target, targetHandle }) => `xy-edge__${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
568
689
  const connectionExists = (edge, edges) => {
569
690
  return edges.some((el) => el.source === edge.source &&
570
691
  el.target === edge.target &&
571
692
  (el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
572
693
  (el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle)));
573
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
+ */
574
703
  const addEdgeBase = (edgeParams, edges) => {
575
704
  if (!edgeParams.source || !edgeParams.target) {
576
705
  devWarn('006', errorMessages['error006']());
@@ -589,8 +718,22 @@ const addEdgeBase = (edgeParams, edges) => {
589
718
  if (connectionExists(edge, edges)) {
590
719
  return edges;
591
720
  }
721
+ if (edge.sourceHandle === null) {
722
+ delete edge.sourceHandle;
723
+ }
724
+ if (edge.targetHandle === null) {
725
+ delete edge.targetHandle;
726
+ }
592
727
  return edges.concat(edge);
593
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
+ */
594
737
  const updateEdgeBase = (oldEdge, newConnection, edges, options = { shouldReplaceId: true }) => {
595
738
  const { id: oldEdgeId, ...rest } = oldEdge;
596
739
  if (!newConnection.source || !newConnection.target) {
@@ -614,6 +757,26 @@ const updateEdgeBase = (oldEdge, newConnection, edges, options = { shouldReplace
614
757
  return edges.filter((e) => e.id !== oldEdgeId).concat(edge);
615
758
  };
616
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
+ */
617
780
  function getStraightPath({ sourceX, sourceY, targetX, targetY, }) {
618
781
  const [labelX, labelY, offsetX, offsetY] = getEdgeCenter({
619
782
  sourceX,
@@ -762,6 +925,28 @@ function getBend(a, b, c, size) {
762
925
  const yDir = a.y < c.y ? -1 : 1;
763
926
  return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;
764
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
+ */
765
950
  function getSmoothStepPath({ sourceX, sourceY, sourcePosition = Position.Bottom, targetX, targetY, targetPosition = Position.Top, borderRadius = 5, centerX, centerY, offset = 20, }) {
766
951
  const [points, labelX, labelY, offsetX, offsetY] = getPoints({
767
952
  source: { x: sourceX, y: sourceY },
@@ -785,18 +970,22 @@ function getSmoothStepPath({ sourceX, sourceY, sourcePosition = Position.Bottom,
785
970
  return [path, labelX, labelY, offsetX, offsetY];
786
971
  }
787
972
 
973
+ function isNodeInitialized(node) {
974
+ return !!(node?.[internalsSymbol]?.handleBounds || node?.handles?.length) && !!(node?.computed?.width || node?.width);
975
+ }
788
976
  function getEdgePosition(params) {
789
- const [sourceNodeRect, sourceHandleBounds, isSourceValid] = getHandleDataByNode(params.sourceNode);
790
- const [targetNodeRect, targetHandleBounds, isTargetValid] = getHandleDataByNode(params.targetNode);
791
- if (!isSourceValid || !isTargetValid) {
977
+ const { sourceNode, targetNode } = params;
978
+ if (!isNodeInitialized(sourceNode) || !isNodeInitialized(targetNode)) {
792
979
  return null;
793
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(
794
985
  // when connection type is loose we can define all handles as sources and connect source -> source
795
- const targetNodeHandles = params.connectionMode === ConnectionMode.Strict
796
- ? targetHandleBounds.target
797
- : (targetHandleBounds.target ?? []).concat(targetHandleBounds.source ?? []);
798
- const sourceHandle = getHandle(sourceHandleBounds.source, params.sourceHandle);
799
- const targetHandle = getHandle(targetNodeHandles, params.targetHandle);
986
+ params.connectionMode === ConnectionMode.Strict
987
+ ? targetHandleBounds?.target ?? []
988
+ : (targetHandleBounds?.target ?? []).concat(targetHandleBounds?.source ?? []), params.targetHandle);
800
989
  const sourcePosition = sourceHandle?.position || Position.Bottom;
801
990
  const targetPosition = targetHandle?.position || Position.Top;
802
991
  if (!sourceHandle || !targetHandle) {
@@ -807,8 +996,8 @@ function getEdgePosition(params) {
807
996
  }));
808
997
  return null;
809
998
  }
810
- const { x: sourceX, y: sourceY } = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
811
- const { x: targetX, y: targetY } = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
999
+ const [sourceX, sourceY] = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
1000
+ const [targetX, targetY] = getHandlePosition(targetPosition, targetNode, targetHandle);
812
1001
  return {
813
1002
  sourceX,
814
1003
  sourceY,
@@ -822,67 +1011,37 @@ function toHandleBounds(handles) {
822
1011
  if (!handles) {
823
1012
  return null;
824
1013
  }
825
- return handles.reduce((res, item) => {
826
- item.width = item.width || 1;
827
- item.height = item.height || 1;
828
- if (item.type === 'source') {
829
- res.source?.push(item);
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);
830
1021
  }
831
- if (item.type === 'target') {
832
- res.target?.push(item);
1022
+ else if (handle.type === 'target') {
1023
+ target.push(handle);
833
1024
  }
834
- return res;
835
- }, {
836
- source: [],
837
- target: [],
838
- });
839
- }
840
- function getHandleDataByNode(node) {
841
- const handleBounds = node?.[internalsSymbol]?.handleBounds || toHandleBounds(node?.handles) || null;
842
- const nodeWidth = node?.computed?.width || node?.width;
843
- const nodeHeight = node?.computed?.height || node?.height;
844
- const isValid = handleBounds &&
845
- nodeWidth &&
846
- nodeHeight &&
847
- typeof node?.computed?.positionAbsolute?.x !== 'undefined' &&
848
- typeof node?.computed?.positionAbsolute?.y !== 'undefined';
849
- return [
850
- {
851
- x: node?.computed?.positionAbsolute?.x || 0,
852
- y: node?.computed?.positionAbsolute?.y || 0,
853
- width: nodeWidth || 0,
854
- height: nodeHeight || 0,
855
- },
856
- handleBounds,
857
- !!isValid,
858
- ];
1025
+ }
1026
+ return {
1027
+ source,
1028
+ target,
1029
+ };
859
1030
  }
860
- function getHandlePosition(position, nodeRect, handle = null) {
861
- const x = (handle?.x || 0) + nodeRect.x;
862
- const y = (handle?.y || 0) + nodeRect.y;
863
- const width = handle?.width || nodeRect.width;
864
- const height = handle?.height || nodeRect.height;
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);
865
1036
  switch (position) {
866
1037
  case Position.Top:
867
- return {
868
- x: x + width / 2,
869
- y,
870
- };
1038
+ return [x + width / 2, y];
871
1039
  case Position.Right:
872
- return {
873
- x: x + width,
874
- y: y + height / 2,
875
- };
1040
+ return [x + width, y + height / 2];
876
1041
  case Position.Bottom:
877
- return {
878
- x: x + width / 2,
879
- y: y + height,
880
- };
1042
+ return [x + width / 2, y + height];
881
1043
  case Position.Left:
882
- return {
883
- x,
884
- y: y + height / 2,
885
- };
1044
+ return [x, y + height / 2];
886
1045
  }
887
1046
  }
888
1047
  function getHandle(bounds, handleId) {
@@ -979,10 +1138,13 @@ function updateAbsolutePositions(nodes, nodeLookup, nodeOrigin = [0, 0], parentN
979
1138
  ...node.position,
980
1139
  z: node[internalsSymbol]?.z ?? 0,
981
1140
  }, parentNode?.origin || nodeOrigin);
982
- node.computed.positionAbsolute = {
983
- x,
984
- y,
985
- };
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;
986
1148
  node[internalsSymbol].z = z;
987
1149
  if (parentNodes?.[node.id]) {
988
1150
  node[internalsSymbol].isParent = true;
@@ -991,15 +1153,21 @@ function updateAbsolutePositions(nodes, nodeLookup, nodeOrigin = [0, 0], parentN
991
1153
  return node;
992
1154
  });
993
1155
  }
994
- function updateNodes(nodes, nodeLookup, options = {
1156
+ function adoptUserProvidedNodes(nodes, nodeLookup, options = {
995
1157
  nodeOrigin: [0, 0],
996
1158
  elevateNodesOnSelect: true,
997
1159
  defaults: {},
998
1160
  }) {
1161
+ const tmpLookup = new Map(nodeLookup);
1162
+ nodeLookup.clear();
999
1163
  const parentNodes = {};
1000
1164
  const selectedNodeZ = options?.elevateNodesOnSelect ? 1000 : 0;
1001
1165
  const nextNodes = nodes.map((n) => {
1002
- const currentStoreNode = nodeLookup.get(n.id);
1166
+ const currentStoreNode = tmpLookup.get(n.id);
1167
+ if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) {
1168
+ nodeLookup.set(n.id, currentStoreNode);
1169
+ return currentStoreNode;
1170
+ }
1003
1171
  const node = {
1004
1172
  ...options.defaults,
1005
1173
  ...n,
@@ -1019,6 +1187,7 @@ function updateNodes(nodes, nodeLookup, options = {
1019
1187
  value: {
1020
1188
  handleBounds: currInternals?.handleBounds,
1021
1189
  z,
1190
+ userProvidedNode: n,
1022
1191
  },
1023
1192
  });
1024
1193
  nodeLookup.set(node.id, node);
@@ -1093,6 +1262,21 @@ function panBy({ delta, panZoom, transform, translateExtent, width, height, }) {
1093
1262
  (nextViewport.x !== transform[0] || nextViewport.y !== transform[1] || nextViewport.k !== transform[2]);
1094
1263
  return transformChanged;
1095
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
+ }
1096
1280
 
1097
1281
  function wrapSelectionDragFunc(selectionFunc) {
1098
1282
  return (event, _, nodes) => selectionFunc?.(event, nodes);
@@ -1424,7 +1608,6 @@ function getConnectionStatus(isInsideConnectionRadius, isHandleValid) {
1424
1608
  return connectionStatus;
1425
1609
  }
1426
1610
 
1427
- const nullConnection = { source: null, target: null, sourceHandle: null, targetHandle: null };
1428
1611
  const alwaysValid = () => true;
1429
1612
  let connectionStartHandle = null;
1430
1613
  function onPointerDown(event, { connectionMode, connectionRadius, handleId, nodeId, edgeUpdaterType, isTarget, domNode, nodes, lib, autoPanOnConnect, panBy, cancelConnection, onConnectStart, onConnect, onConnectEnd, isValidConnection = alwaysValid, onEdgeUpdateEnd, updateConnection, getTransform, }) {
@@ -1509,7 +1692,7 @@ function onPointerDown(event, { connectionMode, connectionRadius, handleId, node
1509
1692
  if (!closestHandle && !isValid && !handleDomNode) {
1510
1693
  return resetRecentHandle(prevActiveHandle, lib);
1511
1694
  }
1512
- if (connection.source !== connection.target && handleDomNode) {
1695
+ if (connection?.source !== connection?.target && handleDomNode) {
1513
1696
  resetRecentHandle(prevActiveHandle, lib);
1514
1697
  prevActiveHandle = handleDomNode;
1515
1698
  handleDomNode.classList.add('connecting', `${lib}-flow__handle-connecting`);
@@ -1557,7 +1740,7 @@ function isValidHandle(event, { handle, connectionMode, fromNodeId, fromHandleId
1557
1740
  const result = {
1558
1741
  handleDomNode: handleToCheck,
1559
1742
  isValid: false,
1560
- connection: nullConnection,
1743
+ connection: null,
1561
1744
  endHandle: null,
1562
1745
  };
1563
1746
  if (handleToCheck) {
@@ -1566,6 +1749,9 @@ function isValidHandle(event, { handle, connectionMode, fromNodeId, fromHandleId
1566
1749
  const handleId = handleToCheck.getAttribute('data-handleid');
1567
1750
  const connectable = handleToCheck.classList.contains('connectable');
1568
1751
  const connectableEnd = handleToCheck.classList.contains('connectableend');
1752
+ if (!handleNodeId) {
1753
+ return result;
1754
+ }
1569
1755
  const connection = {
1570
1756
  source: isTarget ? handleNodeId : fromNodeId,
1571
1757
  sourceHandle: isTarget ? handleId : fromHandleId,
@@ -1614,8 +1800,7 @@ function XYMinimap({ domNode, panZoom, getTransform, getViewScale }) {
1614
1800
  if (event.sourceEvent.type !== 'mousemove' || !panZoom) {
1615
1801
  return;
1616
1802
  }
1617
- // @TODO: how to calculate the correct next position? Math.max(1, transform[2]) is a workaround.
1618
- const moveScale = getViewScale() * Math.max(1, transform[2]) * (inversePan ? -1 : 1);
1803
+ const moveScale = getViewScale() * Math.max(transform[2], Math.log(transform[2])) * (inversePan ? -1 : 1);
1619
1804
  const position = {
1620
1805
  x: transform[0] - event.sourceEvent.movementX * moveScale,
1621
1806
  y: transform[1] - event.sourceEvent.movementY * moveScale,
@@ -1988,4 +2173,4 @@ function XYPanZoom({ domNode, minZoom, maxZoom, translateExtent, viewport, onPan
1988
2173
  };
1989
2174
  }
1990
2175
 
1991
- 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, getNodeToolbarTransform, getNodesBounds, getNodesInside, getOutgoersBase, getOverlappingArea, getPointerPosition, getPositionWithOrigin, getSmoothStepPath, getStraightPath, getViewportForBounds, groupEdgesByZLevel, infiniteExtent, internalsSymbol, isEdgeBase, isEdgeVisible, isInputDOMNode, isMacOs, isMouseEvent, isNodeBase, isNumeric, isRectObject, nodeToBox, nodeToRect, panBy, pointToRendererPoint, rectToBox, rendererPointToPoint, snapPosition, updateAbsolutePositions, updateEdgeBase, updateNodeDimensions, updateNodes };
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 };