@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.
- package/README.md +4 -0
- package/REFERENCE.md +180 -2
- package/dist/renderengine-workspace.umd.js +8704 -5310
- package/dist/renderengine.umd.js +5675 -3398
- package/examples/archimate.html +68 -66
- package/examples/data-model.html +223 -13
- package/package.json +1 -1
- package/src/bian.js +182 -0
- package/src/charting/DiagramEditor.js +5 -26
- package/src/charting/DiagramTypeRegistry.js +8 -2
- package/src/charting/ModelCheckDialog.js +63 -0
- package/src/charting/ModelChecker.js +26 -0
- package/src/charting/ViewTabStrip.js +94 -81
- package/src/charting/datamodel/BianLandscapeReader.js +185 -0
- package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
- package/src/charting/datamodel/DataModel.js +356 -10
- package/src/charting/datamodel/DataModelChecker.js +92 -0
- package/src/charting/datamodel/DataModelCompared.js +127 -0
- package/src/charting/datamodel/DataModelFileFormat.js +33 -0
- package/src/charting/datamodel/DataModelPerspective.js +45 -0
- package/src/charting/datamodel/DataType.js +108 -0
- package/src/charting/datamodel/EnumerationType.js +51 -0
- package/src/charting/datamodel/ServiceDomain.js +177 -0
- package/src/charting/datamodel/ServiceLandscape.js +104 -0
- package/src/charting/datamodel/StructuredType.js +63 -0
- package/src/charting/datamodel/index.js +10 -0
- package/src/charting/diagramModel/MergeModelCommand.js +43 -37
- package/src/charting/diagramModel/OpenViewList.js +75 -0
- package/src/charting/diagramModel/ViewSetChecker.js +22 -34
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +12 -4
- package/src/charting/diagrams/EdgeGeometry.js +578 -553
- package/src/charting/diagrams/GraphDiagram.js +11 -1
- package/src/charting/diagrams/NodeGeometry.js +169 -158
- package/src/charting/glyphs/PersonGlyph.js +24 -23
- package/src/index.js +2666 -2664
- package/src/workspace/EditorShell.js +166 -67
- package/src/workspace/PanelLayout.js +56 -0
- package/src/workspace/layout/TabsNode.js +205 -191
- package/src/workspace/parts/ArchimateCanvasPart.js +5 -7
- package/src/workspace/parts/DataModelPart.js +339 -29
- package/src/workspace/parts/DiagramEditorPart.js +2 -8
- package/src/workspace/parts/EditorPart.js +461 -407
- package/src/workspace/parts/ModelFile.js +216 -187
- package/src/workspace/parts/ModelsPart.js +483 -192
- package/src/workspace/storage/BackupFolder.js +145 -0
- package/src/workspace/storage/KeyValueStore.js +92 -0
- package/src/workspace/storage/ModelLibrary.js +65 -0
- package/src/workspace/workArea.js +417 -413
- package/src/yaml/YamlReader.js +372 -0
|
@@ -1,407 +1,461 @@
|
|
|
1
|
-
import { Part } from '../Part.js';
|
|
2
|
-
import { ViewMenu } from '../../charting/ViewMenu.js';
|
|
3
|
-
import { ToolbarMenu } from '../../charting/toolbar/index.js';
|
|
4
|
-
import { modelFileMenuItems } from '../../charting/ModelFileFormat.js';
|
|
5
|
-
import { ModelFile } from './ModelFile.js';
|
|
6
|
-
import { reportHtml, reportFileName } from '../../charting/report/reportHtml.js';
|
|
7
|
-
import { downloadBlob } from '../../exportImage.js';
|
|
8
|
-
import { FindReplaceDialog } from '../../charting/FindReplaceDialog.js';
|
|
9
|
-
import { FormDialog } from '../../charting/FormDialog.js';
|
|
10
|
-
import { NAME_TRANSFORMS } from '../../charting/diagramModel/nameTransforms.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return this.
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{
|
|
182
|
-
|
|
183
|
-
],
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
*
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
this.
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
]
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}),
|
|
282
|
-
...this.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
this.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
*
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
|
|
391
|
-
this.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
this.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
1
|
+
import { Part } from '../Part.js';
|
|
2
|
+
import { ViewMenu } from '../../charting/ViewMenu.js';
|
|
3
|
+
import { ToolbarMenu } from '../../charting/toolbar/index.js';
|
|
4
|
+
import { modelFileMenuItems } from '../../charting/ModelFileFormat.js';
|
|
5
|
+
import { ModelFile } from './ModelFile.js';
|
|
6
|
+
import { reportHtml, reportFileName } from '../../charting/report/reportHtml.js';
|
|
7
|
+
import { downloadBlob } from '../../exportImage.js';
|
|
8
|
+
import { FindReplaceDialog } from '../../charting/FindReplaceDialog.js';
|
|
9
|
+
import { FormDialog } from '../../charting/FormDialog.js';
|
|
10
|
+
import { NAME_TRANSFORMS } from '../../charting/diagramModel/nameTransforms.js';
|
|
11
|
+
import { OpenViewList } from '../../charting/diagramModel/OpenViewList.js';
|
|
12
|
+
import { ViewTabStrip } from '../../charting/ViewTabStrip.js';
|
|
13
|
+
import { PopupMenu } from '../../charting/PopupMenu.js';
|
|
14
|
+
import { PanelLayout } from '../PanelLayout.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* EditorPart — base for workbench parts that edit a model of several views
|
|
18
|
+
* (a ViewSetModel): the template editor's DiagramEditorPart and
|
|
19
|
+
* ArchimateEditorPart.
|
|
20
|
+
*
|
|
21
|
+
* Owns what they share: the model (made by the subclass from the part's JSON
|
|
22
|
+
* spec), the view menu (new, duplicate, rename, delete) over it, a File menu
|
|
23
|
+
* that starts with those items and offers the part's file formats (export,
|
|
24
|
+
* and import, which replaces the model), and disposing the editor with the
|
|
25
|
+
* part. Subclasses build their editor in render() and supply
|
|
26
|
+
* `createModel(spec)`, and `fileFormats()` when they read and write files.
|
|
27
|
+
*
|
|
28
|
+
* With file formats the part also opens and saves a file (ModelFile): Open…,
|
|
29
|
+
* Save and Save As… (Ctrl+O, Ctrl+S, Ctrl+Shift+S), writing back to the file
|
|
30
|
+
* it opened or last saved where the browser allows; it knows when the model
|
|
31
|
+
* has changed since (`model.isDirty()`), shows it (`fileLabel`, the page
|
|
32
|
+
* title) and asks before the page is left with changes unsaved.
|
|
33
|
+
*
|
|
34
|
+
* Hosted by a ModelsPart (`ctx.models`: several models open at once, a tab
|
|
35
|
+
* each), its File menu also offers New model, Open… in a new tab and Close
|
|
36
|
+
* model, an import opens in a new tab rather than replacing this model, a
|
|
37
|
+
* Tools menu compares and merges models (ModelsPart#toolItems), and its tab
|
|
38
|
+
* follows its file's name and saved state.
|
|
39
|
+
*
|
|
40
|
+
* Its tabs are the views open (OpenViewList), as a browser's are: a model
|
|
41
|
+
* opens (or is imported) with its first view only, a view chosen in the tree
|
|
42
|
+
* opens a tab, × closes the tab but keeps the view, and + makes a new one
|
|
43
|
+
* (viewTabs). Its side panels can be resized and folded (`layout`, a
|
|
44
|
+
* PanelLayout the editor shell reads), kept as they were across renders.
|
|
45
|
+
*/
|
|
46
|
+
export class EditorPart extends Part {
|
|
47
|
+
/** What a view is called in this part's menus ('view', 'diagram'). */
|
|
48
|
+
static viewNoun = 'view';
|
|
49
|
+
|
|
50
|
+
constructor(spec = {}) {
|
|
51
|
+
super(spec);
|
|
52
|
+
if (new.target === EditorPart) throw new TypeError('EditorPart is abstract — subclass it');
|
|
53
|
+
this._model = this.createModel(spec);
|
|
54
|
+
/** The editor this part hosts; set by render(). */
|
|
55
|
+
this.editor = null;
|
|
56
|
+
/** The file the model is open from, and saved to (ModelFile). */
|
|
57
|
+
this.file = new ModelFile(this);
|
|
58
|
+
/** The views with a tab, in order (spec `openViews`: their ids). */
|
|
59
|
+
this.openViews = new OpenViewList(this._model, spec.openViews ?? null);
|
|
60
|
+
/** The side panels' widths, and which are folded (spec `layout`), for the editor shell. */
|
|
61
|
+
this.layout = new PanelLayout(spec.layout ?? {});
|
|
62
|
+
/** Its id among the models kept in this browser (ModelsPart with `keep`), or null. */
|
|
63
|
+
this.keepId = spec.keepId ?? null;
|
|
64
|
+
this._followEdits(this._model);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The model as a kept entry (ModelLibrary): in its file's format, else the
|
|
69
|
+
* part's save format, with its name and file name; null without a format.
|
|
70
|
+
*/
|
|
71
|
+
keepEntry() {
|
|
72
|
+
return this.file.keepEntry();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The tab strip: the open views (OpenViewList), each closable while
|
|
77
|
+
* another is open (the view stays in the model), a right-click menu per
|
|
78
|
+
* tab (the view menu), and + for something new (newItems: the one there
|
|
79
|
+
* is, else a menu of them).
|
|
80
|
+
* @param {{orientation?: 'horizontal'|'vertical'}} [options]
|
|
81
|
+
*/
|
|
82
|
+
viewTabs({ orientation = 'horizontal' } = {}) {
|
|
83
|
+
const open = this.openViews;
|
|
84
|
+
return new ViewTabStrip({
|
|
85
|
+
orientation,
|
|
86
|
+
document: this.document,
|
|
87
|
+
onSelect: (id) => open.open(id),
|
|
88
|
+
onClose: (id) => { if (open.close(id) && this.mounted) this.render(); },
|
|
89
|
+
canClose: (id) => open.canClose(id),
|
|
90
|
+
onAdd: (e) => {
|
|
91
|
+
const items = this.newItems().filter(i => !i.separator);
|
|
92
|
+
if (items.length === 1) items[0].action();
|
|
93
|
+
else PopupMenu.show(e?.clientX ?? 0, e?.clientY ?? 0, items, this.document);
|
|
94
|
+
},
|
|
95
|
+
menuItems: (id) => this.viewMenu.items(id),
|
|
96
|
+
}).render(open.sync(), this.model.currentViewId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Install the file keys and the leave-page guard once mounted. */
|
|
100
|
+
mount(host, ctx) {
|
|
101
|
+
const first = !this.mounted;
|
|
102
|
+
super.mount(host, ctx);
|
|
103
|
+
if (!first || !this.fileFormats().length) return;
|
|
104
|
+
this.file.listen();
|
|
105
|
+
this.file.showState();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What the part's title shows: the file's name, and • when there are unsaved changes. */
|
|
109
|
+
get fileLabel() {
|
|
110
|
+
return this.file.label;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Show the saved state on every edit of `model`, from now on as saved and
|
|
115
|
+
* with nothing to undo (making it, or opening it, is no edit). @private
|
|
116
|
+
*/
|
|
117
|
+
_followEdits(model) {
|
|
118
|
+
this._unsubscribeEdits?.();
|
|
119
|
+
model.history?.clear?.();
|
|
120
|
+
model.markSaved?.();
|
|
121
|
+
this._unsubscribeEdits = model.onEdit?.(() => {
|
|
122
|
+
this.file.showState();
|
|
123
|
+
this.modelsHost?.modelEdited?.(this); // kept in this browser, a moment after
|
|
124
|
+
}) ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The model this part edits, built from its spec. @abstract */
|
|
128
|
+
createModel(spec) {
|
|
129
|
+
throw new Error(`${this.constructor.name} must implement createModel()`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The model being edited (a subclass whose editor can replace it reads it from there). */
|
|
133
|
+
get model() {
|
|
134
|
+
return this._model;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The page this part is mounted in. */
|
|
138
|
+
get document() {
|
|
139
|
+
return this.container?.ownerDocument ?? globalThis.document;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* View menu items over the current model. A subclass that can show a
|
|
144
|
+
* view's properties (`showViewProperties(view)`, the name included) gets
|
|
145
|
+
* "Properties…" in place of "Rename…".
|
|
146
|
+
*/
|
|
147
|
+
get viewMenu() {
|
|
148
|
+
if (this._viewMenu?.model !== this.model) {
|
|
149
|
+
this._viewMenu = new ViewMenu(this.model, {
|
|
150
|
+
document: () => this.document,
|
|
151
|
+
noun: this.constructor.viewNoun,
|
|
152
|
+
editProperties: typeof this.showViewProperties === 'function' ? (view) => this.showViewProperties(view) : null,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return this._viewMenu;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** File menu (fileMenuItems). */
|
|
159
|
+
fileMenu() {
|
|
160
|
+
return new ToolbarMenu('File', () => this.fileMenuItems());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The menus beside File and Edit that work across models: Tools, with a ModelsPart. None without. */
|
|
164
|
+
hostMenus() {
|
|
165
|
+
return this.modelsHost ? [new ToolbarMenu('Tools', () => this.modelsHost.toolItems(this))] : [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Edit menu: undo and redo (the editor's), then Find and replace… over the whole model. */
|
|
169
|
+
editMenu() {
|
|
170
|
+
return new ToolbarMenu('Edit', () => this.editMenuItems());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
editMenuItems() {
|
|
174
|
+
const history = this.editor?.history;
|
|
175
|
+
return [
|
|
176
|
+
{ label: 'Undo', disabled: !history?.canUndo || undefined, action: () => this.editor.undo() },
|
|
177
|
+
{ label: 'Redo', disabled: !history?.canRedo || undefined, action: () => this.editor.redo() },
|
|
178
|
+
...(typeof this.model.search === 'function' ? [
|
|
179
|
+
{ separator: true },
|
|
180
|
+
{ label: 'Find and replace…', action: () => this.findAndReplace() },
|
|
181
|
+
{ label: 'Transform names…', action: () => this.askTransformNames() },
|
|
182
|
+
...(this.model.search().propertyNames().length ? [{ label: 'Rename property…', action: () => this.askRenameProperty() }] : []),
|
|
183
|
+
] : []),
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Run a model-wide edit (null: nothing to do; a rename, a merge) as one undo step, then redraw. */
|
|
188
|
+
runModelEdit(command) {
|
|
189
|
+
if (!command) return null;
|
|
190
|
+
(this.editor?.history ?? this.model.history).execute(command);
|
|
191
|
+
if (this.mounted) this.render();
|
|
192
|
+
return command;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Transform names…: Title Case, Sentence case, split camelCase and the
|
|
197
|
+
* rest (nameTransforms.js) on every name, or on those containing some
|
|
198
|
+
* text. One undo step.
|
|
199
|
+
*/
|
|
200
|
+
askTransformNames() {
|
|
201
|
+
return new FormDialog({
|
|
202
|
+
title: 'Transform names',
|
|
203
|
+
fields: [
|
|
204
|
+
{ key: 'transform', label: 'Make them', type: 'select', value: 'title', choices: NAME_TRANSFORMS.map(([k, label]) => [k, label]) },
|
|
205
|
+
{ key: 'text', label: 'Only names containing', type: 'text', value: '', placeholder: 'every name' },
|
|
206
|
+
],
|
|
207
|
+
submitLabel: 'Transform',
|
|
208
|
+
idPrefix: 'transform-names-',
|
|
209
|
+
document: this.document,
|
|
210
|
+
validate: ({ transform, text }) => (this.model.search().transformCommand(transform, text) ? null : 'No name would change'),
|
|
211
|
+
onApply: ({ transform, text }) => this.transformNames(transform, text),
|
|
212
|
+
}).open();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Transform names (ModelSearch#transformCommand), one undo step. */
|
|
216
|
+
transformNames(transform, text = '') {
|
|
217
|
+
return this.runModelEdit(this.model.search().transformCommand(transform, text));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Rename property…: a property's name, everywhere in the model. One undo step. */
|
|
221
|
+
askRenameProperty() {
|
|
222
|
+
const names = this.model.search().propertyNames();
|
|
223
|
+
return new FormDialog({
|
|
224
|
+
title: 'Rename property',
|
|
225
|
+
fields: [
|
|
226
|
+
{ key: 'from', label: 'Property', type: 'select', value: names[0], choices: names.map(n => [n, n]) },
|
|
227
|
+
{ key: 'to', label: 'New name', type: 'text', value: '' },
|
|
228
|
+
],
|
|
229
|
+
submitLabel: 'Rename',
|
|
230
|
+
idPrefix: 'rename-property-',
|
|
231
|
+
document: this.document,
|
|
232
|
+
validate: ({ from, to }) => (to?.trim() ? (to.trim() === from ? 'That is its name' : null) : 'Give its new name'),
|
|
233
|
+
onApply: ({ from, to }) => this.renameProperty(from, to.trim()),
|
|
234
|
+
}).open();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Rename a property everywhere (ModelSearch#renamePropertyCommand), one undo step. */
|
|
238
|
+
renameProperty(from, to) {
|
|
239
|
+
return this.runModelEdit(this.model.search().renamePropertyCommand(from, to));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Find and replace over the model (FindReplaceDialog on `model.search()`):
|
|
244
|
+
* a replace is one undo step on the model's history, and choosing a
|
|
245
|
+
* match shows it (revealMatch). The last search is kept between opens.
|
|
246
|
+
*/
|
|
247
|
+
findAndReplace() {
|
|
248
|
+
this._findReplace ??= new FindReplaceDialog({
|
|
249
|
+
model: () => this.model,
|
|
250
|
+
history: () => this.editor?.history ?? this.model.currentView.history,
|
|
251
|
+
onReveal: (match) => this.revealMatch(match),
|
|
252
|
+
onReplaced: () => { if (this.mounted) this.render(); },
|
|
253
|
+
document: () => this.document,
|
|
254
|
+
});
|
|
255
|
+
return this._findReplace.open();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Show a match: its view, and on it the item (the editor's revealFinding). */
|
|
259
|
+
revealMatch(match) {
|
|
260
|
+
if (match.viewId && match.viewId !== this.model.currentViewId) this.model.switchTo(match.viewId); // the part redraws for it
|
|
261
|
+
if (match.kind === 'model' || match.kind === 'view') return;
|
|
262
|
+
this.editor?.revealFinding?.({ kind: match.kind, ids: [match.id] });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** What File offers to make: a new view here; a subclass adds its own kinds (a canvas, ...). */
|
|
266
|
+
newItems() {
|
|
267
|
+
return [this.viewMenu.newViewItem()];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The ModelsPart this part is a tab of (several models open at once), or null. */
|
|
271
|
+
get modelsHost() {
|
|
272
|
+
return this.ctx?.models ?? null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** With a ModelsPart: New model, Open… in a new tab (when it opens files) and Close model. */
|
|
276
|
+
modelItems() {
|
|
277
|
+
const host = this.modelsHost;
|
|
278
|
+
if (!host) return [];
|
|
279
|
+
return [
|
|
280
|
+
{ label: 'New model', action: () => host.newModel() },
|
|
281
|
+
...(this.fileFormats().length ? [{ label: 'Open… in a new tab', action: () => host.openInNewTab(this) }] : []),
|
|
282
|
+
...(this.fileFormats().length ? [{ label: 'Duplicate model', action: () => host.duplicate(this) }] : []),
|
|
283
|
+
{ label: 'Close model', action: () => host.closeModel(this.id) },
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The file or its saved state changed: a ModelsPart's tab follows it. */
|
|
288
|
+
onFileState() {
|
|
289
|
+
this.modelsHost?.fileStateChanged?.(this);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The File menu, grouped: **New** ▸ (a view, a subclass's kinds, a model),
|
|
294
|
+
* Open…, Save, Save As… (and, with a ModelsPart, Open… in a new tab, Close
|
|
295
|
+
* model), **Import** ▸ and **Export** ▸ (each file format, a subclass's own,
|
|
296
|
+
* the HTML report and the pictures), a subclass's own groups, and **This
|
|
297
|
+
* view** ▸ (its properties, duplicate, delete). A group of one is that item.
|
|
298
|
+
*/
|
|
299
|
+
fileMenuItems() {
|
|
300
|
+
const models = Object.fromEntries(this.modelItems().map(i => [i.label, i]));
|
|
301
|
+
const [open, ...saving] = this.fileFormats().length ? this.file.menuItems() : [];
|
|
302
|
+
const some = (...items) => items.filter(Boolean);
|
|
303
|
+
return EditorPart.tidyMenu([
|
|
304
|
+
...EditorPart.menuGroup('New', some(...this.newItems(), models['New model'], ...(this.modelsHost?.baseItems?.() ?? []))),
|
|
305
|
+
{ separator: true },
|
|
306
|
+
...some(open, models['Open… in a new tab'], ...saving, models['Duplicate model'], models['Close model']),
|
|
307
|
+
...EditorPart.menuGroup('This browser', this.modelsHost?.keepItems?.(this) ?? []),
|
|
308
|
+
{ separator: true },
|
|
309
|
+
...EditorPart.menuGroup('Import', this.importItems(), { prefix: true }),
|
|
310
|
+
...EditorPart.menuGroup('Export', this.exportItems(), { prefix: true }),
|
|
311
|
+
...this.ownFileItems(),
|
|
312
|
+
{ separator: true },
|
|
313
|
+
...EditorPart.menuGroup(`This ${this.constructor.viewNoun}`, this.viewMenu.items(this.model.currentViewId)),
|
|
314
|
+
]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* File > Import's items: each format that imports, then the subclass's own
|
|
319
|
+
* (ownImportItems). An imported model opens in a new tab with a ModelsPart
|
|
320
|
+
* (onImported), else replaces this one.
|
|
321
|
+
*/
|
|
322
|
+
importItems() {
|
|
323
|
+
return EditorPart.tidyMenu([
|
|
324
|
+
...modelFileMenuItems(this.fileFormats(), {
|
|
325
|
+
model: () => this.model, only: 'import',
|
|
326
|
+
onImport: (model) => this.onImported(model),
|
|
327
|
+
document: () => this.document,
|
|
328
|
+
}),
|
|
329
|
+
...this.ownImportItems(),
|
|
330
|
+
]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** A model imported from a file: in a new tab with a ModelsPart, else in place of this one. Returns the part showing it. */
|
|
334
|
+
onImported(model) {
|
|
335
|
+
if (this.modelsHost) return this.modelsHost.openModel(model);
|
|
336
|
+
this.replaceModel(model);
|
|
337
|
+
this.file.forget();
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** File > Export's items: each format, the subclass's own (ownExportItems), then the HTML report and the pictures. */
|
|
342
|
+
exportItems() {
|
|
343
|
+
return EditorPart.tidyMenu([
|
|
344
|
+
...modelFileMenuItems(this.fileFormats(), { model: () => this.model, only: 'export', document: () => this.document }),
|
|
345
|
+
...this.ownExportItems(),
|
|
346
|
+
{ separator: true },
|
|
347
|
+
// In Export, "Export HTML report" is "HTML report", "Export image (PNG)" "Image (PNG)".
|
|
348
|
+
...[...this.reportItems(), ...(this.editor?.imageMenuItems?.() ?? [])]
|
|
349
|
+
.map(i => ({ ...i, label: i.label.replace(/^Export (.)/, (_, c) => c.toUpperCase()) })),
|
|
350
|
+
]);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** A subclass's own File > Import items. None here. */
|
|
354
|
+
ownImportItems() {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** A subclass's own File > Export items. None here. */
|
|
359
|
+
ownExportItems() {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Items as a submenu named `label`; none is nothing, and one is that item
|
|
365
|
+
* itself (with `prefix`, named as the group says it: "Export HTML report").
|
|
366
|
+
*/
|
|
367
|
+
static menuGroup(label, items, { prefix = false } = {}) {
|
|
368
|
+
const real = EditorPart.tidyMenu(items);
|
|
369
|
+
if (!real.length) return [];
|
|
370
|
+
if (real.length > 1) return [{ label, items: real }];
|
|
371
|
+
const [only] = real;
|
|
372
|
+
if (!prefix) return [only];
|
|
373
|
+
// "Image (PNG)" reads "Export image (PNG)"; an acronym (HTML, CSV) or ".archimate" stays as it is.
|
|
374
|
+
const word = /^[A-Z][a-z]/.test(only.label) ? only.label.charAt(0).toLowerCase() + only.label.slice(1) : only.label;
|
|
375
|
+
return [{ ...only, label: `${label} ${word}` }];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Separators only between items: none first, last or twice in a row. */
|
|
379
|
+
static tidyMenu(items) {
|
|
380
|
+
const out = [];
|
|
381
|
+
for (const item of items) {
|
|
382
|
+
if (item.separator && (!out.length || out.at(-1).separator)) continue;
|
|
383
|
+
out.push(item);
|
|
384
|
+
}
|
|
385
|
+
while (out.at(-1)?.separator) out.pop();
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Export HTML report, when the model has a report (ViewSetModel#report). */
|
|
390
|
+
reportItems() {
|
|
391
|
+
return typeof this.model.report === 'function' ? [{ label: 'Export HTML report', action: () => this.exportReport() }] : [];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The model's report as one HTML file (reportHtml), each view's picture
|
|
396
|
+
* in it: downloaded as `<model>-report.html`. Returns the HTML.
|
|
397
|
+
*/
|
|
398
|
+
exportReport() {
|
|
399
|
+
const report = this.model.report({ pictures: this.viewPictures() });
|
|
400
|
+
const html = reportHtml(report.data());
|
|
401
|
+
downloadBlob(new (this.document.defaultView ?? globalThis).Blob([html], { type: 'text/html' }), reportFileName(report.title));
|
|
402
|
+
return html;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Every view's picture (DiagramEditor#viewPicture), by view id: each view
|
|
407
|
+
* is shown in turn, then the one that was. None when the editor draws no pictures.
|
|
408
|
+
*/
|
|
409
|
+
viewPictures() {
|
|
410
|
+
const model = this.model;
|
|
411
|
+
if (typeof this.editor?.viewPicture !== 'function') return {};
|
|
412
|
+
const start = model.currentViewId;
|
|
413
|
+
const pictures = {};
|
|
414
|
+
// Going through every view to draw it opens none of them as a tab.
|
|
415
|
+
return this.openViews.while(() => this._eachViewPicture(model, start, pictures));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** @private */
|
|
419
|
+
_eachViewPicture(model, start, pictures) {
|
|
420
|
+
try {
|
|
421
|
+
for (const view of model.views()) {
|
|
422
|
+
if (model.currentViewId !== view.id) model.switchTo(view.id); // the part redraws for it
|
|
423
|
+
const picture = this.editor?.viewPicture?.();
|
|
424
|
+
if (picture) pictures[view.id] = picture;
|
|
425
|
+
}
|
|
426
|
+
} finally {
|
|
427
|
+
if (model.currentViewId !== start) model.switchTo(start);
|
|
428
|
+
}
|
|
429
|
+
return pictures;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** A subclass's own File items or groups, after Import and Export (templates, ...). None here. */
|
|
433
|
+
ownFileItems() {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** The file formats (ModelFileFormat) this part's model is exported to and imported from: none here. */
|
|
438
|
+
fileFormats() {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Edit another model (an imported one) in place of this one, and show it. */
|
|
443
|
+
replaceModel(model) {
|
|
444
|
+
this._model = model;
|
|
445
|
+
this.openViews = new OpenViewList(model);
|
|
446
|
+
this._followEdits(model);
|
|
447
|
+
this.modelReplaced(model);
|
|
448
|
+
if (this.mounted) this.render();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** After `replaceModel`: a subclass moves what follows the model (change listeners, ...) onto the new one. */
|
|
452
|
+
modelReplaced(model) {}
|
|
453
|
+
|
|
454
|
+
dispose() {
|
|
455
|
+
this._unsubscribeEdits?.();
|
|
456
|
+
this.file.restoreTitle();
|
|
457
|
+
this.editor?.dispose?.();
|
|
458
|
+
this.editor = null;
|
|
459
|
+
super.dispose();
|
|
460
|
+
}
|
|
461
|
+
}
|