@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.js
CHANGED
|
@@ -185,6 +185,134 @@ function computeLayout(documentData, mode = "tree", options = {}) {
|
|
|
185
185
|
}
|
|
186
186
|
var LAYOUT_MODES = ["tree", "radial", "grid"];
|
|
187
187
|
|
|
188
|
+
// src/mindmap.js
|
|
189
|
+
var DIRECTION_PORTS = { right: "right", left: "left", down: "bottom", up: "top" };
|
|
190
|
+
var PORT_DIRECTIONS = { right: "right", left: "left", bottom: "down", top: "up" };
|
|
191
|
+
var OPPOSITE_DIRECTIONS = { right: "left", left: "right", down: "up", up: "down" };
|
|
192
|
+
var DIRECTION_VECTORS = {
|
|
193
|
+
right: { x: 1, y: 0 },
|
|
194
|
+
left: { x: -1, y: 0 },
|
|
195
|
+
down: { x: 0, y: 1 },
|
|
196
|
+
up: { x: 0, y: -1 }
|
|
197
|
+
};
|
|
198
|
+
var BRANCH_GAP = 72;
|
|
199
|
+
var SIBLING_GAP = 24;
|
|
200
|
+
var COLLISION_GAP = 12;
|
|
201
|
+
var COLLISION_STEP = 12;
|
|
202
|
+
var MAX_COLLISION_STEPS = 400;
|
|
203
|
+
var BRANCH_RING_STEP = 48;
|
|
204
|
+
var MAX_BRANCH_RINGS = 8;
|
|
205
|
+
var NAVIGATION_CONE = Math.tan(Math.PI / 3);
|
|
206
|
+
function centerOf(node) {
|
|
207
|
+
return { x: node.x + node.width / 2, y: node.y + node.height / 2 };
|
|
208
|
+
}
|
|
209
|
+
function oppositeDirection(direction) {
|
|
210
|
+
return OPPOSITE_DIRECTIONS[direction] || "left";
|
|
211
|
+
}
|
|
212
|
+
function findSpatialNeighbor(nodes, fromNode, direction) {
|
|
213
|
+
const vector = DIRECTION_VECTORS[direction];
|
|
214
|
+
if (!vector || !fromNode) return null;
|
|
215
|
+
const origin = centerOf(fromNode);
|
|
216
|
+
let bestCone = null;
|
|
217
|
+
let bestPlane = null;
|
|
218
|
+
nodes.forEach((node) => {
|
|
219
|
+
if (node.id === fromNode.id) return;
|
|
220
|
+
const center = centerOf(node);
|
|
221
|
+
const dx = center.x - origin.x;
|
|
222
|
+
const dy = center.y - origin.y;
|
|
223
|
+
const along = dx * vector.x + dy * vector.y;
|
|
224
|
+
if (along <= 0) return;
|
|
225
|
+
const across = Math.abs(dx * vector.y - dy * vector.x);
|
|
226
|
+
const score = along + across * 2;
|
|
227
|
+
if (across <= along * NAVIGATION_CONE && (!bestCone || score < bestCone.score)) {
|
|
228
|
+
bestCone = { node, score };
|
|
229
|
+
}
|
|
230
|
+
if (!bestPlane || score < bestPlane.score) bestPlane = { node, score };
|
|
231
|
+
});
|
|
232
|
+
return (bestCone || bestPlane)?.node ?? null;
|
|
233
|
+
}
|
|
234
|
+
function findNearestNode(nodes, point) {
|
|
235
|
+
let best = null;
|
|
236
|
+
nodes.forEach((node) => {
|
|
237
|
+
const center = centerOf(node);
|
|
238
|
+
const distance = Math.hypot(center.x - point.x, center.y - point.y);
|
|
239
|
+
if (!best || distance < best.distance) best = { node, distance };
|
|
240
|
+
});
|
|
241
|
+
return best?.node ?? null;
|
|
242
|
+
}
|
|
243
|
+
function overlapsAny(rect, nodes) {
|
|
244
|
+
return nodes.some(
|
|
245
|
+
(node) => rect.x < node.x + node.width + COLLISION_GAP && rect.x + rect.width + COLLISION_GAP > node.x && rect.y < node.y + node.height + COLLISION_GAP && rect.y + rect.height + COLLISION_GAP > node.y
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
function placeBranchNode({
|
|
249
|
+
nodes,
|
|
250
|
+
parent,
|
|
251
|
+
siblings = [],
|
|
252
|
+
direction,
|
|
253
|
+
size,
|
|
254
|
+
anchor = null,
|
|
255
|
+
before = false
|
|
256
|
+
}) {
|
|
257
|
+
const horizontal = direction === "right" || direction === "left";
|
|
258
|
+
const cross = horizontal ? "y" : "x";
|
|
259
|
+
const crossSize = horizontal ? "height" : "width";
|
|
260
|
+
const lastSibling = siblings.reduce(
|
|
261
|
+
(last, node) => !last || node[cross] + node[crossSize] > last[cross] + last[crossSize] ? node : last,
|
|
262
|
+
null
|
|
263
|
+
);
|
|
264
|
+
const reference = anchor || lastSibling;
|
|
265
|
+
const rect = { x: 0, y: 0, width: size.width, height: size.height };
|
|
266
|
+
if (reference) {
|
|
267
|
+
if (direction === "right") rect.x = reference.x;
|
|
268
|
+
if (direction === "left") rect.x = reference.x + reference.width - size.width;
|
|
269
|
+
if (direction === "down") rect.y = reference.y;
|
|
270
|
+
if (direction === "up") rect.y = reference.y + reference.height - size.height;
|
|
271
|
+
rect[cross] = before ? reference[cross] - SIBLING_GAP - size[crossSize] : reference[cross] + reference[crossSize] + SIBLING_GAP;
|
|
272
|
+
} else {
|
|
273
|
+
if (direction === "right") rect.x = parent.x + parent.width + BRANCH_GAP;
|
|
274
|
+
if (direction === "left") rect.x = parent.x - BRANCH_GAP - size.width;
|
|
275
|
+
if (direction === "down") rect.y = parent.y + parent.height + BRANCH_GAP;
|
|
276
|
+
if (direction === "up") rect.y = parent.y - BRANCH_GAP - size.height;
|
|
277
|
+
rect[cross] = parent[cross] + parent[crossSize] / 2 - size[crossSize] / 2;
|
|
278
|
+
}
|
|
279
|
+
const crossSign = reference ? before ? -1 : 1 : 0;
|
|
280
|
+
const result = nearestFreeSlot(
|
|
281
|
+
rect,
|
|
282
|
+
nodes,
|
|
283
|
+
cross,
|
|
284
|
+
horizontal ? "x" : "y",
|
|
285
|
+
DIRECTION_VECTORS[direction],
|
|
286
|
+
crossSign
|
|
287
|
+
);
|
|
288
|
+
return { x: Math.round(result.x * 100) / 100, y: Math.round(result.y * 100) / 100 };
|
|
289
|
+
}
|
|
290
|
+
function crossOffsetAt(index, crossSign) {
|
|
291
|
+
if (crossSign) return crossSign * COLLISION_STEP * index;
|
|
292
|
+
return COLLISION_STEP * Math.ceil(index / 2) * (index % 2 ? 1 : -1);
|
|
293
|
+
}
|
|
294
|
+
function nearestFreeSlot(rect, nodes, cross, main, vector, crossSign) {
|
|
295
|
+
const origin = { ...rect };
|
|
296
|
+
const outward = vector.x + vector.y;
|
|
297
|
+
let best = null;
|
|
298
|
+
for (let ring = 0; ring <= MAX_BRANCH_RINGS; ring += 1) {
|
|
299
|
+
const mainOffset = ring * BRANCH_RING_STEP;
|
|
300
|
+
if (best && mainOffset >= best.cost) break;
|
|
301
|
+
const candidate = { ...origin, [main]: origin[main] + outward * mainOffset };
|
|
302
|
+
for (let index = 0; index <= MAX_COLLISION_STEPS; index += 1) {
|
|
303
|
+
const crossOffset = crossOffsetAt(index, crossSign);
|
|
304
|
+
const cost = Math.hypot(mainOffset, crossOffset);
|
|
305
|
+
if (best && cost >= best.cost) break;
|
|
306
|
+
candidate[cross] = origin[cross] + crossOffset;
|
|
307
|
+
if (!overlapsAny(candidate, nodes)) {
|
|
308
|
+
best = { rect: { ...candidate }, cost };
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return best ? best.rect : origin;
|
|
314
|
+
}
|
|
315
|
+
|
|
188
316
|
// src/core.js
|
|
189
317
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
190
318
|
var TOOLBAR_ICON_PATHS = {
|
|
@@ -194,6 +322,7 @@ var TOOLBAR_ICON_PATHS = {
|
|
|
194
322
|
"fit-view": "M216,48V88a8,8,0,0,1-16,0V67.31l-42.34,42.35a8,8,0,0,1-11.32-11.32L188.69,56H168a8,8,0,0,1,0-16h40A8,8,0,0,1,216,48ZM98.34,146.34,56,188.69V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H88a8,8,0,0,0,0-16H67.31l42.35-42.34a8,8,0,0,0-11.32-11.32Zm11.32-36.68L67.31,56H88a8,8,0,0,0,0-16H48a8,8,0,0,0-8,8V88a8,8,0,0,0,16,0V67.31l42.34,42.35a8,8,0,0,0,11.32-11.32ZM208,160a8,8,0,0,0-8,8v20.69l-42.34-42.35a8,8,0,0,0-11.32,11.32L188.69,200H168a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,208,160Z",
|
|
195
323
|
undo: "M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z",
|
|
196
324
|
redo: "M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z",
|
|
325
|
+
minimap: "M228.92,49.69a8,8,0,0,0-6.86-1.45L160.93,63.52,99.58,32.84a8,8,0,0,0-5.52-.6l-64,16A8,8,0,0,0,24,56V200a8,8,0,0,0,9.94,7.76l61.13-15.28,61.35,30.68A8.15,8.15,0,0,0,160,224a8,8,0,0,0,1.94-.24l64-16A8,8,0,0,0,232,200V56A8,8,0,0,0,228.92,49.69ZM104,52.94l48,24V203.06l-48-24ZM40,62.25l48-12v127.5l-48,12Zm176,131.5-48,12V78.25l48-12Z",
|
|
197
326
|
clear: "M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"
|
|
198
327
|
};
|
|
199
328
|
var TOOLBAR_ACTIONS = [
|
|
@@ -233,11 +362,12 @@ function createToolbarButton({ action, label, icon, disabled = false }) {
|
|
|
233
362
|
return button;
|
|
234
363
|
}
|
|
235
364
|
var DEFAULT_GRID_SIZE = 24;
|
|
236
|
-
var
|
|
237
|
-
var
|
|
238
|
-
var
|
|
239
|
-
var
|
|
240
|
-
var
|
|
365
|
+
var DEFAULT_MIN_SCALE = 0.35;
|
|
366
|
+
var DEFAULT_MAX_SCALE = 3;
|
|
367
|
+
var DEFAULT_MIN_NODE_SIZE = 56;
|
|
368
|
+
var DEFAULT_MAX_NODE_SIZE = 420;
|
|
369
|
+
var DEFAULT_WORLD_EXTENT = 12e3;
|
|
370
|
+
var WIDE_WORLD_EXTENT = 24e4;
|
|
241
371
|
var MAX_NODES = 1e4;
|
|
242
372
|
var MAX_EDGES = 1e4;
|
|
243
373
|
var RESIZE_HANDLES = ["n", "ne", "e", "se", "s", "sw", "w", "nw"];
|
|
@@ -245,21 +375,32 @@ var DEFAULT_EDGE_STROKE_WIDTH = 2.25;
|
|
|
245
375
|
var MIN_EDGE_STROKE_WIDTH = 1.25;
|
|
246
376
|
var MAX_EDGE_STROKE_WIDTH = 6;
|
|
247
377
|
var CONNECTION_PORT_RADIUS = 6;
|
|
378
|
+
var PORT_HANDLE_OFFSET = 12;
|
|
248
379
|
var CONNECTION_PORT_HIT_RADIUS = 14;
|
|
380
|
+
var PORT_CLICK_SLOP = 4;
|
|
381
|
+
var RESIZE_HANDLE_SIZE = 9;
|
|
382
|
+
var SELECTION_OUTSET = 4;
|
|
383
|
+
var GUIDE_SNAP_DISTANCE = 6;
|
|
384
|
+
var GUIDE_OVERHANG = 12;
|
|
385
|
+
var COLLAPSE_TOGGLE_OFFSET = 36;
|
|
386
|
+
var MINIMAP_WIDTH = 180;
|
|
387
|
+
var MINIMAP_HEIGHT = 120;
|
|
388
|
+
var MINIMAP_MIN_CANVAS_WIDTH = 480;
|
|
389
|
+
var SELECTION_BOX_TYPES = /* @__PURE__ */ new Set(["circle", "diamond", "label"]);
|
|
249
390
|
var RECONNECT_ENDPOINT_RADIUS = 7;
|
|
250
391
|
var RECONNECT_ENDPOINT_HIT_RADIUS = 12;
|
|
251
392
|
var EDGE_HIT_STROKE_MIN = 16;
|
|
252
393
|
var CONNECTION_SNAP_PADDING = 36;
|
|
253
394
|
var CONNECTION_HYSTERESIS = 16;
|
|
254
395
|
var CONNECTION_CENTER_LOCK_RADIUS = 18;
|
|
255
|
-
var RESIZE_PORT_GAP =
|
|
396
|
+
var RESIZE_PORT_GAP = 28;
|
|
256
397
|
var EDGE_STROKE_PRESETS = [
|
|
257
398
|
{ id: "thin", label: "Thin", width: 1.75 },
|
|
258
399
|
{ id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
|
|
259
400
|
{ id: "bold", label: "Bold", width: 3.5 }
|
|
260
401
|
];
|
|
261
|
-
var VD_FLOWCHART_VERSION = "1.
|
|
262
|
-
var FLOWCHART_DOCUMENT_VERSION = "1.
|
|
402
|
+
var VD_FLOWCHART_VERSION = "1.5.0";
|
|
403
|
+
var FLOWCHART_DOCUMENT_VERSION = "1.3.0";
|
|
263
404
|
var FLOWCHART_NODE_TYPES = [
|
|
264
405
|
"rounded-rect",
|
|
265
406
|
"rect",
|
|
@@ -272,7 +413,65 @@ var FLOWCHART_NODE_TYPES = [
|
|
|
272
413
|
var FLOWCHART_PORTS = ["top", "right", "bottom", "left"];
|
|
273
414
|
var FLOWCHART_EDGE_MARKERS = ["none", "arrow", "dot"];
|
|
274
415
|
var FLOWCHART_EDGE_ROUTES = ["curve", "straight", "orthogonal"];
|
|
275
|
-
var
|
|
416
|
+
var FLOWCHART_KEYBOARD_SHORTCUTS = ["mindmap", "basic"];
|
|
417
|
+
function normalizeKeyboardShortcuts(value) {
|
|
418
|
+
return value === "basic" ? "basic" : "mindmap";
|
|
419
|
+
}
|
|
420
|
+
var ARROW_DIRECTIONS = {
|
|
421
|
+
ArrowRight: "right",
|
|
422
|
+
ArrowLeft: "left",
|
|
423
|
+
ArrowDown: "down",
|
|
424
|
+
ArrowUp: "up"
|
|
425
|
+
};
|
|
426
|
+
function getShortcutRows(mode, readonly) {
|
|
427
|
+
const navigation = [
|
|
428
|
+
["Arrow keys", "Select the nearest node in that direction"],
|
|
429
|
+
["Shift + Arrow", "Select a connection on that side (repeat to cycle)"],
|
|
430
|
+
["Shift + click, Shift + drag", "Add nodes to the selection, or select with a box"],
|
|
431
|
+
["Cmd/Ctrl + A", "Select all nodes"]
|
|
432
|
+
];
|
|
433
|
+
const view = [
|
|
434
|
+
["Cmd/Ctrl + = or -", "Zoom in or out"],
|
|
435
|
+
["Cmd/Ctrl + 0", "Zoom to 100%"],
|
|
436
|
+
["Shift + 1", "Fit the diagram to the view"]
|
|
437
|
+
];
|
|
438
|
+
const help = [["?", "Show or hide this list"]];
|
|
439
|
+
if (readonly) {
|
|
440
|
+
return [...navigation, ...view, ...help, ["Tab", "Leave the canvas"]];
|
|
441
|
+
}
|
|
442
|
+
const mindmap = mode === "mindmap" ? [
|
|
443
|
+
["Tab", "Add a child node"],
|
|
444
|
+
["Enter", "Add a sibling below (Shift + Enter: above)"],
|
|
445
|
+
["F2, Space, or type", "Edit the label"],
|
|
446
|
+
["Enter while editing", "Save the label (Shift + Enter: new line)"],
|
|
447
|
+
["Tab while editing", "Save and add a child"]
|
|
448
|
+
] : [
|
|
449
|
+
["Enter or F2", "Edit the label"],
|
|
450
|
+
["Cmd/Ctrl + Enter", "Save the label"]
|
|
451
|
+
];
|
|
452
|
+
return [
|
|
453
|
+
...navigation,
|
|
454
|
+
...mindmap,
|
|
455
|
+
[
|
|
456
|
+
mode === "mindmap" ? "Enter, F2, or type on a connection" : "Enter or F2 on a connection",
|
|
457
|
+
"Edit the connection label"
|
|
458
|
+
],
|
|
459
|
+
["Cmd/Ctrl + /", "Collapse or expand the branch"],
|
|
460
|
+
["Alt + Arrow", "Nudge the node (add Shift for 1 px)"],
|
|
461
|
+
["Delete or Backspace", "Delete the selection"],
|
|
462
|
+
["Cmd/Ctrl + D", "Duplicate the node"],
|
|
463
|
+
["Cmd/Ctrl + Z, Shift + Cmd/Ctrl + Z", "Undo, redo"],
|
|
464
|
+
["Cmd/Ctrl + C, X, V", "Copy, cut, paste"],
|
|
465
|
+
...view,
|
|
466
|
+
["Esc", "Cancel editing, then the tool, then deselect"],
|
|
467
|
+
[
|
|
468
|
+
mode === "mindmap" ? "Shift + Tab, or Esc then Tab" : "Tab or Shift + Tab",
|
|
469
|
+
"Leave the canvas"
|
|
470
|
+
],
|
|
471
|
+
...help
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
var COALESCING_REASONS = /* @__PURE__ */ new Set(["node:update", "edge:update", "node:nudge"]);
|
|
276
475
|
var DEFAULT_EDGE_ROUTE = "curve";
|
|
277
476
|
var ORTHOGONAL_STUB_LENGTH = 32;
|
|
278
477
|
var ORTHOGONAL_CORNER_RADIUS = 12;
|
|
@@ -341,6 +540,17 @@ function deepClone(value) {
|
|
|
341
540
|
function formatNumber(value) {
|
|
342
541
|
return Number(value.toFixed(2));
|
|
343
542
|
}
|
|
543
|
+
function formatScale(value) {
|
|
544
|
+
return Number(toFiniteNumber(value, 1).toFixed(4));
|
|
545
|
+
}
|
|
546
|
+
function normalizeBoundPair(min, max, fallbackMin, fallbackMax) {
|
|
547
|
+
const low = Number(min);
|
|
548
|
+
const high = Number(max);
|
|
549
|
+
if (!Number.isFinite(low) || !Number.isFinite(high) || low <= 0 || high <= 0 || low >= high) {
|
|
550
|
+
return { min: fallbackMin, max: fallbackMax };
|
|
551
|
+
}
|
|
552
|
+
return { min: low, max: high };
|
|
553
|
+
}
|
|
344
554
|
function sanitizeId(value) {
|
|
345
555
|
if (value == null) return "";
|
|
346
556
|
return String(value).trim();
|
|
@@ -398,21 +608,36 @@ function normalizeNodeType(type) {
|
|
|
398
608
|
function getNodeSpec(type) {
|
|
399
609
|
return DEFAULT_NODE_SPECS[normalizeNodeType(type)];
|
|
400
610
|
}
|
|
401
|
-
function
|
|
611
|
+
function nodeVisualScale(node) {
|
|
612
|
+
if (!node || node.type === "junction") return 1;
|
|
613
|
+
const spec = getNodeSpec(node.type);
|
|
614
|
+
const width = spec.width > 0 ? toFiniteNumber(node.width, spec.width) / spec.width : 1;
|
|
615
|
+
const height = spec.height > 0 ? toFiniteNumber(node.height, spec.height) / spec.height : 1;
|
|
616
|
+
return clamp(Math.min(1, width, height), 0.01, 1);
|
|
617
|
+
}
|
|
618
|
+
function edgeVisualScale(fromNode, toNode) {
|
|
619
|
+
if (fromNode && toNode) return Math.min(nodeVisualScale(fromNode), nodeVisualScale(toNode));
|
|
620
|
+
if (fromNode) return nodeVisualScale(fromNode);
|
|
621
|
+
if (toNode) return nodeVisualScale(toNode);
|
|
622
|
+
return 1;
|
|
623
|
+
}
|
|
624
|
+
function getNodeSizeBounds(type, sizeBounds) {
|
|
402
625
|
const spec = getNodeSpec(type);
|
|
626
|
+
const min = sizeBounds?.min ?? DEFAULT_MIN_NODE_SIZE;
|
|
627
|
+
const max = sizeBounds?.max ?? DEFAULT_MAX_NODE_SIZE;
|
|
403
628
|
return {
|
|
404
|
-
minWidth: spec.minWidth ??
|
|
405
|
-
minHeight: spec.minHeight ??
|
|
406
|
-
maxWidth: spec.maxWidth ??
|
|
407
|
-
maxHeight: spec.maxHeight ??
|
|
629
|
+
minWidth: spec.minWidth ?? min,
|
|
630
|
+
minHeight: spec.minHeight ?? min,
|
|
631
|
+
maxWidth: spec.maxWidth ?? max,
|
|
632
|
+
maxHeight: spec.maxHeight ?? max
|
|
408
633
|
};
|
|
409
634
|
}
|
|
410
|
-
function clampNodeWidth(type, width, fallback) {
|
|
411
|
-
const bounds = getNodeSizeBounds(type);
|
|
635
|
+
function clampNodeWidth(type, width, fallback, sizeBounds) {
|
|
636
|
+
const bounds = getNodeSizeBounds(type, sizeBounds);
|
|
412
637
|
return clamp(toFiniteNumber(width, fallback), bounds.minWidth, bounds.maxWidth);
|
|
413
638
|
}
|
|
414
|
-
function clampNodeHeight(type, height, fallback) {
|
|
415
|
-
const bounds = getNodeSizeBounds(type);
|
|
639
|
+
function clampNodeHeight(type, height, fallback, sizeBounds) {
|
|
640
|
+
const bounds = getNodeSizeBounds(type, sizeBounds);
|
|
416
641
|
return clamp(toFiniteNumber(height, fallback), bounds.minHeight, bounds.maxHeight);
|
|
417
642
|
}
|
|
418
643
|
function isNodeResizable(nodeOrType) {
|
|
@@ -436,26 +661,59 @@ function getStrokePresetWidth(presetId) {
|
|
|
436
661
|
const preset = EDGE_STROKE_PRESETS.find((item) => item.id === presetId);
|
|
437
662
|
return preset?.width ?? DEFAULT_EDGE_STROKE_WIDTH;
|
|
438
663
|
}
|
|
439
|
-
function normalizeViewport(viewport) {
|
|
664
|
+
function normalizeViewport(viewport, scaleBounds) {
|
|
665
|
+
const min = scaleBounds?.min ?? DEFAULT_MIN_SCALE;
|
|
666
|
+
const max = scaleBounds?.max ?? DEFAULT_MAX_SCALE;
|
|
440
667
|
return {
|
|
441
668
|
x: toFiniteNumber(viewport?.x, 0),
|
|
442
669
|
y: toFiniteNumber(viewport?.y, 0),
|
|
443
|
-
scale: clamp(toFiniteNumber(viewport?.scale, 1),
|
|
670
|
+
scale: clamp(formatScale(toFiniteNumber(viewport?.scale, 1)), min, max)
|
|
444
671
|
};
|
|
445
672
|
}
|
|
446
|
-
function normalizeNode(rawNode, index, usedIds) {
|
|
673
|
+
function normalizeNode(rawNode, index, usedIds, sizeBounds) {
|
|
447
674
|
const type = normalizeNodeType(rawNode?.type);
|
|
448
675
|
const spec = getNodeSpec(type);
|
|
449
|
-
|
|
676
|
+
const node = {
|
|
450
677
|
id: ensureUniqueId(rawNode?.id, "node", usedIds),
|
|
451
678
|
type,
|
|
452
679
|
x: toFiniteNumber(rawNode?.x, index * 28),
|
|
453
680
|
y: toFiniteNumber(rawNode?.y, index * 18),
|
|
454
|
-
width: clampNodeWidth(type, rawNode?.width, spec.width),
|
|
455
|
-
height: clampNodeHeight(type, rawNode?.height, spec.height),
|
|
681
|
+
width: clampNodeWidth(type, rawNode?.width, spec.width, sizeBounds),
|
|
682
|
+
height: clampNodeHeight(type, rawNode?.height, spec.height, sizeBounds),
|
|
456
683
|
text: rawNode?.text == null ? spec.text : String(rawNode.text),
|
|
457
684
|
data: isPlainObject(rawNode?.data) ? deepClone(rawNode.data) : {}
|
|
458
685
|
};
|
|
686
|
+
if (rawNode?.collapsed === true) node.collapsed = true;
|
|
687
|
+
return node;
|
|
688
|
+
}
|
|
689
|
+
function computeHiddenNodeIds(nodes, edges) {
|
|
690
|
+
const collapsed = new Set(nodes.filter((node) => node.collapsed).map((node) => node.id));
|
|
691
|
+
if (!collapsed.size) return /* @__PURE__ */ new Set();
|
|
692
|
+
const outgoing = new Map(nodes.map((node) => [node.id, []]));
|
|
693
|
+
edges.forEach((edge) => {
|
|
694
|
+
if (edge.from.nodeId === edge.to.nodeId) return;
|
|
695
|
+
outgoing.get(edge.from.nodeId)?.push(edge.to.nodeId);
|
|
696
|
+
});
|
|
697
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
698
|
+
collapsed.forEach((rootId) => {
|
|
699
|
+
const queue2 = [...outgoing.get(rootId) || []];
|
|
700
|
+
for (let index = 0; index < queue2.length; index += 1) {
|
|
701
|
+
const id = queue2[index];
|
|
702
|
+
if (id === rootId || candidates.has(id)) continue;
|
|
703
|
+
candidates.add(id);
|
|
704
|
+
queue2.push(...outgoing.get(id) || []);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
const queue = nodes.filter((node) => !candidates.has(node.id) && !collapsed.has(node.id)).map((node) => node.id);
|
|
708
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
709
|
+
const id = queue[index];
|
|
710
|
+
(outgoing.get(id) || []).forEach((childId) => {
|
|
711
|
+
if (!candidates.has(childId)) return;
|
|
712
|
+
candidates.delete(childId);
|
|
713
|
+
if (!collapsed.has(childId)) queue.push(childId);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return candidates;
|
|
459
717
|
}
|
|
460
718
|
function normalizeEndpoint(rawEndpoint, fallbackPort) {
|
|
461
719
|
return {
|
|
@@ -514,7 +772,7 @@ function isSupportedDocumentVersion(value) {
|
|
|
514
772
|
if (parsed.minor === max.minor && parsed.patch > max.patch) return false;
|
|
515
773
|
return true;
|
|
516
774
|
}
|
|
517
|
-
function normalizeDocument(input) {
|
|
775
|
+
function normalizeDocument(input, limits = {}) {
|
|
518
776
|
let source = input;
|
|
519
777
|
if (typeof source === "string") {
|
|
520
778
|
try {
|
|
@@ -537,13 +795,13 @@ function normalizeDocument(input) {
|
|
|
537
795
|
}
|
|
538
796
|
}
|
|
539
797
|
const usedNodeIds = /* @__PURE__ */ new Set();
|
|
540
|
-
const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds));
|
|
798
|
+
const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds, limits.sizeBounds));
|
|
541
799
|
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
542
800
|
const usedEdgeIds = /* @__PURE__ */ new Set();
|
|
543
801
|
const edges = (Array.isArray(source.edges) ? source.edges : []).slice(0, MAX_EDGES).map((edge, index) => normalizeEdge(edge, index, nodeIds, usedEdgeIds)).filter(Boolean);
|
|
544
802
|
return {
|
|
545
803
|
version: FLOWCHART_DOCUMENT_VERSION,
|
|
546
|
-
viewport: normalizeViewport(source.viewport),
|
|
804
|
+
viewport: normalizeViewport(source.viewport, limits.scaleBounds),
|
|
547
805
|
nodes,
|
|
548
806
|
edges
|
|
549
807
|
};
|
|
@@ -608,6 +866,15 @@ function getPortPosition(node, port) {
|
|
|
608
866
|
return { x: node.x, y: node.y + node.height / 2 };
|
|
609
867
|
}
|
|
610
868
|
}
|
|
869
|
+
function getPortHandlePosition(node, port, scale = 1) {
|
|
870
|
+
const point = getPortPosition(node, port);
|
|
871
|
+
const normal = getPortNormal(port);
|
|
872
|
+
const offset = PORT_HANDLE_OFFSET / scale;
|
|
873
|
+
return {
|
|
874
|
+
x: formatNumber(point.x + normal.x * offset),
|
|
875
|
+
y: formatNumber(point.y + normal.y * offset)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
611
878
|
function getNearestPort(node, point) {
|
|
612
879
|
return FLOWCHART_PORTS.reduce((best, port) => {
|
|
613
880
|
const portPoint = getPortPosition(node, port);
|
|
@@ -751,19 +1018,30 @@ function getPolylineLabelPoint(points) {
|
|
|
751
1018
|
const last = points[points.length - 1] || { x: 0, y: 0 };
|
|
752
1019
|
return { x: formatNumber(last.x), y: formatNumber(last.y) };
|
|
753
1020
|
}
|
|
754
|
-
function
|
|
1021
|
+
function routeMetrics(edgeScale = 1) {
|
|
1022
|
+
const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
|
|
1023
|
+
return {
|
|
1024
|
+
scale,
|
|
1025
|
+
minCurveArm: MIN_CURVE_ARM * scale,
|
|
1026
|
+
maxCurveArm: MAX_CURVE_ARM * scale,
|
|
1027
|
+
stub: ORTHOGONAL_STUB_LENGTH * scale,
|
|
1028
|
+
corner: ORTHOGONAL_CORNER_RADIUS * scale,
|
|
1029
|
+
clearance: ORTHOGONAL_CLEARANCE * scale
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
function curveControlPoint(point, port, target, curvature, metrics = routeMetrics(1)) {
|
|
755
1033
|
const normal = getPortNormal(port);
|
|
756
1034
|
const dx = target.x - point.x;
|
|
757
1035
|
const dy = target.y - point.y;
|
|
758
1036
|
const distance = Math.hypot(dx, dy) || 1;
|
|
759
1037
|
const align = (normal.x * dx + normal.y * dy) / distance;
|
|
760
1038
|
const ease = 0.55 + 0.45 * align;
|
|
761
|
-
const arm = clamp(distance * curvature * ease,
|
|
1039
|
+
const arm = clamp(distance * curvature * ease, metrics.minCurveArm, metrics.maxCurveArm);
|
|
762
1040
|
return { x: point.x + normal.x * arm, y: point.y + normal.y * arm };
|
|
763
1041
|
}
|
|
764
|
-
function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left") {
|
|
765
|
-
const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE);
|
|
766
|
-
const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE);
|
|
1042
|
+
function buildCurvePath(fromPoint, toPoint, fromPort = "right", toPort = "left", metrics = routeMetrics(1)) {
|
|
1043
|
+
const controlA = curveControlPoint(fromPoint, fromPort, toPoint, EDGE_CURVATURE, metrics);
|
|
1044
|
+
const controlB = curveControlPoint(toPoint, toPort, fromPoint, EDGE_CURVATURE, metrics);
|
|
767
1045
|
const labelX = 0.125 * fromPoint.x + 0.375 * controlA.x + 0.375 * controlB.x + 0.125 * toPoint.x;
|
|
768
1046
|
const labelY = 0.125 * fromPoint.y + 0.375 * controlA.y + 0.375 * controlB.y + 0.125 * toPoint.y;
|
|
769
1047
|
return {
|
|
@@ -825,11 +1103,11 @@ function getOrthogonalMidpoints(fromStub, toStub, fromPort, toPort) {
|
|
|
825
1103
|
}
|
|
826
1104
|
return [{ x: fromStub.x, y: toStub.y }];
|
|
827
1105
|
}
|
|
828
|
-
function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect) {
|
|
1106
|
+
function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics = routeMetrics(1)) {
|
|
829
1107
|
const fromNormal = getPortNormal(fromPort);
|
|
830
1108
|
const toNormal = getPortNormal(toPort);
|
|
831
|
-
const fromStub = offsetPoint(fromPoint, fromNormal,
|
|
832
|
-
const toStub = offsetPoint(toPoint, toNormal,
|
|
1109
|
+
const fromStub = offsetPoint(fromPoint, fromNormal, metrics.stub);
|
|
1110
|
+
const toStub = offsetPoint(toPoint, toNormal, metrics.stub);
|
|
833
1111
|
const sourceHorizontal = isHorizontalPort(fromPort);
|
|
834
1112
|
const targetHorizontal = isHorizontalPort(toPort);
|
|
835
1113
|
const mids = [];
|
|
@@ -869,13 +1147,13 @@ function getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toR
|
|
|
869
1147
|
);
|
|
870
1148
|
return points;
|
|
871
1149
|
}
|
|
872
|
-
function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null) {
|
|
1150
|
+
function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "left", fromRect = null, toRect = null, metrics = routeMetrics(1)) {
|
|
873
1151
|
let points;
|
|
874
1152
|
if (fromRect && toRect) {
|
|
875
|
-
points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
|
|
1153
|
+
points = getOrthogonalPoints(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
|
|
876
1154
|
} else {
|
|
877
|
-
const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort),
|
|
878
|
-
const toStub = offsetPoint(toPoint, getPortNormal(toPort),
|
|
1155
|
+
const fromStub = offsetPoint(fromPoint, getPortNormal(fromPort), metrics.stub);
|
|
1156
|
+
const toStub = offsetPoint(toPoint, getPortNormal(toPort), metrics.stub);
|
|
879
1157
|
points = [];
|
|
880
1158
|
[
|
|
881
1159
|
fromPoint,
|
|
@@ -887,7 +1165,7 @@ function buildOrthogonalPath(fromPoint, toPoint, fromPort = "right", toPort = "l
|
|
|
887
1165
|
}
|
|
888
1166
|
const label = getPolylineLabelPoint(points);
|
|
889
1167
|
return {
|
|
890
|
-
d: buildRoundedPath(points,
|
|
1168
|
+
d: buildRoundedPath(points, metrics.corner),
|
|
891
1169
|
labelX: label.x,
|
|
892
1170
|
labelY: label.y
|
|
893
1171
|
};
|
|
@@ -898,6 +1176,7 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
|
|
|
898
1176
|
const fromPoint = edge?.fromPoint || (fromNode ? getPortPosition(fromNode, fromPort) : null);
|
|
899
1177
|
const toPoint = edge?.toPoint || (toNode ? getPortPosition(toNode, toPort) : null);
|
|
900
1178
|
const route = normalizeEdgeRoute(edge?.route);
|
|
1179
|
+
const metrics = routeMetrics(edgeVisualScale(fromNode, toNode));
|
|
901
1180
|
if (!fromPoint || !toPoint) {
|
|
902
1181
|
return { d: "", labelX: 0, labelY: 0 };
|
|
903
1182
|
}
|
|
@@ -905,15 +1184,17 @@ function buildEdgePath(edge, fromNode = null, toNode = null) {
|
|
|
905
1184
|
return buildStraightPath(fromPoint, toPoint);
|
|
906
1185
|
}
|
|
907
1186
|
if (route === "orthogonal") {
|
|
908
|
-
const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode,
|
|
909
|
-
const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode,
|
|
910
|
-
return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect);
|
|
1187
|
+
const fromRect = !edge?.fromPoint && fromNode ? getNodeRectBounds(fromNode, metrics.clearance) : null;
|
|
1188
|
+
const toRect = !edge?.toPoint && toNode ? getNodeRectBounds(toNode, metrics.clearance) : null;
|
|
1189
|
+
return buildOrthogonalPath(fromPoint, toPoint, fromPort, toPort, fromRect, toRect, metrics);
|
|
911
1190
|
}
|
|
912
|
-
return buildCurvePath(fromPoint, toPoint, fromPort, toPort);
|
|
1191
|
+
return buildCurvePath(fromPoint, toPoint, fromPort, toPort, metrics);
|
|
913
1192
|
}
|
|
914
|
-
function createArrowMarker(id, strokeWidth, reversed = false) {
|
|
915
|
-
const
|
|
916
|
-
const
|
|
1193
|
+
function createArrowMarker(id, strokeWidth, reversed = false, edgeScale = 1) {
|
|
1194
|
+
const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
|
|
1195
|
+
const effective = Math.max(0.02, strokeWidth * scale);
|
|
1196
|
+
const size = formatNumber(Math.max(9 * scale, effective * 4.4));
|
|
1197
|
+
const refInset = formatNumber(Math.max(1.5 * scale, effective * 0.85));
|
|
917
1198
|
const marker = svgEl("marker", {
|
|
918
1199
|
id,
|
|
919
1200
|
markerWidth: size,
|
|
@@ -931,9 +1212,11 @@ function createArrowMarker(id, strokeWidth, reversed = false) {
|
|
|
931
1212
|
);
|
|
932
1213
|
return marker;
|
|
933
1214
|
}
|
|
934
|
-
function createDotMarker(id, strokeWidth) {
|
|
935
|
-
const
|
|
936
|
-
const
|
|
1215
|
+
function createDotMarker(id, strokeWidth, edgeScale = 1) {
|
|
1216
|
+
const scale = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
|
|
1217
|
+
const effective = Math.max(0.02, strokeWidth * scale);
|
|
1218
|
+
const size = formatNumber(Math.max(8 * scale, effective * 3.3));
|
|
1219
|
+
const radius = formatNumber(Math.max(2.75 * scale, effective * 1.45));
|
|
937
1220
|
const marker = svgEl("marker", {
|
|
938
1221
|
id,
|
|
939
1222
|
markerWidth: size,
|
|
@@ -975,6 +1258,68 @@ function getResizeCursor(handle) {
|
|
|
975
1258
|
if (handle === "ne" || handle === "sw") return "nesw-resize";
|
|
976
1259
|
return "nwse-resize";
|
|
977
1260
|
}
|
|
1261
|
+
function collectGuideLines(nodes) {
|
|
1262
|
+
const x = [];
|
|
1263
|
+
const y = [];
|
|
1264
|
+
nodes.forEach((node) => {
|
|
1265
|
+
const right = node.x + node.width;
|
|
1266
|
+
const bottom = node.y + node.height;
|
|
1267
|
+
[node.x, node.x + node.width / 2, right].forEach(
|
|
1268
|
+
(value) => x.push({ value, start: node.y, end: bottom })
|
|
1269
|
+
);
|
|
1270
|
+
[node.y, node.y + node.height / 2, bottom].forEach(
|
|
1271
|
+
(value) => y.push({ value, start: node.x, end: right })
|
|
1272
|
+
);
|
|
1273
|
+
});
|
|
1274
|
+
const byValue = (a, b) => a.value - b.value;
|
|
1275
|
+
return { x: x.sort(byValue), y: y.sort(byValue) };
|
|
1276
|
+
}
|
|
1277
|
+
function lowerBound(lines, value) {
|
|
1278
|
+
let low = 0;
|
|
1279
|
+
let high = lines.length;
|
|
1280
|
+
while (low < high) {
|
|
1281
|
+
const middle = low + high >> 1;
|
|
1282
|
+
if (lines[middle].value < value) low = middle + 1;
|
|
1283
|
+
else high = middle;
|
|
1284
|
+
}
|
|
1285
|
+
return low;
|
|
1286
|
+
}
|
|
1287
|
+
function snapToGuides(lines, candidates, threshold) {
|
|
1288
|
+
let best = null;
|
|
1289
|
+
candidates.forEach((candidate) => {
|
|
1290
|
+
const index = lowerBound(lines, candidate);
|
|
1291
|
+
[lines[index - 1], lines[index]].forEach((line) => {
|
|
1292
|
+
if (!line) return;
|
|
1293
|
+
const offset = line.value - candidate;
|
|
1294
|
+
if (Math.abs(offset) <= threshold && (!best || Math.abs(offset) < Math.abs(best.offset))) {
|
|
1295
|
+
best = { offset };
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
if (!best) return null;
|
|
1300
|
+
const positions = candidates.map((candidate) => candidate + best.offset);
|
|
1301
|
+
best.lines = lines.filter(
|
|
1302
|
+
(line) => positions.some((position) => Math.abs(line.value - position) < 0.5)
|
|
1303
|
+
);
|
|
1304
|
+
return best;
|
|
1305
|
+
}
|
|
1306
|
+
function guideSegments(axis, lines, box) {
|
|
1307
|
+
const byValue = /* @__PURE__ */ new Map();
|
|
1308
|
+
lines.forEach((line) => {
|
|
1309
|
+
const current = byValue.get(line.value);
|
|
1310
|
+
byValue.set(line.value, {
|
|
1311
|
+
start: Math.min(current?.start ?? Infinity, line.start),
|
|
1312
|
+
end: Math.max(current?.end ?? -Infinity, line.end)
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
const boxStart = axis === "x" ? box.top : box.left;
|
|
1316
|
+
const boxEnd = boxStart + (axis === "x" ? box.height : box.width);
|
|
1317
|
+
return [...byValue].map(([value, extent]) => {
|
|
1318
|
+
const start = Math.min(extent.start, boxStart) - GUIDE_OVERHANG;
|
|
1319
|
+
const end = Math.max(extent.end, boxEnd) + GUIDE_OVERHANG;
|
|
1320
|
+
return axis === "x" ? { x1: value, y1: start, x2: value, y2: end } : { x1: start, y1: value, x2: end, y2: value };
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
978
1323
|
function getBounds(nodes) {
|
|
979
1324
|
if (!nodes.length) {
|
|
980
1325
|
return { left: 0, top: 0, right: 0, bottom: 0 };
|
|
@@ -1128,9 +1473,26 @@ var VdFlowchart = class {
|
|
|
1128
1473
|
this.element = resolveElement(options.element || options.target);
|
|
1129
1474
|
this.readonly = Boolean(options.readonly);
|
|
1130
1475
|
this.gridSize = clamp(toFiniteNumber(options.gridSize, DEFAULT_GRID_SIZE), 12, 64);
|
|
1131
|
-
|
|
1476
|
+
const scaleBounds = normalizeBoundPair(
|
|
1477
|
+
options.minScale,
|
|
1478
|
+
options.maxScale,
|
|
1479
|
+
DEFAULT_MIN_SCALE,
|
|
1480
|
+
DEFAULT_MAX_SCALE
|
|
1481
|
+
);
|
|
1482
|
+
const sizeBounds = normalizeBoundPair(
|
|
1483
|
+
options.minNodeSize,
|
|
1484
|
+
options.maxNodeSize,
|
|
1485
|
+
DEFAULT_MIN_NODE_SIZE,
|
|
1486
|
+
DEFAULT_MAX_NODE_SIZE
|
|
1487
|
+
);
|
|
1488
|
+
this.minScale = scaleBounds.min;
|
|
1489
|
+
this.maxScale = scaleBounds.max;
|
|
1490
|
+
this.minNodeSize = sizeBounds.min;
|
|
1491
|
+
this.maxNodeSize = sizeBounds.max;
|
|
1492
|
+
this.documentData = normalizeDocument(options.data || {}, { scaleBounds, sizeBounds });
|
|
1132
1493
|
this.listeners = {};
|
|
1133
1494
|
this.selection = null;
|
|
1495
|
+
this.selectedNodeIds = /* @__PURE__ */ new Set();
|
|
1134
1496
|
this.interaction = null;
|
|
1135
1497
|
this.activeTool = null;
|
|
1136
1498
|
this.layoutMode = "tree";
|
|
@@ -1138,6 +1500,7 @@ var VdFlowchart = class {
|
|
|
1138
1500
|
this.destroyed = false;
|
|
1139
1501
|
this.lastNodePointer = null;
|
|
1140
1502
|
this.reconnectEdgeId = null;
|
|
1503
|
+
this.edgeCycleAnchor = null;
|
|
1141
1504
|
this.clipboard = null;
|
|
1142
1505
|
this.gridPatternId = nextId("flowchart-grid");
|
|
1143
1506
|
this.markerIds = /* @__PURE__ */ new Map();
|
|
@@ -1147,6 +1510,16 @@ var VdFlowchart = class {
|
|
|
1147
1510
|
this.history = [];
|
|
1148
1511
|
this.historyIndex = -1;
|
|
1149
1512
|
this.isApplyingHistory = false;
|
|
1513
|
+
this.historyBatchDepth = 0;
|
|
1514
|
+
this.historyBatchDirty = false;
|
|
1515
|
+
this.coalesceInsertedText = false;
|
|
1516
|
+
this.keyboardShortcuts = normalizeKeyboardShortcuts(options.keyboardShortcuts);
|
|
1517
|
+
this.snapGuides = options.snapGuides !== false;
|
|
1518
|
+
this.minimapEnabled = options.minimap !== false;
|
|
1519
|
+
this.autoLayout = Boolean(options.autoLayout);
|
|
1520
|
+
this.minimapFrame = 0;
|
|
1521
|
+
this.minimapDrag = null;
|
|
1522
|
+
this.shortcutsHelpOpen = false;
|
|
1150
1523
|
this.autoFit = Boolean(options.autoFit);
|
|
1151
1524
|
this.readyEmitted = false;
|
|
1152
1525
|
this.resizeObserver = null;
|
|
@@ -1187,6 +1560,7 @@ var VdFlowchart = class {
|
|
|
1187
1560
|
if (!ready) return false;
|
|
1188
1561
|
this.readyEmitted = true;
|
|
1189
1562
|
if (this.autoFit) this.fitView();
|
|
1563
|
+
else this.scheduleMinimap();
|
|
1190
1564
|
this.emit("ready", this);
|
|
1191
1565
|
return true;
|
|
1192
1566
|
};
|
|
@@ -1256,8 +1630,25 @@ var VdFlowchart = class {
|
|
|
1256
1630
|
className: "vd-flowchart-toolbar-label",
|
|
1257
1631
|
text: "100%"
|
|
1258
1632
|
});
|
|
1633
|
+
this.shortcutsButton = createElement("button", {
|
|
1634
|
+
className: "vd-flowchart-btn vd-flowchart-icon-btn vd-flowchart-shortcuts-btn",
|
|
1635
|
+
title: "Keyboard shortcuts (?)",
|
|
1636
|
+
text: "?"
|
|
1637
|
+
});
|
|
1638
|
+
this.shortcutsButton.setAttribute("type", "button");
|
|
1639
|
+
this.shortcutsButton.setAttribute("data-flowchart-action", "shortcuts");
|
|
1640
|
+
this.shortcutsButton.setAttribute("aria-label", "Keyboard shortcuts");
|
|
1641
|
+
this.shortcutsButton.setAttribute("aria-expanded", "false");
|
|
1642
|
+
this.minimapButton = createToolbarButton({
|
|
1643
|
+
action: "minimap",
|
|
1644
|
+
label: "Toggle minimap",
|
|
1645
|
+
icon: "minimap"
|
|
1646
|
+
});
|
|
1647
|
+
this.minimapButton.setAttribute("aria-pressed", this.minimapEnabled ? "true" : "false");
|
|
1259
1648
|
toolbarLeft.appendChild(this.arrangeSelect);
|
|
1260
1649
|
toolbarLeft.appendChild(this.clearButton);
|
|
1650
|
+
toolbarRight.appendChild(this.minimapButton);
|
|
1651
|
+
toolbarRight.appendChild(this.shortcutsButton);
|
|
1261
1652
|
toolbarRight.appendChild(this.zoomLabel);
|
|
1262
1653
|
this.toolbar.appendChild(toolbarLeft);
|
|
1263
1654
|
this.toolbar.appendChild(toolbarRight);
|
|
@@ -1326,23 +1717,50 @@ var VdFlowchart = class {
|
|
|
1326
1717
|
this.world = svgEl("g", { class: "vd-flowchart-world" });
|
|
1327
1718
|
this.gridRect = svgEl("rect", {
|
|
1328
1719
|
class: "vd-flowchart-grid",
|
|
1329
|
-
x: -
|
|
1330
|
-
y: -
|
|
1331
|
-
width:
|
|
1332
|
-
height:
|
|
1720
|
+
x: -DEFAULT_WORLD_EXTENT / 2,
|
|
1721
|
+
y: -DEFAULT_WORLD_EXTENT / 2,
|
|
1722
|
+
width: DEFAULT_WORLD_EXTENT,
|
|
1723
|
+
height: DEFAULT_WORLD_EXTENT,
|
|
1333
1724
|
fill: `url(#${this.gridPatternId})`
|
|
1334
1725
|
});
|
|
1335
1726
|
this.edgesLayer = svgEl("g", { class: "vd-flowchart-edges" });
|
|
1336
1727
|
this.previewLayer = svgEl("g", { class: "vd-flowchart-preview" });
|
|
1337
1728
|
this.nodesLayer = svgEl("g", { class: "vd-flowchart-nodes" });
|
|
1338
1729
|
this.overlayLayer = svgEl("g", { class: "vd-flowchart-overlay" });
|
|
1730
|
+
this.guidesLayer = svgEl("g", { class: "vd-flowchart-guides" });
|
|
1339
1731
|
this.world.appendChild(this.gridRect);
|
|
1340
1732
|
this.world.appendChild(this.edgesLayer);
|
|
1341
1733
|
this.world.appendChild(this.previewLayer);
|
|
1342
1734
|
this.world.appendChild(this.nodesLayer);
|
|
1343
1735
|
this.world.appendChild(this.overlayLayer);
|
|
1736
|
+
this.world.appendChild(this.guidesLayer);
|
|
1344
1737
|
this.svg.appendChild(this.world);
|
|
1345
1738
|
this.canvasEl.appendChild(this.svg);
|
|
1739
|
+
this.shortcutsHelp = createElement("div", { className: "vd-flowchart-shortcuts" });
|
|
1740
|
+
this.shortcutsHelp.id = nextId("flowchart-shortcuts");
|
|
1741
|
+
this.shortcutsHelp.setAttribute("role", "dialog");
|
|
1742
|
+
this.shortcutsHelp.setAttribute("aria-label", "Keyboard shortcuts");
|
|
1743
|
+
this.shortcutsHelp.hidden = true;
|
|
1744
|
+
this.shortcutsHelp.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
1745
|
+
this.shortcutsHelp.addEventListener("wheel", (event) => event.stopPropagation());
|
|
1746
|
+
this.shortcutsButton.setAttribute("aria-controls", this.shortcutsHelp.id);
|
|
1747
|
+
this.canvasEl.appendChild(this.shortcutsHelp);
|
|
1748
|
+
this.minimapEl = createElement("div", { className: "vd-flowchart-minimap" });
|
|
1749
|
+
this.minimapEl.setAttribute("aria-hidden", "true");
|
|
1750
|
+
this.minimapEl.hidden = true;
|
|
1751
|
+
this.minimapSvg = svgEl("svg", {
|
|
1752
|
+
class: "vd-flowchart-minimap-svg",
|
|
1753
|
+
width: MINIMAP_WIDTH,
|
|
1754
|
+
height: MINIMAP_HEIGHT,
|
|
1755
|
+
viewBox: `0 0 ${MINIMAP_WIDTH} ${MINIMAP_HEIGHT}`
|
|
1756
|
+
});
|
|
1757
|
+
this.minimapEl.appendChild(this.minimapSvg);
|
|
1758
|
+
this.minimapEl.addEventListener("pointerdown", (event) => this.handleMinimapPointer(event));
|
|
1759
|
+
this.minimapEl.addEventListener("pointermove", (event) => this.handleMinimapPointer(event));
|
|
1760
|
+
this.minimapEl.addEventListener("pointerup", (event) => this.endMinimapDrag(event));
|
|
1761
|
+
this.minimapEl.addEventListener("pointercancel", (event) => this.endMinimapDrag(event));
|
|
1762
|
+
this.minimapEl.addEventListener("wheel", (event) => event.stopPropagation());
|
|
1763
|
+
this.canvasEl.appendChild(this.minimapEl);
|
|
1346
1764
|
this.inspectorPanel = createElement("aside", {
|
|
1347
1765
|
className: "vd-flowchart-panel vd-flowchart-panel--inspector"
|
|
1348
1766
|
});
|
|
@@ -1450,11 +1868,15 @@ var VdFlowchart = class {
|
|
|
1450
1868
|
this.graphOutline.appendChild(this.graphList);
|
|
1451
1869
|
this.root.appendChild(this.graphOutline);
|
|
1452
1870
|
this.canvasEl.setAttribute("role", "group");
|
|
1871
|
+
this.syncCanvasLabel();
|
|
1872
|
+
this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
|
|
1873
|
+
}
|
|
1874
|
+
syncCanvasLabel() {
|
|
1875
|
+
const keys = this.readonly || this.keyboardShortcuts === "basic" ? "Arrow keys select nodes; Enter or F2 edits a label." : "Arrow keys select nodes; Tab adds a child, Enter adds a sibling, F2 edits a label.";
|
|
1453
1876
|
this.canvasEl.setAttribute(
|
|
1454
1877
|
"aria-label",
|
|
1455
|
-
|
|
1878
|
+
`Diagram canvas. ${keys} Press ? for all shortcuts. Graph outline provides connections.`
|
|
1456
1879
|
);
|
|
1457
|
-
this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
|
|
1458
1880
|
}
|
|
1459
1881
|
renderGraphOutline() {
|
|
1460
1882
|
if (!this.graphOutline) return;
|
|
@@ -1464,8 +1886,9 @@ var VdFlowchart = class {
|
|
|
1464
1886
|
const selected = this.selection?.kind === "node" ? this.selection.id : null;
|
|
1465
1887
|
this.graphStatus.textContent = selected ? `Selected: ${labels.get(selected)}. ${nodes.length} nodes, ${edges.length} connections.` : `${nodes.length} nodes, ${edges.length} connections.`;
|
|
1466
1888
|
if (!this.graphOutline.open) return;
|
|
1889
|
+
const hidden = this.getHiddenNodeIds();
|
|
1467
1890
|
const signature = JSON.stringify([
|
|
1468
|
-
nodes.map((node) => [node.id, node.text]),
|
|
1891
|
+
nodes.map((node) => [node.id, node.text, Boolean(node.collapsed), hidden.has(node.id)]),
|
|
1469
1892
|
edges.map((edge) => [edge.id, edge.from.nodeId, edge.to.nodeId, edge.label])
|
|
1470
1893
|
]);
|
|
1471
1894
|
if (signature !== this.graphSignature) {
|
|
@@ -1491,8 +1914,10 @@ var VdFlowchart = class {
|
|
|
1491
1914
|
const from = incoming.get(node.id);
|
|
1492
1915
|
const relationships = [
|
|
1493
1916
|
to.length ? `Connects to ${to.join(", ")}.` : "No outgoing connections.",
|
|
1494
|
-
from.length ? `Connected from ${from.join(", ")}.` : ""
|
|
1495
|
-
|
|
1917
|
+
from.length ? `Connected from ${from.join(", ")}.` : "",
|
|
1918
|
+
node.collapsed ? "Branch collapsed." : "",
|
|
1919
|
+
hidden.has(node.id) ? "Hidden in a collapsed branch." : ""
|
|
1920
|
+
].filter(Boolean);
|
|
1496
1921
|
this.graphList.appendChild(
|
|
1497
1922
|
createElement("li", { text: `${labels.get(node.id)}. ${relationships.join(" ")}` })
|
|
1498
1923
|
);
|
|
@@ -1577,6 +2002,157 @@ var VdFlowchart = class {
|
|
|
1577
2002
|
if (action === "undo") this.undo();
|
|
1578
2003
|
if (action === "redo") this.redo();
|
|
1579
2004
|
if (action === "clear" && !this.readonly) this.clear();
|
|
2005
|
+
if (action === "shortcuts") this.toggleShortcutsHelp();
|
|
2006
|
+
if (action === "minimap") this.setMinimapEnabled(!this.minimapEnabled);
|
|
2007
|
+
}
|
|
2008
|
+
setMinimapEnabled(enabled) {
|
|
2009
|
+
this.minimapEnabled = Boolean(enabled);
|
|
2010
|
+
this.minimapButton.setAttribute("aria-pressed", this.minimapEnabled ? "true" : "false");
|
|
2011
|
+
this.drawMinimap();
|
|
2012
|
+
return this;
|
|
2013
|
+
}
|
|
2014
|
+
scheduleMinimap() {
|
|
2015
|
+
if (this.minimapFrame || !hasWindow()) return;
|
|
2016
|
+
const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (callback) => setTimeout(callback, 16);
|
|
2017
|
+
this.minimapFrame = raf(() => {
|
|
2018
|
+
this.minimapFrame = 0;
|
|
2019
|
+
this.drawMinimap();
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
// World-to-minimap mapping covering every visible node and the viewport.
|
|
2023
|
+
getMinimapTransform() {
|
|
2024
|
+
const viewport = this.documentData.viewport;
|
|
2025
|
+
const width = this.canvasEl.clientWidth;
|
|
2026
|
+
const height = this.canvasEl.clientHeight;
|
|
2027
|
+
const view = {
|
|
2028
|
+
left: -viewport.x / viewport.scale,
|
|
2029
|
+
top: -viewport.y / viewport.scale,
|
|
2030
|
+
right: (width - viewport.x) / viewport.scale,
|
|
2031
|
+
bottom: (height - viewport.y) / viewport.scale
|
|
2032
|
+
};
|
|
2033
|
+
const content = getBounds(this.getVisibleNodes());
|
|
2034
|
+
const left = Math.min(content.left, view.left);
|
|
2035
|
+
const top = Math.min(content.top, view.top);
|
|
2036
|
+
const spanX = Math.max(1, Math.max(content.right, view.right) - left);
|
|
2037
|
+
const spanY = Math.max(1, Math.max(content.bottom, view.bottom) - top);
|
|
2038
|
+
const padding = 8;
|
|
2039
|
+
const scale = Math.min(
|
|
2040
|
+
(MINIMAP_WIDTH - padding * 2) / spanX,
|
|
2041
|
+
(MINIMAP_HEIGHT - padding * 2) / spanY
|
|
2042
|
+
);
|
|
2043
|
+
return {
|
|
2044
|
+
scale,
|
|
2045
|
+
offsetX: (MINIMAP_WIDTH - spanX * scale) / 2 - left * scale,
|
|
2046
|
+
offsetY: (MINIMAP_HEIGHT - spanY * scale) / 2 - top * scale,
|
|
2047
|
+
view
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
drawMinimap() {
|
|
2051
|
+
if (this.destroyed || !this.minimapEl) return;
|
|
2052
|
+
const nodes = this.getVisibleNodes();
|
|
2053
|
+
const visible = this.minimapEnabled && nodes.length > 0 && this.canvasEl.clientWidth >= MINIMAP_MIN_CANVAS_WIDTH;
|
|
2054
|
+
this.minimapEl.hidden = !visible;
|
|
2055
|
+
if (!visible) return;
|
|
2056
|
+
const transform = this.minimapDrag?.transform || this.getMinimapTransform();
|
|
2057
|
+
const view = this.getMinimapTransform().view;
|
|
2058
|
+
const map = (x, y) => ({
|
|
2059
|
+
x: formatNumber(x * transform.scale + transform.offsetX),
|
|
2060
|
+
y: formatNumber(y * transform.scale + transform.offsetY)
|
|
2061
|
+
});
|
|
2062
|
+
clearChildren(this.minimapSvg);
|
|
2063
|
+
nodes.forEach((node) => {
|
|
2064
|
+
const point = map(node.x, node.y);
|
|
2065
|
+
const selected = this.isNodeSelected(node.id);
|
|
2066
|
+
this.minimapSvg.appendChild(
|
|
2067
|
+
svgEl("rect", {
|
|
2068
|
+
class: `vd-flowchart-minimap-node${selected ? " is-selected" : ""}`,
|
|
2069
|
+
x: point.x,
|
|
2070
|
+
y: point.y,
|
|
2071
|
+
width: formatNumber(Math.max(1.5, node.width * transform.scale)),
|
|
2072
|
+
height: formatNumber(Math.max(1.5, node.height * transform.scale)),
|
|
2073
|
+
rx: 1.5
|
|
2074
|
+
})
|
|
2075
|
+
);
|
|
2076
|
+
});
|
|
2077
|
+
const corner = map(view.left, view.top);
|
|
2078
|
+
this.minimapSvg.appendChild(
|
|
2079
|
+
svgEl("rect", {
|
|
2080
|
+
class: "vd-flowchart-minimap-viewport",
|
|
2081
|
+
x: corner.x,
|
|
2082
|
+
y: corner.y,
|
|
2083
|
+
width: formatNumber((view.right - view.left) * transform.scale),
|
|
2084
|
+
height: formatNumber((view.bottom - view.top) * transform.scale)
|
|
2085
|
+
})
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
// Centre the main view on the world point under the minimap pointer. The
|
|
2089
|
+
// mapping is frozen for the whole drag so the map does not shift under it.
|
|
2090
|
+
handleMinimapPointer(event) {
|
|
2091
|
+
event.stopPropagation();
|
|
2092
|
+
if (event.type === "pointerdown") {
|
|
2093
|
+
if (event.button != null && event.button !== 0) return;
|
|
2094
|
+
event.preventDefault();
|
|
2095
|
+
this.minimapDrag = { pointerId: event.pointerId, transform: this.getMinimapTransform() };
|
|
2096
|
+
try {
|
|
2097
|
+
this.minimapEl.setPointerCapture?.(event.pointerId);
|
|
2098
|
+
} catch {
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
if (!this.minimapDrag || this.minimapDrag.pointerId !== event.pointerId) return;
|
|
2102
|
+
const rect = this.minimapEl.getBoundingClientRect();
|
|
2103
|
+
const { transform } = this.minimapDrag;
|
|
2104
|
+
this.centerViewOn(
|
|
2105
|
+
(event.clientX - rect.left - transform.offsetX) / transform.scale,
|
|
2106
|
+
(event.clientY - rect.top - transform.offsetY) / transform.scale
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
endMinimapDrag(event) {
|
|
2110
|
+
event.stopPropagation();
|
|
2111
|
+
if (!this.minimapDrag || this.minimapDrag.pointerId !== event.pointerId) return;
|
|
2112
|
+
this.minimapDrag = null;
|
|
2113
|
+
this.syncJsonTextarea();
|
|
2114
|
+
this.emitViewportChange("viewport:pan");
|
|
2115
|
+
this.drawMinimap();
|
|
2116
|
+
}
|
|
2117
|
+
centerViewOn(worldX, worldY) {
|
|
2118
|
+
const viewport = this.documentData.viewport;
|
|
2119
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
2120
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
2121
|
+
viewport.x = formatNumber(width / 2 - worldX * viewport.scale);
|
|
2122
|
+
viewport.y = formatNumber(height / 2 - worldY * viewport.scale);
|
|
2123
|
+
this.render({ inspector: false, json: false });
|
|
2124
|
+
return this;
|
|
2125
|
+
}
|
|
2126
|
+
toggleShortcutsHelp(force) {
|
|
2127
|
+
const open = typeof force === "boolean" ? force : !this.shortcutsHelpOpen;
|
|
2128
|
+
this.shortcutsHelpOpen = open;
|
|
2129
|
+
this.shortcutsButton.setAttribute("aria-expanded", open ? "true" : "false");
|
|
2130
|
+
if (open) this.renderShortcutsHelp();
|
|
2131
|
+
this.shortcutsHelp.hidden = !open;
|
|
2132
|
+
if (!open && this.shortcutsHelp.contains(document.activeElement)) this.canvasEl.focus();
|
|
2133
|
+
return this;
|
|
2134
|
+
}
|
|
2135
|
+
renderShortcutsHelp() {
|
|
2136
|
+
clearChildren(this.shortcutsHelp);
|
|
2137
|
+
const header = createElement("div", { className: "vd-flowchart-shortcuts-header" });
|
|
2138
|
+
header.appendChild(
|
|
2139
|
+
createElement("h4", { className: "vd-flowchart-panel-title", text: "Keyboard shortcuts" })
|
|
2140
|
+
);
|
|
2141
|
+
const close = createElement("button", {
|
|
2142
|
+
className: "vd-flowchart-btn vd-flowchart-icon-btn",
|
|
2143
|
+
text: "\xD7"
|
|
2144
|
+
});
|
|
2145
|
+
close.setAttribute("type", "button");
|
|
2146
|
+
close.setAttribute("aria-label", "Close keyboard shortcuts");
|
|
2147
|
+
close.addEventListener("click", () => this.toggleShortcutsHelp(false));
|
|
2148
|
+
header.appendChild(close);
|
|
2149
|
+
this.shortcutsHelp.appendChild(header);
|
|
2150
|
+
const list = createElement("dl", { className: "vd-flowchart-shortcuts-list" });
|
|
2151
|
+
getShortcutRows(this.keyboardShortcuts, this.readonly).forEach(([keys, action]) => {
|
|
2152
|
+
list.appendChild(createElement("dt", { text: keys }));
|
|
2153
|
+
list.appendChild(createElement("dd", { text: action }));
|
|
2154
|
+
});
|
|
2155
|
+
this.shortcutsHelp.appendChild(list);
|
|
1580
2156
|
}
|
|
1581
2157
|
handleArrangeChange(event) {
|
|
1582
2158
|
if (this.readonly) return;
|
|
@@ -1685,38 +2261,48 @@ var VdFlowchart = class {
|
|
|
1685
2261
|
);
|
|
1686
2262
|
}
|
|
1687
2263
|
}
|
|
2264
|
+
// Keys the editor consumes stop here, so page-level shortcuts (a docs
|
|
2265
|
+
// site's "/" search, a host's Escape handler) do not also fire.
|
|
1688
2266
|
handleKeyDown(event) {
|
|
1689
|
-
|
|
2267
|
+
const alreadyHandled = event.defaultPrevented;
|
|
2268
|
+
this.handleEditorKey(event);
|
|
2269
|
+
if (!alreadyHandled && event.defaultPrevented) event.stopPropagation();
|
|
2270
|
+
}
|
|
2271
|
+
handleEditorKey(event) {
|
|
2272
|
+
const target = event.target;
|
|
2273
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable)) {
|
|
1690
2274
|
return;
|
|
1691
2275
|
}
|
|
1692
|
-
if (event.
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
event.preventDefault();
|
|
1704
|
-
this.selectNode(nodes[next].id);
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
if (event.key === "Enter" && !this.readonly && this.selection?.kind === "node") {
|
|
1708
|
-
event.preventDefault();
|
|
1709
|
-
this.startTextEdit(this.selection.id);
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
2276
|
+
if (event.isComposing) return;
|
|
2277
|
+
const key = event.key;
|
|
2278
|
+
const modKey = event.metaKey || event.ctrlKey;
|
|
2279
|
+
if (key === "Escape" && this.shortcutsHelpOpen) {
|
|
2280
|
+
event.preventDefault();
|
|
2281
|
+
this.toggleShortcutsHelp(false);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
if (target === this.canvasEl && this.handleCanvasKeyDown(event)) {
|
|
2285
|
+
event.preventDefault();
|
|
2286
|
+
return;
|
|
1712
2287
|
}
|
|
1713
2288
|
if (this.readonly) return;
|
|
1714
|
-
if (
|
|
2289
|
+
if (key === "Escape") {
|
|
2290
|
+
if (this.activeTool) this.setActiveTool(null);
|
|
2291
|
+
else if (this.reconnectEdgeId) {
|
|
2292
|
+
this.reconnectEdgeId = null;
|
|
2293
|
+
this.render({ inspector: false, json: false });
|
|
2294
|
+
} else if (this.selection) this.select(null);
|
|
2295
|
+
else return;
|
|
1715
2296
|
event.preventDefault();
|
|
1716
|
-
this.setActiveTool(null);
|
|
1717
2297
|
return;
|
|
1718
2298
|
}
|
|
1719
|
-
|
|
2299
|
+
if (modKey && (key === "d" || key === "D")) {
|
|
2300
|
+
if (this.selection?.kind === "node") {
|
|
2301
|
+
event.preventDefault();
|
|
2302
|
+
this.duplicateSelection();
|
|
2303
|
+
}
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
1720
2306
|
if (modKey && (event.key === "z" || event.key === "Z")) {
|
|
1721
2307
|
event.preventDefault();
|
|
1722
2308
|
if (event.shiftKey) {
|
|
@@ -1755,11 +2341,250 @@ var VdFlowchart = class {
|
|
|
1755
2341
|
if (!this.selection) return;
|
|
1756
2342
|
if (event.key === "Backspace" || event.key === "Delete") {
|
|
1757
2343
|
event.preventDefault();
|
|
2344
|
+
if (this.selection.kind === "node" && !this.hasMultiSelection()) {
|
|
2345
|
+
const removedId = this.selection.id;
|
|
2346
|
+
const parent = this.getParentNode(removedId);
|
|
2347
|
+
const sibling = parent ? this.getChildNodes(parent.id).find((node) => node.id !== removedId) : null;
|
|
2348
|
+
if (this.deleteSelection()) {
|
|
2349
|
+
const next = parent && this.findNode(parent.id) || sibling;
|
|
2350
|
+
if (next) this.select({ kind: "node", id: next.id });
|
|
2351
|
+
}
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
1758
2354
|
this.deleteSelection();
|
|
1759
2355
|
}
|
|
1760
2356
|
}
|
|
2357
|
+
// Keys that only apply while the canvas itself has focus. Returns true when
|
|
2358
|
+
// the key was handled (the caller prevents the default action).
|
|
2359
|
+
handleCanvasKeyDown(event) {
|
|
2360
|
+
const key = event.key;
|
|
2361
|
+
const modKey = event.metaKey || event.ctrlKey;
|
|
2362
|
+
const mindmap = this.keyboardShortcuts === "mindmap";
|
|
2363
|
+
const selectedNode = this.selection?.kind === "node" ? this.findNode(this.selection.id) : null;
|
|
2364
|
+
const selectedEdge = this.selection?.kind === "edge" ? this.findEdge(this.selection.id) : null;
|
|
2365
|
+
const movable = !this.readonly && Boolean(selectedNode);
|
|
2366
|
+
const editable = movable && !this.hasMultiSelection();
|
|
2367
|
+
if (key === "?" && !modKey && !event.altKey) {
|
|
2368
|
+
this.toggleShortcutsHelp();
|
|
2369
|
+
return true;
|
|
2370
|
+
}
|
|
2371
|
+
if (modKey && !event.altKey) {
|
|
2372
|
+
if (key === "=" || key === "+") return Boolean(this.zoomIn());
|
|
2373
|
+
if (key === "-" || key === "_") return Boolean(this.zoomOut());
|
|
2374
|
+
if (key === "0") return Boolean(this.zoomTo(1));
|
|
2375
|
+
if (key === "a" || key === "A") {
|
|
2376
|
+
this.selectNodes(this.getVisibleNodes().map((node) => node.id));
|
|
2377
|
+
return true;
|
|
2378
|
+
}
|
|
2379
|
+
if (key === "/" && selectedNode && !this.hasMultiSelection()) {
|
|
2380
|
+
this.toggleCollapsed(selectedNode.id);
|
|
2381
|
+
return true;
|
|
2382
|
+
}
|
|
2383
|
+
return false;
|
|
2384
|
+
}
|
|
2385
|
+
if (event.shiftKey && !event.altKey && (key === "!" || event.code === "Digit1")) {
|
|
2386
|
+
return Boolean(this.fitView());
|
|
2387
|
+
}
|
|
2388
|
+
const direction = ARROW_DIRECTIONS[key];
|
|
2389
|
+
if (direction) {
|
|
2390
|
+
if (event.altKey) {
|
|
2391
|
+
if (movable) {
|
|
2392
|
+
this.nudgeNodes(this.getSelectedNodeIds(), direction, event.shiftKey ? 1 : this.gridSize);
|
|
2393
|
+
}
|
|
2394
|
+
return true;
|
|
2395
|
+
}
|
|
2396
|
+
if (event.shiftKey) {
|
|
2397
|
+
const anchorId = selectedNode?.id ?? (selectedEdge ? this.edgeCycleAnchor : null);
|
|
2398
|
+
return anchorId ? this.selectSideEdge(anchorId, direction) : false;
|
|
2399
|
+
}
|
|
2400
|
+
this.navigateSelection(direction);
|
|
2401
|
+
return true;
|
|
2402
|
+
}
|
|
2403
|
+
const nodes = this.getVisibleNodes();
|
|
2404
|
+
if ((key === "Home" || key === "End") && nodes.length) {
|
|
2405
|
+
this.selectNode(nodes[key === "Home" ? 0 : nodes.length - 1].id);
|
|
2406
|
+
return true;
|
|
2407
|
+
}
|
|
2408
|
+
if (selectedEdge && !this.readonly && !event.altKey) {
|
|
2409
|
+
if (key === "F2" || key === "Enter" || key === " " && mindmap) {
|
|
2410
|
+
return this.startEdgeLabelEdit(selectedEdge.id);
|
|
2411
|
+
}
|
|
2412
|
+
if (mindmap && key.length === 1 && key !== " ") {
|
|
2413
|
+
return this.startEdgeLabelEdit(selectedEdge.id, { initialText: key });
|
|
2414
|
+
}
|
|
2415
|
+
return false;
|
|
2416
|
+
}
|
|
2417
|
+
if (!editable || event.altKey) return false;
|
|
2418
|
+
if (key === "F2" || key === "Enter" && !mindmap || key === " " && mindmap) {
|
|
2419
|
+
this.startTextEdit(selectedNode.id);
|
|
2420
|
+
return true;
|
|
2421
|
+
}
|
|
2422
|
+
if (!mindmap) return false;
|
|
2423
|
+
if (key === "Tab" && !event.shiftKey) {
|
|
2424
|
+
this.insertBranchNode(selectedNode.id, { edit: true });
|
|
2425
|
+
return true;
|
|
2426
|
+
}
|
|
2427
|
+
if (key === "Enter") {
|
|
2428
|
+
this.insertSiblingNode(selectedNode.id, { before: event.shiftKey, edit: true });
|
|
2429
|
+
return true;
|
|
2430
|
+
}
|
|
2431
|
+
if (key.length === 1 && key !== " " && isNodeTextEditable(selectedNode)) {
|
|
2432
|
+
this.startTextEdit(selectedNode.id, { initialText: key });
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
return false;
|
|
2436
|
+
}
|
|
2437
|
+
// Move the selection to the nearest node in `direction`; with nothing (or an
|
|
2438
|
+
// edge) selected, start from the node closest to the centre of the view.
|
|
2439
|
+
navigateSelection(direction) {
|
|
2440
|
+
const nodes = this.getVisibleNodes();
|
|
2441
|
+
if (!nodes.length) return false;
|
|
2442
|
+
const current = this.selection?.kind === "node" ? this.findNode(this.selection.id) : null;
|
|
2443
|
+
const edge = this.selection?.kind === "edge" ? this.findEdge(this.selection.id) : null;
|
|
2444
|
+
const next = current ? findSpatialNeighbor(nodes, current, direction) : edge ? this.getEdgeEndInDirection(edge, direction) : findNearestNode(nodes, this.getViewportCenter());
|
|
2445
|
+
if (!next) return false;
|
|
2446
|
+
this.selectNode(next.id);
|
|
2447
|
+
this.revealNode(next.id);
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
// The end of `edge` that best matches `direction`, measured from the label
|
|
2451
|
+
// anchor so either end can be reached from a selected connection.
|
|
2452
|
+
getEdgeEndInDirection(edge, direction) {
|
|
2453
|
+
const fromNode = this.findNode(edge.from.nodeId);
|
|
2454
|
+
const toNode = this.findNode(edge.to.nodeId);
|
|
2455
|
+
if (!fromNode || !toNode) return fromNode || toNode;
|
|
2456
|
+
const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
|
|
2457
|
+
const vector = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] }[direction];
|
|
2458
|
+
const score = (node) => (node.x + node.width / 2 - labelX) * vector[0] + (node.y + node.height / 2 - labelY) * vector[1];
|
|
2459
|
+
return score(toNode) >= score(fromNode) ? toNode : fromNode;
|
|
2460
|
+
}
|
|
2461
|
+
// Connections attached to `nodeId` on the `direction` side, outgoing or
|
|
2462
|
+
// incoming, in document order.
|
|
2463
|
+
getSideEdges(nodeId, direction) {
|
|
2464
|
+
const port = DIRECTION_PORTS[direction];
|
|
2465
|
+
return this.documentData.edges.filter(
|
|
2466
|
+
(edge) => edge.from.nodeId === nodeId && edge.from.port === port || edge.to.nodeId === nodeId && edge.to.port === port
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
// Select a connection on one side of a node; repeating cycles through them.
|
|
2470
|
+
selectSideEdge(nodeId, direction) {
|
|
2471
|
+
const edges = this.getSideEdges(nodeId, direction);
|
|
2472
|
+
if (!edges.length) return false;
|
|
2473
|
+
const currentIndex = this.selection?.kind === "edge" ? edges.findIndex((edge) => edge.id === this.selection.id) : -1;
|
|
2474
|
+
const next = edges[(currentIndex + 1) % edges.length];
|
|
2475
|
+
this.select({ kind: "edge", id: next.id });
|
|
2476
|
+
this.edgeCycleAnchor = nodeId;
|
|
2477
|
+
return true;
|
|
2478
|
+
}
|
|
2479
|
+
nudgeNode(nodeId, direction, distance) {
|
|
2480
|
+
const [node] = this.nudgeNodes([nodeId], direction, distance);
|
|
2481
|
+
return node || null;
|
|
2482
|
+
}
|
|
2483
|
+
// Move nodes together; repeated nudges of the same selection coalesce into
|
|
2484
|
+
// one undo step.
|
|
2485
|
+
nudgeNodes(nodeIds, direction, distance) {
|
|
2486
|
+
const step = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] }[direction];
|
|
2487
|
+
if (this.readonly || !step) return [];
|
|
2488
|
+
const nodes = nodeIds.map((id) => this.findNode(id)).filter(Boolean);
|
|
2489
|
+
if (!nodes.length) return [];
|
|
2490
|
+
nodes.forEach((node) => {
|
|
2491
|
+
node.x = formatNumber(node.x + step[0] * distance);
|
|
2492
|
+
node.y = formatNumber(node.y + step[1] * distance);
|
|
2493
|
+
});
|
|
2494
|
+
this.render({ inspector: true, json: true });
|
|
2495
|
+
this.emitChange(
|
|
2496
|
+
"node:nudge",
|
|
2497
|
+
nodes.length > 1 ? { nodeIds: nodes.map((node) => node.id) } : { node: deepClone(nodes[0]) }
|
|
2498
|
+
);
|
|
2499
|
+
return nodes.map((node) => deepClone(node));
|
|
2500
|
+
}
|
|
2501
|
+
zoomTo(scale) {
|
|
2502
|
+
const viewport = this.documentData.viewport;
|
|
2503
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
2504
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
2505
|
+
this.scaleAround(
|
|
2506
|
+
clamp(formatScale(scale), this.minScale, this.maxScale) / viewport.scale,
|
|
2507
|
+
width / 2,
|
|
2508
|
+
height / 2,
|
|
2509
|
+
"viewport:zoom"
|
|
2510
|
+
);
|
|
2511
|
+
return this;
|
|
2512
|
+
}
|
|
2513
|
+
duplicateSelection() {
|
|
2514
|
+
if (this.readonly || this.selection?.kind !== "node") return null;
|
|
2515
|
+
if (this.hasMultiSelection()) return this.pasteNodeGroup(this.captureNodeGroup());
|
|
2516
|
+
const node = this.findNode(this.selection.id);
|
|
2517
|
+
return node ? this.pasteNodeData(node) : null;
|
|
2518
|
+
}
|
|
2519
|
+
// Selected nodes plus the connections that run between them.
|
|
2520
|
+
captureNodeGroup() {
|
|
2521
|
+
const ids = new Set(this.getSelectedNodeIds());
|
|
2522
|
+
return {
|
|
2523
|
+
nodes: deepClone(this.documentData.nodes.filter((node) => ids.has(node.id))),
|
|
2524
|
+
edges: deepClone(
|
|
2525
|
+
this.documentData.edges.filter(
|
|
2526
|
+
(edge) => ids.has(edge.from.nodeId) && ids.has(edge.to.nodeId)
|
|
2527
|
+
)
|
|
2528
|
+
)
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
// Paste a captured group offset by one step, remapping ids so internal
|
|
2532
|
+
// connections follow their copies. One change, one undo step.
|
|
2533
|
+
pasteNodeGroup(group) {
|
|
2534
|
+
if (this.readonly || !group?.nodes?.length) return [];
|
|
2535
|
+
const usedNodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
2536
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
2537
|
+
const nodes = group.nodes.map((data, index) => {
|
|
2538
|
+
const node = normalizeNode(
|
|
2539
|
+
{ ...data, id: void 0, x: data.x + 24, y: data.y + 24 },
|
|
2540
|
+
this.documentData.nodes.length + index,
|
|
2541
|
+
usedNodeIds,
|
|
2542
|
+
this.nodeSizeBounds()
|
|
2543
|
+
);
|
|
2544
|
+
idMap.set(data.id, node.id);
|
|
2545
|
+
return node;
|
|
2546
|
+
});
|
|
2547
|
+
this.documentData.nodes.push(...nodes);
|
|
2548
|
+
const nodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
2549
|
+
const usedEdgeIds = new Set(this.documentData.edges.map((edge) => edge.id));
|
|
2550
|
+
group.edges.forEach((data, index) => {
|
|
2551
|
+
const edge = normalizeEdge(
|
|
2552
|
+
{
|
|
2553
|
+
...data,
|
|
2554
|
+
id: void 0,
|
|
2555
|
+
from: { ...data.from, nodeId: idMap.get(data.from.nodeId) },
|
|
2556
|
+
to: { ...data.to, nodeId: idMap.get(data.to.nodeId) }
|
|
2557
|
+
},
|
|
2558
|
+
this.documentData.edges.length + index,
|
|
2559
|
+
nodeIds,
|
|
2560
|
+
usedEdgeIds
|
|
2561
|
+
);
|
|
2562
|
+
if (edge) this.documentData.edges.push(edge);
|
|
2563
|
+
});
|
|
2564
|
+
const ids = nodes.map((node) => node.id);
|
|
2565
|
+
this.selectNodes(ids);
|
|
2566
|
+
this.emitChange("nodes:add", { nodeIds: ids });
|
|
2567
|
+
return nodes.map((node) => deepClone(node));
|
|
2568
|
+
}
|
|
2569
|
+
pasteNodeData(data) {
|
|
2570
|
+
const usedIds = new Set(this.documentData.nodes.map((node2) => node2.id));
|
|
2571
|
+
const node = normalizeNode(
|
|
2572
|
+
{ ...data, id: void 0, collapsed: void 0, x: data.x + 24, y: data.y + 24 },
|
|
2573
|
+
this.documentData.nodes.length,
|
|
2574
|
+
usedIds,
|
|
2575
|
+
this.nodeSizeBounds()
|
|
2576
|
+
);
|
|
2577
|
+
this.documentData.nodes.push(node);
|
|
2578
|
+
this.select({ kind: "node", id: node.id });
|
|
2579
|
+
this.emitChange("node:add", { node: deepClone(node) });
|
|
2580
|
+
return deepClone(node);
|
|
2581
|
+
}
|
|
1761
2582
|
copySelection() {
|
|
1762
2583
|
if (!this.selection) return false;
|
|
2584
|
+
if (this.hasMultiSelection()) {
|
|
2585
|
+
this.clipboard = { kind: "nodes", data: this.captureNodeGroup() };
|
|
2586
|
+
return true;
|
|
2587
|
+
}
|
|
1763
2588
|
if (this.selection.kind === "node") {
|
|
1764
2589
|
const node = this.findNode(this.selection.id);
|
|
1765
2590
|
if (!node) return false;
|
|
@@ -1774,22 +2599,14 @@ var VdFlowchart = class {
|
|
|
1774
2599
|
pasteClipboard() {
|
|
1775
2600
|
if (!this.clipboard || this.readonly) return false;
|
|
1776
2601
|
if (this.clipboard.kind === "node") {
|
|
1777
|
-
|
|
1778
|
-
const node = normalizeNode(
|
|
1779
|
-
{
|
|
1780
|
-
...this.clipboard.data,
|
|
1781
|
-
id: void 0,
|
|
1782
|
-
x: this.clipboard.data.x + 24,
|
|
1783
|
-
y: this.clipboard.data.y + 24
|
|
1784
|
-
},
|
|
1785
|
-
this.documentData.nodes.length,
|
|
1786
|
-
usedIds2
|
|
1787
|
-
);
|
|
1788
|
-
this.documentData.nodes.push(node);
|
|
1789
|
-
this.select({ kind: "node", id: node.id });
|
|
1790
|
-
this.emitChange("node:add", { node: deepClone(node) });
|
|
2602
|
+
this.pasteNodeData(this.clipboard.data);
|
|
1791
2603
|
return true;
|
|
1792
2604
|
}
|
|
2605
|
+
if (this.clipboard.kind === "nodes") {
|
|
2606
|
+
const pasted = this.pasteNodeGroup(this.clipboard.data);
|
|
2607
|
+
this.clipboard = { kind: "nodes", data: this.captureNodeGroup() };
|
|
2608
|
+
return pasted.length > 0;
|
|
2609
|
+
}
|
|
1793
2610
|
const usedIds = new Set(this.documentData.edges.map((edge2) => edge2.id));
|
|
1794
2611
|
const nodeIds = new Set(this.documentData.nodes.map((node) => node.id));
|
|
1795
2612
|
const edge = normalizeEdge(
|
|
@@ -1816,16 +2633,16 @@ var VdFlowchart = class {
|
|
|
1816
2633
|
const edgeTarget = event.target.closest("[data-edge-id]");
|
|
1817
2634
|
if (edgeTarget && !event.target.closest("[data-edge-endpoint]")) {
|
|
1818
2635
|
const edgeId = edgeTarget.getAttribute("data-edge-id");
|
|
1819
|
-
if (edgeId && this.
|
|
1820
|
-
this.reconnectEdgeId = edgeId;
|
|
1821
|
-
this.select({ kind: "edge", id: edgeId });
|
|
2636
|
+
if (edgeId && this.startEdgeLabelEdit(edgeId)) {
|
|
1822
2637
|
event.preventDefault();
|
|
1823
2638
|
event.stopPropagation();
|
|
1824
2639
|
return;
|
|
1825
2640
|
}
|
|
1826
2641
|
}
|
|
1827
2642
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1828
|
-
if (!nodeTarget || event.target.closest("[data-edge-id]"))
|
|
2643
|
+
if (!nodeTarget || event.target.closest("[data-edge-id]") || event.target.closest("[data-collapse-toggle]")) {
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
1829
2646
|
const nodeId = nodeTarget.getAttribute("data-node-id");
|
|
1830
2647
|
if (this.startTextEdit(nodeId)) {
|
|
1831
2648
|
event.preventDefault();
|
|
@@ -1835,7 +2652,7 @@ var VdFlowchart = class {
|
|
|
1835
2652
|
handleClick(event) {
|
|
1836
2653
|
if (this.readonly || event.detail < 2) return;
|
|
1837
2654
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1838
|
-
if (!nodeTarget || event.target.closest("[data-port]") || event.target.closest("[data-resize-handle]"))
|
|
2655
|
+
if (!nodeTarget || event.target.closest("[data-port]") || event.target.closest("[data-resize-handle]") || event.target.closest("[data-collapse-toggle]"))
|
|
1839
2656
|
return;
|
|
1840
2657
|
if (this.startTextEdit(nodeTarget.getAttribute("data-node-id"))) {
|
|
1841
2658
|
event.preventDefault();
|
|
@@ -1848,6 +2665,13 @@ var VdFlowchart = class {
|
|
|
1848
2665
|
this.stopTextEdit({ commit: true });
|
|
1849
2666
|
}
|
|
1850
2667
|
this.canvasEl.focus();
|
|
2668
|
+
const collapseTarget = event.target.closest("[data-collapse-toggle]");
|
|
2669
|
+
if (collapseTarget) {
|
|
2670
|
+
this.lastNodePointer = null;
|
|
2671
|
+
this.toggleCollapsed(collapseTarget.getAttribute("data-collapse-toggle"));
|
|
2672
|
+
event.preventDefault();
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
1851
2675
|
const nodeTarget = event.target.closest("[data-node-id]");
|
|
1852
2676
|
const portTarget = event.target.closest("[data-port]");
|
|
1853
2677
|
const resizeTarget = event.target.closest("[data-resize-handle]");
|
|
@@ -1883,6 +2707,13 @@ var VdFlowchart = class {
|
|
|
1883
2707
|
event.preventDefault();
|
|
1884
2708
|
return;
|
|
1885
2709
|
}
|
|
2710
|
+
if (nodeTarget && !portTarget && !resizeTarget && !edgeTarget && event.shiftKey) {
|
|
2711
|
+
this.reconnectEdgeId = null;
|
|
2712
|
+
this.lastNodePointer = null;
|
|
2713
|
+
this.toggleNodeSelection(nodeTarget.getAttribute("data-node-id"));
|
|
2714
|
+
event.preventDefault();
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
1886
2717
|
if (nodeTarget && !portTarget && !resizeTarget && !edgeTarget && !this.readonly) {
|
|
1887
2718
|
if (this.activeTool === "arrow") {
|
|
1888
2719
|
this.select({ kind: "node", id: nodeTarget.getAttribute("data-node-id") });
|
|
@@ -1909,6 +2740,9 @@ var VdFlowchart = class {
|
|
|
1909
2740
|
this.interaction = {
|
|
1910
2741
|
kind: "connect",
|
|
1911
2742
|
pointerId: event.pointerId,
|
|
2743
|
+
startClientX: event.clientX,
|
|
2744
|
+
startClientY: event.clientY,
|
|
2745
|
+
moved: false,
|
|
1912
2746
|
source: { nodeId, port },
|
|
1913
2747
|
target: null,
|
|
1914
2748
|
previousSnap: null,
|
|
@@ -1959,22 +2793,27 @@ var VdFlowchart = class {
|
|
|
1959
2793
|
if (nodeTarget) {
|
|
1960
2794
|
this.reconnectEdgeId = null;
|
|
1961
2795
|
const nodeId = nodeTarget.getAttribute("data-node-id");
|
|
1962
|
-
this.
|
|
2796
|
+
const groupIds = this.hasMultiSelection() && this.isNodeSelected(nodeId) ? this.getSelectedNodeIds() : [nodeId];
|
|
2797
|
+
if (groupIds.length > 1) this.selectNodes(groupIds, { primary: nodeId });
|
|
2798
|
+
else this.select({ kind: "node", id: nodeId });
|
|
1963
2799
|
if (!this.readonly) {
|
|
1964
|
-
|
|
1965
|
-
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
1966
|
-
this.interaction = {
|
|
1967
|
-
kind: "drag-node",
|
|
1968
|
-
pointerId: event.pointerId,
|
|
1969
|
-
nodeId,
|
|
1970
|
-
offsetX: world.x - node.x,
|
|
1971
|
-
offsetY: world.y - node.y,
|
|
1972
|
-
moved: false
|
|
1973
|
-
};
|
|
2800
|
+
this.interaction = this.createDragInteraction(event, nodeId, groupIds);
|
|
1974
2801
|
this.capturePointer(event.pointerId);
|
|
1975
2802
|
}
|
|
1976
2803
|
return;
|
|
1977
2804
|
}
|
|
2805
|
+
if (event.shiftKey) {
|
|
2806
|
+
const startWorld = this.clientToWorld(event.clientX, event.clientY);
|
|
2807
|
+
this.interaction = {
|
|
2808
|
+
kind: "marquee",
|
|
2809
|
+
pointerId: event.pointerId,
|
|
2810
|
+
startWorld,
|
|
2811
|
+
baseIds: this.selection?.kind === "node" ? this.getSelectedNodeIds() : []
|
|
2812
|
+
};
|
|
2813
|
+
this.capturePointer(event.pointerId);
|
|
2814
|
+
event.preventDefault();
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
1978
2817
|
this.select(null);
|
|
1979
2818
|
this.reconnectEdgeId = null;
|
|
1980
2819
|
this.interaction = {
|
|
@@ -1989,20 +2828,123 @@ var VdFlowchart = class {
|
|
|
1989
2828
|
this.capturePointer(event.pointerId);
|
|
1990
2829
|
event.preventDefault();
|
|
1991
2830
|
}
|
|
2831
|
+
createDragInteraction(event, nodeId, nodeIds) {
|
|
2832
|
+
const nodes = nodeIds.map((id) => this.findNode(id)).filter(Boolean);
|
|
2833
|
+
const moving = new Set(nodes.map((node) => node.id));
|
|
2834
|
+
return {
|
|
2835
|
+
kind: "drag-node",
|
|
2836
|
+
pointerId: event.pointerId,
|
|
2837
|
+
nodeId,
|
|
2838
|
+
nodeIds: nodes.map((node) => node.id),
|
|
2839
|
+
startWorld: this.clientToWorld(event.clientX, event.clientY),
|
|
2840
|
+
origins: new Map(nodes.map((node) => [node.id, { x: node.x, y: node.y }])),
|
|
2841
|
+
bounds: getBounds(nodes),
|
|
2842
|
+
guideLines: this.snapGuides ? collectGuideLines(this.getVisibleNodes().filter((node) => !moving.has(node.id))) : null,
|
|
2843
|
+
snapDisabled: false,
|
|
2844
|
+
moved: false
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
// Translate every dragged node by the pointer delta, snapping the group's
|
|
2848
|
+
// bounds to the nearest alignment guide on each axis. Alt turns snapping
|
|
2849
|
+
// off for the rest of the gesture.
|
|
2850
|
+
moveDraggedNodes(event) {
|
|
2851
|
+
const interaction = this.interaction;
|
|
2852
|
+
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2853
|
+
let dx = world.x - interaction.startWorld.x;
|
|
2854
|
+
let dy = world.y - interaction.startWorld.y;
|
|
2855
|
+
if (event.altKey) interaction.snapDisabled = true;
|
|
2856
|
+
let guides = [];
|
|
2857
|
+
if (interaction.guideLines && !interaction.snapDisabled) {
|
|
2858
|
+
const box = interaction.bounds;
|
|
2859
|
+
const threshold = GUIDE_SNAP_DISTANCE / this.documentData.viewport.scale;
|
|
2860
|
+
const left = box.left + dx;
|
|
2861
|
+
const top = box.top + dy;
|
|
2862
|
+
const width = box.right - box.left;
|
|
2863
|
+
const height = box.bottom - box.top;
|
|
2864
|
+
const snapX = snapToGuides(
|
|
2865
|
+
interaction.guideLines.x,
|
|
2866
|
+
[left, left + width / 2, left + width],
|
|
2867
|
+
threshold
|
|
2868
|
+
);
|
|
2869
|
+
const snapY = snapToGuides(
|
|
2870
|
+
interaction.guideLines.y,
|
|
2871
|
+
[top, top + height / 2, top + height],
|
|
2872
|
+
threshold
|
|
2873
|
+
);
|
|
2874
|
+
if (snapX) dx += snapX.offset;
|
|
2875
|
+
if (snapY) dy += snapY.offset;
|
|
2876
|
+
const snapped = { left: box.left + dx, top: box.top + dy, width, height };
|
|
2877
|
+
if (snapX) guides = guides.concat(guideSegments("x", snapX.lines, snapped));
|
|
2878
|
+
if (snapY) guides = guides.concat(guideSegments("y", snapY.lines, snapped));
|
|
2879
|
+
}
|
|
2880
|
+
let changed = false;
|
|
2881
|
+
interaction.nodeIds.forEach((id) => {
|
|
2882
|
+
const node = this.findNode(id);
|
|
2883
|
+
const origin = interaction.origins.get(id);
|
|
2884
|
+
if (!node || !origin) return;
|
|
2885
|
+
const nextX = formatNumber(origin.x + dx);
|
|
2886
|
+
const nextY = formatNumber(origin.y + dy);
|
|
2887
|
+
if (nextX === node.x && nextY === node.y) return;
|
|
2888
|
+
node.x = nextX;
|
|
2889
|
+
node.y = nextY;
|
|
2890
|
+
changed = true;
|
|
2891
|
+
this.renderDraggedNode(node);
|
|
2892
|
+
});
|
|
2893
|
+
if (changed) interaction.moved = true;
|
|
2894
|
+
this.renderGuides(guides);
|
|
2895
|
+
}
|
|
2896
|
+
// Live marquee: select every visible node the box touches, added to the
|
|
2897
|
+
// selection the gesture started from.
|
|
2898
|
+
updateMarquee(event) {
|
|
2899
|
+
const interaction = this.interaction;
|
|
2900
|
+
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2901
|
+
const box = {
|
|
2902
|
+
left: Math.min(interaction.startWorld.x, world.x),
|
|
2903
|
+
top: Math.min(interaction.startWorld.y, world.y),
|
|
2904
|
+
right: Math.max(interaction.startWorld.x, world.x),
|
|
2905
|
+
bottom: Math.max(interaction.startWorld.y, world.y)
|
|
2906
|
+
};
|
|
2907
|
+
clearChildren(this.guidesLayer);
|
|
2908
|
+
this.guidesLayer.appendChild(
|
|
2909
|
+
svgEl("rect", {
|
|
2910
|
+
class: "vd-flowchart-marquee",
|
|
2911
|
+
x: formatNumber(box.left),
|
|
2912
|
+
y: formatNumber(box.top),
|
|
2913
|
+
width: formatNumber(box.right - box.left),
|
|
2914
|
+
height: formatNumber(box.bottom - box.top)
|
|
2915
|
+
})
|
|
2916
|
+
);
|
|
2917
|
+
const hits = this.getVisibleNodes().filter(
|
|
2918
|
+
(node) => node.x <= box.right && node.x + node.width >= box.left && node.y <= box.bottom && node.y + node.height >= box.top
|
|
2919
|
+
).map((node) => node.id);
|
|
2920
|
+
const ids = [.../* @__PURE__ */ new Set([...interaction.baseIds, ...hits])];
|
|
2921
|
+
const key = ids.join(",");
|
|
2922
|
+
if (key === interaction.lastKey) return;
|
|
2923
|
+
interaction.lastKey = key;
|
|
2924
|
+
this.selectNodes(ids, { primary: hits[hits.length - 1] });
|
|
2925
|
+
}
|
|
2926
|
+
renderGuides(guides) {
|
|
2927
|
+
clearChildren(this.guidesLayer);
|
|
2928
|
+
guides.forEach((guide) => {
|
|
2929
|
+
this.guidesLayer.appendChild(
|
|
2930
|
+
svgEl("line", {
|
|
2931
|
+
class: "vd-flowchart-guide",
|
|
2932
|
+
x1: guide.x1,
|
|
2933
|
+
y1: guide.y1,
|
|
2934
|
+
x2: guide.x2,
|
|
2935
|
+
y2: guide.y2
|
|
2936
|
+
})
|
|
2937
|
+
);
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
1992
2940
|
handlePointerMove(event) {
|
|
1993
2941
|
if (!this.interaction || this.interaction.pointerId !== event.pointerId) return;
|
|
1994
2942
|
if (this.interaction.kind === "drag-node") {
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
if (nextX !== node.x || nextY !== node.y) {
|
|
2001
|
-
node.x = nextX;
|
|
2002
|
-
node.y = nextY;
|
|
2003
|
-
this.interaction.moved = true;
|
|
2004
|
-
this.renderDraggedNode(node);
|
|
2005
|
-
}
|
|
2943
|
+
this.moveDraggedNodes(event);
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
if (this.interaction.kind === "marquee") {
|
|
2947
|
+
this.updateMarquee(event);
|
|
2006
2948
|
return;
|
|
2007
2949
|
}
|
|
2008
2950
|
if (this.interaction.kind === "resize-node") {
|
|
@@ -2037,6 +2979,14 @@ var VdFlowchart = class {
|
|
|
2037
2979
|
return;
|
|
2038
2980
|
}
|
|
2039
2981
|
if (this.interaction.kind === "connect") {
|
|
2982
|
+
if (!this.interaction.moved) {
|
|
2983
|
+
const travel = Math.hypot(
|
|
2984
|
+
event.clientX - this.interaction.startClientX,
|
|
2985
|
+
event.clientY - this.interaction.startClientY
|
|
2986
|
+
);
|
|
2987
|
+
if (!(travel > PORT_CLICK_SLOP)) return;
|
|
2988
|
+
this.interaction.moved = true;
|
|
2989
|
+
}
|
|
2040
2990
|
const world = this.clientToWorld(event.clientX, event.clientY);
|
|
2041
2991
|
const snapTarget = this.findConnectionTarget(
|
|
2042
2992
|
world,
|
|
@@ -2123,10 +3073,19 @@ var VdFlowchart = class {
|
|
|
2123
3073
|
}
|
|
2124
3074
|
return;
|
|
2125
3075
|
}
|
|
3076
|
+
if (interaction.kind === "marquee") {
|
|
3077
|
+
clearChildren(this.guidesLayer);
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
2126
3080
|
if (interaction.kind === "drag-node") {
|
|
3081
|
+
clearChildren(this.guidesLayer);
|
|
2127
3082
|
if (interaction.moved) {
|
|
2128
3083
|
this.render({ inspector: true, json: true });
|
|
2129
|
-
|
|
3084
|
+
if (interaction.nodeIds.length > 1) {
|
|
3085
|
+
this.emitChange("nodes:move", { nodeIds: [...interaction.nodeIds] });
|
|
3086
|
+
} else {
|
|
3087
|
+
this.emitChange("node:move", { node: deepClone(this.findNode(interaction.nodeId)) });
|
|
3088
|
+
}
|
|
2130
3089
|
} else {
|
|
2131
3090
|
this.render({ inspector: false, json: false });
|
|
2132
3091
|
}
|
|
@@ -2142,6 +3101,14 @@ var VdFlowchart = class {
|
|
|
2142
3101
|
return;
|
|
2143
3102
|
}
|
|
2144
3103
|
if (interaction.kind === "connect") {
|
|
3104
|
+
if (!interaction.moved && this.activeTool !== "arrow") {
|
|
3105
|
+
this.render({ inspector: false, json: false });
|
|
3106
|
+
this.insertBranchNode(interaction.source.nodeId, {
|
|
3107
|
+
direction: PORT_DIRECTIONS[interaction.source.port],
|
|
3108
|
+
edit: true
|
|
3109
|
+
});
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
2145
3112
|
const world = this.clientToWorld(event.clientX || 0, event.clientY || 0);
|
|
2146
3113
|
const snapTarget = interaction.target ? { node: this.findNode(interaction.target.nodeId), port: interaction.target.port } : this.findConnectionTarget(
|
|
2147
3114
|
world,
|
|
@@ -2217,16 +3184,17 @@ var VdFlowchart = class {
|
|
|
2217
3184
|
if (this.readonly) return false;
|
|
2218
3185
|
if (this.interaction?.kind === "connect" || this.interaction?.kind === "reconnect") return true;
|
|
2219
3186
|
if (this.activeTool === "arrow") return true;
|
|
2220
|
-
return this.selection?.kind === "node" && this.selection.id === node.id;
|
|
3187
|
+
return this.selection?.kind === "node" && this.selection.id === node.id && !this.hasMultiSelection();
|
|
2221
3188
|
}
|
|
2222
|
-
getMarkerId(markerType, position, strokeWidth) {
|
|
3189
|
+
getMarkerId(markerType, position, strokeWidth, edgeScale = 1) {
|
|
2223
3190
|
const width = normalizeEdgeStrokeWidth(strokeWidth);
|
|
2224
|
-
const
|
|
3191
|
+
const scale = clamp(formatNumber(Math.max(edgeScale, 0.01)), 0.01, 1);
|
|
3192
|
+
const key = `${markerType}:${position}:${width}:${scale}`;
|
|
2225
3193
|
if (this.markerIds.has(key)) {
|
|
2226
3194
|
return this.markerIds.get(key);
|
|
2227
3195
|
}
|
|
2228
3196
|
const markerId = nextId(`flowchart-${markerType}-${position}`);
|
|
2229
|
-
const marker = markerType === "dot" ? createDotMarker(markerId, width) : createArrowMarker(markerId, width, position === "start");
|
|
3197
|
+
const marker = markerType === "dot" ? createDotMarker(markerId, width, scale) : createArrowMarker(markerId, width, position === "start", scale);
|
|
2230
3198
|
this.markerDefs.appendChild(marker);
|
|
2231
3199
|
this.markerIds.set(key, markerId);
|
|
2232
3200
|
return markerId;
|
|
@@ -2234,13 +3202,20 @@ var VdFlowchart = class {
|
|
|
2234
3202
|
getEdgeStrokeWidth(edge) {
|
|
2235
3203
|
return normalizeEdgeStrokeWidth(edge?.strokeWidth);
|
|
2236
3204
|
}
|
|
2237
|
-
getEdgeHitStrokeWidth(edge) {
|
|
2238
|
-
|
|
3205
|
+
getEdgeHitStrokeWidth(edge, edgeScale = 1) {
|
|
3206
|
+
const visual = clamp(toFiniteNumber(edgeScale, 1), 0.01, 1);
|
|
3207
|
+
const base = this.getEdgeStrokeWidth(edge);
|
|
3208
|
+
const view = Math.max(this.documentData?.viewport?.scale || 1, 1e-4);
|
|
3209
|
+
const legacy = Math.max(EDGE_HIT_STROKE_MIN, base + 12);
|
|
3210
|
+
if (visual >= 0.999 && view >= DEFAULT_MIN_SCALE && view <= DEFAULT_MAX_SCALE) {
|
|
3211
|
+
return formatNumber(legacy);
|
|
3212
|
+
}
|
|
3213
|
+
return formatNumber(Math.max(base * visual, EDGE_HIT_STROKE_MIN / view));
|
|
2239
3214
|
}
|
|
2240
3215
|
findConnectionTarget(worldPoint, excludeNodeId, previousSnap = null, referencePoint = null) {
|
|
2241
3216
|
const snapPadding = CONNECTION_SNAP_PADDING / this.documentData.viewport.scale;
|
|
2242
3217
|
const hysteresisMargin = CONNECTION_HYSTERESIS / this.documentData.viewport.scale;
|
|
2243
|
-
const best = this.
|
|
3218
|
+
const best = this.getVisibleNodes().reduce((candidate, node) => {
|
|
2244
3219
|
if (node.id === excludeNodeId) return candidate;
|
|
2245
3220
|
const distanceToBounds2 = getDistanceToNodeBounds(node, worldPoint);
|
|
2246
3221
|
if (distanceToBounds2 > snapPadding) return candidate;
|
|
@@ -2292,17 +3267,37 @@ var VdFlowchart = class {
|
|
|
2292
3267
|
let width = original.width;
|
|
2293
3268
|
let height = original.height;
|
|
2294
3269
|
if (handle.includes("e")) {
|
|
2295
|
-
width = clampNodeWidth(
|
|
3270
|
+
width = clampNodeWidth(
|
|
3271
|
+
nextType,
|
|
3272
|
+
original.width + deltaX,
|
|
3273
|
+
original.width,
|
|
3274
|
+
this.nodeSizeBounds()
|
|
3275
|
+
);
|
|
2296
3276
|
}
|
|
2297
3277
|
if (handle.includes("s")) {
|
|
2298
|
-
height = clampNodeHeight(
|
|
3278
|
+
height = clampNodeHeight(
|
|
3279
|
+
nextType,
|
|
3280
|
+
original.height + deltaY,
|
|
3281
|
+
original.height,
|
|
3282
|
+
this.nodeSizeBounds()
|
|
3283
|
+
);
|
|
2299
3284
|
}
|
|
2300
3285
|
if (handle.includes("w")) {
|
|
2301
|
-
width = clampNodeWidth(
|
|
3286
|
+
width = clampNodeWidth(
|
|
3287
|
+
nextType,
|
|
3288
|
+
original.width - deltaX,
|
|
3289
|
+
original.width,
|
|
3290
|
+
this.nodeSizeBounds()
|
|
3291
|
+
);
|
|
2302
3292
|
x = original.x + original.width - width;
|
|
2303
3293
|
}
|
|
2304
3294
|
if (handle.includes("n")) {
|
|
2305
|
-
height = clampNodeHeight(
|
|
3295
|
+
height = clampNodeHeight(
|
|
3296
|
+
nextType,
|
|
3297
|
+
original.height - deltaY,
|
|
3298
|
+
original.height,
|
|
3299
|
+
this.nodeSizeBounds()
|
|
3300
|
+
);
|
|
2306
3301
|
y = original.y + original.height - height;
|
|
2307
3302
|
}
|
|
2308
3303
|
return {
|
|
@@ -2338,7 +3333,7 @@ var VdFlowchart = class {
|
|
|
2338
3333
|
scaleAround(factor, localX, localY, reason) {
|
|
2339
3334
|
const viewport = this.documentData.viewport;
|
|
2340
3335
|
const anchor = this.localToWorld(localX, localY);
|
|
2341
|
-
const nextScale = clamp(
|
|
3336
|
+
const nextScale = clamp(formatScale(viewport.scale * factor), this.minScale, this.maxScale);
|
|
2342
3337
|
if (nextScale === viewport.scale) return;
|
|
2343
3338
|
viewport.scale = nextScale;
|
|
2344
3339
|
viewport.x = formatNumber(localX - anchor.x * nextScale);
|
|
@@ -2349,7 +3344,10 @@ var VdFlowchart = class {
|
|
|
2349
3344
|
updateToolbarLabel() {
|
|
2350
3345
|
this.zoomLabel.textContent = `${Math.round(this.documentData.viewport.scale * 100)}%`;
|
|
2351
3346
|
}
|
|
2352
|
-
|
|
3347
|
+
// Options: `initialText` replaces the label with typed text (type-to-edit);
|
|
3348
|
+
// `inserted` marks a node created by a keyboard/handle insertion so naming it
|
|
3349
|
+
// shares the insertion's undo step.
|
|
3350
|
+
startTextEdit(nodeId, options = {}) {
|
|
2353
3351
|
if (this.readonly) return false;
|
|
2354
3352
|
const node = this.findNode(nodeId);
|
|
2355
3353
|
if (!node || !isNodeTextEditable(node)) return false;
|
|
@@ -2361,9 +3359,10 @@ var VdFlowchart = class {
|
|
|
2361
3359
|
}
|
|
2362
3360
|
this.stopTextEdit({ commit: true });
|
|
2363
3361
|
this.select({ kind: "node", id: node.id });
|
|
3362
|
+
const replacing = typeof options.initialText === "string";
|
|
2364
3363
|
const textarea = createElement("textarea", {
|
|
2365
3364
|
className: `vd-flowchart-text-editor vd-flowchart-text-editor--${node.type}`,
|
|
2366
|
-
value: node.text,
|
|
3365
|
+
value: replacing ? options.initialText : node.text,
|
|
2367
3366
|
rows: 1
|
|
2368
3367
|
});
|
|
2369
3368
|
textarea.setAttribute("data-node-id", node.id);
|
|
@@ -2372,19 +3371,7 @@ var VdFlowchart = class {
|
|
|
2372
3371
|
textarea.addEventListener("input", () => this.positionTextEditor());
|
|
2373
3372
|
textarea.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
2374
3373
|
textarea.addEventListener("dblclick", (event) => event.stopPropagation());
|
|
2375
|
-
textarea.addEventListener("keydown", (event) =>
|
|
2376
|
-
if (event.key === "Escape") {
|
|
2377
|
-
event.preventDefault();
|
|
2378
|
-
this.stopTextEdit({ commit: false });
|
|
2379
|
-
this.canvasEl.focus();
|
|
2380
|
-
return;
|
|
2381
|
-
}
|
|
2382
|
-
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
2383
|
-
event.preventDefault();
|
|
2384
|
-
this.stopTextEdit({ commit: true });
|
|
2385
|
-
this.canvasEl.focus();
|
|
2386
|
-
}
|
|
2387
|
-
});
|
|
3374
|
+
textarea.addEventListener("keydown", (event) => this.handleTextEditorKeyDown(event, node));
|
|
2388
3375
|
textarea.addEventListener("blur", () => {
|
|
2389
3376
|
if (this.textEditor?.textarea === textarea) {
|
|
2390
3377
|
this.stopTextEdit({ commit: true });
|
|
@@ -2393,18 +3380,97 @@ var VdFlowchart = class {
|
|
|
2393
3380
|
this.textEditor = {
|
|
2394
3381
|
nodeId: node.id,
|
|
2395
3382
|
previousText: node.text,
|
|
3383
|
+
inserted: Boolean(options.inserted),
|
|
2396
3384
|
textarea
|
|
2397
3385
|
};
|
|
2398
3386
|
this.canvasEl.appendChild(textarea);
|
|
2399
3387
|
this.render({ inspector: true, json: false });
|
|
2400
|
-
|
|
2401
|
-
if (this.textEditor?.textarea
|
|
2402
|
-
|
|
3388
|
+
const focusEditor = () => {
|
|
3389
|
+
if (this.textEditor?.textarea !== textarea) return;
|
|
3390
|
+
textarea.focus();
|
|
3391
|
+
if (replacing) {
|
|
3392
|
+
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
3393
|
+
} else {
|
|
2403
3394
|
textarea.select();
|
|
2404
3395
|
}
|
|
3396
|
+
};
|
|
3397
|
+
focusEditor();
|
|
3398
|
+
window.requestAnimationFrame(() => {
|
|
3399
|
+
if (document.activeElement !== textarea) focusEditor();
|
|
3400
|
+
});
|
|
3401
|
+
return true;
|
|
3402
|
+
}
|
|
3403
|
+
// Inline label editor for a connection, placed on the label anchor
|
|
3404
|
+
// (curve midpoint). Labels render on one line, so Enter always saves.
|
|
3405
|
+
startEdgeLabelEdit(edgeId, options = {}) {
|
|
3406
|
+
if (this.readonly) return false;
|
|
3407
|
+
const edge = this.findEdge(sanitizeId(edgeId));
|
|
3408
|
+
if (!edge) return false;
|
|
3409
|
+
this.stopTextEdit({ commit: true });
|
|
3410
|
+
this.select({ kind: "edge", id: edge.id });
|
|
3411
|
+
const replacing = typeof options.initialText === "string";
|
|
3412
|
+
const textarea = createElement("textarea", {
|
|
3413
|
+
className: "vd-flowchart-text-editor vd-flowchart-text-editor--edge",
|
|
3414
|
+
value: replacing ? options.initialText : edge.label,
|
|
3415
|
+
rows: 1
|
|
3416
|
+
});
|
|
3417
|
+
textarea.setAttribute("data-edge-id", edge.id);
|
|
3418
|
+
textarea.setAttribute("aria-label", "Edit connection label");
|
|
3419
|
+
textarea.spellcheck = false;
|
|
3420
|
+
textarea.addEventListener("input", () => this.positionTextEditor());
|
|
3421
|
+
textarea.addEventListener("pointerdown", (event) => event.stopPropagation());
|
|
3422
|
+
textarea.addEventListener("dblclick", (event) => event.stopPropagation());
|
|
3423
|
+
textarea.addEventListener("keydown", (event) => {
|
|
3424
|
+
if (event.isComposing) return;
|
|
3425
|
+
if (event.key === "Escape" || event.key === "Enter") {
|
|
3426
|
+
event.preventDefault();
|
|
3427
|
+
this.stopTextEdit({ commit: event.key === "Enter" });
|
|
3428
|
+
this.canvasEl.focus();
|
|
3429
|
+
}
|
|
3430
|
+
});
|
|
3431
|
+
textarea.addEventListener("blur", () => {
|
|
3432
|
+
if (this.textEditor?.textarea === textarea) this.stopTextEdit({ commit: true });
|
|
3433
|
+
});
|
|
3434
|
+
this.textEditor = { nodeId: null, edgeId: edge.id, previousText: edge.label, textarea };
|
|
3435
|
+
this.canvasEl.appendChild(textarea);
|
|
3436
|
+
this.render({ inspector: true, json: false });
|
|
3437
|
+
const focusEditor = () => {
|
|
3438
|
+
if (this.textEditor?.textarea !== textarea) return;
|
|
3439
|
+
textarea.focus();
|
|
3440
|
+
if (replacing) textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
3441
|
+
else textarea.select();
|
|
3442
|
+
};
|
|
3443
|
+
focusEditor();
|
|
3444
|
+
window.requestAnimationFrame(() => {
|
|
3445
|
+
if (document.activeElement !== textarea) focusEditor();
|
|
2405
3446
|
});
|
|
2406
3447
|
return true;
|
|
2407
3448
|
}
|
|
3449
|
+
handleTextEditorKeyDown(event, node) {
|
|
3450
|
+
if (event.isComposing) return;
|
|
3451
|
+
const mindmap = this.keyboardShortcuts === "mindmap";
|
|
3452
|
+
if (event.key === "Escape") {
|
|
3453
|
+
event.preventDefault();
|
|
3454
|
+
this.stopTextEdit({ commit: false });
|
|
3455
|
+
this.canvasEl.focus();
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
if (event.key === "Enter") {
|
|
3459
|
+
const modKey = event.ctrlKey || event.metaKey;
|
|
3460
|
+
const commits = modKey || mindmap && node.type !== "textbox" && !event.shiftKey;
|
|
3461
|
+
if (!commits) return;
|
|
3462
|
+
event.preventDefault();
|
|
3463
|
+
this.stopTextEdit({ commit: true });
|
|
3464
|
+
this.canvasEl.focus();
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
if (event.key === "Tab" && mindmap && !event.shiftKey && !event.altKey && !event.ctrlKey) {
|
|
3468
|
+
event.preventDefault();
|
|
3469
|
+
this.stopTextEdit({ commit: true });
|
|
3470
|
+
this.canvasEl.focus();
|
|
3471
|
+
this.insertBranchNode(node.id, { edit: true });
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
2408
3474
|
stopTextEdit(options = {}) {
|
|
2409
3475
|
if (!this.textEditor) return false;
|
|
2410
3476
|
const commit = options.commit !== false;
|
|
@@ -2412,12 +3478,26 @@ var VdFlowchart = class {
|
|
|
2412
3478
|
const nextText = editor.textarea.value;
|
|
2413
3479
|
this.textEditor = null;
|
|
2414
3480
|
editor.textarea.remove();
|
|
3481
|
+
if (editor.edgeId) {
|
|
3482
|
+
const label = nextText.replace(/\s*\n\s*/g, " ").trim();
|
|
3483
|
+
if (commit && !this.readonly && label !== editor.previousText) {
|
|
3484
|
+
this.updateEdge(editor.edgeId, { label }, { inspector: true, reason: "edge:update" });
|
|
3485
|
+
} else {
|
|
3486
|
+
this.render({ scene: true, inspector: false, json: false });
|
|
3487
|
+
}
|
|
3488
|
+
return true;
|
|
3489
|
+
}
|
|
2415
3490
|
if (commit && !this.readonly && nextText !== editor.previousText) {
|
|
2416
|
-
this.
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
3491
|
+
this.coalesceInsertedText = editor.inserted;
|
|
3492
|
+
try {
|
|
3493
|
+
this.updateNode(
|
|
3494
|
+
editor.nodeId,
|
|
3495
|
+
{ text: nextText },
|
|
3496
|
+
{ inspector: true, reason: "node:update" }
|
|
3497
|
+
);
|
|
3498
|
+
} finally {
|
|
3499
|
+
this.coalesceInsertedText = false;
|
|
3500
|
+
}
|
|
2421
3501
|
} else {
|
|
2422
3502
|
this.render({ scene: true, inspector: false, json: false });
|
|
2423
3503
|
}
|
|
@@ -2425,6 +3505,10 @@ var VdFlowchart = class {
|
|
|
2425
3505
|
}
|
|
2426
3506
|
positionTextEditor() {
|
|
2427
3507
|
if (!this.textEditor) return;
|
|
3508
|
+
if (this.textEditor.edgeId) {
|
|
3509
|
+
this.positionEdgeLabelEditor();
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
2428
3512
|
const node = this.findNode(this.textEditor.nodeId);
|
|
2429
3513
|
if (!node) {
|
|
2430
3514
|
this.stopTextEdit({ commit: false });
|
|
@@ -2432,24 +3516,50 @@ var VdFlowchart = class {
|
|
|
2432
3516
|
}
|
|
2433
3517
|
const viewport = this.documentData.viewport;
|
|
2434
3518
|
const scale = viewport.scale;
|
|
3519
|
+
const visual = nodeVisualScale(node);
|
|
2435
3520
|
const textarea = this.textEditor.textarea;
|
|
2436
3521
|
const width = node.width * scale;
|
|
2437
3522
|
const height = node.height * scale;
|
|
2438
3523
|
const metrics = getNodeFontMetrics(node);
|
|
2439
|
-
const lineHeight = metrics.lineHeight * scale;
|
|
3524
|
+
const lineHeight = metrics.lineHeight * visual * scale;
|
|
2440
3525
|
const lineCount = Math.max(1, textarea.value.split(/\r?\n/).length);
|
|
2441
|
-
const horizontalPadding = (node.type === "textbox" ? 16 : 12) * scale;
|
|
2442
|
-
const verticalPadding = node.type === "textbox" ? 12 * scale : Math.max(4, (height - lineCount * lineHeight) / 2);
|
|
3526
|
+
const horizontalPadding = (node.type === "textbox" ? 16 : 12) * visual * scale;
|
|
3527
|
+
const verticalPadding = node.type === "textbox" ? 12 * visual * scale : Math.max(4 * visual, (height - lineCount * lineHeight) / 2);
|
|
2443
3528
|
textarea.style.left = `${formatNumber(viewport.x + node.x * scale)}px`;
|
|
2444
3529
|
textarea.style.top = `${formatNumber(viewport.y + node.y * scale)}px`;
|
|
2445
3530
|
textarea.style.width = `${formatNumber(width)}px`;
|
|
2446
3531
|
textarea.style.height = `${formatNumber(height)}px`;
|
|
2447
|
-
textarea.style.fontSize = `${formatNumber(metrics.fontSize * scale)}px`;
|
|
3532
|
+
textarea.style.fontSize = `${formatNumber(metrics.fontSize * visual * scale)}px`;
|
|
2448
3533
|
textarea.style.lineHeight = `${formatNumber(lineHeight)}px`;
|
|
2449
3534
|
textarea.style.padding = `${formatNumber(verticalPadding)}px ${formatNumber(horizontalPadding)}px`;
|
|
2450
3535
|
}
|
|
3536
|
+
positionEdgeLabelEditor() {
|
|
3537
|
+
const edge = this.findEdge(this.textEditor.edgeId);
|
|
3538
|
+
const fromNode = edge && this.findNode(edge.from.nodeId);
|
|
3539
|
+
const toNode = edge && this.findNode(edge.to.nodeId);
|
|
3540
|
+
if (!edge || !fromNode || !toNode) {
|
|
3541
|
+
this.stopTextEdit({ commit: false });
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
const visual = edgeVisualScale(fromNode, toNode);
|
|
3545
|
+
const { labelX, labelY } = buildEdgePath(edge, fromNode, toNode);
|
|
3546
|
+
const viewport = this.documentData.viewport;
|
|
3547
|
+
const textarea = this.textEditor.textarea;
|
|
3548
|
+
const fontSize = Math.max(11 * visual, 13 * visual * viewport.scale);
|
|
3549
|
+
const width = clamp(textarea.value.length * fontSize * 0.62 + 28 * visual, 96 * visual, 280);
|
|
3550
|
+
const height = fontSize * 1.4 + 12 * visual;
|
|
3551
|
+
const labelOffset = 8 * visual;
|
|
3552
|
+
textarea.style.left = `${formatNumber(viewport.x + labelX * viewport.scale - width / 2)}px`;
|
|
3553
|
+
textarea.style.top = `${formatNumber(viewport.y + (labelY - labelOffset) * viewport.scale - height / 2)}px`;
|
|
3554
|
+
textarea.style.width = `${formatNumber(width)}px`;
|
|
3555
|
+
textarea.style.height = `${formatNumber(height)}px`;
|
|
3556
|
+
textarea.style.fontSize = `${formatNumber(fontSize)}px`;
|
|
3557
|
+
textarea.style.lineHeight = `${formatNumber(fontSize * 1.4)}px`;
|
|
3558
|
+
textarea.style.padding = "6px 10px";
|
|
3559
|
+
}
|
|
2451
3560
|
render(options = {}) {
|
|
2452
3561
|
if (this.destroyed) return;
|
|
3562
|
+
this.syncGrid();
|
|
2453
3563
|
const shouldRenderScene = options.scene !== false;
|
|
2454
3564
|
const shouldRenderInspector = options.inspector !== false;
|
|
2455
3565
|
const shouldRenderJson = options.json !== false;
|
|
@@ -2460,8 +3570,10 @@ var VdFlowchart = class {
|
|
|
2460
3570
|
this.updateToolbarLabel();
|
|
2461
3571
|
this.updatePaletteState();
|
|
2462
3572
|
this.positionTextEditor();
|
|
3573
|
+
this.scheduleMinimap();
|
|
2463
3574
|
}
|
|
2464
3575
|
renderScene() {
|
|
3576
|
+
this.syncSelectedNodeIds();
|
|
2465
3577
|
const viewport = this.documentData.viewport;
|
|
2466
3578
|
this.world.setAttribute(
|
|
2467
3579
|
"transform",
|
|
@@ -2471,7 +3583,14 @@ var VdFlowchart = class {
|
|
|
2471
3583
|
clearChildren(this.previewLayer);
|
|
2472
3584
|
clearChildren(this.nodesLayer);
|
|
2473
3585
|
clearChildren(this.overlayLayer);
|
|
2474
|
-
|
|
3586
|
+
clearChildren(this.markerDefs);
|
|
3587
|
+
this.markerIds = /* @__PURE__ */ new Map();
|
|
3588
|
+
const hidden = this.getHiddenNodeIds();
|
|
3589
|
+
this.sceneHidden = hidden;
|
|
3590
|
+
this.sceneOutgoing = this.getOutgoingMap();
|
|
3591
|
+
const nodeMap = new Map(
|
|
3592
|
+
this.documentData.nodes.filter((node) => !hidden.has(node.id)).map((node) => [node.id, node])
|
|
3593
|
+
);
|
|
2475
3594
|
this.sceneNodeMap = nodeMap;
|
|
2476
3595
|
this.nodeElements = /* @__PURE__ */ new Map();
|
|
2477
3596
|
this.edgeElements = /* @__PURE__ */ new Map();
|
|
@@ -2491,6 +3610,7 @@ var VdFlowchart = class {
|
|
|
2491
3610
|
this.previewLayer.appendChild(this.renderPreviewEdge(this.interaction));
|
|
2492
3611
|
}
|
|
2493
3612
|
this.documentData.nodes.forEach((node) => {
|
|
3613
|
+
if (hidden.has(node.id)) return;
|
|
2494
3614
|
const element = this.renderNode(node);
|
|
2495
3615
|
this.nodesLayer.appendChild(element);
|
|
2496
3616
|
this.nodeElements.set(node.id, element);
|
|
@@ -2528,17 +3648,18 @@ var VdFlowchart = class {
|
|
|
2528
3648
|
}
|
|
2529
3649
|
}
|
|
2530
3650
|
}
|
|
2531
|
-
applyEdgeMarkers(pathElement, edge) {
|
|
3651
|
+
applyEdgeMarkers(pathElement, edge, edgeScale = 1) {
|
|
3652
|
+
const width = this.getEdgeStrokeWidth(edge);
|
|
2532
3653
|
if (edge.startMarker && edge.startMarker !== "none") {
|
|
2533
3654
|
pathElement.setAttribute(
|
|
2534
3655
|
"marker-start",
|
|
2535
|
-
`url(#${this.getMarkerId(edge.startMarker, "start",
|
|
3656
|
+
`url(#${this.getMarkerId(edge.startMarker, "start", width, edgeScale)})`
|
|
2536
3657
|
);
|
|
2537
3658
|
}
|
|
2538
3659
|
if (edge.endMarker && edge.endMarker !== "none") {
|
|
2539
3660
|
pathElement.setAttribute(
|
|
2540
3661
|
"marker-end",
|
|
2541
|
-
`url(#${this.getMarkerId(edge.endMarker, "end",
|
|
3662
|
+
`url(#${this.getMarkerId(edge.endMarker, "end", width, edgeScale)})`
|
|
2542
3663
|
);
|
|
2543
3664
|
}
|
|
2544
3665
|
}
|
|
@@ -2546,6 +3667,7 @@ var VdFlowchart = class {
|
|
|
2546
3667
|
const fromNode = nodeMap.get(edge.from.nodeId);
|
|
2547
3668
|
const toNode = nodeMap.get(edge.to.nodeId);
|
|
2548
3669
|
if (!fromNode || !toNode) return null;
|
|
3670
|
+
const visual = edgeVisualScale(fromNode, toNode);
|
|
2549
3671
|
const edgePath = buildEdgePath(edge, fromNode, toNode);
|
|
2550
3672
|
const selected = this.selection?.kind === "edge" && this.selection.id === edge.id;
|
|
2551
3673
|
const inReconnectMode = this.reconnectEdgeId === edge.id;
|
|
@@ -2553,13 +3675,14 @@ var VdFlowchart = class {
|
|
|
2553
3675
|
class: `vd-flowchart-edge${selected ? " is-selected" : ""}${inReconnectMode ? " is-reconnecting" : ""}`,
|
|
2554
3676
|
"data-edge-id": edge.id
|
|
2555
3677
|
});
|
|
2556
|
-
const
|
|
3678
|
+
const baseStroke = this.getEdgeStrokeWidth(edge);
|
|
3679
|
+
const strokeWidth = visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke;
|
|
2557
3680
|
if (selected) {
|
|
2558
3681
|
group.appendChild(
|
|
2559
3682
|
svgEl("path", {
|
|
2560
3683
|
class: "vd-flowchart-edge-selection",
|
|
2561
3684
|
d: edgePath.d,
|
|
2562
|
-
"stroke-width": formatNumber(strokeWidth + 7)
|
|
3685
|
+
"stroke-width": visual < 0.999 ? formatNumber((baseStroke + 7) * visual) : formatNumber(strokeWidth + 7)
|
|
2563
3686
|
})
|
|
2564
3687
|
);
|
|
2565
3688
|
}
|
|
@@ -2569,23 +3692,28 @@ var VdFlowchart = class {
|
|
|
2569
3692
|
"stroke-width": strokeWidth,
|
|
2570
3693
|
"data-edge-id": edge.id
|
|
2571
3694
|
});
|
|
2572
|
-
this.applyEdgeMarkers(visiblePath, edge);
|
|
3695
|
+
this.applyEdgeMarkers(visiblePath, edge, visual);
|
|
2573
3696
|
const hitPath = svgEl("path", {
|
|
2574
3697
|
class: "vd-flowchart-edge-hit",
|
|
2575
3698
|
d: edgePath.d,
|
|
2576
|
-
"stroke-width": this.getEdgeHitStrokeWidth(edge),
|
|
3699
|
+
"stroke-width": this.getEdgeHitStrokeWidth(edge, visual),
|
|
2577
3700
|
"data-edge-id": edge.id
|
|
2578
3701
|
});
|
|
2579
3702
|
group.appendChild(visiblePath);
|
|
2580
3703
|
group.appendChild(hitPath);
|
|
2581
|
-
if (edge.label) {
|
|
3704
|
+
if (edge.label && this.textEditor?.edgeId !== edge.id) {
|
|
3705
|
+
const labelOffset = visual < 0.999 ? formatNumber(8 * visual) : 8;
|
|
2582
3706
|
const label = svgEl("text", {
|
|
2583
3707
|
class: "vd-flowchart-edge-label",
|
|
2584
3708
|
x: edgePath.labelX,
|
|
2585
|
-
y: edgePath.labelY -
|
|
3709
|
+
y: edgePath.labelY - labelOffset,
|
|
2586
3710
|
"text-anchor": "middle",
|
|
2587
3711
|
"data-edge-id": edge.id
|
|
2588
3712
|
});
|
|
3713
|
+
if (visual < 0.999) {
|
|
3714
|
+
label.style.fontSize = `${formatNumber(Math.max(0.06, 12 * visual))}px`;
|
|
3715
|
+
label.style.strokeWidth = `${formatNumber(Math.max(0.03, 5 * visual))}px`;
|
|
3716
|
+
}
|
|
2589
3717
|
label.textContent = edge.label;
|
|
2590
3718
|
group.appendChild(label);
|
|
2591
3719
|
}
|
|
@@ -2624,23 +3752,32 @@ var VdFlowchart = class {
|
|
|
2624
3752
|
renderPreviewEdge(interaction) {
|
|
2625
3753
|
const fromPort = interaction.fromPort || interaction.source?.port || "right";
|
|
2626
3754
|
const toPort = interaction.toPort || interaction.target?.port || "left";
|
|
2627
|
-
const
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
3755
|
+
const fromNode = interaction.source?.nodeId ? this.findNode(interaction.source.nodeId) : null;
|
|
3756
|
+
const toNode = interaction.target?.nodeId ? this.findNode(interaction.target.nodeId) : null;
|
|
3757
|
+
const visual = edgeVisualScale(fromNode, toNode);
|
|
3758
|
+
const edgePath = buildEdgePath(
|
|
3759
|
+
{
|
|
3760
|
+
strokeWidth: interaction.strokeWidth,
|
|
3761
|
+
route: interaction.route,
|
|
3762
|
+
fromPoint: interaction.fromPoint,
|
|
3763
|
+
toPoint: interaction.toPoint,
|
|
3764
|
+
from: { port: fromPort },
|
|
3765
|
+
to: { port: toPort }
|
|
3766
|
+
},
|
|
3767
|
+
fromNode,
|
|
3768
|
+
toNode
|
|
3769
|
+
);
|
|
3770
|
+
const baseStroke = this.getEdgeStrokeWidth(interaction);
|
|
2635
3771
|
return svgEl("path", {
|
|
2636
3772
|
class: "vd-flowchart-preview-path",
|
|
2637
3773
|
d: edgePath.d,
|
|
2638
|
-
"stroke-width":
|
|
3774
|
+
"stroke-width": visual < 0.999 ? formatNumber(Math.max(0.02, baseStroke * visual)) : baseStroke
|
|
2639
3775
|
});
|
|
2640
3776
|
}
|
|
2641
3777
|
renderNode(node) {
|
|
2642
|
-
const selected = this.
|
|
2643
|
-
const
|
|
3778
|
+
const selected = this.isNodeSelected(node.id);
|
|
3779
|
+
const soloSelected = selected && !this.hasMultiSelection();
|
|
3780
|
+
const dragging = this.interaction?.kind === "drag-node" && this.interaction.nodeIds.includes(node.id);
|
|
2644
3781
|
const resizing = this.interaction?.kind === "resize-node" && this.interaction.nodeId === node.id;
|
|
2645
3782
|
const editing = this.textEditor?.nodeId === node.id;
|
|
2646
3783
|
const group = svgEl("g", {
|
|
@@ -2649,24 +3786,45 @@ var VdFlowchart = class {
|
|
|
2649
3786
|
"data-node-id": node.id
|
|
2650
3787
|
});
|
|
2651
3788
|
const portsVisible = this.shouldShowNodePorts(node);
|
|
3789
|
+
const visual = nodeVisualScale(node);
|
|
3790
|
+
const corner = node.type === "rounded-rect" || node.type === "textbox" ? 16 : node.type === "junction" ? node.width / 2 : 6;
|
|
2652
3791
|
const hitbox = svgEl("rect", {
|
|
2653
3792
|
x: 0,
|
|
2654
3793
|
y: 0,
|
|
2655
3794
|
width: node.width,
|
|
2656
3795
|
height: node.height,
|
|
2657
|
-
rx:
|
|
3796
|
+
rx: visual < 0.999 && node.type !== "junction" ? formatNumber(corner * visual) : corner,
|
|
2658
3797
|
fill: "transparent",
|
|
2659
3798
|
"data-node-id": node.id
|
|
2660
3799
|
});
|
|
2661
3800
|
group.appendChild(hitbox);
|
|
2662
|
-
|
|
3801
|
+
const shape = this.renderNodeShape(node);
|
|
3802
|
+
this.applyScaledNodeShape(node, shape);
|
|
3803
|
+
group.appendChild(shape);
|
|
2663
3804
|
group.appendChild(this.renderNodeText(node));
|
|
2664
|
-
|
|
3805
|
+
const scale = this.documentData.viewport.scale || 1;
|
|
3806
|
+
if (soloSelected && !this.readonly && SELECTION_BOX_TYPES.has(node.type)) {
|
|
3807
|
+
const outset = SELECTION_OUTSET / scale;
|
|
3808
|
+
group.appendChild(
|
|
3809
|
+
svgEl("rect", {
|
|
3810
|
+
class: "vd-flowchart-selection-box",
|
|
3811
|
+
x: -outset,
|
|
3812
|
+
y: -outset,
|
|
3813
|
+
width: node.width + outset * 2,
|
|
3814
|
+
height: node.height + outset * 2
|
|
3815
|
+
})
|
|
3816
|
+
);
|
|
3817
|
+
}
|
|
3818
|
+
if (soloSelected && !this.readonly && isNodeResizable(node)) {
|
|
2665
3819
|
group.appendChild(this.renderResizeControls(node));
|
|
2666
3820
|
}
|
|
2667
|
-
const
|
|
3821
|
+
const collapseToggle = this.renderCollapseToggle(node, soloSelected);
|
|
3822
|
+
if (this.readonly) {
|
|
3823
|
+
if (collapseToggle) group.appendChild(collapseToggle);
|
|
3824
|
+
return group;
|
|
3825
|
+
}
|
|
2668
3826
|
FLOWCHART_PORTS.forEach((port) => {
|
|
2669
|
-
const position =
|
|
3827
|
+
const position = getPortHandlePosition({ ...node, x: 0, y: 0 }, port, scale);
|
|
2670
3828
|
const portGroup = svgEl("g", {
|
|
2671
3829
|
class: `vd-flowchart-port-group${portsVisible ? " is-visible" : ""}`,
|
|
2672
3830
|
"data-node-id": node.id,
|
|
@@ -2688,15 +3846,68 @@ var VdFlowchart = class {
|
|
|
2688
3846
|
r: CONNECTION_PORT_RADIUS / scale
|
|
2689
3847
|
})
|
|
2690
3848
|
);
|
|
3849
|
+
const arm = (CONNECTION_PORT_RADIUS - 2.5) / scale;
|
|
3850
|
+
portGroup.appendChild(
|
|
3851
|
+
svgEl("path", {
|
|
3852
|
+
class: "vd-flowchart-port-plus",
|
|
3853
|
+
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)}`
|
|
3854
|
+
})
|
|
3855
|
+
);
|
|
2691
3856
|
group.appendChild(portGroup);
|
|
2692
3857
|
});
|
|
3858
|
+
if (collapseToggle) group.appendChild(collapseToggle);
|
|
3859
|
+
return group;
|
|
3860
|
+
}
|
|
3861
|
+
// "+N" on a collapsed node, or "−" on a single selected node with children,
|
|
3862
|
+
// placed beyond the connection handle on the side its branch leaves from.
|
|
3863
|
+
renderCollapseToggle(node, soloSelected) {
|
|
3864
|
+
const outgoing = this.sceneOutgoing || this.getOutgoingMap();
|
|
3865
|
+
const childIds = (outgoing.get(node.id) || []).filter((id) => id !== node.id);
|
|
3866
|
+
if (!childIds.length) return null;
|
|
3867
|
+
const count = node.collapsed ? this.countCollapsedDescendants(
|
|
3868
|
+
node.id,
|
|
3869
|
+
this.sceneHidden || this.getHiddenNodeIds(),
|
|
3870
|
+
outgoing
|
|
3871
|
+
) : 0;
|
|
3872
|
+
if (!node.collapsed && !(soloSelected && !this.readonly)) return null;
|
|
3873
|
+
const sides = /* @__PURE__ */ new Map();
|
|
3874
|
+
this.documentData.edges.forEach((edge) => {
|
|
3875
|
+
if (edge.from.nodeId !== node.id || edge.to.nodeId === node.id) return;
|
|
3876
|
+
sides.set(edge.from.port, (sides.get(edge.from.port) || 0) + 1);
|
|
3877
|
+
});
|
|
3878
|
+
const port = [...sides].sort((a, b) => b[1] - a[1])[0]?.[0] || DIRECTION_PORTS[this.getGrowthDirection(node)];
|
|
3879
|
+
const scale = this.documentData.viewport.scale || 1;
|
|
3880
|
+
const anchor = getPortPosition({ ...node, x: 0, y: 0 }, port);
|
|
3881
|
+
const normal = getPortNormal(port);
|
|
3882
|
+
const offset = COLLAPSE_TOGGLE_OFFSET / scale;
|
|
3883
|
+
const cx = formatNumber(anchor.x + normal.x * offset);
|
|
3884
|
+
const cy = formatNumber(anchor.y + normal.y * offset);
|
|
3885
|
+
const label = node.collapsed ? `+${count}` : "\u2212";
|
|
3886
|
+
const radius = (node.collapsed && label.length > 2 ? 12 : 9) / scale;
|
|
3887
|
+
const group = svgEl("g", {
|
|
3888
|
+
class: `vd-flowchart-collapse-toggle${node.collapsed ? " is-collapsed" : ""}`,
|
|
3889
|
+
"data-collapse-toggle": node.id
|
|
3890
|
+
});
|
|
3891
|
+
const title = svgEl("title");
|
|
3892
|
+
title.textContent = node.collapsed ? `Expand ${count} hidden nodes` : "Collapse branch";
|
|
3893
|
+
group.appendChild(title);
|
|
3894
|
+
group.appendChild(svgEl("circle", { cx, cy, r: formatNumber(radius) }));
|
|
3895
|
+
const text = svgEl("text", {
|
|
3896
|
+
x: cx,
|
|
3897
|
+
y: cy,
|
|
3898
|
+
"font-size": formatNumber(10 / scale),
|
|
3899
|
+
"text-anchor": "middle",
|
|
3900
|
+
"dominant-baseline": "central"
|
|
3901
|
+
});
|
|
3902
|
+
text.textContent = label;
|
|
3903
|
+
group.appendChild(text);
|
|
2693
3904
|
return group;
|
|
2694
3905
|
}
|
|
2695
3906
|
renderResizeControls(node) {
|
|
2696
3907
|
const scale = this.documentData.viewport.scale || 1;
|
|
2697
3908
|
const zone = 14 / scale;
|
|
2698
3909
|
const cornerZone = 22 / scale;
|
|
2699
|
-
const handleSize =
|
|
3910
|
+
const handleSize = RESIZE_HANDLE_SIZE / scale;
|
|
2700
3911
|
const gap = RESIZE_PORT_GAP / scale;
|
|
2701
3912
|
const group = svgEl("g", { class: "vd-flowchart-resize-controls" });
|
|
2702
3913
|
const zones = {
|
|
@@ -2726,12 +3937,6 @@ var VdFlowchart = class {
|
|
|
2726
3937
|
};
|
|
2727
3938
|
RESIZE_HANDLES.forEach((handle) => {
|
|
2728
3939
|
const zoneRect = zones[handle];
|
|
2729
|
-
const position = getResizeHandlePosition(node, handle);
|
|
2730
|
-
const dotOffset = 8 / scale;
|
|
2731
|
-
if (handle.includes("e")) position.x += dotOffset;
|
|
2732
|
-
if (handle.includes("w")) position.x -= dotOffset;
|
|
2733
|
-
if (handle.includes("n")) position.y -= dotOffset;
|
|
2734
|
-
if (handle.includes("s")) position.y += dotOffset;
|
|
2735
3940
|
const cursor = getResizeCursor(handle);
|
|
2736
3941
|
const zoneSegments = [];
|
|
2737
3942
|
if (handle === "e" || handle === "w") {
|
|
@@ -2766,14 +3971,19 @@ var VdFlowchart = class {
|
|
|
2766
3971
|
});
|
|
2767
3972
|
group.appendChild(hit);
|
|
2768
3973
|
});
|
|
3974
|
+
if (handle.length === 1) return;
|
|
3975
|
+
const position = getResizeHandlePosition(node, handle);
|
|
3976
|
+
const outset = SELECTION_OUTSET / scale;
|
|
3977
|
+
position.x += handle.includes("e") ? outset : -outset;
|
|
3978
|
+
position.y += handle.includes("s") ? outset : -outset;
|
|
2769
3979
|
const dot = svgEl("rect", {
|
|
2770
3980
|
class: "vd-flowchart-resize-handle",
|
|
2771
3981
|
x: position.x - handleSize / 2,
|
|
2772
3982
|
y: position.y - handleSize / 2,
|
|
2773
3983
|
width: handleSize,
|
|
2774
3984
|
height: handleSize,
|
|
2775
|
-
rx: handleSize /
|
|
2776
|
-
ry: handleSize /
|
|
3985
|
+
rx: handleSize / 5,
|
|
3986
|
+
ry: handleSize / 5,
|
|
2777
3987
|
"data-node-id": node.id,
|
|
2778
3988
|
"data-resize-handle": handle,
|
|
2779
3989
|
style: `cursor: ${cursor}`
|
|
@@ -2782,6 +3992,21 @@ var VdFlowchart = class {
|
|
|
2782
3992
|
});
|
|
2783
3993
|
return group;
|
|
2784
3994
|
}
|
|
3995
|
+
applyScaledNodeShape(node, shape) {
|
|
3996
|
+
const visual = nodeVisualScale(node);
|
|
3997
|
+
if (!shape || visual >= 0.999) return;
|
|
3998
|
+
const baseStroke = node.type === "junction" ? 1.25 : 1.5;
|
|
3999
|
+
shape.style.strokeWidth = String(formatNumber(Math.max(0.03, baseStroke * visual)));
|
|
4000
|
+
if (node.type === "textbox") {
|
|
4001
|
+
shape.style.strokeDasharray = `${formatNumber(8 * visual)} ${formatNumber(5 * visual)}`;
|
|
4002
|
+
}
|
|
4003
|
+
const baseRadius = node.type === "rounded-rect" ? 18 : node.type === "textbox" ? 14 : node.type === "rect" ? 2 : 0;
|
|
4004
|
+
if (baseRadius > 0) {
|
|
4005
|
+
const scaled = String(formatNumber(baseRadius * visual));
|
|
4006
|
+
shape.setAttribute("rx", scaled);
|
|
4007
|
+
shape.setAttribute("ry", scaled);
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
2785
4010
|
renderNodeShape(node) {
|
|
2786
4011
|
const baseClass = `vd-flowchart-node-shape vd-flowchart-node-shape--${node.type}`;
|
|
2787
4012
|
if (node.type === "rounded-rect") {
|
|
@@ -2858,21 +4083,28 @@ var VdFlowchart = class {
|
|
|
2858
4083
|
if (node.type === "junction" || !node.text) {
|
|
2859
4084
|
return textElement;
|
|
2860
4085
|
}
|
|
4086
|
+
const visual = nodeVisualScale(node);
|
|
4087
|
+
const metrics = getNodeFontMetrics(node);
|
|
4088
|
+
const lineHeight = visual < 0.999 ? Math.max(0.06, metrics.lineHeight * visual) : metrics.lineHeight;
|
|
4089
|
+
if (visual < 0.999) {
|
|
4090
|
+
textElement.style.fontSize = `${formatNumber(Math.max(0.05, metrics.fontSize * visual))}px`;
|
|
4091
|
+
}
|
|
4092
|
+
const equivalentWidth = visual < 0.999 ? node.width / visual : node.width;
|
|
2861
4093
|
if (node.type === "textbox") {
|
|
2862
|
-
const lines2 = wrapText(node.text, estimateChars(
|
|
2863
|
-
const
|
|
4094
|
+
const lines2 = wrapText(node.text, estimateChars(equivalentWidth - 32));
|
|
4095
|
+
const padX = visual < 0.999 ? formatNumber(16 * visual) : 16;
|
|
4096
|
+
const startY2 = visual < 0.999 ? formatNumber(28 * visual) : 28;
|
|
2864
4097
|
lines2.forEach((line, index) => {
|
|
2865
4098
|
const span = svgEl("tspan", {
|
|
2866
|
-
x:
|
|
2867
|
-
y:
|
|
4099
|
+
x: padX,
|
|
4100
|
+
y: visual < 0.999 ? formatNumber(startY2 + index * lineHeight) : startY2 + index * lineHeight
|
|
2868
4101
|
});
|
|
2869
4102
|
span.textContent = line;
|
|
2870
4103
|
textElement.appendChild(span);
|
|
2871
4104
|
});
|
|
2872
4105
|
return textElement;
|
|
2873
4106
|
}
|
|
2874
|
-
const lines = wrapText(node.text, estimateChars(
|
|
2875
|
-
const { lineHeight } = getNodeFontMetrics(node);
|
|
4107
|
+
const lines = wrapText(node.text, estimateChars(equivalentWidth));
|
|
2876
4108
|
const totalHeight = (lines.length - 1) * lineHeight;
|
|
2877
4109
|
const startY = node.height / 2 - totalHeight / 2;
|
|
2878
4110
|
textElement.setAttribute("text-anchor", "middle");
|
|
@@ -2880,7 +4112,7 @@ var VdFlowchart = class {
|
|
|
2880
4112
|
lines.forEach((line, index) => {
|
|
2881
4113
|
const span = svgEl("tspan", {
|
|
2882
4114
|
x: node.width / 2,
|
|
2883
|
-
y: startY + index * lineHeight,
|
|
4115
|
+
y: visual < 0.999 ? formatNumber(startY + index * lineHeight) : startY + index * lineHeight,
|
|
2884
4116
|
"text-anchor": "middle",
|
|
2885
4117
|
"dominant-baseline": "middle"
|
|
2886
4118
|
});
|
|
@@ -2902,6 +4134,16 @@ var VdFlowchart = class {
|
|
|
2902
4134
|
);
|
|
2903
4135
|
return;
|
|
2904
4136
|
}
|
|
4137
|
+
if (this.hasMultiSelection()) {
|
|
4138
|
+
this.selectionMeta.textContent = `${this.selectedNodeIds.size} nodes selected`;
|
|
4139
|
+
this.selectionFields.appendChild(
|
|
4140
|
+
createElement("p", {
|
|
4141
|
+
className: "vd-flowchart-selection-empty",
|
|
4142
|
+
text: "Drag, nudge with Alt+Arrow, copy, duplicate, or delete them together."
|
|
4143
|
+
})
|
|
4144
|
+
);
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
2905
4147
|
if (this.selection.kind === "node") {
|
|
2906
4148
|
const node = this.findNode(this.selection.id);
|
|
2907
4149
|
if (!node) {
|
|
@@ -2922,12 +4164,12 @@ var VdFlowchart = class {
|
|
|
2922
4164
|
textArea.setAttribute("data-field", "node-text");
|
|
2923
4165
|
const widthInput = createElement("input", { value: node.width, type: "number" });
|
|
2924
4166
|
widthInput.setAttribute("data-field", "node-width");
|
|
2925
|
-
widthInput.setAttribute("min", String(
|
|
2926
|
-
widthInput.setAttribute("max", String(
|
|
4167
|
+
widthInput.setAttribute("min", String(this.minNodeSize));
|
|
4168
|
+
widthInput.setAttribute("max", String(this.maxNodeSize));
|
|
2927
4169
|
const heightInput = createElement("input", { value: node.height, type: "number" });
|
|
2928
4170
|
heightInput.setAttribute("data-field", "node-height");
|
|
2929
|
-
heightInput.setAttribute("min", String(
|
|
2930
|
-
heightInput.setAttribute("max", String(
|
|
4171
|
+
heightInput.setAttribute("min", String(this.minNodeSize));
|
|
4172
|
+
heightInput.setAttribute("max", String(this.maxNodeSize));
|
|
2931
4173
|
this.selectionFields.appendChild(createField("Type", typeSelect));
|
|
2932
4174
|
if (isNodeTextEditable(node)) {
|
|
2933
4175
|
this.selectionFields.appendChild(createField("Text", textArea));
|
|
@@ -3016,16 +4258,69 @@ var VdFlowchart = class {
|
|
|
3016
4258
|
}
|
|
3017
4259
|
select(selection) {
|
|
3018
4260
|
const next = selection && selection.id && selection.kind ? { kind: selection.kind, id: selection.id } : null;
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
4261
|
+
this.applySelection(next, next?.kind === "node" ? [next.id] : []);
|
|
4262
|
+
}
|
|
4263
|
+
// Single funnel for selection changes: `selection` is the primary item and
|
|
4264
|
+
// `selectedNodeIds` every selected node (the primary one included).
|
|
4265
|
+
applySelection(primary, nodeIds) {
|
|
4266
|
+
const previousKey = this.getSelectionKey();
|
|
4267
|
+
this.selection = primary;
|
|
4268
|
+
this.selectedNodeIds = new Set(nodeIds);
|
|
4269
|
+
if (primary?.kind === "node") this.selectedNodeIds.add(primary.id);
|
|
4270
|
+
if (previousKey !== this.getSelectionKey()) {
|
|
4271
|
+
this.edgeCycleAnchor = null;
|
|
3023
4272
|
this.render({ scene: true, inspector: true, json: false });
|
|
3024
|
-
this.emit("select", {
|
|
4273
|
+
this.emit("select", {
|
|
4274
|
+
selection: this.getSelectionSnapshot(),
|
|
4275
|
+
nodeIds: this.getSelectedNodeIds()
|
|
4276
|
+
});
|
|
3025
4277
|
return;
|
|
3026
4278
|
}
|
|
3027
4279
|
this.render({ scene: true, inspector: false, json: false });
|
|
3028
4280
|
}
|
|
4281
|
+
getSelectionKey() {
|
|
4282
|
+
const primary = this.selection ? `${this.selection.kind}:${this.selection.id}` : "";
|
|
4283
|
+
return `${primary}|${[...this.selectedNodeIds || []].sort().join(",")}`;
|
|
4284
|
+
}
|
|
4285
|
+
// Keep the set valid after mutations that bypass applySelection (undo,
|
|
4286
|
+
// load, removal): drop missing nodes and keep the primary node in the set.
|
|
4287
|
+
syncSelectedNodeIds() {
|
|
4288
|
+
if (this.selection?.kind !== "node") {
|
|
4289
|
+
this.selectedNodeIds = /* @__PURE__ */ new Set();
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
const existing = new Set(this.documentData.nodes.map((node) => node.id));
|
|
4293
|
+
const hidden = this.getHiddenNodeIds();
|
|
4294
|
+
this.selectedNodeIds = new Set(
|
|
4295
|
+
[...this.selectedNodeIds || []].filter((id) => existing.has(id) && !hidden.has(id))
|
|
4296
|
+
);
|
|
4297
|
+
this.selectedNodeIds.add(this.selection.id);
|
|
4298
|
+
}
|
|
4299
|
+
getSelectedNodeIds() {
|
|
4300
|
+
return [...this.selectedNodeIds || []];
|
|
4301
|
+
}
|
|
4302
|
+
isNodeSelected(nodeId) {
|
|
4303
|
+
return Boolean(this.selectedNodeIds?.has(nodeId));
|
|
4304
|
+
}
|
|
4305
|
+
hasMultiSelection() {
|
|
4306
|
+
return (this.selectedNodeIds?.size || 0) > 1;
|
|
4307
|
+
}
|
|
4308
|
+
/** Select several nodes; `primary` (default: the last id) drives single-node UI. */
|
|
4309
|
+
selectNodes(nodeIds, options = {}) {
|
|
4310
|
+
const existing = new Set(this.documentData.nodes.map((node) => node.id));
|
|
4311
|
+
const ids = [...new Set((nodeIds || []).map(sanitizeId))].filter((id) => existing.has(id));
|
|
4312
|
+
const primaryId = ids.includes(options.primary) ? options.primary : ids[ids.length - 1];
|
|
4313
|
+
this.applySelection(primaryId ? { kind: "node", id: primaryId } : null, ids);
|
|
4314
|
+
return this;
|
|
4315
|
+
}
|
|
4316
|
+
toggleNodeSelection(nodeId) {
|
|
4317
|
+
const ids = this.selection?.kind === "node" ? this.getSelectedNodeIds() : [];
|
|
4318
|
+
if (ids.includes(nodeId)) {
|
|
4319
|
+
const rest = ids.filter((id) => id !== nodeId);
|
|
4320
|
+
return this.selectNodes(rest, { primary: rest[rest.length - 1] });
|
|
4321
|
+
}
|
|
4322
|
+
return this.selectNodes([...ids, nodeId], { primary: nodeId });
|
|
4323
|
+
}
|
|
3029
4324
|
selectNode(nodeId) {
|
|
3030
4325
|
this.select({ kind: "node", id: sanitizeId(nodeId) });
|
|
3031
4326
|
return this;
|
|
@@ -3046,6 +4341,69 @@ var VdFlowchart = class {
|
|
|
3046
4341
|
findNode(nodeId) {
|
|
3047
4342
|
return this.documentData.nodes.find((node) => node.id === nodeId) || null;
|
|
3048
4343
|
}
|
|
4344
|
+
getHiddenNodeIds() {
|
|
4345
|
+
return computeHiddenNodeIds(this.documentData.nodes, this.documentData.edges);
|
|
4346
|
+
}
|
|
4347
|
+
getVisibleNodes() {
|
|
4348
|
+
const hidden = this.getHiddenNodeIds();
|
|
4349
|
+
return hidden.size ? this.documentData.nodes.filter((node) => !hidden.has(node.id)) : this.documentData.nodes;
|
|
4350
|
+
}
|
|
4351
|
+
isNodeHidden(nodeId) {
|
|
4352
|
+
return this.getHiddenNodeIds().has(nodeId);
|
|
4353
|
+
}
|
|
4354
|
+
getOutgoingMap() {
|
|
4355
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
4356
|
+
this.documentData.edges.forEach((edge) => {
|
|
4357
|
+
if (!outgoing.has(edge.from.nodeId)) outgoing.set(edge.from.nodeId, []);
|
|
4358
|
+
outgoing.get(edge.from.nodeId).push(edge.to.nodeId);
|
|
4359
|
+
});
|
|
4360
|
+
return outgoing;
|
|
4361
|
+
}
|
|
4362
|
+
// Hidden nodes under one collapsed node (its "+N" count).
|
|
4363
|
+
countCollapsedDescendants(nodeId, hidden = this.getHiddenNodeIds(), outgoing = this.getOutgoingMap()) {
|
|
4364
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4365
|
+
const queue = [nodeId];
|
|
4366
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
4367
|
+
(outgoing.get(queue[index]) || []).forEach((childId) => {
|
|
4368
|
+
if (seen.has(childId) || !hidden.has(childId)) return;
|
|
4369
|
+
seen.add(childId);
|
|
4370
|
+
queue.push(childId);
|
|
4371
|
+
});
|
|
4372
|
+
}
|
|
4373
|
+
return seen.size;
|
|
4374
|
+
}
|
|
4375
|
+
setCollapsed(nodeId, collapsed) {
|
|
4376
|
+
if (this.readonly) return false;
|
|
4377
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4378
|
+
if (!node) return false;
|
|
4379
|
+
const next = Boolean(collapsed);
|
|
4380
|
+
if (Boolean(node.collapsed) === next) return false;
|
|
4381
|
+
if (next && !this.getChildNodes(node.id).length) return false;
|
|
4382
|
+
if (next) node.collapsed = true;
|
|
4383
|
+
else delete node.collapsed;
|
|
4384
|
+
if (this.selection?.kind === "node") {
|
|
4385
|
+
const hidden = this.getHiddenNodeIds();
|
|
4386
|
+
const visibleIds = this.getSelectedNodeIds().filter((id) => !hidden.has(id));
|
|
4387
|
+
if (hidden.has(this.selection.id)) {
|
|
4388
|
+
this.select({ kind: "node", id: node.id });
|
|
4389
|
+
} else if (visibleIds.length !== this.selectedNodeIds.size) {
|
|
4390
|
+
this.selectNodes(visibleIds, { primary: this.selection.id });
|
|
4391
|
+
}
|
|
4392
|
+
} else if (this.selection?.kind === "edge") {
|
|
4393
|
+
const edge = this.findEdge(this.selection.id);
|
|
4394
|
+
const hidden = this.getHiddenNodeIds();
|
|
4395
|
+
if (edge && (hidden.has(edge.from.nodeId) || hidden.has(edge.to.nodeId))) {
|
|
4396
|
+
this.select({ kind: "node", id: node.id });
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
this.render({ inspector: true, json: true });
|
|
4400
|
+
this.emitChange("node:collapse", { node: deepClone(node) });
|
|
4401
|
+
return true;
|
|
4402
|
+
}
|
|
4403
|
+
toggleCollapsed(nodeId) {
|
|
4404
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4405
|
+
return node ? this.setCollapsed(node.id, !node.collapsed) : false;
|
|
4406
|
+
}
|
|
3049
4407
|
findEdge(edgeId) {
|
|
3050
4408
|
return this.documentData.edges.find((edge) => edge.id === edgeId) || null;
|
|
3051
4409
|
}
|
|
@@ -3056,7 +4414,8 @@ var VdFlowchart = class {
|
|
|
3056
4414
|
}
|
|
3057
4415
|
emitChange(reason, extra = {}) {
|
|
3058
4416
|
if (this.historyEnabled && !this.isApplyingHistory) {
|
|
3059
|
-
this.
|
|
4417
|
+
if (this.historyBatchDepth > 0) this.historyBatchDirty = true;
|
|
4418
|
+
else this.recordHistory(reason);
|
|
3060
4419
|
}
|
|
3061
4420
|
this.syncJsonTextarea();
|
|
3062
4421
|
this.emit("change", {
|
|
@@ -3100,7 +4459,9 @@ var VdFlowchart = class {
|
|
|
3100
4459
|
const snapshot = this.toJSON();
|
|
3101
4460
|
const targetKey = this.selection ? `${this.selection.kind}:${this.selection.id}` : "";
|
|
3102
4461
|
const top = this.history[this.historyIndex];
|
|
3103
|
-
const
|
|
4462
|
+
const atTip = Boolean(top) && this.historyIndex === this.history.length - 1;
|
|
4463
|
+
const namesInsertedNode = this.coalesceInsertedText && top?.reason === "node:insert" && reason === "node:update";
|
|
4464
|
+
const canCoalesce = atTip && top.targetKey === targetKey && (namesInsertedNode || COALESCING_REASONS.has(reason) && top.reason === reason);
|
|
3104
4465
|
if (canCoalesce) {
|
|
3105
4466
|
top.snapshot = snapshot;
|
|
3106
4467
|
return;
|
|
@@ -3122,8 +4483,8 @@ var VdFlowchart = class {
|
|
|
3122
4483
|
this.reconnectEdgeId = null;
|
|
3123
4484
|
const currentViewport = deepClone(this.documentData.viewport);
|
|
3124
4485
|
const previousSelection = this.selection;
|
|
3125
|
-
this.documentData = normalizeDocument(snapshot);
|
|
3126
|
-
this.documentData.viewport = normalizeViewport(currentViewport);
|
|
4486
|
+
this.documentData = normalizeDocument(snapshot, this.normalizationLimits());
|
|
4487
|
+
this.documentData.viewport = normalizeViewport(currentViewport, this.scaleBounds());
|
|
3127
4488
|
this.selection = this.resolvePreservedSelection(previousSelection, true);
|
|
3128
4489
|
this.render();
|
|
3129
4490
|
this.emitChange(reason);
|
|
@@ -3200,7 +4561,8 @@ var VdFlowchart = class {
|
|
|
3200
4561
|
y: partialNode.y == null ? fallbackY : partialNode.y
|
|
3201
4562
|
},
|
|
3202
4563
|
this.documentData.nodes.length,
|
|
3203
|
-
usedIds
|
|
4564
|
+
usedIds,
|
|
4565
|
+
this.nodeSizeBounds()
|
|
3204
4566
|
);
|
|
3205
4567
|
this.documentData.nodes.push(node);
|
|
3206
4568
|
this.select({ kind: "node", id: node.id });
|
|
@@ -3216,19 +4578,180 @@ var VdFlowchart = class {
|
|
|
3216
4578
|
const parent = this.findNode(sanitizeId(parentId));
|
|
3217
4579
|
if (!parent) return null;
|
|
3218
4580
|
const { direction = "right", distance, angle, edge: edgeOptions, ...nodeOptions } = options;
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
4581
|
+
return this.batchHistory("node:add-child", () => {
|
|
4582
|
+
const node = this.addNode({
|
|
4583
|
+
...nodeOptions,
|
|
4584
|
+
relativeTo: { node: parent.id, direction, distance, angle }
|
|
4585
|
+
});
|
|
4586
|
+
const edge = this.addEdge({
|
|
4587
|
+
from: parent.id,
|
|
4588
|
+
to: node.id,
|
|
4589
|
+
autoPort: true,
|
|
4590
|
+
endMarker: "arrow",
|
|
4591
|
+
...isPlainObject(edgeOptions) ? edgeOptions : {}
|
|
4592
|
+
});
|
|
4593
|
+
this.select({ kind: "node", id: node.id });
|
|
4594
|
+
return { node, edge };
|
|
3222
4595
|
});
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
4596
|
+
}
|
|
4597
|
+
// Run `fn` with history recording suspended, then record a single entry for
|
|
4598
|
+
// everything it changed (change events still fire for each mutation).
|
|
4599
|
+
batchHistory(reason, fn) {
|
|
4600
|
+
this.historyBatchDepth += 1;
|
|
4601
|
+
let result;
|
|
4602
|
+
try {
|
|
4603
|
+
result = fn();
|
|
4604
|
+
} finally {
|
|
4605
|
+
this.historyBatchDepth -= 1;
|
|
4606
|
+
}
|
|
4607
|
+
if (this.historyBatchDepth === 0 && this.historyBatchDirty) {
|
|
4608
|
+
this.historyBatchDirty = false;
|
|
4609
|
+
if (this.historyEnabled && !this.isApplyingHistory) this.recordHistory(reason);
|
|
4610
|
+
}
|
|
4611
|
+
return result;
|
|
4612
|
+
}
|
|
4613
|
+
// Mind-map structure: a node's parent is the source of its first incoming
|
|
4614
|
+
// edge; its children are the targets of its outgoing edges. The side a
|
|
4615
|
+
// branch is on is the port its edge leaves the parent from, which is what
|
|
4616
|
+
// the user sees (centre geometry is ambiguous for diagonal nodes).
|
|
4617
|
+
getParentEdge(nodeId) {
|
|
4618
|
+
return this.documentData.edges.find(
|
|
4619
|
+
(edge) => edge.to.nodeId === nodeId && edge.from.nodeId !== nodeId
|
|
4620
|
+
) || null;
|
|
4621
|
+
}
|
|
4622
|
+
getParentNode(nodeId) {
|
|
4623
|
+
const edge = this.getParentEdge(nodeId);
|
|
4624
|
+
return edge ? this.findNode(edge.from.nodeId) : null;
|
|
4625
|
+
}
|
|
4626
|
+
getChildNodes(nodeId, direction = null) {
|
|
4627
|
+
const port = direction ? DIRECTION_PORTS[direction] : null;
|
|
4628
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4629
|
+
this.documentData.edges.forEach((edge) => {
|
|
4630
|
+
if (edge.from.nodeId !== nodeId || edge.to.nodeId === nodeId) return;
|
|
4631
|
+
if (!port || edge.from.port === port) ids.add(edge.to.nodeId);
|
|
4632
|
+
});
|
|
4633
|
+
return this.documentData.nodes.filter((node) => ids.has(node.id));
|
|
4634
|
+
}
|
|
4635
|
+
// Direction a branch grows from `node`: the side its parent edge leaves
|
|
4636
|
+
// from, or the emptier horizontal side for a root.
|
|
4637
|
+
getGrowthDirection(node) {
|
|
4638
|
+
const parentEdge = this.getParentEdge(node.id);
|
|
4639
|
+
if (parentEdge) return PORT_DIRECTIONS[parentEdge.from.port] || "right";
|
|
4640
|
+
const right = this.getChildNodes(node.id, "right").length;
|
|
4641
|
+
const left = this.getChildNodes(node.id, "left").length;
|
|
4642
|
+
return left < right ? "left" : "right";
|
|
4643
|
+
}
|
|
4644
|
+
// Shape for a new branch node: copy a sibling when there is one, otherwise
|
|
4645
|
+
// the parent's box shape, falling back to a rounded step.
|
|
4646
|
+
getBranchTemplate(parent, sibling) {
|
|
4647
|
+
const source = sibling || (["rounded-rect", "rect", "textbox"].includes(parent.type) ? parent : null) || null;
|
|
4648
|
+
if (source) return { type: source.type, width: source.width, height: source.height };
|
|
4649
|
+
const spec = DEFAULT_NODE_SPECS["rounded-rect"];
|
|
4650
|
+
return { type: "rounded-rect", width: spec.width, height: spec.height };
|
|
4651
|
+
}
|
|
4652
|
+
/**
|
|
4653
|
+
* Add a node connected from `parentId` on the `direction` side (default: the
|
|
4654
|
+
* branch's growth direction), stacked after `anchorId` or the last sibling on
|
|
4655
|
+
* that side. The node and its edge are one undo step; the new edge copies a
|
|
4656
|
+
* sibling's (or the parent's incoming) edge style. With `edit: true` the
|
|
4657
|
+
* label editor opens with the text selected.
|
|
4658
|
+
*/
|
|
4659
|
+
insertBranchNode(parentId, options = {}) {
|
|
4660
|
+
if (this.readonly) return null;
|
|
4661
|
+
const parent = this.findNode(sanitizeId(parentId));
|
|
4662
|
+
if (!parent) return null;
|
|
4663
|
+
const direction = DIRECTION_PORTS[options.direction] ? options.direction : this.getGrowthDirection(parent);
|
|
4664
|
+
const siblings = this.getChildNodes(parent.id, direction);
|
|
4665
|
+
const anchor = options.anchorId ? this.findNode(sanitizeId(options.anchorId)) : null;
|
|
4666
|
+
const lastSibling = siblings[siblings.length - 1] || null;
|
|
4667
|
+
const template = this.getBranchTemplate(parent, anchor || lastSibling);
|
|
4668
|
+
const position = placeBranchNode({
|
|
4669
|
+
nodes: this.documentData.nodes,
|
|
4670
|
+
parent,
|
|
4671
|
+
siblings,
|
|
4672
|
+
direction,
|
|
4673
|
+
size: template,
|
|
4674
|
+
anchor,
|
|
4675
|
+
before: Boolean(options.before)
|
|
4676
|
+
});
|
|
4677
|
+
const styleSource = this.documentData.edges.find(
|
|
4678
|
+
(edge) => edge.from.nodeId === parent.id && edge.to.nodeId === (anchor || lastSibling)?.id
|
|
4679
|
+
) || this.documentData.edges.find((edge) => edge.to.nodeId === parent.id);
|
|
4680
|
+
const edgeStyle = styleSource ? {
|
|
4681
|
+
route: styleSource.route,
|
|
4682
|
+
strokeWidth: styleSource.strokeWidth,
|
|
4683
|
+
startMarker: styleSource.startMarker,
|
|
4684
|
+
endMarker: styleSource.endMarker
|
|
4685
|
+
} : { endMarker: "arrow" };
|
|
4686
|
+
const result = this.batchHistory("node:insert", () => {
|
|
4687
|
+
if (parent.collapsed) delete parent.collapsed;
|
|
4688
|
+
const node = this.addNode({ ...template, x: position.x, y: position.y });
|
|
4689
|
+
const edge = this.addEdge({
|
|
4690
|
+
from: { nodeId: parent.id, port: DIRECTION_PORTS[direction] },
|
|
4691
|
+
to: { nodeId: node.id, port: DIRECTION_PORTS[oppositeDirection(direction)] },
|
|
4692
|
+
...edgeStyle
|
|
4693
|
+
});
|
|
4694
|
+
if (edge && anchor) this.placeEdgeBySibling(edge.id, parent.id, anchor.id, options.before);
|
|
4695
|
+
if (this.autoLayout && this.layoutMode !== "grid") this.layout(this.layoutMode);
|
|
4696
|
+
this.select({ kind: "node", id: node.id });
|
|
4697
|
+
return { node, edge };
|
|
4698
|
+
});
|
|
4699
|
+
this.revealNode(result.node.id);
|
|
4700
|
+
if (options.edit) this.startTextEdit(result.node.id, { inserted: true });
|
|
4701
|
+
return result;
|
|
4702
|
+
}
|
|
4703
|
+
// Tree and radial layouts order children by connection order, so a sibling's
|
|
4704
|
+
// connection goes right after (or before) its anchor's instead of last.
|
|
4705
|
+
placeEdgeBySibling(edgeId, parentId, anchorId, before = false) {
|
|
4706
|
+
const edges = this.documentData.edges;
|
|
4707
|
+
const fromIndex = edges.findIndex((edge) => edge.id === edgeId);
|
|
4708
|
+
if (fromIndex === -1) return;
|
|
4709
|
+
const [moved] = edges.splice(fromIndex, 1);
|
|
4710
|
+
const anchorIndex = edges.findIndex(
|
|
4711
|
+
(edge) => edge.from.nodeId === parentId && edge.to.nodeId === anchorId
|
|
4712
|
+
);
|
|
4713
|
+
if (anchorIndex === -1) edges.push(moved);
|
|
4714
|
+
else edges.splice(before ? anchorIndex : anchorIndex + 1, 0, moved);
|
|
4715
|
+
}
|
|
4716
|
+
// Insert a sibling after (or before) `nodeId`; a node without a parent gets a
|
|
4717
|
+
// child instead, which is what Enter on a mind-map root means.
|
|
4718
|
+
insertSiblingNode(nodeId, options = {}) {
|
|
4719
|
+
if (this.readonly) return null;
|
|
4720
|
+
const node = this.findNode(sanitizeId(nodeId));
|
|
4721
|
+
if (!node) return null;
|
|
4722
|
+
const parentEdge = this.getParentEdge(node.id);
|
|
4723
|
+
const parent = parentEdge ? this.findNode(parentEdge.from.nodeId) : null;
|
|
4724
|
+
if (!parent) return this.insertBranchNode(node.id, { edit: options.edit });
|
|
4725
|
+
return this.insertBranchNode(parent.id, {
|
|
4726
|
+
direction: PORT_DIRECTIONS[parentEdge.from.port],
|
|
4727
|
+
anchorId: node.id,
|
|
4728
|
+
before: Boolean(options.before),
|
|
4729
|
+
edit: options.edit
|
|
3229
4730
|
});
|
|
3230
|
-
|
|
3231
|
-
|
|
4731
|
+
}
|
|
4732
|
+
// Pan just enough to bring a node fully into view (with a small margin).
|
|
4733
|
+
revealNode(nodeId) {
|
|
4734
|
+
const node = this.findNode(nodeId);
|
|
4735
|
+
const width = this.canvasEl.clientWidth;
|
|
4736
|
+
const height = this.canvasEl.clientHeight;
|
|
4737
|
+
if (!node || !width || !height) return;
|
|
4738
|
+
const viewport = this.documentData.viewport;
|
|
4739
|
+
const margin = 32;
|
|
4740
|
+
const left = viewport.x + node.x * viewport.scale;
|
|
4741
|
+
const top = viewport.y + node.y * viewport.scale;
|
|
4742
|
+
const right = left + node.width * viewport.scale;
|
|
4743
|
+
const bottom = top + node.height * viewport.scale;
|
|
4744
|
+
let dx = 0;
|
|
4745
|
+
let dy = 0;
|
|
4746
|
+
if (right > width - margin) dx = width - margin - right;
|
|
4747
|
+
if (left + dx < margin) dx = margin - left;
|
|
4748
|
+
if (bottom > height - margin) dy = height - margin - bottom;
|
|
4749
|
+
if (top + dy < margin) dy = margin - top;
|
|
4750
|
+
if (!dx && !dy) return;
|
|
4751
|
+
viewport.x = formatNumber(viewport.x + dx);
|
|
4752
|
+
viewport.y = formatNumber(viewport.y + dy);
|
|
4753
|
+
this.render({ inspector: false, json: true });
|
|
4754
|
+
this.emitViewportChange("viewport:pan");
|
|
3232
4755
|
}
|
|
3233
4756
|
// Arrange nodes with a built-in layout. Positions are computed by the pure
|
|
3234
4757
|
// computeLayout() module, then applied in place (NOT via load(), which would
|
|
@@ -3292,8 +4815,8 @@ var VdFlowchart = class {
|
|
|
3292
4815
|
node.x = patch.x == null ? node.x : formatNumber(toFiniteNumber(patch.x, node.x));
|
|
3293
4816
|
node.y = patch.y == null ? node.y : formatNumber(toFiniteNumber(patch.y, node.y));
|
|
3294
4817
|
node.text = isNodeTextEditable(nextType) ? patch.text == null ? node.text : String(patch.text) : getNodeSpec(nextType).text;
|
|
3295
|
-
node.width = clampNodeWidth(nextType, patch.width, node.width);
|
|
3296
|
-
node.height = clampNodeHeight(nextType, patch.height, node.height);
|
|
4818
|
+
node.width = clampNodeWidth(nextType, patch.width, node.width, this.nodeSizeBounds());
|
|
4819
|
+
node.height = clampNodeHeight(nextType, patch.height, node.height, this.nodeSizeBounds());
|
|
3297
4820
|
if (isPlainObject(patch.data)) {
|
|
3298
4821
|
node.data = deepClone(patch.data);
|
|
3299
4822
|
}
|
|
@@ -3476,11 +4999,51 @@ var VdFlowchart = class {
|
|
|
3476
4999
|
}
|
|
3477
5000
|
deleteSelection() {
|
|
3478
5001
|
if (!this.selection || this.readonly) return false;
|
|
5002
|
+
if (this.hasMultiSelection()) {
|
|
5003
|
+
return this.removeNodeIds(new Set(this.getSelectedNodeIds()), "node:remove");
|
|
5004
|
+
}
|
|
3479
5005
|
if (this.selection.kind === "node") return this.removeNode(this.selection.id);
|
|
3480
5006
|
return this.removeEdge(this.selection.id);
|
|
3481
5007
|
}
|
|
5008
|
+
nodeSizeBounds() {
|
|
5009
|
+
return { min: this.minNodeSize, max: this.maxNodeSize };
|
|
5010
|
+
}
|
|
5011
|
+
scaleBounds() {
|
|
5012
|
+
return { min: this.minScale, max: this.maxScale };
|
|
5013
|
+
}
|
|
5014
|
+
normalizationLimits() {
|
|
5015
|
+
return { scaleBounds: this.scaleBounds(), sizeBounds: this.nodeSizeBounds() };
|
|
5016
|
+
}
|
|
5017
|
+
hasWideCamera() {
|
|
5018
|
+
return this.minScale < DEFAULT_MIN_SCALE || this.maxScale > DEFAULT_MAX_SCALE;
|
|
5019
|
+
}
|
|
5020
|
+
syncGrid() {
|
|
5021
|
+
const wide = this.hasWideCamera();
|
|
5022
|
+
const extent = wide ? WIDE_WORLD_EXTENT : DEFAULT_WORLD_EXTENT;
|
|
5023
|
+
this.gridRect?.setAttribute("x", String(-extent / 2));
|
|
5024
|
+
this.gridRect?.setAttribute("y", String(-extent / 2));
|
|
5025
|
+
this.gridRect?.setAttribute("width", String(extent));
|
|
5026
|
+
this.gridRect?.setAttribute("height", String(extent));
|
|
5027
|
+
const pattern = this.svg?.querySelector("pattern");
|
|
5028
|
+
const path = pattern?.querySelector("path");
|
|
5029
|
+
if (!pattern || !path) return;
|
|
5030
|
+
let tile = this.gridSize;
|
|
5031
|
+
if (wide) {
|
|
5032
|
+
const scale = Math.max(this.minScale, this.documentData?.viewport?.scale || 1);
|
|
5033
|
+
while (tile * scale < 12 && tile < extent / 8) tile *= 4;
|
|
5034
|
+
while (tile * scale > 48 && tile > 1.5) tile /= 4;
|
|
5035
|
+
tile = formatNumber(tile);
|
|
5036
|
+
path.setAttribute("vector-effect", "non-scaling-stroke");
|
|
5037
|
+
} else {
|
|
5038
|
+
path.removeAttribute("vector-effect");
|
|
5039
|
+
}
|
|
5040
|
+
path.setAttribute("stroke-width", "1");
|
|
5041
|
+
pattern.setAttribute("width", String(tile));
|
|
5042
|
+
pattern.setAttribute("height", String(tile));
|
|
5043
|
+
path.setAttribute("d", `M ${tile} 0 L 0 0 0 ${tile}`);
|
|
5044
|
+
}
|
|
3482
5045
|
setViewport(viewport) {
|
|
3483
|
-
this.documentData.viewport = normalizeViewport(viewport);
|
|
5046
|
+
this.documentData.viewport = normalizeViewport(viewport, this.scaleBounds());
|
|
3484
5047
|
this.render({ inspector: false, json: true });
|
|
3485
5048
|
this.emitViewportChange("viewport:set");
|
|
3486
5049
|
return this;
|
|
@@ -3498,45 +5061,80 @@ var VdFlowchart = class {
|
|
|
3498
5061
|
return this;
|
|
3499
5062
|
}
|
|
3500
5063
|
resetView() {
|
|
3501
|
-
this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 });
|
|
5064
|
+
this.documentData.viewport = normalizeViewport({ x: 0, y: 0, scale: 1 }, this.scaleBounds());
|
|
3502
5065
|
this.render({ inspector: false, json: true });
|
|
3503
5066
|
this.emitViewportChange("viewport:reset");
|
|
3504
5067
|
return this;
|
|
3505
5068
|
}
|
|
3506
5069
|
fitView() {
|
|
3507
|
-
|
|
5070
|
+
const nodes = this.getVisibleNodes();
|
|
5071
|
+
if (!nodes.length) {
|
|
3508
5072
|
return this.resetView();
|
|
3509
5073
|
}
|
|
3510
|
-
const bounds = getBounds(
|
|
5074
|
+
const bounds = getBounds(nodes);
|
|
3511
5075
|
const width = this.canvasEl.clientWidth || 800;
|
|
3512
5076
|
const height = this.canvasEl.clientHeight || 560;
|
|
3513
5077
|
const padding = 80;
|
|
3514
5078
|
const contentWidth = Math.max(1, bounds.right - bounds.left);
|
|
3515
5079
|
const contentHeight = Math.max(1, bounds.bottom - bounds.top);
|
|
3516
|
-
const scale =
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
5080
|
+
const scale = formatScale(
|
|
5081
|
+
clamp(
|
|
5082
|
+
Math.min((width - padding * 2) / contentWidth, (height - padding * 2) / contentHeight),
|
|
5083
|
+
this.minScale,
|
|
5084
|
+
this.maxScale
|
|
5085
|
+
)
|
|
3520
5086
|
);
|
|
3521
5087
|
this.documentData.viewport = {
|
|
3522
5088
|
x: formatNumber(width / 2 - (bounds.left + bounds.right) / 2 * scale),
|
|
3523
5089
|
y: formatNumber(height / 2 - (bounds.top + bounds.bottom) / 2 * scale),
|
|
3524
|
-
scale
|
|
5090
|
+
scale
|
|
3525
5091
|
};
|
|
3526
5092
|
this.render({ inspector: false, json: true });
|
|
3527
5093
|
this.emitViewportChange("viewport:fit");
|
|
3528
5094
|
return this;
|
|
3529
5095
|
}
|
|
5096
|
+
fitNode(nodeId, padding = 48) {
|
|
5097
|
+
const node = this.findNode(nodeId);
|
|
5098
|
+
if (!node || this.isNodeHidden(node.id)) return this;
|
|
5099
|
+
const pad = Math.max(0, toFiniteNumber(padding, 48));
|
|
5100
|
+
const width = this.canvasEl.clientWidth || 800;
|
|
5101
|
+
const height = this.canvasEl.clientHeight || 560;
|
|
5102
|
+
const availableWidth = Math.max(1, width - pad * 2);
|
|
5103
|
+
const availableHeight = Math.max(1, height - pad * 2);
|
|
5104
|
+
const scale = formatScale(
|
|
5105
|
+
clamp(
|
|
5106
|
+
Math.min(
|
|
5107
|
+
availableWidth / Math.max(1, node.width),
|
|
5108
|
+
availableHeight / Math.max(1, node.height)
|
|
5109
|
+
),
|
|
5110
|
+
this.minScale,
|
|
5111
|
+
this.maxScale
|
|
5112
|
+
)
|
|
5113
|
+
);
|
|
5114
|
+
const centerX = node.x + node.width / 2;
|
|
5115
|
+
const centerY = node.y + node.height / 2;
|
|
5116
|
+
this.documentData.viewport = {
|
|
5117
|
+
x: formatNumber(width / 2 - centerX * scale),
|
|
5118
|
+
y: formatNumber(height / 2 - centerY * scale),
|
|
5119
|
+
scale
|
|
5120
|
+
};
|
|
5121
|
+
this.render({ inspector: false, json: true });
|
|
5122
|
+
this.emitViewportChange("viewport:fit-node");
|
|
5123
|
+
return this;
|
|
5124
|
+
}
|
|
3530
5125
|
clear() {
|
|
3531
5126
|
this.stopTextEdit({ commit: false });
|
|
3532
5127
|
this.activeTool = null;
|
|
3533
5128
|
this.reconnectEdgeId = null;
|
|
3534
5129
|
this.clipboard = null;
|
|
3535
|
-
this.documentData = normalizeDocument(
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
5130
|
+
this.documentData = normalizeDocument(
|
|
5131
|
+
{
|
|
5132
|
+
nodes: [],
|
|
5133
|
+
edges: [],
|
|
5134
|
+
viewport: { x: 0, y: 0, scale: 1 }
|
|
5135
|
+
},
|
|
5136
|
+
this.normalizationLimits()
|
|
5137
|
+
);
|
|
3540
5138
|
this.selection = null;
|
|
3541
5139
|
this.render();
|
|
3542
5140
|
this.emitChange("clear");
|
|
@@ -3565,6 +5163,41 @@ var VdFlowchart = class {
|
|
|
3565
5163
|
pattern.querySelector("path").setAttribute("d", `M ${this.gridSize} 0 L 0 0 0 ${this.gridSize}`);
|
|
3566
5164
|
}
|
|
3567
5165
|
if ("autoFit" in options) this.autoFit = Boolean(options.autoFit);
|
|
5166
|
+
if ("keyboardShortcuts" in options) {
|
|
5167
|
+
this.keyboardShortcuts = normalizeKeyboardShortcuts(options.keyboardShortcuts);
|
|
5168
|
+
}
|
|
5169
|
+
if ("snapGuides" in options) this.snapGuides = options.snapGuides !== false;
|
|
5170
|
+
if ("minimap" in options) this.setMinimapEnabled(options.minimap !== false);
|
|
5171
|
+
if ("autoLayout" in options) this.autoLayout = Boolean(options.autoLayout);
|
|
5172
|
+
if ("minScale" in options || "maxScale" in options || "minNodeSize" in options || "maxNodeSize" in options) {
|
|
5173
|
+
const nextScale = normalizeBoundPair(
|
|
5174
|
+
"minScale" in options ? options.minScale : this.minScale,
|
|
5175
|
+
"maxScale" in options ? options.maxScale : this.maxScale,
|
|
5176
|
+
DEFAULT_MIN_SCALE,
|
|
5177
|
+
DEFAULT_MAX_SCALE
|
|
5178
|
+
);
|
|
5179
|
+
const nextSize = normalizeBoundPair(
|
|
5180
|
+
"minNodeSize" in options ? options.minNodeSize : this.minNodeSize,
|
|
5181
|
+
"maxNodeSize" in options ? options.maxNodeSize : this.maxNodeSize,
|
|
5182
|
+
DEFAULT_MIN_NODE_SIZE,
|
|
5183
|
+
DEFAULT_MAX_NODE_SIZE
|
|
5184
|
+
);
|
|
5185
|
+
this.minScale = nextScale.min;
|
|
5186
|
+
this.maxScale = nextScale.max;
|
|
5187
|
+
this.minNodeSize = nextSize.min;
|
|
5188
|
+
this.maxNodeSize = nextSize.max;
|
|
5189
|
+
const clamped = clamp(
|
|
5190
|
+
formatScale(this.documentData.viewport.scale),
|
|
5191
|
+
this.minScale,
|
|
5192
|
+
this.maxScale
|
|
5193
|
+
);
|
|
5194
|
+
if (clamped !== this.documentData.viewport.scale) {
|
|
5195
|
+
this.documentData.viewport.scale = clamped;
|
|
5196
|
+
this.pendingBoundsEvent = true;
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5199
|
+
this.syncCanvasLabel();
|
|
5200
|
+
if (this.shortcutsHelpOpen) this.renderShortcutsHelp();
|
|
3568
5201
|
if ("history" in options && options.history !== false !== this.historyEnabled) {
|
|
3569
5202
|
this.historyEnabled = options.history !== false;
|
|
3570
5203
|
this.history = [];
|
|
@@ -3580,11 +5213,15 @@ var VdFlowchart = class {
|
|
|
3580
5213
|
}
|
|
3581
5214
|
}
|
|
3582
5215
|
this.render();
|
|
5216
|
+
if (this.pendingBoundsEvent) {
|
|
5217
|
+
this.pendingBoundsEvent = false;
|
|
5218
|
+
this.emitViewportChange("viewport:bounds");
|
|
5219
|
+
}
|
|
3583
5220
|
this.updateHistoryButtons();
|
|
3584
5221
|
return this;
|
|
3585
5222
|
}
|
|
3586
5223
|
load(data, options = {}) {
|
|
3587
|
-
const documentData = normalizeDocument(data);
|
|
5224
|
+
const documentData = normalizeDocument(data, this.normalizationLimits());
|
|
3588
5225
|
if (options.silent && JSON.stringify(documentData) === JSON.stringify(this.documentData)) {
|
|
3589
5226
|
return this;
|
|
3590
5227
|
}
|
|
@@ -3613,6 +5250,10 @@ var VdFlowchart = class {
|
|
|
3613
5250
|
destroy() {
|
|
3614
5251
|
if (this.destroyed) return;
|
|
3615
5252
|
this.destroyed = true;
|
|
5253
|
+
if (this.minimapFrame && typeof cancelAnimationFrame === "function") {
|
|
5254
|
+
cancelAnimationFrame(this.minimapFrame);
|
|
5255
|
+
}
|
|
5256
|
+
this.minimapFrame = 0;
|
|
3616
5257
|
this.activeTool = null;
|
|
3617
5258
|
this.stopTextEdit({ commit: false });
|
|
3618
5259
|
this.resizeObserver?.disconnect();
|
|
@@ -3629,6 +5270,21 @@ var VdFlowchart = class {
|
|
|
3629
5270
|
|
|
3630
5271
|
// src/vue.js
|
|
3631
5272
|
var FORWARDED_EVENTS = ["change", "select", "viewport", "connect", "ready"];
|
|
5273
|
+
var EDITOR_OPTION_KEYS = [
|
|
5274
|
+
"readonly",
|
|
5275
|
+
"gridSize",
|
|
5276
|
+
"autoFit",
|
|
5277
|
+
"history",
|
|
5278
|
+
"historyLimit",
|
|
5279
|
+
"keyboardShortcuts",
|
|
5280
|
+
"snapGuides",
|
|
5281
|
+
"minimap",
|
|
5282
|
+
"autoLayout",
|
|
5283
|
+
"minScale",
|
|
5284
|
+
"maxScale",
|
|
5285
|
+
"minNodeSize",
|
|
5286
|
+
"maxNodeSize"
|
|
5287
|
+
];
|
|
3632
5288
|
var VdFlowchart2 = defineComponent({
|
|
3633
5289
|
name: "VdFlowchart",
|
|
3634
5290
|
props: {
|
|
@@ -3645,21 +5301,42 @@ var VdFlowchart2 = defineComponent({
|
|
|
3645
5301
|
/** Enable the built-in undo/redo history (default true). */
|
|
3646
5302
|
history: { type: Boolean, default: true },
|
|
3647
5303
|
/** Maximum number of history entries to retain. */
|
|
3648
|
-
historyLimit: { type: Number, default: void 0 }
|
|
5304
|
+
historyLimit: { type: Number, default: void 0 },
|
|
5305
|
+
/** Keyboard model: 'mindmap' (Tab child, Enter sibling) or 'basic'. */
|
|
5306
|
+
keyboardShortcuts: { type: String, default: "mindmap" },
|
|
5307
|
+
/** Snap dragged nodes to other nodes' edges and centres (default true). */
|
|
5308
|
+
snapGuides: { type: Boolean, default: true },
|
|
5309
|
+
/** Show the overview minimap in the canvas corner (default true). */
|
|
5310
|
+
minimap: { type: Boolean, default: true },
|
|
5311
|
+
/** Re-run the current layout after keyboard or handle insertions (default false). */
|
|
5312
|
+
autoLayout: { type: Boolean, default: false },
|
|
5313
|
+
/** Lowest camera scale. Defaults to 0.35. Set it together with maxScale. */
|
|
5314
|
+
minScale: { type: Number, default: void 0 },
|
|
5315
|
+
/** Highest camera scale. Defaults to 3. Set it together with minScale. */
|
|
5316
|
+
maxScale: { type: Number, default: void 0 },
|
|
5317
|
+
/** Smallest resizable-node side. Defaults to 56. Set it together with maxNodeSize. */
|
|
5318
|
+
minNodeSize: { type: Number, default: void 0 },
|
|
5319
|
+
/** Largest resizable-node side. Defaults to 420. Set it together with minNodeSize. */
|
|
5320
|
+
maxNodeSize: { type: Number, default: void 0 }
|
|
3649
5321
|
},
|
|
3650
5322
|
emits: ["change", "select", "viewport", "connect", "ready"],
|
|
3651
5323
|
setup(props, { emit, expose }) {
|
|
3652
5324
|
const el = ref(null);
|
|
3653
5325
|
let instance = null;
|
|
5326
|
+
const editorOptions = () => Object.fromEntries(
|
|
5327
|
+
EDITOR_OPTION_KEYS.flatMap((key) => {
|
|
5328
|
+
const value = props[key];
|
|
5329
|
+
if ((key === "minScale" || key === "maxScale" || key === "minNodeSize" || key === "maxNodeSize") && value == null) {
|
|
5330
|
+
return [];
|
|
5331
|
+
}
|
|
5332
|
+
return [[key, value]];
|
|
5333
|
+
})
|
|
5334
|
+
);
|
|
3654
5335
|
const create = () => {
|
|
3655
5336
|
instance = new VdFlowchart({
|
|
3656
5337
|
element: el.value,
|
|
3657
5338
|
data: props.data,
|
|
3658
|
-
|
|
3659
|
-
gridSize: props.gridSize,
|
|
3660
|
-
autoFit: props.autoFit,
|
|
3661
|
-
history: props.history,
|
|
3662
|
-
historyLimit: props.historyLimit
|
|
5339
|
+
...editorOptions()
|
|
3663
5340
|
});
|
|
3664
5341
|
FORWARDED_EVENTS.forEach((name) => {
|
|
3665
5342
|
instance.on(name, (payload) => emit(name, payload));
|
|
@@ -3679,16 +5356,9 @@ var VdFlowchart2 = defineComponent({
|
|
|
3679
5356
|
{ deep: true }
|
|
3680
5357
|
);
|
|
3681
5358
|
watch(
|
|
3682
|
-
() =>
|
|
5359
|
+
() => EDITOR_OPTION_KEYS.map((key) => props[key]),
|
|
3683
5360
|
() => {
|
|
3684
|
-
if (
|
|
3685
|
-
instance.updateOptions({
|
|
3686
|
-
readonly: props.readonly,
|
|
3687
|
-
gridSize: props.gridSize,
|
|
3688
|
-
autoFit: props.autoFit,
|
|
3689
|
-
history: props.history,
|
|
3690
|
-
historyLimit: props.historyLimit
|
|
3691
|
-
});
|
|
5361
|
+
if (instance) instance.updateOptions(editorOptions());
|
|
3692
5362
|
}
|
|
3693
5363
|
);
|
|
3694
5364
|
onBeforeUnmount(() => {
|
|
@@ -3703,7 +5373,8 @@ var VdFlowchart2 = defineComponent({
|
|
|
3703
5373
|
redo: () => instance?.redo(),
|
|
3704
5374
|
canUndo: () => Boolean(instance?.canUndo()),
|
|
3705
5375
|
canRedo: () => Boolean(instance?.canRedo()),
|
|
3706
|
-
layout: (mode, options) => instance?.layout(mode, options)
|
|
5376
|
+
layout: (mode, options) => instance?.layout(mode, options),
|
|
5377
|
+
fitNode: (nodeId, padding) => instance?.fitNode(nodeId, padding)
|
|
3707
5378
|
});
|
|
3708
5379
|
return () => h("div", { ref: el, class: "vd-flowchart" });
|
|
3709
5380
|
}
|
|
@@ -3712,6 +5383,7 @@ export {
|
|
|
3712
5383
|
FLOWCHART_DOCUMENT_VERSION,
|
|
3713
5384
|
FLOWCHART_EDGE_MARKERS,
|
|
3714
5385
|
FLOWCHART_EDGE_ROUTES,
|
|
5386
|
+
FLOWCHART_KEYBOARD_SHORTCUTS,
|
|
3715
5387
|
FLOWCHART_NODE_TYPES,
|
|
3716
5388
|
FLOWCHART_PORTS,
|
|
3717
5389
|
LAYOUT_MODES,
|