@peter.prib/renderengine 0.2.6 → 0.2.7

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.
Files changed (49) hide show
  1. package/README.md +4 -0
  2. package/REFERENCE.md +180 -2
  3. package/dist/renderengine-workspace.umd.js +8704 -5310
  4. package/dist/renderengine.umd.js +5675 -3398
  5. package/examples/archimate.html +68 -66
  6. package/examples/data-model.html +223 -13
  7. package/package.json +1 -1
  8. package/src/bian.js +182 -0
  9. package/src/charting/DiagramEditor.js +5 -26
  10. package/src/charting/DiagramTypeRegistry.js +8 -2
  11. package/src/charting/ModelCheckDialog.js +63 -0
  12. package/src/charting/ModelChecker.js +26 -0
  13. package/src/charting/ViewTabStrip.js +94 -81
  14. package/src/charting/datamodel/BianLandscapeReader.js +185 -0
  15. package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
  16. package/src/charting/datamodel/DataModel.js +356 -10
  17. package/src/charting/datamodel/DataModelChecker.js +92 -0
  18. package/src/charting/datamodel/DataModelCompared.js +127 -0
  19. package/src/charting/datamodel/DataModelFileFormat.js +33 -0
  20. package/src/charting/datamodel/DataModelPerspective.js +45 -0
  21. package/src/charting/datamodel/DataType.js +108 -0
  22. package/src/charting/datamodel/EnumerationType.js +51 -0
  23. package/src/charting/datamodel/ServiceDomain.js +177 -0
  24. package/src/charting/datamodel/ServiceLandscape.js +104 -0
  25. package/src/charting/datamodel/StructuredType.js +63 -0
  26. package/src/charting/datamodel/index.js +10 -0
  27. package/src/charting/diagramModel/MergeModelCommand.js +43 -37
  28. package/src/charting/diagramModel/OpenViewList.js +75 -0
  29. package/src/charting/diagramModel/ViewSetChecker.js +22 -34
  30. package/src/charting/diagrams/DataModelRelationshipStrategy.js +12 -4
  31. package/src/charting/diagrams/EdgeGeometry.js +578 -553
  32. package/src/charting/diagrams/GraphDiagram.js +11 -1
  33. package/src/charting/diagrams/NodeGeometry.js +169 -158
  34. package/src/charting/glyphs/PersonGlyph.js +24 -23
  35. package/src/index.js +2666 -2664
  36. package/src/workspace/EditorShell.js +166 -67
  37. package/src/workspace/PanelLayout.js +56 -0
  38. package/src/workspace/layout/TabsNode.js +205 -191
  39. package/src/workspace/parts/ArchimateCanvasPart.js +5 -7
  40. package/src/workspace/parts/DataModelPart.js +339 -29
  41. package/src/workspace/parts/DiagramEditorPart.js +2 -8
  42. package/src/workspace/parts/EditorPart.js +461 -407
  43. package/src/workspace/parts/ModelFile.js +216 -187
  44. package/src/workspace/parts/ModelsPart.js +483 -192
  45. package/src/workspace/storage/BackupFolder.js +145 -0
  46. package/src/workspace/storage/KeyValueStore.js +92 -0
  47. package/src/workspace/storage/ModelLibrary.js +65 -0
  48. package/src/workspace/workArea.js +417 -413
  49. package/src/yaml/YamlReader.js +372 -0
@@ -1,191 +1,205 @@
1
- import { LayoutNode } from './LayoutNode.js';
2
- import { el } from '../dom.js';
3
-
4
- /**
5
- * TabsNode — a tab strip over a stack of children, one visible at a time.
6
- * This is what workbench's eight mutually exclusive tool-panels become, and
7
- * what a WorkArea uses as its editor groups.
8
- *
9
- * Three deliberate behaviours:
10
- * - **Lazy first mount.** A child is only mounted when its tab is first
11
- * selected, so opening a workspace does not pay for eight panels nobody
12
- * looked at. This is the case ContextBus's retained-value replay exists for:
13
- * a part mounting on first activation still sees current selection.
14
- * - **No unmount on switch.** Once mounted a child stays mounted and is merely
15
- * hidden. Tearing down and rebuilding per switch is what makes a shell feel
16
- * slow, and it would discard scroll position and unsaved input.
17
- * - **Closing is optional and delegable.** `closable` adds a close control per
18
- * tab (per-child `closable: false` opts one out — a pinned welcome tab, say).
19
- * With no `ctx.onTabClose` the node closes the tab itself, so a plain layout
20
- * gets working close buttons; with one, the handler owns it, which is how
21
- * WorkArea keeps closing in its model instead of in the DOM.
22
- */
23
- export class TabsNode extends LayoutNode {
24
- static type = 'tabs';
25
-
26
- constructor(spec = {}) {
27
- super(spec);
28
- this.active = Number.isInteger(spec.active) ? spec.active : 0;
29
- /** Group identity — what "open beside THIS group" addresses. */
30
- this.id = spec.id ?? null;
31
- this.closable = spec.closable ?? false;
32
- this._panes = [];
33
- this._tabs = [];
34
- }
35
-
36
- build(ctx) {
37
- this.el.classList.add('ws-tabs');
38
- this.el.append(
39
- el({ cls: 'ws-tabstrip', attrs: { role: 'tablist' }, ref: n => { this.strip = n; } }),
40
- el({ cls: 'ws-tabbody', ref: n => { this.body = n; } }),
41
- );
42
-
43
- // Capture phase: a part may stopPropagation on its own clicks, and
44
- // "the group you last touched" has to hold even then, or the next
45
- // thing you open lands somewhere you weren't looking.
46
- if (this.id) {
47
- this.el.addEventListener('pointerdown', () => ctx.onGroupFocus?.(this.id, this), true);
48
- }
49
-
50
- this._panes = [];
51
- this._tabs = [];
52
-
53
- this.children.forEach((child, i) => {
54
- const pane = el({ cls: 'ws-tabpane' });
55
- this.body.appendChild(pane);
56
- // Render before reading label(): a PartNode only resolves its part
57
- // — and therefore its title — during render(). Building the tab
58
- // first would label every tab with the raw part id.
59
- child.render(pane, ctx);
60
- this._panes.push(pane);
61
- this._tabs.push(this.strip.appendChild(this._buildTab(child, i)));
62
- });
63
-
64
- if (this.children.length) this.select(Math.min(this.active, this.children.length - 1), true);
65
- }
66
-
67
- /**
68
- * One tab. A `<div role="tab">` rather than a `<button>` because a closable
69
- * tab nests a close button, and a button inside a button is invalid markup
70
- * that browsers reparent — so `tabindex`/`keydown` stand in for the button
71
- * behaviour that is lost.
72
- */
73
- _buildTab(child, i) {
74
- const label = child.label?.() ?? child.spec?.title ?? `Tab ${i + 1}`;
75
- const closable = this.closable && (child.spec?.closable ?? true);
76
- return el({
77
- cls: 'ws-tab',
78
- attrs: { role: 'tab', tabindex: '0', title: label },
79
- on: {
80
- click: () => this.select(i),
81
- keydown: (e) => {
82
- if (e.key !== 'Enter' && e.key !== ' ') return;
83
- e.preventDefault();
84
- this.select(i);
85
- },
86
- // Middle-click closes, as it does in every editor and browser.
87
- auxclick: (e) => { if (closable && e.button === 1) { e.preventDefault(); this.requestClose(i); } },
88
- },
89
- children: [
90
- { tag: 'span', cls: 'ws-tab-label', text: label },
91
- closable && {
92
- tag: 'button',
93
- cls: 'ws-tabclose',
94
- text: '×',
95
- attrs: { type: 'button', tabindex: '-1', 'aria-label': `Close ${label}` },
96
- // Without stopPropagation the tab's own click handler also
97
- // fires and selects the tab we are closing.
98
- on: { click: (e) => { e.stopPropagation(); this.requestClose(i); } },
99
- },
100
- ].filter(Boolean),
101
- });
102
- }
103
-
104
- select(index, silent = false) {
105
- if (index < 0 || index >= this.children.length) return;
106
- this.active = index;
107
- this._tabs.forEach((t, i) => {
108
- const on = i === index;
109
- t.classList.toggle('ws-tab-active', on);
110
- t.setAttribute('aria-selected', String(on));
111
- });
112
- this._panes.forEach((p, i) => { p.style.display = i === index ? '' : 'none'; });
113
- // Mount on first reveal only — activate() is idempotent.
114
- this.children[index].activate?.();
115
- if (!silent) this._emitChange();
116
- }
117
-
118
- /**
119
- * A tab's close control was used. Hands off to `ctx.onTabClose` when the
120
- * shell wants to own the decision (it may prompt about unsaved work, or
121
- * rearrange the surrounding splits); otherwise closes it here.
122
- */
123
- requestClose(index) {
124
- const child = this.children[index];
125
- if (!child) return;
126
- const detail = { tabs: this, index, partId: child.spec?.partId ?? null, node: child };
127
- if (this.ctx?.onTabClose) { this.ctx.onTabClose(detail); return; }
128
- this.closeTab(index);
129
- }
130
-
131
- /**
132
- * Remove a tab and its child. Selection moves to the neighbour rather than
133
- * back to the first tab, which is what closing the tab you were reading
134
- * should feel like.
135
- */
136
- closeTab(index) {
137
- const child = this.children[index];
138
- if (!child) return null;
139
- child.dispose();
140
- this._panes[index]?.remove?.();
141
- this._tabs[index]?.remove?.();
142
- this.children.splice(index, 1);
143
- this._panes.splice(index, 1);
144
- this._tabs.splice(index, 1);
145
- // Rebuilt so each tab's handlers close over its new index.
146
- this._tabs.forEach((tab, i) => {
147
- const fresh = this._buildTab(this.children[i], i);
148
- tab.replaceWith(fresh);
149
- this._tabs[i] = fresh;
150
- });
151
- if (this.children.length) {
152
- this.select(Math.min(index, this.children.length - 1), true);
153
- } else {
154
- this.active = 0;
155
- }
156
- this._emitChange();
157
- return child;
158
- }
159
-
160
- /** Show each tab's label as its child gives it now (a part's title that changed). */
161
- refreshLabels() {
162
- this._tabs.forEach((tab, i) => {
163
- const fresh = this._buildTab(this.children[i], i);
164
- fresh.classList.toggle('ws-tab-active', i === this.active);
165
- fresh.setAttribute('aria-selected', String(i === this.active));
166
- tab.replaceWith(fresh);
167
- this._tabs[i] = fresh;
168
- });
169
- }
170
-
171
- /** Activating the container activates only the visible child. */
172
- activate() {
173
- if (this.children.length) this.children[this.active]?.activate?.();
174
- }
175
-
176
- serialize() {
177
- // Spread the spec first: `id` and `closable` are part of the group's
178
- // identity, and a work area that lost group ids on save would reopen
179
- // with every "open beside this group" pointing at nothing.
180
- // The spec is spread rather than rebuilt field by field: `id` and
181
- // `closable` are part of a group's identity, and a work area that lost
182
- // group ids on save would come back with every "open beside this group"
183
- // pointing at nothing.
184
- return {
185
- ...this.spec,
186
- type: TabsNode.type,
187
- active: this.active,
188
- children: this.children.map(c => c.serialize()),
189
- };
190
- }
191
- }
1
+ import { LayoutNode } from './LayoutNode.js';
2
+ import { el } from '../dom.js';
3
+
4
+ /**
5
+ * TabsNode — a tab strip over a stack of children, one visible at a time.
6
+ * This is what workbench's eight mutually exclusive tool-panels become, and
7
+ * what a WorkArea uses as its editor groups.
8
+ *
9
+ * Three deliberate behaviours:
10
+ * - **Lazy first mount.** A child is only mounted when its tab is first
11
+ * selected, so opening a workspace does not pay for eight panels nobody
12
+ * looked at. This is the case ContextBus's retained-value replay exists for:
13
+ * a part mounting on first activation still sees current selection.
14
+ * - **No unmount on switch.** Once mounted a child stays mounted and is merely
15
+ * hidden. Tearing down and rebuilding per switch is what makes a shell feel
16
+ * slow, and it would discard scroll position and unsaved input.
17
+ * - **Closing is optional and delegable.** `closable` adds a close control per
18
+ * tab (per-child `closable: false` opts one out — a pinned welcome tab, say).
19
+ * With no `ctx.onTabClose` the node closes the tab itself, so a plain layout
20
+ * gets working close buttons; with one, the handler owns it, which is how
21
+ * WorkArea keeps closing in its model instead of in the DOM.
22
+ */
23
+ export class TabsNode extends LayoutNode {
24
+ static type = 'tabs';
25
+
26
+ constructor(spec = {}) {
27
+ super(spec);
28
+ this.active = Number.isInteger(spec.active) ? spec.active : 0;
29
+ /** Group identity — what "open beside THIS group" addresses. */
30
+ this.id = spec.id ?? null;
31
+ this.closable = spec.closable ?? false;
32
+ this._panes = [];
33
+ this._tabs = [];
34
+ }
35
+
36
+ build(ctx) {
37
+ this.el.classList.add('ws-tabs');
38
+ this.el.append(
39
+ el({ cls: 'ws-tabstrip', attrs: { role: 'tablist' }, ref: n => { this.strip = n; } }),
40
+ el({ cls: 'ws-tabbody', ref: n => { this.body = n; } }),
41
+ );
42
+
43
+ // Capture phase: a part may stopPropagation on its own clicks, and
44
+ // "the group you last touched" has to hold even then, or the next
45
+ // thing you open lands somewhere you weren't looking.
46
+ if (this.id) {
47
+ this.el.addEventListener('pointerdown', () => ctx.onGroupFocus?.(this.id, this), true);
48
+ }
49
+
50
+ this._panes = [];
51
+ this._tabs = [];
52
+
53
+ this.children.forEach((child, i) => {
54
+ const pane = el({ cls: 'ws-tabpane' });
55
+ this.body.appendChild(pane);
56
+ // Render before reading label(): a PartNode only resolves its part
57
+ // — and therefore its title — during render(). Building the tab
58
+ // first would label every tab with the raw part id.
59
+ child.render(pane, ctx);
60
+ this._panes.push(pane);
61
+ this._tabs.push(this.strip.appendChild(this._buildTab(child, i)));
62
+ });
63
+
64
+ if (this.children.length) this.select(Math.min(this.active, this.children.length - 1), true);
65
+ }
66
+
67
+ /**
68
+ * One tab. A `<div role="tab">` rather than a `<button>` because a closable
69
+ * tab nests a close button, and a button inside a button is invalid markup
70
+ * that browsers reparent — so `tabindex`/`keydown` stand in for the button
71
+ * behaviour that is lost.
72
+ */
73
+ _buildTab(child, i) {
74
+ const label = child.label?.() ?? child.spec?.title ?? `Tab ${i + 1}`;
75
+ const closable = this.closable && (child.spec?.closable ?? true);
76
+ return el({
77
+ cls: 'ws-tab',
78
+ attrs: { role: 'tab', tabindex: '0', title: label },
79
+ on: {
80
+ click: () => this.select(i),
81
+ keydown: (e) => {
82
+ if (e.key !== 'Enter' && e.key !== ' ') return;
83
+ e.preventDefault();
84
+ this.select(i);
85
+ },
86
+ // Middle-click closes, as it does in every editor and browser.
87
+ auxclick: (e) => { if (closable && e.button === 1) { e.preventDefault(); this.requestClose(i); } },
88
+ // Right-click: the shell's menu for what the tab holds, when it offers one.
89
+ contextmenu: (e) => this.requestMenu(i, e),
90
+ },
91
+ children: [
92
+ { tag: 'span', cls: 'ws-tab-label', text: label },
93
+ closable && {
94
+ tag: 'button',
95
+ cls: 'ws-tabclose',
96
+ text: '×',
97
+ attrs: { type: 'button', tabindex: '-1', 'aria-label': `Close ${label}` },
98
+ // Without stopPropagation the tab's own click handler also
99
+ // fires and selects the tab we are closing.
100
+ on: { click: (e) => { e.stopPropagation(); this.requestClose(i); } },
101
+ },
102
+ ].filter(Boolean),
103
+ });
104
+ }
105
+
106
+ select(index, silent = false) {
107
+ if (index < 0 || index >= this.children.length) return;
108
+ this.active = index;
109
+ this._tabs.forEach((t, i) => {
110
+ const on = i === index;
111
+ t.classList.toggle('ws-tab-active', on);
112
+ t.setAttribute('aria-selected', String(on));
113
+ });
114
+ this._panes.forEach((p, i) => { p.style.display = i === index ? '' : 'none'; });
115
+ // Mount on first reveal only — activate() is idempotent.
116
+ this.children[index].activate?.();
117
+ if (!silent) this._emitChange();
118
+ }
119
+
120
+ /**
121
+ * A tab was right-clicked: `ctx.onTabMenu` (the shell's menu for what it
122
+ * holds) gets it, with the event to place the menu by; without one, the
123
+ * browser's own menu shows.
124
+ */
125
+ requestMenu(index, event) {
126
+ const child = this.children[index];
127
+ if (!child || !this.ctx?.onTabMenu) return;
128
+ const shown = this.ctx.onTabMenu({ tabs: this, index, partId: child.spec?.partId ?? null, node: child, event });
129
+ if (shown) event.preventDefault();
130
+ }
131
+
132
+ /**
133
+ * A tab's close control was used. Hands off to `ctx.onTabClose` when the
134
+ * shell wants to own the decision (it may prompt about unsaved work, or
135
+ * rearrange the surrounding splits); otherwise closes it here.
136
+ */
137
+ requestClose(index) {
138
+ const child = this.children[index];
139
+ if (!child) return;
140
+ const detail = { tabs: this, index, partId: child.spec?.partId ?? null, node: child };
141
+ if (this.ctx?.onTabClose) { this.ctx.onTabClose(detail); return; }
142
+ this.closeTab(index);
143
+ }
144
+
145
+ /**
146
+ * Remove a tab and its child. Selection moves to the neighbour rather than
147
+ * back to the first tab, which is what closing the tab you were reading
148
+ * should feel like.
149
+ */
150
+ closeTab(index) {
151
+ const child = this.children[index];
152
+ if (!child) return null;
153
+ child.dispose();
154
+ this._panes[index]?.remove?.();
155
+ this._tabs[index]?.remove?.();
156
+ this.children.splice(index, 1);
157
+ this._panes.splice(index, 1);
158
+ this._tabs.splice(index, 1);
159
+ // Rebuilt so each tab's handlers close over its new index.
160
+ this._tabs.forEach((tab, i) => {
161
+ const fresh = this._buildTab(this.children[i], i);
162
+ tab.replaceWith(fresh);
163
+ this._tabs[i] = fresh;
164
+ });
165
+ if (this.children.length) {
166
+ this.select(Math.min(index, this.children.length - 1), true);
167
+ } else {
168
+ this.active = 0;
169
+ }
170
+ this._emitChange();
171
+ return child;
172
+ }
173
+
174
+ /** Show each tab's label as its child gives it now (a part's title that changed). */
175
+ refreshLabels() {
176
+ this._tabs.forEach((tab, i) => {
177
+ const fresh = this._buildTab(this.children[i], i);
178
+ fresh.classList.toggle('ws-tab-active', i === this.active);
179
+ fresh.setAttribute('aria-selected', String(i === this.active));
180
+ tab.replaceWith(fresh);
181
+ this._tabs[i] = fresh;
182
+ });
183
+ }
184
+
185
+ /** Activating the container activates only the visible child. */
186
+ activate() {
187
+ if (this.children.length) this.children[this.active]?.activate?.();
188
+ }
189
+
190
+ serialize() {
191
+ // Spread the spec first: `id` and `closable` are part of the group's
192
+ // identity, and a work area that lost group ids on save would reopen
193
+ // with every "open beside this group" pointing at nothing.
194
+ // The spec is spread rather than rebuilt field by field: `id` and
195
+ // `closable` are part of a group's identity, and a work area that lost
196
+ // group ids on save would come back with every "open beside this group"
197
+ // pointing at nothing.
198
+ return {
199
+ ...this.spec,
200
+ type: TabsNode.type,
201
+ active: this.active,
202
+ children: this.children.map(c => c.serialize()),
203
+ };
204
+ }
205
+ }
@@ -6,7 +6,6 @@ import { ArchimateCanvasEditor, ArchimatePalette, typeTitle } from '../../charti
6
6
  import { ArchimateNotation } from '../../archimate.js';
7
7
  import { MenuBar } from '../../charting/MenuBar.js';
8
8
  import { ToolbarText } from '../../charting/toolbar/index.js';
9
- import { ViewTabStrip } from '../../charting/ViewTabStrip.js';
10
9
  import { FormDialog } from '../../charting/FormDialog.js';
11
10
  import { CsvImportDialog } from '../../charting/archimate/CsvImportDialog.js';
12
11
  import { pickFile } from '../../charting/toolbar/pickFile.js';
@@ -189,13 +188,12 @@ export class ArchimateCanvasPart extends EditorPart {
189
188
  elements.id = 'elements';
190
189
  canvas.appendChild(elements);
191
190
 
192
- const tabs = new ViewTabStrip({
193
- document: doc,
194
- onSelect: (id) => model.switchTo(id),
195
- menuItems: (id) => this.viewMenu.items(id),
196
- }).render(model.views(), model.currentViewId);
191
+ const tabs = this.viewTabs();
197
192
 
198
- const { toolbar, side, main, aside } = new EditorShell({ document: doc, sideWidth: '260px', aside: true, asideWidth: this.spec.analysis ? '260px' : '210px' }).mount(this.container);
193
+ const { toolbar, side, main, aside } = new EditorShell({
194
+ document: doc, sideWidth: '260px', aside: true, asideWidth: this.spec.analysis ? '260px' : '210px',
195
+ layout: this.layout, labels: { side: 'Model', aside: this.spec.analysis ? 'Palette and analysis' : 'Palette' },
196
+ }).mount(this.container);
199
197
  Object.assign(aside.style, { padding: '6px', fontSize: '12px' });
200
198
  // With `analysis`, the palette shares the right with the analysis of the selected element.
201
199
  let paletteHost = aside, analysisHost = null;