@vanduo-oss/vd3-flowchart 1.3.0 → 1.5.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 +93 -0
- package/README.md +75 -8
- package/SKILL.md +32 -3
- package/dist/core.d.ts +105 -2
- package/dist/index.cjs +1937 -265
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1937 -265
- 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 +18 -0
- package/package.json +5 -2
- package/recipes/editor.vue +4 -2
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 = [
|
|
@@ -268,11 +398,12 @@ function createToolbarButton({ action, label, icon, disabled = false }) {
|
|
|
268
398
|
return button;
|
|
269
399
|
}
|
|
270
400
|
var DEFAULT_GRID_SIZE = 24;
|
|
271
|
-
var
|
|
272
|
-
var
|
|
273
|
-
var
|
|
274
|
-
var
|
|
275
|
-
var
|
|
401
|
+
var DEFAULT_MIN_SCALE = 0.35;
|
|
402
|
+
var DEFAULT_MAX_SCALE = 3;
|
|
403
|
+
var DEFAULT_MIN_NODE_SIZE = 56;
|
|
404
|
+
var DEFAULT_MAX_NODE_SIZE = 420;
|
|
405
|
+
var DEFAULT_WORLD_EXTENT = 12e3;
|
|
406
|
+
var WIDE_WORLD_EXTENT = 24e4;
|
|
276
407
|
var MAX_NODES = 1e4;
|
|
277
408
|
var MAX_EDGES = 1e4;
|
|
278
409
|
var RESIZE_HANDLES = ["n", "ne", "e", "se", "s", "sw", "w", "nw"];
|
|
@@ -280,21 +411,32 @@ var DEFAULT_EDGE_STROKE_WIDTH = 2.25;
|
|
|
280
411
|
var MIN_EDGE_STROKE_WIDTH = 1.25;
|
|
281
412
|
var MAX_EDGE_STROKE_WIDTH = 6;
|
|
282
413
|
var CONNECTION_PORT_RADIUS = 6;
|
|
414
|
+
var PORT_HANDLE_OFFSET = 12;
|
|
283
415
|
var CONNECTION_PORT_HIT_RADIUS = 14;
|
|
416
|
+
var PORT_CLICK_SLOP = 4;
|
|
417
|
+
var RESIZE_HANDLE_SIZE = 9;
|
|
418
|
+
var SELECTION_OUTSET = 4;
|
|
419
|
+
var GUIDE_SNAP_DISTANCE = 6;
|
|
420
|
+
var GUIDE_OVERHANG = 12;
|
|
421
|
+
var COLLAPSE_TOGGLE_OFFSET = 36;
|
|
422
|
+
var MINIMAP_WIDTH = 180;
|
|
423
|
+
var MINIMAP_HEIGHT = 120;
|
|
424
|
+
var MINIMAP_MIN_CANVAS_WIDTH = 480;
|
|
425
|
+
var SELECTION_BOX_TYPES = /* @__PURE__ */ new Set(["circle", "diamond", "label"]);
|
|
284
426
|
var RECONNECT_ENDPOINT_RADIUS = 7;
|
|
285
427
|
var RECONNECT_ENDPOINT_HIT_RADIUS = 12;
|
|
286
428
|
var EDGE_HIT_STROKE_MIN = 16;
|
|
287
429
|
var CONNECTION_SNAP_PADDING = 36;
|
|
288
430
|
var CONNECTION_HYSTERESIS = 16;
|
|
289
431
|
var CONNECTION_CENTER_LOCK_RADIUS = 18;
|
|
290
|
-
var RESIZE_PORT_GAP =
|
|
432
|
+
var RESIZE_PORT_GAP = 28;
|
|
291
433
|
var EDGE_STROKE_PRESETS = [
|
|
292
434
|
{ id: "thin", label: "Thin", width: 1.75 },
|
|
293
435
|
{ id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
|
|
294
436
|
{ id: "bold", label: "Bold", width: 3.5 }
|
|
295
437
|
];
|
|
296
|
-
var VD_FLOWCHART_VERSION = "1.
|
|
297
|
-
var FLOWCHART_DOCUMENT_VERSION = "1.
|
|
438
|
+
var VD_FLOWCHART_VERSION = "1.5.0";
|
|
439
|
+
var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
|
|
298
440
|
var FLOWCHART_NODE_TYPES = [
|
|
299
441
|
"rounded-rect",
|
|
300
442
|
"rect",
|
|
@@ -307,7 +449,65 @@ var FLOWCHART_NODE_TYPES = [
|
|
|
307
449
|
var FLOWCHART_PORTS = ["top", "right", "bottom", "left"];
|
|
308
450
|
var FLOWCHART_EDGE_MARKERS = ["none", "arrow", "dot"];
|
|
309
451
|
var FLOWCHART_EDGE_ROUTES = ["curve", "straight", "orthogonal"];
|
|
310
|
-
var
|
|
452
|
+
var FLOWCHART_KEYBOARD_SHORTCUTS = ["mindmap", "basic"];
|
|
453
|
+
function normalizeKeyboardShortcuts(value) {
|
|
454
|
+
return value === "basic" ? "basic" : "mindmap";
|
|
455
|
+
}
|
|
456
|
+
var ARROW_DIRECTIONS = {
|
|
457
|
+
ArrowRight: "right",
|
|
458
|
+
ArrowLeft: "left",
|
|
459
|
+
ArrowDown: "down",
|
|
460
|
+
ArrowUp: "up"
|
|
461
|
+
};
|
|
462
|
+
function getShortcutRows(mode, readonly) {
|
|
463
|
+
const navigation = [
|
|
464
|
+
["Arrow keys", "Select the nearest node in that direction"],
|
|
465
|
+
["Shift + Arrow", "Select a connection on that side (repeat to cycle)"],
|
|
466
|
+
["Shift + click, Shift + drag", "Add nodes to the selection, or select with a box"],
|
|
467
|
+
["Cmd/Ctrl + A", "Select all nodes"]
|
|
468
|
+
];
|
|
469
|
+
const view = [
|
|
470
|
+
["Cmd/Ctrl + = or -", "Zoom in or out"],
|
|
471
|
+
["Cmd/Ctrl + 0", "Zoom to 100%"],
|
|
472
|
+
["Shift + 1", "Fit the diagram to the view"]
|
|
473
|
+
];
|
|
474
|
+
const help = [["?", "Show or hide this list"]];
|
|
475
|
+
if (readonly) {
|
|
476
|
+
return [...navigation, ...view, ...help, ["Tab", "Leave the canvas"]];
|
|
477
|
+
}
|
|
478
|
+
const mindmap = mode === "mindmap" ? [
|
|
479
|
+
["Tab", "Add a child node"],
|
|
480
|
+
["Enter", "Add a sibling below (Shift + Enter: above)"],
|
|
481
|
+
["F2, Space, or type", "Edit the label"],
|
|
482
|
+
["Enter while editing", "Save the label (Shift + Enter: new line)"],
|
|
483
|
+
["Tab while editing", "Save and add a child"]
|
|
484
|
+
] : [
|
|
485
|
+
["Enter or F2", "Edit the label"],
|
|
486
|
+
["Cmd/Ctrl + Enter", "Save the label"]
|
|
487
|
+
];
|
|
488
|
+
return [
|
|
489
|
+
...navigation,
|
|
490
|
+
...mindmap,
|
|
491
|
+
[
|
|
492
|
+
mode === "mindmap" ? "Enter, F2, or type on a connection" : "Enter or F2 on a connection",
|
|
493
|
+
"Edit the connection label"
|
|
494
|
+
],
|
|
495
|
+
["Cmd/Ctrl + /", "Collapse or expand the branch"],
|
|
496
|
+
["Alt + Arrow", "Nudge the node (add Shift for 1 px)"],
|
|
497
|
+
["Delete or Backspace", "Delete the selection"],
|
|
498
|
+
["Cmd/Ctrl + D", "Duplicate the node"],
|
|
499
|
+
["Cmd/Ctrl + Z, Shift + Cmd/Ctrl + Z", "Undo, redo"],
|
|
500
|
+
["Cmd/Ctrl + C, X, V", "Copy, cut, paste"],
|
|
501
|
+
...view,
|
|
502
|
+
["Esc", "Cancel editing, then the tool, then deselect"],
|
|
503
|
+
[
|
|
504
|
+
mode === "mindmap" ? "Shift + Tab, or Esc then Tab" : "Tab or Shift + Tab",
|
|
505
|
+
"Leave the canvas"
|
|
506
|
+
],
|
|
507
|
+
...help
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
var COALESCING_REASONS = /* @__PURE__ */ new Set(["node:update", "edge:update", "node:nudge"]);
|
|
311
511
|
var DEFAULT_EDGE_ROUTE = "curve";
|
|
312
512
|
var ORTHOGONAL_STUB_LENGTH = 32;
|
|
313
513
|
var ORTHOGONAL_CORNER_RADIUS = 12;
|
|
@@ -376,6 +576,17 @@ function deepClone(value) {
|
|
|
376
576
|
function formatNumber(value) {
|
|
377
577
|
return Number(value.toFixed(2));
|
|
378
578
|
}
|
|
579
|
+
function formatScale(value) {
|
|
580
|
+
return Number(toFiniteNumber(value, 1).toFixed(4));
|
|
581
|
+
}
|
|
582
|
+
function normalizeBoundPair(min, max, fallbackMin, fallbackMax) {
|
|
583
|
+
const low = Number(min);
|
|
584
|
+
const high = Number(max);
|
|
585
|
+
if (!Number.isFinite(low) || !Number.isFinite(high) || low <= 0 || high <= 0 || low >= high) {
|
|
586
|
+
return { min: fallbackMin, max: fallbackMax };
|
|
587
|
+
}
|
|
588
|
+
return { min: low, max: high };
|
|
589
|
+
}
|
|
379
590
|
function sanitizeId(value) {
|
|
380
591
|
if (value == null) return "";
|
|
381
592
|
return String(value).trim();
|
|
@@ -433,21 +644,36 @@ function normalizeNodeType(type) {
|
|
|
433
644
|
function getNodeSpec(type) {
|
|
434
645
|
return DEFAULT_NODE_SPECS[normalizeNodeType(type)];
|
|
435
646
|
}
|
|
436
|
-
function
|
|
647
|
+
function nodeVisualScale(node) {
|
|
648
|
+
if (!node || node.type === "junction") return 1;
|
|
649
|
+
const spec = getNodeSpec(node.type);
|
|
650
|
+
const width = spec.width > 0 ? toFiniteNumber(node.width, spec.width) / spec.width : 1;
|
|
651
|
+
const height = spec.height > 0 ? toFiniteNumber(node.height, spec.height) / spec.height : 1;
|
|
652
|
+
return clamp(Math.min(1, width, height), 0.01, 1);
|
|
653
|
+
}
|
|
654
|
+
function edgeVisualScale(fromNode, toNode) {
|
|
655
|
+
if (fromNode && toNode) return Math.min(nodeVisualScale(fromNode), nodeVisualScale(toNode));
|
|
656
|
+
if (fromNode) return nodeVisualScale(fromNode);
|
|
657
|
+
if (toNode) return nodeVisualScale(toNode);
|
|
658
|
+
return 1;
|
|
659
|
+
}
|
|
660
|
+
function getNodeSizeBounds(type, sizeBounds) {
|
|
437
661
|
const spec = getNodeSpec(type);
|
|
662
|
+
const min = sizeBounds?.min ?? DEFAULT_MIN_NODE_SIZE;
|
|
663
|
+
const max = sizeBounds?.max ?? DEFAULT_MAX_NODE_SIZE;
|
|
438
664
|
return {
|
|
439
|
-
minWidth: spec.minWidth ??
|
|
440
|
-
minHeight: spec.minHeight ??
|
|
441
|
-
maxWidth: spec.maxWidth ??
|
|
442
|
-
maxHeight: spec.maxHeight ??
|
|
665
|
+
minWidth: spec.minWidth ?? min,
|
|
666
|
+
minHeight: spec.minHeight ?? min,
|
|
667
|
+
maxWidth: spec.maxWidth ?? max,
|
|
668
|
+
maxHeight: spec.maxHeight ?? max
|
|
443
669
|
};
|
|
444
670
|
}
|
|
445
|
-
function clampNodeWidth(type, width, fallback) {
|
|
446
|
-
const bounds = getNodeSizeBounds(type);
|
|
671
|
+
function clampNodeWidth(type, width, fallback, sizeBounds) {
|
|
672
|
+
const bounds = getNodeSizeBounds(type, sizeBounds);
|
|
447
673
|
return clamp(toFiniteNumber(width, fallback), bounds.minWidth, bounds.maxWidth);
|
|
448
674
|
}
|
|
449
|
-
function clampNodeHeight(type, height, fallback) {
|
|
450
|
-
const bounds = getNodeSizeBounds(type);
|
|
675
|
+
function clampNodeHeight(type, height, fallback, sizeBounds) {
|
|
676
|
+
const bounds = getNodeSizeBounds(type, sizeBounds);
|
|
451
677
|
return clamp(toFiniteNumber(height, fallback), bounds.minHeight, bounds.maxHeight);
|
|
452
678
|
}
|
|
453
679
|
function isNodeResizable(nodeOrType) {
|
|
@@ -471,26 +697,59 @@ function getStrokePresetWidth(presetId) {
|
|
|
471
697
|
const preset = EDGE_STROKE_PRESETS.find((item) => item.id === presetId);
|
|
472
698
|
return preset?.width ?? DEFAULT_EDGE_STROKE_WIDTH;
|
|
473
699
|
}
|
|
474
|
-
function normalizeViewport(viewport) {
|
|
700
|
+
function normalizeViewport(viewport, scaleBounds) {
|
|
701
|
+
const min = scaleBounds?.min ?? DEFAULT_MIN_SCALE;
|
|
702
|
+
const max = scaleBounds?.max ?? DEFAULT_MAX_SCALE;
|
|
475
703
|
return {
|
|
476
704
|
x: toFiniteNumber(viewport?.x, 0),
|
|
477
705
|
y: toFiniteNumber(viewport?.y, 0),
|
|
478
|
-
scale: clamp(toFiniteNumber(viewport?.scale, 1),
|
|
706
|
+
scale: clamp(formatScale(toFiniteNumber(viewport?.scale, 1)), min, max)
|
|
479
707
|
};
|
|
480
708
|
}
|
|
481
|
-
function normalizeNode(rawNode, index, usedIds) {
|
|
709
|
+
function normalizeNode(rawNode, index, usedIds, sizeBounds) {
|
|
482
710
|
const type = normalizeNodeType(rawNode?.type);
|
|
483
711
|
const spec = getNodeSpec(type);
|
|
484
|
-
|
|
712
|
+
const node = {
|
|
485
713
|
id: ensureUniqueId(rawNode?.id, "node", usedIds),
|
|
486
714
|
type,
|
|
487
715
|
x: toFiniteNumber(rawNode?.x, index * 28),
|
|
488
716
|
y: toFiniteNumber(rawNode?.y, index * 18),
|
|
489
|
-
width: clampNodeWidth(type, rawNode?.width, spec.width),
|
|
490
|
-
height: clampNodeHeight(type, rawNode?.height, spec.height),
|
|
717
|
+
width: clampNodeWidth(type, rawNode?.width, spec.width, sizeBounds),
|
|
718
|
+
height: clampNodeHeight(type, rawNode?.height, spec.height, sizeBounds),
|
|
491
719
|
text: rawNode?.text == null ? spec.text : String(rawNode.text),
|
|
492
720
|
data: isPlainObject(rawNode?.data) ? deepClone(rawNode.data) : {}
|
|
493
721
|
};
|
|
722
|
+
if (rawNode?.collapsed === true) node.collapsed = true;
|
|
723
|
+
return node;
|
|
724
|
+
}
|
|
725
|
+
function computeHiddenNodeIds(nodes, edges) {
|
|
726
|
+
const collapsed = new Set(nodes.filter((node) => node.collapsed).map((node) => node.id));
|
|
727
|
+
if (!collapsed.size) return /* @__PURE__ */ new Set();
|
|
728
|
+
const outgoing = new Map(nodes.map((node) => [node.id, []]));
|
|
729
|
+
edges.forEach((edge) => {
|
|
730
|
+
if (edge.from.nodeId === edge.to.nodeId) return;
|
|
731
|
+
outgoing.get(edge.from.nodeId)?.push(edge.to.nodeId);
|
|
732
|
+
});
|
|
733
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
734
|
+
collapsed.forEach((rootId) => {
|
|
735
|
+
const queue2 = [...outgoing.get(rootId) || []];
|
|
736
|
+
for (let index = 0; index < queue2.length; index += 1) {
|
|
737
|
+
const id = queue2[index];
|
|
738
|
+
if (id === rootId || candidates.has(id)) continue;
|
|
739
|
+
candidates.add(id);
|
|
740
|
+
queue2.push(...outgoing.get(id) || []);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
const queue = nodes.filter((node) => !candidates.has(node.id) && !collapsed.has(node.id)).map((node) => node.id);
|
|
744
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
745
|
+
const id = queue[index];
|
|
746
|
+
(outgoing.get(id) || []).forEach((childId) => {
|
|
747
|
+
if (!candidates.has(childId)) return;
|
|
748
|
+
candidates.delete(childId);
|
|
749
|
+
if (!collapsed.has(childId)) queue.push(childId);
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return candidates;
|
|
494
753
|
}
|
|
495
754
|
function normalizeEndpoint(rawEndpoint, fallbackPort) {
|
|
496
755
|
return {
|
|
@@ -549,7 +808,7 @@ function isSupportedDocumentVersion(value) {
|
|
|
549
808
|
if (parsed.minor === max.minor && parsed.patch > max.patch) return false;
|
|
550
809
|
return true;
|
|
551
810
|
}
|
|
552
|
-
function normalizeDocument(input) {
|
|
811
|
+
function normalizeDocument(input, limits = {}) {
|
|
553
812
|
let source = input;
|
|
554
813
|
if (typeof source === "string") {
|
|
555
814
|
try {
|
|
@@ -572,13 +831,13 @@ function normalizeDocument(input) {
|
|
|
572
831
|
}
|
|
573
832
|
}
|
|
574
833
|
const usedNodeIds = /* @__PURE__ */ new Set();
|
|
575
|
-
const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds));
|
|
834
|
+
const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds, limits.sizeBounds));
|
|
576
835
|
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
577
836
|
const usedEdgeIds = /* @__PURE__ */ new Set();
|
|
578
837
|
const edges = (Array.isArray(source.edges) ? source.edges : []).slice(0, MAX_EDGES).map((edge, index) => normalizeEdge(edge, index, nodeIds, usedEdgeIds)).filter(Boolean);
|
|
579
838
|
return {
|
|
580
839
|
version: FLOWCHART_DOCUMENT_VERSION,
|
|
581
|
-
viewport: normalizeViewport(source.viewport),
|
|
840
|
+
viewport: normalizeViewport(source.viewport, limits.scaleBounds),
|
|
582
841
|
nodes,
|
|
583
842
|
edges
|
|
584
843
|
};
|
|
@@ -643,6 +902,15 @@ function getPortPosition(node, port) {
|
|
|
643
902
|
return { x: node.x, y: node.y + node.height / 2 };
|
|
644
903
|
}
|
|
645
904
|
}
|
|
905
|
+
function getPortHandlePosition(node, port, scale = 1) {
|
|
906
|
+
const point = getPortPosition(node, port);
|
|
907
|
+
const normal = getPortNormal(port);
|
|
908
|
+
const offset = PORT_HANDLE_OFFSET / scale;
|
|
909
|
+
return {
|
|
910
|
+
x: formatNumber(point.x + normal.x * offset),
|
|
911
|
+
y: formatNumber(point.y + normal.y * offset)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
646
914
|
function getNearestPort(node, point) {
|
|
647
915
|
return FLOWCHART_PORTS.reduce((best, port) => {
|
|
648
916
|
const portPoint = getPortPosition(node, port);
|
|
@@ -786,19 +1054,30 @@ function getPolylineLabelPoint(points) {
|
|
|
786
1054
|
const last = points[points.length - 1] || { x: 0, y: 0 };
|
|
787
1055
|
return { x: formatNumber(last.x), y: formatNumber(last.y) };
|
|
788
1056
|
}
|
|
789
|
-
function
|
|
1057
|
+
function routeMetrics(edgeScale = 1) {
|
|
1058
|
+
const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
|
|
1059
|
+
return {
|
|
1060
|
+
scale,
|
|
1061
|
+
minCurveArm: MIN_CURVE_ARM * scale,
|
|
1062
|
+
maxCurveArm: MAX_CURVE_ARM * scale,
|
|
1063
|
+
stub: ORTHOGONAL_STUB_LENGTH * scale,
|
|
1064
|
+
corner: ORTHOGONAL_CORNER_RADIUS * scale,
|
|
1065
|
+
clearance: ORTHOGONAL_CLEARANCE * scale
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function curveControlPoint(point, port, target, curvature, metrics = routeMetrics(1)) {
|
|
790
1069
|
const normal = getPortNormal(port);
|
|
791
1070
|
const dx = target.x - point.x;
|
|
792
1071
|
const dy = target.y - point.y;
|
|
793
1072
|
const distance = Math.hypot(dx, dy) || 1;
|
|
794
1073
|
const align = (normal.x * dx + normal.y * dy) / distance;
|
|
795
1074
|
const ease = 0.55 + 0.45 * align;
|
|
796
|
-
const arm = clamp(distance * curvature * ease,
|
|
1075
|
+
const arm = clamp(distance * curvature * ease, metrics.minCurveArm, metrics.maxCurveArm);
|
|
797
1076
|
return { x: point.x + normal.x * arm, y: point.y + normal.y * arm };
|
|
798
1077
|
}
|
|
799
|
-
function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left") {
|
|
800
|
-
const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE);
|
|
801
|
-
const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE);
|
|
1078
|
+
function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left", metrics = routeMetrics(1)) {
|
|
1079
|
+
const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE, metrics);
|
|
1080
|
+
const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE, metrics);
|
|
802
1081
|
const labelX = 0.125 * fromPoint.x + 0.375 * controlA.x + 0.375 * controlB.x + 0.125 * toPoint.x;
|
|
803
1082
|
const labelY = 0.125 * fromPoint.y + 0.375 * controlA.y + 0.375 * controlB.y + 0.125 * toPoint.y;
|
|
804
1083
|
return {
|
|
@@ -860,11 +1139,11 @@ function getOrthogonalMidpoints(fromStub, toStub, fromPort, toPort) {
|
|
|
860
1139
|
}
|
|
861
1140
|
return [{ x: fromStub.x, y: toStub.y }];
|
|
862
1141
|
}
|
|
863
|
-
function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect) {
|
|
1142
|
+
function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics = routeMetrics(1)) {
|
|
864
1143
|
const fromNormal = getPortNormal(fromPort);
|
|
865
1144
|
const toNormal = getPortNormal(toPort);
|
|
866
|
-
const fromStub = offsetPoint(fromPoint, fromNormal,
|
|
867
|
-
const toStub = offsetPoint(toPoint, toNormal,
|
|
1145
|
+
const fromStub = offsetPoint(fromPoint, fromNormal, metrics.stub);
|
|
1146
|
+
const toStub = offsetPoint(toPoint, toNormal, metrics.stub);
|
|
868
1147
|
const sourceHorizontal = isHorizontalPort(fromPort);
|
|
869
1148
|
const targetHorizontal = isHorizontalPort(toPort);
|
|
870
1149
|
const mids = [];
|
|
@@ -904,13 +1183,13 @@ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toR
|
|
|
904
1183
|
);
|
|
905
1184
|
return points;
|
|
906
1185
|
}
|
|
907
|
-
function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null) {
|
|
1186
|
+
function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null, metrics = routeMetrics(1)) {
|
|
908
1187
|
let points;
|
|
909
1188
|
if (fromRect && toRect) {
|
|
910
|
-
points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
|
|
1189
|
+
points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
|
|
911
1190
|
} else {
|
|
912
|
-
const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort),
|
|
913
|
-
const toStub = offsetPoint(toPoint, getPortNormal(toPort),
|
|
1191
|
+
const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), metrics.stub);
|
|
1192
|
+
const toStub = offsetPoint(toPoint, getPortNormal(toPort), metrics.stub);
|
|
914
1193
|
points = [];
|
|
915
1194
|
[
|
|
916
1195
|
fromPoint,
|
|
@@ -922,7 +1201,7 @@ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "l
|
|
|
922
1201
|
}
|
|
923
1202
|
const label = getPolylineLabelPoint(points);
|
|
924
1203
|
return {
|
|
925
|
-
d: buildRoundedPath(points,
|
|
1204
|
+
d: buildRoundedPath(points, metrics.corner),
|
|
926
1205
|
labelX: label.x,
|
|
927
1206
|
labelY: label.y
|
|
928
1207
|
};
|
|
@@ -933,6 +1212,7 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
|
|
|
933
1212
|
const fromPoint = edge?.fromPoint || (fromNode ? getPortPosition(fromNode, fromPort) : null);
|
|
934
1213
|
const toPoint = edge?.toPoint || (toNode ? getPortPosition(toNode, toPort) : null);
|
|
935
1214
|
const route = normalizeEdgeRoute(edge?.route);
|
|
1215
|
+
const metrics = routeMetrics(edgeVisualScale(fromNode, toNode));
|
|
936
1216
|
if (!fromPoint || !toPoint) {
|
|
937
1217
|
return { d: "", labelX: 0, labelY: 0 };
|
|
938
1218
|
}
|
|
@@ -940,15 +1220,17 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
|
|
|
940
1220
|
return buildStraightPath(fromPoint, toPoint);
|
|
941
1221
|
}
|
|
942
1222
|
if (route === "orthogonal") {
|
|
943
|
-
const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode,
|
|
944
|
-
const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode,
|
|
945
|
-
return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
|
|
1223
|
+
const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode, metrics.clearance) : null;
|
|
1224
|
+
const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode, metrics.clearance) : null;
|
|
1225
|
+
return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
|
|
946
1226
|
}
|
|
947
|
-
return buildCurvePath(fromPoint, toPoint, fromPort, toPort);
|
|
1227
|
+
return buildCurvePath(fromPoint, toPoint, fromPort, toPort, metrics);
|
|
948
1228
|
}
|
|
949
|
-
function createArrowMarker(id, strokeWidth, reversed = false) {
|
|
950
|
-
const
|
|
951
|
-
const
|
|
1229
|
+
function createArrowMarker(id, strokeWidth, reversed = false, edgeScale = 1) {
|
|
1230
|
+
const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
|
|
1231
|
+
const effective = Math.max(0.02, strokeWidth * scale);
|
|
1232
|
+
const size = formatNumber(Math.max(9 * scale, effective * 4.4));
|
|
1233
|
+
const refInset = formatNumber(Math.max(1.5 * scale, effective * 0.85));
|
|
952
1234
|
const marker = svgEl("marker", {
|
|
953
1235
|
id,
|
|
954
1236
|
markerWidth: size,
|
|
@@ -966,9 +1248,11 @@ function createArrowMarker(id, strokeWidth, reversed = false) {
|
|
|
966
1248
|
);
|
|
967
1249
|
return marker;
|
|
968
1250
|
}
|
|
969
|
-
function createDotMarker(id, strokeWidth) {
|
|
970
|
-
const
|
|
971
|
-
const
|
|
1251
|
+
function createDotMarker(id, strokeWidth, edgeScale = 1) {
|
|
1252
|
+
const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
|
|
1253
|
+
const effective = Math.max(0.02, strokeWidth * scale);
|
|
1254
|
+
const size = formatNumber(Math.max(8 * scale, effective * 3.3));
|
|
1255
|
+
const radius = formatNumber(Math.max(2.75 * scale, effective * 1.45));
|
|
972
1256
|
const marker = svgEl("marker", {
|
|
973
1257
|
id,
|
|
974
1258
|
markerWidth: size,
|
|
@@ -1010,6 +1294,68 @@ function getResizeCursor(handle) {
|
|
|
1010
1294
|
if (handle === "ne" || handle === "sw") return "nesw-resize";
|
|
1011
1295
|
return "nwse-resize";
|
|
1012
1296
|
}
|
|
1297
|
+
function collectGuideLines(nodes) {
|
|
1298
|
+
const x = [];
|
|
1299
|
+
const y = [];
|
|
1300
|
+
nodes.forEach((node) => {
|
|
1301
|
+
const right = node.x + node.width;
|
|
1302
|
+
const bottom = node.y + node.height;
|
|
1303
|
+
[node.x, node.x + node.width / 2, right].forEach(
|
|
1304
|
+
(value) => x.push({ value, start: node.y, end: bottom })
|
|
1305
|
+
);
|
|
1306
|
+
[node.y, node.y + node.height / 2, bottom].forEach(
|
|
1307
|
+
(value) => y.push({ value, start: node.x, end: right })
|
|
1308
|
+
);
|
|
1309
|
+
});
|
|
1310
|
+
const byValue = (a, b) => a.value - b.value;
|
|
1311
|
+
return { x: x.sort(byValue), y: y.sort(byValue) };
|
|
1312
|
+
}
|
|
1313
|
+
function lowerBound(lines, value) {
|
|
1314
|
+
let low = 0;
|
|
1315
|
+
let high = lines.length;
|
|
1316
|
+
while (low < high) {
|
|
1317
|
+
const middle = low + high >> 1;
|
|
1318
|
+
if (lines[middle].value < value) low = middle + 1;
|
|
1319
|
+
else high = middle;
|
|
1320
|
+
}
|
|
1321
|
+
return low;
|
|
1322
|
+
}
|
|
1323
|
+
function snapToGuides(lines, candidates, threshold) {
|
|
1324
|
+
let best = null;
|
|
1325
|
+
candidates.forEach((candidate) => {
|
|
1326
|
+
const index = lowerBound(lines, candidate);
|
|
1327
|
+
[lines[index - 1], lines[index]].forEach((line) => {
|
|
1328
|
+
if (!line) return;
|
|
1329
|
+
const offset = line.value - candidate;
|
|
1330
|
+
if (Math.abs(offset) <= threshold && (!best || Math.abs(offset) < Math.abs(best.offset))) {
|
|
1331
|
+
best = { offset };
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
if (!best) return null;
|
|
1336
|
+
const positions = candidates.map((candidate) => candidate + best.offset);
|
|
1337
|
+
best.lines = lines.filter(
|
|
1338
|
+
(line) => positions.some((position) => Math.abs(line.value - position) < 0.5)
|
|
1339
|
+
);
|
|
1340
|
+
return best;
|
|
1341
|
+
}
|
|
1342
|
+
function guideSegments(axis, lines, box) {
|
|
1343
|
+
const byValue = /* @__PURE__ */ new Map();
|
|
1344
|
+
lines.forEach((line) => {
|
|
1345
|
+
const current = byValue.get(line.value);
|
|
1346
|
+
byValue.set(line.value, {
|
|
1347
|
+
start: Math.min(current?.start ?? Infinity, line.start),
|
|
1348
|
+
end: Math.max(current?.end ?? -Infinity, line.end)
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1351
|
+
const boxStart = axis === "x" ? box.top : box.left;
|
|
1352
|
+
const boxEnd = boxStart + (axis === "x" ? box.height : box.width);
|
|
1353
|
+
return [...byValue].map(([value, extent]) => {
|
|
1354
|
+
const start = Math.min(extent.start, boxStart) - GUIDE_OVERHANG;
|
|
1355
|
+
const end = Math.max(extent.end, boxEnd) + GUIDE_OVERHANG;
|
|
1356
|
+
return axis === "x" ? { x1: value, y1: start, x2: value, y2: end } : { x1: start, y1: value, x2: end, y2: value };
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1013
1359
|
function getBounds(nodes) {
|
|
1014
1360
|
if (!nodes.length) {
|
|
1015
1361
|
return { left: 0, top: 0, right: 0, bottom: 0 };
|
|
@@ -1163,9 +1509,26 @@ var VdFlowchart = class {
|
|
|
1163
1509
|
this.element = resolveElement(options.element || options.target);
|
|
1164
1510
|
this.readonly = Boolean(options.readonly);
|
|
1165
1511
|
this.gridSize = clamp(toFiniteNumber(options.gridSize, DEFAULT_GRID_SIZE), 12, 64);
|
|
1166
|
-
|
|
1512
|
+
const scaleBounds = normalizeBoundPair(
|
|
1513
|
+
options.minScale,
|
|
1514
|
+
options.maxScale,
|
|
1515
|
+
DEFAULT_MIN_SCALE,
|
|
1516
|
+
DEFAULT_MAX_SCALE
|
|
1517
|
+
);
|
|
1518
|
+
const sizeBounds = normalizeBoundPair(
|
|
1519
|
+
options.minNodeSize,
|
|
1520
|
+
options.maxNodeSize,
|
|
1521
|
+
DEFAULT_MIN_NODE_SIZE,
|
|
1522
|
+
DEFAULT_MAX_NODE_SIZE
|
|
1523
|
+
);
|
|
1524
|
+
this.minScale = scaleBounds.min;
|
|
1525
|
+
this.maxScale = scaleBounds.max;
|
|
1526
|
+
this.minNodeSize = sizeBounds.min;
|
|
1527
|
+
this.maxNodeSize = sizeBounds.max;
|
|
1528
|
+
this.documentData = normalizeDocument(options.data || {}, { scaleBounds, sizeBounds });
|
|
1167
1529
|
this.listeners = {};
|
|
1168
1530
|
this.selection = null;
|
|
1531
|
+
this.selectedNodeIds = /* @__PURE__ */ new Set();
|
|
1169
1532
|
this.interaction = null;
|
|
1170
1533
|
this.activeTool = null;
|
|
1171
1534
|
this.layoutMode = "tree";
|
|
@@ -1173,6 +1536,7 @@ var VdFlowchart = class {
|
|
|
1173
1536
|
this.destroyed = false;
|
|
1174
1537
|
this.lastNodePointer = null;
|
|
1175
1538
|
this.reconnectEdgeId = null;
|
|
1539
|
+
this.edgeCycleAnchor = null;
|
|
1176
1540
|
this.clipboard = null;
|
|
1177
1541
|
this.gridPatternId = nextId("flowchart-grid");
|
|
1178
1542
|
this.markerIds = /* @__PURE__ */ new Map();
|
|
@@ -1182,6 +1546,16 @@ var VdFlowchart = class {
|
|
|
1182
1546
|
this.history = [];
|
|
1183
1547
|
this.historyIndex = -1;
|
|
1184
1548
|
this.isApplyingHistory = false;
|
|
1549
|
+
this.historyBatchDepth = 0;
|
|
1550
|
+
this.historyBatchDirty = false;
|
|
1551
|
+
this.coalesceInsertedText = false;
|
|
1552
|
+
this.keyboardShortcuts = normalizeKeyboardShortcuts(options.keyboardShortcuts);
|
|
1553
|
+
this.snapGuides = options.snapGuides !== false;
|
|
1554
|
+
this.minimapEnabled = options.minimap !== false;
|
|
1555
|
+
this.autoLayout = Boolean(options.autoLayout);
|
|
1556
|
+
this.minimapFrame = 0;
|
|
1557
|
+
this.minimapDrag = null;
|
|
1558
|
+
this.shortcutsHelpOpen = false;
|
|
1185
1559
|
this.autoFit = Boolean(options.autoFit);
|
|
1186
1560
|
this.readyEmitted = false;
|
|
1187
1561
|
this.resizeObserver = null;
|
|
@@ -1222,6 +1596,7 @@ var VdFlowchart = class {
|
|
|
1222
1596
|
if (!ready) return false;
|
|
1223
1597
|
this.readyEmitted = true;
|
|
1224
1598
|
if (this.autoFit) this.fitView();
|
|
1599
|
+
else this.scheduleMinimap();
|
|
1225
1600
|
this.emit("ready", this);
|
|
1226
1601
|
return true;
|
|
1227
1602
|
};
|
|
@@ -1291,8 +1666,25 @@ var VdFlowchart = class {
|
|
|
1291
1666
|
className: "vd-flowchart-toolbar-label",
|
|
1292
1667
|
text: "100%"
|
|
1293
1668
|
});
|
|
1669
|
+
this.shortcutsButton = createElement("button", {
|
|
1670
|
+
className: "vd-flowchart-btn vd-flowchart-icon-btn vd-flowchart-shortcuts-btn",
|
|
1671
|
+
title: "Keyboard shortcuts (?)",
|
|
1672
|
+
text: "?"
|
|
1673
|
+
});
|
|
1674
|
+
this.shortcutsButton.setAttribute("type", "button");
|
|
1675
|
+
this.shortcutsButton.setAttribute("data-flowchart-action", "shortcuts");
|
|
1676
|
+
this.shortcutsButton.setAttribute("aria-label", "Keyboard shortcuts");
|
|
1677
|
+
this.shortcutsButton.setAttribute("aria-expanded", "false");
|
|
1678
|
+
this.minimapButton = createToolbarButton({
|
|
1679
|
+
action: "minimap",
|
|
1680
|
+
label: "Toggle minimap",
|
|
1681
|
+
icon: "minimap"
|
|
1682
|
+
});
|
|
1683
|
+
this.minimapButton.setAttribute("aria-pressed", this.minimapEnabled ? "true" : "false");
|
|
1294
1684
|
toolbarLeft.appendChild(this.arrangeSelect);
|
|
1295
1685
|
toolbarLeft.appendChild(this.clearButton);
|
|
1686
|
+
toolbarRight.appendChild(this.minimapButton);
|
|
1687
|
+
toolbarRight.appendChild(this.shortcutsButton);
|
|
1296
1688
|
toolbarRight.appendChild(this.zoomLabel);
|
|
1297
1689
|
this.toolbar.appendChild(toolbarLeft);
|
|
1298
1690
|
this.toolbar.appendChild(toolbarRight);
|
|
@@ -1361,23 +1753,50 @@ var VdFlowchart = class {
|
|
|
1361
1753
|
this.world = svgEl("g", { class: "vd-flowchart-world" });
|
|
1362
1754
|
this.gridRect = svgEl("rect", {
|
|
1363
1755
|
class: "vd-flowchart-grid",
|
|
1364
|
-
x: -
|
|
1365
|
-
y: -
|
|
1366
|
-
width:
|
|
1367
|
-
height:
|
|
1756
|
+
x: -DEFAULT_WORLD_EXTENT / 2,
|
|
1757
|
+
y: -DEFAULT_WORLD_EXTENT / 2,
|
|
1758
|
+
width: DEFAULT_WORLD_EXTENT,
|
|
1759
|
+
height: DEFAULT_WORLD_EXTENT,
|
|
1368
1760
|
fill: `url(#${this.gridPatternId})`
|
|
1369
1761
|
});
|
|
1370
1762
|
this.edgesLayer = svgEl("g", { class: "vd-flowchart-edges" });
|
|
1371
1763
|
this.previewLayer = svgEl("g", { class: "vd-flowchart-preview" });
|
|
1372
1764
|
this.nodesLayer = svgEl("g", { class: "vd-flowchart-nodes" });
|
|
1373
1765
|
this.overlayLayer = svgEl("g", { class: "vd-flowchart-overlay" });
|
|
1766
|
+
this.guidesLayer = svgEl("g", { class: "vd-flowchart-guides" });
|
|
1374
1767
|
this.world.appendChild(this.gridRect);
|
|
1375
1768
|
this.world.appendChild(this.edgesLayer);
|
|
1376
1769
|
this.world.appendChild(this.previewLayer);
|
|
1377
1770
|
this.world.appendChild(this.nodesLayer);
|
|
1378
1771
|
this.world.appendChild(this.overlayLayer);
|
|
1772
|
+
this.world.appendChild(this.guidesLayer);
|
|
1379
1773
|
this.svg.appendChild(this.world);
|
|
1380
1774
|
this.canvasEl.appendChild(this.svg);
|
|
1775
|
+
this.shortcutsHelp = createElement("div", { className: "vd-flowchart-shortcuts" });
|
|
1776
|
+
this.shortcutsHelp.id = nextId("flowchart-shortcuts");
|
|
1777
|
+
this.shortcutsHelp.setAttribute("role", "dialog");
|
|
1778
|
+
this.shortcutsHelp.setAttribute("aria-label", "Keyboard shortcuts");
|
|
1779
|
+
this.shortcutsHelp.hidden = true;
|
|
1780
|
+
this.shortcutsHelp.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
1781
|
+
this.shortcutsHelp.addEventListener("wheel", (event) => event.stopPropagation());
|
|
1782
|
+
this.shortcutsButton.setAttribute("aria-controls", this.shortcutsHelp.id);
|
|
1783
|
+
this.canvasEl.appendChild(this.shortcutsHelp);
|
|
1784
|
+
this.minimapEl = createElement("div", { className: "vd-flowchart-minimap" });
|
|
1785
|
+
this.minimapEl.setAttribute("aria-hidden", "true");
|
|
1786
|
+
this.minimapEl.hidden = true;
|
|
1787
|
+
this.minimapSvg = svgEl("svg", {
|
|
1788
|
+
class: "vd-flowchart-minimap-svg",
|
|
1789
|
+
width: MINIMAP_WIDTH,
|
|
1790
|
+
height: MINIMAP_HEIGHT,
|
|
1791
|
+
viewBox: `0 0 ${MINIMAP_WIDTH} ${MINIMAP_HEIGHT}`
|
|
1792
|
+
});
|
|
1793
|
+
this.minimapEl.appendChild(this.minimapSvg);
|
|
1794
|
+
this.minimapEl.addEventListener("pointerdown", (event) => this.handleMinimapPointer(event));
|
|
1795
|
+
this.minimapEl.addEventListener("pointermove", (event) => this.handleMinimapPointer(event));
|
|
1796
|
+
this.minimapEl.addEventListener("pointerup", (event) => this.endMinimapDrag(event));
|
|
1797
|
+
this.minimapEl.addEventListener("pointercancel", (event) => this.endMinimapDrag(event));
|
|
1798
|
+
this.minimapEl.addEventListener("wheel", (event) => event.stopPropagation());
|
|
1799
|
+
this.canvasEl.appendChild(this.minimapEl);
|
|
1381
1800
|
this.inspectorPanel = createElement("aside", {
|
|
1382
1801
|
className: "vd-flowchart-panel vd-flowchart-panel--inspector"
|
|
1383
1802
|
});
|
|
@@ -1485,11 +1904,15 @@ var VdFlowchart = class {
|
|
|
1485
1904
|
this.graphOutline.appendChild(this.graphList);
|
|
1486
1905
|
this.root.appendChild(this.graphOutline);
|
|
1487
1906
|
this.canvasEl.setAttribute("role", "group");
|
|
1907
|
+
this.syncCanvasLabel();
|
|
1908
|
+
this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
|
|
1909
|
+
}
|
|
1910
|
+
syncCanvasLabel() {
|
|
1911
|
+
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
1912
|
this.canvasEl.setAttribute(
|
|
1489
1913
|
"aria-label",
|
|
1490
|
-
|
|
1914
|
+
`Diagram canvas. ${keys} Press ? for all shortcuts. Graph outline provides connections.`
|
|
1491
1915
|
);
|
|
1492
|
-
this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
|
|
1493
1916
|
}
|
|
1494
1917
|
renderGraphOutline() {
|
|
1495
1918
|
if (!this.graphOutline) return;
|
|
@@ -1499,8 +1922,9 @@ var VdFlowchart = class {
|
|
|
1499
1922
|
const selected = this.selection?.kind === "node" ? this.selection.id : null;
|
|
1500
1923
|
this.graphStatus.textContent = selected ? `Selected: ${labels.get(selected)}. ${nodes.length} nodes, ${edges.length} connections.` : `${nodes.length} nodes, ${edges.length} connections.`;
|
|
1501
1924
|
if (!this.graphOutline.open) return;
|
|
1925
|
+
const hidden = this.getHiddenNodeIds();
|
|
1502
1926
|
const signature = JSON.stringify([
|
|
1503
|
-
nodes.map((node) => [node.id, node.text]),
|
|
1927
|
+
nodes.map((node) => [node.id, node.text, Boolean(node.collapsed), hidden.has(node.id)]),
|
|
1504
1928
|
edges.map((edge) => [edge.id, edge.from.nodeId, edge.to.nodeId, edge.label])
|
|
1505
1929
|
]);
|
|
1506
1930
|
if (signature !== this.graphSignature) {
|
|
@@ -1526,8 +1950,10 @@ var VdFlowchart = class {
|
|
|
1526
1950
|
const from = incoming.get(node.id);
|
|
1527
1951
|
const relationships = [
|
|
1528
1952
|
to.length ? `Connects to ${to.join(", ")}.` : "No outgoing connections.",
|
|
1529
|
-
from.length ? `Connected from ${from.join(", ")}.` : ""
|
|
1530
|
-
|
|
1953
|
+
from.length ? `Connected from ${from.join(", ")}.` : "",
|
|
1954
|
+
node.collapsed ? "Branch collapsed." : "",
|
|
1955
|
+
hidden.has(node.id) ? "Hidden in a collapsed branch." : ""
|
|
1956
|
+
].filter(Boolean);
|
|
1531
1957
|
this.graphList.appendChild(
|
|
1532
1958
|
createElement("li", { text: `${labels.get(node.id)}. ${relationships.join(" ")}` })
|
|
1533
1959
|
);
|
|
@@ -1612,6 +2038,157 @@ var VdFlowchart = class {
|
|
|
1612
2038
|
if (action === "undo") this.undo();
|
|
1613
2039
|
if (action === "redo") this.redo();
|
|
1614
2040
|
if (action === "clear" && !this.readonly) this.clear();
|
|
2041
|
+
if (action === "shortcuts") this.toggleShortcutsHelp();
|
|
2042
|
+
if (action === "minimap") this.setMinimapEnabled(!this.minimapEnabled);
|
|
2043
|
+
}
|
|
2044
|
+
setMinimapEnabled(enabled) {
|
|
2045
|
+
this.minimapEnabled = Boolean(enabled);
|
|
2046
|
+
this.minimapButton.setAttribute("aria-pressed", this.minimapEnabled ? "true" : "false");
|
|
2047
|
+
this.drawMinimap();
|
|
2048
|
+
return this;
|
|
2049
|
+
}
|
|
2050
|
+
scheduleMinimap() {
|
|
2051
|
+
if (this.minimapFrame || !hasWindow()) return;
|
|
2052
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (callback) => setTimeout(callback, 16);
|
|
2053
|
+
this.minimapFrame = raf(() => {
|
|
2054
|
+
this.minimapFrame = 0;
|
|
2055
|
+
this.drawMinimap();
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
// World-to-minimap mapping covering every visible node and the viewport.
|
|
2059
|
+
getMinimapTransform() {
|
|
2060
|
+
const viewport = this.documentData.viewport;
|
|
2061
|
+
const width = this.canvasEl.clientWidth;
|
|
2062
|
+
const height = this.canvasEl.clientHeight;
|
|
2063
|
+
const view = {
|
|
2064
|
+
left: -viewport.x / viewport.scale,
|
|
2065
|
+
top: -viewport.y / viewport.scale,
|
|
2066
|
+
right: (width - viewport.x) / viewport.scale,
|
|
2067
|
+
bottom: (height - viewport.y) / viewport.scale
|
|
2068
|
+
};
|
|
2069
|
+
const content = getBounds(this.getVisibleNodes());
|
|
2070
|
+
const left = Math.min(content.left, view.left);
|
|
2071
|
+
const top = Math.min(content.top, view.top);
|
|
2072
|
+
const spanX = Math.max(1, Math.max(content.right, view.right) - left);
|
|
2073
|
+
const spanY = Math.max(1, Math.max(content.bottom, view.bottom) - top);
|
|
2074
|
+
const padding = 8;
|
|
2075
|
+
const scale = Math.min(
|
|
2076
|
+
(MINIMAP_WIDTH - padding * 2) / spanX,
|
|
2077
|
+
(MINIMAP_HEIGHT - padding * 2) / spanY
|
|
2078
|
+
);
|
|
2079
|
+
return {
|
|
2080
|
+
scale,
|
|
2081
|
+
offsetX: (MINIMAP_WIDTH - spanX * scale) / 2 - left * scale,
|
|
2082
|
+
offsetY: (MINIMAP_HEIGHT - spanY * scale) / 2 - top * scale,
|
|
2083
|
+
view
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
drawMinimap() {
|
|
2087
|
+
if (this.destroyed || !this.minimapEl) return;
|
|
2088
|
+
const nodes = this.getVisibleNodes();
|
|
2089
|
+
const visible = this.minimapEnabled && nodes.length > 0 && this.canvasEl.clientWidth >= MINIMAP_MIN_CANVAS_WIDTH;
|
|
2090
|
+
this.minimapEl.hidden = !visible;
|
|
2091
|
+
if (!visible) return;
|
|
2092
|
+
const transform = this.minimapDrag?.transform || this.getMinimapTransform();
|
|
2093
|
+
const view = this.getMinimapTransform().view;
|
|
2094
|
+
const map = (x, y) => ({
|
|
2095
|
+
x: formatNumber(x * transform.scale + transform.offsetX),
|
|
2096
|
+
y: formatNumber(y * transform.scale + transform.offsetY)
|
|
2097
|
+
});
|
|
2098
|
+
clearChildren(this.minimapSvg);
|
|
2099
|
+
nodes.forEach((node) => {
|
|
2100
|
+
const point = map(node.x, node.y);
|
|
2101
|
+
const selected = this.isNodeSelected(node.id);
|
|
2102
|
+
this.minimapSvg.appendChild(
|
|
2103
|
+
svgEl("rect", {
|
|
2104
|
+
class: `vd-flowchart-minimap-node${selected ? " is-selected" : ""}`,
|
|
2105
|
+
x: point.x,
|
|
2106
|
+
y: point.y,
|
|
2107
|
+
width: formatNumber(Math.max(1.5, node.width * transform.scale)),
|
|
2108
|
+
height: formatNumber(Math.max(1.5, node.height * transform.scale)),
|
|
2109
|
+
rx: 1.5
|
|
2110
|
+
})
|
|
2111
|
+
);
|
|
2112
|
+
});
|
|
2113
|
+
const corner = map(view.left, view.top);
|
|
2114
|
+
this.minimapSvg.appendChild(
|
|
2115
|
+
svgEl("rect", {
|
|
2116
|
+
class: "vd-flowchart-minimap-viewport",
|
|
2117
|
+
x: corner.x,
|
|
2118
|
+
y: corner.y,
|
|
2119
|
+
width: formatNumber((view.right - view.left) * transform.scale),
|
|
2120
|
+
height: formatNumber((view.bottom - view.top) * transform.scale)
|
|
2121
|
+
})
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
// Centre the main view on the world point under the minimap pointer. The
|
|
2125
|
+
// mapping is frozen for the whole drag so the map does not shift under it.
|
|
2126
|
+
handleMinimapPointer(event) {
|
|
2127
|
+
event.stopPropagation();
|
|
2128
|
+
if (event.type === "pointerdown") {
|
|
2129
|
+
if (event.button != null && event.button !== 0) return;
|
|
2130
|
+
event.preventDefault();
|
|
2131
|
+
this.minimapDrag = { pointerId: event.pointerId, transform: this.getMinimapTransform() };
|
|
2132
|
+
try {
|
|
2133
|
+
this.minimapEl.setPointerCapture?.(event.pointerId);
|
|
2134
|
+
} catch {
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (!this.minimapDrag || this.minimapDrag.pointerId !== event.pointerId) return;
|
|
2138
|
+
const rect = this.minimapEl.getBoundingClientRect();
|
|
2139
|
+
const { transform } = this.minimapDrag;
|
|
2140
|
+
this.centerViewOn(
|
|
2141
|
+
(event.clientX - rect.left - transform.offsetX) / transform.scale,
|
|
2142
|
+
(event.clientY - rect.top - transform.offsetY) / transform.scale
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
endMinimapDrag(event) {
|
|
2146
|
+
event.stopPropagation();
|
|
2147
|
+
if (!this.minimapDrag || this.minimapDrag.pointerId !== event.pointerId) return;
|
|
2148
|
+
this.minimapDrag = null;
|
|
2149
|
+
this.syncJsonTextarea();
|
|
2150
|
+
this.emitViewportChange("viewport:pan");
|
|
2151
|
+
this.drawMinimap();
|
|
2152
|
+
}
|
|
2153
|
+
centerViewOn(worldX, worldY) {
|
|
2154
|
+
const viewport = this.documentData.viewport;
|
|
2155
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
2156
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
2157
|
+
viewport.x = formatNumber(width / 2 - worldX * viewport.scale);
|
|
2158
|
+
viewport.y = formatNumber(height / 2 - worldY * viewport.scale);
|
|
2159
|
+
this.render({ inspector: false, json: false });
|
|
2160
|
+
return this;
|
|
2161
|
+
}
|
|
2162
|
+
toggleShortcutsHelp(force) {
|
|
2163
|
+
const open = typeof force === "boolean" ? force : !this.shortcutsHelpOpen;
|
|
2164
|
+
this.shortcutsHelpOpen = open;
|
|
2165
|
+
this.shortcutsButton.setAttribute("aria-expanded", open ? "true" : "false");
|
|
2166
|
+
if (open) this.renderShortcutsHelp();
|
|
2167
|
+
this.shortcutsHelp.hidden = !open;
|
|
2168
|
+
if (!open && this.shortcutsHelp.contains(document.activeElement)) this.canvasEl.focus();
|
|
2169
|
+
return this;
|
|
2170
|
+
}
|
|
2171
|
+
renderShortcutsHelp() {
|
|
2172
|
+
clearChildren(this.shortcutsHelp);
|
|
2173
|
+
const header = createElement("div", { className: "vd-flowchart-shortcuts-header" });
|
|
2174
|
+
header.appendChild(
|
|
2175
|
+
createElement("h4", { className: "vd-flowchart-panel-title", text: "Keyboard shortcuts" })
|
|
2176
|
+
);
|
|
2177
|
+
const close = createElement("button", {
|
|
2178
|
+
className: "vd-flowchart-btn vd-flowchart-icon-btn",
|
|
2179
|
+
text: "\xD7"
|
|
2180
|
+
});
|
|
2181
|
+
close.setAttribute("type", "button");
|
|
2182
|
+
close.setAttribute("aria-label", "Close keyboard shortcuts");
|
|
2183
|
+
close.addEventListener("click", () => this.toggleShortcutsHelp(false));
|
|
2184
|
+
header.appendChild(close);
|
|
2185
|
+
this.shortcutsHelp.appendChild(header);
|
|
2186
|
+
const list = createElement("dl", { className: "vd-flowchart-shortcuts-list" });
|
|
2187
|
+
getShortcutRows(this.keyboardShortcuts, this.readonly).forEach(([keys, action]) => {
|
|
2188
|
+
list.appendChild(createElement("dt", { text: keys }));
|
|
2189
|
+
list.appendChild(createElement("dd", { text: action }));
|
|
2190
|
+
});
|
|
2191
|
+
this.shortcutsHelp.appendChild(list);
|
|
1615
2192
|
}
|
|
1616
2193
|
handleArrangeChange(event) {
|
|
1617
2194
|
if (this.readonly) return;
|
|
@@ -1720,38 +2297,48 @@ var VdFlowchart = class {
|
|
|
1720
2297
|
);
|
|
1721
2298
|
}
|
|
1722
2299
|
}
|
|
2300
|
+
// Keys the editor consumes stop here, so page-level shortcuts (a docs
|
|
2301
|
+
// site's "/" search, a host's Escape handler) do not also fire.
|
|
1723
2302
|
handleKeyDown(event) {
|
|
1724
|
-
|
|
2303
|
+
const alreadyHandled = event.defaultPrevented;
|
|
2304
|
+
this.handleEditorKey(event);
|
|
2305
|
+
if (!alreadyHandled && event.defaultPrevented) event.stopPropagation();
|
|
2306
|
+
}
|
|
2307
|
+
handleEditorKey(event) {
|
|
2308
|
+
const target = event.target;
|
|
2309
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable)) {
|
|
1725
2310
|
return;
|
|
1726
2311
|
}
|
|
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
|
-
}
|
|
2312
|
+
if (event.isComposing) return;
|
|
2313
|
+
const key = event.key;
|
|
2314
|
+
const modKey = event.metaKey || event.ctrlKey;
|
|
2315
|
+
if (key === "Escape" && this.shortcutsHelpOpen) {
|
|
2316
|
+
event.preventDefault();
|
|
2317
|
+
this.toggleShortcutsHelp(false);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
if (target === this.canvasEl && this.handleCanvasKeyDown(event)) {
|
|
2321
|
+
event.preventDefault();
|
|
2322
|
+
return;
|
|
1747
2323
|
}
|
|
1748
2324
|
if (this.readonly) return;
|
|
1749
|
-
if (
|
|
2325
|
+
if (key === "Escape") {
|
|
2326
|
+
if (this.activeTool) this.setActiveTool(null);
|
|
2327
|
+
else if (this.reconnectEdgeId) {
|
|
2328
|
+
this.reconnectEdgeId = null;
|
|
2329
|
+
this.render({ inspector: false, json: false });
|
|
2330
|
+
} else if (this.selection) this.select(null);
|
|
2331
|
+
else return;
|
|
1750
2332
|
event.preventDefault();
|
|
1751
|
-
this.setActiveTool(null);
|
|
1752
2333
|
return;
|
|
1753
2334
|
}
|
|
1754
|
-
|
|
2335
|
+
if (modKey && (key === "d" || key === "D")) {
|
|
2336
|
+
if (this.selection?.kind === "node") {
|
|
2337
|
+
event.preventDefault();
|
|
2338
|
+
this.duplicateSelection();
|
|
2339
|
+
}
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
1755
2342
|
if (modKey && (event.key === "z" || event.key === "Z")) {
|
|
1756
2343
|
event.preventDefault();
|
|
1757
2344
|
if (event.shiftKey) {
|
|
@@ -1790,11 +2377,250 @@ var VdFlowchart = class {
|
|
|
1790
2377
|
if (!this.selection) return;
|
|
1791
2378
|
if (event.key === "Backspace" || event.key === "Delete") {
|
|
1792
2379
|
event.preventDefault();
|
|
2380
|
+
if (this.selection.kind === "node" && !this.hasMultiSelection()) {
|
|
2381
|
+
const removedId = this.selection.id;
|
|
2382
|
+
const parent = this.getParentNode(removedId);
|
|
2383
|
+
const sibling = parent ? this.getChildNodes(parent.id).find((node) => node.id !== removedId) : null;
|
|
2384
|
+
if (this.deleteSelection()) {
|
|
2385
|
+
const next = parent && this.findNode(parent.id) || sibling;
|
|
2386
|
+
if (next) this.select({ kind: "node", id: next.id });
|
|
2387
|
+
}
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
1793
2390
|
this.deleteSelection();
|
|
1794
2391
|
}
|
|
1795
2392
|
}
|
|
2393
|
+
// Keys that only apply while the canvas itself has focus. Returns true when
|
|
2394
|
+
// the key was handled (the caller prevents the default action).
|
|
2395
|
+
handleCanvasKeyDown(event) {
|
|
2396
|
+
const key = event.key;
|
|
2397
|
+
const modKey = event.metaKey || event.ctrlKey;
|
|
2398
|
+
const mindmap = this.keyboardShortcuts === "mindmap";
|
|
2399
|
+
const selectedNode = this.selection?.kind === "node" ? this.findNode(this.selection.id) : null;
|
|
2400
|
+
const selectedEdge = this.selection?.kind === "edge" ? this.findEdge(this.selection.id) : null;
|
|
2401
|
+
const movable = !this.readonly && Boolean(selectedNode);
|
|
2402
|
+
const editable = movable && !this.hasMultiSelection();
|
|
2403
|
+
if (key === "?" && !modKey && !event.altKey) {
|
|
2404
|
+
this.toggleShortcutsHelp();
|
|
2405
|
+
return true;
|
|
2406
|
+
}
|
|
2407
|
+
if (modKey && !event.altKey) {
|
|
2408
|
+
if (key === "=" || key === "+") return Boolean(this.zoomIn());
|
|
2409
|
+
if (key === "-" || key === "_") return Boolean(this.zoomOut());
|
|
2410
|
+
if (key === "0") return Boolean(this.zoomTo(1));
|
|
2411
|
+
if (key === "a" || key === "A") {
|
|
2412
|
+
this.selectNodes(this.getVisibleNodes().map((node) => node.id));
|
|
2413
|
+
return true;
|
|
2414
|
+
}
|
|
2415
|
+
if (key === "/" && selectedNode && !this.hasMultiSelection()) {
|
|
2416
|
+
this.toggleCollapsed(selectedNode.id);
|
|
2417
|
+
return true;
|
|
2418
|
+
}
|
|
2419
|
+
return false;
|
|
2420
|
+
}
|
|
2421
|
+
if (event.shiftKey && !event.altKey && (key === "!" || event.code === "Digit1")) {
|
|
2422
|
+
return Boolean(this.fitView());
|
|
2423
|
+
}
|
|
2424
|
+
const direction = ARROW_DIRECTIONS[key];
|
|
2425
|
+
if (direction) {
|
|
2426
|
+
if (event.altKey) {
|
|
2427
|
+
if (movable) {
|
|
2428
|
+
this.nudgeNodes(this.getSelectedNodeIds(), direction, event.shiftKey ? 1 : this.gridSize);
|
|
2429
|
+
}
|
|
2430
|
+
return true;
|
|
2431
|
+
}
|
|
2432
|
+
if (event.shiftKey) {
|
|
2433
|
+
const anchorId = selectedNode?.id ?? (selectedEdge ? this.edgeCycleAnchor : null);
|
|
2434
|
+
return anchorId ? this.selectSideEdge(anchorId, direction) : false;
|
|
2435
|
+
}
|
|
2436
|
+
this.navigateSelection(direction);
|
|
2437
|
+
return true;
|
|
2438
|
+
}
|
|
2439
|
+
const nodes = this.getVisibleNodes();
|
|
2440
|
+
if ((key === "Home" || key === "End") && nodes.length) {
|
|
2441
|
+
this.selectNode(nodes[key === "Home" ? 0 : nodes.length - 1].id);
|
|
2442
|
+
return true;
|
|
2443
|
+
}
|
|
2444
|
+
if (selectedEdge && !this.readonly && !event.altKey) {
|
|
2445
|
+
if (key === "F2" || key === "Enter" || key === " " && mindmap) {
|
|
2446
|
+
return this.startEdgeLabelEdit(selectedEdge.id);
|
|
2447
|
+
}
|
|
2448
|
+
if (mindmap && key.length === 1 && key !== " ") {
|
|
2449
|
+
return this.startEdgeLabelEdit(selectedEdge.id, { initialText: key });
|
|
2450
|
+
}
|
|
2451
|
+
return false;
|
|
2452
|
+
}
|
|
2453
|
+
if (!editable || event.altKey) return false;
|
|
2454
|
+
if (key === "F2" || key === "Enter" && !mindmap || key === " " && mindmap) {
|
|
2455
|
+
this.startTextEdit(selectedNode.id);
|
|
2456
|
+
return true;
|
|
2457
|
+
}
|
|
2458
|
+
if (!mindmap) return false;
|
|
2459
|
+
if (key === "Tab" && !event.shiftKey) {
|
|
2460
|
+
this.insertBranchNode(selectedNode.id, { edit: true });
|
|
2461
|
+
return true;
|
|
2462
|
+
}
|
|
2463
|
+
if (key === "Enter") {
|
|
2464
|
+
this.insertSiblingNode(selectedNode.id, { before: event.shiftKey, edit: true });
|
|
2465
|
+
return true;
|
|
2466
|
+
}
|
|
2467
|
+
if (key.length === 1 && key !== " " && isNodeTextEditable(selectedNode)) {
|
|
2468
|
+
this.startTextEdit(selectedNode.id, { initialText: key });
|
|
2469
|
+
return true;
|
|
2470
|
+
}
|
|
2471
|
+
return false;
|
|
2472
|
+
}
|
|
2473
|
+
// Move the selection to the nearest node in `direction`; with nothing (or an
|
|
2474
|
+
// edge) selected, start from the node closest to the centre of the view.
|
|
2475
|
+
navigateSelection(direction) {
|
|
2476
|
+
const nodes = this.getVisibleNodes();
|
|
2477
|
+
if (!nodes.length) return false;
|
|
2478
|
+
const current = this.selection?.kind === "node" ? this.findNode(this.selection.id) : null;
|
|
2479
|
+
const edge = this.selection?.kind === "edge" ? this.findEdge(this.selection.id) : null;
|
|
2480
|
+
const next = current ? findSpatialNeighbor(nodes, current, direction) : edge ? this.getEdgeEndInDirection(edge, direction) : findNearestNode(nodes, this.getViewportCenter());
|
|
2481
|
+
if (!next) return false;
|
|
2482
|
+
this.selectNode(next.id);
|
|
2483
|
+
this.revealNode(next.id);
|
|
2484
|
+
return true;
|
|
2485
|
+
}
|
|
2486
|
+
// The end of `edge` that best matches `direction`, measured from the label
|
|
2487
|
+
// anchor so either end can be reached from a selected connection.
|
|
2488
|
+
getEdgeEndInDirection(edge, direction) {
|
|
2489
|
+
const fromNode = this.findNode(edge.from.nodeId);
|
|
2490
|
+
const toNode = this.findNode(edge.to.nodeId);
|
|
2491
|
+
if (!fromNode || !toNode) return fromNode || toNode;
|
|
2492
|
+
const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
|
|
2493
|
+
const vector = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] }[direction];
|
|
2494
|
+
const score = (node) => (node.x + node.width / 2 - labelX) * vector[0] + (node.y + node.height / 2 - labelY) * vector[1];
|
|
2495
|
+
return score(toNode) >= score(fromNode) ? toNode : fromNode;
|
|
2496
|
+
}
|
|
2497
|
+
// Connections attached to `nodeId` on the `direction` side, outgoing or
|
|
2498
|
+
// incoming, in document order.
|
|
2499
|
+
getSideEdges(nodeId, direction) {
|
|
2500
|
+
const port = DIRECTION_PORTS[direction];
|
|
2501
|
+
return this.documentData.edges.filter(
|
|
2502
|
+
(edge) => edge.from.nodeId === nodeId && edge.from.port === port || edge.to.nodeId === nodeId && edge.to.port === port
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
// Select a connection on one side of a node; repeating cycles through them.
|
|
2506
|
+
selectSideEdge(nodeId, direction) {
|
|
2507
|
+
const edges = this.getSideEdges(nodeId, direction);
|
|
2508
|
+
if (!edges.length) return false;
|
|
2509
|
+
const currentIndex = this.selection?.kind === "edge" ? edges.findIndex((edge) => edge.id === this.selection.id) : -1;
|
|
2510
|
+
const next = edges[(currentIndex + 1) % edges.length];
|
|
2511
|
+
this.select({ kind: "edge", id: next.id });
|
|
2512
|
+
this.edgeCycleAnchor = nodeId;
|
|
2513
|
+
return true;
|
|
2514
|
+
}
|
|
2515
|
+
nudgeNode(nodeId, direction, distance) {
|
|
2516
|
+
const [node] = this.nudgeNodes([nodeId], direction, distance);
|
|
2517
|
+
return node || null;
|
|
2518
|
+
}
|
|
2519
|
+
// Move nodes together; repeated nudges of the same selection coalesce into
|
|
2520
|
+
// one undo step.
|
|
2521
|
+
nudgeNodes(nodeIds, direction, distance) {
|
|
2522
|
+
const step = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] }[direction];
|
|
2523
|
+
if (this.readonly || !step) return [];
|
|
2524
|
+
const nodes = nodeIds.map((id) => this.findNode(id)).filter(Boolean);
|
|
2525
|
+
if (!nodes.length) return [];
|
|
2526
|
+
nodes.forEach((node) => {
|
|
2527
|
+
node.x = formatNumber(node.x + step[0] * distance);
|
|
2528
|
+
node.y = formatNumber(node.y + step[1] * distance);
|
|
2529
|
+
});
|
|
2530
|
+
this.render({ inspector: true, json: true });
|
|
2531
|
+
this.emitChange(
|
|
2532
|
+
"node:nudge",
|
|
2533
|
+
nodes.length > 1 ? { nodeIds: nodes.map((node) => node.id) } : { node: deepClone(nodes[0]) }
|
|
2534
|
+
);
|
|
2535
|
+
return nodes.map((node) => deepClone(node));
|
|
2536
|
+
}
|
|
2537
|
+
zoomTo(scale) {
|
|
2538
|
+
const viewport = this.documentData.viewport;
|
|
2539
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
2540
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
2541
|
+
this.scaleAround(
|
|
2542
|
+
clamp(formatScale(scale), this.minScale, this.maxScale) / viewport.scale,
|
|
2543
|
+
width / 2,
|
|
2544
|
+
height / 2,
|
|
2545
|
+
"viewport:zoom"
|
|
2546
|
+
);
|
|
2547
|
+
return this;
|
|
2548
|
+
}
|
|
2549
|
+
duplicateSelection() {
|
|
2550
|
+
if (this.readonly || this.selection?.kind !== "node") return null;
|
|
2551
|
+
if (this.hasMultiSelection()) return this.pasteNodeGroup(this.captureNodeGroup());
|
|
2552
|
+
const node = this.findNode(this.selection.id);
|
|
2553
|
+
return node ? this.pasteNodeData(node) : null;
|
|
2554
|
+
}
|
|
2555
|
+
// Selected nodes plus the connections that run between them.
|
|
2556
|
+
captureNodeGroup() {
|
|
2557
|
+
const ids = new Set(this.getSelectedNodeIds());
|
|
2558
|
+
return {
|
|
2559
|
+
nodes: deepClone(this.documentData.nodes.filter((node) => ids.has(node.id))),
|
|
2560
|
+
edges: deepClone(
|
|
2561
|
+
this.documentData.edges.filter(
|
|
2562
|
+
(edge) => ids.has(edge.from.nodeId) && ids.has(edge.to.nodeId)
|
|
2563
|
+
)
|
|
2564
|
+
)
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
// Paste a captured group offset by one step, remapping ids so internal
|
|
2568
|
+
// connections follow their copies. One change, one undo step.
|
|
2569
|
+
pasteNodeGroup(group) {
|
|
2570
|
+
if (this.readonly || !group?.nodes?.length) return [];
|
|
2571
|
+
const usedNodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
2572
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
2573
|
+
const nodes = group.nodes.map((data, index) => {
|
|
2574
|
+
const node = normalizeNode(
|
|
2575
|
+
{ ...data, id: void 0, x: data.x + 24, y: data.y + 24 },
|
|
2576
|
+
this.documentData.nodes.length + index,
|
|
2577
|
+
usedNodeIds,
|
|
2578
|
+
this.nodeSizeBounds()
|
|
2579
|
+
);
|
|
2580
|
+
idMap.set(data.id, node.id);
|
|
2581
|
+
return node;
|
|
2582
|
+
});
|
|
2583
|
+
this.documentData.nodes.push(...nodes);
|
|
2584
|
+
const nodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
2585
|
+
const usedEdgeIds = new Set(this.documentData.edges.map((edge) => edge.id));
|
|
2586
|
+
group.edges.forEach((data, index) => {
|
|
2587
|
+
const edge = normalizeEdge(
|
|
2588
|
+
{
|
|
2589
|
+
...data,
|
|
2590
|
+
id: void 0,
|
|
2591
|
+
from: { ...data.from, nodeId: idMap.get(data.from.nodeId) },
|
|
2592
|
+
to: { ...data.to, nodeId: idMap.get(data.to.nodeId) }
|
|
2593
|
+
},
|
|
2594
|
+
this.documentData.edges.length + index,
|
|
2595
|
+
nodeIds,
|
|
2596
|
+
usedEdgeIds
|
|
2597
|
+
);
|
|
2598
|
+
if (edge) this.documentData.edges.push(edge);
|
|
2599
|
+
});
|
|
2600
|
+
const ids = nodes.map((node) => node.id);
|
|
2601
|
+
this.selectNodes(ids);
|
|
2602
|
+
this.emitChange("nodes:add", { nodeIds: ids });
|
|
2603
|
+
return nodes.map((node) => deepClone(node));
|
|
2604
|
+
}
|
|
2605
|
+
pasteNodeData(data) {
|
|
2606
|
+
const usedIds = new Set(this.documentData.nodes.map((node2) => node2.id));
|
|
2607
|
+
const node = normalizeNode(
|
|
2608
|
+
{ ...data, id: void 0, collapsed: void 0, x: data.x + 24, y: data.y + 24 },
|
|
2609
|
+
this.documentData.nodes.length,
|
|
2610
|
+
usedIds,
|
|
2611
|
+
this.nodeSizeBounds()
|
|
2612
|
+
);
|
|
2613
|
+
this.documentData.nodes.push(node);
|
|
2614
|
+
this.select({ kind: "node", id: node.id });
|
|
2615
|
+
this.emitChange("node:add", { node: deepClone(node) });
|
|
2616
|
+
return deepClone(node);
|
|
2617
|
+
}
|
|
1796
2618
|
copySelection() {
|
|
1797
2619
|
if (!this.selection) return false;
|
|
2620
|
+
if (this.hasMultiSelection()) {
|
|
2621
|
+
this.clipboard = { kind: "nodes", data: this.captureNodeGroup() };
|
|
2622
|
+
return true;
|
|
2623
|
+
}
|
|
1798
2624
|
if (this.selection.kind === "node") {
|
|
1799
2625
|
const node = this.findNode(this.selection.id);
|
|
1800
2626
|
if (!node) return false;
|
|
@@ -1809,22 +2635,14 @@ var VdFlowchart = class {
|
|
|
1809
2635
|
pasteClipboard() {
|
|
1810
2636
|
if (!this.clipboard || this.readonly) return false;
|
|
1811
2637
|
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) });
|
|
2638
|
+
this.pasteNodeData(this.clipboard.data);
|
|
1826
2639
|
return true;
|
|
1827
2640
|
}
|
|
2641
|
+
if (this.clipboard.kind === "nodes") {
|
|
2642
|
+
const pasted = this.pasteNodeGroup(this.clipboard.data);
|
|
2643
|
+
this.clipboard = { kind: "nodes", data: this.captureNodeGroup() };
|
|
2644
|
+
return pasted.length > 0;
|
|
2645
|
+
}
|
|
1828
2646
|
const usedIds = new Set(this.documentData.edges.map((edge2) => edge2.id));
|
|
1829
2647
|
const nodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
1830
2648
|
const edge = normalizeEdge(
|
|
@@ -1851,16 +2669,16 @@ var VdFlowchart = class {
|
|
|
1851
2669
|
const edgeTarget = event.target.closest("[data-edge-id]");
|
|
1852
2670
|
if (edgeTarget && !event.target.closest("[data-edge-endpoint]")) {
|
|
1853
2671
|
const edgeId = edgeTarget.getAttribute("data-edge-id");
|
|
1854
|
-
if (edgeId && this.
|
|
1855
|
-
this.reconnectEdgeId = edgeId;
|
|
1856
|
-
this.select({ kind: "edge", id: edgeId });
|
|
2672
|
+
if (edgeId && this.startEdgeLabelEdit(edgeId)) {
|
|
1857
2673
|
event.preventDefault();
|
|
1858
2674
|
event.stopPropagation();
|
|
1859
2675
|
return;
|
|
1860
2676
|
}
|
|
1861
2677
|
}
|
|
1862
2678
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1863
|
-
if (!nodeTarget || event.target.closest("[data-edge-id]"))
|
|
2679
|
+
if (!nodeTarget || event.target.closest("[data-edge-id]") || event.target.closest("[data-collapse-toggle]")) {
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
1864
2682
|
const nodeId = nodeTarget.getAttribute("data-node-id");
|
|
1865
2683
|
if (this.startTextEdit(nodeId)) {
|
|
1866
2684
|
event.preventDefault();
|
|
@@ -1870,7 +2688,7 @@ var VdFlowchart = class {
|
|
|
1870
2688
|
handleClick(event) {
|
|
1871
2689
|
if (this.readonly || event.detail < 2) return;
|
|
1872
2690
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1873
|
-
if (!nodeTarget || event.target.closest("[data-port]") || event.target.closest("[data-resize-handle]"))
|
|
2691
|
+
if (!nodeTarget || event.target.closest("[data-port]") || event.target.closest("[data-resize-handle]") || event.target.closest("[data-collapse-toggle]"))
|
|
1874
2692
|
return;
|
|
1875
2693
|
if (this.startTextEdit(nodeTarget.getAttribute("data-node-id"))) {
|
|
1876
2694
|
event.preventDefault();
|
|
@@ -1883,6 +2701,13 @@ var VdFlowchart = class {
|
|
|
1883
2701
|
this.stopTextEdit({ commit: true });
|
|
1884
2702
|
}
|
|
1885
2703
|
this.canvasEl.focus();
|
|
2704
|
+
const collapseTarget = event.target.closest("[data-collapse-toggle]");
|
|
2705
|
+
if (collapseTarget) {
|
|
2706
|
+
this.lastNodePointer = null;
|
|
2707
|
+
this.toggleCollapsed(collapseTarget.getAttribute("data-collapse-toggle"));
|
|
2708
|
+
event.preventDefault();
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
1886
2711
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1887
2712
|
const portTarget = event.target.closest("[data-port]");
|
|
1888
2713
|
const resizeTarget = event.target.closest("[data-resize-handle]");
|
|
@@ -1918,6 +2743,13 @@ var VdFlowchart = class {
|
|
|
1918
2743
|
event.preventDefault();
|
|
1919
2744
|
return;
|
|
1920
2745
|
}
|
|
2746
|
+
if (nodeTarget && !portTarget && !resizeTarget && !edgeTarget && event.shiftKey) {
|
|
2747
|
+
this.reconnectEdgeId = null;
|
|
2748
|
+
this.lastNodePointer = null;
|
|
2749
|
+
this.toggleNodeSelection(nodeTarget.getAttribute("data-node-id"));
|
|
2750
|
+
event.preventDefault();
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
1921
2753
|
if (nodeTarget && !portTarget && !resizeTarget && !edgeTarget && !this.readonly) {
|
|
1922
2754
|
if (this.activeTool === "arrow") {
|
|
1923
2755
|
this.select({ kind: "node", id: nodeTarget.getAttribute("data-node-id") });
|
|
@@ -1944,6 +2776,9 @@ var VdFlowchart = class {
|
|
|
1944
2776
|
this.interaction = {
|
|
1945
2777
|
kind: "connect",
|
|
1946
2778
|
pointerId: event.pointerId,
|
|
2779
|
+
startClientX: event.clientX,
|
|
2780
|
+
startClientY: event.clientY,
|
|
2781
|
+
moved: false,
|
|
1947
2782
|
source: { nodeId, port },
|
|
1948
2783
|
target: null,
|
|
1949
2784
|
previousSnap: null,
|
|
@@ -1994,22 +2829,27 @@ var VdFlowchart = class {
|
|
|
1994
2829
|
if (nodeTarget) {
|
|
1995
2830
|
this.reconnectEdgeId = null;
|
|
1996
2831
|
const nodeId = nodeTarget.getAttribute("data-node-id");
|
|
1997
|
-
this.
|
|
2832
|
+
const groupIds = this.hasMultiSelection() && this.isNodeSelected(nodeId) ? this.getSelectedNodeIds() : [nodeId];
|
|
2833
|
+
if (groupIds.length > 1) this.selectNodes(groupIds, { primary: nodeId });
|
|
2834
|
+
else this.select({ kind: "node", id: nodeId });
|
|
1998
2835
|
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
|
-
};
|
|
2836
|
+
this.interaction = this.createDragInteraction(event, nodeId, groupIds);
|
|
2009
2837
|
this.capturePointer(event.pointerId);
|
|
2010
2838
|
}
|
|
2011
2839
|
return;
|
|
2012
2840
|
}
|
|
2841
|
+
if (event.shiftKey) {
|
|
2842
|
+
const startWorld = this.clientToWorld(event.clientX, event.clientY);
|
|
2843
|
+
this.interaction = {
|
|
2844
|
+
kind: "marquee",
|
|
2845
|
+
pointerId: event.pointerId,
|
|
2846
|
+
startWorld,
|
|
2847
|
+
baseIds: this.selection?.kind === "node" ? this.getSelectedNodeIds() : []
|
|
2848
|
+
};
|
|
2849
|
+
this.capturePointer(event.pointerId);
|
|
2850
|
+
event.preventDefault();
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2013
2853
|
this.select(null);
|
|
2014
2854
|
this.reconnectEdgeId = null;
|
|
2015
2855
|
this.interaction = {
|
|
@@ -2024,20 +2864,123 @@ var VdFlowchart = class {
|
|
|
2024
2864
|
this.capturePointer(event.pointerId);
|
|
2025
2865
|
event.preventDefault();
|
|
2026
2866
|
}
|
|
2867
|
+
createDragInteraction(event, nodeId, nodeIds) {
|
|
2868
|
+
const nodes = nodeIds.map((id) => this.findNode(id)).filter(Boolean);
|
|
2869
|
+
const moving = new Set(nodes.map((node) => node.id));
|
|
2870
|
+
return {
|
|
2871
|
+
kind: "drag-node",
|
|
2872
|
+
pointerId: event.pointerId,
|
|
2873
|
+
nodeId,
|
|
2874
|
+
nodeIds: nodes.map((node) => node.id),
|
|
2875
|
+
startWorld: this.clientToWorld(event.clientX, event.clientY),
|
|
2876
|
+
origins: new Map(nodes.map((node) => [node.id, { x: node.x, y: node.y }])),
|
|
2877
|
+
bounds: getBounds(nodes),
|
|
2878
|
+
guideLines: this.snapGuides ? collectGuideLines(this.getVisibleNodes().filter((node) => !moving.has(node.id))) : null,
|
|
2879
|
+
snapDisabled: false,
|
|
2880
|
+
moved: false
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
// Translate every dragged node by the pointer delta, snapping the group's
|
|
2884
|
+
// bounds to the nearest alignment guide on each axis. Alt turns snapping
|
|
2885
|
+
// off for the rest of the gesture.
|
|
2886
|
+
moveDraggedNodes(event) {
|
|
2887
|
+
const interaction = this.interaction;
|
|
2888
|
+
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2889
|
+
let dx = world.x - interaction.startWorld.x;
|
|
2890
|
+
let dy = world.y - interaction.startWorld.y;
|
|
2891
|
+
if (event.altKey) interaction.snapDisabled = true;
|
|
2892
|
+
let guides = [];
|
|
2893
|
+
if (interaction.guideLines && !interaction.snapDisabled) {
|
|
2894
|
+
const box = interaction.bounds;
|
|
2895
|
+
const threshold = GUIDE_SNAP_DISTANCE / this.documentData.viewport.scale;
|
|
2896
|
+
const left = box.left + dx;
|
|
2897
|
+
const top = box.top + dy;
|
|
2898
|
+
const width = box.right - box.left;
|
|
2899
|
+
const height = box.bottom - box.top;
|
|
2900
|
+
const snapX = snapToGuides(
|
|
2901
|
+
interaction.guideLines.x,
|
|
2902
|
+
[left, left + width / 2, left + width],
|
|
2903
|
+
threshold
|
|
2904
|
+
);
|
|
2905
|
+
const snapY = snapToGuides(
|
|
2906
|
+
interaction.guideLines.y,
|
|
2907
|
+
[top, top + height / 2, top + height],
|
|
2908
|
+
threshold
|
|
2909
|
+
);
|
|
2910
|
+
if (snapX) dx += snapX.offset;
|
|
2911
|
+
if (snapY) dy += snapY.offset;
|
|
2912
|
+
const snapped = { left: box.left + dx, top: box.top + dy, width, height };
|
|
2913
|
+
if (snapX) guides = guides.concat(guideSegments("x", snapX.lines, snapped));
|
|
2914
|
+
if (snapY) guides = guides.concat(guideSegments("y", snapY.lines, snapped));
|
|
2915
|
+
}
|
|
2916
|
+
let changed = false;
|
|
2917
|
+
interaction.nodeIds.forEach((id) => {
|
|
2918
|
+
const node = this.findNode(id);
|
|
2919
|
+
const origin = interaction.origins.get(id);
|
|
2920
|
+
if (!node || !origin) return;
|
|
2921
|
+
const nextX = formatNumber(origin.x + dx);
|
|
2922
|
+
const nextY = formatNumber(origin.y + dy);
|
|
2923
|
+
if (nextX === node.x && nextY === node.y) return;
|
|
2924
|
+
node.x = nextX;
|
|
2925
|
+
node.y = nextY;
|
|
2926
|
+
changed = true;
|
|
2927
|
+
this.renderDraggedNode(node);
|
|
2928
|
+
});
|
|
2929
|
+
if (changed) interaction.moved = true;
|
|
2930
|
+
this.renderGuides(guides);
|
|
2931
|
+
}
|
|
2932
|
+
// Live marquee: select every visible node the box touches, added to the
|
|
2933
|
+
// selection the gesture started from.
|
|
2934
|
+
updateMarquee(event) {
|
|
2935
|
+
const interaction = this.interaction;
|
|
2936
|
+
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2937
|
+
const box = {
|
|
2938
|
+
left: Math.min(interaction.startWorld.x, world.x),
|
|
2939
|
+
top: Math.min(interaction.startWorld.y, world.y),
|
|
2940
|
+
right: Math.max(interaction.startWorld.x, world.x),
|
|
2941
|
+
bottom: Math.max(interaction.startWorld.y, world.y)
|
|
2942
|
+
};
|
|
2943
|
+
clearChildren(this.guidesLayer);
|
|
2944
|
+
this.guidesLayer.appendChild(
|
|
2945
|
+
svgEl("rect", {
|
|
2946
|
+
class: "vd-flowchart-marquee",
|
|
2947
|
+
x: formatNumber(box.left),
|
|
2948
|
+
y: formatNumber(box.top),
|
|
2949
|
+
width: formatNumber(box.right - box.left),
|
|
2950
|
+
height: formatNumber(box.bottom - box.top)
|
|
2951
|
+
})
|
|
2952
|
+
);
|
|
2953
|
+
const hits = this.getVisibleNodes().filter(
|
|
2954
|
+
(node) => node.x <= box.right && node.x + node.width >= box.left && node.y <= box.bottom && node.y + node.height >= box.top
|
|
2955
|
+
).map((node) => node.id);
|
|
2956
|
+
const ids = [.../* @__PURE__ */ new Set([...interaction.baseIds, ...hits])];
|
|
2957
|
+
const key = ids.join(",");
|
|
2958
|
+
if (key === interaction.lastKey) return;
|
|
2959
|
+
interaction.lastKey = key;
|
|
2960
|
+
this.selectNodes(ids, { primary: hits[hits.length - 1] });
|
|
2961
|
+
}
|
|
2962
|
+
renderGuides(guides) {
|
|
2963
|
+
clearChildren(this.guidesLayer);
|
|
2964
|
+
guides.forEach((guide) => {
|
|
2965
|
+
this.guidesLayer.appendChild(
|
|
2966
|
+
svgEl("line", {
|
|
2967
|
+
class: "vd-flowchart-guide",
|
|
2968
|
+
x1: guide.x1,
|
|
2969
|
+
y1: guide.y1,
|
|
2970
|
+
x2: guide.x2,
|
|
2971
|
+
y2: guide.y2
|
|
2972
|
+
})
|
|
2973
|
+
);
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2027
2976
|
handlePointerMove(event) {
|
|
2028
2977
|
if (!this.interaction || this.interaction.pointerId !== event.pointerId) return;
|
|
2029
2978
|
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
|
-
}
|
|
2979
|
+
this.moveDraggedNodes(event);
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
if (this.interaction.kind === "marquee") {
|
|
2983
|
+
this.updateMarquee(event);
|
|
2041
2984
|
return;
|
|
2042
2985
|
}
|
|
2043
2986
|
if (this.interaction.kind === "resize-node") {
|
|
@@ -2072,6 +3015,14 @@ var VdFlowchart = class {
|
|
|
2072
3015
|
return;
|
|
2073
3016
|
}
|
|
2074
3017
|
if (this.interaction.kind === "connect") {
|
|
3018
|
+
if (!this.interaction.moved) {
|
|
3019
|
+
const travel = Math.hypot(
|
|
3020
|
+
event.clientX - this.interaction.startClientX,
|
|
3021
|
+
event.clientY - this.interaction.startClientY
|
|
3022
|
+
);
|
|
3023
|
+
if (!(travel > PORT_CLICK_SLOP)) return;
|
|
3024
|
+
this.interaction.moved = true;
|
|
3025
|
+
}
|
|
2075
3026
|
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2076
3027
|
const snapTarget = this.findConnectionTarget(
|
|
2077
3028
|
world,
|
|
@@ -2158,10 +3109,19 @@ var VdFlowchart = class {
|
|
|
2158
3109
|
}
|
|
2159
3110
|
return;
|
|
2160
3111
|
}
|
|
3112
|
+
if (interaction.kind === "marquee") {
|
|
3113
|
+
clearChildren(this.guidesLayer);
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
2161
3116
|
if (interaction.kind === "drag-node") {
|
|
3117
|
+
clearChildren(this.guidesLayer);
|
|
2162
3118
|
if (interaction.moved) {
|
|
2163
3119
|
this.render({ inspector: true, json: true });
|
|
2164
|
-
|
|
3120
|
+
if (interaction.nodeIds.length > 1) {
|
|
3121
|
+
this.emitChange("nodes:move", { nodeIds: [...interaction.nodeIds] });
|
|
3122
|
+
} else {
|
|
3123
|
+
this.emitChange("node:move", { node: deepClone(this.findNode(interaction.nodeId)) });
|
|
3124
|
+
}
|
|
2165
3125
|
} else {
|
|
2166
3126
|
this.render({ inspector: false, json: false });
|
|
2167
3127
|
}
|
|
@@ -2177,6 +3137,14 @@ var VdFlowchart = class {
|
|
|
2177
3137
|
return;
|
|
2178
3138
|
}
|
|
2179
3139
|
if (interaction.kind === "connect") {
|
|
3140
|
+
if (!interaction.moved && this.activeTool !== "arrow") {
|
|
3141
|
+
this.render({ inspector: false, json: false });
|
|
3142
|
+
this.insertBranchNode(interaction.source.nodeId, {
|
|
3143
|
+
direction: PORT_DIRECTIONS[interaction.source.port],
|
|
3144
|
+
edit: true
|
|
3145
|
+
});
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
2180
3148
|
const world = this.clientToWorld(event.clientX || 0, event.clientY || 0);
|
|
2181
3149
|
const snapTarget = interaction.target ? { node: this.findNode(interaction.target.nodeId), port: interaction.target.port } : this.findConnectionTarget(
|
|
2182
3150
|
world,
|
|
@@ -2252,16 +3220,17 @@ var VdFlowchart = class {
|
|
|
2252
3220
|
if (this.readonly) return false;
|
|
2253
3221
|
if (this.interaction?.kind === "connect" || this.interaction?.kind === "reconnect") return true;
|
|
2254
3222
|
if (this.activeTool === "arrow") return true;
|
|
2255
|
-
return this.selection?.kind === "node" && this.selection.id === node.id;
|
|
3223
|
+
return this.selection?.kind === "node" && this.selection.id === node.id && !this.hasMultiSelection();
|
|
2256
3224
|
}
|
|
2257
|
-
getMarkerId(markerType, position, strokeWidth) {
|
|
3225
|
+
getMarkerId(markerType, position, strokeWidth, edgeScale = 1) {
|
|
2258
3226
|
const width = normalizeEdgeStrokeWidth(strokeWidth);
|
|
2259
|
-
const
|
|
3227
|
+
const scale = clamp(formatNumber(Math.max(edgeScale, 0.01)), 0.01, 1);
|
|
3228
|
+
const key = `${markerType}:${position}:${width}:${scale}`;
|
|
2260
3229
|
if (this.markerIds.has(key)) {
|
|
2261
3230
|
return this.markerIds.get(key);
|
|
2262
3231
|
}
|
|
2263
3232
|
const markerId = nextId(`flowchart-${markerType}-${position}`);
|
|
2264
|
-
const marker = markerType === "dot" ? createDotMarker(markerId, width) : createArrowMarker(markerId, width, position === "start");
|
|
3233
|
+
const marker = markerType === "dot" ? createDotMarker(markerId, width, scale) : createArrowMarker(markerId, width, position === "start", scale);
|
|
2265
3234
|
this.markerDefs.appendChild(marker);
|
|
2266
3235
|
this.markerIds.set(key, markerId);
|
|
2267
3236
|
return markerId;
|
|
@@ -2269,13 +3238,20 @@ var VdFlowchart = class {
|
|
|
2269
3238
|
getEdgeStrokeWidth(edge) {
|
|
2270
3239
|
return normalizeEdgeStrokeWidth(edge?.strokeWidth);
|
|
2271
3240
|
}
|
|
2272
|
-
getEdgeHitStrokeWidth(edge) {
|
|
2273
|
-
|
|
3241
|
+
getEdgeHitStrokeWidth(edge, edgeScale = 1) {
|
|
3242
|
+
const visual = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
|
|
3243
|
+
const base = this.getEdgeStrokeWidth(edge);
|
|
3244
|
+
const view = Math.max(this.documentData?.viewport?.scale || 1, 1e-4);
|
|
3245
|
+
const legacy = Math.max(EDGE_HIT_STROKE_MIN, base + 12);
|
|
3246
|
+
if (visual >= 0.999 && view >= DEFAULT_MIN_SCALE && view <= DEFAULT_MAX_SCALE) {
|
|
3247
|
+
return formatNumber(legacy);
|
|
3248
|
+
}
|
|
3249
|
+
return formatNumber(Math.max(base * visual, EDGE_HIT_STROKE_MIN / view));
|
|
2274
3250
|
}
|
|
2275
3251
|
findConnectionTarget(worldPoint, excludeNodeId, previousSnap = null, referencePoint = null) {
|
|
2276
3252
|
const snapPadding = CONNECTION_SNAP_PADDING / this.documentData.viewport.scale;
|
|
2277
3253
|
const hysteresisMargin = CONNECTION_HYSTERESIS / this.documentData.viewport.scale;
|
|
2278
|
-
const best = this.
|
|
3254
|
+
const best = this.getVisibleNodes().reduce((candidate, node) => {
|
|
2279
3255
|
if (node.id === excludeNodeId) return candidate;
|
|
2280
3256
|
const distanceToBounds2 = getDistanceToNodeBounds(node, worldPoint);
|
|
2281
3257
|
if (distanceToBounds2 > snapPadding) return candidate;
|
|
@@ -2327,17 +3303,37 @@ var VdFlowchart = class {
|
|
|
2327
3303
|
let width = original.width;
|
|
2328
3304
|
let height = original.height;
|
|
2329
3305
|
if (handle.includes("e")) {
|
|
2330
|
-
width = clampNodeWidth(
|
|
3306
|
+
width = clampNodeWidth(
|
|
3307
|
+
nextType,
|
|
3308
|
+
original.width + deltaX,
|
|
3309
|
+
original.width,
|
|
3310
|
+
this.nodeSizeBounds()
|
|
3311
|
+
);
|
|
2331
3312
|
}
|
|
2332
3313
|
if (handle.includes("s")) {
|
|
2333
|
-
height = clampNodeHeight(
|
|
3314
|
+
height = clampNodeHeight(
|
|
3315
|
+
nextType,
|
|
3316
|
+
original.height + deltaY,
|
|
3317
|
+
original.height,
|
|
3318
|
+
this.nodeSizeBounds()
|
|
3319
|
+
);
|
|
2334
3320
|
}
|
|
2335
3321
|
if (handle.includes("w")) {
|
|
2336
|
-
width = clampNodeWidth(
|
|
3322
|
+
width = clampNodeWidth(
|
|
3323
|
+
nextType,
|
|
3324
|
+
original.width - deltaX,
|
|
3325
|
+
original.width,
|
|
3326
|
+
this.nodeSizeBounds()
|
|
3327
|
+
);
|
|
2337
3328
|
x = original.x + original.width - width;
|
|
2338
3329
|
}
|
|
2339
3330
|
if (handle.includes("n")) {
|
|
2340
|
-
height = clampNodeHeight(
|
|
3331
|
+
height = clampNodeHeight(
|
|
3332
|
+
nextType,
|
|
3333
|
+
original.height - deltaY,
|
|
3334
|
+
original.height,
|
|
3335
|
+
this.nodeSizeBounds()
|
|
3336
|
+
);
|
|
2341
3337
|
y = original.y + original.height - height;
|
|
2342
3338
|
}
|
|
2343
3339
|
return {
|
|
@@ -2373,7 +3369,7 @@ var VdFlowchart = class {
|
|
|
2373
3369
|
scaleAround(factor, localX, localY, reason) {
|
|
2374
3370
|
const viewport = this.documentData.viewport;
|
|
2375
3371
|
const anchor = this.localToWorld(localX, localY);
|
|
2376
|
-
const nextScale = clamp(
|
|
3372
|
+
const nextScale = clamp(formatScale(viewport.scale * factor), this.minScale, this.maxScale);
|
|
2377
3373
|
if (nextScale === viewport.scale) return;
|
|
2378
3374
|
viewport.scale = nextScale;
|
|
2379
3375
|
viewport.x = formatNumber(localX - anchor.x * nextScale);
|
|
@@ -2384,7 +3380,10 @@ var VdFlowchart = class {
|
|
|
2384
3380
|
updateToolbarLabel() {
|
|
2385
3381
|
this.zoomLabel.textContent = `${Math.round(this.documentData.viewport.scale * 100)}%`;
|
|
2386
3382
|
}
|
|
2387
|
-
|
|
3383
|
+
// Options: `initialText` replaces the label with typed text (type-to-edit);
|
|
3384
|
+
// `inserted` marks a node created by a keyboard/handle insertion so naming it
|
|
3385
|
+
// shares the insertion's undo step.
|
|
3386
|
+
startTextEdit(nodeId, options = {}) {
|
|
2388
3387
|
if (this.readonly) return false;
|
|
2389
3388
|
const node = this.findNode(nodeId);
|
|
2390
3389
|
if (!node || !isNodeTextEditable(node)) return false;
|
|
@@ -2396,9 +3395,10 @@ var VdFlowchart = class {
|
|
|
2396
3395
|
}
|
|
2397
3396
|
this.stopTextEdit({ commit: true });
|
|
2398
3397
|
this.select({ kind: "node", id: node.id });
|
|
3398
|
+
const replacing = typeof options.initialText === "string";
|
|
2399
3399
|
const textarea = createElement("textarea", {
|
|
2400
3400
|
className: `vd-flowchart-text-editor vd-flowchart-text-editor--${node.type}`,
|
|
2401
|
-
value: node.text,
|
|
3401
|
+
value: replacing ? options.initialText : node.text,
|
|
2402
3402
|
rows: 1
|
|
2403
3403
|
});
|
|
2404
3404
|
textarea.setAttribute("data-node-id", node.id);
|
|
@@ -2407,19 +3407,7 @@ var VdFlowchart = class {
|
|
|
2407
3407
|
textarea.addEventListener("input", () => this.positionTextEditor());
|
|
2408
3408
|
textarea.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
2409
3409
|
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
|
-
});
|
|
3410
|
+
textarea.addEventListener("keydown", (event) => this.handleTextEditorKeyDown(event, node));
|
|
2423
3411
|
textarea.addEventListener("blur", () => {
|
|
2424
3412
|
if (this.textEditor?.textarea === textarea) {
|
|
2425
3413
|
this.stopTextEdit({ commit: true });
|
|
@@ -2428,18 +3416,97 @@ var VdFlowchart = class {
|
|
|
2428
3416
|
this.textEditor = {
|
|
2429
3417
|
nodeId: node.id,
|
|
2430
3418
|
previousText: node.text,
|
|
3419
|
+
inserted: Boolean(options.inserted),
|
|
2431
3420
|
textarea
|
|
2432
3421
|
};
|
|
2433
3422
|
this.canvasEl.appendChild(textarea);
|
|
2434
3423
|
this.render({ inspector: true, json: false });
|
|
2435
|
-
|
|
2436
|
-
if (this.textEditor?.textarea
|
|
2437
|
-
|
|
3424
|
+
const focusEditor = () => {
|
|
3425
|
+
if (this.textEditor?.textarea !== textarea) return;
|
|
3426
|
+
textarea.focus();
|
|
3427
|
+
if (replacing) {
|
|
3428
|
+
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
3429
|
+
} else {
|
|
2438
3430
|
textarea.select();
|
|
2439
3431
|
}
|
|
3432
|
+
};
|
|
3433
|
+
focusEditor();
|
|
3434
|
+
window.requestAnimationFrame(() => {
|
|
3435
|
+
if (document.activeElement !== textarea) focusEditor();
|
|
3436
|
+
});
|
|
3437
|
+
return true;
|
|
3438
|
+
}
|
|
3439
|
+
// Inline label editor for a connection, placed on the label anchor
|
|
3440
|
+
// (curve midpoint). Labels render on one line, so Enter always saves.
|
|
3441
|
+
startEdgeLabelEdit(edgeId, options = {}) {
|
|
3442
|
+
if (this.readonly) return false;
|
|
3443
|
+
const edge = this.findEdge(sanitizeId(edgeId));
|
|
3444
|
+
if (!edge) return false;
|
|
3445
|
+
this.stopTextEdit({ commit: true });
|
|
3446
|
+
this.select({ kind: "edge", id: edge.id });
|
|
3447
|
+
const replacing = typeof options.initialText === "string";
|
|
3448
|
+
const textarea = createElement("textarea", {
|
|
3449
|
+
className: "vd-flowchart-text-editor vd-flowchart-text-editor--edge",
|
|
3450
|
+
value: replacing ? options.initialText : edge.label,
|
|
3451
|
+
rows: 1
|
|
3452
|
+
});
|
|
3453
|
+
textarea.setAttribute("data-edge-id", edge.id);
|
|
3454
|
+
textarea.setAttribute("aria-label", "Edit connection label");
|
|
3455
|
+
textarea.spellcheck = false;
|
|
3456
|
+
textarea.addEventListener("input", () => this.positionTextEditor());
|
|
3457
|
+
textarea.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
3458
|
+
textarea.addEventListener("dblclick", (event) => event.stopPropagation());
|
|
3459
|
+
textarea.addEventListener("keydown", (event) => {
|
|
3460
|
+
if (event.isComposing) return;
|
|
3461
|
+
if (event.key === "Escape" || event.key === "Enter") {
|
|
3462
|
+
event.preventDefault();
|
|
3463
|
+
this.stopTextEdit({ commit: event.key === "Enter" });
|
|
3464
|
+
this.canvasEl.focus();
|
|
3465
|
+
}
|
|
3466
|
+
});
|
|
3467
|
+
textarea.addEventListener("blur", () => {
|
|
3468
|
+
if (this.textEditor?.textarea === textarea) this.stopTextEdit({ commit: true });
|
|
3469
|
+
});
|
|
3470
|
+
this.textEditor = { nodeId: null, edgeId: edge.id, previousText: edge.label, textarea };
|
|
3471
|
+
this.canvasEl.appendChild(textarea);
|
|
3472
|
+
this.render({ inspector: true, json: false });
|
|
3473
|
+
const focusEditor = () => {
|
|
3474
|
+
if (this.textEditor?.textarea !== textarea) return;
|
|
3475
|
+
textarea.focus();
|
|
3476
|
+
if (replacing) textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
3477
|
+
else textarea.select();
|
|
3478
|
+
};
|
|
3479
|
+
focusEditor();
|
|
3480
|
+
window.requestAnimationFrame(() => {
|
|
3481
|
+
if (document.activeElement !== textarea) focusEditor();
|
|
2440
3482
|
});
|
|
2441
3483
|
return true;
|
|
2442
3484
|
}
|
|
3485
|
+
handleTextEditorKeyDown(event, node) {
|
|
3486
|
+
if (event.isComposing) return;
|
|
3487
|
+
const mindmap = this.keyboardShortcuts === "mindmap";
|
|
3488
|
+
if (event.key === "Escape") {
|
|
3489
|
+
event.preventDefault();
|
|
3490
|
+
this.stopTextEdit({ commit: false });
|
|
3491
|
+
this.canvasEl.focus();
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
if (event.key === "Enter") {
|
|
3495
|
+
const modKey = event.ctrlKey || event.metaKey;
|
|
3496
|
+
const commits = modKey || mindmap && node.type !== "textbox" && !event.shiftKey;
|
|
3497
|
+
if (!commits) return;
|
|
3498
|
+
event.preventDefault();
|
|
3499
|
+
this.stopTextEdit({ commit: true });
|
|
3500
|
+
this.canvasEl.focus();
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
if (event.key === "Tab" && mindmap && !event.shiftKey && !event.altKey && !event.ctrlKey) {
|
|
3504
|
+
event.preventDefault();
|
|
3505
|
+
this.stopTextEdit({ commit: true });
|
|
3506
|
+
this.canvasEl.focus();
|
|
3507
|
+
this.insertBranchNode(node.id, { edit: true });
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
2443
3510
|
stopTextEdit(options = {}) {
|
|
2444
3511
|
if (!this.textEditor) return false;
|
|
2445
3512
|
const commit = options.commit !== false;
|
|
@@ -2447,12 +3514,26 @@ var VdFlowchart = class {
|
|
|
2447
3514
|
const nextText = editor.textarea.value;
|
|
2448
3515
|
this.textEditor = null;
|
|
2449
3516
|
editor.textarea.remove();
|
|
3517
|
+
if (editor.edgeId) {
|
|
3518
|
+
const label = nextText.replace(/\s*\n\s*/g, " ").trim();
|
|
3519
|
+
if (commit && !this.readonly && label !== editor.previousText) {
|
|
3520
|
+
this.updateEdge(editor.edgeId, { label }, { inspector: true, reason: "edge:update" });
|
|
3521
|
+
} else {
|
|
3522
|
+
this.render({ scene: true, inspector: false, json: false });
|
|
3523
|
+
}
|
|
3524
|
+
return true;
|
|
3525
|
+
}
|
|
2450
3526
|
if (commit && !this.readonly && nextText !== editor.previousText) {
|
|
2451
|
-
this.
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
3527
|
+
this.coalesceInsertedText = editor.inserted;
|
|
3528
|
+
try {
|
|
3529
|
+
this.updateNode(
|
|
3530
|
+
editor.nodeId,
|
|
3531
|
+
{ text: nextText },
|
|
3532
|
+
{ inspector: true, reason: "node:update" }
|
|
3533
|
+
);
|
|
3534
|
+
} finally {
|
|
3535
|
+
this.coalesceInsertedText = false;
|
|
3536
|
+
}
|
|
2456
3537
|
} else {
|
|
2457
3538
|
this.render({ scene: true, inspector: false, json: false });
|
|
2458
3539
|
}
|
|
@@ -2460,6 +3541,10 @@ var VdFlowchart = class {
|
|
|
2460
3541
|
}
|
|
2461
3542
|
positionTextEditor() {
|
|
2462
3543
|
if (!this.textEditor) return;
|
|
3544
|
+
if (this.textEditor.edgeId) {
|
|
3545
|
+
this.positionEdgeLabelEditor();
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
2463
3548
|
const node = this.findNode(this.textEditor.nodeId);
|
|
2464
3549
|
if (!node) {
|
|
2465
3550
|
this.stopTextEdit({ commit: false });
|
|
@@ -2467,24 +3552,50 @@ var VdFlowchart = class {
|
|
|
2467
3552
|
}
|
|
2468
3553
|
const viewport = this.documentData.viewport;
|
|
2469
3554
|
const scale = viewport.scale;
|
|
3555
|
+
const visual = nodeVisualScale(node);
|
|
2470
3556
|
const textarea = this.textEditor.textarea;
|
|
2471
3557
|
const width = node.width * scale;
|
|
2472
3558
|
const height = node.height * scale;
|
|
2473
3559
|
const metrics = getNodeFontMetrics(node);
|
|
2474
|
-
const lineHeight = metrics.lineHeight * scale;
|
|
3560
|
+
const lineHeight = metrics.lineHeight * visual * scale;
|
|
2475
3561
|
const lineCount = Math.max(1, textarea.value.split(/\r?\n/).length);
|
|
2476
|
-
const horizontalPadding = (node.type === "textbox" ? 16 : 12) * scale;
|
|
2477
|
-
const verticalPadding = node.type === "textbox" ? 12 * scale : Math.max(4, (height - lineCount * lineHeight) / 2);
|
|
3562
|
+
const horizontalPadding = (node.type === "textbox" ? 16 : 12) * visual * scale;
|
|
3563
|
+
const verticalPadding = node.type === "textbox" ? 12 * visual * scale : Math.max(4 * visual, (height - lineCount * lineHeight) / 2);
|
|
2478
3564
|
textarea.style.left = `${formatNumber(viewport.x + node.x * scale)}px`;
|
|
2479
3565
|
textarea.style.top = `${formatNumber(viewport.y + node.y * scale)}px`;
|
|
2480
3566
|
textarea.style.width = `${formatNumber(width)}px`;
|
|
2481
3567
|
textarea.style.height = `${formatNumber(height)}px`;
|
|
2482
|
-
textarea.style.fontSize = `${formatNumber(metrics.fontSize * scale)}px`;
|
|
3568
|
+
textarea.style.fontSize = `${formatNumber(metrics.fontSize * visual * scale)}px`;
|
|
2483
3569
|
textarea.style.lineHeight = `${formatNumber(lineHeight)}px`;
|
|
2484
3570
|
textarea.style.padding = `${formatNumber(verticalPadding)}px ${formatNumber(horizontalPadding)}px`;
|
|
2485
3571
|
}
|
|
3572
|
+
positionEdgeLabelEditor() {
|
|
3573
|
+
const edge = this.findEdge(this.textEditor.edgeId);
|
|
3574
|
+
const fromNode = edge && this.findNode(edge.from.nodeId);
|
|
3575
|
+
const toNode = edge && this.findNode(edge.to.nodeId);
|
|
3576
|
+
if (!edge || !fromNode || !toNode) {
|
|
3577
|
+
this.stopTextEdit({ commit: false });
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
const visual = edgeVisualScale(fromNode, toNode);
|
|
3581
|
+
const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
|
|
3582
|
+
const viewport = this.documentData.viewport;
|
|
3583
|
+
const textarea = this.textEditor.textarea;
|
|
3584
|
+
const fontSize = Math.max(11 * visual, 13 * visual * viewport.scale);
|
|
3585
|
+
const width = clamp(textarea.value.length * fontSize * 0.62 + 28 * visual, 96 * visual, 280);
|
|
3586
|
+
const height = fontSize * 1.4 + 12 * visual;
|
|
3587
|
+
const labelOffset = 8 * visual;
|
|
3588
|
+
textarea.style.left = `${formatNumber(viewport.x + labelX * viewport.scale - width / 2)}px`;
|
|
3589
|
+
textarea.style.top = `${formatNumber(viewport.y + (labelY - labelOffset) * viewport.scale - height / 2)}px`;
|
|
3590
|
+
textarea.style.width = `${formatNumber(width)}px`;
|
|
3591
|
+
textarea.style.height = `${formatNumber(height)}px`;
|
|
3592
|
+
textarea.style.fontSize = `${formatNumber(fontSize)}px`;
|
|
3593
|
+
textarea.style.lineHeight = `${formatNumber(fontSize * 1.4)}px`;
|
|
3594
|
+
textarea.style.padding = "6px 10px";
|
|
3595
|
+
}
|
|
2486
3596
|
render(options = {}) {
|
|
2487
3597
|
if (this.destroyed) return;
|
|
3598
|
+
this.syncGrid();
|
|
2488
3599
|
const shouldRenderScene = options.scene !== false;
|
|
2489
3600
|
const shouldRenderInspector = options.inspector !== false;
|
|
2490
3601
|
const shouldRenderJson = options.json !== false;
|
|
@@ -2495,8 +3606,10 @@ var VdFlowchart = class {
|
|
|
2495
3606
|
this.updateToolbarLabel();
|
|
2496
3607
|
this.updatePaletteState();
|
|
2497
3608
|
this.positionTextEditor();
|
|
3609
|
+
this.scheduleMinimap();
|
|
2498
3610
|
}
|
|
2499
3611
|
renderScene() {
|
|
3612
|
+
this.syncSelectedNodeIds();
|
|
2500
3613
|
const viewport = this.documentData.viewport;
|
|
2501
3614
|
this.world.setAttribute(
|
|
2502
3615
|
"transform",
|
|
@@ -2506,7 +3619,14 @@ var VdFlowchart = class {
|
|
|
2506
3619
|
clearChildren(this.previewLayer);
|
|
2507
3620
|
clearChildren(this.nodesLayer);
|
|
2508
3621
|
clearChildren(this.overlayLayer);
|
|
2509
|
-
|
|
3622
|
+
clearChildren(this.markerDefs);
|
|
3623
|
+
this.markerIds = /* @__PURE__ */ new Map();
|
|
3624
|
+
const hidden = this.getHiddenNodeIds();
|
|
3625
|
+
this.sceneHidden = hidden;
|
|
3626
|
+
this.sceneOutgoing = this.getOutgoingMap();
|
|
3627
|
+
const nodeMap = new Map(
|
|
3628
|
+
this.documentData.nodes.filter((node) => !hidden.has(node.id)).map((node) => [node.id, node])
|
|
3629
|
+
);
|
|
2510
3630
|
this.sceneNodeMap = nodeMap;
|
|
2511
3631
|
this.nodeElements = /* @__PURE__ */ new Map();
|
|
2512
3632
|
this.edgeElements = /* @__PURE__ */ new Map();
|
|
@@ -2526,6 +3646,7 @@ var VdFlowchart = class {
|
|
|
2526
3646
|
this.previewLayer.appendChild(this.renderPreviewEdge(this.interaction));
|
|
2527
3647
|
}
|
|
2528
3648
|
this.documentData.nodes.forEach((node) => {
|
|
3649
|
+
if (hidden.has(node.id)) return;
|
|
2529
3650
|
const element = this.renderNode(node);
|
|
2530
3651
|
this.nodesLayer.appendChild(element);
|
|
2531
3652
|
this.nodeElements.set(node.id, element);
|
|
@@ -2563,17 +3684,18 @@ var VdFlowchart = class {
|
|
|
2563
3684
|
}
|
|
2564
3685
|
}
|
|
2565
3686
|
}
|
|
2566
|
-
applyEdgeMarkers(pathElement, edge) {
|
|
3687
|
+
applyEdgeMarkers(pathElement, edge, edgeScale = 1) {
|
|
3688
|
+
const width = this.getEdgeStrokeWidth(edge);
|
|
2567
3689
|
if (edge.startMarker && edge.startMarker !== "none") {
|
|
2568
3690
|
pathElement.setAttribute(
|
|
2569
3691
|
"marker-start",
|
|
2570
|
-
`url(#${this.getMarkerId(edge.startMarker, "start",
|
|
3692
|
+
`url(#${this.getMarkerId(edge.startMarker, "start", width, edgeScale)})`
|
|
2571
3693
|
);
|
|
2572
3694
|
}
|
|
2573
3695
|
if (edge.endMarker && edge.endMarker !== "none") {
|
|
2574
3696
|
pathElement.setAttribute(
|
|
2575
3697
|
"marker-end",
|
|
2576
|
-
`url(#${this.getMarkerId(edge.endMarker, "end",
|
|
3698
|
+
`url(#${this.getMarkerId(edge.endMarker, "end", width, edgeScale)})`
|
|
2577
3699
|
);
|
|
2578
3700
|
}
|
|
2579
3701
|
}
|
|
@@ -2581,6 +3703,7 @@ var VdFlowchart = class {
|
|
|
2581
3703
|
const fromNode = nodeMap.get(edge.from.nodeId);
|
|
2582
3704
|
const toNode = nodeMap.get(edge.to.nodeId);
|
|
2583
3705
|
if (!fromNode || !toNode) return null;
|
|
3706
|
+
const visual = edgeVisualScale(fromNode, toNode);
|
|
2584
3707
|
const edgePath = buildEdgePath(edge, fromNode, toNode);
|
|
2585
3708
|
const selected = this.selection?.kind === "edge" && this.selection.id === edge.id;
|
|
2586
3709
|
const inReconnectMode = this.reconnectEdgeId === edge.id;
|
|
@@ -2588,13 +3711,14 @@ var VdFlowchart = class {
|
|
|
2588
3711
|
class: `vd-flowchart-edge${selected ? " is-selected" : ""}${inReconnectMode ? " is-reconnecting" : ""}`,
|
|
2589
3712
|
"data-edge-id": edge.id
|
|
2590
3713
|
});
|
|
2591
|
-
const
|
|
3714
|
+
const baseStroke = this.getEdgeStrokeWidth(edge);
|
|
3715
|
+
const strokeWidth = visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke;
|
|
2592
3716
|
if (selected) {
|
|
2593
3717
|
group.appendChild(
|
|
2594
3718
|
svgEl("path", {
|
|
2595
3719
|
class: "vd-flowchart-edge-selection",
|
|
2596
3720
|
d: edgePath.d,
|
|
2597
|
-
"stroke-width": formatNumber(strokeWidth + 7)
|
|
3721
|
+
"stroke-width": visual < 0.999 ? formatNumber((baseStroke + 7) * visual) : formatNumber(strokeWidth + 7)
|
|
2598
3722
|
})
|
|
2599
3723
|
);
|
|
2600
3724
|
}
|
|
@@ -2604,23 +3728,28 @@ var VdFlowchart = class {
|
|
|
2604
3728
|
"stroke-width": strokeWidth,
|
|
2605
3729
|
"data-edge-id": edge.id
|
|
2606
3730
|
});
|
|
2607
|
-
this.applyEdgeMarkers(visiblePath, edge);
|
|
3731
|
+
this.applyEdgeMarkers(visiblePath, edge, visual);
|
|
2608
3732
|
const hitPath = svgEl("path", {
|
|
2609
3733
|
class: "vd-flowchart-edge-hit",
|
|
2610
3734
|
d: edgePath.d,
|
|
2611
|
-
"stroke-width": this.getEdgeHitStrokeWidth(edge),
|
|
3735
|
+
"stroke-width": this.getEdgeHitStrokeWidth(edge, visual),
|
|
2612
3736
|
"data-edge-id": edge.id
|
|
2613
3737
|
});
|
|
2614
3738
|
group.appendChild(visiblePath);
|
|
2615
3739
|
group.appendChild(hitPath);
|
|
2616
|
-
if (edge.label) {
|
|
3740
|
+
if (edge.label && this.textEditor?.edgeId !== edge.id) {
|
|
3741
|
+
const labelOffset = visual < 0.999 ? formatNumber(8 * visual) : 8;
|
|
2617
3742
|
const label = svgEl("text", {
|
|
2618
3743
|
class: "vd-flowchart-edge-label",
|
|
2619
3744
|
x: edgePath.labelX,
|
|
2620
|
-
y: edgePath.labelY -
|
|
3745
|
+
y: edgePath.labelY - labelOffset,
|
|
2621
3746
|
"text-anchor": "middle",
|
|
2622
3747
|
"data-edge-id": edge.id
|
|
2623
3748
|
});
|
|
3749
|
+
if (visual < 0.999) {
|
|
3750
|
+
label.style.fontSize = `${formatNumber(Math.max(0.06, 12 * visual))}px`;
|
|
3751
|
+
label.style.strokeWidth = `${formatNumber(Math.max(0.03, 5 * visual))}px`;
|
|
3752
|
+
}
|
|
2624
3753
|
label.textContent = edge.label;
|
|
2625
3754
|
group.appendChild(label);
|
|
2626
3755
|
}
|
|
@@ -2659,23 +3788,32 @@ var VdFlowchart = class {
|
|
|
2659
3788
|
renderPreviewEdge(interaction) {
|
|
2660
3789
|
const fromPort = interaction.fromPort || interaction.source?.port || "right";
|
|
2661
3790
|
const toPort = interaction.toPort || interaction.target?.port || "left";
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
3791
|
+
const fromNode = interaction.source?.nodeId ? this.findNode(interaction.source.nodeId) : null;
|
|
3792
|
+
const toNode = interaction.target?.nodeId ? this.findNode(interaction.target.nodeId) : null;
|
|
3793
|
+
const visual = edgeVisualScale(fromNode, toNode);
|
|
3794
|
+
const edgePath = buildEdgePath(
|
|
3795
|
+
{
|
|
3796
|
+
strokeWidth: interaction.strokeWidth,
|
|
3797
|
+
route: interaction.route,
|
|
3798
|
+
fromPoint: interaction.fromPoint,
|
|
3799
|
+
toPoint: interaction.toPoint,
|
|
3800
|
+
from: { port: fromPort },
|
|
3801
|
+
to: { port: toPort }
|
|
3802
|
+
},
|
|
3803
|
+
fromNode,
|
|
3804
|
+
toNode
|
|
3805
|
+
);
|
|
3806
|
+
const baseStroke = this.getEdgeStrokeWidth(interaction);
|
|
2670
3807
|
return svgEl("path", {
|
|
2671
3808
|
class: "vd-flowchart-preview-path",
|
|
2672
3809
|
d: edgePath.d,
|
|
2673
|
-
"stroke-width":
|
|
3810
|
+
"stroke-width": visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke
|
|
2674
3811
|
});
|
|
2675
3812
|
}
|
|
2676
3813
|
renderNode(node) {
|
|
2677
|
-
const selected = this.
|
|
2678
|
-
const
|
|
3814
|
+
const selected = this.isNodeSelected(node.id);
|
|
3815
|
+
const soloSelected = selected && !this.hasMultiSelection();
|
|
3816
|
+
const dragging = this.interaction?.kind === "drag-node" && this.interaction.nodeIds.includes(node.id);
|
|
2679
3817
|
const resizing = this.interaction?.kind === "resize-node" && this.interaction.nodeId === node.id;
|
|
2680
3818
|
const editing = this.textEditor?.nodeId === node.id;
|
|
2681
3819
|
const group = svgEl("g", {
|
|
@@ -2684,24 +3822,45 @@ var VdFlowchart = class {
|
|
|
2684
3822
|
"data-node-id": node.id
|
|
2685
3823
|
});
|
|
2686
3824
|
const portsVisible = this.shouldShowNodePorts(node);
|
|
3825
|
+
const visual = nodeVisualScale(node);
|
|
3826
|
+
const corner = node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6;
|
|
2687
3827
|
const hitbox = svgEl("rect", {
|
|
2688
3828
|
x: 0,
|
|
2689
3829
|
y: 0,
|
|
2690
3830
|
width: node.width,
|
|
2691
3831
|
height: node.height,
|
|
2692
|
-
rx:
|
|
3832
|
+
rx: visual < 0.999 && node.type !== "junction" ? formatNumber(corner * visual) : corner,
|
|
2693
3833
|
fill: "transparent",
|
|
2694
3834
|
"data-node-id": node.id
|
|
2695
3835
|
});
|
|
2696
3836
|
group.appendChild(hitbox);
|
|
2697
|
-
|
|
3837
|
+
const shape = this.renderNodeShape(node);
|
|
3838
|
+
this.applyScaledNodeShape(node, shape);
|
|
3839
|
+
group.appendChild(shape);
|
|
2698
3840
|
group.appendChild(this.renderNodeText(node));
|
|
2699
|
-
|
|
3841
|
+
const scale = this.documentData.viewport.scale || 1;
|
|
3842
|
+
if (soloSelected && !this.readonly && SELECTION_BOX_TYPES.has(node.type)) {
|
|
3843
|
+
const outset = SELECTION_OUTSET / scale;
|
|
3844
|
+
group.appendChild(
|
|
3845
|
+
svgEl("rect", {
|
|
3846
|
+
class: "vd-flowchart-selection-box",
|
|
3847
|
+
x: -outset,
|
|
3848
|
+
y: -outset,
|
|
3849
|
+
width: node.width + outset * 2,
|
|
3850
|
+
height: node.height + outset * 2
|
|
3851
|
+
})
|
|
3852
|
+
);
|
|
3853
|
+
}
|
|
3854
|
+
if (soloSelected && !this.readonly && isNodeResizable(node)) {
|
|
2700
3855
|
group.appendChild(this.renderResizeControls(node));
|
|
2701
3856
|
}
|
|
2702
|
-
const
|
|
3857
|
+
const collapseToggle = this.renderCollapseToggle(node, soloSelected);
|
|
3858
|
+
if (this.readonly) {
|
|
3859
|
+
if (collapseToggle) group.appendChild(collapseToggle);
|
|
3860
|
+
return group;
|
|
3861
|
+
}
|
|
2703
3862
|
FLOWCHART_PORTS.forEach((port) => {
|
|
2704
|
-
const position =
|
|
3863
|
+
const position = getPortHandlePosition({ ...node, x: 0, y: 0 }, port, scale);
|
|
2705
3864
|
const portGroup = svgEl("g", {
|
|
2706
3865
|
class: `vd-flowchart-port-group${portsVisible ? " is-visible" : ""}`,
|
|
2707
3866
|
"data-node-id": node.id,
|
|
@@ -2723,15 +3882,68 @@ var VdFlowchart = class {
|
|
|
2723
3882
|
r: CONNECTION_PORT_RADIUS / scale
|
|
2724
3883
|
})
|
|
2725
3884
|
);
|
|
3885
|
+
const arm = (CONNECTION_PORT_RADIUS - 2.5) / scale;
|
|
3886
|
+
portGroup.appendChild(
|
|
3887
|
+
svgEl("path", {
|
|
3888
|
+
class: "vd-flowchart-port-plus",
|
|
3889
|
+
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)}`
|
|
3890
|
+
})
|
|
3891
|
+
);
|
|
2726
3892
|
group.appendChild(portGroup);
|
|
2727
3893
|
});
|
|
3894
|
+
if (collapseToggle) group.appendChild(collapseToggle);
|
|
3895
|
+
return group;
|
|
3896
|
+
}
|
|
3897
|
+
// "+N" on a collapsed node, or "−" on a single selected node with children,
|
|
3898
|
+
// placed beyond the connection handle on the side its branch leaves from.
|
|
3899
|
+
renderCollapseToggle(node, soloSelected) {
|
|
3900
|
+
const outgoing = this.sceneOutgoing || this.getOutgoingMap();
|
|
3901
|
+
const childIds = (outgoing.get(node.id) || []).filter((id) => id !== node.id);
|
|
3902
|
+
if (!childIds.length) return null;
|
|
3903
|
+
const count = node.collapsed ? this.countCollapsedDescendants(
|
|
3904
|
+
node.id,
|
|
3905
|
+
this.sceneHidden || this.getHiddenNodeIds(),
|
|
3906
|
+
outgoing
|
|
3907
|
+
) : 0;
|
|
3908
|
+
if (!node.collapsed && !(soloSelected && !this.readonly)) return null;
|
|
3909
|
+
const sides = /* @__PURE__ */ new Map();
|
|
3910
|
+
this.documentData.edges.forEach((edge) => {
|
|
3911
|
+
if (edge.from.nodeId !== node.id || edge.to.nodeId === node.id) return;
|
|
3912
|
+
sides.set(edge.from.port, (sides.get(edge.from.port) || 0) + 1);
|
|
3913
|
+
});
|
|
3914
|
+
const port = [...sides].sort((a, b) => b[1] - a[1])[0]?.[0] || DIRECTION_PORTS[this.getGrowthDirection(node)];
|
|
3915
|
+
const scale = this.documentData.viewport.scale || 1;
|
|
3916
|
+
const anchor = getPortPosition({ ...node, x: 0, y: 0 }, port);
|
|
3917
|
+
const normal = getPortNormal(port);
|
|
3918
|
+
const offset = COLLAPSE_TOGGLE_OFFSET / scale;
|
|
3919
|
+
const cx = formatNumber(anchor.x + normal.x * offset);
|
|
3920
|
+
const cy = formatNumber(anchor.y + normal.y * offset);
|
|
3921
|
+
const label = node.collapsed ? `+${count}` : "\u2212";
|
|
3922
|
+
const radius = (node.collapsed && label.length > 2 ? 12 : 9) / scale;
|
|
3923
|
+
const group = svgEl("g", {
|
|
3924
|
+
class: `vd-flowchart-collapse-toggle${node.collapsed ? " is-collapsed" : ""}`,
|
|
3925
|
+
"data-collapse-toggle": node.id
|
|
3926
|
+
});
|
|
3927
|
+
const title = svgEl("title");
|
|
3928
|
+
title.textContent = node.collapsed ? `Expand ${count} hidden nodes` : "Collapse branch";
|
|
3929
|
+
group.appendChild(title);
|
|
3930
|
+
group.appendChild(svgEl("circle", { cx, cy, r: formatNumber(radius) }));
|
|
3931
|
+
const text = svgEl("text", {
|
|
3932
|
+
x: cx,
|
|
3933
|
+
y: cy,
|
|
3934
|
+
"font-size": formatNumber(10 / scale),
|
|
3935
|
+
"text-anchor": "middle",
|
|
3936
|
+
"dominant-baseline": "central"
|
|
3937
|
+
});
|
|
3938
|
+
text.textContent = label;
|
|
3939
|
+
group.appendChild(text);
|
|
2728
3940
|
return group;
|
|
2729
3941
|
}
|
|
2730
3942
|
renderResizeControls(node) {
|
|
2731
3943
|
const scale = this.documentData.viewport.scale || 1;
|
|
2732
3944
|
const zone = 14 / scale;
|
|
2733
3945
|
const cornerZone = 22 / scale;
|
|
2734
|
-
const handleSize =
|
|
3946
|
+
const handleSize = RESIZE_HANDLE_SIZE / scale;
|
|
2735
3947
|
const gap = RESIZE_PORT_GAP / scale;
|
|
2736
3948
|
const group = svgEl("g", { class: "vd-flowchart-resize-controls" });
|
|
2737
3949
|
const zones = {
|
|
@@ -2761,12 +3973,6 @@ var VdFlowchart = class {
|
|
|
2761
3973
|
};
|
|
2762
3974
|
RESIZE_HANDLES.forEach((handle) => {
|
|
2763
3975
|
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
3976
|
const cursor = getResizeCursor(handle);
|
|
2771
3977
|
const zoneSegments = [];
|
|
2772
3978
|
if (handle === "e" || handle === "w") {
|
|
@@ -2801,14 +4007,19 @@ var VdFlowchart = class {
|
|
|
2801
4007
|
});
|
|
2802
4008
|
group.appendChild(hit);
|
|
2803
4009
|
});
|
|
4010
|
+
if (handle.length === 1) return;
|
|
4011
|
+
const position = getResizeHandlePosition(node, handle);
|
|
4012
|
+
const outset = SELECTION_OUTSET / scale;
|
|
4013
|
+
position.x += handle.includes("e") ? outset : -outset;
|
|
4014
|
+
position.y += handle.includes("s") ? outset : -outset;
|
|
2804
4015
|
const dot = svgEl("rect", {
|
|
2805
4016
|
class: "vd-flowchart-resize-handle",
|
|
2806
4017
|
x: position.x - handleSize / 2,
|
|
2807
4018
|
y: position.y - handleSize / 2,
|
|
2808
4019
|
width: handleSize,
|
|
2809
4020
|
height: handleSize,
|
|
2810
|
-
rx: handleSize /
|
|
2811
|
-
ry: handleSize /
|
|
4021
|
+
rx: handleSize / 5,
|
|
4022
|
+
ry: handleSize / 5,
|
|
2812
4023
|
"data-node-id": node.id,
|
|
2813
4024
|
"data-resize-handle": handle,
|
|
2814
4025
|
style: `cursor: ${cursor}`
|
|
@@ -2817,6 +4028,21 @@ var VdFlowchart = class {
|
|
|
2817
4028
|
});
|
|
2818
4029
|
return group;
|
|
2819
4030
|
}
|
|
4031
|
+
applyScaledNodeShape(node, shape) {
|
|
4032
|
+
const visual = nodeVisualScale(node);
|
|
4033
|
+
if (!shape || visual >= 0.999) return;
|
|
4034
|
+
const baseStroke = node.type === "junction" ? 1.25 : 1.5;
|
|
4035
|
+
shape.style.strokeWidth = String(formatNumber(Math.max(0.03, baseStroke * visual)));
|
|
4036
|
+
if (node.type === "textbox") {
|
|
4037
|
+
shape.style.strokeDasharray = `${formatNumber(8 * visual)} ${formatNumber(5 * visual)}`;
|
|
4038
|
+
}
|
|
4039
|
+
const baseRadius = node.type === "rounded-rect" ? 18 : node.type === "textbox" ? 14 : node.type === "rect" ? 2 : 0;
|
|
4040
|
+
if (baseRadius > 0) {
|
|
4041
|
+
const scaled = String(formatNumber(baseRadius * visual));
|
|
4042
|
+
shape.setAttribute("rx", scaled);
|
|
4043
|
+
shape.setAttribute("ry", scaled);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
2820
4046
|
renderNodeShape(node) {
|
|
2821
4047
|
const baseClass = `vd-flowchart-node-shape vd-flowchart-node-shape--${node.type}`;
|
|
2822
4048
|
if (node.type === "rounded-rect") {
|
|
@@ -2893,21 +4119,28 @@ var VdFlowchart = class {
|
|
|
2893
4119
|
if (node.type === "junction" || !node.text) {
|
|
2894
4120
|
return textElement;
|
|
2895
4121
|
}
|
|
4122
|
+
const visual = nodeVisualScale(node);
|
|
4123
|
+
const metrics = getNodeFontMetrics(node);
|
|
4124
|
+
const lineHeight = visual < 0.999 ? Math.max(0.06, metrics.lineHeight * visual) : metrics.lineHeight;
|
|
4125
|
+
if (visual < 0.999) {
|
|
4126
|
+
textElement.style.fontSize = `${formatNumber(Math.max(0.05, metrics.fontSize * visual))}px`;
|
|
4127
|
+
}
|
|
4128
|
+
const equivalentWidth = visual < 0.999 ? node.width / visual : node.width;
|
|
2896
4129
|
if (node.type === "textbox") {
|
|
2897
|
-
const lines2 = wrapText(node.text, estimateChars(
|
|
2898
|
-
const
|
|
4130
|
+
const lines2 = wrapText(node.text, estimateChars(equivalentWidth - 32));
|
|
4131
|
+
const padX = visual < 0.999 ? formatNumber(16 * visual) : 16;
|
|
4132
|
+
const startY2 = visual < 0.999 ? formatNumber(28 * visual) : 28;
|
|
2899
4133
|
lines2.forEach((line, index) => {
|
|
2900
4134
|
const span = svgEl("tspan", {
|
|
2901
|
-
x:
|
|
2902
|
-
y:
|
|
4135
|
+
x: padX,
|
|
4136
|
+
y: visual < 0.999 ? formatNumber(startY2 + index * lineHeight) : startY2 + index * lineHeight
|
|
2903
4137
|
});
|
|
2904
4138
|
span.textContent = line;
|
|
2905
4139
|
textElement.appendChild(span);
|
|
2906
4140
|
});
|
|
2907
4141
|
return textElement;
|
|
2908
4142
|
}
|
|
2909
|
-
const lines = wrapText(node.text, estimateChars(
|
|
2910
|
-
const { lineHeight } = getNodeFontMetrics(node);
|
|
4143
|
+
const lines = wrapText(node.text, estimateChars(equivalentWidth));
|
|
2911
4144
|
const totalHeight = (lines.length - 1) * lineHeight;
|
|
2912
4145
|
const startY = node.height / 2 - totalHeight / 2;
|
|
2913
4146
|
textElement.setAttribute("text-anchor", "middle");
|
|
@@ -2915,7 +4148,7 @@ var VdFlowchart = class {
|
|
|
2915
4148
|
lines.forEach((line, index) => {
|
|
2916
4149
|
const span = svgEl("tspan", {
|
|
2917
4150
|
x: node.width / 2,
|
|
2918
|
-
y: startY + index * lineHeight,
|
|
4151
|
+
y: visual < 0.999 ? formatNumber(startY + index * lineHeight) : startY + index * lineHeight,
|
|
2919
4152
|
"text-anchor": "middle",
|
|
2920
4153
|
"dominant-baseline": "middle"
|
|
2921
4154
|
});
|
|
@@ -2937,6 +4170,16 @@ var VdFlowchart = class {
|
|
|
2937
4170
|
);
|
|
2938
4171
|
return;
|
|
2939
4172
|
}
|
|
4173
|
+
if (this.hasMultiSelection()) {
|
|
4174
|
+
this.selectionMeta.textContent = `${this.selectedNodeIds.size} nodes selected`;
|
|
4175
|
+
this.selectionFields.appendChild(
|
|
4176
|
+
createElement("p", {
|
|
4177
|
+
className: "vd-flowchart-selection-empty",
|
|
4178
|
+
text: "Drag, nudge with Alt+Arrow, copy, duplicate, or delete them together."
|
|
4179
|
+
})
|
|
4180
|
+
);
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
2940
4183
|
if (this.selection.kind === "node") {
|
|
2941
4184
|
const node = this.findNode(this.selection.id);
|
|
2942
4185
|
if (!node) {
|
|
@@ -2957,12 +4200,12 @@ var VdFlowchart = class {
|
|
|
2957
4200
|
textArea.setAttribute("data-field", "node-text");
|
|
2958
4201
|
const widthInput = createElement("input", { value: node.width, type: "number" });
|
|
2959
4202
|
widthInput.setAttribute("data-field", "node-width");
|
|
2960
|
-
widthInput.setAttribute("min", String(
|
|
2961
|
-
widthInput.setAttribute("max", String(
|
|
4203
|
+
widthInput.setAttribute("min", String(this.minNodeSize));
|
|
4204
|
+
widthInput.setAttribute("max", String(this.maxNodeSize));
|
|
2962
4205
|
const heightInput = createElement("input", { value: node.height, type: "number" });
|
|
2963
4206
|
heightInput.setAttribute("data-field", "node-height");
|
|
2964
|
-
heightInput.setAttribute("min", String(
|
|
2965
|
-
heightInput.setAttribute("max", String(
|
|
4207
|
+
heightInput.setAttribute("min", String(this.minNodeSize));
|
|
4208
|
+
heightInput.setAttribute("max", String(this.maxNodeSize));
|
|
2966
4209
|
this.selectionFields.appendChild(createField("Type", typeSelect));
|
|
2967
4210
|
if (isNodeTextEditable(node)) {
|
|
2968
4211
|
this.selectionFields.appendChild(createField("Text", textArea));
|
|
@@ -3051,16 +4294,69 @@ var VdFlowchart = class {
|
|
|
3051
4294
|
}
|
|
3052
4295
|
select(selection) {
|
|
3053
4296
|
const next = selection && selection.id && selection.kind ? { kind: selection.kind, id: selection.id } : null;
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
4297
|
+
this.applySelection(next, next?.kind === "node" ? [next.id] : []);
|
|
4298
|
+
}
|
|
4299
|
+
// Single funnel for selection changes: `selection` is the primary item and
|
|
4300
|
+
// `selectedNodeIds` every selected node (the primary one included).
|
|
4301
|
+
applySelection(primary, nodeIds) {
|
|
4302
|
+
const previousKey = this.getSelectionKey();
|
|
4303
|
+
this.selection = primary;
|
|
4304
|
+
this.selectedNodeIds = new Set(nodeIds);
|
|
4305
|
+
if (primary?.kind === "node") this.selectedNodeIds.add(primary.id);
|
|
4306
|
+
if (previousKey !== this.getSelectionKey()) {
|
|
4307
|
+
this.edgeCycleAnchor = null;
|
|
3058
4308
|
this.render({ scene: true, inspector: true, json: false });
|
|
3059
|
-
this.emit("select", {
|
|
4309
|
+
this.emit("select", {
|
|
4310
|
+
selection: this.getSelectionSnapshot(),
|
|
4311
|
+
nodeIds: this.getSelectedNodeIds()
|
|
4312
|
+
});
|
|
3060
4313
|
return;
|
|
3061
4314
|
}
|
|
3062
4315
|
this.render({ scene: true, inspector: false, json: false });
|
|
3063
4316
|
}
|
|
4317
|
+
getSelectionKey() {
|
|
4318
|
+
const primary = this.selection ? `${this.selection.kind}:${this.selection.id}` : "";
|
|
4319
|
+
return `${primary}|${[...this.selectedNodeIds || []].sort().join(",")}`;
|
|
4320
|
+
}
|
|
4321
|
+
// Keep the set valid after mutations that bypass applySelection (undo,
|
|
4322
|
+
// load, removal): drop missing nodes and keep the primary node in the set.
|
|
4323
|
+
syncSelectedNodeIds() {
|
|
4324
|
+
if (this.selection?.kind !== "node") {
|
|
4325
|
+
this.selectedNodeIds = /* @__PURE__ */ new Set();
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
const existing = new Set(this.documentData.nodes.map((node) => node.id));
|
|
4329
|
+
const hidden = this.getHiddenNodeIds();
|
|
4330
|
+
this.selectedNodeIds = new Set(
|
|
4331
|
+
[...this.selectedNodeIds || []].filter((id) => existing.has(id) && !hidden.has(id))
|
|
4332
|
+
);
|
|
4333
|
+
this.selectedNodeIds.add(this.selection.id);
|
|
4334
|
+
}
|
|
4335
|
+
getSelectedNodeIds() {
|
|
4336
|
+
return [...this.selectedNodeIds || []];
|
|
4337
|
+
}
|
|
4338
|
+
isNodeSelected(nodeId) {
|
|
4339
|
+
return Boolean(this.selectedNodeIds?.has(nodeId));
|
|
4340
|
+
}
|
|
4341
|
+
hasMultiSelection() {
|
|
4342
|
+
return (this.selectedNodeIds?.size || 0) > 1;
|
|
4343
|
+
}
|
|
4344
|
+
/** Select several nodes; `primary` (default: the last id) drives single-node UI. */
|
|
4345
|
+
selectNodes(nodeIds, options = {}) {
|
|
4346
|
+
const existing = new Set(this.documentData.nodes.map((node) => node.id));
|
|
4347
|
+
const ids = [...new Set((nodeIds || []).map(sanitizeId))].filter((id) => existing.has(id));
|
|
4348
|
+
const primaryId = ids.includes(options.primary) ? options.primary : ids[ids.length - 1];
|
|
4349
|
+
this.applySelection(primaryId ? { kind: "node", id: primaryId } : null, ids);
|
|
4350
|
+
return this;
|
|
4351
|
+
}
|
|
4352
|
+
toggleNodeSelection(nodeId) {
|
|
4353
|
+
const ids = this.selection?.kind === "node" ? this.getSelectedNodeIds() : [];
|
|
4354
|
+
if (ids.includes(nodeId)) {
|
|
4355
|
+
const rest = ids.filter((id) => id !== nodeId);
|
|
4356
|
+
return this.selectNodes(rest, { primary: rest[rest.length - 1] });
|
|
4357
|
+
}
|
|
4358
|
+
return this.selectNodes([...ids, nodeId], { primary: nodeId });
|
|
4359
|
+
}
|
|
3064
4360
|
selectNode(nodeId) {
|
|
3065
4361
|
this.select({ kind: "node", id: sanitizeId(nodeId) });
|
|
3066
4362
|
return this;
|
|
@@ -3081,6 +4377,69 @@ var VdFlowchart = class {
|
|
|
3081
4377
|
findNode(nodeId) {
|
|
3082
4378
|
return this.documentData.nodes.find((node) => node.id === nodeId) || null;
|
|
3083
4379
|
}
|
|
4380
|
+
getHiddenNodeIds() {
|
|
4381
|
+
return computeHiddenNodeIds(this.documentData.nodes, this.documentData.edges);
|
|
4382
|
+
}
|
|
4383
|
+
getVisibleNodes() {
|
|
4384
|
+
const hidden = this.getHiddenNodeIds();
|
|
4385
|
+
return hidden.size ? this.documentData.nodes.filter((node) => !hidden.has(node.id)) : this.documentData.nodes;
|
|
4386
|
+
}
|
|
4387
|
+
isNodeHidden(nodeId) {
|
|
4388
|
+
return this.getHiddenNodeIds().has(nodeId);
|
|
4389
|
+
}
|
|
4390
|
+
getOutgoingMap() {
|
|
4391
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
4392
|
+
this.documentData.edges.forEach((edge) => {
|
|
4393
|
+
if (!outgoing.has(edge.from.nodeId)) outgoing.set(edge.from.nodeId, []);
|
|
4394
|
+
outgoing.get(edge.from.nodeId).push(edge.to.nodeId);
|
|
4395
|
+
});
|
|
4396
|
+
return outgoing;
|
|
4397
|
+
}
|
|
4398
|
+
// Hidden nodes under one collapsed node (its "+N" count).
|
|
4399
|
+
countCollapsedDescendants(nodeId, hidden = this.getHiddenNodeIds(), outgoing = this.getOutgoingMap()) {
|
|
4400
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4401
|
+
const queue = [nodeId];
|
|
4402
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
4403
|
+
(outgoing.get(queue[index]) || []).forEach((childId) => {
|
|
4404
|
+
if (seen.has(childId) || !hidden.has(childId)) return;
|
|
4405
|
+
seen.add(childId);
|
|
4406
|
+
queue.push(childId);
|
|
4407
|
+
});
|
|
4408
|
+
}
|
|
4409
|
+
return seen.size;
|
|
4410
|
+
}
|
|
4411
|
+
setCollapsed(nodeId, collapsed) {
|
|
4412
|
+
if (this.readonly) return false;
|
|
4413
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4414
|
+
if (!node) return false;
|
|
4415
|
+
const next = Boolean(collapsed);
|
|
4416
|
+
if (Boolean(node.collapsed) === next) return false;
|
|
4417
|
+
if (next && !this.getChildNodes(node.id).length) return false;
|
|
4418
|
+
if (next) node.collapsed = true;
|
|
4419
|
+
else delete node.collapsed;
|
|
4420
|
+
if (this.selection?.kind === "node") {
|
|
4421
|
+
const hidden = this.getHiddenNodeIds();
|
|
4422
|
+
const visibleIds = this.getSelectedNodeIds().filter((id) => !hidden.has(id));
|
|
4423
|
+
if (hidden.has(this.selection.id)) {
|
|
4424
|
+
this.select({ kind: "node", id: node.id });
|
|
4425
|
+
} else if (visibleIds.length !== this.selectedNodeIds.size) {
|
|
4426
|
+
this.selectNodes(visibleIds, { primary: this.selection.id });
|
|
4427
|
+
}
|
|
4428
|
+
} else if (this.selection?.kind === "edge") {
|
|
4429
|
+
const edge = this.findEdge(this.selection.id);
|
|
4430
|
+
const hidden = this.getHiddenNodeIds();
|
|
4431
|
+
if (edge && (hidden.has(edge.from.nodeId) || hidden.has(edge.to.nodeId))) {
|
|
4432
|
+
this.select({ kind: "node", id: node.id });
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
this.render({ inspector: true, json: true });
|
|
4436
|
+
this.emitChange("node:collapse", { node: deepClone(node) });
|
|
4437
|
+
return true;
|
|
4438
|
+
}
|
|
4439
|
+
toggleCollapsed(nodeId) {
|
|
4440
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4441
|
+
return node ? this.setCollapsed(node.id, !node.collapsed) : false;
|
|
4442
|
+
}
|
|
3084
4443
|
findEdge(edgeId) {
|
|
3085
4444
|
return this.documentData.edges.find((edge) => edge.id === edgeId) || null;
|
|
3086
4445
|
}
|
|
@@ -3091,7 +4450,8 @@ var VdFlowchart = class {
|
|
|
3091
4450
|
}
|
|
3092
4451
|
emitChange(reason, extra = {}) {
|
|
3093
4452
|
if (this.historyEnabled && !this.isApplyingHistory) {
|
|
3094
|
-
this.
|
|
4453
|
+
if (this.historyBatchDepth > 0) this.historyBatchDirty = true;
|
|
4454
|
+
else this.recordHistory(reason);
|
|
3095
4455
|
}
|
|
3096
4456
|
this.syncJsonTextarea();
|
|
3097
4457
|
this.emit("change", {
|
|
@@ -3135,7 +4495,9 @@ var VdFlowchart = class {
|
|
|
3135
4495
|
const snapshot = this.toJSON();
|
|
3136
4496
|
const targetKey = this.selection ? `${this.selection.kind}:${this.selection.id}` : "";
|
|
3137
4497
|
const top = this.history[this.historyIndex];
|
|
3138
|
-
const
|
|
4498
|
+
const atTip = Boolean(top) && this.historyIndex === this.history.length - 1;
|
|
4499
|
+
const namesInsertedNode = this.coalesceInsertedText && top?.reason === "node:insert" && reason === "node:update";
|
|
4500
|
+
const canCoalesce = atTip && top.targetKey === targetKey && (namesInsertedNode || COALESCING_REASONS.has(reason) && top.reason === reason);
|
|
3139
4501
|
if (canCoalesce) {
|
|
3140
4502
|
top.snapshot = snapshot;
|
|
3141
4503
|
return;
|
|
@@ -3157,8 +4519,8 @@ var VdFlowchart = class {
|
|
|
3157
4519
|
this.reconnectEdgeId = null;
|
|
3158
4520
|
const currentViewport = deepClone(this.documentData.viewport);
|
|
3159
4521
|
const previousSelection = this.selection;
|
|
3160
|
-
this.documentData = normalizeDocument(snapshot);
|
|
3161
|
-
this.documentData.viewport = normalizeViewport(currentViewport);
|
|
4522
|
+
this.documentData = normalizeDocument(snapshot, this.normalizationLimits());
|
|
4523
|
+
this.documentData.viewport = normalizeViewport(currentViewport, this.scaleBounds());
|
|
3162
4524
|
this.selection = this.resolvePreservedSelection(previousSelection, true);
|
|
3163
4525
|
this.render();
|
|
3164
4526
|
this.emitChange(reason);
|
|
@@ -3235,7 +4597,8 @@ var VdFlowchart = class {
|
|
|
3235
4597
|
y: partialNode.y == null ? fallbackY : partialNode.y
|
|
3236
4598
|
},
|
|
3237
4599
|
this.documentData.nodes.length,
|
|
3238
|
-
usedIds
|
|
4600
|
+
usedIds,
|
|
4601
|
+
this.nodeSizeBounds()
|
|
3239
4602
|
);
|
|
3240
4603
|
this.documentData.nodes.push(node);
|
|
3241
4604
|
this.select({ kind: "node", id: node.id });
|
|
@@ -3251,19 +4614,180 @@ var VdFlowchart = class {
|
|
|
3251
4614
|
const parent = this.findNode(sanitizeId(parentId));
|
|
3252
4615
|
if (!parent) return null;
|
|
3253
4616
|
const { direction = "right", distance, angle, edge: edgeOptions, ...nodeOptions } = options;
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
4617
|
+
return this.batchHistory("node:add-child", () => {
|
|
4618
|
+
const node = this.addNode({
|
|
4619
|
+
...nodeOptions,
|
|
4620
|
+
relativeTo: { node: parent.id, direction, distance, angle }
|
|
4621
|
+
});
|
|
4622
|
+
const edge = this.addEdge({
|
|
4623
|
+
from: parent.id,
|
|
4624
|
+
to: node.id,
|
|
4625
|
+
autoPort: true,
|
|
4626
|
+
endMarker: "arrow",
|
|
4627
|
+
...isPlainObject(edgeOptions) ? edgeOptions : {}
|
|
4628
|
+
});
|
|
4629
|
+
this.select({ kind: "node", id: node.id });
|
|
4630
|
+
return { node, edge };
|
|
3257
4631
|
});
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
4632
|
+
}
|
|
4633
|
+
// Run `fn` with history recording suspended, then record a single entry for
|
|
4634
|
+
// everything it changed (change events still fire for each mutation).
|
|
4635
|
+
batchHistory(reason, fn) {
|
|
4636
|
+
this.historyBatchDepth += 1;
|
|
4637
|
+
let result;
|
|
4638
|
+
try {
|
|
4639
|
+
result = fn();
|
|
4640
|
+
} finally {
|
|
4641
|
+
this.historyBatchDepth -= 1;
|
|
4642
|
+
}
|
|
4643
|
+
if (this.historyBatchDepth === 0 && this.historyBatchDirty) {
|
|
4644
|
+
this.historyBatchDirty = false;
|
|
4645
|
+
if (this.historyEnabled && !this.isApplyingHistory) this.recordHistory(reason);
|
|
4646
|
+
}
|
|
4647
|
+
return result;
|
|
4648
|
+
}
|
|
4649
|
+
// Mind-map structure: a node's parent is the source of its first incoming
|
|
4650
|
+
// edge; its children are the targets of its outgoing edges. The side a
|
|
4651
|
+
// branch is on is the port its edge leaves the parent from, which is what
|
|
4652
|
+
// the user sees (centre geometry is ambiguous for diagonal nodes).
|
|
4653
|
+
getParentEdge(nodeId) {
|
|
4654
|
+
return this.documentData.edges.find(
|
|
4655
|
+
(edge) => edge.to.nodeId === nodeId && edge.from.nodeId !== nodeId
|
|
4656
|
+
) || null;
|
|
4657
|
+
}
|
|
4658
|
+
getParentNode(nodeId) {
|
|
4659
|
+
const edge = this.getParentEdge(nodeId);
|
|
4660
|
+
return edge ? this.findNode(edge.from.nodeId) : null;
|
|
4661
|
+
}
|
|
4662
|
+
getChildNodes(nodeId, direction = null) {
|
|
4663
|
+
const port = direction ? DIRECTION_PORTS[direction] : null;
|
|
4664
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4665
|
+
this.documentData.edges.forEach((edge) => {
|
|
4666
|
+
if (edge.from.nodeId !== nodeId || edge.to.nodeId === nodeId) return;
|
|
4667
|
+
if (!port || edge.from.port === port) ids.add(edge.to.nodeId);
|
|
4668
|
+
});
|
|
4669
|
+
return this.documentData.nodes.filter((node) => ids.has(node.id));
|
|
4670
|
+
}
|
|
4671
|
+
// Direction a branch grows from `node`: the side its parent edge leaves
|
|
4672
|
+
// from, or the emptier horizontal side for a root.
|
|
4673
|
+
getGrowthDirection(node) {
|
|
4674
|
+
const parentEdge = this.getParentEdge(node.id);
|
|
4675
|
+
if (parentEdge) return PORT_DIRECTIONS[parentEdge.from.port] || "right";
|
|
4676
|
+
const right = this.getChildNodes(node.id, "right").length;
|
|
4677
|
+
const left = this.getChildNodes(node.id, "left").length;
|
|
4678
|
+
return left < right ? "left" : "right";
|
|
4679
|
+
}
|
|
4680
|
+
// Shape for a new branch node: copy a sibling when there is one, otherwise
|
|
4681
|
+
// the parent's box shape, falling back to a rounded step.
|
|
4682
|
+
getBranchTemplate(parent, sibling) {
|
|
4683
|
+
const source = sibling || (["rounded-rect", "rect", "textbox"].includes(parent.type) ? parent : null) || null;
|
|
4684
|
+
if (source) return { type: source.type, width: source.width, height: source.height };
|
|
4685
|
+
const spec = DEFAULT_NODE_SPECS["rounded-rect"];
|
|
4686
|
+
return { type: "rounded-rect", width: spec.width, height: spec.height };
|
|
4687
|
+
}
|
|
4688
|
+
/**
|
|
4689
|
+
* Add a node connected from `parentId` on the `direction` side (default: the
|
|
4690
|
+
* branch's growth direction), stacked after `anchorId` or the last sibling on
|
|
4691
|
+
* that side. The node and its edge are one undo step; the new edge copies a
|
|
4692
|
+
* sibling's (or the parent's incoming) edge style. With `edit: true` the
|
|
4693
|
+
* label editor opens with the text selected.
|
|
4694
|
+
*/
|
|
4695
|
+
insertBranchNode(parentId, options = {}) {
|
|
4696
|
+
if (this.readonly) return null;
|
|
4697
|
+
const parent = this.findNode(sanitizeId(parentId));
|
|
4698
|
+
if (!parent) return null;
|
|
4699
|
+
const direction = DIRECTION_PORTS[options.direction] ? options.direction : this.getGrowthDirection(parent);
|
|
4700
|
+
const siblings = this.getChildNodes(parent.id, direction);
|
|
4701
|
+
const anchor = options.anchorId ? this.findNode(sanitizeId(options.anchorId)) : null;
|
|
4702
|
+
const lastSibling = siblings[siblings.length - 1] || null;
|
|
4703
|
+
const template = this.getBranchTemplate(parent, anchor || lastSibling);
|
|
4704
|
+
const position = placeBranchNode({
|
|
4705
|
+
nodes: this.documentData.nodes,
|
|
4706
|
+
parent,
|
|
4707
|
+
siblings,
|
|
4708
|
+
direction,
|
|
4709
|
+
size: template,
|
|
4710
|
+
anchor,
|
|
4711
|
+
before: Boolean(options.before)
|
|
4712
|
+
});
|
|
4713
|
+
const styleSource = this.documentData.edges.find(
|
|
4714
|
+
(edge) => edge.from.nodeId === parent.id && edge.to.nodeId === (anchor || lastSibling)?.id
|
|
4715
|
+
) || this.documentData.edges.find((edge) => edge.to.nodeId === parent.id);
|
|
4716
|
+
const edgeStyle = styleSource ? {
|
|
4717
|
+
route: styleSource.route,
|
|
4718
|
+
strokeWidth: styleSource.strokeWidth,
|
|
4719
|
+
startMarker: styleSource.startMarker,
|
|
4720
|
+
endMarker: styleSource.endMarker
|
|
4721
|
+
} : { endMarker: "arrow" };
|
|
4722
|
+
const result = this.batchHistory("node:insert", () => {
|
|
4723
|
+
if (parent.collapsed) delete parent.collapsed;
|
|
4724
|
+
const node = this.addNode({ ...template, x: position.x, y: position.y });
|
|
4725
|
+
const edge = this.addEdge({
|
|
4726
|
+
from: { nodeId: parent.id, port: DIRECTION_PORTS[direction] },
|
|
4727
|
+
to: { nodeId: node.id, port: DIRECTION_PORTS[oppositeDirection(direction)] },
|
|
4728
|
+
...edgeStyle
|
|
4729
|
+
});
|
|
4730
|
+
if (edge && anchor) this.placeEdgeBySibling(edge.id, parent.id, anchor.id, options.before);
|
|
4731
|
+
if (this.autoLayout && this.layoutMode !== "grid") this.layout(this.layoutMode);
|
|
4732
|
+
this.select({ kind: "node", id: node.id });
|
|
4733
|
+
return { node, edge };
|
|
4734
|
+
});
|
|
4735
|
+
this.revealNode(result.node.id);
|
|
4736
|
+
if (options.edit) this.startTextEdit(result.node.id, { inserted: true });
|
|
4737
|
+
return result;
|
|
4738
|
+
}
|
|
4739
|
+
// Tree and radial layouts order children by connection order, so a sibling's
|
|
4740
|
+
// connection goes right after (or before) its anchor's instead of last.
|
|
4741
|
+
placeEdgeBySibling(edgeId, parentId, anchorId, before = false) {
|
|
4742
|
+
const edges = this.documentData.edges;
|
|
4743
|
+
const fromIndex = edges.findIndex((edge) => edge.id === edgeId);
|
|
4744
|
+
if (fromIndex === -1) return;
|
|
4745
|
+
const [moved] = edges.splice(fromIndex, 1);
|
|
4746
|
+
const anchorIndex = edges.findIndex(
|
|
4747
|
+
(edge) => edge.from.nodeId === parentId && edge.to.nodeId === anchorId
|
|
4748
|
+
);
|
|
4749
|
+
if (anchorIndex === -1) edges.push(moved);
|
|
4750
|
+
else edges.splice(before ? anchorIndex : anchorIndex + 1, 0, moved);
|
|
4751
|
+
}
|
|
4752
|
+
// Insert a sibling after (or before) `nodeId`; a node without a parent gets a
|
|
4753
|
+
// child instead, which is what Enter on a mind-map root means.
|
|
4754
|
+
insertSiblingNode(nodeId, options = {}) {
|
|
4755
|
+
if (this.readonly) return null;
|
|
4756
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4757
|
+
if (!node) return null;
|
|
4758
|
+
const parentEdge = this.getParentEdge(node.id);
|
|
4759
|
+
const parent = parentEdge ? this.findNode(parentEdge.from.nodeId) : null;
|
|
4760
|
+
if (!parent) return this.insertBranchNode(node.id, { edit: options.edit });
|
|
4761
|
+
return this.insertBranchNode(parent.id, {
|
|
4762
|
+
direction: PORT_DIRECTIONS[parentEdge.from.port],
|
|
4763
|
+
anchorId: node.id,
|
|
4764
|
+
before: Boolean(options.before),
|
|
4765
|
+
edit: options.edit
|
|
3264
4766
|
});
|
|
3265
|
-
|
|
3266
|
-
|
|
4767
|
+
}
|
|
4768
|
+
// Pan just enough to bring a node fully into view (with a small margin).
|
|
4769
|
+
revealNode(nodeId) {
|
|
4770
|
+
const node = this.findNode(nodeId);
|
|
4771
|
+
const width = this.canvasEl.clientWidth;
|
|
4772
|
+
const height = this.canvasEl.clientHeight;
|
|
4773
|
+
if (!node || !width || !height) return;
|
|
4774
|
+
const viewport = this.documentData.viewport;
|
|
4775
|
+
const margin = 32;
|
|
4776
|
+
const left = viewport.x + node.x * viewport.scale;
|
|
4777
|
+
const top = viewport.y + node.y * viewport.scale;
|
|
4778
|
+
const right = left + node.width * viewport.scale;
|
|
4779
|
+
const bottom = top + node.height * viewport.scale;
|
|
4780
|
+
let dx = 0;
|
|
4781
|
+
let dy = 0;
|
|
4782
|
+
if (right > width - margin) dx = width - margin - right;
|
|
4783
|
+
if (left + dx < margin) dx = margin - left;
|
|
4784
|
+
if (bottom > height - margin) dy = height - margin - bottom;
|
|
4785
|
+
if (top + dy < margin) dy = margin - top;
|
|
4786
|
+
if (!dx && !dy) return;
|
|
4787
|
+
viewport.x = formatNumber(viewport.x + dx);
|
|
4788
|
+
viewport.y = formatNumber(viewport.y + dy);
|
|
4789
|
+
this.render({ inspector: false, json: true });
|
|
4790
|
+
this.emitViewportChange("viewport:pan");
|
|
3267
4791
|
}
|
|
3268
4792
|
// Arrange nodes with a built-in layout. Positions are computed by the pure
|
|
3269
4793
|
// computeLayout() module, then applied in place (NOT via load(), which would
|
|
@@ -3327,8 +4851,8 @@ var VdFlowchart = class {
|
|
|
3327
4851
|
node.x = patch.x == null ? node.x : formatNumber(toFiniteNumber(patch.x, node.x));
|
|
3328
4852
|
node.y = patch.y == null ? node.y : formatNumber(toFiniteNumber(patch.y, node.y));
|
|
3329
4853
|
node.text = isNodeTextEditable(nextType) ? patch.text == null ? node.text : String(patch.text) : getNodeSpec(nextType).text;
|
|
3330
|
-
node.width = clampNodeWidth(nextType, patch.width, node.width);
|
|
3331
|
-
node.height = clampNodeHeight(nextType, patch.height, node.height);
|
|
4854
|
+
node.width = clampNodeWidth(nextType, patch.width, node.width, this.nodeSizeBounds());
|
|
4855
|
+
node.height = clampNodeHeight(nextType, patch.height, node.height, this.nodeSizeBounds());
|
|
3332
4856
|
if (isPlainObject(patch.data)) {
|
|
3333
4857
|
node.data = deepClone(patch.data);
|
|
3334
4858
|
}
|
|
@@ -3511,11 +5035,51 @@ var VdFlowchart = class {
|
|
|
3511
5035
|
}
|
|
3512
5036
|
deleteSelection() {
|
|
3513
5037
|
if (!this.selection || this.readonly) return false;
|
|
5038
|
+
if (this.hasMultiSelection()) {
|
|
5039
|
+
return this.removeNodeIds(new Set(this.getSelectedNodeIds()), "node:remove");
|
|
5040
|
+
}
|
|
3514
5041
|
if (this.selection.kind === "node") return this.removeNode(this.selection.id);
|
|
3515
5042
|
return this.removeEdge(this.selection.id);
|
|
3516
5043
|
}
|
|
5044
|
+
nodeSizeBounds() {
|
|
5045
|
+
return { min: this.minNodeSize, max: this.maxNodeSize };
|
|
5046
|
+
}
|
|
5047
|
+
scaleBounds() {
|
|
5048
|
+
return { min: this.minScale, max: this.maxScale };
|
|
5049
|
+
}
|
|
5050
|
+
normalizationLimits() {
|
|
5051
|
+
return { scaleBounds: this.scaleBounds(), sizeBounds: this.nodeSizeBounds() };
|
|
5052
|
+
}
|
|
5053
|
+
hasWideCamera() {
|
|
5054
|
+
return this.minScale < DEFAULT_MIN_SCALE || this.maxScale > DEFAULT_MAX_SCALE;
|
|
5055
|
+
}
|
|
5056
|
+
syncGrid() {
|
|
5057
|
+
const wide = this.hasWideCamera();
|
|
5058
|
+
const extent = wide ? WIDE_WORLD_EXTENT : DEFAULT_WORLD_EXTENT;
|
|
5059
|
+
this.gridRect?.setAttribute("x", String(-extent / 2));
|
|
5060
|
+
this.gridRect?.setAttribute("y", String(-extent / 2));
|
|
5061
|
+
this.gridRect?.setAttribute("width", String(extent));
|
|
5062
|
+
this.gridRect?.setAttribute("height", String(extent));
|
|
5063
|
+
const pattern = this.svg?.querySelector("pattern");
|
|
5064
|
+
const path = pattern?.querySelector("path");
|
|
5065
|
+
if (!pattern || !path) return;
|
|
5066
|
+
let tile = this.gridSize;
|
|
5067
|
+
if (wide) {
|
|
5068
|
+
const scale = Math.max(this.minScale, this.documentData?.viewport?.scale || 1);
|
|
5069
|
+
while (tile * scale < 12 && tile < extent / 8) tile *= 4;
|
|
5070
|
+
while (tile * scale > 48 && tile > 1.5) tile /= 4;
|
|
5071
|
+
tile = formatNumber(tile);
|
|
5072
|
+
path.setAttribute("vector-effect", "non-scaling-stroke");
|
|
5073
|
+
} else {
|
|
5074
|
+
path.removeAttribute("vector-effect");
|
|
5075
|
+
}
|
|
5076
|
+
path.setAttribute("stroke-width", "1");
|
|
5077
|
+
pattern.setAttribute("width", String(tile));
|
|
5078
|
+
pattern.setAttribute("height", String(tile));
|
|
5079
|
+
path.setAttribute("d", `M ${tile} 0 L 0 0 0 ${tile}`);
|
|
5080
|
+
}
|
|
3517
5081
|
setViewport(viewport) {
|
|
3518
|
-
this.documentData.viewport = normalizeViewport(viewport);
|
|
5082
|
+
this.documentData.viewport = normalizeViewport(viewport, this.scaleBounds());
|
|
3519
5083
|
this.render({ inspector: false, json: true });
|
|
3520
5084
|
this.emitViewportChange("viewport:set");
|
|
3521
5085
|
return this;
|
|
@@ -3533,45 +5097,80 @@ var VdFlowchart = class {
|
|
|
3533
5097
|
return this;
|
|
3534
5098
|
}
|
|
3535
5099
|
resetView() {
|
|
3536
|
-
this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 });
|
|
5100
|
+
this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 }, this.scaleBounds());
|
|
3537
5101
|
this.render({ inspector: false, json: true });
|
|
3538
5102
|
this.emitViewportChange("viewport:reset");
|
|
3539
5103
|
return this;
|
|
3540
5104
|
}
|
|
3541
5105
|
fitView() {
|
|
3542
|
-
|
|
5106
|
+
const nodes = this.getVisibleNodes();
|
|
5107
|
+
if (!nodes.length) {
|
|
3543
5108
|
return this.resetView();
|
|
3544
5109
|
}
|
|
3545
|
-
const bounds = getBounds(
|
|
5110
|
+
const bounds = getBounds(nodes);
|
|
3546
5111
|
const width = this.canvasEl.clientWidth || 800;
|
|
3547
5112
|
const height = this.canvasEl.clientHeight || 560;
|
|
3548
5113
|
const padding = 80;
|
|
3549
5114
|
const contentWidth = Math.max(1, bounds.right - bounds.left);
|
|
3550
5115
|
const contentHeight = Math.max(1, bounds.bottom - bounds.top);
|
|
3551
|
-
const scale =
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
5116
|
+
const scale = formatScale(
|
|
5117
|
+
clamp(
|
|
5118
|
+
Math.min((width - padding * 2) / contentWidth, (height - padding * 2) / contentHeight),
|
|
5119
|
+
this.minScale,
|
|
5120
|
+
this.maxScale
|
|
5121
|
+
)
|
|
3555
5122
|
);
|
|
3556
5123
|
this.documentData.viewport = {
|
|
3557
5124
|
x: formatNumber(width / 2 - (bounds.left + bounds.right) / 2 * scale),
|
|
3558
5125
|
y: formatNumber(height / 2 - (bounds.top + bounds.bottom) / 2 * scale),
|
|
3559
|
-
scale
|
|
5126
|
+
scale
|
|
3560
5127
|
};
|
|
3561
5128
|
this.render({ inspector: false, json: true });
|
|
3562
5129
|
this.emitViewportChange("viewport:fit");
|
|
3563
5130
|
return this;
|
|
3564
5131
|
}
|
|
5132
|
+
fitNode(nodeId, padding = 48) {
|
|
5133
|
+
const node = this.findNode(nodeId);
|
|
5134
|
+
if (!node || this.isNodeHidden(node.id)) return this;
|
|
5135
|
+
const pad = Math.max(0, toFiniteNumber(padding, 48));
|
|
5136
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
5137
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
5138
|
+
const availableWidth = Math.max(1, width - pad * 2);
|
|
5139
|
+
const availableHeight = Math.max(1, height - pad * 2);
|
|
5140
|
+
const scale = formatScale(
|
|
5141
|
+
clamp(
|
|
5142
|
+
Math.min(
|
|
5143
|
+
availableWidth / Math.max(1, node.width),
|
|
5144
|
+
availableHeight / Math.max(1, node.height)
|
|
5145
|
+
),
|
|
5146
|
+
this.minScale,
|
|
5147
|
+
this.maxScale
|
|
5148
|
+
)
|
|
5149
|
+
);
|
|
5150
|
+
const centerX = node.x + node.width / 2;
|
|
5151
|
+
const centerY = node.y + node.height / 2;
|
|
5152
|
+
this.documentData.viewport = {
|
|
5153
|
+
x: formatNumber(width / 2 - centerX * scale),
|
|
5154
|
+
y: formatNumber(height / 2 - centerY * scale),
|
|
5155
|
+
scale
|
|
5156
|
+
};
|
|
5157
|
+
this.render({ inspector: false, json: true });
|
|
5158
|
+
this.emitViewportChange("viewport:fit-node");
|
|
5159
|
+
return this;
|
|
5160
|
+
}
|
|
3565
5161
|
clear() {
|
|
3566
5162
|
this.stopTextEdit({ commit: false });
|
|
3567
5163
|
this.activeTool = null;
|
|
3568
5164
|
this.reconnectEdgeId = null;
|
|
3569
5165
|
this.clipboard = null;
|
|
3570
|
-
this.documentData = normalizeDocument(
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
5166
|
+
this.documentData = normalizeDocument(
|
|
5167
|
+
{
|
|
5168
|
+
nodes: [],
|
|
5169
|
+
edges: [],
|
|
5170
|
+
viewport: { x: 0, y: 0, scale: 1 }
|
|
5171
|
+
},
|
|
5172
|
+
this.normalizationLimits()
|
|
5173
|
+
);
|
|
3575
5174
|
this.selection = null;
|
|
3576
5175
|
this.render();
|
|
3577
5176
|
this.emitChange("clear");
|
|
@@ -3600,6 +5199,41 @@ var VdFlowchart = class {
|
|
|
3600
5199
|
pattern.querySelector("path").setAttribute("d", `M ${this.gridSize} 0 L 0 0 0 ${this.gridSize}`);
|
|
3601
5200
|
}
|
|
3602
5201
|
if ("autoFit" in options) this.autoFit = Boolean(options.autoFit);
|
|
5202
|
+
if ("keyboardShortcuts" in options) {
|
|
5203
|
+
this.keyboardShortcuts = normalizeKeyboardShortcuts(options.keyboardShortcuts);
|
|
5204
|
+
}
|
|
5205
|
+
if ("snapGuides" in options) this.snapGuides = options.snapGuides !== false;
|
|
5206
|
+
if ("minimap" in options) this.setMinimapEnabled(options.minimap !== false);
|
|
5207
|
+
if ("autoLayout" in options) this.autoLayout = Boolean(options.autoLayout);
|
|
5208
|
+
if ("minScale" in options || "maxScale" in options || "minNodeSize" in options || "maxNodeSize" in options) {
|
|
5209
|
+
const nextScale = normalizeBoundPair(
|
|
5210
|
+
"minScale" in options ? options.minScale : this.minScale,
|
|
5211
|
+
"maxScale" in options ? options.maxScale : this.maxScale,
|
|
5212
|
+
DEFAULT_MIN_SCALE,
|
|
5213
|
+
DEFAULT_MAX_SCALE
|
|
5214
|
+
);
|
|
5215
|
+
const nextSize = normalizeBoundPair(
|
|
5216
|
+
"minNodeSize" in options ? options.minNodeSize : this.minNodeSize,
|
|
5217
|
+
"maxNodeSize" in options ? options.maxNodeSize : this.maxNodeSize,
|
|
5218
|
+
DEFAULT_MIN_NODE_SIZE,
|
|
5219
|
+
DEFAULT_MAX_NODE_SIZE
|
|
5220
|
+
);
|
|
5221
|
+
this.minScale = nextScale.min;
|
|
5222
|
+
this.maxScale = nextScale.max;
|
|
5223
|
+
this.minNodeSize = nextSize.min;
|
|
5224
|
+
this.maxNodeSize = nextSize.max;
|
|
5225
|
+
const clamped = clamp(
|
|
5226
|
+
formatScale(this.documentData.viewport.scale),
|
|
5227
|
+
this.minScale,
|
|
5228
|
+
this.maxScale
|
|
5229
|
+
);
|
|
5230
|
+
if (clamped !== this.documentData.viewport.scale) {
|
|
5231
|
+
this.documentData.viewport.scale = clamped;
|
|
5232
|
+
this.pendingBoundsEvent = true;
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
this.syncCanvasLabel();
|
|
5236
|
+
if (this.shortcutsHelpOpen) this.renderShortcutsHelp();
|
|
3603
5237
|
if ("history" in options && options.history !== false !== this.historyEnabled) {
|
|
3604
5238
|
this.historyEnabled = options.history !== false;
|
|
3605
5239
|
this.history = [];
|
|
@@ -3615,11 +5249,15 @@ var VdFlowchart = class {
|
|
|
3615
5249
|
}
|
|
3616
5250
|
}
|
|
3617
5251
|
this.render();
|
|
5252
|
+
if (this.pendingBoundsEvent) {
|
|
5253
|
+
this.pendingBoundsEvent = false;
|
|
5254
|
+
this.emitViewportChange("viewport:bounds");
|
|
5255
|
+
}
|
|
3618
5256
|
this.updateHistoryButtons();
|
|
3619
5257
|
return this;
|
|
3620
5258
|
}
|
|
3621
5259
|
load(data, options = {}) {
|
|
3622
|
-
const documentData = normalizeDocument(data);
|
|
5260
|
+
const documentData = normalizeDocument(data, this.normalizationLimits());
|
|
3623
5261
|
if (options.silent && JSON.stringify(documentData) === JSON.stringify(this.documentData)) {
|
|
3624
5262
|
return this;
|
|
3625
5263
|
}
|
|
@@ -3648,6 +5286,10 @@ var VdFlowchart = class {
|
|
|
3648
5286
|
destroy() {
|
|
3649
5287
|
if (this.destroyed) return;
|
|
3650
5288
|
this.destroyed = true;
|
|
5289
|
+
if (this.minimapFrame && typeof cancelAnimationFrame === "function") {
|
|
5290
|
+
cancelAnimationFrame(this.minimapFrame);
|
|
5291
|
+
}
|
|
5292
|
+
this.minimapFrame = 0;
|
|
3651
5293
|
this.activeTool = null;
|
|
3652
5294
|
this.stopTextEdit({ commit: false });
|
|
3653
5295
|
this.resizeObserver?.disconnect();
|
|
@@ -3664,6 +5306,21 @@ var VdFlowchart = class {
|
|
|
3664
5306
|
|
|
3665
5307
|
// src/vue.js
|
|
3666
5308
|
var FORWARDED_EVENTS = ["change", "select", "viewport", "connect", "ready"];
|
|
5309
|
+
var EDITOR_OPTION_KEYS = [
|
|
5310
|
+
"readonly",
|
|
5311
|
+
"gridSize",
|
|
5312
|
+
"autoFit",
|
|
5313
|
+
"history",
|
|
5314
|
+
"historyLimit",
|
|
5315
|
+
"keyboardShortcuts",
|
|
5316
|
+
"snapGuides",
|
|
5317
|
+
"minimap",
|
|
5318
|
+
"autoLayout",
|
|
5319
|
+
"minScale",
|
|
5320
|
+
"maxScale",
|
|
5321
|
+
"minNodeSize",
|
|
5322
|
+
"maxNodeSize"
|
|
5323
|
+
];
|
|
3667
5324
|
var VdFlowchart2 = (0, import_vue.defineComponent)({
|
|
3668
5325
|
name: "VdFlowchart",
|
|
3669
5326
|
props: {
|
|
@@ -3680,21 +5337,42 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
|
|
|
3680
5337
|
/** Enable the built-in undo/redo history (default true). */
|
|
3681
5338
|
history: { type: Boolean, default: true },
|
|
3682
5339
|
/** Maximum number of history entries to retain. */
|
|
3683
|
-
historyLimit: { type: Number, default: void 0 }
|
|
5340
|
+
historyLimit: { type: Number, default: void 0 },
|
|
5341
|
+
/** Keyboard model: 'mindmap' (Tab child, Enter sibling) or 'basic'. */
|
|
5342
|
+
keyboardShortcuts: { type: String, default: "mindmap" },
|
|
5343
|
+
/** Snap dragged nodes to other nodes' edges and centres (default true). */
|
|
5344
|
+
snapGuides: { type: Boolean, default: true },
|
|
5345
|
+
/** Show the overview minimap in the canvas corner (default true). */
|
|
5346
|
+
minimap: { type: Boolean, default: true },
|
|
5347
|
+
/** Re-run the current layout after keyboard or handle insertions (default false). */
|
|
5348
|
+
autoLayout: { type: Boolean, default: false },
|
|
5349
|
+
/** Lowest camera scale. Defaults to 0.35. Set it together with maxScale. */
|
|
5350
|
+
minScale: { type: Number, default: void 0 },
|
|
5351
|
+
/** Highest camera scale. Defaults to 3. Set it together with minScale. */
|
|
5352
|
+
maxScale: { type: Number, default: void 0 },
|
|
5353
|
+
/** Smallest resizable-node side. Defaults to 56. Set it together with maxNodeSize. */
|
|
5354
|
+
minNodeSize: { type: Number, default: void 0 },
|
|
5355
|
+
/** Largest resizable-node side. Defaults to 420. Set it together with minNodeSize. */
|
|
5356
|
+
maxNodeSize: { type: Number, default: void 0 }
|
|
3684
5357
|
},
|
|
3685
5358
|
emits: ["change", "select", "viewport", "connect", "ready"],
|
|
3686
5359
|
setup(props, { emit, expose }) {
|
|
3687
5360
|
const el = (0, import_vue.ref)(null);
|
|
3688
5361
|
let instance = null;
|
|
5362
|
+
const editorOptions = () => Object.fromEntries(
|
|
5363
|
+
EDITOR_OPTION_KEYS.flatMap((key) => {
|
|
5364
|
+
const value = props[key];
|
|
5365
|
+
if ((key === "minScale" || key === "maxScale" || key === "minNodeSize" || key === "maxNodeSize") && value == null) {
|
|
5366
|
+
return [];
|
|
5367
|
+
}
|
|
5368
|
+
return [[key, value]];
|
|
5369
|
+
})
|
|
5370
|
+
);
|
|
3689
5371
|
const create = () => {
|
|
3690
5372
|
instance = new VdFlowchart({
|
|
3691
5373
|
element: el.value,
|
|
3692
5374
|
data: props.data,
|
|
3693
|
-
|
|
3694
|
-
gridSize: props.gridSize,
|
|
3695
|
-
autoFit: props.autoFit,
|
|
3696
|
-
history: props.history,
|
|
3697
|
-
historyLimit: props.historyLimit
|
|
5375
|
+
...editorOptions()
|
|
3698
5376
|
});
|
|
3699
5377
|
FORWARDED_EVENTS.forEach((name) => {
|
|
3700
5378
|
instance.on(name, (payload) => emit(name, payload));
|
|
@@ -3714,16 +5392,9 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
|
|
|
3714
5392
|
{ deep: true }
|
|
3715
5393
|
);
|
|
3716
5394
|
(0, import_vue.watch)(
|
|
3717
|
-
() =>
|
|
5395
|
+
() => EDITOR_OPTION_KEYS.map((key) => props[key]),
|
|
3718
5396
|
() => {
|
|
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
|
-
});
|
|
5397
|
+
if (instance) instance.updateOptions(editorOptions());
|
|
3727
5398
|
}
|
|
3728
5399
|
);
|
|
3729
5400
|
(0, import_vue.onBeforeUnmount)(() => {
|
|
@@ -3738,7 +5409,8 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
|
|
|
3738
5409
|
redo: () => instance?.redo(),
|
|
3739
5410
|
canUndo: () => Boolean(instance?.canUndo()),
|
|
3740
5411
|
canRedo: () => Boolean(instance?.canRedo()),
|
|
3741
|
-
layout: (mode, options) => instance?.layout(mode, options)
|
|
5412
|
+
layout: (mode, options) => instance?.layout(mode, options),
|
|
5413
|
+
fitNode: (nodeId, padding) => instance?.fitNode(nodeId, padding)
|
|
3742
5414
|
});
|
|
3743
5415
|
return () => (0, import_vue.h)("div", { ref: el, class: "vd-flowchart" });
|
|
3744
5416
|
}
|