@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.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
FLOWCHART_DOCUMENT_VERSION: () => FLOWCHART_DOCUMENT_VERSION,
|
|
24
24
|
FLOWCHART_EDGE_MARKERS: () => FLOWCHART_EDGE_MARKERS,
|
|
25
25
|
FLOWCHART_EDGE_ROUTES: () => FLOWCHART_EDGE_ROUTES,
|
|
26
|
+
FLOWCHART_KEYBOARD_SHORTCUTS: () => FLOWCHART_KEYBOARD_SHORTCUTS,
|
|
26
27
|
FLOWCHART_NODE_TYPES: () => FLOWCHART_NODE_TYPES,
|
|
27
28
|
FLOWCHART_PORTS: () => FLOWCHART_PORTS,
|
|
28
29
|
LAYOUT_MODES: () => LAYOUT_MODES,
|
|
@@ -220,6 +221,134 @@ function computeLayout(documentData, mode = "tree", options = {}) {
|
|
|
220
221
|
}
|
|
221
222
|
var LAYOUT_MODES = ["tree", "radial", "grid"];
|
|
222
223
|
|
|
224
|
+
// src/mindmap.js
|
|
225
|
+
var DIRECTION_PORTS = { right: "right", left: "left", down: "bottom", up: "top" };
|
|
226
|
+
var PORT_DIRECTIONS = { right: "right", left: "left", bottom: "down", top: "up" };
|
|
227
|
+
var OPPOSITE_DIRECTIONS = { right: "left", left: "right", down: "up", up: "down" };
|
|
228
|
+
var DIRECTION_VECTORS = {
|
|
229
|
+
right: { x: 1, y: 0 },
|
|
230
|
+
left: { x: -1, y: 0 },
|
|
231
|
+
down: { x: 0, y: 1 },
|
|
232
|
+
up: { x: 0, y: -1 }
|
|
233
|
+
};
|
|
234
|
+
var BRANCH_GAP = 72;
|
|
235
|
+
var SIBLING_GAP = 24;
|
|
236
|
+
var COLLISION_GAP = 12;
|
|
237
|
+
var COLLISION_STEP = 12;
|
|
238
|
+
var MAX_COLLISION_STEPS = 400;
|
|
239
|
+
var BRANCH_RING_STEP = 48;
|
|
240
|
+
var MAX_BRANCH_RINGS = 8;
|
|
241
|
+
var NAVIGATION_CONE = Math.tan(Math.PI / 3);
|
|
242
|
+
function centerOf(node) {
|
|
243
|
+
return { x: node.x + node.width / 2, y: node.y + node.height / 2 };
|
|
244
|
+
}
|
|
245
|
+
function oppositeDirection(direction) {
|
|
246
|
+
return OPPOSITE_DIRECTIONS[direction] || "left";
|
|
247
|
+
}
|
|
248
|
+
function findSpatialNeighbor(nodes, fromNode, direction) {
|
|
249
|
+
const vector = DIRECTION_VECTORS[direction];
|
|
250
|
+
if (!vector || !fromNode) return null;
|
|
251
|
+
const origin = centerOf(fromNode);
|
|
252
|
+
let bestCone = null;
|
|
253
|
+
let bestPlane = null;
|
|
254
|
+
nodes.forEach((node) => {
|
|
255
|
+
if (node.id === fromNode.id) return;
|
|
256
|
+
const center = centerOf(node);
|
|
257
|
+
const dx = center.x - origin.x;
|
|
258
|
+
const dy = center.y - origin.y;
|
|
259
|
+
const along = dx * vector.x + dy * vector.y;
|
|
260
|
+
if (along <= 0) return;
|
|
261
|
+
const across = Math.abs(dx * vector.y - dy * vector.x);
|
|
262
|
+
const score = along + across * 2;
|
|
263
|
+
if (across <= along * NAVIGATION_CONE && (!bestCone || score < bestCone.score)) {
|
|
264
|
+
bestCone = { node, score };
|
|
265
|
+
}
|
|
266
|
+
if (!bestPlane || score < bestPlane.score) bestPlane = { node, score };
|
|
267
|
+
});
|
|
268
|
+
return (bestCone || bestPlane)?.node ?? null;
|
|
269
|
+
}
|
|
270
|
+
function findNearestNode(nodes, point) {
|
|
271
|
+
let best = null;
|
|
272
|
+
nodes.forEach((node) => {
|
|
273
|
+
const center = centerOf(node);
|
|
274
|
+
const distance = Math.hypot(center.x - point.x, center.y - point.y);
|
|
275
|
+
if (!best || distance < best.distance) best = { node, distance };
|
|
276
|
+
});
|
|
277
|
+
return best?.node ?? null;
|
|
278
|
+
}
|
|
279
|
+
function overlapsAny(rect, nodes) {
|
|
280
|
+
return nodes.some(
|
|
281
|
+
(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
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
function placeBranchNode({
|
|
285
|
+
nodes,
|
|
286
|
+
parent,
|
|
287
|
+
siblings = [],
|
|
288
|
+
direction,
|
|
289
|
+
size,
|
|
290
|
+
anchor = null,
|
|
291
|
+
before = false
|
|
292
|
+
}) {
|
|
293
|
+
const horizontal = direction === "right" || direction === "left";
|
|
294
|
+
const cross = horizontal ? "y" : "x";
|
|
295
|
+
const crossSize = horizontal ? "height" : "width";
|
|
296
|
+
const lastSibling = siblings.reduce(
|
|
297
|
+
(last, node) => !last || node[cross] + node[crossSize] > last[cross] + last[crossSize] ? node : last,
|
|
298
|
+
null
|
|
299
|
+
);
|
|
300
|
+
const reference = anchor || lastSibling;
|
|
301
|
+
const rect = { x: 0, y: 0, width: size.width, height: size.height };
|
|
302
|
+
if (reference) {
|
|
303
|
+
if (direction === "right") rect.x = reference.x;
|
|
304
|
+
if (direction === "left") rect.x = reference.x + reference.width - size.width;
|
|
305
|
+
if (direction === "down") rect.y = reference.y;
|
|
306
|
+
if (direction === "up") rect.y = reference.y + reference.height - size.height;
|
|
307
|
+
rect[cross] = before ? reference[cross] - SIBLING_GAP - size[crossSize] : reference[cross] + reference[crossSize] + SIBLING_GAP;
|
|
308
|
+
} else {
|
|
309
|
+
if (direction === "right") rect.x = parent.x + parent.width + BRANCH_GAP;
|
|
310
|
+
if (direction === "left") rect.x = parent.x - BRANCH_GAP - size.width;
|
|
311
|
+
if (direction === "down") rect.y = parent.y + parent.height + BRANCH_GAP;
|
|
312
|
+
if (direction === "up") rect.y = parent.y - BRANCH_GAP - size.height;
|
|
313
|
+
rect[cross] = parent[cross] + parent[crossSize] / 2 - size[crossSize] / 2;
|
|
314
|
+
}
|
|
315
|
+
const crossSign = reference ? before ? -1 : 1 : 0;
|
|
316
|
+
const result = nearestFreeSlot(
|
|
317
|
+
rect,
|
|
318
|
+
nodes,
|
|
319
|
+
cross,
|
|
320
|
+
horizontal ? "x" : "y",
|
|
321
|
+
DIRECTION_VECTORS[direction],
|
|
322
|
+
crossSign
|
|
323
|
+
);
|
|
324
|
+
return { x: Math.round(result.x * 100) / 100, y: Math.round(result.y * 100) / 100 };
|
|
325
|
+
}
|
|
326
|
+
function crossOffsetAt(index, crossSign) {
|
|
327
|
+
if (crossSign) return crossSign * COLLISION_STEP * index;
|
|
328
|
+
return COLLISION_STEP * Math.ceil(index / 2) * (index % 2 ? 1 : -1);
|
|
329
|
+
}
|
|
330
|
+
function nearestFreeSlot(rect, nodes, cross, main, vector, crossSign) {
|
|
331
|
+
const origin = { ...rect };
|
|
332
|
+
const outward = vector.x + vector.y;
|
|
333
|
+
let best = null;
|
|
334
|
+
for (let ring = 0; ring <= MAX_BRANCH_RINGS; ring += 1) {
|
|
335
|
+
const mainOffset = ring * BRANCH_RING_STEP;
|
|
336
|
+
if (best && mainOffset >= best.cost) break;
|
|
337
|
+
const candidate = { ...origin, [main]: origin[main] + outward * mainOffset };
|
|
338
|
+
for (let index = 0; index <= MAX_COLLISION_STEPS; index += 1) {
|
|
339
|
+
const crossOffset = crossOffsetAt(index, crossSign);
|
|
340
|
+
const cost = Math.hypot(mainOffset, crossOffset);
|
|
341
|
+
if (best && cost >= best.cost) break;
|
|
342
|
+
candidate[cross] = origin[cross] + crossOffset;
|
|
343
|
+
if (!overlapsAny(candidate, nodes)) {
|
|
344
|
+
best = { rect: { ...candidate }, cost };
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return best ? best.rect : origin;
|
|
350
|
+
}
|
|
351
|
+
|
|
223
352
|
// src/core.js
|
|
224
353
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
225
354
|
var TOOLBAR_ICON_PATHS = {
|
|
@@ -229,6 +358,7 @@ var TOOLBAR_ICON_PATHS = {
|
|
|
229
358
|
"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",
|
|
230
359
|
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",
|
|
231
360
|
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",
|
|
361
|
+
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",
|
|
232
362
|
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"
|
|
233
363
|
};
|
|
234
364
|
var TOOLBAR_ACTIONS = [
|
|
@@ -280,21 +410,32 @@ var DEFAULT_EDGE_STROKE_WIDTH = 2.25;
|
|
|
280
410
|
var MIN_EDGE_STROKE_WIDTH = 1.25;
|
|
281
411
|
var MAX_EDGE_STROKE_WIDTH = 6;
|
|
282
412
|
var CONNECTION_PORT_RADIUS = 6;
|
|
413
|
+
var PORT_HANDLE_OFFSET = 12;
|
|
283
414
|
var CONNECTION_PORT_HIT_RADIUS = 14;
|
|
415
|
+
var PORT_CLICK_SLOP = 4;
|
|
416
|
+
var RESIZE_HANDLE_SIZE = 9;
|
|
417
|
+
var SELECTION_OUTSET = 4;
|
|
418
|
+
var GUIDE_SNAP_DISTANCE = 6;
|
|
419
|
+
var GUIDE_OVERHANG = 12;
|
|
420
|
+
var COLLAPSE_TOGGLE_OFFSET = 36;
|
|
421
|
+
var MINIMAP_WIDTH = 180;
|
|
422
|
+
var MINIMAP_HEIGHT = 120;
|
|
423
|
+
var MINIMAP_MIN_CANVAS_WIDTH = 480;
|
|
424
|
+
var SELECTION_BOX_TYPES = /* @__PURE__ */ new Set(["circle", "diamond", "label"]);
|
|
284
425
|
var RECONNECT_ENDPOINT_RADIUS = 7;
|
|
285
426
|
var RECONNECT_ENDPOINT_HIT_RADIUS = 12;
|
|
286
427
|
var EDGE_HIT_STROKE_MIN = 16;
|
|
287
428
|
var CONNECTION_SNAP_PADDING = 36;
|
|
288
429
|
var CONNECTION_HYSTERESIS = 16;
|
|
289
430
|
var CONNECTION_CENTER_LOCK_RADIUS = 18;
|
|
290
|
-
var RESIZE_PORT_GAP =
|
|
431
|
+
var RESIZE_PORT_GAP = 28;
|
|
291
432
|
var EDGE_STROKE_PRESETS = [
|
|
292
433
|
{ id: "thin", label: "Thin", width: 1.75 },
|
|
293
434
|
{ id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
|
|
294
435
|
{ id: "bold", label: "Bold", width: 3.5 }
|
|
295
436
|
];
|
|
296
|
-
var VD_FLOWCHART_VERSION = "1.
|
|
297
|
-
var FLOWCHART_DOCUMENT_VERSION = "1.
|
|
437
|
+
var VD_FLOWCHART_VERSION = "1.4.0";
|
|
438
|
+
var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
|
|
298
439
|
var FLOWCHART_NODE_TYPES = [
|
|
299
440
|
"rounded-rect",
|
|
300
441
|
"rect",
|
|
@@ -307,7 +448,65 @@ var FLOWCHART_NODE_TYPES = [
|
|
|
307
448
|
var FLOWCHART_PORTS = ["top", "right", "bottom", "left"];
|
|
308
449
|
var FLOWCHART_EDGE_MARKERS = ["none", "arrow", "dot"];
|
|
309
450
|
var FLOWCHART_EDGE_ROUTES = ["curve", "straight", "orthogonal"];
|
|
310
|
-
var
|
|
451
|
+
var FLOWCHART_KEYBOARD_SHORTCUTS = ["mindmap", "basic"];
|
|
452
|
+
function normalizeKeyboardShortcuts(value) {
|
|
453
|
+
return value === "basic" ? "basic" : "mindmap";
|
|
454
|
+
}
|
|
455
|
+
var ARROW_DIRECTIONS = {
|
|
456
|
+
ArrowRight: "right",
|
|
457
|
+
ArrowLeft: "left",
|
|
458
|
+
ArrowDown: "down",
|
|
459
|
+
ArrowUp: "up"
|
|
460
|
+
};
|
|
461
|
+
function getShortcutRows(mode, readonly) {
|
|
462
|
+
const navigation = [
|
|
463
|
+
["Arrow keys", "Select the nearest node in that direction"],
|
|
464
|
+
["Shift + Arrow", "Select a connection on that side (repeat to cycle)"],
|
|
465
|
+
["Shift + click, Shift + drag", "Add nodes to the selection, or select with a box"],
|
|
466
|
+
["Cmd/Ctrl + A", "Select all nodes"]
|
|
467
|
+
];
|
|
468
|
+
const view = [
|
|
469
|
+
["Cmd/Ctrl + = or -", "Zoom in or out"],
|
|
470
|
+
["Cmd/Ctrl + 0", "Zoom to 100%"],
|
|
471
|
+
["Shift + 1", "Fit the diagram to the view"]
|
|
472
|
+
];
|
|
473
|
+
const help = [["?", "Show or hide this list"]];
|
|
474
|
+
if (readonly) {
|
|
475
|
+
return [...navigation, ...view, ...help, ["Tab", "Leave the canvas"]];
|
|
476
|
+
}
|
|
477
|
+
const mindmap = mode === "mindmap" ? [
|
|
478
|
+
["Tab", "Add a child node"],
|
|
479
|
+
["Enter", "Add a sibling below (Shift + Enter: above)"],
|
|
480
|
+
["F2, Space, or type", "Edit the label"],
|
|
481
|
+
["Enter while editing", "Save the label (Shift + Enter: new line)"],
|
|
482
|
+
["Tab while editing", "Save and add a child"]
|
|
483
|
+
] : [
|
|
484
|
+
["Enter or F2", "Edit the label"],
|
|
485
|
+
["Cmd/Ctrl + Enter", "Save the label"]
|
|
486
|
+
];
|
|
487
|
+
return [
|
|
488
|
+
...navigation,
|
|
489
|
+
...mindmap,
|
|
490
|
+
[
|
|
491
|
+
mode === "mindmap" ? "Enter, F2, or type on a connection" : "Enter or F2 on a connection",
|
|
492
|
+
"Edit the connection label"
|
|
493
|
+
],
|
|
494
|
+
["Cmd/Ctrl + /", "Collapse or expand the branch"],
|
|
495
|
+
["Alt + Arrow", "Nudge the node (add Shift for 1 px)"],
|
|
496
|
+
["Delete or Backspace", "Delete the selection"],
|
|
497
|
+
["Cmd/Ctrl + D", "Duplicate the node"],
|
|
498
|
+
["Cmd/Ctrl + Z, Shift + Cmd/Ctrl + Z", "Undo, redo"],
|
|
499
|
+
["Cmd/Ctrl + C, X, V", "Copy, cut, paste"],
|
|
500
|
+
...view,
|
|
501
|
+
["Esc", "Cancel editing, then the tool, then deselect"],
|
|
502
|
+
[
|
|
503
|
+
mode === "mindmap" ? "Shift + Tab, or Esc then Tab" : "Tab or Shift + Tab",
|
|
504
|
+
"Leave the canvas"
|
|
505
|
+
],
|
|
506
|
+
...help
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
var COALESCING_REASONS = /* @__PURE__ */ new Set(["node:update", "edge:update", "node:nudge"]);
|
|
311
510
|
var DEFAULT_EDGE_ROUTE = "curve";
|
|
312
511
|
var ORTHOGONAL_STUB_LENGTH = 32;
|
|
313
512
|
var ORTHOGONAL_CORNER_RADIUS = 12;
|
|
@@ -481,7 +680,7 @@ function normalizeViewport(viewport) {
|
|
|
481
680
|
function normalizeNode(rawNode, index, usedIds) {
|
|
482
681
|
const type = normalizeNodeType(rawNode?.type);
|
|
483
682
|
const spec = getNodeSpec(type);
|
|
484
|
-
|
|
683
|
+
const node = {
|
|
485
684
|
id: ensureUniqueId(rawNode?.id, "node", usedIds),
|
|
486
685
|
type,
|
|
487
686
|
x: toFiniteNumber(rawNode?.x, index * 28),
|
|
@@ -491,6 +690,37 @@ function normalizeNode(rawNode, index, usedIds) {
|
|
|
491
690
|
text: rawNode?.text == null ? spec.text : String(rawNode.text),
|
|
492
691
|
data: isPlainObject(rawNode?.data) ? deepClone(rawNode.data) : {}
|
|
493
692
|
};
|
|
693
|
+
if (rawNode?.collapsed === true) node.collapsed = true;
|
|
694
|
+
return node;
|
|
695
|
+
}
|
|
696
|
+
function computeHiddenNodeIds(nodes, edges) {
|
|
697
|
+
const collapsed = new Set(nodes.filter((node) => node.collapsed).map((node) => node.id));
|
|
698
|
+
if (!collapsed.size) return /* @__PURE__ */ new Set();
|
|
699
|
+
const outgoing = new Map(nodes.map((node) => [node.id, []]));
|
|
700
|
+
edges.forEach((edge) => {
|
|
701
|
+
if (edge.from.nodeId === edge.to.nodeId) return;
|
|
702
|
+
outgoing.get(edge.from.nodeId)?.push(edge.to.nodeId);
|
|
703
|
+
});
|
|
704
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
705
|
+
collapsed.forEach((rootId) => {
|
|
706
|
+
const queue2 = [...outgoing.get(rootId) || []];
|
|
707
|
+
for (let index = 0; index < queue2.length; index += 1) {
|
|
708
|
+
const id = queue2[index];
|
|
709
|
+
if (id === rootId || candidates.has(id)) continue;
|
|
710
|
+
candidates.add(id);
|
|
711
|
+
queue2.push(...outgoing.get(id) || []);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
const queue = nodes.filter((node) => !candidates.has(node.id) && !collapsed.has(node.id)).map((node) => node.id);
|
|
715
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
716
|
+
const id = queue[index];
|
|
717
|
+
(outgoing.get(id) || []).forEach((childId) => {
|
|
718
|
+
if (!candidates.has(childId)) return;
|
|
719
|
+
candidates.delete(childId);
|
|
720
|
+
if (!collapsed.has(childId)) queue.push(childId);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
return candidates;
|
|
494
724
|
}
|
|
495
725
|
function normalizeEndpoint(rawEndpoint, fallbackPort) {
|
|
496
726
|
return {
|
|
@@ -643,6 +873,15 @@ function getPortPosition(node, port) {
|
|
|
643
873
|
return { x: node.x, y: node.y + node.height / 2 };
|
|
644
874
|
}
|
|
645
875
|
}
|
|
876
|
+
function getPortHandlePosition(node, port, scale = 1) {
|
|
877
|
+
const point = getPortPosition(node, port);
|
|
878
|
+
const normal = getPortNormal(port);
|
|
879
|
+
const offset = PORT_HANDLE_OFFSET / scale;
|
|
880
|
+
return {
|
|
881
|
+
x: formatNumber(point.x + normal.x * offset),
|
|
882
|
+
y: formatNumber(point.y + normal.y * offset)
|
|
883
|
+
};
|
|
884
|
+
}
|
|
646
885
|
function getNearestPort(node, point) {
|
|
647
886
|
return FLOWCHART_PORTS.reduce((best, port) => {
|
|
648
887
|
const portPoint = getPortPosition(node, port);
|
|
@@ -1010,6 +1249,68 @@ function getResizeCursor(handle) {
|
|
|
1010
1249
|
if (handle === "ne" || handle === "sw") return "nesw-resize";
|
|
1011
1250
|
return "nwse-resize";
|
|
1012
1251
|
}
|
|
1252
|
+
function collectGuideLines(nodes) {
|
|
1253
|
+
const x = [];
|
|
1254
|
+
const y = [];
|
|
1255
|
+
nodes.forEach((node) => {
|
|
1256
|
+
const right = node.x + node.width;
|
|
1257
|
+
const bottom = node.y + node.height;
|
|
1258
|
+
[node.x, node.x + node.width / 2, right].forEach(
|
|
1259
|
+
(value) => x.push({ value, start: node.y, end: bottom })
|
|
1260
|
+
);
|
|
1261
|
+
[node.y, node.y + node.height / 2, bottom].forEach(
|
|
1262
|
+
(value) => y.push({ value, start: node.x, end: right })
|
|
1263
|
+
);
|
|
1264
|
+
});
|
|
1265
|
+
const byValue = (a, b) => a.value - b.value;
|
|
1266
|
+
return { x: x.sort(byValue), y: y.sort(byValue) };
|
|
1267
|
+
}
|
|
1268
|
+
function lowerBound(lines, value) {
|
|
1269
|
+
let low = 0;
|
|
1270
|
+
let high = lines.length;
|
|
1271
|
+
while (low < high) {
|
|
1272
|
+
const middle = low + high >> 1;
|
|
1273
|
+
if (lines[middle].value < value) low = middle + 1;
|
|
1274
|
+
else high = middle;
|
|
1275
|
+
}
|
|
1276
|
+
return low;
|
|
1277
|
+
}
|
|
1278
|
+
function snapToGuides(lines, candidates, threshold) {
|
|
1279
|
+
let best = null;
|
|
1280
|
+
candidates.forEach((candidate) => {
|
|
1281
|
+
const index = lowerBound(lines, candidate);
|
|
1282
|
+
[lines[index - 1], lines[index]].forEach((line) => {
|
|
1283
|
+
if (!line) return;
|
|
1284
|
+
const offset = line.value - candidate;
|
|
1285
|
+
if (Math.abs(offset) <= threshold && (!best || Math.abs(offset) < Math.abs(best.offset))) {
|
|
1286
|
+
best = { offset };
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1290
|
+
if (!best) return null;
|
|
1291
|
+
const positions = candidates.map((candidate) => candidate + best.offset);
|
|
1292
|
+
best.lines = lines.filter(
|
|
1293
|
+
(line) => positions.some((position) => Math.abs(line.value - position) < 0.5)
|
|
1294
|
+
);
|
|
1295
|
+
return best;
|
|
1296
|
+
}
|
|
1297
|
+
function guideSegments(axis, lines, box) {
|
|
1298
|
+
const byValue = /* @__PURE__ */ new Map();
|
|
1299
|
+
lines.forEach((line) => {
|
|
1300
|
+
const current = byValue.get(line.value);
|
|
1301
|
+
byValue.set(line.value, {
|
|
1302
|
+
start: Math.min(current?.start ?? Infinity, line.start),
|
|
1303
|
+
end: Math.max(current?.end ?? -Infinity, line.end)
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
const boxStart = axis === "x" ? box.top : box.left;
|
|
1307
|
+
const boxEnd = boxStart + (axis === "x" ? box.height : box.width);
|
|
1308
|
+
return [...byValue].map(([value, extent]) => {
|
|
1309
|
+
const start = Math.min(extent.start, boxStart) - GUIDE_OVERHANG;
|
|
1310
|
+
const end = Math.max(extent.end, boxEnd) + GUIDE_OVERHANG;
|
|
1311
|
+
return axis === "x" ? { x1: value, y1: start, x2: value, y2: end } : { x1: start, y1: value, x2: end, y2: value };
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1013
1314
|
function getBounds(nodes) {
|
|
1014
1315
|
if (!nodes.length) {
|
|
1015
1316
|
return { left: 0, top: 0, right: 0, bottom: 0 };
|
|
@@ -1166,6 +1467,7 @@ var VdFlowchart = class {
|
|
|
1166
1467
|
this.documentData = normalizeDocument(options.data || {});
|
|
1167
1468
|
this.listeners = {};
|
|
1168
1469
|
this.selection = null;
|
|
1470
|
+
this.selectedNodeIds = /* @__PURE__ */ new Set();
|
|
1169
1471
|
this.interaction = null;
|
|
1170
1472
|
this.activeTool = null;
|
|
1171
1473
|
this.layoutMode = "tree";
|
|
@@ -1173,6 +1475,7 @@ var VdFlowchart = class {
|
|
|
1173
1475
|
this.destroyed = false;
|
|
1174
1476
|
this.lastNodePointer = null;
|
|
1175
1477
|
this.reconnectEdgeId = null;
|
|
1478
|
+
this.edgeCycleAnchor = null;
|
|
1176
1479
|
this.clipboard = null;
|
|
1177
1480
|
this.gridPatternId = nextId("flowchart-grid");
|
|
1178
1481
|
this.markerIds = /* @__PURE__ */ new Map();
|
|
@@ -1182,6 +1485,16 @@ var VdFlowchart = class {
|
|
|
1182
1485
|
this.history = [];
|
|
1183
1486
|
this.historyIndex = -1;
|
|
1184
1487
|
this.isApplyingHistory = false;
|
|
1488
|
+
this.historyBatchDepth = 0;
|
|
1489
|
+
this.historyBatchDirty = false;
|
|
1490
|
+
this.coalesceInsertedText = false;
|
|
1491
|
+
this.keyboardShortcuts = normalizeKeyboardShortcuts(options.keyboardShortcuts);
|
|
1492
|
+
this.snapGuides = options.snapGuides !== false;
|
|
1493
|
+
this.minimapEnabled = options.minimap !== false;
|
|
1494
|
+
this.autoLayout = Boolean(options.autoLayout);
|
|
1495
|
+
this.minimapFrame = 0;
|
|
1496
|
+
this.minimapDrag = null;
|
|
1497
|
+
this.shortcutsHelpOpen = false;
|
|
1185
1498
|
this.autoFit = Boolean(options.autoFit);
|
|
1186
1499
|
this.readyEmitted = false;
|
|
1187
1500
|
this.resizeObserver = null;
|
|
@@ -1222,6 +1535,7 @@ var VdFlowchart = class {
|
|
|
1222
1535
|
if (!ready) return false;
|
|
1223
1536
|
this.readyEmitted = true;
|
|
1224
1537
|
if (this.autoFit) this.fitView();
|
|
1538
|
+
else this.scheduleMinimap();
|
|
1225
1539
|
this.emit("ready", this);
|
|
1226
1540
|
return true;
|
|
1227
1541
|
};
|
|
@@ -1291,8 +1605,25 @@ var VdFlowchart = class {
|
|
|
1291
1605
|
className: "vd-flowchart-toolbar-label",
|
|
1292
1606
|
text: "100%"
|
|
1293
1607
|
});
|
|
1608
|
+
this.shortcutsButton = createElement("button", {
|
|
1609
|
+
className: "vd-flowchart-btn vd-flowchart-icon-btn vd-flowchart-shortcuts-btn",
|
|
1610
|
+
title: "Keyboard shortcuts (?)",
|
|
1611
|
+
text: "?"
|
|
1612
|
+
});
|
|
1613
|
+
this.shortcutsButton.setAttribute("type", "button");
|
|
1614
|
+
this.shortcutsButton.setAttribute("data-flowchart-action", "shortcuts");
|
|
1615
|
+
this.shortcutsButton.setAttribute("aria-label", "Keyboard shortcuts");
|
|
1616
|
+
this.shortcutsButton.setAttribute("aria-expanded", "false");
|
|
1617
|
+
this.minimapButton = createToolbarButton({
|
|
1618
|
+
action: "minimap",
|
|
1619
|
+
label: "Toggle minimap",
|
|
1620
|
+
icon: "minimap"
|
|
1621
|
+
});
|
|
1622
|
+
this.minimapButton.setAttribute("aria-pressed", this.minimapEnabled ? "true" : "false");
|
|
1294
1623
|
toolbarLeft.appendChild(this.arrangeSelect);
|
|
1295
1624
|
toolbarLeft.appendChild(this.clearButton);
|
|
1625
|
+
toolbarRight.appendChild(this.minimapButton);
|
|
1626
|
+
toolbarRight.appendChild(this.shortcutsButton);
|
|
1296
1627
|
toolbarRight.appendChild(this.zoomLabel);
|
|
1297
1628
|
this.toolbar.appendChild(toolbarLeft);
|
|
1298
1629
|
this.toolbar.appendChild(toolbarRight);
|
|
@@ -1371,13 +1702,40 @@ var VdFlowchart = class {
|
|
|
1371
1702
|
this.previewLayer = svgEl("g", { class: "vd-flowchart-preview" });
|
|
1372
1703
|
this.nodesLayer = svgEl("g", { class: "vd-flowchart-nodes" });
|
|
1373
1704
|
this.overlayLayer = svgEl("g", { class: "vd-flowchart-overlay" });
|
|
1705
|
+
this.guidesLayer = svgEl("g", { class: "vd-flowchart-guides" });
|
|
1374
1706
|
this.world.appendChild(this.gridRect);
|
|
1375
1707
|
this.world.appendChild(this.edgesLayer);
|
|
1376
1708
|
this.world.appendChild(this.previewLayer);
|
|
1377
1709
|
this.world.appendChild(this.nodesLayer);
|
|
1378
1710
|
this.world.appendChild(this.overlayLayer);
|
|
1711
|
+
this.world.appendChild(this.guidesLayer);
|
|
1379
1712
|
this.svg.appendChild(this.world);
|
|
1380
1713
|
this.canvasEl.appendChild(this.svg);
|
|
1714
|
+
this.shortcutsHelp = createElement("div", { className: "vd-flowchart-shortcuts" });
|
|
1715
|
+
this.shortcutsHelp.id = nextId("flowchart-shortcuts");
|
|
1716
|
+
this.shortcutsHelp.setAttribute("role", "dialog");
|
|
1717
|
+
this.shortcutsHelp.setAttribute("aria-label", "Keyboard shortcuts");
|
|
1718
|
+
this.shortcutsHelp.hidden = true;
|
|
1719
|
+
this.shortcutsHelp.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
1720
|
+
this.shortcutsHelp.addEventListener("wheel", (event) => event.stopPropagation());
|
|
1721
|
+
this.shortcutsButton.setAttribute("aria-controls", this.shortcutsHelp.id);
|
|
1722
|
+
this.canvasEl.appendChild(this.shortcutsHelp);
|
|
1723
|
+
this.minimapEl = createElement("div", { className: "vd-flowchart-minimap" });
|
|
1724
|
+
this.minimapEl.setAttribute("aria-hidden", "true");
|
|
1725
|
+
this.minimapEl.hidden = true;
|
|
1726
|
+
this.minimapSvg = svgEl("svg", {
|
|
1727
|
+
class: "vd-flowchart-minimap-svg",
|
|
1728
|
+
width: MINIMAP_WIDTH,
|
|
1729
|
+
height: MINIMAP_HEIGHT,
|
|
1730
|
+
viewBox: `0 0 ${MINIMAP_WIDTH} ${MINIMAP_HEIGHT}`
|
|
1731
|
+
});
|
|
1732
|
+
this.minimapEl.appendChild(this.minimapSvg);
|
|
1733
|
+
this.minimapEl.addEventListener("pointerdown", (event) => this.handleMinimapPointer(event));
|
|
1734
|
+
this.minimapEl.addEventListener("pointermove", (event) => this.handleMinimapPointer(event));
|
|
1735
|
+
this.minimapEl.addEventListener("pointerup", (event) => this.endMinimapDrag(event));
|
|
1736
|
+
this.minimapEl.addEventListener("pointercancel", (event) => this.endMinimapDrag(event));
|
|
1737
|
+
this.minimapEl.addEventListener("wheel", (event) => event.stopPropagation());
|
|
1738
|
+
this.canvasEl.appendChild(this.minimapEl);
|
|
1381
1739
|
this.inspectorPanel = createElement("aside", {
|
|
1382
1740
|
className: "vd-flowchart-panel vd-flowchart-panel--inspector"
|
|
1383
1741
|
});
|
|
@@ -1485,11 +1843,15 @@ var VdFlowchart = class {
|
|
|
1485
1843
|
this.graphOutline.appendChild(this.graphList);
|
|
1486
1844
|
this.root.appendChild(this.graphOutline);
|
|
1487
1845
|
this.canvasEl.setAttribute("role", "group");
|
|
1846
|
+
this.syncCanvasLabel();
|
|
1847
|
+
this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
|
|
1848
|
+
}
|
|
1849
|
+
syncCanvasLabel() {
|
|
1850
|
+
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.";
|
|
1488
1851
|
this.canvasEl.setAttribute(
|
|
1489
1852
|
"aria-label",
|
|
1490
|
-
|
|
1853
|
+
`Diagram canvas. ${keys} Press ? for all shortcuts. Graph outline provides connections.`
|
|
1491
1854
|
);
|
|
1492
|
-
this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
|
|
1493
1855
|
}
|
|
1494
1856
|
renderGraphOutline() {
|
|
1495
1857
|
if (!this.graphOutline) return;
|
|
@@ -1499,8 +1861,9 @@ var VdFlowchart = class {
|
|
|
1499
1861
|
const selected = this.selection?.kind === "node" ? this.selection.id : null;
|
|
1500
1862
|
this.graphStatus.textContent = selected ? `Selected: ${labels.get(selected)}. ${nodes.length} nodes, ${edges.length} connections.` : `${nodes.length} nodes, ${edges.length} connections.`;
|
|
1501
1863
|
if (!this.graphOutline.open) return;
|
|
1864
|
+
const hidden = this.getHiddenNodeIds();
|
|
1502
1865
|
const signature = JSON.stringify([
|
|
1503
|
-
nodes.map((node) => [node.id, node.text]),
|
|
1866
|
+
nodes.map((node) => [node.id, node.text, Boolean(node.collapsed), hidden.has(node.id)]),
|
|
1504
1867
|
edges.map((edge) => [edge.id, edge.from.nodeId, edge.to.nodeId, edge.label])
|
|
1505
1868
|
]);
|
|
1506
1869
|
if (signature !== this.graphSignature) {
|
|
@@ -1526,8 +1889,10 @@ var VdFlowchart = class {
|
|
|
1526
1889
|
const from = incoming.get(node.id);
|
|
1527
1890
|
const relationships = [
|
|
1528
1891
|
to.length ? `Connects to ${to.join(", ")}.` : "No outgoing connections.",
|
|
1529
|
-
from.length ? `Connected from ${from.join(", ")}.` : ""
|
|
1530
|
-
|
|
1892
|
+
from.length ? `Connected from ${from.join(", ")}.` : "",
|
|
1893
|
+
node.collapsed ? "Branch collapsed." : "",
|
|
1894
|
+
hidden.has(node.id) ? "Hidden in a collapsed branch." : ""
|
|
1895
|
+
].filter(Boolean);
|
|
1531
1896
|
this.graphList.appendChild(
|
|
1532
1897
|
createElement("li", { text: `${labels.get(node.id)}. ${relationships.join(" ")}` })
|
|
1533
1898
|
);
|
|
@@ -1612,6 +1977,157 @@ var VdFlowchart = class {
|
|
|
1612
1977
|
if (action === "undo") this.undo();
|
|
1613
1978
|
if (action === "redo") this.redo();
|
|
1614
1979
|
if (action === "clear" && !this.readonly) this.clear();
|
|
1980
|
+
if (action === "shortcuts") this.toggleShortcutsHelp();
|
|
1981
|
+
if (action === "minimap") this.setMinimapEnabled(!this.minimapEnabled);
|
|
1982
|
+
}
|
|
1983
|
+
setMinimapEnabled(enabled) {
|
|
1984
|
+
this.minimapEnabled = Boolean(enabled);
|
|
1985
|
+
this.minimapButton.setAttribute("aria-pressed", this.minimapEnabled ? "true" : "false");
|
|
1986
|
+
this.drawMinimap();
|
|
1987
|
+
return this;
|
|
1988
|
+
}
|
|
1989
|
+
scheduleMinimap() {
|
|
1990
|
+
if (this.minimapFrame || !hasWindow()) return;
|
|
1991
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (callback) => setTimeout(callback, 16);
|
|
1992
|
+
this.minimapFrame = raf(() => {
|
|
1993
|
+
this.minimapFrame = 0;
|
|
1994
|
+
this.drawMinimap();
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
// World-to-minimap mapping covering every visible node and the viewport.
|
|
1998
|
+
getMinimapTransform() {
|
|
1999
|
+
const viewport = this.documentData.viewport;
|
|
2000
|
+
const width = this.canvasEl.clientWidth;
|
|
2001
|
+
const height = this.canvasEl.clientHeight;
|
|
2002
|
+
const view = {
|
|
2003
|
+
left: -viewport.x / viewport.scale,
|
|
2004
|
+
top: -viewport.y / viewport.scale,
|
|
2005
|
+
right: (width - viewport.x) / viewport.scale,
|
|
2006
|
+
bottom: (height - viewport.y) / viewport.scale
|
|
2007
|
+
};
|
|
2008
|
+
const content = getBounds(this.getVisibleNodes());
|
|
2009
|
+
const left = Math.min(content.left, view.left);
|
|
2010
|
+
const top = Math.min(content.top, view.top);
|
|
2011
|
+
const spanX = Math.max(1, Math.max(content.right, view.right) - left);
|
|
2012
|
+
const spanY = Math.max(1, Math.max(content.bottom, view.bottom) - top);
|
|
2013
|
+
const padding = 8;
|
|
2014
|
+
const scale = Math.min(
|
|
2015
|
+
(MINIMAP_WIDTH - padding * 2) / spanX,
|
|
2016
|
+
(MINIMAP_HEIGHT - padding * 2) / spanY
|
|
2017
|
+
);
|
|
2018
|
+
return {
|
|
2019
|
+
scale,
|
|
2020
|
+
offsetX: (MINIMAP_WIDTH - spanX * scale) / 2 - left * scale,
|
|
2021
|
+
offsetY: (MINIMAP_HEIGHT - spanY * scale) / 2 - top * scale,
|
|
2022
|
+
view
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
drawMinimap() {
|
|
2026
|
+
if (this.destroyed || !this.minimapEl) return;
|
|
2027
|
+
const nodes = this.getVisibleNodes();
|
|
2028
|
+
const visible = this.minimapEnabled && nodes.length > 0 && this.canvasEl.clientWidth >= MINIMAP_MIN_CANVAS_WIDTH;
|
|
2029
|
+
this.minimapEl.hidden = !visible;
|
|
2030
|
+
if (!visible) return;
|
|
2031
|
+
const transform = this.minimapDrag?.transform || this.getMinimapTransform();
|
|
2032
|
+
const view = this.getMinimapTransform().view;
|
|
2033
|
+
const map = (x, y) => ({
|
|
2034
|
+
x: formatNumber(x * transform.scale + transform.offsetX),
|
|
2035
|
+
y: formatNumber(y * transform.scale + transform.offsetY)
|
|
2036
|
+
});
|
|
2037
|
+
clearChildren(this.minimapSvg);
|
|
2038
|
+
nodes.forEach((node) => {
|
|
2039
|
+
const point = map(node.x, node.y);
|
|
2040
|
+
const selected = this.isNodeSelected(node.id);
|
|
2041
|
+
this.minimapSvg.appendChild(
|
|
2042
|
+
svgEl("rect", {
|
|
2043
|
+
class: `vd-flowchart-minimap-node${selected ? " is-selected" : ""}`,
|
|
2044
|
+
x: point.x,
|
|
2045
|
+
y: point.y,
|
|
2046
|
+
width: formatNumber(Math.max(1.5, node.width * transform.scale)),
|
|
2047
|
+
height: formatNumber(Math.max(1.5, node.height * transform.scale)),
|
|
2048
|
+
rx: 1.5
|
|
2049
|
+
})
|
|
2050
|
+
);
|
|
2051
|
+
});
|
|
2052
|
+
const corner = map(view.left, view.top);
|
|
2053
|
+
this.minimapSvg.appendChild(
|
|
2054
|
+
svgEl("rect", {
|
|
2055
|
+
class: "vd-flowchart-minimap-viewport",
|
|
2056
|
+
x: corner.x,
|
|
2057
|
+
y: corner.y,
|
|
2058
|
+
width: formatNumber((view.right - view.left) * transform.scale),
|
|
2059
|
+
height: formatNumber((view.bottom - view.top) * transform.scale)
|
|
2060
|
+
})
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
// Centre the main view on the world point under the minimap pointer. The
|
|
2064
|
+
// mapping is frozen for the whole drag so the map does not shift under it.
|
|
2065
|
+
handleMinimapPointer(event) {
|
|
2066
|
+
event.stopPropagation();
|
|
2067
|
+
if (event.type === "pointerdown") {
|
|
2068
|
+
if (event.button != null && event.button !== 0) return;
|
|
2069
|
+
event.preventDefault();
|
|
2070
|
+
this.minimapDrag = { pointerId: event.pointerId, transform: this.getMinimapTransform() };
|
|
2071
|
+
try {
|
|
2072
|
+
this.minimapEl.setPointerCapture?.(event.pointerId);
|
|
2073
|
+
} catch {
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (!this.minimapDrag || this.minimapDrag.pointerId !== event.pointerId) return;
|
|
2077
|
+
const rect = this.minimapEl.getBoundingClientRect();
|
|
2078
|
+
const { transform } = this.minimapDrag;
|
|
2079
|
+
this.centerViewOn(
|
|
2080
|
+
(event.clientX - rect.left - transform.offsetX) / transform.scale,
|
|
2081
|
+
(event.clientY - rect.top - transform.offsetY) / transform.scale
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
endMinimapDrag(event) {
|
|
2085
|
+
event.stopPropagation();
|
|
2086
|
+
if (!this.minimapDrag || this.minimapDrag.pointerId !== event.pointerId) return;
|
|
2087
|
+
this.minimapDrag = null;
|
|
2088
|
+
this.syncJsonTextarea();
|
|
2089
|
+
this.emitViewportChange("viewport:pan");
|
|
2090
|
+
this.drawMinimap();
|
|
2091
|
+
}
|
|
2092
|
+
centerViewOn(worldX, worldY) {
|
|
2093
|
+
const viewport = this.documentData.viewport;
|
|
2094
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
2095
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
2096
|
+
viewport.x = formatNumber(width / 2 - worldX * viewport.scale);
|
|
2097
|
+
viewport.y = formatNumber(height / 2 - worldY * viewport.scale);
|
|
2098
|
+
this.render({ inspector: false, json: false });
|
|
2099
|
+
return this;
|
|
2100
|
+
}
|
|
2101
|
+
toggleShortcutsHelp(force) {
|
|
2102
|
+
const open = typeof force === "boolean" ? force : !this.shortcutsHelpOpen;
|
|
2103
|
+
this.shortcutsHelpOpen = open;
|
|
2104
|
+
this.shortcutsButton.setAttribute("aria-expanded", open ? "true" : "false");
|
|
2105
|
+
if (open) this.renderShortcutsHelp();
|
|
2106
|
+
this.shortcutsHelp.hidden = !open;
|
|
2107
|
+
if (!open && this.shortcutsHelp.contains(document.activeElement)) this.canvasEl.focus();
|
|
2108
|
+
return this;
|
|
2109
|
+
}
|
|
2110
|
+
renderShortcutsHelp() {
|
|
2111
|
+
clearChildren(this.shortcutsHelp);
|
|
2112
|
+
const header = createElement("div", { className: "vd-flowchart-shortcuts-header" });
|
|
2113
|
+
header.appendChild(
|
|
2114
|
+
createElement("h4", { className: "vd-flowchart-panel-title", text: "Keyboard shortcuts" })
|
|
2115
|
+
);
|
|
2116
|
+
const close = createElement("button", {
|
|
2117
|
+
className: "vd-flowchart-btn vd-flowchart-icon-btn",
|
|
2118
|
+
text: "\xD7"
|
|
2119
|
+
});
|
|
2120
|
+
close.setAttribute("type", "button");
|
|
2121
|
+
close.setAttribute("aria-label", "Close keyboard shortcuts");
|
|
2122
|
+
close.addEventListener("click", () => this.toggleShortcutsHelp(false));
|
|
2123
|
+
header.appendChild(close);
|
|
2124
|
+
this.shortcutsHelp.appendChild(header);
|
|
2125
|
+
const list = createElement("dl", { className: "vd-flowchart-shortcuts-list" });
|
|
2126
|
+
getShortcutRows(this.keyboardShortcuts, this.readonly).forEach(([keys, action]) => {
|
|
2127
|
+
list.appendChild(createElement("dt", { text: keys }));
|
|
2128
|
+
list.appendChild(createElement("dd", { text: action }));
|
|
2129
|
+
});
|
|
2130
|
+
this.shortcutsHelp.appendChild(list);
|
|
1615
2131
|
}
|
|
1616
2132
|
handleArrangeChange(event) {
|
|
1617
2133
|
if (this.readonly) return;
|
|
@@ -1720,38 +2236,48 @@ var VdFlowchart = class {
|
|
|
1720
2236
|
);
|
|
1721
2237
|
}
|
|
1722
2238
|
}
|
|
2239
|
+
// Keys the editor consumes stop here, so page-level shortcuts (a docs
|
|
2240
|
+
// site's "/" search, a host's Escape handler) do not also fire.
|
|
1723
2241
|
handleKeyDown(event) {
|
|
1724
|
-
|
|
2242
|
+
const alreadyHandled = event.defaultPrevented;
|
|
2243
|
+
this.handleEditorKey(event);
|
|
2244
|
+
if (!alreadyHandled && event.defaultPrevented) event.stopPropagation();
|
|
2245
|
+
}
|
|
2246
|
+
handleEditorKey(event) {
|
|
2247
|
+
const target = event.target;
|
|
2248
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable)) {
|
|
1725
2249
|
return;
|
|
1726
2250
|
}
|
|
1727
|
-
if (event.
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
event.preventDefault();
|
|
1739
|
-
this.selectNode(nodes[next].id);
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
if (event.key === "Enter" && !this.readonly && this.selection?.kind === "node") {
|
|
1743
|
-
event.preventDefault();
|
|
1744
|
-
this.startTextEdit(this.selection.id);
|
|
1745
|
-
return;
|
|
1746
|
-
}
|
|
2251
|
+
if (event.isComposing) return;
|
|
2252
|
+
const key = event.key;
|
|
2253
|
+
const modKey = event.metaKey || event.ctrlKey;
|
|
2254
|
+
if (key === "Escape" && this.shortcutsHelpOpen) {
|
|
2255
|
+
event.preventDefault();
|
|
2256
|
+
this.toggleShortcutsHelp(false);
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
if (target === this.canvasEl && this.handleCanvasKeyDown(event)) {
|
|
2260
|
+
event.preventDefault();
|
|
2261
|
+
return;
|
|
1747
2262
|
}
|
|
1748
2263
|
if (this.readonly) return;
|
|
1749
|
-
if (
|
|
2264
|
+
if (key === "Escape") {
|
|
2265
|
+
if (this.activeTool) this.setActiveTool(null);
|
|
2266
|
+
else if (this.reconnectEdgeId) {
|
|
2267
|
+
this.reconnectEdgeId = null;
|
|
2268
|
+
this.render({ inspector: false, json: false });
|
|
2269
|
+
} else if (this.selection) this.select(null);
|
|
2270
|
+
else return;
|
|
1750
2271
|
event.preventDefault();
|
|
1751
|
-
this.setActiveTool(null);
|
|
1752
2272
|
return;
|
|
1753
2273
|
}
|
|
1754
|
-
|
|
2274
|
+
if (modKey && (key === "d" || key === "D")) {
|
|
2275
|
+
if (this.selection?.kind === "node") {
|
|
2276
|
+
event.preventDefault();
|
|
2277
|
+
this.duplicateSelection();
|
|
2278
|
+
}
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
1755
2281
|
if (modKey && (event.key === "z" || event.key === "Z")) {
|
|
1756
2282
|
event.preventDefault();
|
|
1757
2283
|
if (event.shiftKey) {
|
|
@@ -1790,11 +2316,248 @@ var VdFlowchart = class {
|
|
|
1790
2316
|
if (!this.selection) return;
|
|
1791
2317
|
if (event.key === "Backspace" || event.key === "Delete") {
|
|
1792
2318
|
event.preventDefault();
|
|
2319
|
+
if (this.selection.kind === "node" && !this.hasMultiSelection()) {
|
|
2320
|
+
const removedId = this.selection.id;
|
|
2321
|
+
const parent = this.getParentNode(removedId);
|
|
2322
|
+
const sibling = parent ? this.getChildNodes(parent.id).find((node) => node.id !== removedId) : null;
|
|
2323
|
+
if (this.deleteSelection()) {
|
|
2324
|
+
const next = parent && this.findNode(parent.id) || sibling;
|
|
2325
|
+
if (next) this.select({ kind: "node", id: next.id });
|
|
2326
|
+
}
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
1793
2329
|
this.deleteSelection();
|
|
1794
2330
|
}
|
|
1795
2331
|
}
|
|
2332
|
+
// Keys that only apply while the canvas itself has focus. Returns true when
|
|
2333
|
+
// the key was handled (the caller prevents the default action).
|
|
2334
|
+
handleCanvasKeyDown(event) {
|
|
2335
|
+
const key = event.key;
|
|
2336
|
+
const modKey = event.metaKey || event.ctrlKey;
|
|
2337
|
+
const mindmap = this.keyboardShortcuts === "mindmap";
|
|
2338
|
+
const selectedNode = this.selection?.kind === "node" ? this.findNode(this.selection.id) : null;
|
|
2339
|
+
const selectedEdge = this.selection?.kind === "edge" ? this.findEdge(this.selection.id) : null;
|
|
2340
|
+
const movable = !this.readonly && Boolean(selectedNode);
|
|
2341
|
+
const editable = movable && !this.hasMultiSelection();
|
|
2342
|
+
if (key === "?" && !modKey && !event.altKey) {
|
|
2343
|
+
this.toggleShortcutsHelp();
|
|
2344
|
+
return true;
|
|
2345
|
+
}
|
|
2346
|
+
if (modKey && !event.altKey) {
|
|
2347
|
+
if (key === "=" || key === "+") return Boolean(this.zoomIn());
|
|
2348
|
+
if (key === "-" || key === "_") return Boolean(this.zoomOut());
|
|
2349
|
+
if (key === "0") return Boolean(this.zoomTo(1));
|
|
2350
|
+
if (key === "a" || key === "A") {
|
|
2351
|
+
this.selectNodes(this.getVisibleNodes().map((node) => node.id));
|
|
2352
|
+
return true;
|
|
2353
|
+
}
|
|
2354
|
+
if (key === "/" && selectedNode && !this.hasMultiSelection()) {
|
|
2355
|
+
this.toggleCollapsed(selectedNode.id);
|
|
2356
|
+
return true;
|
|
2357
|
+
}
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
if (event.shiftKey && !event.altKey && (key === "!" || event.code === "Digit1")) {
|
|
2361
|
+
return Boolean(this.fitView());
|
|
2362
|
+
}
|
|
2363
|
+
const direction = ARROW_DIRECTIONS[key];
|
|
2364
|
+
if (direction) {
|
|
2365
|
+
if (event.altKey) {
|
|
2366
|
+
if (movable) {
|
|
2367
|
+
this.nudgeNodes(this.getSelectedNodeIds(), direction, event.shiftKey ? 1 : this.gridSize);
|
|
2368
|
+
}
|
|
2369
|
+
return true;
|
|
2370
|
+
}
|
|
2371
|
+
if (event.shiftKey) {
|
|
2372
|
+
const anchorId = selectedNode?.id ?? (selectedEdge ? this.edgeCycleAnchor : null);
|
|
2373
|
+
return anchorId ? this.selectSideEdge(anchorId, direction) : false;
|
|
2374
|
+
}
|
|
2375
|
+
this.navigateSelection(direction);
|
|
2376
|
+
return true;
|
|
2377
|
+
}
|
|
2378
|
+
const nodes = this.getVisibleNodes();
|
|
2379
|
+
if ((key === "Home" || key === "End") && nodes.length) {
|
|
2380
|
+
this.selectNode(nodes[key === "Home" ? 0 : nodes.length - 1].id);
|
|
2381
|
+
return true;
|
|
2382
|
+
}
|
|
2383
|
+
if (selectedEdge && !this.readonly && !event.altKey) {
|
|
2384
|
+
if (key === "F2" || key === "Enter" || key === " " && mindmap) {
|
|
2385
|
+
return this.startEdgeLabelEdit(selectedEdge.id);
|
|
2386
|
+
}
|
|
2387
|
+
if (mindmap && key.length === 1 && key !== " ") {
|
|
2388
|
+
return this.startEdgeLabelEdit(selectedEdge.id, { initialText: key });
|
|
2389
|
+
}
|
|
2390
|
+
return false;
|
|
2391
|
+
}
|
|
2392
|
+
if (!editable || event.altKey) return false;
|
|
2393
|
+
if (key === "F2" || key === "Enter" && !mindmap || key === " " && mindmap) {
|
|
2394
|
+
this.startTextEdit(selectedNode.id);
|
|
2395
|
+
return true;
|
|
2396
|
+
}
|
|
2397
|
+
if (!mindmap) return false;
|
|
2398
|
+
if (key === "Tab" && !event.shiftKey) {
|
|
2399
|
+
this.insertBranchNode(selectedNode.id, { edit: true });
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2402
|
+
if (key === "Enter") {
|
|
2403
|
+
this.insertSiblingNode(selectedNode.id, { before: event.shiftKey, edit: true });
|
|
2404
|
+
return true;
|
|
2405
|
+
}
|
|
2406
|
+
if (key.length === 1 && key !== " " && isNodeTextEditable(selectedNode)) {
|
|
2407
|
+
this.startTextEdit(selectedNode.id, { initialText: key });
|
|
2408
|
+
return true;
|
|
2409
|
+
}
|
|
2410
|
+
return false;
|
|
2411
|
+
}
|
|
2412
|
+
// Move the selection to the nearest node in `direction`; with nothing (or an
|
|
2413
|
+
// edge) selected, start from the node closest to the centre of the view.
|
|
2414
|
+
navigateSelection(direction) {
|
|
2415
|
+
const nodes = this.getVisibleNodes();
|
|
2416
|
+
if (!nodes.length) return false;
|
|
2417
|
+
const current = this.selection?.kind === "node" ? this.findNode(this.selection.id) : null;
|
|
2418
|
+
const edge = this.selection?.kind === "edge" ? this.findEdge(this.selection.id) : null;
|
|
2419
|
+
const next = current ? findSpatialNeighbor(nodes, current, direction) : edge ? this.getEdgeEndInDirection(edge, direction) : findNearestNode(nodes, this.getViewportCenter());
|
|
2420
|
+
if (!next) return false;
|
|
2421
|
+
this.selectNode(next.id);
|
|
2422
|
+
this.revealNode(next.id);
|
|
2423
|
+
return true;
|
|
2424
|
+
}
|
|
2425
|
+
// The end of `edge` that best matches `direction`, measured from the label
|
|
2426
|
+
// anchor so either end can be reached from a selected connection.
|
|
2427
|
+
getEdgeEndInDirection(edge, direction) {
|
|
2428
|
+
const fromNode = this.findNode(edge.from.nodeId);
|
|
2429
|
+
const toNode = this.findNode(edge.to.nodeId);
|
|
2430
|
+
if (!fromNode || !toNode) return fromNode || toNode;
|
|
2431
|
+
const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
|
|
2432
|
+
const vector = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] }[direction];
|
|
2433
|
+
const score = (node) => (node.x + node.width / 2 - labelX) * vector[0] + (node.y + node.height / 2 - labelY) * vector[1];
|
|
2434
|
+
return score(toNode) >= score(fromNode) ? toNode : fromNode;
|
|
2435
|
+
}
|
|
2436
|
+
// Connections attached to `nodeId` on the `direction` side, outgoing or
|
|
2437
|
+
// incoming, in document order.
|
|
2438
|
+
getSideEdges(nodeId, direction) {
|
|
2439
|
+
const port = DIRECTION_PORTS[direction];
|
|
2440
|
+
return this.documentData.edges.filter(
|
|
2441
|
+
(edge) => edge.from.nodeId === nodeId && edge.from.port === port || edge.to.nodeId === nodeId && edge.to.port === port
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
// Select a connection on one side of a node; repeating cycles through them.
|
|
2445
|
+
selectSideEdge(nodeId, direction) {
|
|
2446
|
+
const edges = this.getSideEdges(nodeId, direction);
|
|
2447
|
+
if (!edges.length) return false;
|
|
2448
|
+
const currentIndex = this.selection?.kind === "edge" ? edges.findIndex((edge) => edge.id === this.selection.id) : -1;
|
|
2449
|
+
const next = edges[(currentIndex + 1) % edges.length];
|
|
2450
|
+
this.select({ kind: "edge", id: next.id });
|
|
2451
|
+
this.edgeCycleAnchor = nodeId;
|
|
2452
|
+
return true;
|
|
2453
|
+
}
|
|
2454
|
+
nudgeNode(nodeId, direction, distance) {
|
|
2455
|
+
const [node] = this.nudgeNodes([nodeId], direction, distance);
|
|
2456
|
+
return node || null;
|
|
2457
|
+
}
|
|
2458
|
+
// Move nodes together; repeated nudges of the same selection coalesce into
|
|
2459
|
+
// one undo step.
|
|
2460
|
+
nudgeNodes(nodeIds, direction, distance) {
|
|
2461
|
+
const step = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] }[direction];
|
|
2462
|
+
if (this.readonly || !step) return [];
|
|
2463
|
+
const nodes = nodeIds.map((id) => this.findNode(id)).filter(Boolean);
|
|
2464
|
+
if (!nodes.length) return [];
|
|
2465
|
+
nodes.forEach((node) => {
|
|
2466
|
+
node.x = formatNumber(node.x + step[0] * distance);
|
|
2467
|
+
node.y = formatNumber(node.y + step[1] * distance);
|
|
2468
|
+
});
|
|
2469
|
+
this.render({ inspector: true, json: true });
|
|
2470
|
+
this.emitChange(
|
|
2471
|
+
"node:nudge",
|
|
2472
|
+
nodes.length > 1 ? { nodeIds: nodes.map((node) => node.id) } : { node: deepClone(nodes[0]) }
|
|
2473
|
+
);
|
|
2474
|
+
return nodes.map((node) => deepClone(node));
|
|
2475
|
+
}
|
|
2476
|
+
zoomTo(scale) {
|
|
2477
|
+
const viewport = this.documentData.viewport;
|
|
2478
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
2479
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
2480
|
+
this.scaleAround(
|
|
2481
|
+
clamp(scale, MIN_SCALE, MAX_SCALE) / viewport.scale,
|
|
2482
|
+
width / 2,
|
|
2483
|
+
height / 2,
|
|
2484
|
+
"viewport:zoom"
|
|
2485
|
+
);
|
|
2486
|
+
return this;
|
|
2487
|
+
}
|
|
2488
|
+
duplicateSelection() {
|
|
2489
|
+
if (this.readonly || this.selection?.kind !== "node") return null;
|
|
2490
|
+
if (this.hasMultiSelection()) return this.pasteNodeGroup(this.captureNodeGroup());
|
|
2491
|
+
const node = this.findNode(this.selection.id);
|
|
2492
|
+
return node ? this.pasteNodeData(node) : null;
|
|
2493
|
+
}
|
|
2494
|
+
// Selected nodes plus the connections that run between them.
|
|
2495
|
+
captureNodeGroup() {
|
|
2496
|
+
const ids = new Set(this.getSelectedNodeIds());
|
|
2497
|
+
return {
|
|
2498
|
+
nodes: deepClone(this.documentData.nodes.filter((node) => ids.has(node.id))),
|
|
2499
|
+
edges: deepClone(
|
|
2500
|
+
this.documentData.edges.filter(
|
|
2501
|
+
(edge) => ids.has(edge.from.nodeId) && ids.has(edge.to.nodeId)
|
|
2502
|
+
)
|
|
2503
|
+
)
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
// Paste a captured group offset by one step, remapping ids so internal
|
|
2507
|
+
// connections follow their copies. One change, one undo step.
|
|
2508
|
+
pasteNodeGroup(group) {
|
|
2509
|
+
if (this.readonly || !group?.nodes?.length) return [];
|
|
2510
|
+
const usedNodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
2511
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
2512
|
+
const nodes = group.nodes.map((data, index) => {
|
|
2513
|
+
const node = normalizeNode(
|
|
2514
|
+
{ ...data, id: void 0, x: data.x + 24, y: data.y + 24 },
|
|
2515
|
+
this.documentData.nodes.length + index,
|
|
2516
|
+
usedNodeIds
|
|
2517
|
+
);
|
|
2518
|
+
idMap.set(data.id, node.id);
|
|
2519
|
+
return node;
|
|
2520
|
+
});
|
|
2521
|
+
this.documentData.nodes.push(...nodes);
|
|
2522
|
+
const nodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
2523
|
+
const usedEdgeIds = new Set(this.documentData.edges.map((edge) => edge.id));
|
|
2524
|
+
group.edges.forEach((data, index) => {
|
|
2525
|
+
const edge = normalizeEdge(
|
|
2526
|
+
{
|
|
2527
|
+
...data,
|
|
2528
|
+
id: void 0,
|
|
2529
|
+
from: { ...data.from, nodeId: idMap.get(data.from.nodeId) },
|
|
2530
|
+
to: { ...data.to, nodeId: idMap.get(data.to.nodeId) }
|
|
2531
|
+
},
|
|
2532
|
+
this.documentData.edges.length + index,
|
|
2533
|
+
nodeIds,
|
|
2534
|
+
usedEdgeIds
|
|
2535
|
+
);
|
|
2536
|
+
if (edge) this.documentData.edges.push(edge);
|
|
2537
|
+
});
|
|
2538
|
+
const ids = nodes.map((node) => node.id);
|
|
2539
|
+
this.selectNodes(ids);
|
|
2540
|
+
this.emitChange("nodes:add", { nodeIds: ids });
|
|
2541
|
+
return nodes.map((node) => deepClone(node));
|
|
2542
|
+
}
|
|
2543
|
+
pasteNodeData(data) {
|
|
2544
|
+
const usedIds = new Set(this.documentData.nodes.map((node2) => node2.id));
|
|
2545
|
+
const node = normalizeNode(
|
|
2546
|
+
{ ...data, id: void 0, collapsed: void 0, x: data.x + 24, y: data.y + 24 },
|
|
2547
|
+
this.documentData.nodes.length,
|
|
2548
|
+
usedIds
|
|
2549
|
+
);
|
|
2550
|
+
this.documentData.nodes.push(node);
|
|
2551
|
+
this.select({ kind: "node", id: node.id });
|
|
2552
|
+
this.emitChange("node:add", { node: deepClone(node) });
|
|
2553
|
+
return deepClone(node);
|
|
2554
|
+
}
|
|
1796
2555
|
copySelection() {
|
|
1797
2556
|
if (!this.selection) return false;
|
|
2557
|
+
if (this.hasMultiSelection()) {
|
|
2558
|
+
this.clipboard = { kind: "nodes", data: this.captureNodeGroup() };
|
|
2559
|
+
return true;
|
|
2560
|
+
}
|
|
1798
2561
|
if (this.selection.kind === "node") {
|
|
1799
2562
|
const node = this.findNode(this.selection.id);
|
|
1800
2563
|
if (!node) return false;
|
|
@@ -1809,22 +2572,14 @@ var VdFlowchart = class {
|
|
|
1809
2572
|
pasteClipboard() {
|
|
1810
2573
|
if (!this.clipboard || this.readonly) return false;
|
|
1811
2574
|
if (this.clipboard.kind === "node") {
|
|
1812
|
-
|
|
1813
|
-
const node = normalizeNode(
|
|
1814
|
-
{
|
|
1815
|
-
...this.clipboard.data,
|
|
1816
|
-
id: void 0,
|
|
1817
|
-
x: this.clipboard.data.x + 24,
|
|
1818
|
-
y: this.clipboard.data.y + 24
|
|
1819
|
-
},
|
|
1820
|
-
this.documentData.nodes.length,
|
|
1821
|
-
usedIds2
|
|
1822
|
-
);
|
|
1823
|
-
this.documentData.nodes.push(node);
|
|
1824
|
-
this.select({ kind: "node", id: node.id });
|
|
1825
|
-
this.emitChange("node:add", { node: deepClone(node) });
|
|
2575
|
+
this.pasteNodeData(this.clipboard.data);
|
|
1826
2576
|
return true;
|
|
1827
2577
|
}
|
|
2578
|
+
if (this.clipboard.kind === "nodes") {
|
|
2579
|
+
const pasted = this.pasteNodeGroup(this.clipboard.data);
|
|
2580
|
+
this.clipboard = { kind: "nodes", data: this.captureNodeGroup() };
|
|
2581
|
+
return pasted.length > 0;
|
|
2582
|
+
}
|
|
1828
2583
|
const usedIds = new Set(this.documentData.edges.map((edge2) => edge2.id));
|
|
1829
2584
|
const nodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
1830
2585
|
const edge = normalizeEdge(
|
|
@@ -1851,16 +2606,16 @@ var VdFlowchart = class {
|
|
|
1851
2606
|
const edgeTarget = event.target.closest("[data-edge-id]");
|
|
1852
2607
|
if (edgeTarget && !event.target.closest("[data-edge-endpoint]")) {
|
|
1853
2608
|
const edgeId = edgeTarget.getAttribute("data-edge-id");
|
|
1854
|
-
if (edgeId && this.
|
|
1855
|
-
this.reconnectEdgeId = edgeId;
|
|
1856
|
-
this.select({ kind: "edge", id: edgeId });
|
|
2609
|
+
if (edgeId && this.startEdgeLabelEdit(edgeId)) {
|
|
1857
2610
|
event.preventDefault();
|
|
1858
2611
|
event.stopPropagation();
|
|
1859
2612
|
return;
|
|
1860
2613
|
}
|
|
1861
2614
|
}
|
|
1862
2615
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1863
|
-
if (!nodeTarget || event.target.closest("[data-edge-id]"))
|
|
2616
|
+
if (!nodeTarget || event.target.closest("[data-edge-id]") || event.target.closest("[data-collapse-toggle]")) {
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
1864
2619
|
const nodeId = nodeTarget.getAttribute("data-node-id");
|
|
1865
2620
|
if (this.startTextEdit(nodeId)) {
|
|
1866
2621
|
event.preventDefault();
|
|
@@ -1870,7 +2625,7 @@ var VdFlowchart = class {
|
|
|
1870
2625
|
handleClick(event) {
|
|
1871
2626
|
if (this.readonly || event.detail < 2) return;
|
|
1872
2627
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1873
|
-
if (!nodeTarget || event.target.closest("[data-port]") || event.target.closest("[data-resize-handle]"))
|
|
2628
|
+
if (!nodeTarget || event.target.closest("[data-port]") || event.target.closest("[data-resize-handle]") || event.target.closest("[data-collapse-toggle]"))
|
|
1874
2629
|
return;
|
|
1875
2630
|
if (this.startTextEdit(nodeTarget.getAttribute("data-node-id"))) {
|
|
1876
2631
|
event.preventDefault();
|
|
@@ -1883,6 +2638,13 @@ var VdFlowchart = class {
|
|
|
1883
2638
|
this.stopTextEdit({ commit: true });
|
|
1884
2639
|
}
|
|
1885
2640
|
this.canvasEl.focus();
|
|
2641
|
+
const collapseTarget = event.target.closest("[data-collapse-toggle]");
|
|
2642
|
+
if (collapseTarget) {
|
|
2643
|
+
this.lastNodePointer = null;
|
|
2644
|
+
this.toggleCollapsed(collapseTarget.getAttribute("data-collapse-toggle"));
|
|
2645
|
+
event.preventDefault();
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
1886
2648
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1887
2649
|
const portTarget = event.target.closest("[data-port]");
|
|
1888
2650
|
const resizeTarget = event.target.closest("[data-resize-handle]");
|
|
@@ -1918,6 +2680,13 @@ var VdFlowchart = class {
|
|
|
1918
2680
|
event.preventDefault();
|
|
1919
2681
|
return;
|
|
1920
2682
|
}
|
|
2683
|
+
if (nodeTarget && !portTarget && !resizeTarget && !edgeTarget && event.shiftKey) {
|
|
2684
|
+
this.reconnectEdgeId = null;
|
|
2685
|
+
this.lastNodePointer = null;
|
|
2686
|
+
this.toggleNodeSelection(nodeTarget.getAttribute("data-node-id"));
|
|
2687
|
+
event.preventDefault();
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
1921
2690
|
if (nodeTarget && !portTarget && !resizeTarget && !edgeTarget && !this.readonly) {
|
|
1922
2691
|
if (this.activeTool === "arrow") {
|
|
1923
2692
|
this.select({ kind: "node", id: nodeTarget.getAttribute("data-node-id") });
|
|
@@ -1944,6 +2713,9 @@ var VdFlowchart = class {
|
|
|
1944
2713
|
this.interaction = {
|
|
1945
2714
|
kind: "connect",
|
|
1946
2715
|
pointerId: event.pointerId,
|
|
2716
|
+
startClientX: event.clientX,
|
|
2717
|
+
startClientY: event.clientY,
|
|
2718
|
+
moved: false,
|
|
1947
2719
|
source: { nodeId, port },
|
|
1948
2720
|
target: null,
|
|
1949
2721
|
previousSnap: null,
|
|
@@ -1994,22 +2766,27 @@ var VdFlowchart = class {
|
|
|
1994
2766
|
if (nodeTarget) {
|
|
1995
2767
|
this.reconnectEdgeId = null;
|
|
1996
2768
|
const nodeId = nodeTarget.getAttribute("data-node-id");
|
|
1997
|
-
this.
|
|
2769
|
+
const groupIds = this.hasMultiSelection() && this.isNodeSelected(nodeId) ? this.getSelectedNodeIds() : [nodeId];
|
|
2770
|
+
if (groupIds.length > 1) this.selectNodes(groupIds, { primary: nodeId });
|
|
2771
|
+
else this.select({ kind: "node", id: nodeId });
|
|
1998
2772
|
if (!this.readonly) {
|
|
1999
|
-
|
|
2000
|
-
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2001
|
-
this.interaction = {
|
|
2002
|
-
kind: "drag-node",
|
|
2003
|
-
pointerId: event.pointerId,
|
|
2004
|
-
nodeId,
|
|
2005
|
-
offsetX: world.x - node.x,
|
|
2006
|
-
offsetY: world.y - node.y,
|
|
2007
|
-
moved: false
|
|
2008
|
-
};
|
|
2773
|
+
this.interaction = this.createDragInteraction(event, nodeId, groupIds);
|
|
2009
2774
|
this.capturePointer(event.pointerId);
|
|
2010
2775
|
}
|
|
2011
2776
|
return;
|
|
2012
2777
|
}
|
|
2778
|
+
if (event.shiftKey) {
|
|
2779
|
+
const startWorld = this.clientToWorld(event.clientX, event.clientY);
|
|
2780
|
+
this.interaction = {
|
|
2781
|
+
kind: "marquee",
|
|
2782
|
+
pointerId: event.pointerId,
|
|
2783
|
+
startWorld,
|
|
2784
|
+
baseIds: this.selection?.kind === "node" ? this.getSelectedNodeIds() : []
|
|
2785
|
+
};
|
|
2786
|
+
this.capturePointer(event.pointerId);
|
|
2787
|
+
event.preventDefault();
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2013
2790
|
this.select(null);
|
|
2014
2791
|
this.reconnectEdgeId = null;
|
|
2015
2792
|
this.interaction = {
|
|
@@ -2024,20 +2801,123 @@ var VdFlowchart = class {
|
|
|
2024
2801
|
this.capturePointer(event.pointerId);
|
|
2025
2802
|
event.preventDefault();
|
|
2026
2803
|
}
|
|
2804
|
+
createDragInteraction(event, nodeId, nodeIds) {
|
|
2805
|
+
const nodes = nodeIds.map((id) => this.findNode(id)).filter(Boolean);
|
|
2806
|
+
const moving = new Set(nodes.map((node) => node.id));
|
|
2807
|
+
return {
|
|
2808
|
+
kind: "drag-node",
|
|
2809
|
+
pointerId: event.pointerId,
|
|
2810
|
+
nodeId,
|
|
2811
|
+
nodeIds: nodes.map((node) => node.id),
|
|
2812
|
+
startWorld: this.clientToWorld(event.clientX, event.clientY),
|
|
2813
|
+
origins: new Map(nodes.map((node) => [node.id, { x: node.x, y: node.y }])),
|
|
2814
|
+
bounds: getBounds(nodes),
|
|
2815
|
+
guideLines: this.snapGuides ? collectGuideLines(this.getVisibleNodes().filter((node) => !moving.has(node.id))) : null,
|
|
2816
|
+
snapDisabled: false,
|
|
2817
|
+
moved: false
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
// Translate every dragged node by the pointer delta, snapping the group's
|
|
2821
|
+
// bounds to the nearest alignment guide on each axis. Alt turns snapping
|
|
2822
|
+
// off for the rest of the gesture.
|
|
2823
|
+
moveDraggedNodes(event) {
|
|
2824
|
+
const interaction = this.interaction;
|
|
2825
|
+
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2826
|
+
let dx = world.x - interaction.startWorld.x;
|
|
2827
|
+
let dy = world.y - interaction.startWorld.y;
|
|
2828
|
+
if (event.altKey) interaction.snapDisabled = true;
|
|
2829
|
+
let guides = [];
|
|
2830
|
+
if (interaction.guideLines && !interaction.snapDisabled) {
|
|
2831
|
+
const box = interaction.bounds;
|
|
2832
|
+
const threshold = GUIDE_SNAP_DISTANCE / this.documentData.viewport.scale;
|
|
2833
|
+
const left = box.left + dx;
|
|
2834
|
+
const top = box.top + dy;
|
|
2835
|
+
const width = box.right - box.left;
|
|
2836
|
+
const height = box.bottom - box.top;
|
|
2837
|
+
const snapX = snapToGuides(
|
|
2838
|
+
interaction.guideLines.x,
|
|
2839
|
+
[left, left + width / 2, left + width],
|
|
2840
|
+
threshold
|
|
2841
|
+
);
|
|
2842
|
+
const snapY = snapToGuides(
|
|
2843
|
+
interaction.guideLines.y,
|
|
2844
|
+
[top, top + height / 2, top + height],
|
|
2845
|
+
threshold
|
|
2846
|
+
);
|
|
2847
|
+
if (snapX) dx += snapX.offset;
|
|
2848
|
+
if (snapY) dy += snapY.offset;
|
|
2849
|
+
const snapped = { left: box.left + dx, top: box.top + dy, width, height };
|
|
2850
|
+
if (snapX) guides = guides.concat(guideSegments("x", snapX.lines, snapped));
|
|
2851
|
+
if (snapY) guides = guides.concat(guideSegments("y", snapY.lines, snapped));
|
|
2852
|
+
}
|
|
2853
|
+
let changed = false;
|
|
2854
|
+
interaction.nodeIds.forEach((id) => {
|
|
2855
|
+
const node = this.findNode(id);
|
|
2856
|
+
const origin = interaction.origins.get(id);
|
|
2857
|
+
if (!node || !origin) return;
|
|
2858
|
+
const nextX = formatNumber(origin.x + dx);
|
|
2859
|
+
const nextY = formatNumber(origin.y + dy);
|
|
2860
|
+
if (nextX === node.x && nextY === node.y) return;
|
|
2861
|
+
node.x = nextX;
|
|
2862
|
+
node.y = nextY;
|
|
2863
|
+
changed = true;
|
|
2864
|
+
this.renderDraggedNode(node);
|
|
2865
|
+
});
|
|
2866
|
+
if (changed) interaction.moved = true;
|
|
2867
|
+
this.renderGuides(guides);
|
|
2868
|
+
}
|
|
2869
|
+
// Live marquee: select every visible node the box touches, added to the
|
|
2870
|
+
// selection the gesture started from.
|
|
2871
|
+
updateMarquee(event) {
|
|
2872
|
+
const interaction = this.interaction;
|
|
2873
|
+
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2874
|
+
const box = {
|
|
2875
|
+
left: Math.min(interaction.startWorld.x, world.x),
|
|
2876
|
+
top: Math.min(interaction.startWorld.y, world.y),
|
|
2877
|
+
right: Math.max(interaction.startWorld.x, world.x),
|
|
2878
|
+
bottom: Math.max(interaction.startWorld.y, world.y)
|
|
2879
|
+
};
|
|
2880
|
+
clearChildren(this.guidesLayer);
|
|
2881
|
+
this.guidesLayer.appendChild(
|
|
2882
|
+
svgEl("rect", {
|
|
2883
|
+
class: "vd-flowchart-marquee",
|
|
2884
|
+
x: formatNumber(box.left),
|
|
2885
|
+
y: formatNumber(box.top),
|
|
2886
|
+
width: formatNumber(box.right - box.left),
|
|
2887
|
+
height: formatNumber(box.bottom - box.top)
|
|
2888
|
+
})
|
|
2889
|
+
);
|
|
2890
|
+
const hits = this.getVisibleNodes().filter(
|
|
2891
|
+
(node) => node.x <= box.right && node.x + node.width >= box.left && node.y <= box.bottom && node.y + node.height >= box.top
|
|
2892
|
+
).map((node) => node.id);
|
|
2893
|
+
const ids = [.../* @__PURE__ */ new Set([...interaction.baseIds, ...hits])];
|
|
2894
|
+
const key = ids.join(",");
|
|
2895
|
+
if (key === interaction.lastKey) return;
|
|
2896
|
+
interaction.lastKey = key;
|
|
2897
|
+
this.selectNodes(ids, { primary: hits[hits.length - 1] });
|
|
2898
|
+
}
|
|
2899
|
+
renderGuides(guides) {
|
|
2900
|
+
clearChildren(this.guidesLayer);
|
|
2901
|
+
guides.forEach((guide) => {
|
|
2902
|
+
this.guidesLayer.appendChild(
|
|
2903
|
+
svgEl("line", {
|
|
2904
|
+
class: "vd-flowchart-guide",
|
|
2905
|
+
x1: guide.x1,
|
|
2906
|
+
y1: guide.y1,
|
|
2907
|
+
x2: guide.x2,
|
|
2908
|
+
y2: guide.y2
|
|
2909
|
+
})
|
|
2910
|
+
);
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2027
2913
|
handlePointerMove(event) {
|
|
2028
2914
|
if (!this.interaction || this.interaction.pointerId !== event.pointerId) return;
|
|
2029
2915
|
if (this.interaction.kind === "drag-node") {
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
if (nextX !== node.x || nextY !== node.y) {
|
|
2036
|
-
node.x = nextX;
|
|
2037
|
-
node.y = nextY;
|
|
2038
|
-
this.interaction.moved = true;
|
|
2039
|
-
this.renderDraggedNode(node);
|
|
2040
|
-
}
|
|
2916
|
+
this.moveDraggedNodes(event);
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
if (this.interaction.kind === "marquee") {
|
|
2920
|
+
this.updateMarquee(event);
|
|
2041
2921
|
return;
|
|
2042
2922
|
}
|
|
2043
2923
|
if (this.interaction.kind === "resize-node") {
|
|
@@ -2072,6 +2952,14 @@ var VdFlowchart = class {
|
|
|
2072
2952
|
return;
|
|
2073
2953
|
}
|
|
2074
2954
|
if (this.interaction.kind === "connect") {
|
|
2955
|
+
if (!this.interaction.moved) {
|
|
2956
|
+
const travel = Math.hypot(
|
|
2957
|
+
event.clientX - this.interaction.startClientX,
|
|
2958
|
+
event.clientY - this.interaction.startClientY
|
|
2959
|
+
);
|
|
2960
|
+
if (!(travel > PORT_CLICK_SLOP)) return;
|
|
2961
|
+
this.interaction.moved = true;
|
|
2962
|
+
}
|
|
2075
2963
|
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2076
2964
|
const snapTarget = this.findConnectionTarget(
|
|
2077
2965
|
world,
|
|
@@ -2158,10 +3046,19 @@ var VdFlowchart = class {
|
|
|
2158
3046
|
}
|
|
2159
3047
|
return;
|
|
2160
3048
|
}
|
|
3049
|
+
if (interaction.kind === "marquee") {
|
|
3050
|
+
clearChildren(this.guidesLayer);
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
2161
3053
|
if (interaction.kind === "drag-node") {
|
|
3054
|
+
clearChildren(this.guidesLayer);
|
|
2162
3055
|
if (interaction.moved) {
|
|
2163
3056
|
this.render({ inspector: true, json: true });
|
|
2164
|
-
|
|
3057
|
+
if (interaction.nodeIds.length > 1) {
|
|
3058
|
+
this.emitChange("nodes:move", { nodeIds: [...interaction.nodeIds] });
|
|
3059
|
+
} else {
|
|
3060
|
+
this.emitChange("node:move", { node: deepClone(this.findNode(interaction.nodeId)) });
|
|
3061
|
+
}
|
|
2165
3062
|
} else {
|
|
2166
3063
|
this.render({ inspector: false, json: false });
|
|
2167
3064
|
}
|
|
@@ -2177,6 +3074,14 @@ var VdFlowchart = class {
|
|
|
2177
3074
|
return;
|
|
2178
3075
|
}
|
|
2179
3076
|
if (interaction.kind === "connect") {
|
|
3077
|
+
if (!interaction.moved && this.activeTool !== "arrow") {
|
|
3078
|
+
this.render({ inspector: false, json: false });
|
|
3079
|
+
this.insertBranchNode(interaction.source.nodeId, {
|
|
3080
|
+
direction: PORT_DIRECTIONS[interaction.source.port],
|
|
3081
|
+
edit: true
|
|
3082
|
+
});
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
2180
3085
|
const world = this.clientToWorld(event.clientX || 0, event.clientY || 0);
|
|
2181
3086
|
const snapTarget = interaction.target ? { node: this.findNode(interaction.target.nodeId), port: interaction.target.port } : this.findConnectionTarget(
|
|
2182
3087
|
world,
|
|
@@ -2252,7 +3157,7 @@ var VdFlowchart = class {
|
|
|
2252
3157
|
if (this.readonly) return false;
|
|
2253
3158
|
if (this.interaction?.kind === "connect" || this.interaction?.kind === "reconnect") return true;
|
|
2254
3159
|
if (this.activeTool === "arrow") return true;
|
|
2255
|
-
return this.selection?.kind === "node" && this.selection.id === node.id;
|
|
3160
|
+
return this.selection?.kind === "node" && this.selection.id === node.id && !this.hasMultiSelection();
|
|
2256
3161
|
}
|
|
2257
3162
|
getMarkerId(markerType, position, strokeWidth) {
|
|
2258
3163
|
const width = normalizeEdgeStrokeWidth(strokeWidth);
|
|
@@ -2275,7 +3180,7 @@ var VdFlowchart = class {
|
|
|
2275
3180
|
findConnectionTarget(worldPoint, excludeNodeId, previousSnap = null, referencePoint = null) {
|
|
2276
3181
|
const snapPadding = CONNECTION_SNAP_PADDING / this.documentData.viewport.scale;
|
|
2277
3182
|
const hysteresisMargin = CONNECTION_HYSTERESIS / this.documentData.viewport.scale;
|
|
2278
|
-
const best = this.
|
|
3183
|
+
const best = this.getVisibleNodes().reduce((candidate, node) => {
|
|
2279
3184
|
if (node.id === excludeNodeId) return candidate;
|
|
2280
3185
|
const distanceToBounds2 = getDistanceToNodeBounds(node, worldPoint);
|
|
2281
3186
|
if (distanceToBounds2 > snapPadding) return candidate;
|
|
@@ -2384,7 +3289,10 @@ var VdFlowchart = class {
|
|
|
2384
3289
|
updateToolbarLabel() {
|
|
2385
3290
|
this.zoomLabel.textContent = `${Math.round(this.documentData.viewport.scale * 100)}%`;
|
|
2386
3291
|
}
|
|
2387
|
-
|
|
3292
|
+
// Options: `initialText` replaces the label with typed text (type-to-edit);
|
|
3293
|
+
// `inserted` marks a node created by a keyboard/handle insertion so naming it
|
|
3294
|
+
// shares the insertion's undo step.
|
|
3295
|
+
startTextEdit(nodeId, options = {}) {
|
|
2388
3296
|
if (this.readonly) return false;
|
|
2389
3297
|
const node = this.findNode(nodeId);
|
|
2390
3298
|
if (!node || !isNodeTextEditable(node)) return false;
|
|
@@ -2396,9 +3304,10 @@ var VdFlowchart = class {
|
|
|
2396
3304
|
}
|
|
2397
3305
|
this.stopTextEdit({ commit: true });
|
|
2398
3306
|
this.select({ kind: "node", id: node.id });
|
|
3307
|
+
const replacing = typeof options.initialText === "string";
|
|
2399
3308
|
const textarea = createElement("textarea", {
|
|
2400
3309
|
className: `vd-flowchart-text-editor vd-flowchart-text-editor--${node.type}`,
|
|
2401
|
-
value: node.text,
|
|
3310
|
+
value: replacing ? options.initialText : node.text,
|
|
2402
3311
|
rows: 1
|
|
2403
3312
|
});
|
|
2404
3313
|
textarea.setAttribute("data-node-id", node.id);
|
|
@@ -2407,19 +3316,7 @@ var VdFlowchart = class {
|
|
|
2407
3316
|
textarea.addEventListener("input", () => this.positionTextEditor());
|
|
2408
3317
|
textarea.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
2409
3318
|
textarea.addEventListener("dblclick", (event) => event.stopPropagation());
|
|
2410
|
-
textarea.addEventListener("keydown", (event) =>
|
|
2411
|
-
if (event.key === "Escape") {
|
|
2412
|
-
event.preventDefault();
|
|
2413
|
-
this.stopTextEdit({ commit: false });
|
|
2414
|
-
this.canvasEl.focus();
|
|
2415
|
-
return;
|
|
2416
|
-
}
|
|
2417
|
-
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
2418
|
-
event.preventDefault();
|
|
2419
|
-
this.stopTextEdit({ commit: true });
|
|
2420
|
-
this.canvasEl.focus();
|
|
2421
|
-
}
|
|
2422
|
-
});
|
|
3319
|
+
textarea.addEventListener("keydown", (event) => this.handleTextEditorKeyDown(event, node));
|
|
2423
3320
|
textarea.addEventListener("blur", () => {
|
|
2424
3321
|
if (this.textEditor?.textarea === textarea) {
|
|
2425
3322
|
this.stopTextEdit({ commit: true });
|
|
@@ -2428,18 +3325,97 @@ var VdFlowchart = class {
|
|
|
2428
3325
|
this.textEditor = {
|
|
2429
3326
|
nodeId: node.id,
|
|
2430
3327
|
previousText: node.text,
|
|
3328
|
+
inserted: Boolean(options.inserted),
|
|
2431
3329
|
textarea
|
|
2432
3330
|
};
|
|
2433
3331
|
this.canvasEl.appendChild(textarea);
|
|
2434
3332
|
this.render({ inspector: true, json: false });
|
|
2435
|
-
|
|
2436
|
-
if (this.textEditor?.textarea
|
|
2437
|
-
|
|
3333
|
+
const focusEditor = () => {
|
|
3334
|
+
if (this.textEditor?.textarea !== textarea) return;
|
|
3335
|
+
textarea.focus();
|
|
3336
|
+
if (replacing) {
|
|
3337
|
+
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
3338
|
+
} else {
|
|
2438
3339
|
textarea.select();
|
|
2439
3340
|
}
|
|
3341
|
+
};
|
|
3342
|
+
focusEditor();
|
|
3343
|
+
window.requestAnimationFrame(() => {
|
|
3344
|
+
if (document.activeElement !== textarea) focusEditor();
|
|
3345
|
+
});
|
|
3346
|
+
return true;
|
|
3347
|
+
}
|
|
3348
|
+
// Inline label editor for a connection, placed on the label anchor
|
|
3349
|
+
// (curve midpoint). Labels render on one line, so Enter always saves.
|
|
3350
|
+
startEdgeLabelEdit(edgeId, options = {}) {
|
|
3351
|
+
if (this.readonly) return false;
|
|
3352
|
+
const edge = this.findEdge(sanitizeId(edgeId));
|
|
3353
|
+
if (!edge) return false;
|
|
3354
|
+
this.stopTextEdit({ commit: true });
|
|
3355
|
+
this.select({ kind: "edge", id: edge.id });
|
|
3356
|
+
const replacing = typeof options.initialText === "string";
|
|
3357
|
+
const textarea = createElement("textarea", {
|
|
3358
|
+
className: "vd-flowchart-text-editor vd-flowchart-text-editor--edge",
|
|
3359
|
+
value: replacing ? options.initialText : edge.label,
|
|
3360
|
+
rows: 1
|
|
3361
|
+
});
|
|
3362
|
+
textarea.setAttribute("data-edge-id", edge.id);
|
|
3363
|
+
textarea.setAttribute("aria-label", "Edit connection label");
|
|
3364
|
+
textarea.spellcheck = false;
|
|
3365
|
+
textarea.addEventListener("input", () => this.positionTextEditor());
|
|
3366
|
+
textarea.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
3367
|
+
textarea.addEventListener("dblclick", (event) => event.stopPropagation());
|
|
3368
|
+
textarea.addEventListener("keydown", (event) => {
|
|
3369
|
+
if (event.isComposing) return;
|
|
3370
|
+
if (event.key === "Escape" || event.key === "Enter") {
|
|
3371
|
+
event.preventDefault();
|
|
3372
|
+
this.stopTextEdit({ commit: event.key === "Enter" });
|
|
3373
|
+
this.canvasEl.focus();
|
|
3374
|
+
}
|
|
3375
|
+
});
|
|
3376
|
+
textarea.addEventListener("blur", () => {
|
|
3377
|
+
if (this.textEditor?.textarea === textarea) this.stopTextEdit({ commit: true });
|
|
3378
|
+
});
|
|
3379
|
+
this.textEditor = { nodeId: null, edgeId: edge.id, previousText: edge.label, textarea };
|
|
3380
|
+
this.canvasEl.appendChild(textarea);
|
|
3381
|
+
this.render({ inspector: true, json: false });
|
|
3382
|
+
const focusEditor = () => {
|
|
3383
|
+
if (this.textEditor?.textarea !== textarea) return;
|
|
3384
|
+
textarea.focus();
|
|
3385
|
+
if (replacing) textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
3386
|
+
else textarea.select();
|
|
3387
|
+
};
|
|
3388
|
+
focusEditor();
|
|
3389
|
+
window.requestAnimationFrame(() => {
|
|
3390
|
+
if (document.activeElement !== textarea) focusEditor();
|
|
2440
3391
|
});
|
|
2441
3392
|
return true;
|
|
2442
3393
|
}
|
|
3394
|
+
handleTextEditorKeyDown(event, node) {
|
|
3395
|
+
if (event.isComposing) return;
|
|
3396
|
+
const mindmap = this.keyboardShortcuts === "mindmap";
|
|
3397
|
+
if (event.key === "Escape") {
|
|
3398
|
+
event.preventDefault();
|
|
3399
|
+
this.stopTextEdit({ commit: false });
|
|
3400
|
+
this.canvasEl.focus();
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
if (event.key === "Enter") {
|
|
3404
|
+
const modKey = event.ctrlKey || event.metaKey;
|
|
3405
|
+
const commits = modKey || mindmap && node.type !== "textbox" && !event.shiftKey;
|
|
3406
|
+
if (!commits) return;
|
|
3407
|
+
event.preventDefault();
|
|
3408
|
+
this.stopTextEdit({ commit: true });
|
|
3409
|
+
this.canvasEl.focus();
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
if (event.key === "Tab" && mindmap && !event.shiftKey && !event.altKey && !event.ctrlKey) {
|
|
3413
|
+
event.preventDefault();
|
|
3414
|
+
this.stopTextEdit({ commit: true });
|
|
3415
|
+
this.canvasEl.focus();
|
|
3416
|
+
this.insertBranchNode(node.id, { edit: true });
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
2443
3419
|
stopTextEdit(options = {}) {
|
|
2444
3420
|
if (!this.textEditor) return false;
|
|
2445
3421
|
const commit = options.commit !== false;
|
|
@@ -2447,12 +3423,26 @@ var VdFlowchart = class {
|
|
|
2447
3423
|
const nextText = editor.textarea.value;
|
|
2448
3424
|
this.textEditor = null;
|
|
2449
3425
|
editor.textarea.remove();
|
|
3426
|
+
if (editor.edgeId) {
|
|
3427
|
+
const label = nextText.replace(/\s*\n\s*/g, " ").trim();
|
|
3428
|
+
if (commit && !this.readonly && label !== editor.previousText) {
|
|
3429
|
+
this.updateEdge(editor.edgeId, { label }, { inspector: true, reason: "edge:update" });
|
|
3430
|
+
} else {
|
|
3431
|
+
this.render({ scene: true, inspector: false, json: false });
|
|
3432
|
+
}
|
|
3433
|
+
return true;
|
|
3434
|
+
}
|
|
2450
3435
|
if (commit && !this.readonly && nextText !== editor.previousText) {
|
|
2451
|
-
this.
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
3436
|
+
this.coalesceInsertedText = editor.inserted;
|
|
3437
|
+
try {
|
|
3438
|
+
this.updateNode(
|
|
3439
|
+
editor.nodeId,
|
|
3440
|
+
{ text: nextText },
|
|
3441
|
+
{ inspector: true, reason: "node:update" }
|
|
3442
|
+
);
|
|
3443
|
+
} finally {
|
|
3444
|
+
this.coalesceInsertedText = false;
|
|
3445
|
+
}
|
|
2456
3446
|
} else {
|
|
2457
3447
|
this.render({ scene: true, inspector: false, json: false });
|
|
2458
3448
|
}
|
|
@@ -2460,6 +3450,10 @@ var VdFlowchart = class {
|
|
|
2460
3450
|
}
|
|
2461
3451
|
positionTextEditor() {
|
|
2462
3452
|
if (!this.textEditor) return;
|
|
3453
|
+
if (this.textEditor.edgeId) {
|
|
3454
|
+
this.positionEdgeLabelEditor();
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
2463
3457
|
const node = this.findNode(this.textEditor.nodeId);
|
|
2464
3458
|
if (!node) {
|
|
2465
3459
|
this.stopTextEdit({ commit: false });
|
|
@@ -2483,6 +3477,28 @@ var VdFlowchart = class {
|
|
|
2483
3477
|
textarea.style.lineHeight = `${formatNumber(lineHeight)}px`;
|
|
2484
3478
|
textarea.style.padding = `${formatNumber(verticalPadding)}px ${formatNumber(horizontalPadding)}px`;
|
|
2485
3479
|
}
|
|
3480
|
+
positionEdgeLabelEditor() {
|
|
3481
|
+
const edge = this.findEdge(this.textEditor.edgeId);
|
|
3482
|
+
const fromNode = edge && this.findNode(edge.from.nodeId);
|
|
3483
|
+
const toNode = edge && this.findNode(edge.to.nodeId);
|
|
3484
|
+
if (!edge || !fromNode || !toNode) {
|
|
3485
|
+
this.stopTextEdit({ commit: false });
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
|
|
3489
|
+
const viewport = this.documentData.viewport;
|
|
3490
|
+
const textarea = this.textEditor.textarea;
|
|
3491
|
+
const fontSize = Math.max(11, 13 * viewport.scale);
|
|
3492
|
+
const width = clamp(textarea.value.length * fontSize * 0.62 + 28, 96, 280);
|
|
3493
|
+
const height = fontSize * 1.4 + 12;
|
|
3494
|
+
textarea.style.left = `${formatNumber(viewport.x + labelX * viewport.scale - width / 2)}px`;
|
|
3495
|
+
textarea.style.top = `${formatNumber(viewport.y + (labelY - 8) * viewport.scale - height / 2)}px`;
|
|
3496
|
+
textarea.style.width = `${formatNumber(width)}px`;
|
|
3497
|
+
textarea.style.height = `${formatNumber(height)}px`;
|
|
3498
|
+
textarea.style.fontSize = `${formatNumber(fontSize)}px`;
|
|
3499
|
+
textarea.style.lineHeight = `${formatNumber(fontSize * 1.4)}px`;
|
|
3500
|
+
textarea.style.padding = "6px 10px";
|
|
3501
|
+
}
|
|
2486
3502
|
render(options = {}) {
|
|
2487
3503
|
if (this.destroyed) return;
|
|
2488
3504
|
const shouldRenderScene = options.scene !== false;
|
|
@@ -2495,8 +3511,10 @@ var VdFlowchart = class {
|
|
|
2495
3511
|
this.updateToolbarLabel();
|
|
2496
3512
|
this.updatePaletteState();
|
|
2497
3513
|
this.positionTextEditor();
|
|
3514
|
+
this.scheduleMinimap();
|
|
2498
3515
|
}
|
|
2499
3516
|
renderScene() {
|
|
3517
|
+
this.syncSelectedNodeIds();
|
|
2500
3518
|
const viewport = this.documentData.viewport;
|
|
2501
3519
|
this.world.setAttribute(
|
|
2502
3520
|
"transform",
|
|
@@ -2506,7 +3524,12 @@ var VdFlowchart = class {
|
|
|
2506
3524
|
clearChildren(this.previewLayer);
|
|
2507
3525
|
clearChildren(this.nodesLayer);
|
|
2508
3526
|
clearChildren(this.overlayLayer);
|
|
2509
|
-
const
|
|
3527
|
+
const hidden = this.getHiddenNodeIds();
|
|
3528
|
+
this.sceneHidden = hidden;
|
|
3529
|
+
this.sceneOutgoing = this.getOutgoingMap();
|
|
3530
|
+
const nodeMap = new Map(
|
|
3531
|
+
this.documentData.nodes.filter((node) => !hidden.has(node.id)).map((node) => [node.id, node])
|
|
3532
|
+
);
|
|
2510
3533
|
this.sceneNodeMap = nodeMap;
|
|
2511
3534
|
this.nodeElements = /* @__PURE__ */ new Map();
|
|
2512
3535
|
this.edgeElements = /* @__PURE__ */ new Map();
|
|
@@ -2526,6 +3549,7 @@ var VdFlowchart = class {
|
|
|
2526
3549
|
this.previewLayer.appendChild(this.renderPreviewEdge(this.interaction));
|
|
2527
3550
|
}
|
|
2528
3551
|
this.documentData.nodes.forEach((node) => {
|
|
3552
|
+
if (hidden.has(node.id)) return;
|
|
2529
3553
|
const element = this.renderNode(node);
|
|
2530
3554
|
this.nodesLayer.appendChild(element);
|
|
2531
3555
|
this.nodeElements.set(node.id, element);
|
|
@@ -2613,7 +3637,7 @@ var VdFlowchart = class {
|
|
|
2613
3637
|
});
|
|
2614
3638
|
group.appendChild(visiblePath);
|
|
2615
3639
|
group.appendChild(hitPath);
|
|
2616
|
-
if (edge.label) {
|
|
3640
|
+
if (edge.label && this.textEditor?.edgeId !== edge.id) {
|
|
2617
3641
|
const label = svgEl("text", {
|
|
2618
3642
|
class: "vd-flowchart-edge-label",
|
|
2619
3643
|
x: edgePath.labelX,
|
|
@@ -2674,8 +3698,9 @@ var VdFlowchart = class {
|
|
|
2674
3698
|
});
|
|
2675
3699
|
}
|
|
2676
3700
|
renderNode(node) {
|
|
2677
|
-
const selected = this.
|
|
2678
|
-
const
|
|
3701
|
+
const selected = this.isNodeSelected(node.id);
|
|
3702
|
+
const soloSelected = selected && !this.hasMultiSelection();
|
|
3703
|
+
const dragging = this.interaction?.kind === "drag-node" && this.interaction.nodeIds.includes(node.id);
|
|
2679
3704
|
const resizing = this.interaction?.kind === "resize-node" && this.interaction.nodeId === node.id;
|
|
2680
3705
|
const editing = this.textEditor?.nodeId === node.id;
|
|
2681
3706
|
const group = svgEl("g", {
|
|
@@ -2696,12 +3721,29 @@ var VdFlowchart = class {
|
|
|
2696
3721
|
group.appendChild(hitbox);
|
|
2697
3722
|
group.appendChild(this.renderNodeShape(node));
|
|
2698
3723
|
group.appendChild(this.renderNodeText(node));
|
|
2699
|
-
|
|
3724
|
+
const scale = this.documentData.viewport.scale || 1;
|
|
3725
|
+
if (soloSelected && !this.readonly && SELECTION_BOX_TYPES.has(node.type)) {
|
|
3726
|
+
const outset = SELECTION_OUTSET / scale;
|
|
3727
|
+
group.appendChild(
|
|
3728
|
+
svgEl("rect", {
|
|
3729
|
+
class: "vd-flowchart-selection-box",
|
|
3730
|
+
x: -outset,
|
|
3731
|
+
y: -outset,
|
|
3732
|
+
width: node.width + outset * 2,
|
|
3733
|
+
height: node.height + outset * 2
|
|
3734
|
+
})
|
|
3735
|
+
);
|
|
3736
|
+
}
|
|
3737
|
+
if (soloSelected && !this.readonly && isNodeResizable(node)) {
|
|
2700
3738
|
group.appendChild(this.renderResizeControls(node));
|
|
2701
3739
|
}
|
|
2702
|
-
const
|
|
3740
|
+
const collapseToggle = this.renderCollapseToggle(node, soloSelected);
|
|
3741
|
+
if (this.readonly) {
|
|
3742
|
+
if (collapseToggle) group.appendChild(collapseToggle);
|
|
3743
|
+
return group;
|
|
3744
|
+
}
|
|
2703
3745
|
FLOWCHART_PORTS.forEach((port) => {
|
|
2704
|
-
const position =
|
|
3746
|
+
const position = getPortHandlePosition({ ...node, x: 0, y: 0 }, port, scale);
|
|
2705
3747
|
const portGroup = svgEl("g", {
|
|
2706
3748
|
class: `vd-flowchart-port-group${portsVisible ? " is-visible" : ""}`,
|
|
2707
3749
|
"data-node-id": node.id,
|
|
@@ -2723,15 +3765,68 @@ var VdFlowchart = class {
|
|
|
2723
3765
|
r: CONNECTION_PORT_RADIUS / scale
|
|
2724
3766
|
})
|
|
2725
3767
|
);
|
|
3768
|
+
const arm = (CONNECTION_PORT_RADIUS - 2.5) / scale;
|
|
3769
|
+
portGroup.appendChild(
|
|
3770
|
+
svgEl("path", {
|
|
3771
|
+
class: "vd-flowchart-port-plus",
|
|
3772
|
+
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)}`
|
|
3773
|
+
})
|
|
3774
|
+
);
|
|
2726
3775
|
group.appendChild(portGroup);
|
|
2727
3776
|
});
|
|
3777
|
+
if (collapseToggle) group.appendChild(collapseToggle);
|
|
3778
|
+
return group;
|
|
3779
|
+
}
|
|
3780
|
+
// "+N" on a collapsed node, or "−" on a single selected node with children,
|
|
3781
|
+
// placed beyond the connection handle on the side its branch leaves from.
|
|
3782
|
+
renderCollapseToggle(node, soloSelected) {
|
|
3783
|
+
const outgoing = this.sceneOutgoing || this.getOutgoingMap();
|
|
3784
|
+
const childIds = (outgoing.get(node.id) || []).filter((id) => id !== node.id);
|
|
3785
|
+
if (!childIds.length) return null;
|
|
3786
|
+
const count = node.collapsed ? this.countCollapsedDescendants(
|
|
3787
|
+
node.id,
|
|
3788
|
+
this.sceneHidden || this.getHiddenNodeIds(),
|
|
3789
|
+
outgoing
|
|
3790
|
+
) : 0;
|
|
3791
|
+
if (!node.collapsed && !(soloSelected && !this.readonly)) return null;
|
|
3792
|
+
const sides = /* @__PURE__ */ new Map();
|
|
3793
|
+
this.documentData.edges.forEach((edge) => {
|
|
3794
|
+
if (edge.from.nodeId !== node.id || edge.to.nodeId === node.id) return;
|
|
3795
|
+
sides.set(edge.from.port, (sides.get(edge.from.port) || 0) + 1);
|
|
3796
|
+
});
|
|
3797
|
+
const port = [...sides].sort((a, b) => b[1] - a[1])[0]?.[0] || DIRECTION_PORTS[this.getGrowthDirection(node)];
|
|
3798
|
+
const scale = this.documentData.viewport.scale || 1;
|
|
3799
|
+
const anchor = getPortPosition({ ...node, x: 0, y: 0 }, port);
|
|
3800
|
+
const normal = getPortNormal(port);
|
|
3801
|
+
const offset = COLLAPSE_TOGGLE_OFFSET / scale;
|
|
3802
|
+
const cx = formatNumber(anchor.x + normal.x * offset);
|
|
3803
|
+
const cy = formatNumber(anchor.y + normal.y * offset);
|
|
3804
|
+
const label = node.collapsed ? `+${count}` : "\u2212";
|
|
3805
|
+
const radius = (node.collapsed && label.length > 2 ? 12 : 9) / scale;
|
|
3806
|
+
const group = svgEl("g", {
|
|
3807
|
+
class: `vd-flowchart-collapse-toggle${node.collapsed ? " is-collapsed" : ""}`,
|
|
3808
|
+
"data-collapse-toggle": node.id
|
|
3809
|
+
});
|
|
3810
|
+
const title = svgEl("title");
|
|
3811
|
+
title.textContent = node.collapsed ? `Expand ${count} hidden nodes` : "Collapse branch";
|
|
3812
|
+
group.appendChild(title);
|
|
3813
|
+
group.appendChild(svgEl("circle", { cx, cy, r: formatNumber(radius) }));
|
|
3814
|
+
const text = svgEl("text", {
|
|
3815
|
+
x: cx,
|
|
3816
|
+
y: cy,
|
|
3817
|
+
"font-size": formatNumber(10 / scale),
|
|
3818
|
+
"text-anchor": "middle",
|
|
3819
|
+
"dominant-baseline": "central"
|
|
3820
|
+
});
|
|
3821
|
+
text.textContent = label;
|
|
3822
|
+
group.appendChild(text);
|
|
2728
3823
|
return group;
|
|
2729
3824
|
}
|
|
2730
3825
|
renderResizeControls(node) {
|
|
2731
3826
|
const scale = this.documentData.viewport.scale || 1;
|
|
2732
3827
|
const zone = 14 / scale;
|
|
2733
3828
|
const cornerZone = 22 / scale;
|
|
2734
|
-
const handleSize =
|
|
3829
|
+
const handleSize = RESIZE_HANDLE_SIZE / scale;
|
|
2735
3830
|
const gap = RESIZE_PORT_GAP / scale;
|
|
2736
3831
|
const group = svgEl("g", { class: "vd-flowchart-resize-controls" });
|
|
2737
3832
|
const zones = {
|
|
@@ -2761,12 +3856,6 @@ var VdFlowchart = class {
|
|
|
2761
3856
|
};
|
|
2762
3857
|
RESIZE_HANDLES.forEach((handle) => {
|
|
2763
3858
|
const zoneRect = zones[handle];
|
|
2764
|
-
const position = getResizeHandlePosition(node, handle);
|
|
2765
|
-
const dotOffset = 8 / scale;
|
|
2766
|
-
if (handle.includes("e")) position.x += dotOffset;
|
|
2767
|
-
if (handle.includes("w")) position.x -= dotOffset;
|
|
2768
|
-
if (handle.includes("n")) position.y -= dotOffset;
|
|
2769
|
-
if (handle.includes("s")) position.y += dotOffset;
|
|
2770
3859
|
const cursor = getResizeCursor(handle);
|
|
2771
3860
|
const zoneSegments = [];
|
|
2772
3861
|
if (handle === "e" || handle === "w") {
|
|
@@ -2801,14 +3890,19 @@ var VdFlowchart = class {
|
|
|
2801
3890
|
});
|
|
2802
3891
|
group.appendChild(hit);
|
|
2803
3892
|
});
|
|
3893
|
+
if (handle.length === 1) return;
|
|
3894
|
+
const position = getResizeHandlePosition(node, handle);
|
|
3895
|
+
const outset = SELECTION_OUTSET / scale;
|
|
3896
|
+
position.x += handle.includes("e") ? outset : -outset;
|
|
3897
|
+
position.y += handle.includes("s") ? outset : -outset;
|
|
2804
3898
|
const dot = svgEl("rect", {
|
|
2805
3899
|
class: "vd-flowchart-resize-handle",
|
|
2806
3900
|
x: position.x - handleSize / 2,
|
|
2807
3901
|
y: position.y - handleSize / 2,
|
|
2808
3902
|
width: handleSize,
|
|
2809
3903
|
height: handleSize,
|
|
2810
|
-
rx: handleSize /
|
|
2811
|
-
ry: handleSize /
|
|
3904
|
+
rx: handleSize / 5,
|
|
3905
|
+
ry: handleSize / 5,
|
|
2812
3906
|
"data-node-id": node.id,
|
|
2813
3907
|
"data-resize-handle": handle,
|
|
2814
3908
|
style: `cursor: ${cursor}`
|
|
@@ -2937,6 +4031,16 @@ var VdFlowchart = class {
|
|
|
2937
4031
|
);
|
|
2938
4032
|
return;
|
|
2939
4033
|
}
|
|
4034
|
+
if (this.hasMultiSelection()) {
|
|
4035
|
+
this.selectionMeta.textContent = `${this.selectedNodeIds.size} nodes selected`;
|
|
4036
|
+
this.selectionFields.appendChild(
|
|
4037
|
+
createElement("p", {
|
|
4038
|
+
className: "vd-flowchart-selection-empty",
|
|
4039
|
+
text: "Drag, nudge with Alt+Arrow, copy, duplicate, or delete them together."
|
|
4040
|
+
})
|
|
4041
|
+
);
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
2940
4044
|
if (this.selection.kind === "node") {
|
|
2941
4045
|
const node = this.findNode(this.selection.id);
|
|
2942
4046
|
if (!node) {
|
|
@@ -3051,16 +4155,69 @@ var VdFlowchart = class {
|
|
|
3051
4155
|
}
|
|
3052
4156
|
select(selection) {
|
|
3053
4157
|
const next = selection && selection.id && selection.kind ? { kind: selection.kind, id: selection.id } : null;
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
4158
|
+
this.applySelection(next, next?.kind === "node" ? [next.id] : []);
|
|
4159
|
+
}
|
|
4160
|
+
// Single funnel for selection changes: `selection` is the primary item and
|
|
4161
|
+
// `selectedNodeIds` every selected node (the primary one included).
|
|
4162
|
+
applySelection(primary, nodeIds) {
|
|
4163
|
+
const previousKey = this.getSelectionKey();
|
|
4164
|
+
this.selection = primary;
|
|
4165
|
+
this.selectedNodeIds = new Set(nodeIds);
|
|
4166
|
+
if (primary?.kind === "node") this.selectedNodeIds.add(primary.id);
|
|
4167
|
+
if (previousKey !== this.getSelectionKey()) {
|
|
4168
|
+
this.edgeCycleAnchor = null;
|
|
3058
4169
|
this.render({ scene: true, inspector: true, json: false });
|
|
3059
|
-
this.emit("select", {
|
|
4170
|
+
this.emit("select", {
|
|
4171
|
+
selection: this.getSelectionSnapshot(),
|
|
4172
|
+
nodeIds: this.getSelectedNodeIds()
|
|
4173
|
+
});
|
|
3060
4174
|
return;
|
|
3061
4175
|
}
|
|
3062
4176
|
this.render({ scene: true, inspector: false, json: false });
|
|
3063
4177
|
}
|
|
4178
|
+
getSelectionKey() {
|
|
4179
|
+
const primary = this.selection ? `${this.selection.kind}:${this.selection.id}` : "";
|
|
4180
|
+
return `${primary}|${[...this.selectedNodeIds || []].sort().join(",")}`;
|
|
4181
|
+
}
|
|
4182
|
+
// Keep the set valid after mutations that bypass applySelection (undo,
|
|
4183
|
+
// load, removal): drop missing nodes and keep the primary node in the set.
|
|
4184
|
+
syncSelectedNodeIds() {
|
|
4185
|
+
if (this.selection?.kind !== "node") {
|
|
4186
|
+
this.selectedNodeIds = /* @__PURE__ */ new Set();
|
|
4187
|
+
return;
|
|
4188
|
+
}
|
|
4189
|
+
const existing = new Set(this.documentData.nodes.map((node) => node.id));
|
|
4190
|
+
const hidden = this.getHiddenNodeIds();
|
|
4191
|
+
this.selectedNodeIds = new Set(
|
|
4192
|
+
[...this.selectedNodeIds || []].filter((id) => existing.has(id) && !hidden.has(id))
|
|
4193
|
+
);
|
|
4194
|
+
this.selectedNodeIds.add(this.selection.id);
|
|
4195
|
+
}
|
|
4196
|
+
getSelectedNodeIds() {
|
|
4197
|
+
return [...this.selectedNodeIds || []];
|
|
4198
|
+
}
|
|
4199
|
+
isNodeSelected(nodeId) {
|
|
4200
|
+
return Boolean(this.selectedNodeIds?.has(nodeId));
|
|
4201
|
+
}
|
|
4202
|
+
hasMultiSelection() {
|
|
4203
|
+
return (this.selectedNodeIds?.size || 0) > 1;
|
|
4204
|
+
}
|
|
4205
|
+
/** Select several nodes; `primary` (default: the last id) drives single-node UI. */
|
|
4206
|
+
selectNodes(nodeIds, options = {}) {
|
|
4207
|
+
const existing = new Set(this.documentData.nodes.map((node) => node.id));
|
|
4208
|
+
const ids = [...new Set((nodeIds || []).map(sanitizeId))].filter((id) => existing.has(id));
|
|
4209
|
+
const primaryId = ids.includes(options.primary) ? options.primary : ids[ids.length - 1];
|
|
4210
|
+
this.applySelection(primaryId ? { kind: "node", id: primaryId } : null, ids);
|
|
4211
|
+
return this;
|
|
4212
|
+
}
|
|
4213
|
+
toggleNodeSelection(nodeId) {
|
|
4214
|
+
const ids = this.selection?.kind === "node" ? this.getSelectedNodeIds() : [];
|
|
4215
|
+
if (ids.includes(nodeId)) {
|
|
4216
|
+
const rest = ids.filter((id) => id !== nodeId);
|
|
4217
|
+
return this.selectNodes(rest, { primary: rest[rest.length - 1] });
|
|
4218
|
+
}
|
|
4219
|
+
return this.selectNodes([...ids, nodeId], { primary: nodeId });
|
|
4220
|
+
}
|
|
3064
4221
|
selectNode(nodeId) {
|
|
3065
4222
|
this.select({ kind: "node", id: sanitizeId(nodeId) });
|
|
3066
4223
|
return this;
|
|
@@ -3081,6 +4238,69 @@ var VdFlowchart = class {
|
|
|
3081
4238
|
findNode(nodeId) {
|
|
3082
4239
|
return this.documentData.nodes.find((node) => node.id === nodeId) || null;
|
|
3083
4240
|
}
|
|
4241
|
+
getHiddenNodeIds() {
|
|
4242
|
+
return computeHiddenNodeIds(this.documentData.nodes, this.documentData.edges);
|
|
4243
|
+
}
|
|
4244
|
+
getVisibleNodes() {
|
|
4245
|
+
const hidden = this.getHiddenNodeIds();
|
|
4246
|
+
return hidden.size ? this.documentData.nodes.filter((node) => !hidden.has(node.id)) : this.documentData.nodes;
|
|
4247
|
+
}
|
|
4248
|
+
isNodeHidden(nodeId) {
|
|
4249
|
+
return this.getHiddenNodeIds().has(nodeId);
|
|
4250
|
+
}
|
|
4251
|
+
getOutgoingMap() {
|
|
4252
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
4253
|
+
this.documentData.edges.forEach((edge) => {
|
|
4254
|
+
if (!outgoing.has(edge.from.nodeId)) outgoing.set(edge.from.nodeId, []);
|
|
4255
|
+
outgoing.get(edge.from.nodeId).push(edge.to.nodeId);
|
|
4256
|
+
});
|
|
4257
|
+
return outgoing;
|
|
4258
|
+
}
|
|
4259
|
+
// Hidden nodes under one collapsed node (its "+N" count).
|
|
4260
|
+
countCollapsedDescendants(nodeId, hidden = this.getHiddenNodeIds(), outgoing = this.getOutgoingMap()) {
|
|
4261
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4262
|
+
const queue = [nodeId];
|
|
4263
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
4264
|
+
(outgoing.get(queue[index]) || []).forEach((childId) => {
|
|
4265
|
+
if (seen.has(childId) || !hidden.has(childId)) return;
|
|
4266
|
+
seen.add(childId);
|
|
4267
|
+
queue.push(childId);
|
|
4268
|
+
});
|
|
4269
|
+
}
|
|
4270
|
+
return seen.size;
|
|
4271
|
+
}
|
|
4272
|
+
setCollapsed(nodeId, collapsed) {
|
|
4273
|
+
if (this.readonly) return false;
|
|
4274
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4275
|
+
if (!node) return false;
|
|
4276
|
+
const next = Boolean(collapsed);
|
|
4277
|
+
if (Boolean(node.collapsed) === next) return false;
|
|
4278
|
+
if (next && !this.getChildNodes(node.id).length) return false;
|
|
4279
|
+
if (next) node.collapsed = true;
|
|
4280
|
+
else delete node.collapsed;
|
|
4281
|
+
if (this.selection?.kind === "node") {
|
|
4282
|
+
const hidden = this.getHiddenNodeIds();
|
|
4283
|
+
const visibleIds = this.getSelectedNodeIds().filter((id) => !hidden.has(id));
|
|
4284
|
+
if (hidden.has(this.selection.id)) {
|
|
4285
|
+
this.select({ kind: "node", id: node.id });
|
|
4286
|
+
} else if (visibleIds.length !== this.selectedNodeIds.size) {
|
|
4287
|
+
this.selectNodes(visibleIds, { primary: this.selection.id });
|
|
4288
|
+
}
|
|
4289
|
+
} else if (this.selection?.kind === "edge") {
|
|
4290
|
+
const edge = this.findEdge(this.selection.id);
|
|
4291
|
+
const hidden = this.getHiddenNodeIds();
|
|
4292
|
+
if (edge && (hidden.has(edge.from.nodeId) || hidden.has(edge.to.nodeId))) {
|
|
4293
|
+
this.select({ kind: "node", id: node.id });
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
this.render({ inspector: true, json: true });
|
|
4297
|
+
this.emitChange("node:collapse", { node: deepClone(node) });
|
|
4298
|
+
return true;
|
|
4299
|
+
}
|
|
4300
|
+
toggleCollapsed(nodeId) {
|
|
4301
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4302
|
+
return node ? this.setCollapsed(node.id, !node.collapsed) : false;
|
|
4303
|
+
}
|
|
3084
4304
|
findEdge(edgeId) {
|
|
3085
4305
|
return this.documentData.edges.find((edge) => edge.id === edgeId) || null;
|
|
3086
4306
|
}
|
|
@@ -3091,7 +4311,8 @@ var VdFlowchart = class {
|
|
|
3091
4311
|
}
|
|
3092
4312
|
emitChange(reason, extra = {}) {
|
|
3093
4313
|
if (this.historyEnabled && !this.isApplyingHistory) {
|
|
3094
|
-
this.
|
|
4314
|
+
if (this.historyBatchDepth > 0) this.historyBatchDirty = true;
|
|
4315
|
+
else this.recordHistory(reason);
|
|
3095
4316
|
}
|
|
3096
4317
|
this.syncJsonTextarea();
|
|
3097
4318
|
this.emit("change", {
|
|
@@ -3135,7 +4356,9 @@ var VdFlowchart = class {
|
|
|
3135
4356
|
const snapshot = this.toJSON();
|
|
3136
4357
|
const targetKey = this.selection ? `${this.selection.kind}:${this.selection.id}` : "";
|
|
3137
4358
|
const top = this.history[this.historyIndex];
|
|
3138
|
-
const
|
|
4359
|
+
const atTip = Boolean(top) && this.historyIndex === this.history.length - 1;
|
|
4360
|
+
const namesInsertedNode = this.coalesceInsertedText && top?.reason === "node:insert" && reason === "node:update";
|
|
4361
|
+
const canCoalesce = atTip && top.targetKey === targetKey && (namesInsertedNode || COALESCING_REASONS.has(reason) && top.reason === reason);
|
|
3139
4362
|
if (canCoalesce) {
|
|
3140
4363
|
top.snapshot = snapshot;
|
|
3141
4364
|
return;
|
|
@@ -3251,19 +4474,180 @@ var VdFlowchart = class {
|
|
|
3251
4474
|
const parent = this.findNode(sanitizeId(parentId));
|
|
3252
4475
|
if (!parent) return null;
|
|
3253
4476
|
const { direction = "right", distance, angle, edge: edgeOptions, ...nodeOptions } = options;
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
4477
|
+
return this.batchHistory("node:add-child", () => {
|
|
4478
|
+
const node = this.addNode({
|
|
4479
|
+
...nodeOptions,
|
|
4480
|
+
relativeTo: { node: parent.id, direction, distance, angle }
|
|
4481
|
+
});
|
|
4482
|
+
const edge = this.addEdge({
|
|
4483
|
+
from: parent.id,
|
|
4484
|
+
to: node.id,
|
|
4485
|
+
autoPort: true,
|
|
4486
|
+
endMarker: "arrow",
|
|
4487
|
+
...isPlainObject(edgeOptions) ? edgeOptions : {}
|
|
4488
|
+
});
|
|
4489
|
+
this.select({ kind: "node", id: node.id });
|
|
4490
|
+
return { node, edge };
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
// Run `fn` with history recording suspended, then record a single entry for
|
|
4494
|
+
// everything it changed (change events still fire for each mutation).
|
|
4495
|
+
batchHistory(reason, fn) {
|
|
4496
|
+
this.historyBatchDepth += 1;
|
|
4497
|
+
let result;
|
|
4498
|
+
try {
|
|
4499
|
+
result = fn();
|
|
4500
|
+
} finally {
|
|
4501
|
+
this.historyBatchDepth -= 1;
|
|
4502
|
+
}
|
|
4503
|
+
if (this.historyBatchDepth === 0 && this.historyBatchDirty) {
|
|
4504
|
+
this.historyBatchDirty = false;
|
|
4505
|
+
if (this.historyEnabled && !this.isApplyingHistory) this.recordHistory(reason);
|
|
4506
|
+
}
|
|
4507
|
+
return result;
|
|
4508
|
+
}
|
|
4509
|
+
// Mind-map structure: a node's parent is the source of its first incoming
|
|
4510
|
+
// edge; its children are the targets of its outgoing edges. The side a
|
|
4511
|
+
// branch is on is the port its edge leaves the parent from, which is what
|
|
4512
|
+
// the user sees (centre geometry is ambiguous for diagonal nodes).
|
|
4513
|
+
getParentEdge(nodeId) {
|
|
4514
|
+
return this.documentData.edges.find(
|
|
4515
|
+
(edge) => edge.to.nodeId === nodeId && edge.from.nodeId !== nodeId
|
|
4516
|
+
) || null;
|
|
4517
|
+
}
|
|
4518
|
+
getParentNode(nodeId) {
|
|
4519
|
+
const edge = this.getParentEdge(nodeId);
|
|
4520
|
+
return edge ? this.findNode(edge.from.nodeId) : null;
|
|
4521
|
+
}
|
|
4522
|
+
getChildNodes(nodeId, direction = null) {
|
|
4523
|
+
const port = direction ? DIRECTION_PORTS[direction] : null;
|
|
4524
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4525
|
+
this.documentData.edges.forEach((edge) => {
|
|
4526
|
+
if (edge.from.nodeId !== nodeId || edge.to.nodeId === nodeId) return;
|
|
4527
|
+
if (!port || edge.from.port === port) ids.add(edge.to.nodeId);
|
|
3257
4528
|
});
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
4529
|
+
return this.documentData.nodes.filter((node) => ids.has(node.id));
|
|
4530
|
+
}
|
|
4531
|
+
// Direction a branch grows from `node`: the side its parent edge leaves
|
|
4532
|
+
// from, or the emptier horizontal side for a root.
|
|
4533
|
+
getGrowthDirection(node) {
|
|
4534
|
+
const parentEdge = this.getParentEdge(node.id);
|
|
4535
|
+
if (parentEdge) return PORT_DIRECTIONS[parentEdge.from.port] || "right";
|
|
4536
|
+
const right = this.getChildNodes(node.id, "right").length;
|
|
4537
|
+
const left = this.getChildNodes(node.id, "left").length;
|
|
4538
|
+
return left < right ? "left" : "right";
|
|
4539
|
+
}
|
|
4540
|
+
// Shape for a new branch node: copy a sibling when there is one, otherwise
|
|
4541
|
+
// the parent's box shape, falling back to a rounded step.
|
|
4542
|
+
getBranchTemplate(parent, sibling) {
|
|
4543
|
+
const source = sibling || (["rounded-rect", "rect", "textbox"].includes(parent.type) ? parent : null) || null;
|
|
4544
|
+
if (source) return { type: source.type, width: source.width, height: source.height };
|
|
4545
|
+
const spec = DEFAULT_NODE_SPECS["rounded-rect"];
|
|
4546
|
+
return { type: "rounded-rect", width: spec.width, height: spec.height };
|
|
4547
|
+
}
|
|
4548
|
+
/**
|
|
4549
|
+
* Add a node connected from `parentId` on the `direction` side (default: the
|
|
4550
|
+
* branch's growth direction), stacked after `anchorId` or the last sibling on
|
|
4551
|
+
* that side. The node and its edge are one undo step; the new edge copies a
|
|
4552
|
+
* sibling's (or the parent's incoming) edge style. With `edit: true` the
|
|
4553
|
+
* label editor opens with the text selected.
|
|
4554
|
+
*/
|
|
4555
|
+
insertBranchNode(parentId, options = {}) {
|
|
4556
|
+
if (this.readonly) return null;
|
|
4557
|
+
const parent = this.findNode(sanitizeId(parentId));
|
|
4558
|
+
if (!parent) return null;
|
|
4559
|
+
const direction = DIRECTION_PORTS[options.direction] ? options.direction : this.getGrowthDirection(parent);
|
|
4560
|
+
const siblings = this.getChildNodes(parent.id, direction);
|
|
4561
|
+
const anchor = options.anchorId ? this.findNode(sanitizeId(options.anchorId)) : null;
|
|
4562
|
+
const lastSibling = siblings[siblings.length - 1] || null;
|
|
4563
|
+
const template = this.getBranchTemplate(parent, anchor || lastSibling);
|
|
4564
|
+
const position = placeBranchNode({
|
|
4565
|
+
nodes: this.documentData.nodes,
|
|
4566
|
+
parent,
|
|
4567
|
+
siblings,
|
|
4568
|
+
direction,
|
|
4569
|
+
size: template,
|
|
4570
|
+
anchor,
|
|
4571
|
+
before: Boolean(options.before)
|
|
3264
4572
|
});
|
|
3265
|
-
|
|
3266
|
-
|
|
4573
|
+
const styleSource = this.documentData.edges.find(
|
|
4574
|
+
(edge) => edge.from.nodeId === parent.id && edge.to.nodeId === (anchor || lastSibling)?.id
|
|
4575
|
+
) || this.documentData.edges.find((edge) => edge.to.nodeId === parent.id);
|
|
4576
|
+
const edgeStyle = styleSource ? {
|
|
4577
|
+
route: styleSource.route,
|
|
4578
|
+
strokeWidth: styleSource.strokeWidth,
|
|
4579
|
+
startMarker: styleSource.startMarker,
|
|
4580
|
+
endMarker: styleSource.endMarker
|
|
4581
|
+
} : { endMarker: "arrow" };
|
|
4582
|
+
const result = this.batchHistory("node:insert", () => {
|
|
4583
|
+
if (parent.collapsed) delete parent.collapsed;
|
|
4584
|
+
const node = this.addNode({ ...template, x: position.x, y: position.y });
|
|
4585
|
+
const edge = this.addEdge({
|
|
4586
|
+
from: { nodeId: parent.id, port: DIRECTION_PORTS[direction] },
|
|
4587
|
+
to: { nodeId: node.id, port: DIRECTION_PORTS[oppositeDirection(direction)] },
|
|
4588
|
+
...edgeStyle
|
|
4589
|
+
});
|
|
4590
|
+
if (edge && anchor) this.placeEdgeBySibling(edge.id, parent.id, anchor.id, options.before);
|
|
4591
|
+
if (this.autoLayout && this.layoutMode !== "grid") this.layout(this.layoutMode);
|
|
4592
|
+
this.select({ kind: "node", id: node.id });
|
|
4593
|
+
return { node, edge };
|
|
4594
|
+
});
|
|
4595
|
+
this.revealNode(result.node.id);
|
|
4596
|
+
if (options.edit) this.startTextEdit(result.node.id, { inserted: true });
|
|
4597
|
+
return result;
|
|
4598
|
+
}
|
|
4599
|
+
// Tree and radial layouts order children by connection order, so a sibling's
|
|
4600
|
+
// connection goes right after (or before) its anchor's instead of last.
|
|
4601
|
+
placeEdgeBySibling(edgeId, parentId, anchorId, before = false) {
|
|
4602
|
+
const edges = this.documentData.edges;
|
|
4603
|
+
const fromIndex = edges.findIndex((edge) => edge.id === edgeId);
|
|
4604
|
+
if (fromIndex === -1) return;
|
|
4605
|
+
const [moved] = edges.splice(fromIndex, 1);
|
|
4606
|
+
const anchorIndex = edges.findIndex(
|
|
4607
|
+
(edge) => edge.from.nodeId === parentId && edge.to.nodeId === anchorId
|
|
4608
|
+
);
|
|
4609
|
+
if (anchorIndex === -1) edges.push(moved);
|
|
4610
|
+
else edges.splice(before ? anchorIndex : anchorIndex + 1, 0, moved);
|
|
4611
|
+
}
|
|
4612
|
+
// Insert a sibling after (or before) `nodeId`; a node without a parent gets a
|
|
4613
|
+
// child instead, which is what Enter on a mind-map root means.
|
|
4614
|
+
insertSiblingNode(nodeId, options = {}) {
|
|
4615
|
+
if (this.readonly) return null;
|
|
4616
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4617
|
+
if (!node) return null;
|
|
4618
|
+
const parentEdge = this.getParentEdge(node.id);
|
|
4619
|
+
const parent = parentEdge ? this.findNode(parentEdge.from.nodeId) : null;
|
|
4620
|
+
if (!parent) return this.insertBranchNode(node.id, { edit: options.edit });
|
|
4621
|
+
return this.insertBranchNode(parent.id, {
|
|
4622
|
+
direction: PORT_DIRECTIONS[parentEdge.from.port],
|
|
4623
|
+
anchorId: node.id,
|
|
4624
|
+
before: Boolean(options.before),
|
|
4625
|
+
edit: options.edit
|
|
4626
|
+
});
|
|
4627
|
+
}
|
|
4628
|
+
// Pan just enough to bring a node fully into view (with a small margin).
|
|
4629
|
+
revealNode(nodeId) {
|
|
4630
|
+
const node = this.findNode(nodeId);
|
|
4631
|
+
const width = this.canvasEl.clientWidth;
|
|
4632
|
+
const height = this.canvasEl.clientHeight;
|
|
4633
|
+
if (!node || !width || !height) return;
|
|
4634
|
+
const viewport = this.documentData.viewport;
|
|
4635
|
+
const margin = 32;
|
|
4636
|
+
const left = viewport.x + node.x * viewport.scale;
|
|
4637
|
+
const top = viewport.y + node.y * viewport.scale;
|
|
4638
|
+
const right = left + node.width * viewport.scale;
|
|
4639
|
+
const bottom = top + node.height * viewport.scale;
|
|
4640
|
+
let dx = 0;
|
|
4641
|
+
let dy = 0;
|
|
4642
|
+
if (right > width - margin) dx = width - margin - right;
|
|
4643
|
+
if (left + dx < margin) dx = margin - left;
|
|
4644
|
+
if (bottom > height - margin) dy = height - margin - bottom;
|
|
4645
|
+
if (top + dy < margin) dy = margin - top;
|
|
4646
|
+
if (!dx && !dy) return;
|
|
4647
|
+
viewport.x = formatNumber(viewport.x + dx);
|
|
4648
|
+
viewport.y = formatNumber(viewport.y + dy);
|
|
4649
|
+
this.render({ inspector: false, json: true });
|
|
4650
|
+
this.emitViewportChange("viewport:pan");
|
|
3267
4651
|
}
|
|
3268
4652
|
// Arrange nodes with a built-in layout. Positions are computed by the pure
|
|
3269
4653
|
// computeLayout() module, then applied in place (NOT via load(), which would
|
|
@@ -3511,6 +4895,9 @@ var VdFlowchart = class {
|
|
|
3511
4895
|
}
|
|
3512
4896
|
deleteSelection() {
|
|
3513
4897
|
if (!this.selection || this.readonly) return false;
|
|
4898
|
+
if (this.hasMultiSelection()) {
|
|
4899
|
+
return this.removeNodeIds(new Set(this.getSelectedNodeIds()), "node:remove");
|
|
4900
|
+
}
|
|
3514
4901
|
if (this.selection.kind === "node") return this.removeNode(this.selection.id);
|
|
3515
4902
|
return this.removeEdge(this.selection.id);
|
|
3516
4903
|
}
|
|
@@ -3539,10 +4926,11 @@ var VdFlowchart = class {
|
|
|
3539
4926
|
return this;
|
|
3540
4927
|
}
|
|
3541
4928
|
fitView() {
|
|
3542
|
-
|
|
4929
|
+
const nodes = this.getVisibleNodes();
|
|
4930
|
+
if (!nodes.length) {
|
|
3543
4931
|
return this.resetView();
|
|
3544
4932
|
}
|
|
3545
|
-
const bounds = getBounds(
|
|
4933
|
+
const bounds = getBounds(nodes);
|
|
3546
4934
|
const width = this.canvasEl.clientWidth || 800;
|
|
3547
4935
|
const height = this.canvasEl.clientHeight || 560;
|
|
3548
4936
|
const padding = 80;
|
|
@@ -3600,6 +4988,14 @@ var VdFlowchart = class {
|
|
|
3600
4988
|
pattern.querySelector("path").setAttribute("d", `M ${this.gridSize} 0 L 0 0 0 ${this.gridSize}`);
|
|
3601
4989
|
}
|
|
3602
4990
|
if ("autoFit" in options) this.autoFit = Boolean(options.autoFit);
|
|
4991
|
+
if ("keyboardShortcuts" in options) {
|
|
4992
|
+
this.keyboardShortcuts = normalizeKeyboardShortcuts(options.keyboardShortcuts);
|
|
4993
|
+
}
|
|
4994
|
+
if ("snapGuides" in options) this.snapGuides = options.snapGuides !== false;
|
|
4995
|
+
if ("minimap" in options) this.setMinimapEnabled(options.minimap !== false);
|
|
4996
|
+
if ("autoLayout" in options) this.autoLayout = Boolean(options.autoLayout);
|
|
4997
|
+
this.syncCanvasLabel();
|
|
4998
|
+
if (this.shortcutsHelpOpen) this.renderShortcutsHelp();
|
|
3603
4999
|
if ("history" in options && options.history !== false !== this.historyEnabled) {
|
|
3604
5000
|
this.historyEnabled = options.history !== false;
|
|
3605
5001
|
this.history = [];
|
|
@@ -3648,6 +5044,10 @@ var VdFlowchart = class {
|
|
|
3648
5044
|
destroy() {
|
|
3649
5045
|
if (this.destroyed) return;
|
|
3650
5046
|
this.destroyed = true;
|
|
5047
|
+
if (this.minimapFrame && typeof cancelAnimationFrame === "function") {
|
|
5048
|
+
cancelAnimationFrame(this.minimapFrame);
|
|
5049
|
+
}
|
|
5050
|
+
this.minimapFrame = 0;
|
|
3651
5051
|
this.activeTool = null;
|
|
3652
5052
|
this.stopTextEdit({ commit: false });
|
|
3653
5053
|
this.resizeObserver?.disconnect();
|
|
@@ -3664,6 +5064,17 @@ var VdFlowchart = class {
|
|
|
3664
5064
|
|
|
3665
5065
|
// src/vue.js
|
|
3666
5066
|
var FORWARDED_EVENTS = ["change", "select", "viewport", "connect", "ready"];
|
|
5067
|
+
var EDITOR_OPTION_KEYS = [
|
|
5068
|
+
"readonly",
|
|
5069
|
+
"gridSize",
|
|
5070
|
+
"autoFit",
|
|
5071
|
+
"history",
|
|
5072
|
+
"historyLimit",
|
|
5073
|
+
"keyboardShortcuts",
|
|
5074
|
+
"snapGuides",
|
|
5075
|
+
"minimap",
|
|
5076
|
+
"autoLayout"
|
|
5077
|
+
];
|
|
3667
5078
|
var VdFlowchart2 = (0, import_vue.defineComponent)({
|
|
3668
5079
|
name: "VdFlowchart",
|
|
3669
5080
|
props: {
|
|
@@ -3680,21 +5091,26 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
|
|
|
3680
5091
|
/** Enable the built-in undo/redo history (default true). */
|
|
3681
5092
|
history: { type: Boolean, default: true },
|
|
3682
5093
|
/** Maximum number of history entries to retain. */
|
|
3683
|
-
historyLimit: { type: Number, default: void 0 }
|
|
5094
|
+
historyLimit: { type: Number, default: void 0 },
|
|
5095
|
+
/** Keyboard model: 'mindmap' (Tab child, Enter sibling) or 'basic'. */
|
|
5096
|
+
keyboardShortcuts: { type: String, default: "mindmap" },
|
|
5097
|
+
/** Snap dragged nodes to other nodes' edges and centres (default true). */
|
|
5098
|
+
snapGuides: { type: Boolean, default: true },
|
|
5099
|
+
/** Show the overview minimap in the canvas corner (default true). */
|
|
5100
|
+
minimap: { type: Boolean, default: true },
|
|
5101
|
+
/** Re-run the current layout after keyboard or handle insertions (default false). */
|
|
5102
|
+
autoLayout: { type: Boolean, default: false }
|
|
3684
5103
|
},
|
|
3685
5104
|
emits: ["change", "select", "viewport", "connect", "ready"],
|
|
3686
5105
|
setup(props, { emit, expose }) {
|
|
3687
5106
|
const el = (0, import_vue.ref)(null);
|
|
3688
5107
|
let instance = null;
|
|
5108
|
+
const editorOptions = () => Object.fromEntries(EDITOR_OPTION_KEYS.map((key) => [key, props[key]]));
|
|
3689
5109
|
const create = () => {
|
|
3690
5110
|
instance = new VdFlowchart({
|
|
3691
5111
|
element: el.value,
|
|
3692
5112
|
data: props.data,
|
|
3693
|
-
|
|
3694
|
-
gridSize: props.gridSize,
|
|
3695
|
-
autoFit: props.autoFit,
|
|
3696
|
-
history: props.history,
|
|
3697
|
-
historyLimit: props.historyLimit
|
|
5113
|
+
...editorOptions()
|
|
3698
5114
|
});
|
|
3699
5115
|
FORWARDED_EVENTS.forEach((name) => {
|
|
3700
5116
|
instance.on(name, (payload) => emit(name, payload));
|
|
@@ -3714,16 +5130,9 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
|
|
|
3714
5130
|
{ deep: true }
|
|
3715
5131
|
);
|
|
3716
5132
|
(0, import_vue.watch)(
|
|
3717
|
-
() =>
|
|
5133
|
+
() => EDITOR_OPTION_KEYS.map((key) => props[key]),
|
|
3718
5134
|
() => {
|
|
3719
|
-
if (
|
|
3720
|
-
instance.updateOptions({
|
|
3721
|
-
readonly: props.readonly,
|
|
3722
|
-
gridSize: props.gridSize,
|
|
3723
|
-
autoFit: props.autoFit,
|
|
3724
|
-
history: props.history,
|
|
3725
|
-
historyLimit: props.historyLimit
|
|
3726
|
-
});
|
|
5135
|
+
if (instance) instance.updateOptions(editorOptions());
|
|
3727
5136
|
}
|
|
3728
5137
|
);
|
|
3729
5138
|
(0, import_vue.onBeforeUnmount)(() => {
|