@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/CHANGELOG.md +21 -0
- package/README.md +32 -8
- package/SKILL.md +28 -35
- package/dist/core.d.ts +11 -0
- package/dist/index.cjs +279 -15
- package/dist/index.cjs.map +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +279 -15
- package/dist/index.js.map +2 -2
- package/dist/meta.json +13 -12
- package/dist/vd3-flowchart.css +44 -3
- package/package.json +12 -6
- package/recipes/editor.vue +17 -0
- package/styles.d.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@vanduo-oss/vd3-flowchart` are documented here.
|
|
4
4
|
|
|
5
|
+
## 1.3.0 — 2026-09-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Keyboard canvas navigation (arrows, Home/End, Enter to edit) and a native
|
|
10
|
+
**Graph outline** with labelled Edit/Connect controls.
|
|
11
|
+
- `FLOWCHART_DOCUMENT_VERSION` owns saved `version`. Package release stays on
|
|
12
|
+
`VD_FLOWCHART_VERSION`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Option updates (read-only, grid, history) keep the live editor, camera,
|
|
17
|
+
selection, and applicable history. Parent echoes of emitted documents do
|
|
18
|
+
not loop.
|
|
19
|
+
- Malformed JSON and unsupported future documents throw before the active
|
|
20
|
+
document, selection, or history change. Unversioned input and numeric or
|
|
21
|
+
shortened 1.x versions through 1.2.0 still load.
|
|
22
|
+
- Graph outline refuses self-connection.
|
|
23
|
+
- Node drag translates the moved SVG node and rebuilds only incident edges;
|
|
24
|
+
pointer-up still records history.
|
|
25
|
+
|
|
5
26
|
## 1.2.0 — 2026-09-13
|
|
6
27
|
|
|
7
28
|
Extracted from `@vanduo-oss/vd3-cbun@1.4.2` as a standalone package. Component
|
package/README.md
CHANGED
|
@@ -6,8 +6,9 @@ Vanduo **flowchart** for Vue 3: a node/edge editor with undo/redo, layout
|
|
|
6
6
|
modes, and a framework-agnostic core. Extracted 1-to-1 from
|
|
7
7
|
`@vanduo-oss/vd3-cbun` flowchart **1.2.0**.
|
|
8
8
|
|
|
9
|
-
**Status: 1.
|
|
10
|
-
|
|
9
|
+
**Status: 1.3.0.** `VD_FLOWCHART_VERSION` tracks the package release.
|
|
10
|
+
`FLOWCHART_DOCUMENT_VERSION` (currently `1.2.0`) is the value `toJSON()`
|
|
11
|
+
writes. Do not reset either independently without a compatibility plan.
|
|
11
12
|
|
|
12
13
|
## Install
|
|
13
14
|
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
LAYOUT_MODES,
|
|
31
32
|
FLOWCHART_NODE_TYPES,
|
|
32
33
|
VD_FLOWCHART_VERSION,
|
|
34
|
+
FLOWCHART_DOCUMENT_VERSION,
|
|
33
35
|
} from '@vanduo-oss/vd3-flowchart';
|
|
34
36
|
import '@vanduo-oss/vd3-flowchart/css';
|
|
35
37
|
```
|
|
@@ -47,10 +49,14 @@ if anything but `vue` is externalized. The package declares
|
|
|
47
49
|
|
|
48
50
|
## Version policy
|
|
49
51
|
|
|
50
|
-
`package.json` version **is** `VD_FLOWCHART_VERSION` (`1.
|
|
51
|
-
together
|
|
52
|
-
the
|
|
53
|
-
|
|
52
|
+
`package.json` version **is** `VD_FLOWCHART_VERSION` (`1.3.0`). Bump those
|
|
53
|
+
together for a package release. Keep `FLOWCHART_DOCUMENT_VERSION` unchanged
|
|
54
|
+
unless the serialized schema changes; add fixtures and explicit compatibility
|
|
55
|
+
rules for a format change. Unversioned 1.x documents through 1.2.0 still load.
|
|
56
|
+
Malformed JSON and unsupported future versions throw before the active
|
|
57
|
+
document, selection, or history change. This continues the old-line
|
|
58
|
+
`@vanduo-oss/flowchart` lineage (never reset to `1.0.0`). Do not reuse the
|
|
59
|
+
retired npm name `@vanduo-oss/flowchart`.
|
|
54
60
|
|
|
55
61
|
## Theming
|
|
56
62
|
|
|
@@ -60,10 +66,13 @@ package dependency; any provider of the tokens works.
|
|
|
60
66
|
|
|
61
67
|
```js
|
|
62
68
|
import '@vanduo-oss/vd3/css';
|
|
63
|
-
//
|
|
69
|
+
// Same component styles without bundled icon fonts:
|
|
64
70
|
import '@vanduo-oss/vd3/css/core';
|
|
65
71
|
```
|
|
66
72
|
|
|
73
|
+
`@vanduo-oss/vd3/css/core` is **not** tokens-only. Token JSON is
|
|
74
|
+
`@vanduo-oss/vd3/tokens.json`.
|
|
75
|
+
|
|
67
76
|
Tokens consumed include `--vd-bg-primary`, `--vd-bg-secondary`,
|
|
68
77
|
`--vd-text-primary`, `--vd-text-muted`, `--vd-border-color`, and
|
|
69
78
|
`--vd-color-primary` (via `--vd-flowchart-*` locals).
|
|
@@ -83,9 +92,24 @@ Tokens consumed include `--vd-bg-primary`, `--vd-bg-secondary`,
|
|
|
83
92
|
|
|
84
93
|
| Export | Contents |
|
|
85
94
|
| --- | --- |
|
|
86
|
-
| `@vanduo-oss/vd3-flowchart` | `VdFlowchart` + `VdFlowchartCore`, layout, `FLOWCHART_*`, `VD_FLOWCHART_VERSION` |
|
|
95
|
+
| `@vanduo-oss/vd3-flowchart` | `VdFlowchart` + `VdFlowchartCore`, layout, `FLOWCHART_*`, `VD_FLOWCHART_VERSION`, `FLOWCHART_DOCUMENT_VERSION` |
|
|
87
96
|
| `@vanduo-oss/vd3-flowchart/css` | Stylesheet (`dist/vd3-flowchart.css`) |
|
|
88
97
|
|
|
98
|
+
## Tested interaction envelope
|
|
99
|
+
|
|
100
|
+
Observed on a Mac mini (Apple M4, 10 cores, 24 GB, macOS 26.6.2) with
|
|
101
|
+
Chromium 153 at 1440×1000, no CPU throttling. Two warmups and ten samples per
|
|
102
|
+
synchronous operation; drag used 24 pointer steps. Times include forced layout
|
|
103
|
+
and exclude compositor paint. These are observations, not supported-size
|
|
104
|
+
guarantees.
|
|
105
|
+
|
|
106
|
+
- Graph label edit: 25 / 100 / 250 / 500 nodes → 2.9 / 11.2 / 29.9 / 57 ms.
|
|
107
|
+
- Graph undo + redo: 3.6 / 14 / 34.5 / 69.8 ms.
|
|
108
|
+
- Graph tree/grid layout: 2.9 / 11.1 / 28.4 / 58.9 ms.
|
|
109
|
+
- Graph drag (translate the moved node SVG and rebuild only incident edges): 0.2 / 0.3 / 0.2 / 0.3 ms, down from 1.6 / 4.5 / 10.7 / 20.7 ms before that path.
|
|
110
|
+
|
|
111
|
+
Raw JSON: `vd3-docs/reviews/2026-09-16/performance-results.json`.
|
|
112
|
+
|
|
89
113
|
## Development
|
|
90
114
|
|
|
91
115
|
```sh
|
package/SKILL.md
CHANGED
|
@@ -1,47 +1,40 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vanduo-vd3-flowchart
|
|
3
|
-
description:
|
|
3
|
+
description: Integrate @vanduo-oss/vd3-flowchart into Vue 3 apps with controlled documents, editing, layouts, undo, and JSON persistence.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Vanduo flowchart
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
`
|
|
8
|
+
Install `@vanduo-oss/vd3-flowchart` alongside Vue. Import `VdFlowchart` and
|
|
9
|
+
`@vanduo-oss/vd3-flowchart/css`; no global registration is needed. Styles have
|
|
10
|
+
token fallbacks. In a VD3 app, share the base stylesheet already imported at
|
|
11
|
+
app entry; choose `/css` or `/css/core`, not both.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Start from [the complete editor recipe](recipes/editor.vue). It includes two
|
|
14
|
+
nodes, an edge, a controlled `data` prop, change events, read-only state, and undo.
|
|
15
|
+
Give the editor a definite height. Set `auto-fit` for its first measurable layout.
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
pnpm add @vanduo-oss/vd3-flowchart
|
|
17
|
-
```
|
|
17
|
+
## State and persistence
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
Local edits emit `change` with `{ reason, document }`. Feeding `document` back
|
|
20
|
+
into `data` is safe. External replacements are silent and undoable; identical
|
|
21
|
+
echoes preserve selection/history. Ordinary option changes keep the live editor.
|
|
22
|
+
Disabling history clears it; reenabling starts from the current document.
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
Save `event.document` or `getInstance().toJSON()`. Core `load()` emits by default;
|
|
25
|
+
catch validation errors so a failed import can be reported to the user. Unknown
|
|
26
|
+
future versions and malformed JSON are rejected before changing current work.
|
|
27
|
+
`FLOWCHART_DOCUMENT_VERSION` describes saved JSON; `VD_FLOWCHART_VERSION`
|
|
28
|
+
describes the package release. Do not replace document versions with app versions.
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
import { VdFlowchart, VdFlowchartCore } from '@vanduo-oss/vd3-flowchart';
|
|
26
|
-
import '@vanduo-oss/vd3-flowchart/css';
|
|
27
|
-
```
|
|
30
|
+
## Verification
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`FLOWCHART_EDGE_ROUTES`) plus `VD_FLOWCHART_VERSION`. CSS ships at
|
|
34
|
-
`@vanduo-oss/vd3-flowchart/css`. `VD_FLOWCHART_VERSION` is `1.2.0` — it
|
|
35
|
-
continues the old-line lineage (never reset to `1.0.0`) because the value is
|
|
36
|
-
serialized into user documents via `toJSON().version`; resetting it would
|
|
37
|
-
mislabel documents saved by the old line.
|
|
32
|
+
Add a node, toggle read-only, and confirm the edit remains. Undo/redo, save/reload,
|
|
33
|
+
and echo a change back through `data`. With only the keyboard, select nodes using
|
|
34
|
+
canvas arrows, edit with Enter, save with Ctrl/Cmd+Enter, and use Graph outline
|
|
35
|
+
to read relationships and connect nodes. Check the consumer's screen reader.
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```js
|
|
45
|
-
import '@vanduo-oss/vd3/css';
|
|
46
|
-
import '@vanduo-oss/vd3/css/core';
|
|
47
|
-
```
|
|
37
|
+
Use [Vue declarations](dist/vue.d.ts) for component props/events/exposed methods
|
|
38
|
+
and [core declarations](dist/core.d.ts) for document types and editor methods.
|
|
39
|
+
Core `VdFlowchartCore` requires a browser element and explicit `destroy()`;
|
|
40
|
+
the Vue wrapper mounts and cleans it up automatically.
|
package/dist/core.d.ts
CHANGED
|
@@ -134,6 +134,8 @@ export interface MutationOptions {
|
|
|
134
134
|
export interface LoadOptions {
|
|
135
135
|
/** Keep the prior selection if the entity still exists after load. */
|
|
136
136
|
preserveSelection?: boolean;
|
|
137
|
+
/** Suppress change notification; an identical document is a no-op. Used by Vue data updates. */
|
|
138
|
+
silent?: boolean;
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
export interface RemoveNodeOptions {
|
|
@@ -269,6 +271,13 @@ export class VdFlowchart {
|
|
|
269
271
|
stopTextEdit(options?: { commit?: boolean }): void;
|
|
270
272
|
|
|
271
273
|
// Lifecycle
|
|
274
|
+
/** Update editor options without replacing the document, camera or selection. */
|
|
275
|
+
updateOptions(
|
|
276
|
+
options: Pick<
|
|
277
|
+
VdFlowchartOptions,
|
|
278
|
+
'readonly' | 'gridSize' | 'autoFit' | 'history' | 'historyLimit'
|
|
279
|
+
>,
|
|
280
|
+
): this;
|
|
272
281
|
destroy(): void;
|
|
273
282
|
}
|
|
274
283
|
|
|
@@ -282,6 +291,8 @@ export function computeLayout(
|
|
|
282
291
|
export const LAYOUT_MODES: readonly LayoutMode[];
|
|
283
292
|
|
|
284
293
|
export const VD_FLOWCHART_VERSION: string;
|
|
294
|
+
/** Schema version used by toJSON(), independent of the package release. */
|
|
295
|
+
export const FLOWCHART_DOCUMENT_VERSION: string;
|
|
285
296
|
export const FLOWCHART_NODE_TYPES: readonly FlowchartNodeType[];
|
|
286
297
|
export const FLOWCHART_PORTS: readonly FlowchartPort[];
|
|
287
298
|
export const FLOWCHART_EDGE_MARKERS: readonly FlowchartEdgeMarker[];
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.js
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
FLOWCHART_DOCUMENT_VERSION: () => FLOWCHART_DOCUMENT_VERSION,
|
|
23
24
|
FLOWCHART_EDGE_MARKERS: () => FLOWCHART_EDGE_MARKERS,
|
|
24
25
|
FLOWCHART_EDGE_ROUTES: () => FLOWCHART_EDGE_ROUTES,
|
|
25
26
|
FLOWCHART_NODE_TYPES: () => FLOWCHART_NODE_TYPES,
|
|
@@ -292,7 +293,8 @@ var EDGE_STROKE_PRESETS = [
|
|
|
292
293
|
{ id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
|
|
293
294
|
{ id: "bold", label: "Bold", width: 3.5 }
|
|
294
295
|
];
|
|
295
|
-
var VD_FLOWCHART_VERSION = "1.
|
|
296
|
+
var VD_FLOWCHART_VERSION = "1.3.0";
|
|
297
|
+
var FLOWCHART_DOCUMENT_VERSION = "1.2.0";
|
|
296
298
|
var FLOWCHART_NODE_TYPES = [
|
|
297
299
|
"rounded-rect",
|
|
298
300
|
"rect",
|
|
@@ -529,17 +531,45 @@ function normalizeEdge(rawEdge, index, nodeIds, usedIds) {
|
|
|
529
531
|
syncEdgeKind(edge);
|
|
530
532
|
return edge;
|
|
531
533
|
}
|
|
534
|
+
function parseDocumentVersion(value) {
|
|
535
|
+
const raw = typeof value === "number" && Number.isFinite(value) ? String(value) : typeof value === "string" ? value.trim() : "";
|
|
536
|
+
const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(raw);
|
|
537
|
+
if (!match) return null;
|
|
538
|
+
return {
|
|
539
|
+
major: Number(match[1]),
|
|
540
|
+
minor: Number(match[2] ?? 0),
|
|
541
|
+
patch: Number(match[3] ?? 0)
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function isSupportedDocumentVersion(value) {
|
|
545
|
+
const parsed = parseDocumentVersion(value);
|
|
546
|
+
const max = parseDocumentVersion(FLOWCHART_DOCUMENT_VERSION);
|
|
547
|
+
if (!parsed || !max || parsed.major !== max.major) return false;
|
|
548
|
+
if (parsed.minor > max.minor) return false;
|
|
549
|
+
if (parsed.minor === max.minor && parsed.patch > max.patch) return false;
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
532
552
|
function normalizeDocument(input) {
|
|
533
553
|
let source = input;
|
|
534
554
|
if (typeof source === "string") {
|
|
535
555
|
try {
|
|
536
556
|
source = JSON.parse(source);
|
|
537
557
|
} catch {
|
|
538
|
-
|
|
558
|
+
throw new TypeError("Invalid flowchart JSON. The current document was not changed.");
|
|
539
559
|
}
|
|
540
560
|
}
|
|
541
561
|
if (!isPlainObject(source)) {
|
|
542
|
-
|
|
562
|
+
throw new TypeError("A flowchart document must be an object.");
|
|
563
|
+
}
|
|
564
|
+
if (source.version !== void 0 && !isSupportedDocumentVersion(source.version)) {
|
|
565
|
+
throw new RangeError(
|
|
566
|
+
`Unsupported flowchart document version: ${String(source.version)}. Supported through ${FLOWCHART_DOCUMENT_VERSION}.`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
for (const key of ["nodes", "edges"]) {
|
|
570
|
+
if (source[key] !== void 0 && !Array.isArray(source[key])) {
|
|
571
|
+
throw new TypeError(`Flowchart ${key} must be an array.`);
|
|
572
|
+
}
|
|
543
573
|
}
|
|
544
574
|
const usedNodeIds = /* @__PURE__ */ new Set();
|
|
545
575
|
const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds));
|
|
@@ -547,7 +577,7 @@ function normalizeDocument(input) {
|
|
|
547
577
|
const usedEdgeIds = /* @__PURE__ */ new Set();
|
|
548
578
|
const edges = (Array.isArray(source.edges) ? source.edges : []).slice(0, MAX_EDGES).map((edge, index) => normalizeEdge(edge, index, nodeIds, usedEdgeIds)).filter(Boolean);
|
|
549
579
|
return {
|
|
550
|
-
version:
|
|
580
|
+
version: FLOWCHART_DOCUMENT_VERSION,
|
|
551
581
|
viewport: normalizeViewport(source.viewport),
|
|
552
582
|
nodes,
|
|
553
583
|
edges
|
|
@@ -1002,6 +1032,8 @@ function getBounds(nodes) {
|
|
|
1002
1032
|
function createField(labelText, control) {
|
|
1003
1033
|
const wrapper = createElement("div", { className: "vd-flowchart-field" });
|
|
1004
1034
|
const label = createElement("label", { text: labelText });
|
|
1035
|
+
control.id || (control.id = nextId("flowchart-field"));
|
|
1036
|
+
label.htmlFor = control.id;
|
|
1005
1037
|
wrapper.appendChild(label);
|
|
1006
1038
|
wrapper.appendChild(control);
|
|
1007
1039
|
return wrapper;
|
|
@@ -1386,14 +1418,127 @@ var VdFlowchart = class {
|
|
|
1386
1418
|
this.jsonActions.appendChild(this.loadJsonButton);
|
|
1387
1419
|
this.jsonPanel.appendChild(this.jsonTextarea);
|
|
1388
1420
|
this.jsonPanel.appendChild(this.jsonActions);
|
|
1421
|
+
this.jsonStatus = createElement("p", { className: "vd-flowchart-json-status" });
|
|
1422
|
+
this.jsonStatus.setAttribute("role", "status");
|
|
1423
|
+
this.jsonPanel.appendChild(this.jsonStatus);
|
|
1389
1424
|
this.inspectorPanel.appendChild(this.jsonPanel);
|
|
1390
1425
|
this.body.appendChild(this.palettePanel);
|
|
1391
1426
|
this.body.appendChild(this.canvasEl);
|
|
1392
1427
|
this.body.appendChild(this.inspectorPanel);
|
|
1393
1428
|
this.root.appendChild(this.toolbar);
|
|
1394
1429
|
this.root.appendChild(this.body);
|
|
1430
|
+
this.buildGraphOutline();
|
|
1395
1431
|
this.element.appendChild(this.root);
|
|
1396
1432
|
}
|
|
1433
|
+
buildGraphOutline() {
|
|
1434
|
+
this.graphOutline = createElement("details", { className: "vd-flowchart-outline" });
|
|
1435
|
+
this.graphOutline.appendChild(createElement("summary", { text: "Graph outline" }));
|
|
1436
|
+
this.graphOutline.addEventListener("toggle", () => {
|
|
1437
|
+
if (this.graphOutline.open) this.renderGraphOutline();
|
|
1438
|
+
});
|
|
1439
|
+
this.graphStatus = createElement("p", { className: "vd-flowchart-sr-only" });
|
|
1440
|
+
this.graphStatus.setAttribute("role", "status");
|
|
1441
|
+
this.canvasEl.appendChild(this.graphStatus);
|
|
1442
|
+
this.graphControls = createElement("div", { className: "vd-flowchart-outline-controls" });
|
|
1443
|
+
this.graphNodeSelect = createElement("select");
|
|
1444
|
+
this.graphNodeSelect.setAttribute("aria-label", "Selected node");
|
|
1445
|
+
this.graphNodeSelect.addEventListener(
|
|
1446
|
+
"change",
|
|
1447
|
+
() => this.selectNode(this.graphNodeSelect.value)
|
|
1448
|
+
);
|
|
1449
|
+
this.graphTargetSelect = createElement("select");
|
|
1450
|
+
this.graphTargetSelect.setAttribute("aria-label", "Connection target");
|
|
1451
|
+
this.graphTargetSelect.addEventListener("change", () => this.renderGraphOutline());
|
|
1452
|
+
this.graphConnectButton = createElement("button", {
|
|
1453
|
+
text: "Connect nodes",
|
|
1454
|
+
type: "button",
|
|
1455
|
+
className: "vd-flowchart-btn"
|
|
1456
|
+
});
|
|
1457
|
+
this.graphConnectButton.addEventListener("click", () => {
|
|
1458
|
+
if (this.readonly || this.graphNodeSelect.value === this.graphTargetSelect.value) {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
this.addEdge({
|
|
1462
|
+
from: this.graphNodeSelect.value,
|
|
1463
|
+
to: this.graphTargetSelect.value,
|
|
1464
|
+
autoPort: true
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1467
|
+
this.graphEditButton = createElement("button", {
|
|
1468
|
+
text: "Edit selected label",
|
|
1469
|
+
type: "button",
|
|
1470
|
+
className: "vd-flowchart-btn"
|
|
1471
|
+
});
|
|
1472
|
+
this.graphEditButton.addEventListener(
|
|
1473
|
+
"click",
|
|
1474
|
+
() => this.startTextEdit(this.graphNodeSelect.value)
|
|
1475
|
+
);
|
|
1476
|
+
this.graphControls.append(
|
|
1477
|
+
createField("Selected node", this.graphNodeSelect),
|
|
1478
|
+
createField("Connection target", this.graphTargetSelect),
|
|
1479
|
+
this.graphConnectButton,
|
|
1480
|
+
this.graphEditButton
|
|
1481
|
+
);
|
|
1482
|
+
this.graphOutline.appendChild(this.graphControls);
|
|
1483
|
+
this.graphList = createElement("ul");
|
|
1484
|
+
this.graphList.setAttribute("aria-label", "Nodes and connections");
|
|
1485
|
+
this.graphOutline.appendChild(this.graphList);
|
|
1486
|
+
this.root.appendChild(this.graphOutline);
|
|
1487
|
+
this.canvasEl.setAttribute("role", "group");
|
|
1488
|
+
this.canvasEl.setAttribute(
|
|
1489
|
+
"aria-label",
|
|
1490
|
+
"Diagram canvas. Arrow keys select nodes; Enter edits a label. Graph outline provides connections."
|
|
1491
|
+
);
|
|
1492
|
+
this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
|
|
1493
|
+
}
|
|
1494
|
+
renderGraphOutline() {
|
|
1495
|
+
if (!this.graphOutline) return;
|
|
1496
|
+
const nodes = this.documentData.nodes;
|
|
1497
|
+
const edges = this.documentData.edges;
|
|
1498
|
+
const labels = new Map(nodes.map((node) => [node.id, node.text || node.id]));
|
|
1499
|
+
const selected = this.selection?.kind === "node" ? this.selection.id : null;
|
|
1500
|
+
this.graphStatus.textContent = selected ? `Selected: ${labels.get(selected)}. ${nodes.length} nodes, ${edges.length} connections.` : `${nodes.length} nodes, ${edges.length} connections.`;
|
|
1501
|
+
if (!this.graphOutline.open) return;
|
|
1502
|
+
const signature = JSON.stringify([
|
|
1503
|
+
nodes.map((node) => [node.id, node.text]),
|
|
1504
|
+
edges.map((edge) => [edge.id, edge.from.nodeId, edge.to.nodeId, edge.label])
|
|
1505
|
+
]);
|
|
1506
|
+
if (signature !== this.graphSignature) {
|
|
1507
|
+
this.graphSignature = signature;
|
|
1508
|
+
for (const select of [this.graphNodeSelect, this.graphTargetSelect]) {
|
|
1509
|
+
const previous = select.value;
|
|
1510
|
+
select.replaceChildren();
|
|
1511
|
+
for (const node of nodes)
|
|
1512
|
+
select.appendChild(
|
|
1513
|
+
createElement("option", { value: node.id, text: labels.get(node.id) })
|
|
1514
|
+
);
|
|
1515
|
+
if (labels.has(previous)) select.value = previous;
|
|
1516
|
+
}
|
|
1517
|
+
this.graphList.replaceChildren();
|
|
1518
|
+
const outgoing = new Map(nodes.map((node) => [node.id, []]));
|
|
1519
|
+
const incoming = new Map(nodes.map((node) => [node.id, []]));
|
|
1520
|
+
for (const edge of edges) {
|
|
1521
|
+
outgoing.get(edge.from.nodeId)?.push(`${labels.get(edge.to.nodeId)}${edge.label ? ` (${edge.label})` : ""}`);
|
|
1522
|
+
incoming.get(edge.to.nodeId)?.push(labels.get(edge.from.nodeId));
|
|
1523
|
+
}
|
|
1524
|
+
for (const node of nodes) {
|
|
1525
|
+
const to = outgoing.get(node.id);
|
|
1526
|
+
const from = incoming.get(node.id);
|
|
1527
|
+
const relationships = [
|
|
1528
|
+
to.length ? `Connects to ${to.join(", ")}.` : "No outgoing connections.",
|
|
1529
|
+
from.length ? `Connected from ${from.join(", ")}.` : ""
|
|
1530
|
+
];
|
|
1531
|
+
this.graphList.appendChild(
|
|
1532
|
+
createElement("li", { text: `${labels.get(node.id)}. ${relationships.join(" ")}` })
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (selected) this.graphNodeSelect.value = selected;
|
|
1537
|
+
this.graphNodeSelect.disabled = nodes.length === 0;
|
|
1538
|
+
this.graphTargetSelect.disabled = this.readonly || nodes.length < 2;
|
|
1539
|
+
this.graphConnectButton.disabled = this.readonly || nodes.length < 2 || this.graphNodeSelect.value === this.graphTargetSelect.value;
|
|
1540
|
+
this.graphEditButton.disabled = this.readonly || nodes.length === 0;
|
|
1541
|
+
}
|
|
1397
1542
|
bindEvents() {
|
|
1398
1543
|
this.toolbar.addEventListener("click", this.handleToolbarClick);
|
|
1399
1544
|
this.arrangeSelect.addEventListener("change", this.handleArrangeChange);
|
|
@@ -1500,7 +1645,12 @@ var VdFlowchart = class {
|
|
|
1500
1645
|
return;
|
|
1501
1646
|
}
|
|
1502
1647
|
if (action === "load" && !this.readonly) {
|
|
1503
|
-
|
|
1648
|
+
try {
|
|
1649
|
+
this.load(this.jsonTextarea.value);
|
|
1650
|
+
this.jsonStatus.textContent = "";
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
this.jsonStatus.textContent = error.message;
|
|
1653
|
+
}
|
|
1504
1654
|
}
|
|
1505
1655
|
}
|
|
1506
1656
|
handleSelectionFieldInput(event) {
|
|
@@ -1571,10 +1721,31 @@ var VdFlowchart = class {
|
|
|
1571
1721
|
}
|
|
1572
1722
|
}
|
|
1573
1723
|
handleKeyDown(event) {
|
|
1574
|
-
if (this.readonly) return;
|
|
1575
1724
|
if (event.target && (event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA" || event.target.tagName === "SELECT")) {
|
|
1576
1725
|
return;
|
|
1577
1726
|
}
|
|
1727
|
+
if (event.target === this.canvasEl) {
|
|
1728
|
+
const nodes = this.documentData.nodes;
|
|
1729
|
+
const index = nodes.findIndex((node) => node.id === this.selection?.id);
|
|
1730
|
+
let next = -1;
|
|
1731
|
+
if (event.key === "ArrowDown" || event.key === "ArrowRight")
|
|
1732
|
+
next = (index + 1) % nodes.length;
|
|
1733
|
+
if (event.key === "ArrowUp" || event.key === "ArrowLeft")
|
|
1734
|
+
next = index < 0 ? nodes.length - 1 : (index - 1 + nodes.length) % nodes.length;
|
|
1735
|
+
if (event.key === "Home") next = 0;
|
|
1736
|
+
if (event.key === "End") next = nodes.length - 1;
|
|
1737
|
+
if (nodes[next]) {
|
|
1738
|
+
event.preventDefault();
|
|
1739
|
+
this.selectNode(nodes[next].id);
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
if (event.key === "Enter" && !this.readonly && this.selection?.kind === "node") {
|
|
1743
|
+
event.preventDefault();
|
|
1744
|
+
this.startTextEdit(this.selection.id);
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
if (this.readonly) return;
|
|
1578
1749
|
if (event.key === "Escape" && this.activeTool) {
|
|
1579
1750
|
event.preventDefault();
|
|
1580
1751
|
this.setActiveTool(null);
|
|
@@ -1865,7 +2036,7 @@ var VdFlowchart = class {
|
|
|
1865
2036
|
node.x = nextX;
|
|
1866
2037
|
node.y = nextY;
|
|
1867
2038
|
this.interaction.moved = true;
|
|
1868
|
-
this.
|
|
2039
|
+
this.renderDraggedNode(node);
|
|
1869
2040
|
}
|
|
1870
2041
|
return;
|
|
1871
2042
|
}
|
|
@@ -2240,11 +2411,13 @@ var VdFlowchart = class {
|
|
|
2240
2411
|
if (event.key === "Escape") {
|
|
2241
2412
|
event.preventDefault();
|
|
2242
2413
|
this.stopTextEdit({ commit: false });
|
|
2414
|
+
this.canvasEl.focus();
|
|
2243
2415
|
return;
|
|
2244
2416
|
}
|
|
2245
2417
|
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
2246
2418
|
event.preventDefault();
|
|
2247
2419
|
this.stopTextEdit({ commit: true });
|
|
2420
|
+
this.canvasEl.focus();
|
|
2248
2421
|
}
|
|
2249
2422
|
});
|
|
2250
2423
|
textarea.addEventListener("blur", () => {
|
|
@@ -2318,6 +2491,7 @@ var VdFlowchart = class {
|
|
|
2318
2491
|
if (shouldRenderScene) this.renderScene();
|
|
2319
2492
|
if (shouldRenderInspector) this.renderSelectionPanel();
|
|
2320
2493
|
if (shouldRenderJson) this.syncJsonTextarea();
|
|
2494
|
+
if (shouldRenderInspector) this.renderGraphOutline();
|
|
2321
2495
|
this.updateToolbarLabel();
|
|
2322
2496
|
this.updatePaletteState();
|
|
2323
2497
|
this.positionTextEditor();
|
|
@@ -2333,15 +2507,28 @@ var VdFlowchart = class {
|
|
|
2333
2507
|
clearChildren(this.nodesLayer);
|
|
2334
2508
|
clearChildren(this.overlayLayer);
|
|
2335
2509
|
const nodeMap = new Map(this.documentData.nodes.map((node) => [node.id, node]));
|
|
2510
|
+
this.sceneNodeMap = nodeMap;
|
|
2511
|
+
this.nodeElements = /* @__PURE__ */ new Map();
|
|
2512
|
+
this.edgeElements = /* @__PURE__ */ new Map();
|
|
2513
|
+
this.incidentEdges = /* @__PURE__ */ new Map();
|
|
2336
2514
|
this.documentData.edges.forEach((edge) => {
|
|
2337
2515
|
const edgeElement = this.renderEdge(edge, nodeMap);
|
|
2338
|
-
if (edgeElement)
|
|
2516
|
+
if (edgeElement) {
|
|
2517
|
+
this.edgesLayer.appendChild(edgeElement);
|
|
2518
|
+
this.edgeElements.set(edge.id, edgeElement);
|
|
2519
|
+
for (const id of /* @__PURE__ */ new Set([edge.from.nodeId, edge.to.nodeId])) {
|
|
2520
|
+
if (!this.incidentEdges.has(id)) this.incidentEdges.set(id, []);
|
|
2521
|
+
this.incidentEdges.get(id).push(edge);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2339
2524
|
});
|
|
2340
2525
|
if (this.interaction?.kind === "connect" || this.interaction?.kind === "reconnect") {
|
|
2341
2526
|
this.previewLayer.appendChild(this.renderPreviewEdge(this.interaction));
|
|
2342
2527
|
}
|
|
2343
2528
|
this.documentData.nodes.forEach((node) => {
|
|
2344
|
-
this.
|
|
2529
|
+
const element = this.renderNode(node);
|
|
2530
|
+
this.nodesLayer.appendChild(element);
|
|
2531
|
+
this.nodeElements.set(node.id, element);
|
|
2345
2532
|
});
|
|
2346
2533
|
if (this.reconnectEdgeId && !this.readonly) {
|
|
2347
2534
|
const reconnectEdge = this.findEdge(this.reconnectEdgeId);
|
|
@@ -2357,6 +2544,25 @@ var VdFlowchart = class {
|
|
|
2357
2544
|
}
|
|
2358
2545
|
this.syncConnectingState();
|
|
2359
2546
|
}
|
|
2547
|
+
// Drag frames change one translation and its connected paths. Keep unrelated
|
|
2548
|
+
// SVG elements intact; the release event still refreshes inspector/history.
|
|
2549
|
+
renderDraggedNode(node) {
|
|
2550
|
+
const element = this.nodeElements?.get(node.id);
|
|
2551
|
+
if (!element) {
|
|
2552
|
+
this.render({ inspector: false, json: false });
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
element.setAttribute("transform", `translate(${formatNumber(node.x)} ${formatNumber(node.y)})`);
|
|
2556
|
+
element.classList.add("is-dragging");
|
|
2557
|
+
for (const edge of this.incidentEdges.get(node.id) || []) {
|
|
2558
|
+
const previous = this.edgeElements.get(edge.id);
|
|
2559
|
+
const next = this.renderEdge(edge, this.sceneNodeMap);
|
|
2560
|
+
if (previous && next) {
|
|
2561
|
+
previous.replaceWith(next);
|
|
2562
|
+
this.edgeElements.set(edge.id, next);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2360
2566
|
applyEdgeMarkers(pathElement, edge) {
|
|
2361
2567
|
if (edge.startMarker && edge.startMarker !== "none") {
|
|
2362
2568
|
pathElement.setAttribute(
|
|
@@ -3371,20 +3577,69 @@ var VdFlowchart = class {
|
|
|
3371
3577
|
this.emitChange("clear");
|
|
3372
3578
|
return this;
|
|
3373
3579
|
}
|
|
3580
|
+
updateOptions(options = {}) {
|
|
3581
|
+
if (this.destroyed) return this;
|
|
3582
|
+
if ("readonly" in options) {
|
|
3583
|
+
if (options.readonly && !this.readonly) {
|
|
3584
|
+
this.stopTextEdit({ commit: true });
|
|
3585
|
+
this.interaction = null;
|
|
3586
|
+
this.activeTool = null;
|
|
3587
|
+
this.reconnectEdgeId = null;
|
|
3588
|
+
}
|
|
3589
|
+
this.readonly = Boolean(options.readonly);
|
|
3590
|
+
this.root.classList.toggle("vd-flowchart-readonly", this.readonly);
|
|
3591
|
+
this.arrangeSelect.disabled = this.readonly;
|
|
3592
|
+
this.clearButton.disabled = this.readonly;
|
|
3593
|
+
this.loadJsonButton.disabled = this.readonly;
|
|
3594
|
+
}
|
|
3595
|
+
if ("gridSize" in options) {
|
|
3596
|
+
this.gridSize = clamp(toFiniteNumber(options.gridSize, DEFAULT_GRID_SIZE), 12, 64);
|
|
3597
|
+
const pattern = this.svg.querySelector("pattern");
|
|
3598
|
+
pattern.setAttribute("width", this.gridSize);
|
|
3599
|
+
pattern.setAttribute("height", this.gridSize);
|
|
3600
|
+
pattern.querySelector("path").setAttribute("d", `M ${this.gridSize} 0 L 0 0 0 ${this.gridSize}`);
|
|
3601
|
+
}
|
|
3602
|
+
if ("autoFit" in options) this.autoFit = Boolean(options.autoFit);
|
|
3603
|
+
if ("history" in options && options.history !== false !== this.historyEnabled) {
|
|
3604
|
+
this.historyEnabled = options.history !== false;
|
|
3605
|
+
this.history = [];
|
|
3606
|
+
this.historyIndex = -1;
|
|
3607
|
+
this.seedHistory();
|
|
3608
|
+
}
|
|
3609
|
+
if ("historyLimit" in options) {
|
|
3610
|
+
this.historyLimit = Math.max(1, Math.floor(toFiniteNumber(options.historyLimit, 100)));
|
|
3611
|
+
if (this.history.length > this.historyLimit) {
|
|
3612
|
+
const start = Math.max(0, this.historyIndex - this.historyLimit + 1);
|
|
3613
|
+
this.history = this.history.slice(start, start + this.historyLimit);
|
|
3614
|
+
this.historyIndex -= start;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
this.render();
|
|
3618
|
+
this.updateHistoryButtons();
|
|
3619
|
+
return this;
|
|
3620
|
+
}
|
|
3374
3621
|
load(data, options = {}) {
|
|
3622
|
+
const documentData = normalizeDocument(data);
|
|
3623
|
+
if (options.silent && JSON.stringify(documentData) === JSON.stringify(this.documentData)) {
|
|
3624
|
+
return this;
|
|
3625
|
+
}
|
|
3375
3626
|
this.stopTextEdit({ commit: false });
|
|
3376
3627
|
this.activeTool = null;
|
|
3377
3628
|
this.reconnectEdgeId = null;
|
|
3378
3629
|
const previousSelection = this.selection;
|
|
3379
|
-
this.documentData =
|
|
3630
|
+
this.documentData = documentData;
|
|
3380
3631
|
this.selection = this.resolvePreservedSelection(previousSelection, options.preserveSelection);
|
|
3381
3632
|
this.render();
|
|
3382
|
-
|
|
3633
|
+
if (options.silent) {
|
|
3634
|
+
if (this.historyEnabled) this.recordHistory("load");
|
|
3635
|
+
} else {
|
|
3636
|
+
this.emitChange("load");
|
|
3637
|
+
}
|
|
3383
3638
|
return this;
|
|
3384
3639
|
}
|
|
3385
3640
|
toJSON() {
|
|
3386
3641
|
return deepClone({
|
|
3387
|
-
version:
|
|
3642
|
+
version: FLOWCHART_DOCUMENT_VERSION,
|
|
3388
3643
|
viewport: this.documentData.viewport,
|
|
3389
3644
|
nodes: this.documentData.nodes,
|
|
3390
3645
|
edges: this.documentData.edges
|
|
@@ -3398,6 +3653,10 @@ var VdFlowchart = class {
|
|
|
3398
3653
|
this.resizeObserver?.disconnect();
|
|
3399
3654
|
this.resizeObserver = null;
|
|
3400
3655
|
this.unbindEvents();
|
|
3656
|
+
this.sceneNodeMap?.clear();
|
|
3657
|
+
this.nodeElements?.clear();
|
|
3658
|
+
this.edgeElements?.clear();
|
|
3659
|
+
this.incidentEdges?.clear();
|
|
3401
3660
|
this.element.innerHTML = "";
|
|
3402
3661
|
this.element.classList.remove("vd-flowchart-host");
|
|
3403
3662
|
}
|
|
@@ -3449,7 +3708,7 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
|
|
|
3449
3708
|
() => props.data,
|
|
3450
3709
|
(next) => {
|
|
3451
3710
|
if (instance && typeof instance.load === "function") {
|
|
3452
|
-
instance.load(next, { preserveSelection: props.preserveSelection });
|
|
3711
|
+
instance.load(next, { preserveSelection: props.preserveSelection, silent: true });
|
|
3453
3712
|
}
|
|
3454
3713
|
},
|
|
3455
3714
|
{ deep: true }
|
|
@@ -3458,8 +3717,13 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
|
|
|
3458
3717
|
() => [props.readonly, props.gridSize, props.autoFit, props.history, props.historyLimit],
|
|
3459
3718
|
() => {
|
|
3460
3719
|
if (!instance) return;
|
|
3461
|
-
instance.
|
|
3462
|
-
|
|
3720
|
+
instance.updateOptions({
|
|
3721
|
+
readonly: props.readonly,
|
|
3722
|
+
gridSize: props.gridSize,
|
|
3723
|
+
autoFit: props.autoFit,
|
|
3724
|
+
history: props.history,
|
|
3725
|
+
historyLimit: props.historyLimit
|
|
3726
|
+
});
|
|
3463
3727
|
}
|
|
3464
3728
|
);
|
|
3465
3729
|
(0, import_vue.onBeforeUnmount)(() => {
|