@peter.prib/renderengine 0.2.3 → 0.2.4

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.
@@ -1,345 +1,345 @@
1
- import { render } from '../dom.js';
2
- import { EditorShell } from '../EditorShell.js';
3
- import { EditorPart } from './EditorPart.js';
4
- import { ArchimateModel } from '../../charting/diagrams/ArchimateModel.js';
5
- import { ArchimateCanvasEditor, ArchimatePalette, typeTitle } from '../../charting/archimate/index.js';
6
- import { ArchimateNotation } from '../../archimate.js';
7
- import { MenuBar } from '../../charting/MenuBar.js';
8
- import { ToolbarText } from '../../charting/toolbar/index.js';
9
- import { ViewTabStrip } from '../../charting/ViewTabStrip.js';
10
- import { FormDialog } from '../../charting/FormDialog.js';
11
- import { CsvImportDialog } from '../../charting/archimate/CsvImportDialog.js';
12
- import { pickFile } from '../../charting/toolbar/pickFile.js';
13
- import { SettingsManager } from '../../charting/SettingsManager.js';
14
- import { SettingsViewPropertiesDialog } from '../../charting/SettingsViewPropertiesDialog.js';
15
- import { CanvasTemplateLibrary } from '../../charting/canvas/CanvasTemplateLibrary.js';
16
- import { CanvasTemplateMenu } from '../../charting/canvas/CanvasTemplateMenu.js';
17
- import { CanvasPalette } from '../../charting/canvas/CanvasPalette.js';
18
- import { ARCHIMATE_FILE_FORMATS } from '../../charting/archimate/archimateFileFormats.js';
19
- import { ModelTree } from '../../charting/modelTree/ModelTree.js';
20
- import { buildModelTreePanel } from '../../charting/modelTree/ModelTreePanel.js';
21
- import { buildAnalysisPanel } from '../../charting/modelTree/AnalysisPanel.js';
22
- import { ArchimateSpecializationRegistry } from '../../archimateSpecializations.js';
23
- import { HintPanel } from '../../charting/HintPanel.js';
24
- import { elementMenuItems } from '../../charting/archimate/elementMenuItems.js';
25
- import { editElementProperties } from '../../charting/archimate/elementProperties.js';
26
- import { CustomViewpoints } from '../../charting/archimate/CustomViewpoints.js';
27
-
28
- const SVG_NS = 'http://www.w3.org/2000/svg';
29
-
30
- /**
31
- * ArchimateCanvasPart — the ArchiMate modeller on the diagram editor's own
32
- * canvas (phase 4 testbed): the model tree on the left, one tab per view and
33
- * an ArchimateCanvasEditor over the current view, and a palette of element
34
- * types grouped by layer on the right. Drop or click a palette type to add an
35
- * element, or drag one from the tree to show it (with its relationships to
36
- * what is shown); drag between two elements to add a relationship (its kind
37
- * picked from those the rules allow); every edit is a step on that view's own
38
- * undo history. The File menu exports and imports CSV, .archimate and the
39
- * Exchange Format.
40
- *
41
- * Spec (JSON): `elements`, `relationships`, `views`, `name` (as ArchimateModel
42
- * takes them), `archimateVersion` ('4' or '3.2'), `distinctShapes` (draw each
43
- * element in its notation shape rather than a box), `viewpoint` (a viewpoint name) and
44
- * `viewpointMode` ('fade' or 'hide': elements outside it), `analysis` (true adds the
45
- * analysis of the selected element, under the palette).
46
- */
47
- export class ArchimateCanvasPart extends EditorPart {
48
- static type = 'archimate-canvas';
49
- static viewNoun = 'view';
50
-
51
- constructor(spec = {}) {
52
- super(spec);
53
- this.version = spec.archimateVersion ?? '4';
54
- this.distinctShapes = !!spec.distinctShapes;
55
- this.viewpoint = spec.viewpoint ?? 'All';
56
- this.viewpointMode = spec.viewpointMode ?? 'fade';
57
- this.showDerived = !!spec.showDerived;
58
- this.settings = new SettingsManager();
59
- // A settings change (Model settings…, a view's properties) redraws the canvas.
60
- this._unsubscribeSettings = this.settings.onSettingsChange(() => { if (this.mounted) this.editor?.render(); });
61
- // The user's specializations (or the defaults), as the main editor offers them; one already loaded is kept.
62
- if (!ArchimateSpecializationRegistry.list().length) ArchimateSpecializationRegistry.loadFrom();
63
- this._follow(this.model);
64
- }
65
-
66
- /**
67
- * Re-render on every change to `model`'s views (the editor is rebuilt with the canvas on each
68
- * render), and redraw the tree after every edit (an element renamed, an undo).
69
- * @private
70
- */
71
- _follow(model) {
72
- this._unsubscribe?.();
73
- this._unsubscribeTree?.();
74
- this._unsubscribe = model.onChange(() => { if (this.mounted) this.render(); });
75
- this._unsubscribeTree = model.onEdit?.(() => this._treePanel?.refresh?.()) ?? null;
76
- }
77
-
78
- fileFormats() {
79
- return ARCHIMATE_FILE_FORMATS;
80
- }
81
-
82
- /** A new file is an Archi file (.archimate). */
83
- saveFormat() {
84
- return ARCHIMATE_FILE_FORMATS.find(f => f.featureName === 'Archimate');
85
- }
86
-
87
- /** The file label in the toolbar (and a ModelsPart's tab) follows the saved state. */
88
- onFileState() {
89
- super.onFileState();
90
- this.menuBar?.refresh?.();
91
- }
92
-
93
- /** New view…, then New canvas… and New canvas from template. */
94
- newItems() {
95
- return [...super.newItems(), ...this.templateMenu.newCanvasItems()];
96
- }
97
-
98
- /** File > Templates: the open canvas saved as a model template or into the library. */
99
- ownFileItems() {
100
- const saveAs = this.model.currentView ? this.templateMenu.saveAsTemplateItems(this.model.currentViewId) : [];
101
- return EditorPart.menuGroup('Templates', saveAs);
102
- }
103
-
104
- /** File > Import: an Archi canvas template, and a spreadsheet's rows (CSV with mapping). */
105
- ownImportItems() {
106
- const importTemplate = this.templateMenu.importItem();
107
- return [{ separator: true }, ...(importTemplate ? [{ ...importTemplate, label: 'Archi template…' }] : []), { label: 'CSV with mapping…', action: () => this.importCsvMapped() }];
108
- }
109
-
110
- /** Import CSV with mapping…: a spreadsheet's rows into this model (CsvImportDialog), the new elements on the open view. */
111
- async importCsvMapped() {
112
- const file = await pickFile('.csv,.txt,.tsv', this.document);
113
- if (!file) return null;
114
- return new CsvImportDialog({ model: this.model, view: this.model.currentView, version: this.version, document: this.document, onDone: () => this.render() }).open(await file.text());
115
- }
116
-
117
- /** An imported model: follow it, with a fresh tree, and give it a view if it has none. */
118
- modelReplaced(model) {
119
- if (!model.views().length) model.createView();
120
- this._follow(model);
121
- }
122
-
123
- /** The model tree over the current model (kept across renders, so its mode and selection stay). */
124
- get modelTree() {
125
- if (this.tree?.model !== this.model) {
126
- this.tree = new ModelTree(this.model, { mode: 'byLayer' });
127
- // A view chosen in the tree opens it; an element chosen there is selected on the canvas.
128
- this.tree.onSelectionChange((data) => {
129
- if (data.kind === 'view' && data.view.id !== this.model.currentViewId) this.model.switchTo(data.view.id);
130
- if (data.kind === 'element') { this.selectElement(data.id); this._showAnalysis?.(data.id); }
131
- });
132
- }
133
- return this.tree;
134
- }
135
-
136
- createModel(spec) {
137
- return new ArchimateModel({
138
- name: spec.name ?? '',
139
- elements: spec.elements ?? [],
140
- relationships: spec.relationships ?? [],
141
- views: spec.views?.length ? spec.views : [{ id: 'view-1', name: 'View 1', elementIds: [] }],
142
- });
143
- }
144
-
145
- /** A view's properties: name, documentation and its settings (routing, grid, background, ...). */
146
- showViewProperties(view) {
147
- return new SettingsViewPropertiesDialog({ model: this.model, view, settingsManager: this.settings, document: this.document }).open();
148
- }
149
-
150
- /**
151
- * Ask which relationship a new connection is, among the kinds the rules
152
- * allow between the two elements. Resolves to the kind, or null if cancelled.
153
- */
154
- chooseKind(kinds, from, to, { reason = 'connect' } = {}) {
155
- const nesting = reason === 'nesting';
156
- return new Promise((resolve) => {
157
- new FormDialog({
158
- title: nesting ? `${to.name} nested in ${from.name}` : `New relationship: ${from.name} → ${to.name}`,
159
- description: nesting
160
- ? `Add the relationship the nesting stands for (${typeTitle(from.type ?? '?')} → ${typeTitle(to.type ?? '?')}), in the model; the nesting shows it, so no line is drawn.`
161
- : `${typeTitle(from.type ?? '?')} → ${typeTitle(to.type ?? '?')}: ${kinds.length} allowed kind${kinds.length === 1 ? '' : 's'}`,
162
- fields: [{
163
- key: 'kind', label: 'Kind', type: 'select', value: kinds[0],
164
- choices: kinds.map(k => [k, ArchimateNotation.relationshipType(k)?.description ? `${typeTitle(k)}: ${ArchimateNotation.relationshipType(k).description}` : typeTitle(k)]),
165
- }],
166
- submitLabel: 'Add',
167
- ...(nesting && { cancelLabel: 'No relationship' }),
168
- idPrefix: 'archimate-kind-',
169
- document: this.document,
170
- onApply: ({ kind }) => resolve(kind),
171
- onClose: (applied) => { if (!applied) resolve(null); },
172
- }).open();
173
- });
174
- }
175
-
176
- render() {
177
- const model = this.model;
178
- const view = model.currentView;
179
- const doc = this.document;
180
- const canvasId = `archimate-canvas-${this.id}`;
181
-
182
- const canvas = doc.createElementNS(SVG_NS, 'svg');
183
- canvas.id = canvasId;
184
- canvas.setAttribute('viewBox', '0 0 1000 700');
185
- canvas.style.cssText = 'flex: 1; width: 100%; height: 100%; display: block; background: transparent; box-sizing: border-box;';
186
- const elements = doc.createElementNS(SVG_NS, 'g');
187
- elements.id = 'elements';
188
- canvas.appendChild(elements);
189
-
190
- const tabs = new ViewTabStrip({
191
- document: doc,
192
- onSelect: (id) => model.switchTo(id),
193
- menuItems: (id) => this.viewMenu.items(id),
194
- }).render(model.views(), model.currentViewId);
195
-
196
- const { toolbar, side, main, aside } = new EditorShell({ document: doc, sideWidth: '260px', aside: true, asideWidth: this.spec.analysis ? '260px' : '210px' }).mount(this.container);
197
- Object.assign(aside.style, { padding: '6px', fontSize: '12px' });
198
- // With `analysis`, the palette shares the right with the analysis of the selected element.
199
- let paletteHost = aside, analysisHost = null;
200
- if (this.spec.analysis) {
201
- Object.assign(aside.style, { display: 'flex', flexDirection: 'column', overflow: 'hidden' });
202
- paletteHost = aside.appendChild(doc.createElement('div'));
203
- Object.assign(paletteHost.style, { flex: '1 1 55%', overflowY: 'auto', minHeight: '0' });
204
- analysisHost = aside.appendChild(doc.createElement('div'));
205
- analysisHost.setAttribute('data-analysis', '');
206
- Object.assign(analysisHost.style, { flex: '1 1 45%', overflowY: 'auto', minHeight: '0', borderTop: '1px solid #ddd' });
207
- }
208
- side.appendChild(this._treePanel = buildModelTreePanel(doc, {
209
- modelTree: this.modelTree,
210
- treeDivId: `archimate-canvas-tree-${this.id}`,
211
- viewMenu: this.viewMenu,
212
- templateMenu: this.templateMenu,
213
- elementMenu: (element) => this.elementMenu(element),
214
- search: true,
215
- version: this.version,
216
- }));
217
- main.style.flexDirection = 'column';
218
- render(main, [tabs, { style: { flex: '1', display: 'flex', flexDirection: 'column', minHeight: '0' }, children: [canvas] }], doc);
219
-
220
- const customViewpoints = CustomViewpoints.load();
221
- const previous = this.editor;
222
- previous?.dispose();
223
- this.editor = new ArchimateCanvasEditor({
224
- diagramContainerId: canvasId,
225
- view,
226
- model,
227
- nodeOptions: { archimateVersion: this.version, useDistinctShapes: this.distinctShapes, viewpoint: this.viewpoint, viewpointMode: this.viewpointMode, showDerived: this.showDerived },
228
- chooseKind: (kinds, from, to, options) => this.chooseKind(kinds, from, to, options),
229
- // The view's settings: background, title, watermark, how invalid relationships show.
230
- viewSettings: () => this.settings.getEffectiveSettings(this.model.currentView, 'default'),
231
- settingsManager: this.settings,
232
- // The user's own viewpoints, after the standard ones.
233
- customViewpoints,
234
- // Canvas templates: placed on a canvas, saved from its shapes (the browser's library).
235
- templateMenu: this.templateMenu,
236
- // Version and distinct shapes (⚙ Settings) change the palette too.
237
- onNodeOptionsChange: ({ archimateVersion, useDistinctShapes, viewpoint, viewpointMode, showDerived }) => {
238
- this.showDerived = !!showDerived;
239
- this.version = archimateVersion;
240
- this.distinctShapes = !!useDistinctShapes;
241
- this.viewpoint = viewpoint;
242
- this.viewpointMode = viewpointMode;
243
- this.render();
244
- },
245
- });
246
- if (previous) {
247
- this.editor.showGrid = previous.showGrid;
248
- this.editor.gridSize = previous.gridSize;
249
- }
250
-
251
- // A canvas's palette is Archi's canvas palette; another view's, ArchiMate's element types.
252
- const isCanvas = view.constructor.kind === 'canvas';
253
- const Palette = isCanvas ? CanvasPalette : ArchimatePalette;
254
- new Palette({
255
- version: this.version,
256
- viewpoint: this.viewpoint,
257
- customViewpoints,
258
- document: doc,
259
- // A click adds the type (or box) in the middle of the visible canvas.
260
- onPick: (key) => {
261
- const r = canvas.getBoundingClientRect();
262
- const at = this.editor._clientToDiagram(r.left + r.width / 2, r.top + r.height / 2) ?? { x: 400, y: 300 };
263
- if (isCanvas) this.editor.addCanvasBox(key, at);
264
- else this.editor.addElement(key, at);
265
- },
266
- }).render(paletteHost);
267
-
268
- // The tree follows the canvas's selection; a canvas shows its hint, or the selected box's.
269
- const hints = (this.hintPanel ??= new HintPanel({ document: doc }));
270
- const showHint = (templateId) => hints.mount(canvas.parentElement).show(view.constructor.kind === 'canvas' ? (templateId ? view.hintFor(templateId) : view.hint) : null);
271
- const showAnalysis = this._showAnalysis = (elementId) => {
272
- if (!analysisHost) return;
273
- analysisHost.replaceChildren(buildAnalysisPanel(doc, {
274
- model, elementId: elementId && model.getElement(elementId) ? elementId : null,
275
- onSelectItem: (info) => this.modelTree.select(info.id, info.kind),
276
- }));
277
- };
278
- this.editor.onSelectionChange(({ templateId }) => {
279
- const elementId = templateId && this.editor.templates.get(templateId)?.instance?.elementId;
280
- if (elementId && this.modelTree.getSelectedId() !== elementId && this.model.getElement(elementId)) this.modelTree.select(elementId, 'element');
281
- showHint(templateId);
282
- showAnalysis(elementId);
283
- });
284
- showHint(null);
285
- showAnalysis(this.modelTree.getSelectedKind() === 'element' ? this.modelTree.getSelectedId() : null);
286
- // The tree's filters dim what they leave out on the canvas too.
287
- this._unsubscribeFilter?.();
288
- const dim = () => this.editor?.setElementFilter(this.modelTree.matchingElementIds());
289
- this._unsubscribeFilter = this.modelTree.onFilterChange(dim);
290
- if (this.modelTree.filtering) dim();
291
-
292
- this.menuBar?.dispose();
293
- this.menuBar = new MenuBar({
294
- container: toolbar,
295
- editor: this.editor,
296
- items: [this.fileMenu(), this.editMenu(), ...this.editor.menuBarItems(), new ToolbarText(() => this.fileLabel, { marginLeft: 'auto', color: '#555', fontSize: '12px' })],
297
- }).render();
298
- this.editor.render();
299
- }
300
-
301
- /**
302
- * A model element's menu in the tree (elementMenuItems): its properties,
303
- * the views it is on, adding it to this one, a view generated for it.
304
- */
305
- elementMenu(element) {
306
- const model = this.model;
307
- return elementMenuItems(element, {
308
- model, view: model.currentView,
309
- editProperties: (el) => editElementProperties(el, {
310
- history: model.currentView?.history, version: this.version, document: this.document,
311
- onSaved: () => this.render(),
312
- }),
313
- showIn: (view, instanceId) => {
314
- if (view.id !== model.currentViewId) model.switchTo(view.id); // the part redraws for the new view
315
- this.editor?.selectTemplate(instanceId);
316
- },
317
- addToView: (el) => this.editor?.showElement(el.id, this.editor.visibleCentre()),
318
- });
319
- }
320
-
321
- /** Select the first box of a model element on the current view (nothing if it is not shown). */
322
- selectElement(elementId) {
323
- const editor = this.editor;
324
- if (!editor) return;
325
- const box = [...editor.templates.values()].find(t => t.instance?.elementId === elementId);
326
- if (box && editor.selectedTemplateId !== box.id) editor.selectTemplate(box.id);
327
- }
328
-
329
- /** Canvas templates over the current model: placed on a canvas, saved from its shapes (the browser's library). */
330
- get templateMenu() {
331
- if (this._templateMenu?.model !== this.model) {
332
- this._templateMenu = new CanvasTemplateMenu(this.model, { library: (this.templateLibrary ??= new CanvasTemplateLibrary()), document: () => this.document });
333
- }
334
- return this._templateMenu;
335
- }
336
-
337
- dispose() {
338
- this.menuBar?.dispose();
339
- this._unsubscribeSettings?.();
340
- this._unsubscribeFilter?.();
341
- this._unsubscribe();
342
- this._unsubscribeTree?.();
343
- super.dispose();
344
- }
345
- }
1
+ import { render } from '../dom.js';
2
+ import { EditorShell } from '../EditorShell.js';
3
+ import { EditorPart } from './EditorPart.js';
4
+ import { ArchimateModel } from '../../charting/diagrams/ArchimateModel.js';
5
+ import { ArchimateCanvasEditor, ArchimatePalette, typeTitle } from '../../charting/archimate/index.js';
6
+ import { ArchimateNotation } from '../../archimate.js';
7
+ import { MenuBar } from '../../charting/MenuBar.js';
8
+ import { ToolbarText } from '../../charting/toolbar/index.js';
9
+ import { ViewTabStrip } from '../../charting/ViewTabStrip.js';
10
+ import { FormDialog } from '../../charting/FormDialog.js';
11
+ import { CsvImportDialog } from '../../charting/archimate/CsvImportDialog.js';
12
+ import { pickFile } from '../../charting/toolbar/pickFile.js';
13
+ import { SettingsManager } from '../../charting/SettingsManager.js';
14
+ import { SettingsViewPropertiesDialog } from '../../charting/SettingsViewPropertiesDialog.js';
15
+ import { CanvasTemplateLibrary } from '../../charting/canvas/CanvasTemplateLibrary.js';
16
+ import { CanvasTemplateMenu } from '../../charting/canvas/CanvasTemplateMenu.js';
17
+ import { CanvasPalette } from '../../charting/canvas/CanvasPalette.js';
18
+ import { ARCHIMATE_FILE_FORMATS } from '../../charting/archimate/archimateFileFormats.js';
19
+ import { ModelTree } from '../../charting/modelTree/ModelTree.js';
20
+ import { buildModelTreePanel } from '../../charting/modelTree/ModelTreePanel.js';
21
+ import { buildAnalysisPanel } from '../../charting/modelTree/AnalysisPanel.js';
22
+ import { ArchimateSpecializationRegistry } from '../../archimateSpecializations.js';
23
+ import { HintPanel } from '../../charting/HintPanel.js';
24
+ import { elementMenuItems } from '../../charting/archimate/elementMenuItems.js';
25
+ import { editElementProperties } from '../../charting/archimate/elementProperties.js';
26
+ import { CustomViewpoints } from '../../charting/archimate/CustomViewpoints.js';
27
+
28
+ const SVG_NS = 'http://www.w3.org/2000/svg';
29
+
30
+ /**
31
+ * ArchimateCanvasPart — the ArchiMate modeller on the diagram editor's own
32
+ * canvas (phase 4 testbed): the model tree on the left, one tab per view and
33
+ * an ArchimateCanvasEditor over the current view, and a palette of element
34
+ * types grouped by layer on the right. Drop or click a palette type to add an
35
+ * element, or drag one from the tree to show it (with its relationships to
36
+ * what is shown); drag between two elements to add a relationship (its kind
37
+ * picked from those the rules allow); every edit is a step on that view's own
38
+ * undo history. The File menu exports and imports CSV, .archimate and the
39
+ * Exchange Format.
40
+ *
41
+ * Spec (JSON): `elements`, `relationships`, `views`, `name` (as ArchimateModel
42
+ * takes them), `archimateVersion` ('4' or '3.2'), `distinctShapes` (draw each
43
+ * element in its notation shape rather than a box), `viewpoint` (a viewpoint name) and
44
+ * `viewpointMode` ('fade' or 'hide': elements outside it), `analysis` (true adds the
45
+ * analysis of the selected element, under the palette).
46
+ */
47
+ export class ArchimateCanvasPart extends EditorPart {
48
+ static type = 'archimate-canvas';
49
+ static viewNoun = 'view';
50
+
51
+ constructor(spec = {}) {
52
+ super(spec);
53
+ this.version = spec.archimateVersion ?? '4';
54
+ this.distinctShapes = !!spec.distinctShapes;
55
+ this.viewpoint = spec.viewpoint ?? 'All';
56
+ this.viewpointMode = spec.viewpointMode ?? 'fade';
57
+ this.showDerived = !!spec.showDerived;
58
+ this.settings = new SettingsManager();
59
+ // A settings change (Model settings…, a view's properties) redraws the canvas.
60
+ this._unsubscribeSettings = this.settings.onSettingsChange(() => { if (this.mounted) this.editor?.render(); });
61
+ // The user's specializations (or the defaults), as the main editor offers them; one already loaded is kept.
62
+ if (!ArchimateSpecializationRegistry.list().length) ArchimateSpecializationRegistry.loadFrom();
63
+ this._follow(this.model);
64
+ }
65
+
66
+ /**
67
+ * Re-render on every change to `model`'s views (the editor is rebuilt with the canvas on each
68
+ * render), and redraw the tree after every edit (an element renamed, an undo).
69
+ * @private
70
+ */
71
+ _follow(model) {
72
+ this._unsubscribe?.();
73
+ this._unsubscribeTree?.();
74
+ this._unsubscribe = model.onChange(() => { if (this.mounted) this.render(); });
75
+ this._unsubscribeTree = model.onEdit?.(() => this._treePanel?.refresh?.()) ?? null;
76
+ }
77
+
78
+ fileFormats() {
79
+ return ARCHIMATE_FILE_FORMATS;
80
+ }
81
+
82
+ /** A new file is an Archi file (.archimate). */
83
+ saveFormat() {
84
+ return ARCHIMATE_FILE_FORMATS.find(f => f.featureName === 'Archimate');
85
+ }
86
+
87
+ /** The file label in the toolbar (and a ModelsPart's tab) follows the saved state. */
88
+ onFileState() {
89
+ super.onFileState();
90
+ this.menuBar?.refresh?.();
91
+ }
92
+
93
+ /** New view…, then New canvas… and New canvas from template. */
94
+ newItems() {
95
+ return [...super.newItems(), ...this.templateMenu.newCanvasItems()];
96
+ }
97
+
98
+ /** File > Templates: the open canvas saved as a model template or into the library. */
99
+ ownFileItems() {
100
+ const saveAs = this.model.currentView ? this.templateMenu.saveAsTemplateItems(this.model.currentViewId) : [];
101
+ return EditorPart.menuGroup('Templates', saveAs);
102
+ }
103
+
104
+ /** File > Import: an Archi canvas template, and a spreadsheet's rows (CSV with mapping). */
105
+ ownImportItems() {
106
+ const importTemplate = this.templateMenu.importItem();
107
+ return [{ separator: true }, ...(importTemplate ? [{ ...importTemplate, label: 'Archi template…' }] : []), { label: 'CSV with mapping…', action: () => this.importCsvMapped() }];
108
+ }
109
+
110
+ /** Import CSV with mapping…: a spreadsheet's rows into this model (CsvImportDialog), the new elements on the open view. */
111
+ async importCsvMapped() {
112
+ const file = await pickFile('.csv,.txt,.tsv', this.document);
113
+ if (!file) return null;
114
+ return new CsvImportDialog({ model: this.model, view: this.model.currentView, version: this.version, document: this.document, onDone: () => this.render() }).open(await file.text());
115
+ }
116
+
117
+ /** An imported model: follow it, with a fresh tree, and give it a view if it has none. */
118
+ modelReplaced(model) {
119
+ if (!model.views().length) model.createView();
120
+ this._follow(model);
121
+ }
122
+
123
+ /** The model tree over the current model (kept across renders, so its mode and selection stay). */
124
+ get modelTree() {
125
+ if (this.tree?.model !== this.model) {
126
+ this.tree = new ModelTree(this.model, { mode: 'byLayer' });
127
+ // A view chosen in the tree opens it; an element chosen there is selected on the canvas.
128
+ this.tree.onSelectionChange((data) => {
129
+ if (data.kind === 'view' && data.view.id !== this.model.currentViewId) this.model.switchTo(data.view.id);
130
+ if (data.kind === 'element') { this.selectElement(data.id); this._showAnalysis?.(data.id); }
131
+ });
132
+ }
133
+ return this.tree;
134
+ }
135
+
136
+ createModel(spec) {
137
+ return new ArchimateModel({
138
+ name: spec.name ?? '',
139
+ elements: spec.elements ?? [],
140
+ relationships: spec.relationships ?? [],
141
+ views: spec.views?.length ? spec.views : [{ id: 'view-1', name: 'View 1', elementIds: [] }],
142
+ });
143
+ }
144
+
145
+ /** A view's properties: name, documentation and its settings (routing, grid, background, ...). */
146
+ showViewProperties(view) {
147
+ return new SettingsViewPropertiesDialog({ model: this.model, view, settingsManager: this.settings, document: this.document }).open();
148
+ }
149
+
150
+ /**
151
+ * Ask which relationship a new connection is, among the kinds the rules
152
+ * allow between the two elements. Resolves to the kind, or null if cancelled.
153
+ */
154
+ chooseKind(kinds, from, to, { reason = 'connect' } = {}) {
155
+ const nesting = reason === 'nesting';
156
+ return new Promise((resolve) => {
157
+ new FormDialog({
158
+ title: nesting ? `${to.name} nested in ${from.name}` : `New relationship: ${from.name} → ${to.name}`,
159
+ description: nesting
160
+ ? `Add the relationship the nesting stands for (${typeTitle(from.type ?? '?')} → ${typeTitle(to.type ?? '?')}), in the model; the nesting shows it, so no line is drawn.`
161
+ : `${typeTitle(from.type ?? '?')} → ${typeTitle(to.type ?? '?')}: ${kinds.length} allowed kind${kinds.length === 1 ? '' : 's'}`,
162
+ fields: [{
163
+ key: 'kind', label: 'Kind', type: 'select', value: kinds[0],
164
+ choices: kinds.map(k => [k, ArchimateNotation.relationshipType(k)?.description ? `${typeTitle(k)}: ${ArchimateNotation.relationshipType(k).description}` : typeTitle(k)]),
165
+ }],
166
+ submitLabel: 'Add',
167
+ ...(nesting && { cancelLabel: 'No relationship' }),
168
+ idPrefix: 'archimate-kind-',
169
+ document: this.document,
170
+ onApply: ({ kind }) => resolve(kind),
171
+ onClose: (applied) => { if (!applied) resolve(null); },
172
+ }).open();
173
+ });
174
+ }
175
+
176
+ render() {
177
+ const model = this.model;
178
+ const view = model.currentView;
179
+ const doc = this.document;
180
+ const canvasId = `archimate-canvas-${this.id}`;
181
+
182
+ const canvas = doc.createElementNS(SVG_NS, 'svg');
183
+ canvas.id = canvasId;
184
+ canvas.setAttribute('viewBox', '0 0 1000 700');
185
+ canvas.style.cssText = 'flex: 1; width: 100%; height: 100%; display: block; background: transparent; box-sizing: border-box;';
186
+ const elements = doc.createElementNS(SVG_NS, 'g');
187
+ elements.id = 'elements';
188
+ canvas.appendChild(elements);
189
+
190
+ const tabs = new ViewTabStrip({
191
+ document: doc,
192
+ onSelect: (id) => model.switchTo(id),
193
+ menuItems: (id) => this.viewMenu.items(id),
194
+ }).render(model.views(), model.currentViewId);
195
+
196
+ const { toolbar, side, main, aside } = new EditorShell({ document: doc, sideWidth: '260px', aside: true, asideWidth: this.spec.analysis ? '260px' : '210px' }).mount(this.container);
197
+ Object.assign(aside.style, { padding: '6px', fontSize: '12px' });
198
+ // With `analysis`, the palette shares the right with the analysis of the selected element.
199
+ let paletteHost = aside, analysisHost = null;
200
+ if (this.spec.analysis) {
201
+ Object.assign(aside.style, { display: 'flex', flexDirection: 'column', overflow: 'hidden' });
202
+ paletteHost = aside.appendChild(doc.createElement('div'));
203
+ Object.assign(paletteHost.style, { flex: '1 1 55%', overflowY: 'auto', minHeight: '0' });
204
+ analysisHost = aside.appendChild(doc.createElement('div'));
205
+ analysisHost.setAttribute('data-analysis', '');
206
+ Object.assign(analysisHost.style, { flex: '1 1 45%', overflowY: 'auto', minHeight: '0', borderTop: '1px solid #ddd' });
207
+ }
208
+ side.appendChild(this._treePanel = buildModelTreePanel(doc, {
209
+ modelTree: this.modelTree,
210
+ treeDivId: `archimate-canvas-tree-${this.id}`,
211
+ viewMenu: this.viewMenu,
212
+ templateMenu: this.templateMenu,
213
+ elementMenu: (element) => this.elementMenu(element),
214
+ search: true,
215
+ version: this.version,
216
+ }));
217
+ main.style.flexDirection = 'column';
218
+ render(main, [tabs, { style: { flex: '1', display: 'flex', flexDirection: 'column', minHeight: '0' }, children: [canvas] }], doc);
219
+
220
+ const customViewpoints = CustomViewpoints.load();
221
+ const previous = this.editor;
222
+ previous?.dispose();
223
+ this.editor = new ArchimateCanvasEditor({
224
+ diagramContainerId: canvasId,
225
+ view,
226
+ model,
227
+ nodeOptions: { archimateVersion: this.version, useDistinctShapes: this.distinctShapes, viewpoint: this.viewpoint, viewpointMode: this.viewpointMode, showDerived: this.showDerived },
228
+ chooseKind: (kinds, from, to, options) => this.chooseKind(kinds, from, to, options),
229
+ // The view's settings: background, title, watermark, how invalid relationships show.
230
+ viewSettings: () => this.settings.getEffectiveSettings(this.model.currentView, 'default'),
231
+ settingsManager: this.settings,
232
+ // The user's own viewpoints, after the standard ones.
233
+ customViewpoints,
234
+ // Canvas templates: placed on a canvas, saved from its shapes (the browser's library).
235
+ templateMenu: this.templateMenu,
236
+ // Version and distinct shapes (⚙ Settings) change the palette too.
237
+ onNodeOptionsChange: ({ archimateVersion, useDistinctShapes, viewpoint, viewpointMode, showDerived }) => {
238
+ this.showDerived = !!showDerived;
239
+ this.version = archimateVersion;
240
+ this.distinctShapes = !!useDistinctShapes;
241
+ this.viewpoint = viewpoint;
242
+ this.viewpointMode = viewpointMode;
243
+ this.render();
244
+ },
245
+ });
246
+ if (previous) {
247
+ this.editor.showGrid = previous.showGrid;
248
+ this.editor.gridSize = previous.gridSize;
249
+ }
250
+
251
+ // A canvas's palette is Archi's canvas palette; another view's, ArchiMate's element types.
252
+ const isCanvas = view.constructor.kind === 'canvas';
253
+ const Palette = isCanvas ? CanvasPalette : ArchimatePalette;
254
+ new Palette({
255
+ version: this.version,
256
+ viewpoint: this.viewpoint,
257
+ customViewpoints,
258
+ document: doc,
259
+ // A click adds the type (or box) in the middle of the visible canvas.
260
+ onPick: (key) => {
261
+ const r = canvas.getBoundingClientRect();
262
+ const at = this.editor._clientToDiagram(r.left + r.width / 2, r.top + r.height / 2) ?? { x: 400, y: 300 };
263
+ if (isCanvas) this.editor.addCanvasBox(key, at);
264
+ else this.editor.addElement(key, at);
265
+ },
266
+ }).render(paletteHost);
267
+
268
+ // The tree follows the canvas's selection; a canvas shows its hint, or the selected box's.
269
+ const hints = (this.hintPanel ??= new HintPanel({ document: doc }));
270
+ const showHint = (templateId) => hints.mount(canvas.parentElement).show(view.constructor.kind === 'canvas' ? (templateId ? view.hintFor(templateId) : view.hint) : null);
271
+ const showAnalysis = this._showAnalysis = (elementId) => {
272
+ if (!analysisHost) return;
273
+ analysisHost.replaceChildren(buildAnalysisPanel(doc, {
274
+ model, elementId: elementId && model.getElement(elementId) ? elementId : null,
275
+ onSelectItem: (info) => this.modelTree.select(info.id, info.kind),
276
+ }));
277
+ };
278
+ this.editor.onSelectionChange(({ templateId }) => {
279
+ const elementId = templateId && this.editor.templates.get(templateId)?.instance?.elementId;
280
+ if (elementId && this.modelTree.getSelectedId() !== elementId && this.model.getElement(elementId)) this.modelTree.select(elementId, 'element');
281
+ showHint(templateId);
282
+ showAnalysis(elementId);
283
+ });
284
+ showHint(null);
285
+ showAnalysis(this.modelTree.getSelectedKind() === 'element' ? this.modelTree.getSelectedId() : null);
286
+ // The tree's filters dim what they leave out on the canvas too.
287
+ this._unsubscribeFilter?.();
288
+ const dim = () => this.editor?.setElementFilter(this.modelTree.matchingElementIds());
289
+ this._unsubscribeFilter = this.modelTree.onFilterChange(dim);
290
+ if (this.modelTree.filtering) dim();
291
+
292
+ this.menuBar?.dispose();
293
+ this.menuBar = new MenuBar({
294
+ container: toolbar,
295
+ editor: this.editor,
296
+ items: [this.fileMenu(), this.editMenu(), ...this.hostMenus(), ...this.editor.menuBarItems(), new ToolbarText(() => this.fileLabel, { marginLeft: 'auto', color: '#555', fontSize: '12px' })],
297
+ }).render();
298
+ this.editor.render();
299
+ }
300
+
301
+ /**
302
+ * A model element's menu in the tree (elementMenuItems): its properties,
303
+ * the views it is on, adding it to this one, a view generated for it.
304
+ */
305
+ elementMenu(element) {
306
+ const model = this.model;
307
+ return elementMenuItems(element, {
308
+ model, view: model.currentView,
309
+ editProperties: (el) => editElementProperties(el, {
310
+ history: model.currentView?.history, version: this.version, document: this.document,
311
+ onSaved: () => this.render(),
312
+ }),
313
+ showIn: (view, instanceId) => {
314
+ if (view.id !== model.currentViewId) model.switchTo(view.id); // the part redraws for the new view
315
+ this.editor?.selectTemplate(instanceId);
316
+ },
317
+ addToView: (el) => this.editor?.showElement(el.id, this.editor.visibleCentre()),
318
+ });
319
+ }
320
+
321
+ /** Select the first box of a model element on the current view (nothing if it is not shown). */
322
+ selectElement(elementId) {
323
+ const editor = this.editor;
324
+ if (!editor) return;
325
+ const box = [...editor.templates.values()].find(t => t.instance?.elementId === elementId);
326
+ if (box && editor.selectedTemplateId !== box.id) editor.selectTemplate(box.id);
327
+ }
328
+
329
+ /** Canvas templates over the current model: placed on a canvas, saved from its shapes (the browser's library). */
330
+ get templateMenu() {
331
+ if (this._templateMenu?.model !== this.model) {
332
+ this._templateMenu = new CanvasTemplateMenu(this.model, { library: (this.templateLibrary ??= new CanvasTemplateLibrary()), document: () => this.document });
333
+ }
334
+ return this._templateMenu;
335
+ }
336
+
337
+ dispose() {
338
+ this.menuBar?.dispose();
339
+ this._unsubscribeSettings?.();
340
+ this._unsubscribeFilter?.();
341
+ this._unsubscribe();
342
+ this._unsubscribeTree?.();
343
+ super.dispose();
344
+ }
345
+ }