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