@vanduo-oss/vd3-flowchart 1.3.0 → 1.4.0
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/CHANGELOG.md +66 -0
- package/README.md +47 -5
- package/SKILL.md +23 -3
- package/dist/core.d.ts +91 -2
- package/dist/index.cjs +1561 -152
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1561 -152
- package/dist/index.js.map +4 -4
- package/dist/meta.json +29 -12
- package/dist/vd3-flowchart.css +190 -12
- package/dist/vue.d.ts +9 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -185,6 +185,134 @@ function computeLayout(documentData, mode = "tree", options = {}) {
|
|
|
185
185
|
}
|
|
186
186
|
var LAYOUT_MODES = ["tree", "radial", "grid"];
|
|
187
187
|
|
|
188
|
+
// src/mindmap.js
|
|
189
|
+
var DIRECTION_PORTS = { right: "right", left: "left", down: "bottom", up: "top" };
|
|
190
|
+
var PORT_DIRECTIONS = { right: "right", left: "left", bottom: "down", top: "up" };
|
|
191
|
+
var OPPOSITE_DIRECTIONS = { right: "left", left: "right", down: "up", up: "down" };
|
|
192
|
+
var DIRECTION_VECTORS = {
|
|
193
|
+
right: { x: 1, y: 0 },
|
|
194
|
+
left: { x: -1, y: 0 },
|
|
195
|
+
down: { x: 0, y: 1 },
|
|
196
|
+
up: { x: 0, y: -1 }
|
|
197
|
+
};
|
|
198
|
+
var BRANCH_GAP = 72;
|
|
199
|
+
var SIBLING_GAP = 24;
|
|
200
|
+
var COLLISION_GAP = 12;
|
|
201
|
+
var COLLISION_STEP = 12;
|
|
202
|
+
var MAX_COLLISION_STEPS = 400;
|
|
203
|
+
var BRANCH_RING_STEP = 48;
|
|
204
|
+
var MAX_BRANCH_RINGS = 8;
|
|
205
|
+
var NAVIGATION_CONE = Math.tan(Math.PI / 3);
|
|
206
|
+
function centerOf(node) {
|
|
207
|
+
return { x: node.x + node.width / 2, y: node.y + node.height / 2 };
|
|
208
|
+
}
|
|
209
|
+
function oppositeDirection(direction) {
|
|
210
|
+
return OPPOSITE_DIRECTIONS[direction] || "left";
|
|
211
|
+
}
|
|
212
|
+
function findSpatialNeighbor(nodes, fromNode, direction) {
|
|
213
|
+
const vector = DIRECTION_VECTORS[direction];
|
|
214
|
+
if (!vector || !fromNode) return null;
|
|
215
|
+
const origin = centerOf(fromNode);
|
|
216
|
+
let bestCone = null;
|
|
217
|
+
let bestPlane = null;
|
|
218
|
+
nodes.forEach((node) => {
|
|
219
|
+
if (node.id === fromNode.id) return;
|
|
220
|
+
const center = centerOf(node);
|
|
221
|
+
const dx = center.x - origin.x;
|
|
222
|
+
const dy = center.y - origin.y;
|
|
223
|
+
const along = dx * vector.x + dy * vector.y;
|
|
224
|
+
if (along <= 0) return;
|
|
225
|
+
const across = Math.abs(dx * vector.y - dy * vector.x);
|
|
226
|
+
const score = along + across * 2;
|
|
227
|
+
if (across <= along * NAVIGATION_CONE && (!bestCone || score < bestCone.score)) {
|
|
228
|
+
bestCone = { node, score };
|
|
229
|
+
}
|
|
230
|
+
if (!bestPlane || score < bestPlane.score) bestPlane = { node, score };
|
|
231
|
+
});
|
|
232
|
+
return (bestCone || bestPlane)?.node ?? null;
|
|
233
|
+
}
|
|
234
|
+
function findNearestNode(nodes, point) {
|
|
235
|
+
let best = null;
|
|
236
|
+
nodes.forEach((node) => {
|
|
237
|
+
const center = centerOf(node);
|
|
238
|
+
const distance = Math.hypot(center.x - point.x, center.y - point.y);
|
|
239
|
+
if (!best || distance < best.distance) best = { node, distance };
|
|
240
|
+
});
|
|
241
|
+
return best?.node ?? null;
|
|
242
|
+
}
|
|
243
|
+
function overlapsAny(rect, nodes) {
|
|
244
|
+
return nodes.some(
|
|
245
|
+
(node) => rect.x < node.x + node.width + COLLISION_GAP && rect.x + rect.width + COLLISION_GAP > node.x && rect.y < node.y + node.height + COLLISION_GAP && rect.y + rect.height + COLLISION_GAP > node.y
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
function placeBranchNode({
|
|
249
|
+
nodes,
|
|
250
|
+
parent,
|
|
251
|
+
siblings = [],
|
|
252
|
+
direction,
|
|
253
|
+
size,
|
|
254
|
+
anchor = null,
|
|
255
|
+
before = false
|
|
256
|
+
}) {
|
|
257
|
+
const horizontal = direction === "right" || direction === "left";
|
|
258
|
+
const cross = horizontal ? "y" : "x";
|
|
259
|
+
const crossSize = horizontal ? "height" : "width";
|
|
260
|
+
const lastSibling = siblings.reduce(
|
|
261
|
+
(last, node) => !last || node[cross] + node[crossSize] > last[cross] + last[crossSize] ? node : last,
|
|
262
|
+
null
|
|
263
|
+
);
|
|
264
|
+
const reference = anchor || lastSibling;
|
|
265
|
+
const rect = { x: 0, y: 0, width: size.width, height: size.height };
|
|
266
|
+
if (reference) {
|
|
267
|
+
if (direction === "right") rect.x = reference.x;
|
|
268
|
+
if (direction === "left") rect.x = reference.x + reference.width - size.width;
|
|
269
|
+
if (direction === "down") rect.y = reference.y;
|
|
270
|
+
if (direction === "up") rect.y = reference.y + reference.height - size.height;
|
|
271
|
+
rect[cross] = before ? reference[cross] - SIBLING_GAP - size[crossSize] : reference[cross] + reference[crossSize] + SIBLING_GAP;
|
|
272
|
+
} else {
|
|
273
|
+
if (direction === "right") rect.x = parent.x + parent.width + BRANCH_GAP;
|
|
274
|
+
if (direction === "left") rect.x = parent.x - BRANCH_GAP - size.width;
|
|
275
|
+
if (direction === "down") rect.y = parent.y + parent.height + BRANCH_GAP;
|
|
276
|
+
if (direction === "up") rect.y = parent.y - BRANCH_GAP - size.height;
|
|
277
|
+
rect[cross] = parent[cross] + parent[crossSize] / 2 - size[crossSize] / 2;
|
|
278
|
+
}
|
|
279
|
+
const crossSign = reference ? before ? -1 : 1 : 0;
|
|
280
|
+
const result = nearestFreeSlot(
|
|
281
|
+
rect,
|
|
282
|
+
nodes,
|
|
283
|
+
cross,
|
|
284
|
+
horizontal ? "x" : "y",
|
|
285
|
+
DIRECTION_VECTORS[direction],
|
|
286
|
+
crossSign
|
|
287
|
+
);
|
|
288
|
+
return { x: Math.round(result.x * 100) / 100, y: Math.round(result.y * 100) / 100 };
|
|
289
|
+
}
|
|
290
|
+
function crossOffsetAt(index, crossSign) {
|
|
291
|
+
if (crossSign) return crossSign * COLLISION_STEP * index;
|
|
292
|
+
return COLLISION_STEP * Math.ceil(index / 2) * (index % 2 ? 1 : -1);
|
|
293
|
+
}
|
|
294
|
+
function nearestFreeSlot(rect, nodes, cross, main, vector, crossSign) {
|
|
295
|
+
const origin = { ...rect };
|
|
296
|
+
const outward = vector.x + vector.y;
|
|
297
|
+
let best = null;
|
|
298
|
+
for (let ring = 0; ring <= MAX_BRANCH_RINGS; ring += 1) {
|
|
299
|
+
const mainOffset = ring * BRANCH_RING_STEP;
|
|
300
|
+
if (best && mainOffset >= best.cost) break;
|
|
301
|
+
const candidate = { ...origin, [main]: origin[main] + outward * mainOffset };
|
|
302
|
+
for (let index = 0; index <= MAX_COLLISION_STEPS; index += 1) {
|
|
303
|
+
const crossOffset = crossOffsetAt(index, crossSign);
|
|
304
|
+
const cost = Math.hypot(mainOffset, crossOffset);
|
|
305
|
+
if (best && cost >= best.cost) break;
|
|
306
|
+
candidate[cross] = origin[cross] + crossOffset;
|
|
307
|
+
if (!overlapsAny(candidate, nodes)) {
|
|
308
|
+
best = { rect: { ...candidate }, cost };
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return best ? best.rect : origin;
|
|
314
|
+
}
|
|
315
|
+
|
|
188
316
|
// src/core.js
|
|
189
317
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
190
318
|
var TOOLBAR_ICON_PATHS = {
|
|
@@ -194,6 +322,7 @@ var TOOLBAR_ICON_PATHS = {
|
|
|
194
322
|
"fit-view": "M216,48V88a8,8,0,0,1-16,0V67.31l-42.34,42.35a8,8,0,0,1-11.32-11.32L188.69,56H168a8,8,0,0,1,0-16h40A8,8,0,0,1,216,48ZM98.34,146.34,56,188.69V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H88a8,8,0,0,0,0-16H67.31l42.35-42.34a8,8,0,0,0-11.32-11.32Zm11.32-36.68L67.31,56H88a8,8,0,0,0,0-16H48a8,8,0,0,0-8,8V88a8,8,0,0,0,16,0V67.31l42.34,42.35a8,8,0,0,0,11.32-11.32ZM208,160a8,8,0,0,0-8,8v20.69l-42.34-42.35a8,8,0,0,0-11.32,11.32L188.69,200H168a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,208,160Z",
|
|
195
323
|
undo: "M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z",
|
|
196
324
|
redo: "M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z",
|
|
325
|
+
minimap: "M228.92,49.69a8,8,0,0,0-6.86-1.45L160.93,63.52,99.58,32.84a8,8,0,0,0-5.52-.6l-64,16A8,8,0,0,0,24,56V200a8,8,0,0,0,9.94,7.76l61.13-15.28,61.35,30.68A8.15,8.15,0,0,0,160,224a8,8,0,0,0,1.94-.24l64-16A8,8,0,0,0,232,200V56A8,8,0,0,0,228.92,49.69ZM104,52.94l48,24V203.06l-48-24ZM40,62.25l48-12v127.5l-48,12Zm176,131.5-48,12V78.25l48-12Z",
|
|
197
326
|
clear: "M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"
|
|
198
327
|
};
|
|
199
328
|
var TOOLBAR_ACTIONS = [
|
|
@@ -245,21 +374,32 @@ var DEFAULT_EDGE_STROKE_WIDTH = 2.25;
|
|
|
245
374
|
var MIN_EDGE_STROKE_WIDTH = 1.25;
|
|
246
375
|
var MAX_EDGE_STROKE_WIDTH = 6;
|
|
247
376
|
var CONNECTION_PORT_RADIUS = 6;
|
|
377
|
+
var PORT_HANDLE_OFFSET = 12;
|
|
248
378
|
var CONNECTION_PORT_HIT_RADIUS = 14;
|
|
379
|
+
var PORT_CLICK_SLOP = 4;
|
|
380
|
+
var RESIZE_HANDLE_SIZE = 9;
|
|
381
|
+
var SELECTION_OUTSET = 4;
|
|
382
|
+
var GUIDE_SNAP_DISTANCE = 6;
|
|
383
|
+
var GUIDE_OVERHANG = 12;
|
|
384
|
+
var COLLAPSE_TOGGLE_OFFSET = 36;
|
|
385
|
+
var MINIMAP_WIDTH = 180;
|
|
386
|
+
var MINIMAP_HEIGHT = 120;
|
|
387
|
+
var MINIMAP_MIN_CANVAS_WIDTH = 480;
|
|
388
|
+
var SELECTION_BOX_TYPES = /* @__PURE__ */ new Set(["circle", "diamond", "label"]);
|
|
249
389
|
var RECONNECT_ENDPOINT_RADIUS = 7;
|
|
250
390
|
var RECONNECT_ENDPOINT_HIT_RADIUS = 12;
|
|
251
391
|
var EDGE_HIT_STROKE_MIN = 16;
|
|
252
392
|
var CONNECTION_SNAP_PADDING = 36;
|
|
253
393
|
var CONNECTION_HYSTERESIS = 16;
|
|
254
394
|
var CONNECTION_CENTER_LOCK_RADIUS = 18;
|
|
255
|
-
var RESIZE_PORT_GAP =
|
|
395
|
+
var RESIZE_PORT_GAP = 28;
|
|
256
396
|
var EDGE_STROKE_PRESETS = [
|
|
257
397
|
{ id: "thin", label: "Thin", width: 1.75 },
|
|
258
398
|
{ id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
|
|
259
399
|
{ id: "bold", label: "Bold", width: 3.5 }
|
|
260
400
|
];
|
|
261
|
-
var VD_FLOWCHART_VERSION = "1.
|
|
262
|
-
var FLOWCHART_DOCUMENT_VERSION = "1.
|
|
401
|
+
var VD_FLOWCHART_VERSION = "1.4.0";
|
|
402
|
+
var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
|
|
263
403
|
var FLOWCHART_NODE_TYPES = [
|
|
264
404
|
"rounded-rect",
|
|
265
405
|
"rect",
|
|
@@ -272,7 +412,65 @@ var FLOWCHART_NODE_TYPES = [
|
|
|
272
412
|
var FLOWCHART_PORTS = ["top", "right", "bottom", "left"];
|
|
273
413
|
var FLOWCHART_EDGE_MARKERS = ["none", "arrow", "dot"];
|
|
274
414
|
var FLOWCHART_EDGE_ROUTES = ["curve", "straight", "orthogonal"];
|
|
275
|
-
var
|
|
415
|
+
var FLOWCHART_KEYBOARD_SHORTCUTS = ["mindmap", "basic"];
|
|
416
|
+
function normalizeKeyboardShortcuts(value) {
|
|
417
|
+
return value === "basic" ? "basic" : "mindmap";
|
|
418
|
+
}
|
|
419
|
+
var ARROW_DIRECTIONS = {
|
|
420
|
+
ArrowRight: "right",
|
|
421
|
+
ArrowLeft: "left",
|
|
422
|
+
ArrowDown: "down",
|
|
423
|
+
ArrowUp: "up"
|
|
424
|
+
};
|
|
425
|
+
function getShortcutRows(mode, readonly) {
|
|
426
|
+
const navigation = [
|
|
427
|
+
["Arrow keys", "Select the nearest node in that direction"],
|
|
428
|
+
["Shift + Arrow", "Select a connection on that side (repeat to cycle)"],
|
|
429
|
+
["Shift + click, Shift + drag", "Add nodes to the selection, or select with a box"],
|
|
430
|
+
["Cmd/Ctrl + A", "Select all nodes"]
|
|
431
|
+
];
|
|
432
|
+
const view = [
|
|
433
|
+
["Cmd/Ctrl + = or -", "Zoom in or out"],
|
|
434
|
+
["Cmd/Ctrl + 0", "Zoom to 100%"],
|
|
435
|
+
["Shift + 1", "Fit the diagram to the view"]
|
|
436
|
+
];
|
|
437
|
+
const help = [["?", "Show or hide this list"]];
|
|
438
|
+
if (readonly) {
|
|
439
|
+
return [...navigation, ...view, ...help, ["Tab", "Leave the canvas"]];
|
|
440
|
+
}
|
|
441
|
+
const mindmap = mode === "mindmap" ? [
|
|
442
|
+
["Tab", "Add a child node"],
|
|
443
|
+
["Enter", "Add a sibling below (Shift + Enter: above)"],
|
|
444
|
+
["F2, Space, or type", "Edit the label"],
|
|
445
|
+
["Enter while editing", "Save the label (Shift + Enter: new line)"],
|
|
446
|
+
["Tab while editing", "Save and add a child"]
|
|
447
|
+
] : [
|
|
448
|
+
["Enter or F2", "Edit the label"],
|
|
449
|
+
["Cmd/Ctrl + Enter", "Save the label"]
|
|
450
|
+
];
|
|
451
|
+
return [
|
|
452
|
+
...navigation,
|
|
453
|
+
...mindmap,
|
|
454
|
+
[
|
|
455
|
+
mode === "mindmap" ? "Enter, F2, or type on a connection" : "Enter or F2 on a connection",
|
|
456
|
+
"Edit the connection label"
|
|
457
|
+
],
|
|
458
|
+
["Cmd/Ctrl + /", "Collapse or expand the branch"],
|
|
459
|
+
["Alt + Arrow", "Nudge the node (add Shift for 1 px)"],
|
|
460
|
+
["Delete or Backspace", "Delete the selection"],
|
|
461
|
+
["Cmd/Ctrl + D", "Duplicate the node"],
|
|
462
|
+
["Cmd/Ctrl + Z, Shift + Cmd/Ctrl + Z", "Undo, redo"],
|
|
463
|
+
["Cmd/Ctrl + C, X, V", "Copy, cut, paste"],
|
|
464
|
+
...view,
|
|
465
|
+
["Esc", "Cancel editing, then the tool, then deselect"],
|
|
466
|
+
[
|
|
467
|
+
mode === "mindmap" ? "Shift + Tab, or Esc then Tab" : "Tab or Shift + Tab",
|
|
468
|
+
"Leave the canvas"
|
|
469
|
+
],
|
|
470
|
+
...help
|
|
471
|
+
];
|
|
472
|
+
}
|
|
473
|
+
var COALESCING_REASONS = /* @__PURE__ */ new Set(["node:update", "edge:update", "node:nudge"]);
|
|
276
474
|
var DEFAULT_EDGE_ROUTE = "curve";
|
|
277
475
|
var ORTHOGONAL_STUB_LENGTH = 32;
|
|
278
476
|
var ORTHOGONAL_CORNER_RADIUS = 12;
|
|
@@ -446,7 +644,7 @@ function normalizeViewport(viewport) {
|
|
|
446
644
|
function normalizeNode(rawNode, index, usedIds) {
|
|
447
645
|
const type = normalizeNodeType(rawNode?.type);
|
|
448
646
|
const spec = getNodeSpec(type);
|
|
449
|
-
|
|
647
|
+
const node = {
|
|
450
648
|
id: ensureUniqueId(rawNode?.id, "node", usedIds),
|
|
451
649
|
type,
|
|
452
650
|
x: toFiniteNumber(rawNode?.x, index * 28),
|
|
@@ -456,6 +654,37 @@ function normalizeNode(rawNode, index, usedIds) {
|
|
|
456
654
|
text: rawNode?.text == null ? spec.text : String(rawNode.text),
|
|
457
655
|
data: isPlainObject(rawNode?.data) ? deepClone(rawNode.data) : {}
|
|
458
656
|
};
|
|
657
|
+
if (rawNode?.collapsed === true) node.collapsed = true;
|
|
658
|
+
return node;
|
|
659
|
+
}
|
|
660
|
+
function computeHiddenNodeIds(nodes, edges) {
|
|
661
|
+
const collapsed = new Set(nodes.filter((node) => node.collapsed).map((node) => node.id));
|
|
662
|
+
if (!collapsed.size) return /* @__PURE__ */ new Set();
|
|
663
|
+
const outgoing = new Map(nodes.map((node) => [node.id, []]));
|
|
664
|
+
edges.forEach((edge) => {
|
|
665
|
+
if (edge.from.nodeId === edge.to.nodeId) return;
|
|
666
|
+
outgoing.get(edge.from.nodeId)?.push(edge.to.nodeId);
|
|
667
|
+
});
|
|
668
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
669
|
+
collapsed.forEach((rootId) => {
|
|
670
|
+
const queue2 = [...outgoing.get(rootId) || []];
|
|
671
|
+
for (let index = 0; index < queue2.length; index += 1) {
|
|
672
|
+
const id = queue2[index];
|
|
673
|
+
if (id === rootId || candidates.has(id)) continue;
|
|
674
|
+
candidates.add(id);
|
|
675
|
+
queue2.push(...outgoing.get(id) || []);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
const queue = nodes.filter((node) => !candidates.has(node.id) && !collapsed.has(node.id)).map((node) => node.id);
|
|
679
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
680
|
+
const id = queue[index];
|
|
681
|
+
(outgoing.get(id) || []).forEach((childId) => {
|
|
682
|
+
if (!candidates.has(childId)) return;
|
|
683
|
+
candidates.delete(childId);
|
|
684
|
+
if (!collapsed.has(childId)) queue.push(childId);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return candidates;
|
|
459
688
|
}
|
|
460
689
|
function normalizeEndpoint(rawEndpoint, fallbackPort) {
|
|
461
690
|
return {
|
|
@@ -608,6 +837,15 @@ function getPortPosition(node, port) {
|
|
|
608
837
|
return { x: node.x, y: node.y + node.height / 2 };
|
|
609
838
|
}
|
|
610
839
|
}
|
|
840
|
+
function getPortHandlePosition(node, port, scale = 1) {
|
|
841
|
+
const point = getPortPosition(node, port);
|
|
842
|
+
const normal = getPortNormal(port);
|
|
843
|
+
const offset = PORT_HANDLE_OFFSET / scale;
|
|
844
|
+
return {
|
|
845
|
+
x: formatNumber(point.x + normal.x * offset),
|
|
846
|
+
y: formatNumber(point.y + normal.y * offset)
|
|
847
|
+
};
|
|
848
|
+
}
|
|
611
849
|
function getNearestPort(node, point) {
|
|
612
850
|
return FLOWCHART_PORTS.reduce((best, port) => {
|
|
613
851
|
const portPoint = getPortPosition(node, port);
|
|
@@ -975,6 +1213,68 @@ function getResizeCursor(handle) {
|
|
|
975
1213
|
if (handle === "ne" || handle === "sw") return "nesw-resize";
|
|
976
1214
|
return "nwse-resize";
|
|
977
1215
|
}
|
|
1216
|
+
function collectGuideLines(nodes) {
|
|
1217
|
+
const x = [];
|
|
1218
|
+
const y = [];
|
|
1219
|
+
nodes.forEach((node) => {
|
|
1220
|
+
const right = node.x + node.width;
|
|
1221
|
+
const bottom = node.y + node.height;
|
|
1222
|
+
[node.x, node.x + node.width / 2, right].forEach(
|
|
1223
|
+
(value) => x.push({ value, start: node.y, end: bottom })
|
|
1224
|
+
);
|
|
1225
|
+
[node.y, node.y + node.height / 2, bottom].forEach(
|
|
1226
|
+
(value) => y.push({ value, start: node.x, end: right })
|
|
1227
|
+
);
|
|
1228
|
+
});
|
|
1229
|
+
const byValue = (a, b) => a.value - b.value;
|
|
1230
|
+
return { x: x.sort(byValue), y: y.sort(byValue) };
|
|
1231
|
+
}
|
|
1232
|
+
function lowerBound(lines, value) {
|
|
1233
|
+
let low = 0;
|
|
1234
|
+
let high = lines.length;
|
|
1235
|
+
while (low < high) {
|
|
1236
|
+
const middle = low + high >> 1;
|
|
1237
|
+
if (lines[middle].value < value) low = middle + 1;
|
|
1238
|
+
else high = middle;
|
|
1239
|
+
}
|
|
1240
|
+
return low;
|
|
1241
|
+
}
|
|
1242
|
+
function snapToGuides(lines, candidates, threshold) {
|
|
1243
|
+
let best = null;
|
|
1244
|
+
candidates.forEach((candidate) => {
|
|
1245
|
+
const index = lowerBound(lines, candidate);
|
|
1246
|
+
[lines[index - 1], lines[index]].forEach((line) => {
|
|
1247
|
+
if (!line) return;
|
|
1248
|
+
const offset = line.value - candidate;
|
|
1249
|
+
if (Math.abs(offset) <= threshold && (!best || Math.abs(offset) < Math.abs(best.offset))) {
|
|
1250
|
+
best = { offset };
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
});
|
|
1254
|
+
if (!best) return null;
|
|
1255
|
+
const positions = candidates.map((candidate) => candidate + best.offset);
|
|
1256
|
+
best.lines = lines.filter(
|
|
1257
|
+
(line) => positions.some((position) => Math.abs(line.value - position) < 0.5)
|
|
1258
|
+
);
|
|
1259
|
+
return best;
|
|
1260
|
+
}
|
|
1261
|
+
function guideSegments(axis, lines, box) {
|
|
1262
|
+
const byValue = /* @__PURE__ */ new Map();
|
|
1263
|
+
lines.forEach((line) => {
|
|
1264
|
+
const current = byValue.get(line.value);
|
|
1265
|
+
byValue.set(line.value, {
|
|
1266
|
+
start: Math.min(current?.start ?? Infinity, line.start),
|
|
1267
|
+
end: Math.max(current?.end ?? -Infinity, line.end)
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
1270
|
+
const boxStart = axis === "x" ? box.top : box.left;
|
|
1271
|
+
const boxEnd = boxStart + (axis === "x" ? box.height : box.width);
|
|
1272
|
+
return [...byValue].map(([value, extent]) => {
|
|
1273
|
+
const start = Math.min(extent.start, boxStart) - GUIDE_OVERHANG;
|
|
1274
|
+
const end = Math.max(extent.end, boxEnd) + GUIDE_OVERHANG;
|
|
1275
|
+
return axis === "x" ? { x1: value, y1: start, x2: value, y2: end } : { x1: start, y1: value, x2: end, y2: value };
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
978
1278
|
function getBounds(nodes) {
|
|
979
1279
|
if (!nodes.length) {
|
|
980
1280
|
return { left: 0, top: 0, right: 0, bottom: 0 };
|
|
@@ -1131,6 +1431,7 @@ var VdFlowchart = class {
|
|
|
1131
1431
|
this.documentData = normalizeDocument(options.data || {});
|
|
1132
1432
|
this.listeners = {};
|
|
1133
1433
|
this.selection = null;
|
|
1434
|
+
this.selectedNodeIds = /* @__PURE__ */ new Set();
|
|
1134
1435
|
this.interaction = null;
|
|
1135
1436
|
this.activeTool = null;
|
|
1136
1437
|
this.layoutMode = "tree";
|
|
@@ -1138,6 +1439,7 @@ var VdFlowchart = class {
|
|
|
1138
1439
|
this.destroyed = false;
|
|
1139
1440
|
this.lastNodePointer = null;
|
|
1140
1441
|
this.reconnectEdgeId = null;
|
|
1442
|
+
this.edgeCycleAnchor = null;
|
|
1141
1443
|
this.clipboard = null;
|
|
1142
1444
|
this.gridPatternId = nextId("flowchart-grid");
|
|
1143
1445
|
this.markerIds = /* @__PURE__ */ new Map();
|
|
@@ -1147,6 +1449,16 @@ var VdFlowchart = class {
|
|
|
1147
1449
|
this.history = [];
|
|
1148
1450
|
this.historyIndex = -1;
|
|
1149
1451
|
this.isApplyingHistory = false;
|
|
1452
|
+
this.historyBatchDepth = 0;
|
|
1453
|
+
this.historyBatchDirty = false;
|
|
1454
|
+
this.coalesceInsertedText = false;
|
|
1455
|
+
this.keyboardShortcuts = normalizeKeyboardShortcuts(options.keyboardShortcuts);
|
|
1456
|
+
this.snapGuides = options.snapGuides !== false;
|
|
1457
|
+
this.minimapEnabled = options.minimap !== false;
|
|
1458
|
+
this.autoLayout = Boolean(options.autoLayout);
|
|
1459
|
+
this.minimapFrame = 0;
|
|
1460
|
+
this.minimapDrag = null;
|
|
1461
|
+
this.shortcutsHelpOpen = false;
|
|
1150
1462
|
this.autoFit = Boolean(options.autoFit);
|
|
1151
1463
|
this.readyEmitted = false;
|
|
1152
1464
|
this.resizeObserver = null;
|
|
@@ -1187,6 +1499,7 @@ var VdFlowchart = class {
|
|
|
1187
1499
|
if (!ready) return false;
|
|
1188
1500
|
this.readyEmitted = true;
|
|
1189
1501
|
if (this.autoFit) this.fitView();
|
|
1502
|
+
else this.scheduleMinimap();
|
|
1190
1503
|
this.emit("ready", this);
|
|
1191
1504
|
return true;
|
|
1192
1505
|
};
|
|
@@ -1256,8 +1569,25 @@ var VdFlowchart = class {
|
|
|
1256
1569
|
className: "vd-flowchart-toolbar-label",
|
|
1257
1570
|
text: "100%"
|
|
1258
1571
|
});
|
|
1572
|
+
this.shortcutsButton = createElement("button", {
|
|
1573
|
+
className: "vd-flowchart-btn vd-flowchart-icon-btn vd-flowchart-shortcuts-btn",
|
|
1574
|
+
title: "Keyboard shortcuts (?)",
|
|
1575
|
+
text: "?"
|
|
1576
|
+
});
|
|
1577
|
+
this.shortcutsButton.setAttribute("type", "button");
|
|
1578
|
+
this.shortcutsButton.setAttribute("data-flowchart-action", "shortcuts");
|
|
1579
|
+
this.shortcutsButton.setAttribute("aria-label", "Keyboard shortcuts");
|
|
1580
|
+
this.shortcutsButton.setAttribute("aria-expanded", "false");
|
|
1581
|
+
this.minimapButton = createToolbarButton({
|
|
1582
|
+
action: "minimap",
|
|
1583
|
+
label: "Toggle minimap",
|
|
1584
|
+
icon: "minimap"
|
|
1585
|
+
});
|
|
1586
|
+
this.minimapButton.setAttribute("aria-pressed", this.minimapEnabled ? "true" : "false");
|
|
1259
1587
|
toolbarLeft.appendChild(this.arrangeSelect);
|
|
1260
1588
|
toolbarLeft.appendChild(this.clearButton);
|
|
1589
|
+
toolbarRight.appendChild(this.minimapButton);
|
|
1590
|
+
toolbarRight.appendChild(this.shortcutsButton);
|
|
1261
1591
|
toolbarRight.appendChild(this.zoomLabel);
|
|
1262
1592
|
this.toolbar.appendChild(toolbarLeft);
|
|
1263
1593
|
this.toolbar.appendChild(toolbarRight);
|
|
@@ -1336,13 +1666,40 @@ var VdFlowchart = class {
|
|
|
1336
1666
|
this.previewLayer = svgEl("g", { class: "vd-flowchart-preview" });
|
|
1337
1667
|
this.nodesLayer = svgEl("g", { class: "vd-flowchart-nodes" });
|
|
1338
1668
|
this.overlayLayer = svgEl("g", { class: "vd-flowchart-overlay" });
|
|
1669
|
+
this.guidesLayer = svgEl("g", { class: "vd-flowchart-guides" });
|
|
1339
1670
|
this.world.appendChild(this.gridRect);
|
|
1340
1671
|
this.world.appendChild(this.edgesLayer);
|
|
1341
1672
|
this.world.appendChild(this.previewLayer);
|
|
1342
1673
|
this.world.appendChild(this.nodesLayer);
|
|
1343
1674
|
this.world.appendChild(this.overlayLayer);
|
|
1675
|
+
this.world.appendChild(this.guidesLayer);
|
|
1344
1676
|
this.svg.appendChild(this.world);
|
|
1345
1677
|
this.canvasEl.appendChild(this.svg);
|
|
1678
|
+
this.shortcutsHelp = createElement("div", { className: "vd-flowchart-shortcuts" });
|
|
1679
|
+
this.shortcutsHelp.id = nextId("flowchart-shortcuts");
|
|
1680
|
+
this.shortcutsHelp.setAttribute("role", "dialog");
|
|
1681
|
+
this.shortcutsHelp.setAttribute("aria-label", "Keyboard shortcuts");
|
|
1682
|
+
this.shortcutsHelp.hidden = true;
|
|
1683
|
+
this.shortcutsHelp.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
1684
|
+
this.shortcutsHelp.addEventListener("wheel", (event) => event.stopPropagation());
|
|
1685
|
+
this.shortcutsButton.setAttribute("aria-controls", this.shortcutsHelp.id);
|
|
1686
|
+
this.canvasEl.appendChild(this.shortcutsHelp);
|
|
1687
|
+
this.minimapEl = createElement("div", { className: "vd-flowchart-minimap" });
|
|
1688
|
+
this.minimapEl.setAttribute("aria-hidden", "true");
|
|
1689
|
+
this.minimapEl.hidden = true;
|
|
1690
|
+
this.minimapSvg = svgEl("svg", {
|
|
1691
|
+
class: "vd-flowchart-minimap-svg",
|
|
1692
|
+
width: MINIMAP_WIDTH,
|
|
1693
|
+
height: MINIMAP_HEIGHT,
|
|
1694
|
+
viewBox: `0 0 ${MINIMAP_WIDTH} ${MINIMAP_HEIGHT}`
|
|
1695
|
+
});
|
|
1696
|
+
this.minimapEl.appendChild(this.minimapSvg);
|
|
1697
|
+
this.minimapEl.addEventListener("pointerdown", (event) => this.handleMinimapPointer(event));
|
|
1698
|
+
this.minimapEl.addEventListener("pointermove", (event) => this.handleMinimapPointer(event));
|
|
1699
|
+
this.minimapEl.addEventListener("pointerup", (event) => this.endMinimapDrag(event));
|
|
1700
|
+
this.minimapEl.addEventListener("pointercancel", (event) => this.endMinimapDrag(event));
|
|
1701
|
+
this.minimapEl.addEventListener("wheel", (event) => event.stopPropagation());
|
|
1702
|
+
this.canvasEl.appendChild(this.minimapEl);
|
|
1346
1703
|
this.inspectorPanel = createElement("aside", {
|
|
1347
1704
|
className: "vd-flowchart-panel vd-flowchart-panel--inspector"
|
|
1348
1705
|
});
|
|
@@ -1450,11 +1807,15 @@ var VdFlowchart = class {
|
|
|
1450
1807
|
this.graphOutline.appendChild(this.graphList);
|
|
1451
1808
|
this.root.appendChild(this.graphOutline);
|
|
1452
1809
|
this.canvasEl.setAttribute("role", "group");
|
|
1810
|
+
this.syncCanvasLabel();
|
|
1811
|
+
this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
|
|
1812
|
+
}
|
|
1813
|
+
syncCanvasLabel() {
|
|
1814
|
+
const keys = this.readonly || this.keyboardShortcuts === "basic" ? "Arrow keys select nodes; Enter or F2 edits a label." : "Arrow keys select nodes; Tab adds a child, Enter adds a sibling, F2 edits a label.";
|
|
1453
1815
|
this.canvasEl.setAttribute(
|
|
1454
1816
|
"aria-label",
|
|
1455
|
-
|
|
1817
|
+
`Diagram canvas. ${keys} Press ? for all shortcuts. Graph outline provides connections.`
|
|
1456
1818
|
);
|
|
1457
|
-
this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
|
|
1458
1819
|
}
|
|
1459
1820
|
renderGraphOutline() {
|
|
1460
1821
|
if (!this.graphOutline) return;
|
|
@@ -1464,8 +1825,9 @@ var VdFlowchart = class {
|
|
|
1464
1825
|
const selected = this.selection?.kind === "node" ? this.selection.id : null;
|
|
1465
1826
|
this.graphStatus.textContent = selected ? `Selected: ${labels.get(selected)}. ${nodes.length} nodes, ${edges.length} connections.` : `${nodes.length} nodes, ${edges.length} connections.`;
|
|
1466
1827
|
if (!this.graphOutline.open) return;
|
|
1828
|
+
const hidden = this.getHiddenNodeIds();
|
|
1467
1829
|
const signature = JSON.stringify([
|
|
1468
|
-
nodes.map((node) => [node.id, node.text]),
|
|
1830
|
+
nodes.map((node) => [node.id, node.text, Boolean(node.collapsed), hidden.has(node.id)]),
|
|
1469
1831
|
edges.map((edge) => [edge.id, edge.from.nodeId, edge.to.nodeId, edge.label])
|
|
1470
1832
|
]);
|
|
1471
1833
|
if (signature !== this.graphSignature) {
|
|
@@ -1491,8 +1853,10 @@ var VdFlowchart = class {
|
|
|
1491
1853
|
const from = incoming.get(node.id);
|
|
1492
1854
|
const relationships = [
|
|
1493
1855
|
to.length ? `Connects to ${to.join(", ")}.` : "No outgoing connections.",
|
|
1494
|
-
from.length ? `Connected from ${from.join(", ")}.` : ""
|
|
1495
|
-
|
|
1856
|
+
from.length ? `Connected from ${from.join(", ")}.` : "",
|
|
1857
|
+
node.collapsed ? "Branch collapsed." : "",
|
|
1858
|
+
hidden.has(node.id) ? "Hidden in a collapsed branch." : ""
|
|
1859
|
+
].filter(Boolean);
|
|
1496
1860
|
this.graphList.appendChild(
|
|
1497
1861
|
createElement("li", { text: `${labels.get(node.id)}. ${relationships.join(" ")}` })
|
|
1498
1862
|
);
|
|
@@ -1577,6 +1941,157 @@ var VdFlowchart = class {
|
|
|
1577
1941
|
if (action === "undo") this.undo();
|
|
1578
1942
|
if (action === "redo") this.redo();
|
|
1579
1943
|
if (action === "clear" && !this.readonly) this.clear();
|
|
1944
|
+
if (action === "shortcuts") this.toggleShortcutsHelp();
|
|
1945
|
+
if (action === "minimap") this.setMinimapEnabled(!this.minimapEnabled);
|
|
1946
|
+
}
|
|
1947
|
+
setMinimapEnabled(enabled) {
|
|
1948
|
+
this.minimapEnabled = Boolean(enabled);
|
|
1949
|
+
this.minimapButton.setAttribute("aria-pressed", this.minimapEnabled ? "true" : "false");
|
|
1950
|
+
this.drawMinimap();
|
|
1951
|
+
return this;
|
|
1952
|
+
}
|
|
1953
|
+
scheduleMinimap() {
|
|
1954
|
+
if (this.minimapFrame || !hasWindow()) return;
|
|
1955
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (callback) => setTimeout(callback, 16);
|
|
1956
|
+
this.minimapFrame = raf(() => {
|
|
1957
|
+
this.minimapFrame = 0;
|
|
1958
|
+
this.drawMinimap();
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
// World-to-minimap mapping covering every visible node and the viewport.
|
|
1962
|
+
getMinimapTransform() {
|
|
1963
|
+
const viewport = this.documentData.viewport;
|
|
1964
|
+
const width = this.canvasEl.clientWidth;
|
|
1965
|
+
const height = this.canvasEl.clientHeight;
|
|
1966
|
+
const view = {
|
|
1967
|
+
left: -viewport.x / viewport.scale,
|
|
1968
|
+
top: -viewport.y / viewport.scale,
|
|
1969
|
+
right: (width - viewport.x) / viewport.scale,
|
|
1970
|
+
bottom: (height - viewport.y) / viewport.scale
|
|
1971
|
+
};
|
|
1972
|
+
const content = getBounds(this.getVisibleNodes());
|
|
1973
|
+
const left = Math.min(content.left, view.left);
|
|
1974
|
+
const top = Math.min(content.top, view.top);
|
|
1975
|
+
const spanX = Math.max(1, Math.max(content.right, view.right) - left);
|
|
1976
|
+
const spanY = Math.max(1, Math.max(content.bottom, view.bottom) - top);
|
|
1977
|
+
const padding = 8;
|
|
1978
|
+
const scale = Math.min(
|
|
1979
|
+
(MINIMAP_WIDTH - padding * 2) / spanX,
|
|
1980
|
+
(MINIMAP_HEIGHT - padding * 2) / spanY
|
|
1981
|
+
);
|
|
1982
|
+
return {
|
|
1983
|
+
scale,
|
|
1984
|
+
offsetX: (MINIMAP_WIDTH - spanX * scale) / 2 - left * scale,
|
|
1985
|
+
offsetY: (MINIMAP_HEIGHT - spanY * scale) / 2 - top * scale,
|
|
1986
|
+
view
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
drawMinimap() {
|
|
1990
|
+
if (this.destroyed || !this.minimapEl) return;
|
|
1991
|
+
const nodes = this.getVisibleNodes();
|
|
1992
|
+
const visible = this.minimapEnabled && nodes.length > 0 && this.canvasEl.clientWidth >= MINIMAP_MIN_CANVAS_WIDTH;
|
|
1993
|
+
this.minimapEl.hidden = !visible;
|
|
1994
|
+
if (!visible) return;
|
|
1995
|
+
const transform = this.minimapDrag?.transform || this.getMinimapTransform();
|
|
1996
|
+
const view = this.getMinimapTransform().view;
|
|
1997
|
+
const map = (x, y) => ({
|
|
1998
|
+
x: formatNumber(x * transform.scale + transform.offsetX),
|
|
1999
|
+
y: formatNumber(y * transform.scale + transform.offsetY)
|
|
2000
|
+
});
|
|
2001
|
+
clearChildren(this.minimapSvg);
|
|
2002
|
+
nodes.forEach((node) => {
|
|
2003
|
+
const point = map(node.x, node.y);
|
|
2004
|
+
const selected = this.isNodeSelected(node.id);
|
|
2005
|
+
this.minimapSvg.appendChild(
|
|
2006
|
+
svgEl("rect", {
|
|
2007
|
+
class: `vd-flowchart-minimap-node${selected ? " is-selected" : ""}`,
|
|
2008
|
+
x: point.x,
|
|
2009
|
+
y: point.y,
|
|
2010
|
+
width: formatNumber(Math.max(1.5, node.width * transform.scale)),
|
|
2011
|
+
height: formatNumber(Math.max(1.5, node.height * transform.scale)),
|
|
2012
|
+
rx: 1.5
|
|
2013
|
+
})
|
|
2014
|
+
);
|
|
2015
|
+
});
|
|
2016
|
+
const corner = map(view.left, view.top);
|
|
2017
|
+
this.minimapSvg.appendChild(
|
|
2018
|
+
svgEl("rect", {
|
|
2019
|
+
class: "vd-flowchart-minimap-viewport",
|
|
2020
|
+
x: corner.x,
|
|
2021
|
+
y: corner.y,
|
|
2022
|
+
width: formatNumber((view.right - view.left) * transform.scale),
|
|
2023
|
+
height: formatNumber((view.bottom - view.top) * transform.scale)
|
|
2024
|
+
})
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
// Centre the main view on the world point under the minimap pointer. The
|
|
2028
|
+
// mapping is frozen for the whole drag so the map does not shift under it.
|
|
2029
|
+
handleMinimapPointer(event) {
|
|
2030
|
+
event.stopPropagation();
|
|
2031
|
+
if (event.type === "pointerdown") {
|
|
2032
|
+
if (event.button != null && event.button !== 0) return;
|
|
2033
|
+
event.preventDefault();
|
|
2034
|
+
this.minimapDrag = { pointerId: event.pointerId, transform: this.getMinimapTransform() };
|
|
2035
|
+
try {
|
|
2036
|
+
this.minimapEl.setPointerCapture?.(event.pointerId);
|
|
2037
|
+
} catch {
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
if (!this.minimapDrag || this.minimapDrag.pointerId !== event.pointerId) return;
|
|
2041
|
+
const rect = this.minimapEl.getBoundingClientRect();
|
|
2042
|
+
const { transform } = this.minimapDrag;
|
|
2043
|
+
this.centerViewOn(
|
|
2044
|
+
(event.clientX - rect.left - transform.offsetX) / transform.scale,
|
|
2045
|
+
(event.clientY - rect.top - transform.offsetY) / transform.scale
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
endMinimapDrag(event) {
|
|
2049
|
+
event.stopPropagation();
|
|
2050
|
+
if (!this.minimapDrag || this.minimapDrag.pointerId !== event.pointerId) return;
|
|
2051
|
+
this.minimapDrag = null;
|
|
2052
|
+
this.syncJsonTextarea();
|
|
2053
|
+
this.emitViewportChange("viewport:pan");
|
|
2054
|
+
this.drawMinimap();
|
|
2055
|
+
}
|
|
2056
|
+
centerViewOn(worldX, worldY) {
|
|
2057
|
+
const viewport = this.documentData.viewport;
|
|
2058
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
2059
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
2060
|
+
viewport.x = formatNumber(width / 2 - worldX * viewport.scale);
|
|
2061
|
+
viewport.y = formatNumber(height / 2 - worldY * viewport.scale);
|
|
2062
|
+
this.render({ inspector: false, json: false });
|
|
2063
|
+
return this;
|
|
2064
|
+
}
|
|
2065
|
+
toggleShortcutsHelp(force) {
|
|
2066
|
+
const open = typeof force === "boolean" ? force : !this.shortcutsHelpOpen;
|
|
2067
|
+
this.shortcutsHelpOpen = open;
|
|
2068
|
+
this.shortcutsButton.setAttribute("aria-expanded", open ? "true" : "false");
|
|
2069
|
+
if (open) this.renderShortcutsHelp();
|
|
2070
|
+
this.shortcutsHelp.hidden = !open;
|
|
2071
|
+
if (!open && this.shortcutsHelp.contains(document.activeElement)) this.canvasEl.focus();
|
|
2072
|
+
return this;
|
|
2073
|
+
}
|
|
2074
|
+
renderShortcutsHelp() {
|
|
2075
|
+
clearChildren(this.shortcutsHelp);
|
|
2076
|
+
const header = createElement("div", { className: "vd-flowchart-shortcuts-header" });
|
|
2077
|
+
header.appendChild(
|
|
2078
|
+
createElement("h4", { className: "vd-flowchart-panel-title", text: "Keyboard shortcuts" })
|
|
2079
|
+
);
|
|
2080
|
+
const close = createElement("button", {
|
|
2081
|
+
className: "vd-flowchart-btn vd-flowchart-icon-btn",
|
|
2082
|
+
text: "\xD7"
|
|
2083
|
+
});
|
|
2084
|
+
close.setAttribute("type", "button");
|
|
2085
|
+
close.setAttribute("aria-label", "Close keyboard shortcuts");
|
|
2086
|
+
close.addEventListener("click", () => this.toggleShortcutsHelp(false));
|
|
2087
|
+
header.appendChild(close);
|
|
2088
|
+
this.shortcutsHelp.appendChild(header);
|
|
2089
|
+
const list = createElement("dl", { className: "vd-flowchart-shortcuts-list" });
|
|
2090
|
+
getShortcutRows(this.keyboardShortcuts, this.readonly).forEach(([keys, action]) => {
|
|
2091
|
+
list.appendChild(createElement("dt", { text: keys }));
|
|
2092
|
+
list.appendChild(createElement("dd", { text: action }));
|
|
2093
|
+
});
|
|
2094
|
+
this.shortcutsHelp.appendChild(list);
|
|
1580
2095
|
}
|
|
1581
2096
|
handleArrangeChange(event) {
|
|
1582
2097
|
if (this.readonly) return;
|
|
@@ -1685,38 +2200,48 @@ var VdFlowchart = class {
|
|
|
1685
2200
|
);
|
|
1686
2201
|
}
|
|
1687
2202
|
}
|
|
2203
|
+
// Keys the editor consumes stop here, so page-level shortcuts (a docs
|
|
2204
|
+
// site's "/" search, a host's Escape handler) do not also fire.
|
|
1688
2205
|
handleKeyDown(event) {
|
|
1689
|
-
|
|
2206
|
+
const alreadyHandled = event.defaultPrevented;
|
|
2207
|
+
this.handleEditorKey(event);
|
|
2208
|
+
if (!alreadyHandled && event.defaultPrevented) event.stopPropagation();
|
|
2209
|
+
}
|
|
2210
|
+
handleEditorKey(event) {
|
|
2211
|
+
const target = event.target;
|
|
2212
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable)) {
|
|
1690
2213
|
return;
|
|
1691
2214
|
}
|
|
1692
|
-
if (event.
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
event.preventDefault();
|
|
1704
|
-
this.selectNode(nodes[next].id);
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
if (event.key === "Enter" && !this.readonly && this.selection?.kind === "node") {
|
|
1708
|
-
event.preventDefault();
|
|
1709
|
-
this.startTextEdit(this.selection.id);
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
2215
|
+
if (event.isComposing) return;
|
|
2216
|
+
const key = event.key;
|
|
2217
|
+
const modKey = event.metaKey || event.ctrlKey;
|
|
2218
|
+
if (key === "Escape" && this.shortcutsHelpOpen) {
|
|
2219
|
+
event.preventDefault();
|
|
2220
|
+
this.toggleShortcutsHelp(false);
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
if (target === this.canvasEl && this.handleCanvasKeyDown(event)) {
|
|
2224
|
+
event.preventDefault();
|
|
2225
|
+
return;
|
|
1712
2226
|
}
|
|
1713
2227
|
if (this.readonly) return;
|
|
1714
|
-
if (
|
|
2228
|
+
if (key === "Escape") {
|
|
2229
|
+
if (this.activeTool) this.setActiveTool(null);
|
|
2230
|
+
else if (this.reconnectEdgeId) {
|
|
2231
|
+
this.reconnectEdgeId = null;
|
|
2232
|
+
this.render({ inspector: false, json: false });
|
|
2233
|
+
} else if (this.selection) this.select(null);
|
|
2234
|
+
else return;
|
|
1715
2235
|
event.preventDefault();
|
|
1716
|
-
this.setActiveTool(null);
|
|
1717
2236
|
return;
|
|
1718
2237
|
}
|
|
1719
|
-
|
|
2238
|
+
if (modKey && (key === "d" || key === "D")) {
|
|
2239
|
+
if (this.selection?.kind === "node") {
|
|
2240
|
+
event.preventDefault();
|
|
2241
|
+
this.duplicateSelection();
|
|
2242
|
+
}
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
1720
2245
|
if (modKey && (event.key === "z" || event.key === "Z")) {
|
|
1721
2246
|
event.preventDefault();
|
|
1722
2247
|
if (event.shiftKey) {
|
|
@@ -1755,11 +2280,248 @@ var VdFlowchart = class {
|
|
|
1755
2280
|
if (!this.selection) return;
|
|
1756
2281
|
if (event.key === "Backspace" || event.key === "Delete") {
|
|
1757
2282
|
event.preventDefault();
|
|
2283
|
+
if (this.selection.kind === "node" && !this.hasMultiSelection()) {
|
|
2284
|
+
const removedId = this.selection.id;
|
|
2285
|
+
const parent = this.getParentNode(removedId);
|
|
2286
|
+
const sibling = parent ? this.getChildNodes(parent.id).find((node) => node.id !== removedId) : null;
|
|
2287
|
+
if (this.deleteSelection()) {
|
|
2288
|
+
const next = parent && this.findNode(parent.id) || sibling;
|
|
2289
|
+
if (next) this.select({ kind: "node", id: next.id });
|
|
2290
|
+
}
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
1758
2293
|
this.deleteSelection();
|
|
1759
2294
|
}
|
|
1760
2295
|
}
|
|
2296
|
+
// Keys that only apply while the canvas itself has focus. Returns true when
|
|
2297
|
+
// the key was handled (the caller prevents the default action).
|
|
2298
|
+
handleCanvasKeyDown(event) {
|
|
2299
|
+
const key = event.key;
|
|
2300
|
+
const modKey = event.metaKey || event.ctrlKey;
|
|
2301
|
+
const mindmap = this.keyboardShortcuts === "mindmap";
|
|
2302
|
+
const selectedNode = this.selection?.kind === "node" ? this.findNode(this.selection.id) : null;
|
|
2303
|
+
const selectedEdge = this.selection?.kind === "edge" ? this.findEdge(this.selection.id) : null;
|
|
2304
|
+
const movable = !this.readonly && Boolean(selectedNode);
|
|
2305
|
+
const editable = movable && !this.hasMultiSelection();
|
|
2306
|
+
if (key === "?" && !modKey && !event.altKey) {
|
|
2307
|
+
this.toggleShortcutsHelp();
|
|
2308
|
+
return true;
|
|
2309
|
+
}
|
|
2310
|
+
if (modKey && !event.altKey) {
|
|
2311
|
+
if (key === "=" || key === "+") return Boolean(this.zoomIn());
|
|
2312
|
+
if (key === "-" || key === "_") return Boolean(this.zoomOut());
|
|
2313
|
+
if (key === "0") return Boolean(this.zoomTo(1));
|
|
2314
|
+
if (key === "a" || key === "A") {
|
|
2315
|
+
this.selectNodes(this.getVisibleNodes().map((node) => node.id));
|
|
2316
|
+
return true;
|
|
2317
|
+
}
|
|
2318
|
+
if (key === "/" && selectedNode && !this.hasMultiSelection()) {
|
|
2319
|
+
this.toggleCollapsed(selectedNode.id);
|
|
2320
|
+
return true;
|
|
2321
|
+
}
|
|
2322
|
+
return false;
|
|
2323
|
+
}
|
|
2324
|
+
if (event.shiftKey && !event.altKey && (key === "!" || event.code === "Digit1")) {
|
|
2325
|
+
return Boolean(this.fitView());
|
|
2326
|
+
}
|
|
2327
|
+
const direction = ARROW_DIRECTIONS[key];
|
|
2328
|
+
if (direction) {
|
|
2329
|
+
if (event.altKey) {
|
|
2330
|
+
if (movable) {
|
|
2331
|
+
this.nudgeNodes(this.getSelectedNodeIds(), direction, event.shiftKey ? 1 : this.gridSize);
|
|
2332
|
+
}
|
|
2333
|
+
return true;
|
|
2334
|
+
}
|
|
2335
|
+
if (event.shiftKey) {
|
|
2336
|
+
const anchorId = selectedNode?.id ?? (selectedEdge ? this.edgeCycleAnchor : null);
|
|
2337
|
+
return anchorId ? this.selectSideEdge(anchorId, direction) : false;
|
|
2338
|
+
}
|
|
2339
|
+
this.navigateSelection(direction);
|
|
2340
|
+
return true;
|
|
2341
|
+
}
|
|
2342
|
+
const nodes = this.getVisibleNodes();
|
|
2343
|
+
if ((key === "Home" || key === "End") && nodes.length) {
|
|
2344
|
+
this.selectNode(nodes[key === "Home" ? 0 : nodes.length - 1].id);
|
|
2345
|
+
return true;
|
|
2346
|
+
}
|
|
2347
|
+
if (selectedEdge && !this.readonly && !event.altKey) {
|
|
2348
|
+
if (key === "F2" || key === "Enter" || key === " " && mindmap) {
|
|
2349
|
+
return this.startEdgeLabelEdit(selectedEdge.id);
|
|
2350
|
+
}
|
|
2351
|
+
if (mindmap && key.length === 1 && key !== " ") {
|
|
2352
|
+
return this.startEdgeLabelEdit(selectedEdge.id, { initialText: key });
|
|
2353
|
+
}
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
if (!editable || event.altKey) return false;
|
|
2357
|
+
if (key === "F2" || key === "Enter" && !mindmap || key === " " && mindmap) {
|
|
2358
|
+
this.startTextEdit(selectedNode.id);
|
|
2359
|
+
return true;
|
|
2360
|
+
}
|
|
2361
|
+
if (!mindmap) return false;
|
|
2362
|
+
if (key === "Tab" && !event.shiftKey) {
|
|
2363
|
+
this.insertBranchNode(selectedNode.id, { edit: true });
|
|
2364
|
+
return true;
|
|
2365
|
+
}
|
|
2366
|
+
if (key === "Enter") {
|
|
2367
|
+
this.insertSiblingNode(selectedNode.id, { before: event.shiftKey, edit: true });
|
|
2368
|
+
return true;
|
|
2369
|
+
}
|
|
2370
|
+
if (key.length === 1 && key !== " " && isNodeTextEditable(selectedNode)) {
|
|
2371
|
+
this.startTextEdit(selectedNode.id, { initialText: key });
|
|
2372
|
+
return true;
|
|
2373
|
+
}
|
|
2374
|
+
return false;
|
|
2375
|
+
}
|
|
2376
|
+
// Move the selection to the nearest node in `direction`; with nothing (or an
|
|
2377
|
+
// edge) selected, start from the node closest to the centre of the view.
|
|
2378
|
+
navigateSelection(direction) {
|
|
2379
|
+
const nodes = this.getVisibleNodes();
|
|
2380
|
+
if (!nodes.length) return false;
|
|
2381
|
+
const current = this.selection?.kind === "node" ? this.findNode(this.selection.id) : null;
|
|
2382
|
+
const edge = this.selection?.kind === "edge" ? this.findEdge(this.selection.id) : null;
|
|
2383
|
+
const next = current ? findSpatialNeighbor(nodes, current, direction) : edge ? this.getEdgeEndInDirection(edge, direction) : findNearestNode(nodes, this.getViewportCenter());
|
|
2384
|
+
if (!next) return false;
|
|
2385
|
+
this.selectNode(next.id);
|
|
2386
|
+
this.revealNode(next.id);
|
|
2387
|
+
return true;
|
|
2388
|
+
}
|
|
2389
|
+
// The end of `edge` that best matches `direction`, measured from the label
|
|
2390
|
+
// anchor so either end can be reached from a selected connection.
|
|
2391
|
+
getEdgeEndInDirection(edge, direction) {
|
|
2392
|
+
const fromNode = this.findNode(edge.from.nodeId);
|
|
2393
|
+
const toNode = this.findNode(edge.to.nodeId);
|
|
2394
|
+
if (!fromNode || !toNode) return fromNode || toNode;
|
|
2395
|
+
const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
|
|
2396
|
+
const vector = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] }[direction];
|
|
2397
|
+
const score = (node) => (node.x + node.width / 2 - labelX) * vector[0] + (node.y + node.height / 2 - labelY) * vector[1];
|
|
2398
|
+
return score(toNode) >= score(fromNode) ? toNode : fromNode;
|
|
2399
|
+
}
|
|
2400
|
+
// Connections attached to `nodeId` on the `direction` side, outgoing or
|
|
2401
|
+
// incoming, in document order.
|
|
2402
|
+
getSideEdges(nodeId, direction) {
|
|
2403
|
+
const port = DIRECTION_PORTS[direction];
|
|
2404
|
+
return this.documentData.edges.filter(
|
|
2405
|
+
(edge) => edge.from.nodeId === nodeId && edge.from.port === port || edge.to.nodeId === nodeId && edge.to.port === port
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
// Select a connection on one side of a node; repeating cycles through them.
|
|
2409
|
+
selectSideEdge(nodeId, direction) {
|
|
2410
|
+
const edges = this.getSideEdges(nodeId, direction);
|
|
2411
|
+
if (!edges.length) return false;
|
|
2412
|
+
const currentIndex = this.selection?.kind === "edge" ? edges.findIndex((edge) => edge.id === this.selection.id) : -1;
|
|
2413
|
+
const next = edges[(currentIndex + 1) % edges.length];
|
|
2414
|
+
this.select({ kind: "edge", id: next.id });
|
|
2415
|
+
this.edgeCycleAnchor = nodeId;
|
|
2416
|
+
return true;
|
|
2417
|
+
}
|
|
2418
|
+
nudgeNode(nodeId, direction, distance) {
|
|
2419
|
+
const [node] = this.nudgeNodes([nodeId], direction, distance);
|
|
2420
|
+
return node || null;
|
|
2421
|
+
}
|
|
2422
|
+
// Move nodes together; repeated nudges of the same selection coalesce into
|
|
2423
|
+
// one undo step.
|
|
2424
|
+
nudgeNodes(nodeIds, direction, distance) {
|
|
2425
|
+
const step = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] }[direction];
|
|
2426
|
+
if (this.readonly || !step) return [];
|
|
2427
|
+
const nodes = nodeIds.map((id) => this.findNode(id)).filter(Boolean);
|
|
2428
|
+
if (!nodes.length) return [];
|
|
2429
|
+
nodes.forEach((node) => {
|
|
2430
|
+
node.x = formatNumber(node.x + step[0] * distance);
|
|
2431
|
+
node.y = formatNumber(node.y + step[1] * distance);
|
|
2432
|
+
});
|
|
2433
|
+
this.render({ inspector: true, json: true });
|
|
2434
|
+
this.emitChange(
|
|
2435
|
+
"node:nudge",
|
|
2436
|
+
nodes.length > 1 ? { nodeIds: nodes.map((node) => node.id) } : { node: deepClone(nodes[0]) }
|
|
2437
|
+
);
|
|
2438
|
+
return nodes.map((node) => deepClone(node));
|
|
2439
|
+
}
|
|
2440
|
+
zoomTo(scale) {
|
|
2441
|
+
const viewport = this.documentData.viewport;
|
|
2442
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
2443
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
2444
|
+
this.scaleAround(
|
|
2445
|
+
clamp(scale, MIN_SCALE, MAX_SCALE) / viewport.scale,
|
|
2446
|
+
width / 2,
|
|
2447
|
+
height / 2,
|
|
2448
|
+
"viewport:zoom"
|
|
2449
|
+
);
|
|
2450
|
+
return this;
|
|
2451
|
+
}
|
|
2452
|
+
duplicateSelection() {
|
|
2453
|
+
if (this.readonly || this.selection?.kind !== "node") return null;
|
|
2454
|
+
if (this.hasMultiSelection()) return this.pasteNodeGroup(this.captureNodeGroup());
|
|
2455
|
+
const node = this.findNode(this.selection.id);
|
|
2456
|
+
return node ? this.pasteNodeData(node) : null;
|
|
2457
|
+
}
|
|
2458
|
+
// Selected nodes plus the connections that run between them.
|
|
2459
|
+
captureNodeGroup() {
|
|
2460
|
+
const ids = new Set(this.getSelectedNodeIds());
|
|
2461
|
+
return {
|
|
2462
|
+
nodes: deepClone(this.documentData.nodes.filter((node) => ids.has(node.id))),
|
|
2463
|
+
edges: deepClone(
|
|
2464
|
+
this.documentData.edges.filter(
|
|
2465
|
+
(edge) => ids.has(edge.from.nodeId) && ids.has(edge.to.nodeId)
|
|
2466
|
+
)
|
|
2467
|
+
)
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
// Paste a captured group offset by one step, remapping ids so internal
|
|
2471
|
+
// connections follow their copies. One change, one undo step.
|
|
2472
|
+
pasteNodeGroup(group) {
|
|
2473
|
+
if (this.readonly || !group?.nodes?.length) return [];
|
|
2474
|
+
const usedNodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
2475
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
2476
|
+
const nodes = group.nodes.map((data, index) => {
|
|
2477
|
+
const node = normalizeNode(
|
|
2478
|
+
{ ...data, id: void 0, x: data.x + 24, y: data.y + 24 },
|
|
2479
|
+
this.documentData.nodes.length + index,
|
|
2480
|
+
usedNodeIds
|
|
2481
|
+
);
|
|
2482
|
+
idMap.set(data.id, node.id);
|
|
2483
|
+
return node;
|
|
2484
|
+
});
|
|
2485
|
+
this.documentData.nodes.push(...nodes);
|
|
2486
|
+
const nodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
2487
|
+
const usedEdgeIds = new Set(this.documentData.edges.map((edge) => edge.id));
|
|
2488
|
+
group.edges.forEach((data, index) => {
|
|
2489
|
+
const edge = normalizeEdge(
|
|
2490
|
+
{
|
|
2491
|
+
...data,
|
|
2492
|
+
id: void 0,
|
|
2493
|
+
from: { ...data.from, nodeId: idMap.get(data.from.nodeId) },
|
|
2494
|
+
to: { ...data.to, nodeId: idMap.get(data.to.nodeId) }
|
|
2495
|
+
},
|
|
2496
|
+
this.documentData.edges.length + index,
|
|
2497
|
+
nodeIds,
|
|
2498
|
+
usedEdgeIds
|
|
2499
|
+
);
|
|
2500
|
+
if (edge) this.documentData.edges.push(edge);
|
|
2501
|
+
});
|
|
2502
|
+
const ids = nodes.map((node) => node.id);
|
|
2503
|
+
this.selectNodes(ids);
|
|
2504
|
+
this.emitChange("nodes:add", { nodeIds: ids });
|
|
2505
|
+
return nodes.map((node) => deepClone(node));
|
|
2506
|
+
}
|
|
2507
|
+
pasteNodeData(data) {
|
|
2508
|
+
const usedIds = new Set(this.documentData.nodes.map((node2) => node2.id));
|
|
2509
|
+
const node = normalizeNode(
|
|
2510
|
+
{ ...data, id: void 0, collapsed: void 0, x: data.x + 24, y: data.y + 24 },
|
|
2511
|
+
this.documentData.nodes.length,
|
|
2512
|
+
usedIds
|
|
2513
|
+
);
|
|
2514
|
+
this.documentData.nodes.push(node);
|
|
2515
|
+
this.select({ kind: "node", id: node.id });
|
|
2516
|
+
this.emitChange("node:add", { node: deepClone(node) });
|
|
2517
|
+
return deepClone(node);
|
|
2518
|
+
}
|
|
1761
2519
|
copySelection() {
|
|
1762
2520
|
if (!this.selection) return false;
|
|
2521
|
+
if (this.hasMultiSelection()) {
|
|
2522
|
+
this.clipboard = { kind: "nodes", data: this.captureNodeGroup() };
|
|
2523
|
+
return true;
|
|
2524
|
+
}
|
|
1763
2525
|
if (this.selection.kind === "node") {
|
|
1764
2526
|
const node = this.findNode(this.selection.id);
|
|
1765
2527
|
if (!node) return false;
|
|
@@ -1774,22 +2536,14 @@ var VdFlowchart = class {
|
|
|
1774
2536
|
pasteClipboard() {
|
|
1775
2537
|
if (!this.clipboard || this.readonly) return false;
|
|
1776
2538
|
if (this.clipboard.kind === "node") {
|
|
1777
|
-
|
|
1778
|
-
const node = normalizeNode(
|
|
1779
|
-
{
|
|
1780
|
-
...this.clipboard.data,
|
|
1781
|
-
id: void 0,
|
|
1782
|
-
x: this.clipboard.data.x + 24,
|
|
1783
|
-
y: this.clipboard.data.y + 24
|
|
1784
|
-
},
|
|
1785
|
-
this.documentData.nodes.length,
|
|
1786
|
-
usedIds2
|
|
1787
|
-
);
|
|
1788
|
-
this.documentData.nodes.push(node);
|
|
1789
|
-
this.select({ kind: "node", id: node.id });
|
|
1790
|
-
this.emitChange("node:add", { node: deepClone(node) });
|
|
2539
|
+
this.pasteNodeData(this.clipboard.data);
|
|
1791
2540
|
return true;
|
|
1792
2541
|
}
|
|
2542
|
+
if (this.clipboard.kind === "nodes") {
|
|
2543
|
+
const pasted = this.pasteNodeGroup(this.clipboard.data);
|
|
2544
|
+
this.clipboard = { kind: "nodes", data: this.captureNodeGroup() };
|
|
2545
|
+
return pasted.length > 0;
|
|
2546
|
+
}
|
|
1793
2547
|
const usedIds = new Set(this.documentData.edges.map((edge2) => edge2.id));
|
|
1794
2548
|
const nodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
1795
2549
|
const edge = normalizeEdge(
|
|
@@ -1816,16 +2570,16 @@ var VdFlowchart = class {
|
|
|
1816
2570
|
const edgeTarget = event.target.closest("[data-edge-id]");
|
|
1817
2571
|
if (edgeTarget && !event.target.closest("[data-edge-endpoint]")) {
|
|
1818
2572
|
const edgeId = edgeTarget.getAttribute("data-edge-id");
|
|
1819
|
-
if (edgeId && this.
|
|
1820
|
-
this.reconnectEdgeId = edgeId;
|
|
1821
|
-
this.select({ kind: "edge", id: edgeId });
|
|
2573
|
+
if (edgeId && this.startEdgeLabelEdit(edgeId)) {
|
|
1822
2574
|
event.preventDefault();
|
|
1823
2575
|
event.stopPropagation();
|
|
1824
2576
|
return;
|
|
1825
2577
|
}
|
|
1826
2578
|
}
|
|
1827
2579
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1828
|
-
if (!nodeTarget || event.target.closest("[data-edge-id]"))
|
|
2580
|
+
if (!nodeTarget || event.target.closest("[data-edge-id]") || event.target.closest("[data-collapse-toggle]")) {
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
1829
2583
|
const nodeId = nodeTarget.getAttribute("data-node-id");
|
|
1830
2584
|
if (this.startTextEdit(nodeId)) {
|
|
1831
2585
|
event.preventDefault();
|
|
@@ -1835,7 +2589,7 @@ var VdFlowchart = class {
|
|
|
1835
2589
|
handleClick(event) {
|
|
1836
2590
|
if (this.readonly || event.detail < 2) return;
|
|
1837
2591
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1838
|
-
if (!nodeTarget || event.target.closest("[data-port]") || event.target.closest("[data-resize-handle]"))
|
|
2592
|
+
if (!nodeTarget || event.target.closest("[data-port]") || event.target.closest("[data-resize-handle]") || event.target.closest("[data-collapse-toggle]"))
|
|
1839
2593
|
return;
|
|
1840
2594
|
if (this.startTextEdit(nodeTarget.getAttribute("data-node-id"))) {
|
|
1841
2595
|
event.preventDefault();
|
|
@@ -1848,6 +2602,13 @@ var VdFlowchart = class {
|
|
|
1848
2602
|
this.stopTextEdit({ commit: true });
|
|
1849
2603
|
}
|
|
1850
2604
|
this.canvasEl.focus();
|
|
2605
|
+
const collapseTarget = event.target.closest("[data-collapse-toggle]");
|
|
2606
|
+
if (collapseTarget) {
|
|
2607
|
+
this.lastNodePointer = null;
|
|
2608
|
+
this.toggleCollapsed(collapseTarget.getAttribute("data-collapse-toggle"));
|
|
2609
|
+
event.preventDefault();
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
1851
2612
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1852
2613
|
const portTarget = event.target.closest("[data-port]");
|
|
1853
2614
|
const resizeTarget = event.target.closest("[data-resize-handle]");
|
|
@@ -1883,6 +2644,13 @@ var VdFlowchart = class {
|
|
|
1883
2644
|
event.preventDefault();
|
|
1884
2645
|
return;
|
|
1885
2646
|
}
|
|
2647
|
+
if (nodeTarget && !portTarget && !resizeTarget && !edgeTarget && event.shiftKey) {
|
|
2648
|
+
this.reconnectEdgeId = null;
|
|
2649
|
+
this.lastNodePointer = null;
|
|
2650
|
+
this.toggleNodeSelection(nodeTarget.getAttribute("data-node-id"));
|
|
2651
|
+
event.preventDefault();
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
1886
2654
|
if (nodeTarget && !portTarget && !resizeTarget && !edgeTarget && !this.readonly) {
|
|
1887
2655
|
if (this.activeTool === "arrow") {
|
|
1888
2656
|
this.select({ kind: "node", id: nodeTarget.getAttribute("data-node-id") });
|
|
@@ -1909,6 +2677,9 @@ var VdFlowchart = class {
|
|
|
1909
2677
|
this.interaction = {
|
|
1910
2678
|
kind: "connect",
|
|
1911
2679
|
pointerId: event.pointerId,
|
|
2680
|
+
startClientX: event.clientX,
|
|
2681
|
+
startClientY: event.clientY,
|
|
2682
|
+
moved: false,
|
|
1912
2683
|
source: { nodeId, port },
|
|
1913
2684
|
target: null,
|
|
1914
2685
|
previousSnap: null,
|
|
@@ -1959,22 +2730,27 @@ var VdFlowchart = class {
|
|
|
1959
2730
|
if (nodeTarget) {
|
|
1960
2731
|
this.reconnectEdgeId = null;
|
|
1961
2732
|
const nodeId = nodeTarget.getAttribute("data-node-id");
|
|
1962
|
-
this.
|
|
2733
|
+
const groupIds = this.hasMultiSelection() && this.isNodeSelected(nodeId) ? this.getSelectedNodeIds() : [nodeId];
|
|
2734
|
+
if (groupIds.length > 1) this.selectNodes(groupIds, { primary: nodeId });
|
|
2735
|
+
else this.select({ kind: "node", id: nodeId });
|
|
1963
2736
|
if (!this.readonly) {
|
|
1964
|
-
|
|
1965
|
-
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
1966
|
-
this.interaction = {
|
|
1967
|
-
kind: "drag-node",
|
|
1968
|
-
pointerId: event.pointerId,
|
|
1969
|
-
nodeId,
|
|
1970
|
-
offsetX: world.x - node.x,
|
|
1971
|
-
offsetY: world.y - node.y,
|
|
1972
|
-
moved: false
|
|
1973
|
-
};
|
|
2737
|
+
this.interaction = this.createDragInteraction(event, nodeId, groupIds);
|
|
1974
2738
|
this.capturePointer(event.pointerId);
|
|
1975
2739
|
}
|
|
1976
2740
|
return;
|
|
1977
2741
|
}
|
|
2742
|
+
if (event.shiftKey) {
|
|
2743
|
+
const startWorld = this.clientToWorld(event.clientX, event.clientY);
|
|
2744
|
+
this.interaction = {
|
|
2745
|
+
kind: "marquee",
|
|
2746
|
+
pointerId: event.pointerId,
|
|
2747
|
+
startWorld,
|
|
2748
|
+
baseIds: this.selection?.kind === "node" ? this.getSelectedNodeIds() : []
|
|
2749
|
+
};
|
|
2750
|
+
this.capturePointer(event.pointerId);
|
|
2751
|
+
event.preventDefault();
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
1978
2754
|
this.select(null);
|
|
1979
2755
|
this.reconnectEdgeId = null;
|
|
1980
2756
|
this.interaction = {
|
|
@@ -1989,20 +2765,123 @@ var VdFlowchart = class {
|
|
|
1989
2765
|
this.capturePointer(event.pointerId);
|
|
1990
2766
|
event.preventDefault();
|
|
1991
2767
|
}
|
|
2768
|
+
createDragInteraction(event, nodeId, nodeIds) {
|
|
2769
|
+
const nodes = nodeIds.map((id) => this.findNode(id)).filter(Boolean);
|
|
2770
|
+
const moving = new Set(nodes.map((node) => node.id));
|
|
2771
|
+
return {
|
|
2772
|
+
kind: "drag-node",
|
|
2773
|
+
pointerId: event.pointerId,
|
|
2774
|
+
nodeId,
|
|
2775
|
+
nodeIds: nodes.map((node) => node.id),
|
|
2776
|
+
startWorld: this.clientToWorld(event.clientX, event.clientY),
|
|
2777
|
+
origins: new Map(nodes.map((node) => [node.id, { x: node.x, y: node.y }])),
|
|
2778
|
+
bounds: getBounds(nodes),
|
|
2779
|
+
guideLines: this.snapGuides ? collectGuideLines(this.getVisibleNodes().filter((node) => !moving.has(node.id))) : null,
|
|
2780
|
+
snapDisabled: false,
|
|
2781
|
+
moved: false
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
// Translate every dragged node by the pointer delta, snapping the group's
|
|
2785
|
+
// bounds to the nearest alignment guide on each axis. Alt turns snapping
|
|
2786
|
+
// off for the rest of the gesture.
|
|
2787
|
+
moveDraggedNodes(event) {
|
|
2788
|
+
const interaction = this.interaction;
|
|
2789
|
+
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2790
|
+
let dx = world.x - interaction.startWorld.x;
|
|
2791
|
+
let dy = world.y - interaction.startWorld.y;
|
|
2792
|
+
if (event.altKey) interaction.snapDisabled = true;
|
|
2793
|
+
let guides = [];
|
|
2794
|
+
if (interaction.guideLines && !interaction.snapDisabled) {
|
|
2795
|
+
const box = interaction.bounds;
|
|
2796
|
+
const threshold = GUIDE_SNAP_DISTANCE / this.documentData.viewport.scale;
|
|
2797
|
+
const left = box.left + dx;
|
|
2798
|
+
const top = box.top + dy;
|
|
2799
|
+
const width = box.right - box.left;
|
|
2800
|
+
const height = box.bottom - box.top;
|
|
2801
|
+
const snapX = snapToGuides(
|
|
2802
|
+
interaction.guideLines.x,
|
|
2803
|
+
[left, left + width / 2, left + width],
|
|
2804
|
+
threshold
|
|
2805
|
+
);
|
|
2806
|
+
const snapY = snapToGuides(
|
|
2807
|
+
interaction.guideLines.y,
|
|
2808
|
+
[top, top + height / 2, top + height],
|
|
2809
|
+
threshold
|
|
2810
|
+
);
|
|
2811
|
+
if (snapX) dx += snapX.offset;
|
|
2812
|
+
if (snapY) dy += snapY.offset;
|
|
2813
|
+
const snapped = { left: box.left + dx, top: box.top + dy, width, height };
|
|
2814
|
+
if (snapX) guides = guides.concat(guideSegments("x", snapX.lines, snapped));
|
|
2815
|
+
if (snapY) guides = guides.concat(guideSegments("y", snapY.lines, snapped));
|
|
2816
|
+
}
|
|
2817
|
+
let changed = false;
|
|
2818
|
+
interaction.nodeIds.forEach((id) => {
|
|
2819
|
+
const node = this.findNode(id);
|
|
2820
|
+
const origin = interaction.origins.get(id);
|
|
2821
|
+
if (!node || !origin) return;
|
|
2822
|
+
const nextX = formatNumber(origin.x + dx);
|
|
2823
|
+
const nextY = formatNumber(origin.y + dy);
|
|
2824
|
+
if (nextX === node.x && nextY === node.y) return;
|
|
2825
|
+
node.x = nextX;
|
|
2826
|
+
node.y = nextY;
|
|
2827
|
+
changed = true;
|
|
2828
|
+
this.renderDraggedNode(node);
|
|
2829
|
+
});
|
|
2830
|
+
if (changed) interaction.moved = true;
|
|
2831
|
+
this.renderGuides(guides);
|
|
2832
|
+
}
|
|
2833
|
+
// Live marquee: select every visible node the box touches, added to the
|
|
2834
|
+
// selection the gesture started from.
|
|
2835
|
+
updateMarquee(event) {
|
|
2836
|
+
const interaction = this.interaction;
|
|
2837
|
+
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2838
|
+
const box = {
|
|
2839
|
+
left: Math.min(interaction.startWorld.x, world.x),
|
|
2840
|
+
top: Math.min(interaction.startWorld.y, world.y),
|
|
2841
|
+
right: Math.max(interaction.startWorld.x, world.x),
|
|
2842
|
+
bottom: Math.max(interaction.startWorld.y, world.y)
|
|
2843
|
+
};
|
|
2844
|
+
clearChildren(this.guidesLayer);
|
|
2845
|
+
this.guidesLayer.appendChild(
|
|
2846
|
+
svgEl("rect", {
|
|
2847
|
+
class: "vd-flowchart-marquee",
|
|
2848
|
+
x: formatNumber(box.left),
|
|
2849
|
+
y: formatNumber(box.top),
|
|
2850
|
+
width: formatNumber(box.right - box.left),
|
|
2851
|
+
height: formatNumber(box.bottom - box.top)
|
|
2852
|
+
})
|
|
2853
|
+
);
|
|
2854
|
+
const hits = this.getVisibleNodes().filter(
|
|
2855
|
+
(node) => node.x <= box.right && node.x + node.width >= box.left && node.y <= box.bottom && node.y + node.height >= box.top
|
|
2856
|
+
).map((node) => node.id);
|
|
2857
|
+
const ids = [.../* @__PURE__ */ new Set([...interaction.baseIds, ...hits])];
|
|
2858
|
+
const key = ids.join(",");
|
|
2859
|
+
if (key === interaction.lastKey) return;
|
|
2860
|
+
interaction.lastKey = key;
|
|
2861
|
+
this.selectNodes(ids, { primary: hits[hits.length - 1] });
|
|
2862
|
+
}
|
|
2863
|
+
renderGuides(guides) {
|
|
2864
|
+
clearChildren(this.guidesLayer);
|
|
2865
|
+
guides.forEach((guide) => {
|
|
2866
|
+
this.guidesLayer.appendChild(
|
|
2867
|
+
svgEl("line", {
|
|
2868
|
+
class: "vd-flowchart-guide",
|
|
2869
|
+
x1: guide.x1,
|
|
2870
|
+
y1: guide.y1,
|
|
2871
|
+
x2: guide.x2,
|
|
2872
|
+
y2: guide.y2
|
|
2873
|
+
})
|
|
2874
|
+
);
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
1992
2877
|
handlePointerMove(event) {
|
|
1993
2878
|
if (!this.interaction || this.interaction.pointerId !== event.pointerId) return;
|
|
1994
2879
|
if (this.interaction.kind === "drag-node") {
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
if (nextX !== node.x || nextY !== node.y) {
|
|
2001
|
-
node.x = nextX;
|
|
2002
|
-
node.y = nextY;
|
|
2003
|
-
this.interaction.moved = true;
|
|
2004
|
-
this.renderDraggedNode(node);
|
|
2005
|
-
}
|
|
2880
|
+
this.moveDraggedNodes(event);
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
if (this.interaction.kind === "marquee") {
|
|
2884
|
+
this.updateMarquee(event);
|
|
2006
2885
|
return;
|
|
2007
2886
|
}
|
|
2008
2887
|
if (this.interaction.kind === "resize-node") {
|
|
@@ -2037,6 +2916,14 @@ var VdFlowchart = class {
|
|
|
2037
2916
|
return;
|
|
2038
2917
|
}
|
|
2039
2918
|
if (this.interaction.kind === "connect") {
|
|
2919
|
+
if (!this.interaction.moved) {
|
|
2920
|
+
const travel = Math.hypot(
|
|
2921
|
+
event.clientX - this.interaction.startClientX,
|
|
2922
|
+
event.clientY - this.interaction.startClientY
|
|
2923
|
+
);
|
|
2924
|
+
if (!(travel > PORT_CLICK_SLOP)) return;
|
|
2925
|
+
this.interaction.moved = true;
|
|
2926
|
+
}
|
|
2040
2927
|
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2041
2928
|
const snapTarget = this.findConnectionTarget(
|
|
2042
2929
|
world,
|
|
@@ -2123,10 +3010,19 @@ var VdFlowchart = class {
|
|
|
2123
3010
|
}
|
|
2124
3011
|
return;
|
|
2125
3012
|
}
|
|
3013
|
+
if (interaction.kind === "marquee") {
|
|
3014
|
+
clearChildren(this.guidesLayer);
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
2126
3017
|
if (interaction.kind === "drag-node") {
|
|
3018
|
+
clearChildren(this.guidesLayer);
|
|
2127
3019
|
if (interaction.moved) {
|
|
2128
3020
|
this.render({ inspector: true, json: true });
|
|
2129
|
-
|
|
3021
|
+
if (interaction.nodeIds.length > 1) {
|
|
3022
|
+
this.emitChange("nodes:move", { nodeIds: [...interaction.nodeIds] });
|
|
3023
|
+
} else {
|
|
3024
|
+
this.emitChange("node:move", { node: deepClone(this.findNode(interaction.nodeId)) });
|
|
3025
|
+
}
|
|
2130
3026
|
} else {
|
|
2131
3027
|
this.render({ inspector: false, json: false });
|
|
2132
3028
|
}
|
|
@@ -2142,6 +3038,14 @@ var VdFlowchart = class {
|
|
|
2142
3038
|
return;
|
|
2143
3039
|
}
|
|
2144
3040
|
if (interaction.kind === "connect") {
|
|
3041
|
+
if (!interaction.moved && this.activeTool !== "arrow") {
|
|
3042
|
+
this.render({ inspector: false, json: false });
|
|
3043
|
+
this.insertBranchNode(interaction.source.nodeId, {
|
|
3044
|
+
direction: PORT_DIRECTIONS[interaction.source.port],
|
|
3045
|
+
edit: true
|
|
3046
|
+
});
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
2145
3049
|
const world = this.clientToWorld(event.clientX || 0, event.clientY || 0);
|
|
2146
3050
|
const snapTarget = interaction.target ? { node: this.findNode(interaction.target.nodeId), port: interaction.target.port } : this.findConnectionTarget(
|
|
2147
3051
|
world,
|
|
@@ -2217,7 +3121,7 @@ var VdFlowchart = class {
|
|
|
2217
3121
|
if (this.readonly) return false;
|
|
2218
3122
|
if (this.interaction?.kind === "connect" || this.interaction?.kind === "reconnect") return true;
|
|
2219
3123
|
if (this.activeTool === "arrow") return true;
|
|
2220
|
-
return this.selection?.kind === "node" && this.selection.id === node.id;
|
|
3124
|
+
return this.selection?.kind === "node" && this.selection.id === node.id && !this.hasMultiSelection();
|
|
2221
3125
|
}
|
|
2222
3126
|
getMarkerId(markerType, position, strokeWidth) {
|
|
2223
3127
|
const width = normalizeEdgeStrokeWidth(strokeWidth);
|
|
@@ -2240,7 +3144,7 @@ var VdFlowchart = class {
|
|
|
2240
3144
|
findConnectionTarget(worldPoint, excludeNodeId, previousSnap = null, referencePoint = null) {
|
|
2241
3145
|
const snapPadding = CONNECTION_SNAP_PADDING / this.documentData.viewport.scale;
|
|
2242
3146
|
const hysteresisMargin = CONNECTION_HYSTERESIS / this.documentData.viewport.scale;
|
|
2243
|
-
const best = this.
|
|
3147
|
+
const best = this.getVisibleNodes().reduce((candidate, node) => {
|
|
2244
3148
|
if (node.id === excludeNodeId) return candidate;
|
|
2245
3149
|
const distanceToBounds2 = getDistanceToNodeBounds(node, worldPoint);
|
|
2246
3150
|
if (distanceToBounds2 > snapPadding) return candidate;
|
|
@@ -2349,7 +3253,10 @@ var VdFlowchart = class {
|
|
|
2349
3253
|
updateToolbarLabel() {
|
|
2350
3254
|
this.zoomLabel.textContent = `${Math.round(this.documentData.viewport.scale * 100)}%`;
|
|
2351
3255
|
}
|
|
2352
|
-
|
|
3256
|
+
// Options: `initialText` replaces the label with typed text (type-to-edit);
|
|
3257
|
+
// `inserted` marks a node created by a keyboard/handle insertion so naming it
|
|
3258
|
+
// shares the insertion's undo step.
|
|
3259
|
+
startTextEdit(nodeId, options = {}) {
|
|
2353
3260
|
if (this.readonly) return false;
|
|
2354
3261
|
const node = this.findNode(nodeId);
|
|
2355
3262
|
if (!node || !isNodeTextEditable(node)) return false;
|
|
@@ -2361,9 +3268,10 @@ var VdFlowchart = class {
|
|
|
2361
3268
|
}
|
|
2362
3269
|
this.stopTextEdit({ commit: true });
|
|
2363
3270
|
this.select({ kind: "node", id: node.id });
|
|
3271
|
+
const replacing = typeof options.initialText === "string";
|
|
2364
3272
|
const textarea = createElement("textarea", {
|
|
2365
3273
|
className: `vd-flowchart-text-editor vd-flowchart-text-editor--${node.type}`,
|
|
2366
|
-
value: node.text,
|
|
3274
|
+
value: replacing ? options.initialText : node.text,
|
|
2367
3275
|
rows: 1
|
|
2368
3276
|
});
|
|
2369
3277
|
textarea.setAttribute("data-node-id", node.id);
|
|
@@ -2372,19 +3280,7 @@ var VdFlowchart = class {
|
|
|
2372
3280
|
textarea.addEventListener("input", () => this.positionTextEditor());
|
|
2373
3281
|
textarea.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
2374
3282
|
textarea.addEventListener("dblclick", (event) => event.stopPropagation());
|
|
2375
|
-
textarea.addEventListener("keydown", (event) =>
|
|
2376
|
-
if (event.key === "Escape") {
|
|
2377
|
-
event.preventDefault();
|
|
2378
|
-
this.stopTextEdit({ commit: false });
|
|
2379
|
-
this.canvasEl.focus();
|
|
2380
|
-
return;
|
|
2381
|
-
}
|
|
2382
|
-
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
2383
|
-
event.preventDefault();
|
|
2384
|
-
this.stopTextEdit({ commit: true });
|
|
2385
|
-
this.canvasEl.focus();
|
|
2386
|
-
}
|
|
2387
|
-
});
|
|
3283
|
+
textarea.addEventListener("keydown", (event) => this.handleTextEditorKeyDown(event, node));
|
|
2388
3284
|
textarea.addEventListener("blur", () => {
|
|
2389
3285
|
if (this.textEditor?.textarea === textarea) {
|
|
2390
3286
|
this.stopTextEdit({ commit: true });
|
|
@@ -2393,18 +3289,97 @@ var VdFlowchart = class {
|
|
|
2393
3289
|
this.textEditor = {
|
|
2394
3290
|
nodeId: node.id,
|
|
2395
3291
|
previousText: node.text,
|
|
3292
|
+
inserted: Boolean(options.inserted),
|
|
2396
3293
|
textarea
|
|
2397
3294
|
};
|
|
2398
3295
|
this.canvasEl.appendChild(textarea);
|
|
2399
3296
|
this.render({ inspector: true, json: false });
|
|
2400
|
-
|
|
2401
|
-
if (this.textEditor?.textarea
|
|
2402
|
-
|
|
3297
|
+
const focusEditor = () => {
|
|
3298
|
+
if (this.textEditor?.textarea !== textarea) return;
|
|
3299
|
+
textarea.focus();
|
|
3300
|
+
if (replacing) {
|
|
3301
|
+
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
3302
|
+
} else {
|
|
2403
3303
|
textarea.select();
|
|
2404
3304
|
}
|
|
3305
|
+
};
|
|
3306
|
+
focusEditor();
|
|
3307
|
+
window.requestAnimationFrame(() => {
|
|
3308
|
+
if (document.activeElement !== textarea) focusEditor();
|
|
3309
|
+
});
|
|
3310
|
+
return true;
|
|
3311
|
+
}
|
|
3312
|
+
// Inline label editor for a connection, placed on the label anchor
|
|
3313
|
+
// (curve midpoint). Labels render on one line, so Enter always saves.
|
|
3314
|
+
startEdgeLabelEdit(edgeId, options = {}) {
|
|
3315
|
+
if (this.readonly) return false;
|
|
3316
|
+
const edge = this.findEdge(sanitizeId(edgeId));
|
|
3317
|
+
if (!edge) return false;
|
|
3318
|
+
this.stopTextEdit({ commit: true });
|
|
3319
|
+
this.select({ kind: "edge", id: edge.id });
|
|
3320
|
+
const replacing = typeof options.initialText === "string";
|
|
3321
|
+
const textarea = createElement("textarea", {
|
|
3322
|
+
className: "vd-flowchart-text-editor vd-flowchart-text-editor--edge",
|
|
3323
|
+
value: replacing ? options.initialText : edge.label,
|
|
3324
|
+
rows: 1
|
|
3325
|
+
});
|
|
3326
|
+
textarea.setAttribute("data-edge-id", edge.id);
|
|
3327
|
+
textarea.setAttribute("aria-label", "Edit connection label");
|
|
3328
|
+
textarea.spellcheck = false;
|
|
3329
|
+
textarea.addEventListener("input", () => this.positionTextEditor());
|
|
3330
|
+
textarea.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
3331
|
+
textarea.addEventListener("dblclick", (event) => event.stopPropagation());
|
|
3332
|
+
textarea.addEventListener("keydown", (event) => {
|
|
3333
|
+
if (event.isComposing) return;
|
|
3334
|
+
if (event.key === "Escape" || event.key === "Enter") {
|
|
3335
|
+
event.preventDefault();
|
|
3336
|
+
this.stopTextEdit({ commit: event.key === "Enter" });
|
|
3337
|
+
this.canvasEl.focus();
|
|
3338
|
+
}
|
|
3339
|
+
});
|
|
3340
|
+
textarea.addEventListener("blur", () => {
|
|
3341
|
+
if (this.textEditor?.textarea === textarea) this.stopTextEdit({ commit: true });
|
|
3342
|
+
});
|
|
3343
|
+
this.textEditor = { nodeId: null, edgeId: edge.id, previousText: edge.label, textarea };
|
|
3344
|
+
this.canvasEl.appendChild(textarea);
|
|
3345
|
+
this.render({ inspector: true, json: false });
|
|
3346
|
+
const focusEditor = () => {
|
|
3347
|
+
if (this.textEditor?.textarea !== textarea) return;
|
|
3348
|
+
textarea.focus();
|
|
3349
|
+
if (replacing) textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
3350
|
+
else textarea.select();
|
|
3351
|
+
};
|
|
3352
|
+
focusEditor();
|
|
3353
|
+
window.requestAnimationFrame(() => {
|
|
3354
|
+
if (document.activeElement !== textarea) focusEditor();
|
|
2405
3355
|
});
|
|
2406
3356
|
return true;
|
|
2407
3357
|
}
|
|
3358
|
+
handleTextEditorKeyDown(event, node) {
|
|
3359
|
+
if (event.isComposing) return;
|
|
3360
|
+
const mindmap = this.keyboardShortcuts === "mindmap";
|
|
3361
|
+
if (event.key === "Escape") {
|
|
3362
|
+
event.preventDefault();
|
|
3363
|
+
this.stopTextEdit({ commit: false });
|
|
3364
|
+
this.canvasEl.focus();
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
if (event.key === "Enter") {
|
|
3368
|
+
const modKey = event.ctrlKey || event.metaKey;
|
|
3369
|
+
const commits = modKey || mindmap && node.type !== "textbox" && !event.shiftKey;
|
|
3370
|
+
if (!commits) return;
|
|
3371
|
+
event.preventDefault();
|
|
3372
|
+
this.stopTextEdit({ commit: true });
|
|
3373
|
+
this.canvasEl.focus();
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
if (event.key === "Tab" && mindmap && !event.shiftKey && !event.altKey && !event.ctrlKey) {
|
|
3377
|
+
event.preventDefault();
|
|
3378
|
+
this.stopTextEdit({ commit: true });
|
|
3379
|
+
this.canvasEl.focus();
|
|
3380
|
+
this.insertBranchNode(node.id, { edit: true });
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
2408
3383
|
stopTextEdit(options = {}) {
|
|
2409
3384
|
if (!this.textEditor) return false;
|
|
2410
3385
|
const commit = options.commit !== false;
|
|
@@ -2412,12 +3387,26 @@ var VdFlowchart = class {
|
|
|
2412
3387
|
const nextText = editor.textarea.value;
|
|
2413
3388
|
this.textEditor = null;
|
|
2414
3389
|
editor.textarea.remove();
|
|
3390
|
+
if (editor.edgeId) {
|
|
3391
|
+
const label = nextText.replace(/\s*\n\s*/g, " ").trim();
|
|
3392
|
+
if (commit && !this.readonly && label !== editor.previousText) {
|
|
3393
|
+
this.updateEdge(editor.edgeId, { label }, { inspector: true, reason: "edge:update" });
|
|
3394
|
+
} else {
|
|
3395
|
+
this.render({ scene: true, inspector: false, json: false });
|
|
3396
|
+
}
|
|
3397
|
+
return true;
|
|
3398
|
+
}
|
|
2415
3399
|
if (commit && !this.readonly && nextText !== editor.previousText) {
|
|
2416
|
-
this.
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
3400
|
+
this.coalesceInsertedText = editor.inserted;
|
|
3401
|
+
try {
|
|
3402
|
+
this.updateNode(
|
|
3403
|
+
editor.nodeId,
|
|
3404
|
+
{ text: nextText },
|
|
3405
|
+
{ inspector: true, reason: "node:update" }
|
|
3406
|
+
);
|
|
3407
|
+
} finally {
|
|
3408
|
+
this.coalesceInsertedText = false;
|
|
3409
|
+
}
|
|
2421
3410
|
} else {
|
|
2422
3411
|
this.render({ scene: true, inspector: false, json: false });
|
|
2423
3412
|
}
|
|
@@ -2425,6 +3414,10 @@ var VdFlowchart = class {
|
|
|
2425
3414
|
}
|
|
2426
3415
|
positionTextEditor() {
|
|
2427
3416
|
if (!this.textEditor) return;
|
|
3417
|
+
if (this.textEditor.edgeId) {
|
|
3418
|
+
this.positionEdgeLabelEditor();
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
2428
3421
|
const node = this.findNode(this.textEditor.nodeId);
|
|
2429
3422
|
if (!node) {
|
|
2430
3423
|
this.stopTextEdit({ commit: false });
|
|
@@ -2448,6 +3441,28 @@ var VdFlowchart = class {
|
|
|
2448
3441
|
textarea.style.lineHeight = `${formatNumber(lineHeight)}px`;
|
|
2449
3442
|
textarea.style.padding = `${formatNumber(verticalPadding)}px ${formatNumber(horizontalPadding)}px`;
|
|
2450
3443
|
}
|
|
3444
|
+
positionEdgeLabelEditor() {
|
|
3445
|
+
const edge = this.findEdge(this.textEditor.edgeId);
|
|
3446
|
+
const fromNode = edge && this.findNode(edge.from.nodeId);
|
|
3447
|
+
const toNode = edge && this.findNode(edge.to.nodeId);
|
|
3448
|
+
if (!edge || !fromNode || !toNode) {
|
|
3449
|
+
this.stopTextEdit({ commit: false });
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
|
|
3453
|
+
const viewport = this.documentData.viewport;
|
|
3454
|
+
const textarea = this.textEditor.textarea;
|
|
3455
|
+
const fontSize = Math.max(11, 13 * viewport.scale);
|
|
3456
|
+
const width = clamp(textarea.value.length * fontSize * 0.62 + 28, 96, 280);
|
|
3457
|
+
const height = fontSize * 1.4 + 12;
|
|
3458
|
+
textarea.style.left = `${formatNumber(viewport.x + labelX * viewport.scale - width / 2)}px`;
|
|
3459
|
+
textarea.style.top = `${formatNumber(viewport.y + (labelY - 8) * viewport.scale - height / 2)}px`;
|
|
3460
|
+
textarea.style.width = `${formatNumber(width)}px`;
|
|
3461
|
+
textarea.style.height = `${formatNumber(height)}px`;
|
|
3462
|
+
textarea.style.fontSize = `${formatNumber(fontSize)}px`;
|
|
3463
|
+
textarea.style.lineHeight = `${formatNumber(fontSize * 1.4)}px`;
|
|
3464
|
+
textarea.style.padding = "6px 10px";
|
|
3465
|
+
}
|
|
2451
3466
|
render(options = {}) {
|
|
2452
3467
|
if (this.destroyed) return;
|
|
2453
3468
|
const shouldRenderScene = options.scene !== false;
|
|
@@ -2460,8 +3475,10 @@ var VdFlowchart = class {
|
|
|
2460
3475
|
this.updateToolbarLabel();
|
|
2461
3476
|
this.updatePaletteState();
|
|
2462
3477
|
this.positionTextEditor();
|
|
3478
|
+
this.scheduleMinimap();
|
|
2463
3479
|
}
|
|
2464
3480
|
renderScene() {
|
|
3481
|
+
this.syncSelectedNodeIds();
|
|
2465
3482
|
const viewport = this.documentData.viewport;
|
|
2466
3483
|
this.world.setAttribute(
|
|
2467
3484
|
"transform",
|
|
@@ -2471,7 +3488,12 @@ var VdFlowchart = class {
|
|
|
2471
3488
|
clearChildren(this.previewLayer);
|
|
2472
3489
|
clearChildren(this.nodesLayer);
|
|
2473
3490
|
clearChildren(this.overlayLayer);
|
|
2474
|
-
const
|
|
3491
|
+
const hidden = this.getHiddenNodeIds();
|
|
3492
|
+
this.sceneHidden = hidden;
|
|
3493
|
+
this.sceneOutgoing = this.getOutgoingMap();
|
|
3494
|
+
const nodeMap = new Map(
|
|
3495
|
+
this.documentData.nodes.filter((node) => !hidden.has(node.id)).map((node) => [node.id, node])
|
|
3496
|
+
);
|
|
2475
3497
|
this.sceneNodeMap = nodeMap;
|
|
2476
3498
|
this.nodeElements = /* @__PURE__ */ new Map();
|
|
2477
3499
|
this.edgeElements = /* @__PURE__ */ new Map();
|
|
@@ -2491,6 +3513,7 @@ var VdFlowchart = class {
|
|
|
2491
3513
|
this.previewLayer.appendChild(this.renderPreviewEdge(this.interaction));
|
|
2492
3514
|
}
|
|
2493
3515
|
this.documentData.nodes.forEach((node) => {
|
|
3516
|
+
if (hidden.has(node.id)) return;
|
|
2494
3517
|
const element = this.renderNode(node);
|
|
2495
3518
|
this.nodesLayer.appendChild(element);
|
|
2496
3519
|
this.nodeElements.set(node.id, element);
|
|
@@ -2578,7 +3601,7 @@ var VdFlowchart = class {
|
|
|
2578
3601
|
});
|
|
2579
3602
|
group.appendChild(visiblePath);
|
|
2580
3603
|
group.appendChild(hitPath);
|
|
2581
|
-
if (edge.label) {
|
|
3604
|
+
if (edge.label && this.textEditor?.edgeId !== edge.id) {
|
|
2582
3605
|
const label = svgEl("text", {
|
|
2583
3606
|
class: "vd-flowchart-edge-label",
|
|
2584
3607
|
x: edgePath.labelX,
|
|
@@ -2639,8 +3662,9 @@ var VdFlowchart = class {
|
|
|
2639
3662
|
});
|
|
2640
3663
|
}
|
|
2641
3664
|
renderNode(node) {
|
|
2642
|
-
const selected = this.
|
|
2643
|
-
const
|
|
3665
|
+
const selected = this.isNodeSelected(node.id);
|
|
3666
|
+
const soloSelected = selected && !this.hasMultiSelection();
|
|
3667
|
+
const dragging = this.interaction?.kind === "drag-node" && this.interaction.nodeIds.includes(node.id);
|
|
2644
3668
|
const resizing = this.interaction?.kind === "resize-node" && this.interaction.nodeId === node.id;
|
|
2645
3669
|
const editing = this.textEditor?.nodeId === node.id;
|
|
2646
3670
|
const group = svgEl("g", {
|
|
@@ -2661,12 +3685,29 @@ var VdFlowchart = class {
|
|
|
2661
3685
|
group.appendChild(hitbox);
|
|
2662
3686
|
group.appendChild(this.renderNodeShape(node));
|
|
2663
3687
|
group.appendChild(this.renderNodeText(node));
|
|
2664
|
-
|
|
3688
|
+
const scale = this.documentData.viewport.scale || 1;
|
|
3689
|
+
if (soloSelected && !this.readonly && SELECTION_BOX_TYPES.has(node.type)) {
|
|
3690
|
+
const outset = SELECTION_OUTSET / scale;
|
|
3691
|
+
group.appendChild(
|
|
3692
|
+
svgEl("rect", {
|
|
3693
|
+
class: "vd-flowchart-selection-box",
|
|
3694
|
+
x: -outset,
|
|
3695
|
+
y: -outset,
|
|
3696
|
+
width: node.width + outset * 2,
|
|
3697
|
+
height: node.height + outset * 2
|
|
3698
|
+
})
|
|
3699
|
+
);
|
|
3700
|
+
}
|
|
3701
|
+
if (soloSelected && !this.readonly && isNodeResizable(node)) {
|
|
2665
3702
|
group.appendChild(this.renderResizeControls(node));
|
|
2666
3703
|
}
|
|
2667
|
-
const
|
|
3704
|
+
const collapseToggle = this.renderCollapseToggle(node, soloSelected);
|
|
3705
|
+
if (this.readonly) {
|
|
3706
|
+
if (collapseToggle) group.appendChild(collapseToggle);
|
|
3707
|
+
return group;
|
|
3708
|
+
}
|
|
2668
3709
|
FLOWCHART_PORTS.forEach((port) => {
|
|
2669
|
-
const position =
|
|
3710
|
+
const position = getPortHandlePosition({ ...node, x: 0, y: 0 }, port, scale);
|
|
2670
3711
|
const portGroup = svgEl("g", {
|
|
2671
3712
|
class: `vd-flowchart-port-group${portsVisible ? " is-visible" : ""}`,
|
|
2672
3713
|
"data-node-id": node.id,
|
|
@@ -2688,15 +3729,68 @@ var VdFlowchart = class {
|
|
|
2688
3729
|
r: CONNECTION_PORT_RADIUS / scale
|
|
2689
3730
|
})
|
|
2690
3731
|
);
|
|
3732
|
+
const arm = (CONNECTION_PORT_RADIUS - 2.5) / scale;
|
|
3733
|
+
portGroup.appendChild(
|
|
3734
|
+
svgEl("path", {
|
|
3735
|
+
class: "vd-flowchart-port-plus",
|
|
3736
|
+
d: `M ${formatNumber(position.x - arm)} ${formatNumber(position.y)} H ${formatNumber(position.x + arm)} M ${formatNumber(position.x)} ${formatNumber(position.y - arm)} V ${formatNumber(position.y + arm)}`
|
|
3737
|
+
})
|
|
3738
|
+
);
|
|
2691
3739
|
group.appendChild(portGroup);
|
|
2692
3740
|
});
|
|
3741
|
+
if (collapseToggle) group.appendChild(collapseToggle);
|
|
3742
|
+
return group;
|
|
3743
|
+
}
|
|
3744
|
+
// "+N" on a collapsed node, or "−" on a single selected node with children,
|
|
3745
|
+
// placed beyond the connection handle on the side its branch leaves from.
|
|
3746
|
+
renderCollapseToggle(node, soloSelected) {
|
|
3747
|
+
const outgoing = this.sceneOutgoing || this.getOutgoingMap();
|
|
3748
|
+
const childIds = (outgoing.get(node.id) || []).filter((id) => id !== node.id);
|
|
3749
|
+
if (!childIds.length) return null;
|
|
3750
|
+
const count = node.collapsed ? this.countCollapsedDescendants(
|
|
3751
|
+
node.id,
|
|
3752
|
+
this.sceneHidden || this.getHiddenNodeIds(),
|
|
3753
|
+
outgoing
|
|
3754
|
+
) : 0;
|
|
3755
|
+
if (!node.collapsed && !(soloSelected && !this.readonly)) return null;
|
|
3756
|
+
const sides = /* @__PURE__ */ new Map();
|
|
3757
|
+
this.documentData.edges.forEach((edge) => {
|
|
3758
|
+
if (edge.from.nodeId !== node.id || edge.to.nodeId === node.id) return;
|
|
3759
|
+
sides.set(edge.from.port, (sides.get(edge.from.port) || 0) + 1);
|
|
3760
|
+
});
|
|
3761
|
+
const port = [...sides].sort((a, b) => b[1] - a[1])[0]?.[0] || DIRECTION_PORTS[this.getGrowthDirection(node)];
|
|
3762
|
+
const scale = this.documentData.viewport.scale || 1;
|
|
3763
|
+
const anchor = getPortPosition({ ...node, x: 0, y: 0 }, port);
|
|
3764
|
+
const normal = getPortNormal(port);
|
|
3765
|
+
const offset = COLLAPSE_TOGGLE_OFFSET / scale;
|
|
3766
|
+
const cx = formatNumber(anchor.x + normal.x * offset);
|
|
3767
|
+
const cy = formatNumber(anchor.y + normal.y * offset);
|
|
3768
|
+
const label = node.collapsed ? `+${count}` : "\u2212";
|
|
3769
|
+
const radius = (node.collapsed && label.length > 2 ? 12 : 9) / scale;
|
|
3770
|
+
const group = svgEl("g", {
|
|
3771
|
+
class: `vd-flowchart-collapse-toggle${node.collapsed ? " is-collapsed" : ""}`,
|
|
3772
|
+
"data-collapse-toggle": node.id
|
|
3773
|
+
});
|
|
3774
|
+
const title = svgEl("title");
|
|
3775
|
+
title.textContent = node.collapsed ? `Expand ${count} hidden nodes` : "Collapse branch";
|
|
3776
|
+
group.appendChild(title);
|
|
3777
|
+
group.appendChild(svgEl("circle", { cx, cy, r: formatNumber(radius) }));
|
|
3778
|
+
const text = svgEl("text", {
|
|
3779
|
+
x: cx,
|
|
3780
|
+
y: cy,
|
|
3781
|
+
"font-size": formatNumber(10 / scale),
|
|
3782
|
+
"text-anchor": "middle",
|
|
3783
|
+
"dominant-baseline": "central"
|
|
3784
|
+
});
|
|
3785
|
+
text.textContent = label;
|
|
3786
|
+
group.appendChild(text);
|
|
2693
3787
|
return group;
|
|
2694
3788
|
}
|
|
2695
3789
|
renderResizeControls(node) {
|
|
2696
3790
|
const scale = this.documentData.viewport.scale || 1;
|
|
2697
3791
|
const zone = 14 / scale;
|
|
2698
3792
|
const cornerZone = 22 / scale;
|
|
2699
|
-
const handleSize =
|
|
3793
|
+
const handleSize = RESIZE_HANDLE_SIZE / scale;
|
|
2700
3794
|
const gap = RESIZE_PORT_GAP / scale;
|
|
2701
3795
|
const group = svgEl("g", { class: "vd-flowchart-resize-controls" });
|
|
2702
3796
|
const zones = {
|
|
@@ -2726,12 +3820,6 @@ var VdFlowchart = class {
|
|
|
2726
3820
|
};
|
|
2727
3821
|
RESIZE_HANDLES.forEach((handle) => {
|
|
2728
3822
|
const zoneRect = zones[handle];
|
|
2729
|
-
const position = getResizeHandlePosition(node, handle);
|
|
2730
|
-
const dotOffset = 8 / scale;
|
|
2731
|
-
if (handle.includes("e")) position.x += dotOffset;
|
|
2732
|
-
if (handle.includes("w")) position.x -= dotOffset;
|
|
2733
|
-
if (handle.includes("n")) position.y -= dotOffset;
|
|
2734
|
-
if (handle.includes("s")) position.y += dotOffset;
|
|
2735
3823
|
const cursor = getResizeCursor(handle);
|
|
2736
3824
|
const zoneSegments = [];
|
|
2737
3825
|
if (handle === "e" || handle === "w") {
|
|
@@ -2766,14 +3854,19 @@ var VdFlowchart = class {
|
|
|
2766
3854
|
});
|
|
2767
3855
|
group.appendChild(hit);
|
|
2768
3856
|
});
|
|
3857
|
+
if (handle.length === 1) return;
|
|
3858
|
+
const position = getResizeHandlePosition(node, handle);
|
|
3859
|
+
const outset = SELECTION_OUTSET / scale;
|
|
3860
|
+
position.x += handle.includes("e") ? outset : -outset;
|
|
3861
|
+
position.y += handle.includes("s") ? outset : -outset;
|
|
2769
3862
|
const dot = svgEl("rect", {
|
|
2770
3863
|
class: "vd-flowchart-resize-handle",
|
|
2771
3864
|
x: position.x - handleSize / 2,
|
|
2772
3865
|
y: position.y - handleSize / 2,
|
|
2773
3866
|
width: handleSize,
|
|
2774
3867
|
height: handleSize,
|
|
2775
|
-
rx: handleSize /
|
|
2776
|
-
ry: handleSize /
|
|
3868
|
+
rx: handleSize / 5,
|
|
3869
|
+
ry: handleSize / 5,
|
|
2777
3870
|
"data-node-id": node.id,
|
|
2778
3871
|
"data-resize-handle": handle,
|
|
2779
3872
|
style: `cursor: ${cursor}`
|
|
@@ -2902,6 +3995,16 @@ var VdFlowchart = class {
|
|
|
2902
3995
|
);
|
|
2903
3996
|
return;
|
|
2904
3997
|
}
|
|
3998
|
+
if (this.hasMultiSelection()) {
|
|
3999
|
+
this.selectionMeta.textContent = `${this.selectedNodeIds.size} nodes selected`;
|
|
4000
|
+
this.selectionFields.appendChild(
|
|
4001
|
+
createElement("p", {
|
|
4002
|
+
className: "vd-flowchart-selection-empty",
|
|
4003
|
+
text: "Drag, nudge with Alt+Arrow, copy, duplicate, or delete them together."
|
|
4004
|
+
})
|
|
4005
|
+
);
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
2905
4008
|
if (this.selection.kind === "node") {
|
|
2906
4009
|
const node = this.findNode(this.selection.id);
|
|
2907
4010
|
if (!node) {
|
|
@@ -3016,16 +4119,69 @@ var VdFlowchart = class {
|
|
|
3016
4119
|
}
|
|
3017
4120
|
select(selection) {
|
|
3018
4121
|
const next = selection && selection.id && selection.kind ? { kind: selection.kind, id: selection.id } : null;
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
4122
|
+
this.applySelection(next, next?.kind === "node" ? [next.id] : []);
|
|
4123
|
+
}
|
|
4124
|
+
// Single funnel for selection changes: `selection` is the primary item and
|
|
4125
|
+
// `selectedNodeIds` every selected node (the primary one included).
|
|
4126
|
+
applySelection(primary, nodeIds) {
|
|
4127
|
+
const previousKey = this.getSelectionKey();
|
|
4128
|
+
this.selection = primary;
|
|
4129
|
+
this.selectedNodeIds = new Set(nodeIds);
|
|
4130
|
+
if (primary?.kind === "node") this.selectedNodeIds.add(primary.id);
|
|
4131
|
+
if (previousKey !== this.getSelectionKey()) {
|
|
4132
|
+
this.edgeCycleAnchor = null;
|
|
3023
4133
|
this.render({ scene: true, inspector: true, json: false });
|
|
3024
|
-
this.emit("select", {
|
|
4134
|
+
this.emit("select", {
|
|
4135
|
+
selection: this.getSelectionSnapshot(),
|
|
4136
|
+
nodeIds: this.getSelectedNodeIds()
|
|
4137
|
+
});
|
|
3025
4138
|
return;
|
|
3026
4139
|
}
|
|
3027
4140
|
this.render({ scene: true, inspector: false, json: false });
|
|
3028
4141
|
}
|
|
4142
|
+
getSelectionKey() {
|
|
4143
|
+
const primary = this.selection ? `${this.selection.kind}:${this.selection.id}` : "";
|
|
4144
|
+
return `${primary}|${[...this.selectedNodeIds || []].sort().join(",")}`;
|
|
4145
|
+
}
|
|
4146
|
+
// Keep the set valid after mutations that bypass applySelection (undo,
|
|
4147
|
+
// load, removal): drop missing nodes and keep the primary node in the set.
|
|
4148
|
+
syncSelectedNodeIds() {
|
|
4149
|
+
if (this.selection?.kind !== "node") {
|
|
4150
|
+
this.selectedNodeIds = /* @__PURE__ */ new Set();
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
const existing = new Set(this.documentData.nodes.map((node) => node.id));
|
|
4154
|
+
const hidden = this.getHiddenNodeIds();
|
|
4155
|
+
this.selectedNodeIds = new Set(
|
|
4156
|
+
[...this.selectedNodeIds || []].filter((id) => existing.has(id) && !hidden.has(id))
|
|
4157
|
+
);
|
|
4158
|
+
this.selectedNodeIds.add(this.selection.id);
|
|
4159
|
+
}
|
|
4160
|
+
getSelectedNodeIds() {
|
|
4161
|
+
return [...this.selectedNodeIds || []];
|
|
4162
|
+
}
|
|
4163
|
+
isNodeSelected(nodeId) {
|
|
4164
|
+
return Boolean(this.selectedNodeIds?.has(nodeId));
|
|
4165
|
+
}
|
|
4166
|
+
hasMultiSelection() {
|
|
4167
|
+
return (this.selectedNodeIds?.size || 0) > 1;
|
|
4168
|
+
}
|
|
4169
|
+
/** Select several nodes; `primary` (default: the last id) drives single-node UI. */
|
|
4170
|
+
selectNodes(nodeIds, options = {}) {
|
|
4171
|
+
const existing = new Set(this.documentData.nodes.map((node) => node.id));
|
|
4172
|
+
const ids = [...new Set((nodeIds || []).map(sanitizeId))].filter((id) => existing.has(id));
|
|
4173
|
+
const primaryId = ids.includes(options.primary) ? options.primary : ids[ids.length - 1];
|
|
4174
|
+
this.applySelection(primaryId ? { kind: "node", id: primaryId } : null, ids);
|
|
4175
|
+
return this;
|
|
4176
|
+
}
|
|
4177
|
+
toggleNodeSelection(nodeId) {
|
|
4178
|
+
const ids = this.selection?.kind === "node" ? this.getSelectedNodeIds() : [];
|
|
4179
|
+
if (ids.includes(nodeId)) {
|
|
4180
|
+
const rest = ids.filter((id) => id !== nodeId);
|
|
4181
|
+
return this.selectNodes(rest, { primary: rest[rest.length - 1] });
|
|
4182
|
+
}
|
|
4183
|
+
return this.selectNodes([...ids, nodeId], { primary: nodeId });
|
|
4184
|
+
}
|
|
3029
4185
|
selectNode(nodeId) {
|
|
3030
4186
|
this.select({ kind: "node", id: sanitizeId(nodeId) });
|
|
3031
4187
|
return this;
|
|
@@ -3046,6 +4202,69 @@ var VdFlowchart = class {
|
|
|
3046
4202
|
findNode(nodeId) {
|
|
3047
4203
|
return this.documentData.nodes.find((node) => node.id === nodeId) || null;
|
|
3048
4204
|
}
|
|
4205
|
+
getHiddenNodeIds() {
|
|
4206
|
+
return computeHiddenNodeIds(this.documentData.nodes, this.documentData.edges);
|
|
4207
|
+
}
|
|
4208
|
+
getVisibleNodes() {
|
|
4209
|
+
const hidden = this.getHiddenNodeIds();
|
|
4210
|
+
return hidden.size ? this.documentData.nodes.filter((node) => !hidden.has(node.id)) : this.documentData.nodes;
|
|
4211
|
+
}
|
|
4212
|
+
isNodeHidden(nodeId) {
|
|
4213
|
+
return this.getHiddenNodeIds().has(nodeId);
|
|
4214
|
+
}
|
|
4215
|
+
getOutgoingMap() {
|
|
4216
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
4217
|
+
this.documentData.edges.forEach((edge) => {
|
|
4218
|
+
if (!outgoing.has(edge.from.nodeId)) outgoing.set(edge.from.nodeId, []);
|
|
4219
|
+
outgoing.get(edge.from.nodeId).push(edge.to.nodeId);
|
|
4220
|
+
});
|
|
4221
|
+
return outgoing;
|
|
4222
|
+
}
|
|
4223
|
+
// Hidden nodes under one collapsed node (its "+N" count).
|
|
4224
|
+
countCollapsedDescendants(nodeId, hidden = this.getHiddenNodeIds(), outgoing = this.getOutgoingMap()) {
|
|
4225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4226
|
+
const queue = [nodeId];
|
|
4227
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
4228
|
+
(outgoing.get(queue[index]) || []).forEach((childId) => {
|
|
4229
|
+
if (seen.has(childId) || !hidden.has(childId)) return;
|
|
4230
|
+
seen.add(childId);
|
|
4231
|
+
queue.push(childId);
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
return seen.size;
|
|
4235
|
+
}
|
|
4236
|
+
setCollapsed(nodeId, collapsed) {
|
|
4237
|
+
if (this.readonly) return false;
|
|
4238
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4239
|
+
if (!node) return false;
|
|
4240
|
+
const next = Boolean(collapsed);
|
|
4241
|
+
if (Boolean(node.collapsed) === next) return false;
|
|
4242
|
+
if (next && !this.getChildNodes(node.id).length) return false;
|
|
4243
|
+
if (next) node.collapsed = true;
|
|
4244
|
+
else delete node.collapsed;
|
|
4245
|
+
if (this.selection?.kind === "node") {
|
|
4246
|
+
const hidden = this.getHiddenNodeIds();
|
|
4247
|
+
const visibleIds = this.getSelectedNodeIds().filter((id) => !hidden.has(id));
|
|
4248
|
+
if (hidden.has(this.selection.id)) {
|
|
4249
|
+
this.select({ kind: "node", id: node.id });
|
|
4250
|
+
} else if (visibleIds.length !== this.selectedNodeIds.size) {
|
|
4251
|
+
this.selectNodes(visibleIds, { primary: this.selection.id });
|
|
4252
|
+
}
|
|
4253
|
+
} else if (this.selection?.kind === "edge") {
|
|
4254
|
+
const edge = this.findEdge(this.selection.id);
|
|
4255
|
+
const hidden = this.getHiddenNodeIds();
|
|
4256
|
+
if (edge && (hidden.has(edge.from.nodeId) || hidden.has(edge.to.nodeId))) {
|
|
4257
|
+
this.select({ kind: "node", id: node.id });
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
this.render({ inspector: true, json: true });
|
|
4261
|
+
this.emitChange("node:collapse", { node: deepClone(node) });
|
|
4262
|
+
return true;
|
|
4263
|
+
}
|
|
4264
|
+
toggleCollapsed(nodeId) {
|
|
4265
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4266
|
+
return node ? this.setCollapsed(node.id, !node.collapsed) : false;
|
|
4267
|
+
}
|
|
3049
4268
|
findEdge(edgeId) {
|
|
3050
4269
|
return this.documentData.edges.find((edge) => edge.id === edgeId) || null;
|
|
3051
4270
|
}
|
|
@@ -3056,7 +4275,8 @@ var VdFlowchart = class {
|
|
|
3056
4275
|
}
|
|
3057
4276
|
emitChange(reason, extra = {}) {
|
|
3058
4277
|
if (this.historyEnabled && !this.isApplyingHistory) {
|
|
3059
|
-
this.
|
|
4278
|
+
if (this.historyBatchDepth > 0) this.historyBatchDirty = true;
|
|
4279
|
+
else this.recordHistory(reason);
|
|
3060
4280
|
}
|
|
3061
4281
|
this.syncJsonTextarea();
|
|
3062
4282
|
this.emit("change", {
|
|
@@ -3100,7 +4320,9 @@ var VdFlowchart = class {
|
|
|
3100
4320
|
const snapshot = this.toJSON();
|
|
3101
4321
|
const targetKey = this.selection ? `${this.selection.kind}:${this.selection.id}` : "";
|
|
3102
4322
|
const top = this.history[this.historyIndex];
|
|
3103
|
-
const
|
|
4323
|
+
const atTip = Boolean(top) && this.historyIndex === this.history.length - 1;
|
|
4324
|
+
const namesInsertedNode = this.coalesceInsertedText && top?.reason === "node:insert" && reason === "node:update";
|
|
4325
|
+
const canCoalesce = atTip && top.targetKey === targetKey && (namesInsertedNode || COALESCING_REASONS.has(reason) && top.reason === reason);
|
|
3104
4326
|
if (canCoalesce) {
|
|
3105
4327
|
top.snapshot = snapshot;
|
|
3106
4328
|
return;
|
|
@@ -3216,19 +4438,180 @@ var VdFlowchart = class {
|
|
|
3216
4438
|
const parent = this.findNode(sanitizeId(parentId));
|
|
3217
4439
|
if (!parent) return null;
|
|
3218
4440
|
const { direction = "right", distance, angle, edge: edgeOptions, ...nodeOptions } = options;
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
4441
|
+
return this.batchHistory("node:add-child", () => {
|
|
4442
|
+
const node = this.addNode({
|
|
4443
|
+
...nodeOptions,
|
|
4444
|
+
relativeTo: { node: parent.id, direction, distance, angle }
|
|
4445
|
+
});
|
|
4446
|
+
const edge = this.addEdge({
|
|
4447
|
+
from: parent.id,
|
|
4448
|
+
to: node.id,
|
|
4449
|
+
autoPort: true,
|
|
4450
|
+
endMarker: "arrow",
|
|
4451
|
+
...isPlainObject(edgeOptions) ? edgeOptions : {}
|
|
4452
|
+
});
|
|
4453
|
+
this.select({ kind: "node", id: node.id });
|
|
4454
|
+
return { node, edge };
|
|
4455
|
+
});
|
|
4456
|
+
}
|
|
4457
|
+
// Run `fn` with history recording suspended, then record a single entry for
|
|
4458
|
+
// everything it changed (change events still fire for each mutation).
|
|
4459
|
+
batchHistory(reason, fn) {
|
|
4460
|
+
this.historyBatchDepth += 1;
|
|
4461
|
+
let result;
|
|
4462
|
+
try {
|
|
4463
|
+
result = fn();
|
|
4464
|
+
} finally {
|
|
4465
|
+
this.historyBatchDepth -= 1;
|
|
4466
|
+
}
|
|
4467
|
+
if (this.historyBatchDepth === 0 && this.historyBatchDirty) {
|
|
4468
|
+
this.historyBatchDirty = false;
|
|
4469
|
+
if (this.historyEnabled && !this.isApplyingHistory) this.recordHistory(reason);
|
|
4470
|
+
}
|
|
4471
|
+
return result;
|
|
4472
|
+
}
|
|
4473
|
+
// Mind-map structure: a node's parent is the source of its first incoming
|
|
4474
|
+
// edge; its children are the targets of its outgoing edges. The side a
|
|
4475
|
+
// branch is on is the port its edge leaves the parent from, which is what
|
|
4476
|
+
// the user sees (centre geometry is ambiguous for diagonal nodes).
|
|
4477
|
+
getParentEdge(nodeId) {
|
|
4478
|
+
return this.documentData.edges.find(
|
|
4479
|
+
(edge) => edge.to.nodeId === nodeId && edge.from.nodeId !== nodeId
|
|
4480
|
+
) || null;
|
|
4481
|
+
}
|
|
4482
|
+
getParentNode(nodeId) {
|
|
4483
|
+
const edge = this.getParentEdge(nodeId);
|
|
4484
|
+
return edge ? this.findNode(edge.from.nodeId) : null;
|
|
4485
|
+
}
|
|
4486
|
+
getChildNodes(nodeId, direction = null) {
|
|
4487
|
+
const port = direction ? DIRECTION_PORTS[direction] : null;
|
|
4488
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4489
|
+
this.documentData.edges.forEach((edge) => {
|
|
4490
|
+
if (edge.from.nodeId !== nodeId || edge.to.nodeId === nodeId) return;
|
|
4491
|
+
if (!port || edge.from.port === port) ids.add(edge.to.nodeId);
|
|
3222
4492
|
});
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
4493
|
+
return this.documentData.nodes.filter((node) => ids.has(node.id));
|
|
4494
|
+
}
|
|
4495
|
+
// Direction a branch grows from `node`: the side its parent edge leaves
|
|
4496
|
+
// from, or the emptier horizontal side for a root.
|
|
4497
|
+
getGrowthDirection(node) {
|
|
4498
|
+
const parentEdge = this.getParentEdge(node.id);
|
|
4499
|
+
if (parentEdge) return PORT_DIRECTIONS[parentEdge.from.port] || "right";
|
|
4500
|
+
const right = this.getChildNodes(node.id, "right").length;
|
|
4501
|
+
const left = this.getChildNodes(node.id, "left").length;
|
|
4502
|
+
return left < right ? "left" : "right";
|
|
4503
|
+
}
|
|
4504
|
+
// Shape for a new branch node: copy a sibling when there is one, otherwise
|
|
4505
|
+
// the parent's box shape, falling back to a rounded step.
|
|
4506
|
+
getBranchTemplate(parent, sibling) {
|
|
4507
|
+
const source = sibling || (["rounded-rect", "rect", "textbox"].includes(parent.type) ? parent : null) || null;
|
|
4508
|
+
if (source) return { type: source.type, width: source.width, height: source.height };
|
|
4509
|
+
const spec = DEFAULT_NODE_SPECS["rounded-rect"];
|
|
4510
|
+
return { type: "rounded-rect", width: spec.width, height: spec.height };
|
|
4511
|
+
}
|
|
4512
|
+
/**
|
|
4513
|
+
* Add a node connected from `parentId` on the `direction` side (default: the
|
|
4514
|
+
* branch's growth direction), stacked after `anchorId` or the last sibling on
|
|
4515
|
+
* that side. The node and its edge are one undo step; the new edge copies a
|
|
4516
|
+
* sibling's (or the parent's incoming) edge style. With `edit: true` the
|
|
4517
|
+
* label editor opens with the text selected.
|
|
4518
|
+
*/
|
|
4519
|
+
insertBranchNode(parentId, options = {}) {
|
|
4520
|
+
if (this.readonly) return null;
|
|
4521
|
+
const parent = this.findNode(sanitizeId(parentId));
|
|
4522
|
+
if (!parent) return null;
|
|
4523
|
+
const direction = DIRECTION_PORTS[options.direction] ? options.direction : this.getGrowthDirection(parent);
|
|
4524
|
+
const siblings = this.getChildNodes(parent.id, direction);
|
|
4525
|
+
const anchor = options.anchorId ? this.findNode(sanitizeId(options.anchorId)) : null;
|
|
4526
|
+
const lastSibling = siblings[siblings.length - 1] || null;
|
|
4527
|
+
const template = this.getBranchTemplate(parent, anchor || lastSibling);
|
|
4528
|
+
const position = placeBranchNode({
|
|
4529
|
+
nodes: this.documentData.nodes,
|
|
4530
|
+
parent,
|
|
4531
|
+
siblings,
|
|
4532
|
+
direction,
|
|
4533
|
+
size: template,
|
|
4534
|
+
anchor,
|
|
4535
|
+
before: Boolean(options.before)
|
|
3229
4536
|
});
|
|
3230
|
-
|
|
3231
|
-
|
|
4537
|
+
const styleSource = this.documentData.edges.find(
|
|
4538
|
+
(edge) => edge.from.nodeId === parent.id && edge.to.nodeId === (anchor || lastSibling)?.id
|
|
4539
|
+
) || this.documentData.edges.find((edge) => edge.to.nodeId === parent.id);
|
|
4540
|
+
const edgeStyle = styleSource ? {
|
|
4541
|
+
route: styleSource.route,
|
|
4542
|
+
strokeWidth: styleSource.strokeWidth,
|
|
4543
|
+
startMarker: styleSource.startMarker,
|
|
4544
|
+
endMarker: styleSource.endMarker
|
|
4545
|
+
} : { endMarker: "arrow" };
|
|
4546
|
+
const result = this.batchHistory("node:insert", () => {
|
|
4547
|
+
if (parent.collapsed) delete parent.collapsed;
|
|
4548
|
+
const node = this.addNode({ ...template, x: position.x, y: position.y });
|
|
4549
|
+
const edge = this.addEdge({
|
|
4550
|
+
from: { nodeId: parent.id, port: DIRECTION_PORTS[direction] },
|
|
4551
|
+
to: { nodeId: node.id, port: DIRECTION_PORTS[oppositeDirection(direction)] },
|
|
4552
|
+
...edgeStyle
|
|
4553
|
+
});
|
|
4554
|
+
if (edge && anchor) this.placeEdgeBySibling(edge.id, parent.id, anchor.id, options.before);
|
|
4555
|
+
if (this.autoLayout && this.layoutMode !== "grid") this.layout(this.layoutMode);
|
|
4556
|
+
this.select({ kind: "node", id: node.id });
|
|
4557
|
+
return { node, edge };
|
|
4558
|
+
});
|
|
4559
|
+
this.revealNode(result.node.id);
|
|
4560
|
+
if (options.edit) this.startTextEdit(result.node.id, { inserted: true });
|
|
4561
|
+
return result;
|
|
4562
|
+
}
|
|
4563
|
+
// Tree and radial layouts order children by connection order, so a sibling's
|
|
4564
|
+
// connection goes right after (or before) its anchor's instead of last.
|
|
4565
|
+
placeEdgeBySibling(edgeId, parentId, anchorId, before = false) {
|
|
4566
|
+
const edges = this.documentData.edges;
|
|
4567
|
+
const fromIndex = edges.findIndex((edge) => edge.id === edgeId);
|
|
4568
|
+
if (fromIndex === -1) return;
|
|
4569
|
+
const [moved] = edges.splice(fromIndex, 1);
|
|
4570
|
+
const anchorIndex = edges.findIndex(
|
|
4571
|
+
(edge) => edge.from.nodeId === parentId && edge.to.nodeId === anchorId
|
|
4572
|
+
);
|
|
4573
|
+
if (anchorIndex === -1) edges.push(moved);
|
|
4574
|
+
else edges.splice(before ? anchorIndex : anchorIndex + 1, 0, moved);
|
|
4575
|
+
}
|
|
4576
|
+
// Insert a sibling after (or before) `nodeId`; a node without a parent gets a
|
|
4577
|
+
// child instead, which is what Enter on a mind-map root means.
|
|
4578
|
+
insertSiblingNode(nodeId, options = {}) {
|
|
4579
|
+
if (this.readonly) return null;
|
|
4580
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4581
|
+
if (!node) return null;
|
|
4582
|
+
const parentEdge = this.getParentEdge(node.id);
|
|
4583
|
+
const parent = parentEdge ? this.findNode(parentEdge.from.nodeId) : null;
|
|
4584
|
+
if (!parent) return this.insertBranchNode(node.id, { edit: options.edit });
|
|
4585
|
+
return this.insertBranchNode(parent.id, {
|
|
4586
|
+
direction: PORT_DIRECTIONS[parentEdge.from.port],
|
|
4587
|
+
anchorId: node.id,
|
|
4588
|
+
before: Boolean(options.before),
|
|
4589
|
+
edit: options.edit
|
|
4590
|
+
});
|
|
4591
|
+
}
|
|
4592
|
+
// Pan just enough to bring a node fully into view (with a small margin).
|
|
4593
|
+
revealNode(nodeId) {
|
|
4594
|
+
const node = this.findNode(nodeId);
|
|
4595
|
+
const width = this.canvasEl.clientWidth;
|
|
4596
|
+
const height = this.canvasEl.clientHeight;
|
|
4597
|
+
if (!node || !width || !height) return;
|
|
4598
|
+
const viewport = this.documentData.viewport;
|
|
4599
|
+
const margin = 32;
|
|
4600
|
+
const left = viewport.x + node.x * viewport.scale;
|
|
4601
|
+
const top = viewport.y + node.y * viewport.scale;
|
|
4602
|
+
const right = left + node.width * viewport.scale;
|
|
4603
|
+
const bottom = top + node.height * viewport.scale;
|
|
4604
|
+
let dx = 0;
|
|
4605
|
+
let dy = 0;
|
|
4606
|
+
if (right > width - margin) dx = width - margin - right;
|
|
4607
|
+
if (left + dx < margin) dx = margin - left;
|
|
4608
|
+
if (bottom > height - margin) dy = height - margin - bottom;
|
|
4609
|
+
if (top + dy < margin) dy = margin - top;
|
|
4610
|
+
if (!dx && !dy) return;
|
|
4611
|
+
viewport.x = formatNumber(viewport.x + dx);
|
|
4612
|
+
viewport.y = formatNumber(viewport.y + dy);
|
|
4613
|
+
this.render({ inspector: false, json: true });
|
|
4614
|
+
this.emitViewportChange("viewport:pan");
|
|
3232
4615
|
}
|
|
3233
4616
|
// Arrange nodes with a built-in layout. Positions are computed by the pure
|
|
3234
4617
|
// computeLayout() module, then applied in place (NOT via load(), which would
|
|
@@ -3476,6 +4859,9 @@ var VdFlowchart = class {
|
|
|
3476
4859
|
}
|
|
3477
4860
|
deleteSelection() {
|
|
3478
4861
|
if (!this.selection || this.readonly) return false;
|
|
4862
|
+
if (this.hasMultiSelection()) {
|
|
4863
|
+
return this.removeNodeIds(new Set(this.getSelectedNodeIds()), "node:remove");
|
|
4864
|
+
}
|
|
3479
4865
|
if (this.selection.kind === "node") return this.removeNode(this.selection.id);
|
|
3480
4866
|
return this.removeEdge(this.selection.id);
|
|
3481
4867
|
}
|
|
@@ -3504,10 +4890,11 @@ var VdFlowchart = class {
|
|
|
3504
4890
|
return this;
|
|
3505
4891
|
}
|
|
3506
4892
|
fitView() {
|
|
3507
|
-
|
|
4893
|
+
const nodes = this.getVisibleNodes();
|
|
4894
|
+
if (!nodes.length) {
|
|
3508
4895
|
return this.resetView();
|
|
3509
4896
|
}
|
|
3510
|
-
const bounds = getBounds(
|
|
4897
|
+
const bounds = getBounds(nodes);
|
|
3511
4898
|
const width = this.canvasEl.clientWidth || 800;
|
|
3512
4899
|
const height = this.canvasEl.clientHeight || 560;
|
|
3513
4900
|
const padding = 80;
|
|
@@ -3565,6 +4952,14 @@ var VdFlowchart = class {
|
|
|
3565
4952
|
pattern.querySelector("path").setAttribute("d", `M ${this.gridSize} 0 L 0 0 0 ${this.gridSize}`);
|
|
3566
4953
|
}
|
|
3567
4954
|
if ("autoFit" in options) this.autoFit = Boolean(options.autoFit);
|
|
4955
|
+
if ("keyboardShortcuts" in options) {
|
|
4956
|
+
this.keyboardShortcuts = normalizeKeyboardShortcuts(options.keyboardShortcuts);
|
|
4957
|
+
}
|
|
4958
|
+
if ("snapGuides" in options) this.snapGuides = options.snapGuides !== false;
|
|
4959
|
+
if ("minimap" in options) this.setMinimapEnabled(options.minimap !== false);
|
|
4960
|
+
if ("autoLayout" in options) this.autoLayout = Boolean(options.autoLayout);
|
|
4961
|
+
this.syncCanvasLabel();
|
|
4962
|
+
if (this.shortcutsHelpOpen) this.renderShortcutsHelp();
|
|
3568
4963
|
if ("history" in options && options.history !== false !== this.historyEnabled) {
|
|
3569
4964
|
this.historyEnabled = options.history !== false;
|
|
3570
4965
|
this.history = [];
|
|
@@ -3613,6 +5008,10 @@ var VdFlowchart = class {
|
|
|
3613
5008
|
destroy() {
|
|
3614
5009
|
if (this.destroyed) return;
|
|
3615
5010
|
this.destroyed = true;
|
|
5011
|
+
if (this.minimapFrame && typeof cancelAnimationFrame === "function") {
|
|
5012
|
+
cancelAnimationFrame(this.minimapFrame);
|
|
5013
|
+
}
|
|
5014
|
+
this.minimapFrame = 0;
|
|
3616
5015
|
this.activeTool = null;
|
|
3617
5016
|
this.stopTextEdit({ commit: false });
|
|
3618
5017
|
this.resizeObserver?.disconnect();
|
|
@@ -3629,6 +5028,17 @@ var VdFlowchart = class {
|
|
|
3629
5028
|
|
|
3630
5029
|
// src/vue.js
|
|
3631
5030
|
var FORWARDED_EVENTS = ["change", "select", "viewport", "connect", "ready"];
|
|
5031
|
+
var EDITOR_OPTION_KEYS = [
|
|
5032
|
+
"readonly",
|
|
5033
|
+
"gridSize",
|
|
5034
|
+
"autoFit",
|
|
5035
|
+
"history",
|
|
5036
|
+
"historyLimit",
|
|
5037
|
+
"keyboardShortcuts",
|
|
5038
|
+
"snapGuides",
|
|
5039
|
+
"minimap",
|
|
5040
|
+
"autoLayout"
|
|
5041
|
+
];
|
|
3632
5042
|
var VdFlowchart2 = defineComponent({
|
|
3633
5043
|
name: "VdFlowchart",
|
|
3634
5044
|
props: {
|
|
@@ -3645,21 +5055,26 @@ var VdFlowchart2 = defineComponent({
|
|
|
3645
5055
|
/** Enable the built-in undo/redo history (default true). */
|
|
3646
5056
|
history: { type: Boolean, default: true },
|
|
3647
5057
|
/** Maximum number of history entries to retain. */
|
|
3648
|
-
historyLimit: { type: Number, default: void 0 }
|
|
5058
|
+
historyLimit: { type: Number, default: void 0 },
|
|
5059
|
+
/** Keyboard model: 'mindmap' (Tab child, Enter sibling) or 'basic'. */
|
|
5060
|
+
keyboardShortcuts: { type: String, default: "mindmap" },
|
|
5061
|
+
/** Snap dragged nodes to other nodes' edges and centres (default true). */
|
|
5062
|
+
snapGuides: { type: Boolean, default: true },
|
|
5063
|
+
/** Show the overview minimap in the canvas corner (default true). */
|
|
5064
|
+
minimap: { type: Boolean, default: true },
|
|
5065
|
+
/** Re-run the current layout after keyboard or handle insertions (default false). */
|
|
5066
|
+
autoLayout: { type: Boolean, default: false }
|
|
3649
5067
|
},
|
|
3650
5068
|
emits: ["change", "select", "viewport", "connect", "ready"],
|
|
3651
5069
|
setup(props, { emit, expose }) {
|
|
3652
5070
|
const el = ref(null);
|
|
3653
5071
|
let instance = null;
|
|
5072
|
+
const editorOptions = () => Object.fromEntries(EDITOR_OPTION_KEYS.map((key) => [key, props[key]]));
|
|
3654
5073
|
const create = () => {
|
|
3655
5074
|
instance = new VdFlowchart({
|
|
3656
5075
|
element: el.value,
|
|
3657
5076
|
data: props.data,
|
|
3658
|
-
|
|
3659
|
-
gridSize: props.gridSize,
|
|
3660
|
-
autoFit: props.autoFit,
|
|
3661
|
-
history: props.history,
|
|
3662
|
-
historyLimit: props.historyLimit
|
|
5077
|
+
...editorOptions()
|
|
3663
5078
|
});
|
|
3664
5079
|
FORWARDED_EVENTS.forEach((name) => {
|
|
3665
5080
|
instance.on(name, (payload) => emit(name, payload));
|
|
@@ -3679,16 +5094,9 @@ var VdFlowchart2 = defineComponent({
|
|
|
3679
5094
|
{ deep: true }
|
|
3680
5095
|
);
|
|
3681
5096
|
watch(
|
|
3682
|
-
() =>
|
|
5097
|
+
() => EDITOR_OPTION_KEYS.map((key) => props[key]),
|
|
3683
5098
|
() => {
|
|
3684
|
-
if (
|
|
3685
|
-
instance.updateOptions({
|
|
3686
|
-
readonly: props.readonly,
|
|
3687
|
-
gridSize: props.gridSize,
|
|
3688
|
-
autoFit: props.autoFit,
|
|
3689
|
-
history: props.history,
|
|
3690
|
-
historyLimit: props.historyLimit
|
|
3691
|
-
});
|
|
5099
|
+
if (instance) instance.updateOptions(editorOptions());
|
|
3692
5100
|
}
|
|
3693
5101
|
);
|
|
3694
5102
|
onBeforeUnmount(() => {
|
|
@@ -3712,6 +5120,7 @@ export {
|
|
|
3712
5120
|
FLOWCHART_DOCUMENT_VERSION,
|
|
3713
5121
|
FLOWCHART_EDGE_MARKERS,
|
|
3714
5122
|
FLOWCHART_EDGE_ROUTES,
|
|
5123
|
+
FLOWCHART_KEYBOARD_SHORTCUTS,
|
|
3715
5124
|
FLOWCHART_NODE_TYPES,
|
|
3716
5125
|
FLOWCHART_PORTS,
|
|
3717
5126
|
LAYOUT_MODES,
|