@xyflow/system 0.0.41 → 0.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -192,19 +192,31 @@ const getNodePositionWithOrigin = (node, nodeOrigin = [0, 0]) => {
192
192
  };
193
193
  };
194
194
  /**
195
- * Determines a bounding box that contains all given nodes in an array
195
+ * Internal function for determining a bounding box that contains all given nodes in an array.
196
196
  * @public
197
197
  * @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport.
198
198
  * @param nodes - Nodes to calculate the bounds for
199
199
  * @param params.nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center
200
200
  * @returns Bounding box enclosing all nodes
201
201
  */
202
- const getNodesBounds = (nodes, params = { nodeOrigin: [0, 0] }) => {
202
+ const getNodesBounds = (nodes, params = { nodeOrigin: [0, 0], nodeLookup: undefined }) => {
203
+ if (process.env.NODE_ENV === 'development' && !params.nodeLookup) {
204
+ console.warn('Please use `getNodesBounds` from `useReactFlow`/`useSvelteFlow` hook to ensure correct values for sub flows. If not possible, you have to provide a nodeLookup to support sub flows.');
205
+ }
203
206
  if (nodes.length === 0) {
204
207
  return { x: 0, y: 0, width: 0, height: 0 };
205
208
  }
206
- const box = nodes.reduce((currBox, node) => {
207
- const nodeBox = nodeToBox(node, params.nodeOrigin);
209
+ const box = nodes.reduce((currBox, nodeOrId) => {
210
+ const isId = typeof nodeOrId === 'string';
211
+ let currentNode = !params.nodeLookup && !isId ? nodeOrId : undefined;
212
+ if (params.nodeLookup) {
213
+ currentNode = isId
214
+ ? params.nodeLookup.get(nodeOrId)
215
+ : !isInternalNodeBase(nodeOrId)
216
+ ? params.nodeLookup.get(nodeOrId.id)
217
+ : nodeOrId;
218
+ }
219
+ const nodeBox = currentNode ? nodeToBox(currentNode, params.nodeOrigin) : { x: 0, y: 0, x2: 0, y2: 0 };
208
220
  return getBoundsOfBoxes(currBox, nodeBox);
209
221
  }, { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity });
210
222
  return boxToRect(box);
@@ -286,20 +298,6 @@ async function fitView({ nodes, width, height, panZoom, minZoom, maxZoom }, opti
286
298
  await panZoom.setViewport(viewport, { duration: options?.duration });
287
299
  return Promise.resolve(true);
288
300
  }
289
- /**
290
- * This function clamps the passed extend by the node's width and height.
291
- * This is needed to prevent the node from being dragged outside of its extent.
292
- *
293
- * @param node
294
- * @param extent
295
- * @returns
296
- */
297
- function clampNodeExtent(node, extent) {
298
- if (!extent || extent === 'parent') {
299
- return extent;
300
- }
301
- return [extent[0], [extent[1][0] - (node.measured?.width ?? 0), extent[1][1] - (node.measured?.height ?? 0)]];
302
- }
303
301
  /**
304
302
  * This function calculates the next position of a node, taking into account the node's extent, parent node, and origin.
305
303
  *
@@ -311,36 +309,33 @@ function calculateNodePosition({ nodeId, nextPosition, nodeLookup, nodeOrigin =
311
309
  const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined;
312
310
  const { x: parentX, y: parentY } = parentNode ? parentNode.internals.positionAbsolute : { x: 0, y: 0 };
313
311
  const origin = node.origin ?? nodeOrigin;
314
- let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
312
+ let extent = nodeExtent;
315
313
  if (node.extent === 'parent' && !node.expandParent) {
316
314
  if (!parentNode) {
317
315
  onError?.('005', errorMessages['error005']());
318
316
  }
319
317
  else {
320
- const nodeWidth = node.measured.width;
321
- const nodeHeight = node.measured.height;
322
318
  const parentWidth = parentNode.measured.width;
323
319
  const parentHeight = parentNode.measured.height;
324
- if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
325
- currentExtent = [
320
+ if (parentWidth && parentHeight) {
321
+ extent = [
326
322
  [parentX, parentY],
327
- [parentX + parentWidth - nodeWidth, parentY + parentHeight - nodeHeight],
323
+ [parentX + parentWidth, parentY + parentHeight],
328
324
  ];
329
325
  }
330
326
  }
331
327
  }
332
328
  else if (parentNode && isCoordinateExtent(node.extent)) {
333
- currentExtent = [
329
+ extent = [
334
330
  [node.extent[0][0] + parentX, node.extent[0][1] + parentY],
335
331
  [node.extent[1][0] + parentX, node.extent[1][1] + parentY],
336
332
  ];
337
333
  }
338
- const positionAbsolute = isCoordinateExtent(currentExtent)
339
- ? clampPosition(nextPosition, currentExtent)
334
+ const positionAbsolute = isCoordinateExtent(extent)
335
+ ? clampPosition(nextPosition, extent, node.measured)
340
336
  : nextPosition;
341
337
  return {
342
338
  position: {
343
- // TODO: is there a better way to do this?
344
339
  x: positionAbsolute.x - parentX + node.measured.width * origin[0],
345
340
  y: positionAbsolute.y - parentY + node.measured.height * origin[1],
346
341
  },
@@ -397,10 +392,18 @@ async function getElementsToRemove({ nodesToRemove = [], edgesToRemove = [], nod
397
392
  }
398
393
 
399
394
  const clamp = (val, min = 0, max = 1) => Math.min(Math.max(val, min), max);
400
- const clampPosition = (position = { x: 0, y: 0 }, extent) => ({
401
- x: clamp(position.x, extent[0][0], extent[1][0]),
402
- y: clamp(position.y, extent[0][1], extent[1][1]),
395
+ const clampPosition = (position = { x: 0, y: 0 }, extent, dimensions) => ({
396
+ x: clamp(position.x, extent[0][0], extent[1][0] - (dimensions?.width ?? 0)),
397
+ y: clamp(position.y, extent[0][1], extent[1][1] - (dimensions?.height ?? 0)),
403
398
  });
399
+ function clampPositionToParent(childPosition, childDimensions, parent) {
400
+ const { width: parentWidth, height: parentHeight } = getNodeDimensions(parent);
401
+ const { x: parentX, y: parentY } = parent.internals.positionAbsolute;
402
+ return clampPosition(childPosition, [
403
+ [parentX, parentY],
404
+ [parentX + parentWidth, parentY + parentHeight],
405
+ ], childDimensions);
406
+ }
404
407
  /**
405
408
  * Calculates the velocity of panning when the mouse is close to the edge of the canvas
406
409
  * @internal
@@ -1182,6 +1185,7 @@ function getNodeToolbarTransform(nodeRect, viewport, position, offset, align) {
1182
1185
 
1183
1186
  const defaultOptions = {
1184
1187
  nodeOrigin: [0, 0],
1188
+ nodeExtent: infiniteExtent,
1185
1189
  elevateNodesOnSelect: true,
1186
1190
  defaults: {},
1187
1191
  };
@@ -1189,27 +1193,45 @@ const adoptUserNodesDefaultOptions = {
1189
1193
  ...defaultOptions,
1190
1194
  checkEquality: true,
1191
1195
  };
1196
+ function mergeObjects(base, incoming) {
1197
+ const result = { ...base };
1198
+ for (const key in incoming) {
1199
+ if (incoming[key] !== undefined) {
1200
+ // typecast is safe here, because we check for undefined
1201
+ result[key] = incoming[key];
1202
+ }
1203
+ }
1204
+ return result;
1205
+ }
1192
1206
  function updateAbsolutePositions(nodeLookup, parentLookup, options) {
1193
- const _options = { ...defaultOptions, ...options };
1207
+ const _options = mergeObjects(defaultOptions, options);
1194
1208
  for (const node of nodeLookup.values()) {
1195
- if (!node.parentId) {
1196
- continue;
1209
+ if (node.parentId) {
1210
+ updateChildNode(node, nodeLookup, parentLookup, _options);
1211
+ }
1212
+ else {
1213
+ const positionWithOrigin = getNodePositionWithOrigin(node, _options.nodeOrigin);
1214
+ const extent = isCoordinateExtent(node.extent) ? node.extent : _options.nodeExtent;
1215
+ const clampedPosition = clampPosition(positionWithOrigin, extent, getNodeDimensions(node));
1216
+ node.internals.positionAbsolute = clampedPosition;
1197
1217
  }
1198
- updateChildPosition(node, nodeLookup, parentLookup, _options);
1199
1218
  }
1200
1219
  }
1201
1220
  function adoptUserNodes(nodes, nodeLookup, parentLookup, options) {
1202
- const _options = { ...adoptUserNodesDefaultOptions, ...options };
1221
+ const _options = mergeObjects(adoptUserNodesDefaultOptions, options);
1203
1222
  const tmpLookup = new Map(nodeLookup);
1223
+ const selectedNodeZ = _options?.elevateNodesOnSelect ? 1000 : 0;
1204
1224
  nodeLookup.clear();
1205
1225
  parentLookup.clear();
1206
- const selectedNodeZ = options?.elevateNodesOnSelect ? 1000 : 0;
1207
1226
  for (const userNode of nodes) {
1208
1227
  let internalNode = tmpLookup.get(userNode.id);
1209
1228
  if (_options.checkEquality && userNode === internalNode?.internals.userNode) {
1210
1229
  nodeLookup.set(userNode.id, internalNode);
1211
1230
  }
1212
1231
  else {
1232
+ const positionWithOrigin = getNodePositionWithOrigin(userNode, _options.nodeOrigin);
1233
+ const extent = isCoordinateExtent(userNode.extent) ? userNode.extent : _options.nodeExtent;
1234
+ const clampedPosition = clampPosition(positionWithOrigin, extent, getNodeDimensions(userNode));
1213
1235
  internalNode = {
1214
1236
  ..._options.defaults,
1215
1237
  ...userNode,
@@ -1218,7 +1240,7 @@ function adoptUserNodes(nodes, nodeLookup, parentLookup, options) {
1218
1240
  height: userNode.measured?.height,
1219
1241
  },
1220
1242
  internals: {
1221
- positionAbsolute: getNodePositionWithOrigin(userNode, _options.nodeOrigin),
1243
+ positionAbsolute: clampedPosition,
1222
1244
  // if user re-initializes the node or removes `measured` for whatever reason, we reset the handleBounds so that the node gets re-measured
1223
1245
  handleBounds: !userNode.measured ? undefined : internalNode?.internals.handleBounds,
1224
1246
  z: calculateZ(userNode, selectedNodeZ),
@@ -1228,34 +1250,42 @@ function adoptUserNodes(nodes, nodeLookup, parentLookup, options) {
1228
1250
  nodeLookup.set(userNode.id, internalNode);
1229
1251
  }
1230
1252
  if (userNode.parentId) {
1231
- updateChildPosition(internalNode, nodeLookup, parentLookup, options);
1253
+ updateChildNode(internalNode, nodeLookup, parentLookup, options);
1232
1254
  }
1233
1255
  }
1234
1256
  }
1235
- function updateChildPosition(node, nodeLookup, parentLookup, options) {
1236
- const _options = { ...defaultOptions, ...options };
1237
- const parentId = node.parentId;
1238
- const parentNode = nodeLookup.get(parentId);
1239
- if (!parentNode) {
1240
- console.warn(`Parent node ${parentId} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`);
1257
+ function updateParentLookup(node, parentLookup) {
1258
+ if (!node.parentId) {
1241
1259
  return;
1242
1260
  }
1243
- // update the parentLookup
1244
- const childNodes = parentLookup.get(parentId);
1261
+ const childNodes = parentLookup.get(node.parentId);
1245
1262
  if (childNodes) {
1246
1263
  childNodes.set(node.id, node);
1247
1264
  }
1248
1265
  else {
1249
- parentLookup.set(parentId, new Map([[node.id, node]]));
1266
+ parentLookup.set(node.parentId, new Map([[node.id, node]]));
1267
+ }
1268
+ }
1269
+ /**
1270
+ * Updates positionAbsolute and zIndex of a child node and the parentLookup.
1271
+ */
1272
+ function updateChildNode(node, nodeLookup, parentLookup, options) {
1273
+ const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options);
1274
+ const parentId = node.parentId;
1275
+ const parentNode = nodeLookup.get(parentId);
1276
+ if (!parentNode) {
1277
+ console.warn(`Parent node ${parentId} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`);
1278
+ return;
1250
1279
  }
1251
- const selectedNodeZ = options?.elevateNodesOnSelect ? 1000 : 0;
1252
- const { x, y, z } = calculateChildXYZ(node, parentNode, _options.nodeOrigin, selectedNodeZ);
1253
- const currPosition = node.internals.positionAbsolute;
1254
- const positionChanged = x !== currPosition.x || y !== currPosition.y;
1280
+ updateParentLookup(node, parentLookup);
1281
+ const selectedNodeZ = elevateNodesOnSelect ? 1000 : 0;
1282
+ const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ);
1283
+ const { positionAbsolute } = node.internals;
1284
+ const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y;
1255
1285
  if (positionChanged || z !== node.internals.z) {
1256
1286
  node.internals = {
1257
1287
  ...node.internals,
1258
- positionAbsolute: positionChanged ? { x, y } : currPosition,
1288
+ positionAbsolute: positionChanged ? { x, y } : positionAbsolute,
1259
1289
  z,
1260
1290
  };
1261
1291
  }
@@ -1263,13 +1293,22 @@ function updateChildPosition(node, nodeLookup, parentLookup, options) {
1263
1293
  function calculateZ(node, selectedNodeZ) {
1264
1294
  return (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0);
1265
1295
  }
1266
- function calculateChildXYZ(childNode, parentNode, nodeOrigin, selectedNodeZ) {
1267
- const position = getNodePositionWithOrigin(childNode, nodeOrigin);
1296
+ function calculateChildXYZ(childNode, parentNode, nodeOrigin, nodeExtent, selectedNodeZ) {
1297
+ const { x: parentX, y: parentY } = parentNode.internals.positionAbsolute;
1298
+ const childDimensions = getNodeDimensions(childNode);
1299
+ const positionWithOrigin = getNodePositionWithOrigin(childNode, nodeOrigin);
1300
+ const clampedPosition = isCoordinateExtent(childNode.extent)
1301
+ ? clampPosition(positionWithOrigin, childNode.extent, childDimensions)
1302
+ : positionWithOrigin;
1303
+ let absolutePosition = clampPosition({ x: parentX + clampedPosition.x, y: parentY + clampedPosition.y }, nodeExtent, childDimensions);
1304
+ if (childNode.extent === 'parent') {
1305
+ absolutePosition = clampPositionToParent(absolutePosition, childDimensions, parentNode);
1306
+ }
1268
1307
  const childZ = calculateZ(childNode, selectedNodeZ);
1269
1308
  const parentZ = parentNode.internals.z ?? 0;
1270
1309
  return {
1271
- x: parentNode.internals.positionAbsolute.x + position.x,
1272
- y: parentNode.internals.positionAbsolute.y + position.y,
1310
+ x: absolutePosition.x,
1311
+ y: absolutePosition.y,
1273
1312
  z: parentZ > childZ ? parentZ : childZ,
1274
1313
  };
1275
1314
  }
@@ -1340,7 +1379,7 @@ function handleExpandParent(children, nodeLookup, parentLookup, nodeOrigin = [0,
1340
1379
  }
1341
1380
  return changes;
1342
1381
  }
1343
- function updateNodeInternals(updates, nodeLookup, parentLookup, domNode, nodeOrigin) {
1382
+ function updateNodeInternals(updates, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent) {
1344
1383
  const viewportNode = domNode?.querySelector('.xyflow__viewport');
1345
1384
  let updatedInternals = false;
1346
1385
  if (!viewportNode) {
@@ -1371,17 +1410,25 @@ function updateNodeInternals(updates, nodeLookup, parentLookup, domNode, nodeOri
1371
1410
  (dimensionChanged || !node.internals.handleBounds || update.force));
1372
1411
  if (doUpdate) {
1373
1412
  const nodeBounds = update.nodeElement.getBoundingClientRect();
1413
+ const extent = isCoordinateExtent(node.extent) ? node.extent : nodeExtent;
1414
+ let { positionAbsolute } = node.internals;
1415
+ if (node.parentId && node.extent === 'parent') {
1416
+ positionAbsolute = clampPositionToParent(positionAbsolute, dimensions, nodeLookup.get(node.parentId));
1417
+ }
1418
+ else if (extent) {
1419
+ positionAbsolute = clampPosition(positionAbsolute, extent, dimensions);
1420
+ }
1374
1421
  node.measured = dimensions;
1375
1422
  node.internals = {
1376
1423
  ...node.internals,
1377
- positionAbsolute: getNodePositionWithOrigin(node, nodeOrigin),
1424
+ positionAbsolute,
1378
1425
  handleBounds: {
1379
1426
  source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id),
1380
1427
  target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id),
1381
1428
  },
1382
1429
  };
1383
1430
  if (node.parentId) {
1384
- updateChildPosition(node, nodeLookup, parentLookup, { nodeOrigin });
1431
+ updateChildNode(node, nodeLookup, parentLookup, { nodeOrigin });
1385
1432
  }
1386
1433
  updatedInternals = true;
1387
1434
  if (dimensionChanged) {
@@ -1566,6 +1613,11 @@ function XYDrag({ onNodeMouseDown, getStoreItems, onDragStart, onDrag, onDragSto
1566
1613
  nodesBox = rectToBox(rect);
1567
1614
  }
1568
1615
  for (const [id, dragItem] of dragItems) {
1616
+ if (!nodeLookup.has(id)) {
1617
+ // if the node is not in the nodeLookup anymore, it was probably deleted while dragging
1618
+ // and we don't need to update it anymore
1619
+ continue;
1620
+ }
1569
1621
  let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y };
1570
1622
  if (snapToGrid) {
1571
1623
  nextPosition = snapPosition(nextPosition, snapGrid);
@@ -1674,9 +1726,11 @@ function XYDrag({ onNodeMouseDown, getStoreItems, onDragStart, onDrag, onDragSto
1674
1726
  mousePosition = getEventPosition(event.sourceEvent, containerBounds);
1675
1727
  })
1676
1728
  .on('drag', (event) => {
1677
- const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold } = getStoreItems();
1729
+ const { autoPanOnNodeDrag, transform, snapGrid, snapToGrid, nodeDragThreshold, nodeLookup } = getStoreItems();
1678
1730
  const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
1679
- if (event.sourceEvent.type === 'touchmove' && event.sourceEvent.touches.length > 1) {
1731
+ if ((event.sourceEvent.type === 'touchmove' && event.sourceEvent.touches.length > 1) ||
1732
+ // if user deletes a node while dragging, we need to abort the drag to prevent errors
1733
+ (nodeId && !nodeLookup.has(nodeId))) {
1680
1734
  abortDrag = true;
1681
1735
  }
1682
1736
  if (abortDrag) {
@@ -2874,4 +2928,4 @@ function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }) {
2874
2928
  };
2875
2929
  }
2876
2930
 
2877
- export { ConnectionLineType, ConnectionMode, MarkerType, PanOnScrollMode, Position, ResizeControlVariant, SelectionMode, XYDrag, XYHandle, XYMinimap, XYPanZoom, XYResizer, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSITIONS, addEdge, adoptUserNodes, areConnectionMapsEqual, boxToRect, calcAutoPan, calculateNodePosition, clamp, clampPosition, createMarkerIds, devWarn, elementSelectionKeys, errorMessages, evaluateAbsolutePosition, fitView, getBezierEdgeCenter, getBezierPath, getBoundsOfBoxes, getBoundsOfRects, getConnectedEdges, getConnectionStatus, getDimensions, getEdgeCenter, getEdgePosition, getElementsToRemove, getElevatedEdgeZIndex, getEventPosition, getFitViewNodes, getHandleBounds, getHandlePosition, getHostForElement, getIncomers, getInternalNodesBounds, getMarkerId, getNodeDimensions, getNodePositionWithOrigin, getNodeToolbarTransform, getNodesBounds, getNodesInside, getOutgoers, getOverlappingArea, getPointerPosition, getSmoothStepPath, getStraightPath, getViewportForBounds, handleConnectionChange, handleExpandParent, infiniteExtent, initialConnection, isCoordinateExtent, isEdgeBase, isEdgeVisible, isInputDOMNode, isInternalNodeBase, isMacOs, isMouseEvent, isNodeBase, isNumeric, isRectObject, nodeHasDimensions, nodeToBox, nodeToRect, oppositePosition, panBy, pointToRendererPoint, reconnectEdge, rectToBox, rendererPointToPoint, shallowNodeData, snapPosition, updateAbsolutePositions, updateConnectionLookup, updateNodeInternals };
2931
+ export { ConnectionLineType, ConnectionMode, MarkerType, PanOnScrollMode, Position, ResizeControlVariant, SelectionMode, XYDrag, XYHandle, XYMinimap, XYPanZoom, XYResizer, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSITIONS, addEdge, adoptUserNodes, areConnectionMapsEqual, boxToRect, calcAutoPan, calculateNodePosition, clamp, clampPosition, clampPositionToParent, createMarkerIds, devWarn, elementSelectionKeys, errorMessages, evaluateAbsolutePosition, fitView, getBezierEdgeCenter, getBezierPath, getBoundsOfBoxes, getBoundsOfRects, getConnectedEdges, getConnectionStatus, getDimensions, getEdgeCenter, getEdgePosition, getElementsToRemove, getElevatedEdgeZIndex, getEventPosition, getFitViewNodes, getHandleBounds, getHandlePosition, getHostForElement, getIncomers, getInternalNodesBounds, getMarkerId, getNodeDimensions, getNodePositionWithOrigin, getNodeToolbarTransform, getNodesBounds, getNodesInside, getOutgoers, getOverlappingArea, getPointerPosition, getSmoothStepPath, getStraightPath, getViewportForBounds, handleConnectionChange, handleExpandParent, infiniteExtent, initialConnection, isCoordinateExtent, isEdgeBase, isEdgeVisible, isInputDOMNode, isInternalNodeBase, isMacOs, isMouseEvent, isNodeBase, isNumeric, isRectObject, nodeHasDimensions, nodeToBox, nodeToRect, oppositePosition, panBy, pointToRendererPoint, reconnectEdge, rectToBox, rendererPointToPoint, shallowNodeData, snapPosition, updateAbsolutePositions, updateConnectionLookup, updateNodeInternals };