@vanduo-oss/vd3-flowchart 1.2.0 → 1.3.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/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  computeLayout,
16
16
  LAYOUT_MODES,
17
17
  VD_FLOWCHART_VERSION,
18
+ FLOWCHART_DOCUMENT_VERSION,
18
19
  FLOWCHART_NODE_TYPES,
19
20
  FLOWCHART_PORTS,
20
21
  FLOWCHART_EDGE_MARKERS,
package/dist/index.js CHANGED
@@ -258,7 +258,8 @@ var EDGE_STROKE_PRESETS = [
258
258
  { id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
259
259
  { id: "bold", label: "Bold", width: 3.5 }
260
260
  ];
261
- var VD_FLOWCHART_VERSION = "1.2.0";
261
+ var VD_FLOWCHART_VERSION = "1.3.0";
262
+ var FLOWCHART_DOCUMENT_VERSION = "1.2.0";
262
263
  var FLOWCHART_NODE_TYPES = [
263
264
  "rounded-rect",
264
265
  "rect",
@@ -495,17 +496,45 @@ function normalizeEdge(rawEdge, index, nodeIds, usedIds) {
495
496
  syncEdgeKind(edge);
496
497
  return edge;
497
498
  }
499
+ function parseDocumentVersion(value) {
500
+ const raw = typeof value === "number" && Number.isFinite(value) ? String(value) : typeof value === "string" ? value.trim() : "";
501
+ const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(raw);
502
+ if (!match) return null;
503
+ return {
504
+ major: Number(match[1]),
505
+ minor: Number(match[2] ?? 0),
506
+ patch: Number(match[3] ?? 0)
507
+ };
508
+ }
509
+ function isSupportedDocumentVersion(value) {
510
+ const parsed = parseDocumentVersion(value);
511
+ const max = parseDocumentVersion(FLOWCHART_DOCUMENT_VERSION);
512
+ if (!parsed || !max || parsed.major !== max.major) return false;
513
+ if (parsed.minor > max.minor) return false;
514
+ if (parsed.minor === max.minor && parsed.patch > max.patch) return false;
515
+ return true;
516
+ }
498
517
  function normalizeDocument(input) {
499
518
  let source = input;
500
519
  if (typeof source === "string") {
501
520
  try {
502
521
  source = JSON.parse(source);
503
522
  } catch {
504
- source = {};
523
+ throw new TypeError("Invalid flowchart JSON. The current document was not changed.");
505
524
  }
506
525
  }
507
526
  if (!isPlainObject(source)) {
508
- source = {};
527
+ throw new TypeError("A flowchart document must be an object.");
528
+ }
529
+ if (source.version !== void 0 && !isSupportedDocumentVersion(source.version)) {
530
+ throw new RangeError(
531
+ `Unsupported flowchart document version: ${String(source.version)}. Supported through ${FLOWCHART_DOCUMENT_VERSION}.`
532
+ );
533
+ }
534
+ for (const key of ["nodes", "edges"]) {
535
+ if (source[key] !== void 0 && !Array.isArray(source[key])) {
536
+ throw new TypeError(`Flowchart ${key} must be an array.`);
537
+ }
509
538
  }
510
539
  const usedNodeIds = /* @__PURE__ */ new Set();
511
540
  const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds));
@@ -513,7 +542,7 @@ function normalizeDocument(input) {
513
542
  const usedEdgeIds = /* @__PURE__ */ new Set();
514
543
  const edges = (Array.isArray(source.edges) ? source.edges : []).slice(0, MAX_EDGES).map((edge, index) => normalizeEdge(edge, index, nodeIds, usedEdgeIds)).filter(Boolean);
515
544
  return {
516
- version: VD_FLOWCHART_VERSION,
545
+ version: FLOWCHART_DOCUMENT_VERSION,
517
546
  viewport: normalizeViewport(source.viewport),
518
547
  nodes,
519
548
  edges
@@ -968,6 +997,8 @@ function getBounds(nodes) {
968
997
  function createField(labelText, control) {
969
998
  const wrapper = createElement("div", { className: "vd-flowchart-field" });
970
999
  const label = createElement("label", { text: labelText });
1000
+ control.id || (control.id = nextId("flowchart-field"));
1001
+ label.htmlFor = control.id;
971
1002
  wrapper.appendChild(label);
972
1003
  wrapper.appendChild(control);
973
1004
  return wrapper;
@@ -1352,14 +1383,127 @@ var VdFlowchart = class {
1352
1383
  this.jsonActions.appendChild(this.loadJsonButton);
1353
1384
  this.jsonPanel.appendChild(this.jsonTextarea);
1354
1385
  this.jsonPanel.appendChild(this.jsonActions);
1386
+ this.jsonStatus = createElement("p", { className: "vd-flowchart-json-status" });
1387
+ this.jsonStatus.setAttribute("role", "status");
1388
+ this.jsonPanel.appendChild(this.jsonStatus);
1355
1389
  this.inspectorPanel.appendChild(this.jsonPanel);
1356
1390
  this.body.appendChild(this.palettePanel);
1357
1391
  this.body.appendChild(this.canvasEl);
1358
1392
  this.body.appendChild(this.inspectorPanel);
1359
1393
  this.root.appendChild(this.toolbar);
1360
1394
  this.root.appendChild(this.body);
1395
+ this.buildGraphOutline();
1361
1396
  this.element.appendChild(this.root);
1362
1397
  }
1398
+ buildGraphOutline() {
1399
+ this.graphOutline = createElement("details", { className: "vd-flowchart-outline" });
1400
+ this.graphOutline.appendChild(createElement("summary", { text: "Graph outline" }));
1401
+ this.graphOutline.addEventListener("toggle", () => {
1402
+ if (this.graphOutline.open) this.renderGraphOutline();
1403
+ });
1404
+ this.graphStatus = createElement("p", { className: "vd-flowchart-sr-only" });
1405
+ this.graphStatus.setAttribute("role", "status");
1406
+ this.canvasEl.appendChild(this.graphStatus);
1407
+ this.graphControls = createElement("div", { className: "vd-flowchart-outline-controls" });
1408
+ this.graphNodeSelect = createElement("select");
1409
+ this.graphNodeSelect.setAttribute("aria-label", "Selected node");
1410
+ this.graphNodeSelect.addEventListener(
1411
+ "change",
1412
+ () => this.selectNode(this.graphNodeSelect.value)
1413
+ );
1414
+ this.graphTargetSelect = createElement("select");
1415
+ this.graphTargetSelect.setAttribute("aria-label", "Connection target");
1416
+ this.graphTargetSelect.addEventListener("change", () => this.renderGraphOutline());
1417
+ this.graphConnectButton = createElement("button", {
1418
+ text: "Connect nodes",
1419
+ type: "button",
1420
+ className: "vd-flowchart-btn"
1421
+ });
1422
+ this.graphConnectButton.addEventListener("click", () => {
1423
+ if (this.readonly || this.graphNodeSelect.value === this.graphTargetSelect.value) {
1424
+ return;
1425
+ }
1426
+ this.addEdge({
1427
+ from: this.graphNodeSelect.value,
1428
+ to: this.graphTargetSelect.value,
1429
+ autoPort: true
1430
+ });
1431
+ });
1432
+ this.graphEditButton = createElement("button", {
1433
+ text: "Edit selected label",
1434
+ type: "button",
1435
+ className: "vd-flowchart-btn"
1436
+ });
1437
+ this.graphEditButton.addEventListener(
1438
+ "click",
1439
+ () => this.startTextEdit(this.graphNodeSelect.value)
1440
+ );
1441
+ this.graphControls.append(
1442
+ createField("Selected node", this.graphNodeSelect),
1443
+ createField("Connection target", this.graphTargetSelect),
1444
+ this.graphConnectButton,
1445
+ this.graphEditButton
1446
+ );
1447
+ this.graphOutline.appendChild(this.graphControls);
1448
+ this.graphList = createElement("ul");
1449
+ this.graphList.setAttribute("aria-label", "Nodes and connections");
1450
+ this.graphOutline.appendChild(this.graphList);
1451
+ this.root.appendChild(this.graphOutline);
1452
+ this.canvasEl.setAttribute("role", "group");
1453
+ this.canvasEl.setAttribute(
1454
+ "aria-label",
1455
+ "Diagram canvas. Arrow keys select nodes; Enter edits a label. Graph outline provides connections."
1456
+ );
1457
+ this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
1458
+ }
1459
+ renderGraphOutline() {
1460
+ if (!this.graphOutline) return;
1461
+ const nodes = this.documentData.nodes;
1462
+ const edges = this.documentData.edges;
1463
+ const labels = new Map(nodes.map((node) => [node.id, node.text || node.id]));
1464
+ const selected = this.selection?.kind === "node" ? this.selection.id : null;
1465
+ this.graphStatus.textContent = selected ? `Selected: ${labels.get(selected)}. ${nodes.length} nodes, ${edges.length} connections.` : `${nodes.length} nodes, ${edges.length} connections.`;
1466
+ if (!this.graphOutline.open) return;
1467
+ const signature = JSON.stringify([
1468
+ nodes.map((node) => [node.id, node.text]),
1469
+ edges.map((edge) => [edge.id, edge.from.nodeId, edge.to.nodeId, edge.label])
1470
+ ]);
1471
+ if (signature !== this.graphSignature) {
1472
+ this.graphSignature = signature;
1473
+ for (const select of [this.graphNodeSelect, this.graphTargetSelect]) {
1474
+ const previous = select.value;
1475
+ select.replaceChildren();
1476
+ for (const node of nodes)
1477
+ select.appendChild(
1478
+ createElement("option", { value: node.id, text: labels.get(node.id) })
1479
+ );
1480
+ if (labels.has(previous)) select.value = previous;
1481
+ }
1482
+ this.graphList.replaceChildren();
1483
+ const outgoing = new Map(nodes.map((node) => [node.id, []]));
1484
+ const incoming = new Map(nodes.map((node) => [node.id, []]));
1485
+ for (const edge of edges) {
1486
+ outgoing.get(edge.from.nodeId)?.push(`${labels.get(edge.to.nodeId)}${edge.label ? ` (${edge.label})` : ""}`);
1487
+ incoming.get(edge.to.nodeId)?.push(labels.get(edge.from.nodeId));
1488
+ }
1489
+ for (const node of nodes) {
1490
+ const to = outgoing.get(node.id);
1491
+ const from = incoming.get(node.id);
1492
+ const relationships = [
1493
+ to.length ? `Connects to ${to.join(", ")}.` : "No outgoing connections.",
1494
+ from.length ? `Connected from ${from.join(", ")}.` : ""
1495
+ ];
1496
+ this.graphList.appendChild(
1497
+ createElement("li", { text: `${labels.get(node.id)}. ${relationships.join(" ")}` })
1498
+ );
1499
+ }
1500
+ }
1501
+ if (selected) this.graphNodeSelect.value = selected;
1502
+ this.graphNodeSelect.disabled = nodes.length === 0;
1503
+ this.graphTargetSelect.disabled = this.readonly || nodes.length < 2;
1504
+ this.graphConnectButton.disabled = this.readonly || nodes.length < 2 || this.graphNodeSelect.value === this.graphTargetSelect.value;
1505
+ this.graphEditButton.disabled = this.readonly || nodes.length === 0;
1506
+ }
1363
1507
  bindEvents() {
1364
1508
  this.toolbar.addEventListener("click", this.handleToolbarClick);
1365
1509
  this.arrangeSelect.addEventListener("change", this.handleArrangeChange);
@@ -1466,7 +1610,12 @@ var VdFlowchart = class {
1466
1610
  return;
1467
1611
  }
1468
1612
  if (action === "load" && !this.readonly) {
1469
- this.load(this.jsonTextarea.value);
1613
+ try {
1614
+ this.load(this.jsonTextarea.value);
1615
+ this.jsonStatus.textContent = "";
1616
+ } catch (error) {
1617
+ this.jsonStatus.textContent = error.message;
1618
+ }
1470
1619
  }
1471
1620
  }
1472
1621
  handleSelectionFieldInput(event) {
@@ -1537,10 +1686,31 @@ var VdFlowchart = class {
1537
1686
  }
1538
1687
  }
1539
1688
  handleKeyDown(event) {
1540
- if (this.readonly) return;
1541
1689
  if (event.target && (event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA" || event.target.tagName === "SELECT")) {
1542
1690
  return;
1543
1691
  }
1692
+ if (event.target === this.canvasEl) {
1693
+ const nodes = this.documentData.nodes;
1694
+ const index = nodes.findIndex((node) => node.id === this.selection?.id);
1695
+ let next = -1;
1696
+ if (event.key === "ArrowDown" || event.key === "ArrowRight")
1697
+ next = (index + 1) % nodes.length;
1698
+ if (event.key === "ArrowUp" || event.key === "ArrowLeft")
1699
+ next = index < 0 ? nodes.length - 1 : (index - 1 + nodes.length) % nodes.length;
1700
+ if (event.key === "Home") next = 0;
1701
+ if (event.key === "End") next = nodes.length - 1;
1702
+ if (nodes[next]) {
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
+ }
1712
+ }
1713
+ if (this.readonly) return;
1544
1714
  if (event.key === "Escape" && this.activeTool) {
1545
1715
  event.preventDefault();
1546
1716
  this.setActiveTool(null);
@@ -1831,7 +2001,7 @@ var VdFlowchart = class {
1831
2001
  node.x = nextX;
1832
2002
  node.y = nextY;
1833
2003
  this.interaction.moved = true;
1834
- this.render({ inspector: false, json: false });
2004
+ this.renderDraggedNode(node);
1835
2005
  }
1836
2006
  return;
1837
2007
  }
@@ -2206,11 +2376,13 @@ var VdFlowchart = class {
2206
2376
  if (event.key === "Escape") {
2207
2377
  event.preventDefault();
2208
2378
  this.stopTextEdit({ commit: false });
2379
+ this.canvasEl.focus();
2209
2380
  return;
2210
2381
  }
2211
2382
  if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
2212
2383
  event.preventDefault();
2213
2384
  this.stopTextEdit({ commit: true });
2385
+ this.canvasEl.focus();
2214
2386
  }
2215
2387
  });
2216
2388
  textarea.addEventListener("blur", () => {
@@ -2284,6 +2456,7 @@ var VdFlowchart = class {
2284
2456
  if (shouldRenderScene) this.renderScene();
2285
2457
  if (shouldRenderInspector) this.renderSelectionPanel();
2286
2458
  if (shouldRenderJson) this.syncJsonTextarea();
2459
+ if (shouldRenderInspector) this.renderGraphOutline();
2287
2460
  this.updateToolbarLabel();
2288
2461
  this.updatePaletteState();
2289
2462
  this.positionTextEditor();
@@ -2299,15 +2472,28 @@ var VdFlowchart = class {
2299
2472
  clearChildren(this.nodesLayer);
2300
2473
  clearChildren(this.overlayLayer);
2301
2474
  const nodeMap = new Map(this.documentData.nodes.map((node) => [node.id, node]));
2475
+ this.sceneNodeMap = nodeMap;
2476
+ this.nodeElements = /* @__PURE__ */ new Map();
2477
+ this.edgeElements = /* @__PURE__ */ new Map();
2478
+ this.incidentEdges = /* @__PURE__ */ new Map();
2302
2479
  this.documentData.edges.forEach((edge) => {
2303
2480
  const edgeElement = this.renderEdge(edge, nodeMap);
2304
- if (edgeElement) this.edgesLayer.appendChild(edgeElement);
2481
+ if (edgeElement) {
2482
+ this.edgesLayer.appendChild(edgeElement);
2483
+ this.edgeElements.set(edge.id, edgeElement);
2484
+ for (const id of /* @__PURE__ */ new Set([edge.from.nodeId, edge.to.nodeId])) {
2485
+ if (!this.incidentEdges.has(id)) this.incidentEdges.set(id, []);
2486
+ this.incidentEdges.get(id).push(edge);
2487
+ }
2488
+ }
2305
2489
  });
2306
2490
  if (this.interaction?.kind === "connect" || this.interaction?.kind === "reconnect") {
2307
2491
  this.previewLayer.appendChild(this.renderPreviewEdge(this.interaction));
2308
2492
  }
2309
2493
  this.documentData.nodes.forEach((node) => {
2310
- this.nodesLayer.appendChild(this.renderNode(node));
2494
+ const element = this.renderNode(node);
2495
+ this.nodesLayer.appendChild(element);
2496
+ this.nodeElements.set(node.id, element);
2311
2497
  });
2312
2498
  if (this.reconnectEdgeId && !this.readonly) {
2313
2499
  const reconnectEdge = this.findEdge(this.reconnectEdgeId);
@@ -2323,6 +2509,25 @@ var VdFlowchart = class {
2323
2509
  }
2324
2510
  this.syncConnectingState();
2325
2511
  }
2512
+ // Drag frames change one translation and its connected paths. Keep unrelated
2513
+ // SVG elements intact; the release event still refreshes inspector/history.
2514
+ renderDraggedNode(node) {
2515
+ const element = this.nodeElements?.get(node.id);
2516
+ if (!element) {
2517
+ this.render({ inspector: false, json: false });
2518
+ return;
2519
+ }
2520
+ element.setAttribute("transform", `translate(${formatNumber(node.x)} ${formatNumber(node.y)})`);
2521
+ element.classList.add("is-dragging");
2522
+ for (const edge of this.incidentEdges.get(node.id) || []) {
2523
+ const previous = this.edgeElements.get(edge.id);
2524
+ const next = this.renderEdge(edge, this.sceneNodeMap);
2525
+ if (previous && next) {
2526
+ previous.replaceWith(next);
2527
+ this.edgeElements.set(edge.id, next);
2528
+ }
2529
+ }
2530
+ }
2326
2531
  applyEdgeMarkers(pathElement, edge) {
2327
2532
  if (edge.startMarker && edge.startMarker !== "none") {
2328
2533
  pathElement.setAttribute(
@@ -3337,20 +3542,69 @@ var VdFlowchart = class {
3337
3542
  this.emitChange("clear");
3338
3543
  return this;
3339
3544
  }
3545
+ updateOptions(options = {}) {
3546
+ if (this.destroyed) return this;
3547
+ if ("readonly" in options) {
3548
+ if (options.readonly && !this.readonly) {
3549
+ this.stopTextEdit({ commit: true });
3550
+ this.interaction = null;
3551
+ this.activeTool = null;
3552
+ this.reconnectEdgeId = null;
3553
+ }
3554
+ this.readonly = Boolean(options.readonly);
3555
+ this.root.classList.toggle("vd-flowchart-readonly", this.readonly);
3556
+ this.arrangeSelect.disabled = this.readonly;
3557
+ this.clearButton.disabled = this.readonly;
3558
+ this.loadJsonButton.disabled = this.readonly;
3559
+ }
3560
+ if ("gridSize" in options) {
3561
+ this.gridSize = clamp(toFiniteNumber(options.gridSize, DEFAULT_GRID_SIZE), 12, 64);
3562
+ const pattern = this.svg.querySelector("pattern");
3563
+ pattern.setAttribute("width", this.gridSize);
3564
+ pattern.setAttribute("height", this.gridSize);
3565
+ pattern.querySelector("path").setAttribute("d", `M ${this.gridSize} 0 L 0 0 0 ${this.gridSize}`);
3566
+ }
3567
+ if ("autoFit" in options) this.autoFit = Boolean(options.autoFit);
3568
+ if ("history" in options && options.history !== false !== this.historyEnabled) {
3569
+ this.historyEnabled = options.history !== false;
3570
+ this.history = [];
3571
+ this.historyIndex = -1;
3572
+ this.seedHistory();
3573
+ }
3574
+ if ("historyLimit" in options) {
3575
+ this.historyLimit = Math.max(1, Math.floor(toFiniteNumber(options.historyLimit, 100)));
3576
+ if (this.history.length > this.historyLimit) {
3577
+ const start = Math.max(0, this.historyIndex - this.historyLimit + 1);
3578
+ this.history = this.history.slice(start, start + this.historyLimit);
3579
+ this.historyIndex -= start;
3580
+ }
3581
+ }
3582
+ this.render();
3583
+ this.updateHistoryButtons();
3584
+ return this;
3585
+ }
3340
3586
  load(data, options = {}) {
3587
+ const documentData = normalizeDocument(data);
3588
+ if (options.silent && JSON.stringify(documentData) === JSON.stringify(this.documentData)) {
3589
+ return this;
3590
+ }
3341
3591
  this.stopTextEdit({ commit: false });
3342
3592
  this.activeTool = null;
3343
3593
  this.reconnectEdgeId = null;
3344
3594
  const previousSelection = this.selection;
3345
- this.documentData = normalizeDocument(data);
3595
+ this.documentData = documentData;
3346
3596
  this.selection = this.resolvePreservedSelection(previousSelection, options.preserveSelection);
3347
3597
  this.render();
3348
- this.emitChange("load");
3598
+ if (options.silent) {
3599
+ if (this.historyEnabled) this.recordHistory("load");
3600
+ } else {
3601
+ this.emitChange("load");
3602
+ }
3349
3603
  return this;
3350
3604
  }
3351
3605
  toJSON() {
3352
3606
  return deepClone({
3353
- version: VD_FLOWCHART_VERSION,
3607
+ version: FLOWCHART_DOCUMENT_VERSION,
3354
3608
  viewport: this.documentData.viewport,
3355
3609
  nodes: this.documentData.nodes,
3356
3610
  edges: this.documentData.edges
@@ -3364,6 +3618,10 @@ var VdFlowchart = class {
3364
3618
  this.resizeObserver?.disconnect();
3365
3619
  this.resizeObserver = null;
3366
3620
  this.unbindEvents();
3621
+ this.sceneNodeMap?.clear();
3622
+ this.nodeElements?.clear();
3623
+ this.edgeElements?.clear();
3624
+ this.incidentEdges?.clear();
3367
3625
  this.element.innerHTML = "";
3368
3626
  this.element.classList.remove("vd-flowchart-host");
3369
3627
  }
@@ -3415,7 +3673,7 @@ var VdFlowchart2 = defineComponent({
3415
3673
  () => props.data,
3416
3674
  (next) => {
3417
3675
  if (instance && typeof instance.load === "function") {
3418
- instance.load(next, { preserveSelection: props.preserveSelection });
3676
+ instance.load(next, { preserveSelection: props.preserveSelection, silent: true });
3419
3677
  }
3420
3678
  },
3421
3679
  { deep: true }
@@ -3424,8 +3682,13 @@ var VdFlowchart2 = defineComponent({
3424
3682
  () => [props.readonly, props.gridSize, props.autoFit, props.history, props.historyLimit],
3425
3683
  () => {
3426
3684
  if (!instance) return;
3427
- instance.destroy();
3428
- create();
3685
+ instance.updateOptions({
3686
+ readonly: props.readonly,
3687
+ gridSize: props.gridSize,
3688
+ autoFit: props.autoFit,
3689
+ history: props.history,
3690
+ historyLimit: props.historyLimit
3691
+ });
3429
3692
  }
3430
3693
  );
3431
3694
  onBeforeUnmount(() => {
@@ -3446,6 +3709,7 @@ var VdFlowchart2 = defineComponent({
3446
3709
  }
3447
3710
  });
3448
3711
  export {
3712
+ FLOWCHART_DOCUMENT_VERSION,
3449
3713
  FLOWCHART_EDGE_MARKERS,
3450
3714
  FLOWCHART_EDGE_ROUTES,
3451
3715
  FLOWCHART_NODE_TYPES,