@xyflow/system 0.0.15 → 0.0.17

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 (44) hide show
  1. package/dist/esm/index.js +338 -139
  2. package/dist/esm/index.mjs +338 -139
  3. package/dist/esm/types/general.d.ts +6 -3
  4. package/dist/esm/types/general.d.ts.map +1 -1
  5. package/dist/esm/types/nodes.d.ts +3 -4
  6. package/dist/esm/types/nodes.d.ts.map +1 -1
  7. package/dist/esm/utils/connections.d.ts +3 -3
  8. package/dist/esm/utils/connections.d.ts.map +1 -1
  9. package/dist/esm/utils/dom.d.ts.map +1 -1
  10. package/dist/esm/utils/general.d.ts +1 -0
  11. package/dist/esm/utils/general.d.ts.map +1 -1
  12. package/dist/esm/utils/graph.d.ts +25 -6
  13. package/dist/esm/utils/graph.d.ts.map +1 -1
  14. package/dist/esm/utils/marker.d.ts +3 -1
  15. package/dist/esm/utils/marker.d.ts.map +1 -1
  16. package/dist/esm/xydrag/XYDrag.d.ts +5 -5
  17. package/dist/esm/xydrag/XYDrag.d.ts.map +1 -1
  18. package/dist/esm/xyhandle/XYHandle.d.ts.map +1 -1
  19. package/dist/esm/xyresizer/XYResizer.d.ts +7 -2
  20. package/dist/esm/xyresizer/XYResizer.d.ts.map +1 -1
  21. package/dist/esm/xyresizer/utils.d.ts +12 -13
  22. package/dist/esm/xyresizer/utils.d.ts.map +1 -1
  23. package/dist/umd/index.js +1 -1
  24. package/dist/umd/types/general.d.ts +6 -3
  25. package/dist/umd/types/general.d.ts.map +1 -1
  26. package/dist/umd/types/nodes.d.ts +3 -4
  27. package/dist/umd/types/nodes.d.ts.map +1 -1
  28. package/dist/umd/utils/connections.d.ts +3 -3
  29. package/dist/umd/utils/connections.d.ts.map +1 -1
  30. package/dist/umd/utils/dom.d.ts.map +1 -1
  31. package/dist/umd/utils/general.d.ts +1 -0
  32. package/dist/umd/utils/general.d.ts.map +1 -1
  33. package/dist/umd/utils/graph.d.ts +25 -6
  34. package/dist/umd/utils/graph.d.ts.map +1 -1
  35. package/dist/umd/utils/marker.d.ts +3 -1
  36. package/dist/umd/utils/marker.d.ts.map +1 -1
  37. package/dist/umd/xydrag/XYDrag.d.ts +5 -5
  38. package/dist/umd/xydrag/XYDrag.d.ts.map +1 -1
  39. package/dist/umd/xyhandle/XYHandle.d.ts.map +1 -1
  40. package/dist/umd/xyresizer/XYResizer.d.ts +7 -2
  41. package/dist/umd/xyresizer/XYResizer.d.ts.map +1 -1
  42. package/dist/umd/xyresizer/utils.d.ts +12 -13
  43. package/dist/umd/xyresizer/utils.d.ts.map +1 -1
  44. package/package.json +3 -3
package/dist/esm/index.js CHANGED
@@ -163,8 +163,10 @@ const getIncomers = (node, nodes, edges) => {
163
163
  const getNodePositionWithOrigin = (node, nodeOrigin = [0, 0]) => {
164
164
  if (!node) {
165
165
  return {
166
- x: 0,
167
- y: 0,
166
+ position: {
167
+ x: 0,
168
+ y: 0,
169
+ },
168
170
  positionAbsolute: {
169
171
  x: 0,
170
172
  y: 0,
@@ -178,7 +180,7 @@ const getNodePositionWithOrigin = (node, nodeOrigin = [0, 0]) => {
178
180
  y: node.position.y - offsetY,
179
181
  };
180
182
  return {
181
- ...position,
183
+ position,
182
184
  positionAbsolute: node.computed?.positionAbsolute
183
185
  ? {
184
186
  x: node.computed.positionAbsolute.x - offsetX,
@@ -192,18 +194,18 @@ const getNodePositionWithOrigin = (node, nodeOrigin = [0, 0]) => {
192
194
  * @public
193
195
  * @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport.
194
196
  * @param nodes - Nodes to calculate the bounds for
195
- * @param nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
197
+ * @param params.nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
198
+ * @param params.useRelativePosition - Whether to use the relative or absolute node positions
196
199
  * @returns Bounding box enclosing all nodes
197
200
  */
198
- const getNodesBounds = (nodes, nodeOrigin = [0, 0]) => {
201
+ const getNodesBounds = (nodes, params = { nodeOrigin: [0, 0], useRelativePosition: false }) => {
199
202
  if (nodes.length === 0) {
200
203
  return { x: 0, y: 0, width: 0, height: 0 };
201
204
  }
202
205
  const box = nodes.reduce((currBox, node) => {
203
- const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
206
+ const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
204
207
  return getBoundsOfBoxes(currBox, rectToBox({
205
- x,
206
- y,
208
+ ...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'],
207
209
  width: node.computed?.width ?? node.width ?? 0,
208
210
  height: node.computed?.height ?? node.height ?? 0,
209
211
  }));
@@ -259,64 +261,73 @@ function fitView({ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin =
259
261
  return isVisible;
260
262
  });
261
263
  if (filteredNodes.length > 0) {
262
- const bounds = getNodesBounds(filteredNodes, nodeOrigin);
264
+ const bounds = getNodesBounds(filteredNodes, { nodeOrigin });
263
265
  const viewport = getViewportForBounds(bounds, width, height, options?.minZoom ?? minZoom, options?.maxZoom ?? maxZoom, options?.padding ?? 0.1);
264
266
  panZoom.setViewport(viewport, { duration: options?.duration });
265
267
  return true;
266
268
  }
267
269
  return false;
268
270
  }
271
+ /**
272
+ * This function clamps the passed extend by the node's width and height.
273
+ * This is needed to prevent the node from being dragged outside of its extent.
274
+ *
275
+ * @param node
276
+ * @param extent
277
+ * @returns
278
+ */
269
279
  function clampNodeExtent(node, extent) {
270
280
  if (!extent || extent === 'parent') {
271
281
  return extent;
272
282
  }
273
283
  return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]];
274
284
  }
275
- function calcNextPosition(node, nextPosition, nodes, nodeExtent, nodeOrigin = [0, 0], onError) {
276
- const clampedNodeExtent = clampNodeExtent(node, node.extent || nodeExtent);
277
- let currentExtent = clampedNodeExtent;
278
- let parentNode = null;
279
- let parentPos = { x: 0, y: 0 };
280
- if (node.parentNode) {
281
- parentNode = nodes.find((n) => n.id === node.parentNode) || null;
282
- parentPos = parentNode
283
- ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
284
- : parentPos;
285
- }
285
+ /**
286
+ * This function calculates the next position of a node, taking into account the node's extent, parent node, and origin.
287
+ *
288
+ * @internal
289
+ * @returns position, positionAbsolute
290
+ */
291
+ function calculateNodePosition({ nodeId, nextPosition, nodeLookup, nodeOrigin = [0, 0], nodeExtent, onError, }) {
292
+ const node = nodeLookup.get(nodeId);
293
+ const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : undefined;
294
+ const { x: parentX, y: parentY } = parentNode
295
+ ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
296
+ : { x: 0, y: 0 };
297
+ let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
286
298
  if (node.extent === 'parent' && !node.expandParent) {
287
- const nodeWidth = node.computed?.width;
288
- const nodeHeight = node.computed?.height;
289
- if (node.parentNode && nodeWidth && nodeHeight) {
290
- const currNodeOrigin = node.origin || nodeOrigin;
291
- currentExtent =
292
- parentNode && isNumeric(parentNode.computed?.width) && isNumeric(parentNode.computed?.height)
293
- ? [
294
- [parentPos.x + nodeWidth * currNodeOrigin[0], parentPos.y + nodeHeight * currNodeOrigin[1]],
295
- [
296
- parentPos.x + (parentNode.computed?.width ?? 0) - nodeWidth + nodeWidth * currNodeOrigin[0],
297
- parentPos.y + (parentNode.computed?.height ?? 0) - nodeHeight + nodeHeight * currNodeOrigin[1],
298
- ],
299
- ]
300
- : currentExtent;
299
+ if (!parentNode) {
300
+ onError?.('005', errorMessages['error005']());
301
301
  }
302
302
  else {
303
- onError?.('005', errorMessages['error005']());
304
- currentExtent = clampedNodeExtent;
303
+ const nodeWidth = node.computed?.width;
304
+ const nodeHeight = node.computed?.height;
305
+ const parentWidth = parentNode?.computed?.width;
306
+ const parentHeight = parentNode?.computed?.height;
307
+ if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
308
+ const currNodeOrigin = node.origin || nodeOrigin;
309
+ const extentX = parentX + nodeWidth * currNodeOrigin[0];
310
+ const extentY = parentY + nodeHeight * currNodeOrigin[1];
311
+ currentExtent = [
312
+ [extentX, extentY],
313
+ [extentX + parentWidth - nodeWidth, extentY + parentHeight - nodeHeight],
314
+ ];
315
+ }
305
316
  }
306
317
  }
307
- else if (node.extent && node.parentNode && node.extent !== 'parent') {
318
+ else if (parentNode && isCoordinateExtent(node.extent)) {
308
319
  currentExtent = [
309
- [node.extent[0][0] + parentPos.x, node.extent[0][1] + parentPos.y],
310
- [node.extent[1][0] + parentPos.x, node.extent[1][1] + parentPos.y],
320
+ [node.extent[0][0] + parentX, node.extent[0][1] + parentY],
321
+ [node.extent[1][0] + parentX, node.extent[1][1] + parentY],
311
322
  ];
312
323
  }
313
- const positionAbsolute = currentExtent && currentExtent !== 'parent'
324
+ const positionAbsolute = isCoordinateExtent(currentExtent)
314
325
  ? clampPosition(nextPosition, currentExtent)
315
326
  : nextPosition;
316
327
  return {
317
328
  position: {
318
- x: positionAbsolute.x - parentPos.x,
319
- y: positionAbsolute.y - parentPos.y,
329
+ x: positionAbsolute.x - parentX,
330
+ y: positionAbsolute.y - parentY,
320
331
  },
321
332
  positionAbsolute,
322
333
  };
@@ -503,6 +514,9 @@ const getViewportForBounds = (bounds, width, height, minZoom, maxZoom, padding)
503
514
  return { x, y, zoom: clampedZoom };
504
515
  };
505
516
  const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0;
517
+ function isCoordinateExtent(extent) {
518
+ return extent !== undefined && extent !== 'parent';
519
+ }
506
520
 
507
521
  function getPointerPosition(event, { snapGrid = [0, 0], snapToGrid = false, transform }) {
508
522
  const { x, y } = getEventPosition(event);
@@ -525,10 +539,8 @@ function isInputDOMNode(event) {
525
539
  // using composed path for handling shadow dom
526
540
  const target = (event.composedPath?.()?.[0] || event.target);
527
541
  const isInput = inputTags.includes(target?.nodeName) || target?.hasAttribute('contenteditable');
528
- // we want to be able to do a multi selection event if we are in an input field
529
- const isModifierKey = event.ctrlKey || event.metaKey || event.shiftKey;
530
542
  // when an input field is focused we don't want to trigger deletion or movement of nodes
531
- return (isInput && !isModifierKey) || !!target?.closest('.nokey');
543
+ return isInput || !!target?.closest('.nokey');
532
544
  }
533
545
  const isMouseEvent = (event) => 'clientX' in event;
534
546
  const getEventPosition = (event, bounds) => {
@@ -1048,13 +1060,8 @@ function getHandle(bounds, handleId) {
1048
1060
  if (!bounds) {
1049
1061
  return null;
1050
1062
  }
1051
- if (bounds.length === 1 || !handleId) {
1052
- return bounds[0];
1053
- }
1054
- else if (handleId) {
1055
- return bounds.find((d) => d.id === handleId) || null;
1056
- }
1057
- return null;
1063
+ // if no handleId is given, we use the first handle, otherwise we check for the id
1064
+ return (!handleId ? bounds[0] : bounds.find((d) => d.id === handleId)) || null;
1058
1065
  }
1059
1066
 
1060
1067
  function getMarkerId(marker, id) {
@@ -1070,16 +1077,16 @@ function getMarkerId(marker, id) {
1070
1077
  .map((key) => `${key}=${marker[key]}`)
1071
1078
  .join('&')}`;
1072
1079
  }
1073
- function createMarkerIds(edges, { id, defaultColor }) {
1074
- const ids = [];
1080
+ function createMarkerIds(edges, { id, defaultColor, defaultMarkerStart, defaultMarkerEnd, }) {
1081
+ const ids = new Set();
1075
1082
  return edges
1076
1083
  .reduce((markers, edge) => {
1077
- [edge.markerStart, edge.markerEnd].forEach((marker) => {
1084
+ [edge.markerStart || defaultMarkerStart, edge.markerEnd || defaultMarkerEnd].forEach((marker) => {
1078
1085
  if (marker && typeof marker === 'object') {
1079
1086
  const markerId = getMarkerId(marker, id);
1080
- if (!ids.includes(markerId)) {
1087
+ if (!ids.has(markerId)) {
1081
1088
  markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
1082
- ids.push(markerId);
1089
+ ids.add(markerId);
1083
1090
  }
1084
1091
  }
1085
1092
  });
@@ -1173,8 +1180,8 @@ function adoptUserProvidedNodes(nodes, nodeLookup, options = {
1173
1180
  ...n,
1174
1181
  computed: {
1175
1182
  positionAbsolute: n.position,
1176
- width: n.computed?.width || currentStoreNode?.computed?.width,
1177
- height: n.computed?.height || currentStoreNode?.computed?.height,
1183
+ width: n.computed?.width,
1184
+ height: n.computed?.height,
1178
1185
  },
1179
1186
  };
1180
1187
  const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);
@@ -1201,7 +1208,7 @@ function calculateXYZPosition(node, nodes, nodeLookup, result, nodeOrigin) {
1201
1208
  return result;
1202
1209
  }
1203
1210
  const parentNode = nodeLookup.get(node.parentNode);
1204
- const parentNodePosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
1211
+ const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
1205
1212
  return calculateXYZPosition(parentNode, nodes, nodeLookup, {
1206
1213
  x: (result.x ?? 0) + parentNodePosition.x,
1207
1214
  y: (result.y ?? 0) + parentNodePosition.y,
@@ -1271,7 +1278,7 @@ function updateConnectionLookup(connectionLookup, edgeLookup, edges) {
1271
1278
  const targetKey = `${target}-target-${targetHandle}`;
1272
1279
  const prevSource = connectionLookup.get(sourceKey) || new Map();
1273
1280
  const prevTarget = connectionLookup.get(targetKey) || new Map();
1274
- const connection = { source, target, sourceHandle, targetHandle };
1281
+ const connection = { edgeId: edge.id, source, target, sourceHandle, targetHandle };
1275
1282
  edgeLookup.set(edge.id, edge);
1276
1283
  connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
1277
1284
  connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
@@ -1351,6 +1358,7 @@ function getEventHandlerParams({ nodeId, dragItems, nodeLookup, }) {
1351
1358
  return [nodeId ? nodesFromDragItems.find((n) => n.id === nodeId) : nodesFromDragItems[0], nodesFromDragItems];
1352
1359
  }
1353
1360
 
1361
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1354
1362
  function XYDrag({ domNode, onNodeMouseDown, getStoreItems, onDragStart, onDrag, onDragStop, }) {
1355
1363
  let lastPos = { x: null, y: null };
1356
1364
  let autoPanId = 0;
@@ -1364,12 +1372,12 @@ function XYDrag({ domNode, onNodeMouseDown, getStoreItems, onDragStart, onDrag,
1364
1372
  // public functions
1365
1373
  function update({ noDragClassName, handleSelector, domNode, isSelectable, nodeId }) {
1366
1374
  function updateNodes({ x, y }) {
1367
- const { nodes, nodeLookup, nodeExtent, snapGrid, snapToGrid, nodeOrigin, onNodeDrag, onSelectionDrag, onError, updateNodePositions, } = getStoreItems();
1375
+ const { nodeLookup, nodeExtent, snapGrid, snapToGrid, nodeOrigin, onNodeDrag, onSelectionDrag, onError, updateNodePositions, } = getStoreItems();
1368
1376
  lastPos = { x, y };
1369
1377
  let hasChange = false;
1370
1378
  let nodesBox = { x: 0, y: 0, x2: 0, y2: 0 };
1371
1379
  if (dragItems.length > 1 && nodeExtent) {
1372
- const rect = getNodesBounds(dragItems, nodeOrigin);
1380
+ const rect = getNodesBounds(dragItems, { nodeOrigin });
1373
1381
  nodesBox = rectToBox(rect);
1374
1382
  }
1375
1383
  dragItems = dragItems.map((n) => {
@@ -1391,17 +1399,24 @@ function XYDrag({ domNode, onNodeMouseDown, getStoreItems, onDragStart, onDrag,
1391
1399
  adjustedNodeExtent[1][1] =
1392
1400
  n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
1393
1401
  }
1394
- const updatedPos = calcNextPosition(n, nextPosition, nodes, adjustedNodeExtent, nodeOrigin, onError);
1402
+ const { position, positionAbsolute } = calculateNodePosition({
1403
+ nodeId: n.id,
1404
+ nextPosition,
1405
+ nodeLookup,
1406
+ nodeExtent: adjustedNodeExtent,
1407
+ nodeOrigin,
1408
+ onError,
1409
+ });
1395
1410
  // we want to make sure that we only fire a change event when there is a change
1396
- hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
1397
- n.position = updatedPos.position;
1398
- n.computed.positionAbsolute = updatedPos.positionAbsolute;
1411
+ hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y;
1412
+ n.position = position;
1413
+ n.computed.positionAbsolute = positionAbsolute;
1399
1414
  return n;
1400
1415
  });
1401
1416
  if (!hasChange) {
1402
1417
  return;
1403
1418
  }
1404
- updateNodePositions(dragItems, true, true);
1419
+ updateNodePositions(dragItems, true);
1405
1420
  const onNodeOrSelectionDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
1406
1421
  if (dragEvent && (onDrag || onNodeOrSelectionDrag)) {
1407
1422
  const [currentNode, currentNodes] = getEventHandlerParams({
@@ -1444,7 +1459,7 @@ function XYDrag({ domNode, onNodeMouseDown, getStoreItems, onDragStart, onDrag,
1444
1459
  lastPos = pointerPos;
1445
1460
  dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId);
1446
1461
  const onNodeOrSelectionDragStart = nodeId ? onNodeDragStart : wrapSelectionDragFunc(onSelectionDragStart);
1447
- if (dragItems && (onDragStart || onNodeOrSelectionDragStart)) {
1462
+ if (dragItems.length > 0 && (onDragStart || onNodeOrSelectionDragStart)) {
1448
1463
  const [currentNode, currentNodes] = getEventHandlerParams({
1449
1464
  nodeId,
1450
1465
  dragItems,
@@ -1494,10 +1509,10 @@ function XYDrag({ domNode, onNodeMouseDown, getStoreItems, onDragStart, onDrag,
1494
1509
  autoPanStarted = false;
1495
1510
  dragStarted = false;
1496
1511
  cancelAnimationFrame(autoPanId);
1497
- if (dragItems) {
1512
+ if (dragItems.length > 0) {
1498
1513
  const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
1499
1514
  const onNodeOrSelectionDragStop = nodeId ? onNodeDragStop : wrapSelectionDragFunc(onSelectionDragStop);
1500
- updateNodePositions(dragItems, false, false);
1515
+ updateNodePositions(dragItems, false);
1501
1516
  if (onDragStop || onNodeOrSelectionDragStop) {
1502
1517
  const [currentNode, currentNodes] = getEventHandlerParams({
1503
1518
  nodeId,
@@ -1732,7 +1747,9 @@ function onPointerDown(event, { connectionMode, connectionRadius, handleId, node
1732
1747
  // checks if and returns connection in fom of an object { source: 123, target: 312 }
1733
1748
  function isValidHandle(event, { handle, connectionMode, fromNodeId, fromHandleId, fromType, doc, lib, flowId, isValidConnection = alwaysValid, }) {
1734
1749
  const isTarget = fromType === 'target';
1735
- const handleDomNode = doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`);
1750
+ const handleDomNode = handle
1751
+ ? doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`)
1752
+ : null;
1736
1753
  const { x, y } = getEventPosition(event);
1737
1754
  const handleBelow = doc.elementFromPoint(x, y);
1738
1755
  // we always want to prioritize the handle below the mouse cursor over the closest distance handle,
@@ -2221,65 +2238,173 @@ function getControlDirection(controlPosition) {
2221
2238
  affectsY,
2222
2239
  };
2223
2240
  }
2241
+ function getLowerExtentClamp(lowerExtent, lowerBound) {
2242
+ return Math.max(0, lowerBound - lowerExtent);
2243
+ }
2244
+ function getUpperExtentClamp(upperExtent, upperBound) {
2245
+ return Math.max(0, upperExtent - upperBound);
2246
+ }
2247
+ function getSizeClamp(size, minSize, maxSize) {
2248
+ return Math.max(0, minSize - size, size - maxSize);
2249
+ }
2250
+ function xor(a, b) {
2251
+ return a ? !b : b;
2252
+ }
2224
2253
  /**
2225
- * Calculates new width & height of node after resize based on pointer position
2254
+ * Calculates new width & height and x & y of node after resize based on pointer position
2255
+ * @description - Buckle up, this is a chunky one! If you want to determine the new dimensions of a node after a resize,
2256
+ * you have to account for all possible restrictions: min/max width/height of the node, the maximum extent the node is allowed
2257
+ * to move in (in this case: resize into) determined by the parent node, the minimal extent determined by child nodes
2258
+ * with expandParent or extent: 'parent' set and oh yeah, these things also have to work with keepAspectRatio!
2259
+ * The way this is done is by determining how much each of these restricting actually restricts the resize and then applying the
2260
+ * strongest restriction. Because the resize affects x, y and width, height and width, height of a opposing side with keepAspectRatio,
2261
+ * the resize amount is always kept in distX & distY amount (the distance in mouse movement)
2262
+ * Instead of clamping each value, we first calculate the biggest 'clamp' (for the lack of a better name) and then apply it to all values.
2226
2263
  * @param startValues - starting values of resize
2227
2264
  * @param controlDirection - dimensions affected by the resize
2228
2265
  * @param pointerPosition - the current pointer position corrected for snapping
2229
2266
  * @param boundaries - minimum and maximum dimensions of the node
2230
2267
  * @param keepAspectRatio - prevent changes of asprect ratio
2231
- * @returns width: new width of node, height: new height of node
2268
+ * @returns x, y, width and height of the node after resize
2232
2269
  */
2233
- function getDimensionsAfterResize(startValues, controlDirection, pointerPosition, boundaries, keepAspectRatio) {
2234
- const { isHorizontal, isVertical, affectsX, affectsY } = controlDirection;
2270
+ function getDimensionsAfterResize(startValues, controlDirection, pointerPosition, boundaries, keepAspectRatio, extent, childExtent) {
2271
+ let { affectsX, affectsY } = controlDirection;
2272
+ const { isHorizontal, isVertical } = controlDirection;
2273
+ const isDiagonal = isHorizontal && isVertical;
2235
2274
  const { xSnapped, ySnapped } = pointerPosition;
2236
2275
  const { minWidth, maxWidth, minHeight, maxHeight } = boundaries;
2237
- const { pointerX: startX, pointerY: startY, width: startWidth, height: startHeight, aspectRatio } = startValues;
2238
- const distX = Math.floor(isHorizontal ? xSnapped - startX : 0);
2239
- const distY = Math.floor(isVertical ? ySnapped - startY : 0);
2240
- let width = clamp(startWidth + (affectsX ? -distX : distX), minWidth, maxWidth);
2241
- let height = clamp(startHeight + (affectsY ? -distY : distY), minHeight, maxHeight);
2276
+ const { x: startX, y: startY, width: startWidth, height: startHeight, aspectRatio } = startValues;
2277
+ let distX = Math.floor(isHorizontal ? xSnapped - startValues.pointerX : 0);
2278
+ let distY = Math.floor(isVertical ? ySnapped - startValues.pointerY : 0);
2279
+ const newWidth = startWidth + (affectsX ? -distX : distX);
2280
+ const newHeight = startHeight + (affectsY ? -distY : distY);
2281
+ // Check if maxWidth, minWWidth, maxHeight, minHeight are restricting the resize
2282
+ let clampX = getSizeClamp(newWidth, minWidth, maxWidth);
2283
+ let clampY = getSizeClamp(newHeight, minHeight, maxHeight);
2284
+ // Check if extent is restricting the resize
2285
+ if (extent) {
2286
+ let xExtentClamp = 0;
2287
+ let yExtentClamp = 0;
2288
+ if (affectsX && distX < 0) {
2289
+ xExtentClamp = getLowerExtentClamp(startX + distX, extent[0][0]);
2290
+ }
2291
+ else if (!affectsX && distX > 0) {
2292
+ xExtentClamp = getUpperExtentClamp(startX + newWidth, extent[1][0]);
2293
+ }
2294
+ if (affectsY && distY < 0) {
2295
+ yExtentClamp = getLowerExtentClamp(startY + distY, extent[0][1]);
2296
+ }
2297
+ else if (!affectsY && distY > 0) {
2298
+ yExtentClamp = getUpperExtentClamp(startY + newHeight, extent[1][1]);
2299
+ }
2300
+ clampX = Math.max(clampX, xExtentClamp);
2301
+ clampY = Math.max(clampY, yExtentClamp);
2302
+ }
2303
+ // Check if the child extent is restricting the resize
2304
+ if (childExtent) {
2305
+ let xExtentClamp = 0;
2306
+ let yExtentClamp = 0;
2307
+ if (affectsX && distX > 0) {
2308
+ xExtentClamp = getUpperExtentClamp(startX + distX, childExtent[0][0]);
2309
+ }
2310
+ else if (!affectsX && distX < 0) {
2311
+ xExtentClamp = getLowerExtentClamp(startX + newWidth, childExtent[1][0]);
2312
+ }
2313
+ if (affectsY && distY > 0) {
2314
+ yExtentClamp = getUpperExtentClamp(startY + distY, childExtent[0][1]);
2315
+ }
2316
+ else if (!affectsY && distY < 0) {
2317
+ yExtentClamp = getLowerExtentClamp(startY + newHeight, childExtent[1][1]);
2318
+ }
2319
+ clampX = Math.max(clampX, xExtentClamp);
2320
+ clampY = Math.max(clampY, yExtentClamp);
2321
+ }
2322
+ // Check if the aspect ratio resizing of the other side is restricting the resize
2242
2323
  if (keepAspectRatio) {
2243
- const nextAspectRatio = width / height;
2244
- const isDiagonal = isHorizontal && isVertical;
2245
- const isOnlyHorizontal = isHorizontal && !isVertical;
2246
- const isOnlyVertical = isVertical && !isHorizontal;
2247
- width = (nextAspectRatio <= aspectRatio && isDiagonal) || isOnlyVertical ? height * aspectRatio : width;
2248
- height = (nextAspectRatio > aspectRatio && isDiagonal) || isOnlyHorizontal ? width / aspectRatio : height;
2249
- if (width >= maxWidth) {
2250
- width = maxWidth;
2251
- height = maxWidth / aspectRatio;
2324
+ if (isHorizontal) {
2325
+ // Check if the max dimensions might be restricting the resize
2326
+ const aspectHeightClamp = getSizeClamp(newWidth / aspectRatio, minHeight, maxHeight) * aspectRatio;
2327
+ clampX = Math.max(clampX, aspectHeightClamp);
2328
+ // Check if the extent is restricting the resize
2329
+ if (extent) {
2330
+ let aspectExtentClamp = 0;
2331
+ if ((!affectsX && !affectsY) || (affectsX && !affectsY && isDiagonal)) {
2332
+ aspectExtentClamp = getUpperExtentClamp(startY + newWidth / aspectRatio, extent[1][1]) * aspectRatio;
2333
+ }
2334
+ else {
2335
+ aspectExtentClamp =
2336
+ getLowerExtentClamp(startY + (affectsX ? distX : -distX) / aspectRatio, extent[0][1]) * aspectRatio;
2337
+ }
2338
+ clampX = Math.max(clampX, aspectExtentClamp);
2339
+ }
2340
+ // Check if the child extent is restricting the resize
2341
+ if (childExtent) {
2342
+ let aspectExtentClamp = 0;
2343
+ if ((!affectsX && !affectsY) || (affectsX && !affectsY && isDiagonal)) {
2344
+ aspectExtentClamp = getLowerExtentClamp(startY + newWidth / aspectRatio, childExtent[1][1]) * aspectRatio;
2345
+ }
2346
+ else {
2347
+ aspectExtentClamp =
2348
+ getUpperExtentClamp(startY + (affectsX ? distX : -distX) / aspectRatio, childExtent[0][1]) * aspectRatio;
2349
+ }
2350
+ clampX = Math.max(clampX, aspectExtentClamp);
2351
+ }
2252
2352
  }
2253
- else if (width <= minWidth) {
2254
- width = minWidth;
2255
- height = minWidth / aspectRatio;
2353
+ // Do the same thing for vertical resizing
2354
+ if (isVertical) {
2355
+ const aspectWidthClamp = getSizeClamp(newHeight * aspectRatio, minWidth, maxWidth) / aspectRatio;
2356
+ clampY = Math.max(clampY, aspectWidthClamp);
2357
+ if (extent) {
2358
+ let aspectExtentClamp = 0;
2359
+ if ((!affectsX && !affectsY) || (affectsY && !affectsX && isDiagonal)) {
2360
+ aspectExtentClamp = getUpperExtentClamp(startX + newHeight * aspectRatio, extent[1][0]) / aspectRatio;
2361
+ }
2362
+ else {
2363
+ aspectExtentClamp =
2364
+ getLowerExtentClamp(startX + (affectsY ? distY : -distY) * aspectRatio, extent[0][0]) / aspectRatio;
2365
+ }
2366
+ clampY = Math.max(clampY, aspectExtentClamp);
2367
+ }
2368
+ if (childExtent) {
2369
+ let aspectExtentClamp = 0;
2370
+ if ((!affectsX && !affectsY) || (affectsY && !affectsX && isDiagonal)) {
2371
+ aspectExtentClamp = getLowerExtentClamp(startX + newHeight * aspectRatio, childExtent[1][0]) / aspectRatio;
2372
+ }
2373
+ else {
2374
+ aspectExtentClamp =
2375
+ getUpperExtentClamp(startX + (affectsY ? distY : -distY) * aspectRatio, childExtent[0][0]) / aspectRatio;
2376
+ }
2377
+ clampY = Math.max(clampY, aspectExtentClamp);
2378
+ }
2256
2379
  }
2257
- if (height >= maxHeight) {
2258
- height = maxHeight;
2259
- width = maxHeight * aspectRatio;
2380
+ }
2381
+ distY = distY + (distY < 0 ? clampY : -clampY);
2382
+ distX = distX + (distX < 0 ? clampX : -clampX);
2383
+ if (keepAspectRatio) {
2384
+ if (isDiagonal) {
2385
+ if (newWidth > newHeight * aspectRatio) {
2386
+ distY = (xor(affectsX, affectsY) ? -distX : distX) / aspectRatio;
2387
+ }
2388
+ else {
2389
+ distX = (xor(affectsX, affectsY) ? -distY : distY) * aspectRatio;
2390
+ }
2260
2391
  }
2261
- else if (height <= minHeight) {
2262
- height = minHeight;
2263
- width = minHeight * aspectRatio;
2392
+ else {
2393
+ if (isHorizontal) {
2394
+ distY = distX / aspectRatio;
2395
+ affectsY = affectsX;
2396
+ }
2397
+ else {
2398
+ distX = distY * aspectRatio;
2399
+ affectsX = affectsY;
2400
+ }
2264
2401
  }
2265
2402
  }
2266
2403
  return {
2267
- width,
2268
- height,
2269
- };
2270
- }
2271
- /**
2272
- * Determines new x & y position of node after resize based on new width & height
2273
- * @param startValues - starting values of resize
2274
- * @param controlDirection - dimensions affected by the resize
2275
- * @param width - new width of node
2276
- * @param height - new height of node
2277
- * @returns x: new x position of node, y: new y position of node
2278
- */
2279
- function getPositionAfterResize(startValues, controlDirection, width, height) {
2280
- return {
2281
- x: controlDirection.affectsX ? startValues.x - (width - startValues.width) : startValues.x,
2282
- y: controlDirection.affectsY ? startValues.y - (height - startValues.height) : startValues.y,
2404
+ width: startWidth + (affectsX ? -distX : distX),
2405
+ height: startHeight + (affectsY ? -distY : distY),
2406
+ x: affectsX ? startX + distX : startX,
2407
+ y: affectsY ? startY + distY : startY,
2283
2408
  };
2284
2409
  }
2285
2410
 
@@ -2300,44 +2425,96 @@ const initChange = {
2300
2425
  isWidthChange: false,
2301
2426
  isHeightChange: false,
2302
2427
  };
2428
+ function nodeToParentExtent(node) {
2429
+ return [
2430
+ [0, 0],
2431
+ [node.computed.width, node.computed.height],
2432
+ ];
2433
+ }
2434
+ function nodeToChildExtent(child, parent) {
2435
+ return [
2436
+ [parent.position.x + child.position.x, parent.position.y + child.position.y],
2437
+ [
2438
+ parent.position.x + child.position.x + child.computed.width,
2439
+ parent.position.y + child.position.y + child.computed.height,
2440
+ ],
2441
+ ];
2442
+ }
2303
2443
  function XYResizer({ domNode, nodeId, getStoreItems, onChange }) {
2304
2444
  const selection = select(domNode);
2305
2445
  function update({ controlPosition, boundaries, keepAspectRatio, onResizeStart, onResize, onResizeEnd, shouldResize, }) {
2306
2446
  let prevValues = { ...initPrevValues };
2307
2447
  let startValues = { ...initStartValues };
2308
2448
  const controlDirection = getControlDirection(controlPosition);
2449
+ let node = undefined;
2450
+ let childNodes = [];
2451
+ let parentNode = undefined; // Needed to fix expandParent
2452
+ let parentExtent = undefined;
2453
+ let childExtent = undefined;
2309
2454
  const dragHandler = drag()
2310
2455
  .on('start', (event) => {
2311
2456
  const { nodeLookup, transform, snapGrid, snapToGrid } = getStoreItems();
2312
- const node = nodeLookup.get(nodeId);
2313
- const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
2314
- prevValues = {
2315
- width: node?.computed?.width ?? 0,
2316
- height: node?.computed?.height ?? 0,
2317
- x: node?.position.x ?? 0,
2318
- y: node?.position.y ?? 0,
2319
- };
2320
- startValues = {
2321
- ...prevValues,
2322
- pointerX: xSnapped,
2323
- pointerY: ySnapped,
2324
- aspectRatio: prevValues.width / prevValues.height,
2325
- };
2326
- onResizeStart?.(event, { ...prevValues });
2457
+ node = nodeLookup.get(nodeId);
2458
+ if (node) {
2459
+ const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
2460
+ prevValues = {
2461
+ width: node.computed?.width ?? 0,
2462
+ height: node.computed?.height ?? 0,
2463
+ x: node.position.x ?? 0,
2464
+ y: node.position.y ?? 0,
2465
+ };
2466
+ startValues = {
2467
+ ...prevValues,
2468
+ pointerX: xSnapped,
2469
+ pointerY: ySnapped,
2470
+ aspectRatio: prevValues.width / prevValues.height,
2471
+ };
2472
+ parentNode = undefined;
2473
+ if (node.extent === 'parent' || node.expandParent) {
2474
+ parentNode = nodeLookup.get(node.parentNode);
2475
+ if (parentNode && node.extent === 'parent') {
2476
+ parentExtent = nodeToParentExtent(parentNode);
2477
+ }
2478
+ }
2479
+ // Collect all child nodes to correct their relative positions when top/left changes
2480
+ // Determine largest minimal extent the parent node is allowed to resize to
2481
+ childNodes = [];
2482
+ childExtent = undefined;
2483
+ for (const [childId, child] of nodeLookup) {
2484
+ if (child.parentNode === nodeId) {
2485
+ childNodes.push({
2486
+ id: childId,
2487
+ position: { ...child.position },
2488
+ extent: child.extent,
2489
+ });
2490
+ if (child.extent === 'parent' || child.expandParent) {
2491
+ const extent = nodeToChildExtent(child, node);
2492
+ if (childExtent) {
2493
+ childExtent = [
2494
+ [Math.min(extent[0][0], childExtent[0][0]), Math.min(extent[0][1], childExtent[0][1])],
2495
+ [Math.max(extent[1][0], childExtent[1][0]), Math.max(extent[1][1], childExtent[1][1])],
2496
+ ];
2497
+ }
2498
+ else {
2499
+ childExtent = extent;
2500
+ }
2501
+ }
2502
+ }
2503
+ }
2504
+ onResizeStart?.(event, { ...prevValues });
2505
+ }
2327
2506
  })
2328
2507
  .on('drag', (event) => {
2329
- const { nodeLookup, transform, snapGrid, snapToGrid } = getStoreItems();
2508
+ const { transform, snapGrid, snapToGrid } = getStoreItems();
2330
2509
  const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
2331
- const node = nodeLookup.get(nodeId);
2510
+ const childChanges = [];
2332
2511
  if (node) {
2333
2512
  const change = { ...initChange };
2334
2513
  const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues;
2335
- const { width, height } = getDimensionsAfterResize(startValues, controlDirection, pointerPosition, boundaries, keepAspectRatio);
2514
+ const { width, height, x, y } = getDimensionsAfterResize(startValues, controlDirection, pointerPosition, boundaries, keepAspectRatio, parentExtent, childExtent);
2336
2515
  const isWidthChange = width !== prevWidth;
2337
2516
  const isHeightChange = height !== prevHeight;
2338
2517
  if (controlDirection.affectsX || controlDirection.affectsY) {
2339
- const { x, y } = getPositionAfterResize(startValues, controlDirection, width, height);
2340
- // only transform the node if the width or height changes
2341
2518
  const isXPosChange = x !== prevX && isWidthChange;
2342
2519
  const isYPosChange = y !== prevY && isHeightChange;
2343
2520
  if (isXPosChange || isYPosChange) {
@@ -2347,6 +2524,28 @@ function XYResizer({ domNode, nodeId, getStoreItems, onChange }) {
2347
2524
  change.y = isYPosChange ? y : prevY;
2348
2525
  prevValues.x = change.x;
2349
2526
  prevValues.y = change.y;
2527
+ // Fix expandParent when resizing from top/left
2528
+ if (parentNode && node.expandParent) {
2529
+ if (change.x < 0) {
2530
+ prevValues.x = 0;
2531
+ startValues.x = startValues.x - change.x;
2532
+ }
2533
+ if (change.y < 0) {
2534
+ prevValues.y = 0;
2535
+ startValues.y = startValues.y - change.y;
2536
+ }
2537
+ }
2538
+ }
2539
+ if (childNodes.length > 0) {
2540
+ const xChange = x - prevX;
2541
+ const yChange = y - prevY;
2542
+ for (const childNode of childNodes) {
2543
+ childNode.position = {
2544
+ x: childNode.position.x - xChange,
2545
+ y: childNode.position.y - yChange,
2546
+ };
2547
+ childChanges.push(childNode);
2548
+ }
2350
2549
  }
2351
2550
  }
2352
2551
  if (isWidthChange || isHeightChange) {
@@ -2354,8 +2553,8 @@ function XYResizer({ domNode, nodeId, getStoreItems, onChange }) {
2354
2553
  change.isHeightChange = isHeightChange;
2355
2554
  change.width = width;
2356
2555
  change.height = height;
2357
- prevValues.width = width;
2358
- prevValues.height = height;
2556
+ prevValues.width = change.width;
2557
+ prevValues.height = change.height;
2359
2558
  }
2360
2559
  if (!change.isXPosChange && !change.isYPosChange && !isWidthChange && !isHeightChange) {
2361
2560
  return;
@@ -2374,7 +2573,7 @@ function XYResizer({ domNode, nodeId, getStoreItems, onChange }) {
2374
2573
  return;
2375
2574
  }
2376
2575
  onResize?.(event, nextValues);
2377
- onChange(change);
2576
+ onChange(change, childChanges);
2378
2577
  }
2379
2578
  })
2380
2579
  .on('end', (event) => {
@@ -2391,4 +2590,4 @@ function XYResizer({ domNode, nodeId, getStoreItems, onChange }) {
2391
2590
  };
2392
2591
  }
2393
2592
 
2394
- export { ConnectionLineType, ConnectionMode, MarkerType, PanOnScrollMode, Position, ResizeControlVariant, SelectionMode, XYDrag, XYHandle, XYMinimap, XYPanZoom, XYResizer, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSITIONS, addEdge, adoptUserProvidedNodes, areConnectionMapsEqual, boxToRect, calcAutoPan, calcNextPosition, clamp, clampPosition, createMarkerIds, devWarn, elementSelectionKeys, errorMessages, fitView, getBezierEdgeCenter, getBezierPath, getBoundsOfBoxes, getBoundsOfRects, getConnectedEdges, getDimensions, getEdgeCenter, getEdgePosition, getElementsToRemove, getElevatedEdgeZIndex, getEventPosition, getHandleBounds, getHostForElement, getIncomers, getMarkerId, getNodePositionWithOrigin, getNodeToolbarTransform, getNodesBounds, getNodesInside, getOutgoers, 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, updateEdge, updateNodeDimensions };
2593
+ export { ConnectionLineType, ConnectionMode, MarkerType, PanOnScrollMode, Position, ResizeControlVariant, SelectionMode, XYDrag, XYHandle, XYMinimap, XYPanZoom, XYResizer, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSITIONS, addEdge, adoptUserProvidedNodes, areConnectionMapsEqual, boxToRect, calcAutoPan, calculateNodePosition, clamp, clampPosition, createMarkerIds, devWarn, elementSelectionKeys, errorMessages, fitView, getBezierEdgeCenter, getBezierPath, getBoundsOfBoxes, getBoundsOfRects, getConnectedEdges, getDimensions, getEdgeCenter, getEdgePosition, getElementsToRemove, getElevatedEdgeZIndex, getEventPosition, getHandleBounds, getHostForElement, getIncomers, getMarkerId, getNodePositionWithOrigin, getNodeToolbarTransform, getNodesBounds, getNodesInside, getOutgoers, getOverlappingArea, getPointerPosition, getPositionWithOrigin, getSmoothStepPath, getStraightPath, getViewportForBounds, handleConnectionChange, infiniteExtent, internalsSymbol, isCoordinateExtent, isEdgeBase, isEdgeVisible, isInputDOMNode, isMacOs, isMouseEvent, isNodeBase, isNumeric, isRectObject, nodeToBox, nodeToRect, panBy, pointToRendererPoint, rectToBox, rendererPointToPoint, snapPosition, updateAbsolutePositions, updateConnectionLookup, updateEdge, updateNodeDimensions };