@peter.prib/renderengine 0.5.0 → 0.7.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/README.md +4 -0
- package/REFERENCE.md +252 -17
- package/dist/renderengine-workspace.umd.js +4995 -2932
- package/dist/renderengine.umd.js +3985 -2193
- package/examples/archimate.html +8 -28
- package/examples/data-model.html +45 -556
- package/package.json +70 -70
- package/src/archimateArchiFormat.js +11 -3
- package/src/archimateExchangeFormat.js +17 -9
- package/src/charting/ArchimateDiagramEditor.js +1 -1
- package/src/charting/Axis.js +20 -0
- package/src/charting/DiagramEditor.js +35 -3
- package/src/charting/Dialog.js +195 -0
- package/src/charting/DuplicatesDialog.js +108 -0
- package/src/charting/FormDialog.js +28 -97
- package/src/charting/InteractionManager.js +3 -0
- package/src/charting/Legend.js +45 -2
- package/src/charting/MergeCompareDialog.js +81 -0
- package/src/charting/PopupMenu.js +12 -3
- package/src/charting/SettingsDialog.js +6 -2
- package/src/charting/SettingsManager.js +14 -1
- package/src/charting/archimate/ArchimateCanvasEditor.js +190 -15
- package/src/charting/archimate/ArchimatePalette.js +1 -1
- package/src/charting/charts/AreaChart.js +5 -2
- package/src/charting/charts/BarChart.js +5 -3
- package/src/charting/charts/BarChart3DOrbit.js +5 -7
- package/src/charting/charts/BubbleChart.js +5 -7
- package/src/charting/charts/BubbleChart3D.js +5 -7
- package/src/charting/charts/CandlestickChart.js +3 -1
- package/src/charting/charts/DensityChart.js +4 -6
- package/src/charting/charts/GroupedBarChart.js +2 -2
- package/src/charting/charts/LineChart.js +5 -2
- package/src/charting/charts/ParallelCoordinatesChart.js +2 -2
- package/src/charting/charts/PipeChart.js +4 -6
- package/src/charting/charts/RadarChart.js +5 -6
- package/src/charting/charts/ScatterChart.js +5 -7
- package/src/charting/charts/StackedBarChart.js +4 -2
- package/src/charting/charts/StackedBarChart3DOrbit.js +5 -7
- package/src/charting/charts/StreamGraphChart.js +5 -2
- package/src/charting/charts/WaterfallChart.js +2 -1
- package/src/charting/content/ContentSource.js +129 -0
- package/src/charting/content/ContentSources.js +77 -0
- package/src/charting/datamodel/DataModel.js +116 -0
- package/src/charting/diagrams/ArchimateModel.js +38 -3
- package/src/charting/diagrams/ArchimateModelChecker.js +13 -4
- package/src/charting/diagrams/MergeElementsCommand.js +124 -101
- package/src/charting/diagrams/SvgZoom.js +20 -1
- package/src/charting/diagrams/duplicates.js +95 -13
- package/src/charting/duplicateSearchForm.js +58 -0
- package/src/charting/modelTree/ModelTree.js +239 -202
- package/src/charting/modelTree/ModelTreePanel.js +955 -919
- package/src/charting/modelTree/TreeBuilder.js +93 -45
- package/src/charting/modelTree/TreeViewMode.js +9 -6
- package/src/charting/modelTree/viewFolders.js +15 -6
- package/src/icons/IconSet.js +86 -0
- package/src/icons/index.js +59 -0
- package/src/index.js +65 -9
- package/src/workspace/dom.js +56 -42
- package/src/workspace/index.js +1 -1
- package/src/workspace/nav/NavEntry.js +42 -0
- package/src/workspace/nav/NavGroup.js +43 -0
- package/src/workspace/nav/NavItem.js +53 -0
- package/src/workspace/nav/NavMenu.js +143 -0
- package/src/workspace/nav/NavSection.js +17 -0
- package/src/workspace/nav/index.js +5 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +8 -3
- package/src/workspace/parts/DataModelPart.js +91 -0
- package/src/workspace/parts/EditorPart.js +8 -0
- package/src/workspace/parts/ModelHosted.js +1 -0
- package/src/workspace/parts/ModelsPart.js +765 -622
- package/src/workspace/styles.js +28 -1
|
@@ -1,919 +1,955 @@
|
|
|
1
|
-
import { treeModeRegistry } from '../../treeModeCatalog.js';
|
|
2
|
-
import { CheckboxTreeSelector, OptInCheckboxTreeSelector } from '../CheckboxTreeSelector.js';
|
|
3
|
-
import { ModelTree } from './ModelTree.js';
|
|
4
|
-
|
|
5
|
-
/** A property with more values than this lists them folded, with a box to find one. */
|
|
6
|
-
const PROPERTY_VALUES_FOLDED_ABOVE = 10;
|
|
7
|
-
import { TreeBuilder } from './TreeBuilder.js';
|
|
8
|
-
import { typeGlyph } from '../archimate/typeGlyph.js';
|
|
9
|
-
import { ArchimateNotation } from '../../archimate.js';
|
|
10
|
-
import { PopupMenu } from '../PopupMenu.js';
|
|
11
|
-
import { TREE_LAYERS, treeLayerOf } from './treeLayers.js';
|
|
12
|
-
import { viewFolderNodes } from './viewFolders.js';
|
|
13
|
-
|
|
14
|
-
export { TREE_LAYERS, treeLayerOf } from './treeLayers.js';
|
|
15
|
-
|
|
16
|
-
/** The data type an element dragged from the model tree carries: its model element id. */
|
|
17
|
-
export const MODEL_ELEMENT_MIME = 'application/x-archimate-element';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Tree context menus (folder, element, view, create), shown through the shared
|
|
21
|
-
* PopupMenu. Items: `{ label, action }` or `{ label, submenu: { items } }`,
|
|
22
|
-
* where submenu items may set `isActive` (checked) and `isSeparator` (rule above).
|
|
23
|
-
*/
|
|
24
|
-
class ContextMenuManager {
|
|
25
|
-
constructor(doc) {
|
|
26
|
-
this.doc = doc;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
show(event, items) {
|
|
30
|
-
event.preventDefault();
|
|
31
|
-
event.stopPropagation();
|
|
32
|
-
const toPopup = (item) => (item.submenu
|
|
33
|
-
? { label: item.label, items: item.submenu.items.flatMap(sub => [
|
|
34
|
-
...(sub.isSeparator ? [{ separator: true }] : []),
|
|
35
|
-
{ label: sub.label, action: sub.action, checked: sub.isActive || undefined },
|
|
36
|
-
]) }
|
|
37
|
-
: item); // plain items (separators, danger) are already PopupMenu items
|
|
38
|
-
PopupMenu.show(event.clientX, event.clientY, items.map(toPopup), this.doc);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Manages tree state: selected nodes, expanded folders, etc.
|
|
44
|
-
* Provides setState/getState methods and triggers re-render only on actual changes.
|
|
45
|
-
*/
|
|
46
|
-
class TreeStateManager {
|
|
47
|
-
constructor() {
|
|
48
|
-
this.state = {
|
|
49
|
-
selectedNodeId: null,
|
|
50
|
-
expandedNodes: new Set(),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Update state. Returns true if state changed, false otherwise.
|
|
56
|
-
*/
|
|
57
|
-
setState(updates) {
|
|
58
|
-
let changed = false;
|
|
59
|
-
|
|
60
|
-
if (updates.selectedNodeId !== undefined && updates.selectedNodeId !== this.state.selectedNodeId) {
|
|
61
|
-
this.state.selectedNodeId = updates.selectedNodeId;
|
|
62
|
-
changed = true;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (updates.expandedNodes !== undefined) {
|
|
66
|
-
const oldSize = this.state.expandedNodes.size;
|
|
67
|
-
this.state.expandedNodes = new Set(updates.expandedNodes);
|
|
68
|
-
if (oldSize !== this.state.expandedNodes.size) {
|
|
69
|
-
changed = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return changed;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get current state.
|
|
78
|
-
*/
|
|
79
|
-
getState() {
|
|
80
|
-
return {
|
|
81
|
-
selectedNodeId: this.state.selectedNodeId,
|
|
82
|
-
expandedNodes: new Set(this.state.expandedNodes),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Clear state.
|
|
88
|
-
*/
|
|
89
|
-
clear() {
|
|
90
|
-
this.state.selectedNodeId = null;
|
|
91
|
-
this.state.expandedNodes.clear();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* DOM shell for Archi-style model tree with folder hierarchy:
|
|
97
|
-
* - Views folder at top level
|
|
98
|
-
* - Layer/Type folders for elements (based on grouping mode)
|
|
99
|
-
* Shows icons and element properties on click. The panel's `refresh()` redraws
|
|
100
|
-
* the tree from the model (after an edit: a rename, an undo).
|
|
101
|
-
*/
|
|
102
|
-
export function buildModelTreePanel(doc, { modelTree, onSelectElement = null, onNewView = null, treeDivId = 'model-tree', viewMenu = null, templateMenu = null, elementMenu = null, search = false, version = '4' } = {}) {
|
|
103
|
-
// Store callbacks for use in event handlers. With a ViewMenu, views are
|
|
104
|
-
// created, duplicated, renamed and deleted through the model; without one,
|
|
105
|
-
// "New View" adds a view and hands it to onNewView. With a CanvasTemplateMenu,
|
|
106
|
-
// canvases and templates (the model's, and its library's) get their menus.
|
|
107
|
-
// elementMenu(element) adds items to an element's menu.
|
|
108
|
-
const callbacks = { onNewView, viewMenu, templateMenu, elementMenu };
|
|
109
|
-
|
|
110
|
-
const panel = doc.createElement('div');
|
|
111
|
-
panel.style.cssText = `
|
|
112
|
-
display: flex;
|
|
113
|
-
flex-direction: column;
|
|
114
|
-
height: 100%;
|
|
115
|
-
gap: 8px;
|
|
116
|
-
padding: 8px;
|
|
117
|
-
font-family: sans-serif;
|
|
118
|
-
font-size: 13px;
|
|
119
|
-
overflow: hidden;
|
|
120
|
-
`;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
box.
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
} else if (node.type === '
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return children
|
|
665
|
-
} else if (node.type === '
|
|
666
|
-
const children = [];
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1
|
+
import { treeModeRegistry } from '../../treeModeCatalog.js';
|
|
2
|
+
import { CheckboxTreeSelector, OptInCheckboxTreeSelector } from '../CheckboxTreeSelector.js';
|
|
3
|
+
import { ModelTree } from './ModelTree.js';
|
|
4
|
+
|
|
5
|
+
/** A property with more values than this lists them folded, with a box to find one. */
|
|
6
|
+
const PROPERTY_VALUES_FOLDED_ABOVE = 10;
|
|
7
|
+
import { TreeBuilder } from './TreeBuilder.js';
|
|
8
|
+
import { typeGlyph } from '../archimate/typeGlyph.js';
|
|
9
|
+
import { ArchimateNotation } from '../../archimate.js';
|
|
10
|
+
import { PopupMenu } from '../PopupMenu.js';
|
|
11
|
+
import { TREE_LAYERS, treeLayerOf } from './treeLayers.js';
|
|
12
|
+
import { viewFolderNodes } from './viewFolders.js';
|
|
13
|
+
|
|
14
|
+
export { TREE_LAYERS, treeLayerOf } from './treeLayers.js';
|
|
15
|
+
|
|
16
|
+
/** The data type an element dragged from the model tree carries: its model element id. */
|
|
17
|
+
export const MODEL_ELEMENT_MIME = 'application/x-archimate-element';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tree context menus (folder, element, view, create), shown through the shared
|
|
21
|
+
* PopupMenu. Items: `{ label, action }` or `{ label, submenu: { items } }`,
|
|
22
|
+
* where submenu items may set `isActive` (checked) and `isSeparator` (rule above).
|
|
23
|
+
*/
|
|
24
|
+
class ContextMenuManager {
|
|
25
|
+
constructor(doc) {
|
|
26
|
+
this.doc = doc;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
show(event, items) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
event.stopPropagation();
|
|
32
|
+
const toPopup = (item) => (item.submenu
|
|
33
|
+
? { label: item.label, items: item.submenu.items.flatMap(sub => [
|
|
34
|
+
...(sub.isSeparator ? [{ separator: true }] : []),
|
|
35
|
+
{ label: sub.label, action: sub.action, checked: sub.isActive || undefined },
|
|
36
|
+
]) }
|
|
37
|
+
: item); // plain items (separators, danger) are already PopupMenu items
|
|
38
|
+
PopupMenu.show(event.clientX, event.clientY, items.map(toPopup), this.doc);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Manages tree state: selected nodes, expanded folders, etc.
|
|
44
|
+
* Provides setState/getState methods and triggers re-render only on actual changes.
|
|
45
|
+
*/
|
|
46
|
+
class TreeStateManager {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.state = {
|
|
49
|
+
selectedNodeId: null,
|
|
50
|
+
expandedNodes: new Set(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update state. Returns true if state changed, false otherwise.
|
|
56
|
+
*/
|
|
57
|
+
setState(updates) {
|
|
58
|
+
let changed = false;
|
|
59
|
+
|
|
60
|
+
if (updates.selectedNodeId !== undefined && updates.selectedNodeId !== this.state.selectedNodeId) {
|
|
61
|
+
this.state.selectedNodeId = updates.selectedNodeId;
|
|
62
|
+
changed = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (updates.expandedNodes !== undefined) {
|
|
66
|
+
const oldSize = this.state.expandedNodes.size;
|
|
67
|
+
this.state.expandedNodes = new Set(updates.expandedNodes);
|
|
68
|
+
if (oldSize !== this.state.expandedNodes.size) {
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return changed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get current state.
|
|
78
|
+
*/
|
|
79
|
+
getState() {
|
|
80
|
+
return {
|
|
81
|
+
selectedNodeId: this.state.selectedNodeId,
|
|
82
|
+
expandedNodes: new Set(this.state.expandedNodes),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clear state.
|
|
88
|
+
*/
|
|
89
|
+
clear() {
|
|
90
|
+
this.state.selectedNodeId = null;
|
|
91
|
+
this.state.expandedNodes.clear();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* DOM shell for Archi-style model tree with folder hierarchy:
|
|
97
|
+
* - Views folder at top level
|
|
98
|
+
* - Layer/Type folders for elements (based on grouping mode)
|
|
99
|
+
* Shows icons and element properties on click. The panel's `refresh()` redraws
|
|
100
|
+
* the tree from the model (after an edit: a rename, an undo).
|
|
101
|
+
*/
|
|
102
|
+
export function buildModelTreePanel(doc, { modelTree, onSelectElement = null, onNewView = null, treeDivId = 'model-tree', viewMenu = null, templateMenu = null, elementMenu = null, search = false, version = '4' } = {}) {
|
|
103
|
+
// Store callbacks for use in event handlers. With a ViewMenu, views are
|
|
104
|
+
// created, duplicated, renamed and deleted through the model; without one,
|
|
105
|
+
// "New View" adds a view and hands it to onNewView. With a CanvasTemplateMenu,
|
|
106
|
+
// canvases and templates (the model's, and its library's) get their menus.
|
|
107
|
+
// elementMenu(element) adds items to an element's menu.
|
|
108
|
+
const callbacks = { onNewView, viewMenu, templateMenu, elementMenu };
|
|
109
|
+
|
|
110
|
+
const panel = doc.createElement('div');
|
|
111
|
+
panel.style.cssText = `
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
height: 100%;
|
|
115
|
+
gap: 8px;
|
|
116
|
+
padding: 8px;
|
|
117
|
+
font-family: sans-serif;
|
|
118
|
+
font-size: 13px;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
// Shows whether the search filters now (set with the search box; nothing to show without one).
|
|
124
|
+
let showSearchState = () => {};
|
|
125
|
+
|
|
126
|
+
// A search box (with `search`): the tree's text filter (from modelTree.minFilterChars characters on),
|
|
127
|
+
// which leaves out here the elements, views and folders it does not find and, through
|
|
128
|
+
// modelTree.onFilterChange, tells whatever else listens (the canvas dims them).
|
|
129
|
+
if (search) {
|
|
130
|
+
const box = doc.createElement('input');
|
|
131
|
+
Object.assign(box, { type: 'search', placeholder: '🔍 Search the model', value: modelTree.getFilterText?.() ?? '' });
|
|
132
|
+
box.setAttribute('data-tree-search', '');
|
|
133
|
+
box.style.cssText = 'width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 13px;';
|
|
134
|
+
box.addEventListener('input', () => { modelTree.setFilter(box.value.trim()); rebuildTree(); });
|
|
135
|
+
panel.appendChild(box);
|
|
136
|
+
|
|
137
|
+
// What the search covers (the whole model, or the diagram in front) and whether it reads descriptions.
|
|
138
|
+
const options = doc.createElement('div');
|
|
139
|
+
options.style.cssText = 'display: flex; gap: 8px; align-items: center; font-size: 12px; color: #555;';
|
|
140
|
+
const scope = doc.createElement('select');
|
|
141
|
+
scope.setAttribute('aria-label', 'Search in');
|
|
142
|
+
scope.style.cssText = 'font-size: 12px; padding: 1px 2px;';
|
|
143
|
+
for (const [key, label] of ModelTree.SEARCH_SCOPES) {
|
|
144
|
+
const option = doc.createElement('option');
|
|
145
|
+
Object.assign(option, { value: key, textContent: label, selected: key === modelTree.getSearchScope?.() });
|
|
146
|
+
scope.appendChild(option);
|
|
147
|
+
}
|
|
148
|
+
scope.addEventListener('change', () => { modelTree.setSearchScope(scope.value); rebuildTree(); });
|
|
149
|
+
const described = doc.createElement('label');
|
|
150
|
+
described.style.cssText = 'display: flex; gap: 4px; align-items: center; cursor: pointer;';
|
|
151
|
+
const tick = doc.createElement('input');
|
|
152
|
+
Object.assign(tick, { type: 'checkbox', checked: !!modelTree.getSearchInDescriptions?.() });
|
|
153
|
+
tick.setAttribute('data-search-descriptions', '');
|
|
154
|
+
tick.addEventListener('change', () => { modelTree.setSearchInDescriptions(tick.checked); rebuildTree(); });
|
|
155
|
+
described.append(tick, 'Descriptions');
|
|
156
|
+
options.append('In:', scope, described);
|
|
157
|
+
panel.appendChild(options);
|
|
158
|
+
|
|
159
|
+
// While the text filters the box is marked and says what it found; shorter, how much more to type.
|
|
160
|
+
const state = doc.createElement('div');
|
|
161
|
+
state.setAttribute('data-search-state', '');
|
|
162
|
+
state.style.cssText = 'font-size: 11px; color: #555; min-height: 14px;';
|
|
163
|
+
panel.appendChild(state);
|
|
164
|
+
showSearchState = () => {
|
|
165
|
+
const typed = modelTree.getFilterText?.() ?? '';
|
|
166
|
+
const on = !!modelTree.filterText;
|
|
167
|
+
if (on) box.setAttribute('data-filtering', '');
|
|
168
|
+
else box.removeAttribute('data-filtering');
|
|
169
|
+
box.style.borderColor = on ? '#2a7ae2' : '#ccc';
|
|
170
|
+
box.style.background = on ? '#eef5ff' : '';
|
|
171
|
+
if (on) {
|
|
172
|
+
const elements = modelTree.matchingElementIds?.()?.size ?? 0;
|
|
173
|
+
const views = (modelTree.model?.views?.() ?? []).filter(v => modelTree.textMatches(v.name, v.documentation)).length;
|
|
174
|
+
const count = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
175
|
+
state.textContent = `Filtering: ${count(elements, 'element')}, ${count(views, 'view')} found`;
|
|
176
|
+
} else {
|
|
177
|
+
const more = (modelTree.minFilterChars ?? 0) - typed.length;
|
|
178
|
+
state.textContent = typed && more > 0 ? `Type ${more} more character${more === 1 ? '' : 's'} to search` : '';
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Filter button opens modal dialog
|
|
184
|
+
const filterBtnRow = doc.createElement('div');
|
|
185
|
+
filterBtnRow.style.cssText = 'margin-bottom: 8px;';
|
|
186
|
+
|
|
187
|
+
const filterBtn = doc.createElement('button');
|
|
188
|
+
filterBtn.type = 'button';
|
|
189
|
+
filterBtn.textContent = '🔍 Filters…';
|
|
190
|
+
filterBtn.style.cssText = 'width: 100%; padding: 6px; font-size: 13px; font-weight: bold; background: #f5f5f5; border: 1px solid #ccc; border-radius: 3px; cursor: pointer;';
|
|
191
|
+
filterBtnRow.appendChild(filterBtn);
|
|
192
|
+
panel.appendChild(filterBtnRow);
|
|
193
|
+
|
|
194
|
+
// Helper: build fresh filter area content (called each time modal opens to stay in sync with model changes)
|
|
195
|
+
function buildFilterContent() {
|
|
196
|
+
const filterArea = doc.createElement('div');
|
|
197
|
+
filterArea.style.cssText = `
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
gap: 8px;
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
// Type/Specialization filter (collapsible checkbox tree)
|
|
204
|
+
const typeTreeData = modelTree.getAvailableTypeTree();
|
|
205
|
+
if (typeTreeData && typeTreeData.length > 0) {
|
|
206
|
+
const typeFilterContainer = doc.createElement('div');
|
|
207
|
+
typeFilterContainer.style.cssText = 'padding: 4px; border-bottom: 1px solid #e0e0e0;';
|
|
208
|
+
|
|
209
|
+
const typeHeaderRow = doc.createElement('div');
|
|
210
|
+
typeHeaderRow.style.cssText = 'display: flex; gap: 4px; align-items: center; justify-content: space-between; margin-bottom: 4px;';
|
|
211
|
+
|
|
212
|
+
const typeDetails = doc.createElement('details');
|
|
213
|
+
typeDetails.style.cssText = 'flex: 1;';
|
|
214
|
+
typeDetails.open = true;
|
|
215
|
+
|
|
216
|
+
const typeSummary = doc.createElement('summary');
|
|
217
|
+
typeSummary.style.cssText = 'cursor: pointer; font-weight: bold; user-select: none; padding: 2px 4px;';
|
|
218
|
+
typeSummary.textContent = '▼ Type/Specialization';
|
|
219
|
+
typeDetails.appendChild(typeSummary);
|
|
220
|
+
|
|
221
|
+
// Rename labels for better UX (strip common prefixes)
|
|
222
|
+
const cleanTreeData = _cleanTreeLabels(typeTreeData);
|
|
223
|
+
|
|
224
|
+
// Showing the filter on now (none: every type ticked).
|
|
225
|
+
const typeOn = [...(modelTree.getTypeFilter?.() ?? []), ...(modelTree.getSpecializationFilter?.() ?? [])];
|
|
226
|
+
const typeSelector = new CheckboxTreeSelector({ doc, nodes: cleanTreeData, checkedIds: typeOn.length ? new Set(typeOn) : null });
|
|
227
|
+
const typeContainer = typeSelector.build();
|
|
228
|
+
typeContainer.style.cssText = 'padding-left: 8px; font-size: 12px;';
|
|
229
|
+
typeDetails.appendChild(typeContainer);
|
|
230
|
+
|
|
231
|
+
const typeResetBtn = doc.createElement('button');
|
|
232
|
+
typeResetBtn.type = 'button';
|
|
233
|
+
typeResetBtn.textContent = 'Reset';
|
|
234
|
+
typeResetBtn.style.cssText = 'padding: 2px 6px; font-size: 11px; border: 1px solid #ccc; border-radius: 2px; cursor: pointer; background: #fff; color: #333;';
|
|
235
|
+
typeResetBtn.addEventListener('click', () => {
|
|
236
|
+
typeSelector.setCheckedIds(null);
|
|
237
|
+
modelTree.setTypeFilter(null);
|
|
238
|
+
modelTree.setSpecializationFilter(null);
|
|
239
|
+
rebuildTree();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
typeHeaderRow.appendChild(typeDetails);
|
|
243
|
+
typeHeaderRow.appendChild(typeResetBtn);
|
|
244
|
+
typeFilterContainer.appendChild(typeHeaderRow);
|
|
245
|
+
|
|
246
|
+
typeSelector.onChange((checkedIds) => {
|
|
247
|
+
// CheckboxTreeSelector handles cascading automatically
|
|
248
|
+
const typeIds = new Set([...checkedIds].filter(id => id.startsWith('type:')));
|
|
249
|
+
const specIds = new Set([...checkedIds].filter(id => id.startsWith('spec:')));
|
|
250
|
+
modelTree.setTypeFilter(typeIds.size > 0 ? typeIds : null);
|
|
251
|
+
modelTree.setSpecializationFilter(specIds.size > 0 ? specIds : null);
|
|
252
|
+
rebuildTree();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
filterArea.appendChild(typeFilterContainer);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Property filter (collapsible checkbox tree)
|
|
259
|
+
const propTreeData = modelTree.getAvailablePropertyTree();
|
|
260
|
+
if (propTreeData && propTreeData.length > 0) {
|
|
261
|
+
const propFilterContainer = doc.createElement('div');
|
|
262
|
+
propFilterContainer.style.cssText = 'padding: 4px;';
|
|
263
|
+
|
|
264
|
+
const propHeaderRow = doc.createElement('div');
|
|
265
|
+
propHeaderRow.style.cssText = 'display: flex; gap: 4px; align-items: center; justify-content: space-between; margin-bottom: 4px;';
|
|
266
|
+
|
|
267
|
+
const propDetails = doc.createElement('details');
|
|
268
|
+
propDetails.style.cssText = 'flex: 1;';
|
|
269
|
+
propDetails.open = true;
|
|
270
|
+
|
|
271
|
+
const propSummary = doc.createElement('summary');
|
|
272
|
+
propSummary.style.cssText = 'cursor: pointer; font-weight: bold; user-select: none; padding: 2px 4px;';
|
|
273
|
+
propSummary.textContent = '▼ Properties';
|
|
274
|
+
propDetails.appendChild(propSummary);
|
|
275
|
+
|
|
276
|
+
// Nothing ticked to start (no property filter); a value is ticked to keep what has it.
|
|
277
|
+
// A property with many values is folded, with a box to find one.
|
|
278
|
+
const propOn = modelTree.getPropertyFilter?.();
|
|
279
|
+
const propSelector = new OptInCheckboxTreeSelector({
|
|
280
|
+
doc, nodes: propTreeData, checkedIds: propOn ? new Set(propOn) : null, searchAbove: PROPERTY_VALUES_FOLDED_ABOVE,
|
|
281
|
+
});
|
|
282
|
+
const propContainer = propSelector.build();
|
|
283
|
+
propContainer.style.cssText = 'padding-left: 8px; font-size: 12px;';
|
|
284
|
+
propDetails.appendChild(propContainer);
|
|
285
|
+
|
|
286
|
+
const propResetBtn = doc.createElement('button');
|
|
287
|
+
propResetBtn.type = 'button';
|
|
288
|
+
propResetBtn.textContent = 'Reset';
|
|
289
|
+
propResetBtn.style.cssText = 'padding: 2px 6px; font-size: 11px; border: 1px solid #ccc; border-radius: 2px; cursor: pointer; background: #fff; color: #333;';
|
|
290
|
+
propResetBtn.addEventListener('click', () => {
|
|
291
|
+
propSelector.setCheckedIds(null);
|
|
292
|
+
modelTree.setPropertyFilter(null);
|
|
293
|
+
rebuildTree();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
propHeaderRow.appendChild(propDetails);
|
|
297
|
+
propHeaderRow.appendChild(propResetBtn);
|
|
298
|
+
propFilterContainer.appendChild(propHeaderRow);
|
|
299
|
+
|
|
300
|
+
propSelector.onChange((checkedIds) => {
|
|
301
|
+
const propIds = new Set([...checkedIds].filter(id => id.startsWith('propval:')));
|
|
302
|
+
modelTree.setPropertyFilter(propIds.size > 0 ? propIds : null);
|
|
303
|
+
rebuildTree();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
propFilterContainer.style.cssText = 'padding: 4px;';
|
|
307
|
+
filterArea.appendChild(propFilterContainer);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return filterArea;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Wire up filter button to open modal with fresh filter content
|
|
314
|
+
filterBtn.addEventListener('click', () => {
|
|
315
|
+
const filterContent = buildFilterContent();
|
|
316
|
+
if (filterContent.hasChildNodes()) {
|
|
317
|
+
showFiltersModal(doc, filterContent);
|
|
318
|
+
} else {
|
|
319
|
+
alert('No filters available (no types or properties in model)');
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Helper: show filters in a modal dialog
|
|
324
|
+
function showFiltersModal(doc, filterAreaContent) {
|
|
325
|
+
// Modal backdrop
|
|
326
|
+
const backdrop = doc.createElement('div');
|
|
327
|
+
backdrop.style.cssText = `
|
|
328
|
+
position: fixed;
|
|
329
|
+
top: 0;
|
|
330
|
+
left: 0;
|
|
331
|
+
right: 0;
|
|
332
|
+
bottom: 0;
|
|
333
|
+
background: rgba(0, 0, 0, 0.3);
|
|
334
|
+
z-index: 9999;
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
// Modal dialog
|
|
338
|
+
const modal = doc.createElement('div');
|
|
339
|
+
modal.style.cssText = `
|
|
340
|
+
position: fixed;
|
|
341
|
+
top: 50%;
|
|
342
|
+
left: 50%;
|
|
343
|
+
transform: translate(-50%, -50%);
|
|
344
|
+
width: 90%;
|
|
345
|
+
max-width: 500px;
|
|
346
|
+
max-height: 70vh;
|
|
347
|
+
background: #fff;
|
|
348
|
+
border: 1px solid #ccc;
|
|
349
|
+
border-radius: 6px;
|
|
350
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
351
|
+
z-index: 10000;
|
|
352
|
+
display: flex;
|
|
353
|
+
flex-direction: column;
|
|
354
|
+
overflow: hidden;
|
|
355
|
+
`;
|
|
356
|
+
|
|
357
|
+
// Header
|
|
358
|
+
const header = doc.createElement('div');
|
|
359
|
+
header.style.cssText = `
|
|
360
|
+
padding: 12px 16px;
|
|
361
|
+
border-bottom: 1px solid #e0e0e0;
|
|
362
|
+
display: flex;
|
|
363
|
+
align-items: center;
|
|
364
|
+
justify-content: space-between;
|
|
365
|
+
font-weight: bold;
|
|
366
|
+
font-size: 14px;
|
|
367
|
+
flex-shrink: 0;
|
|
368
|
+
`;
|
|
369
|
+
header.textContent = 'Filters';
|
|
370
|
+
|
|
371
|
+
const closeBtn = doc.createElement('button');
|
|
372
|
+
closeBtn.type = 'button';
|
|
373
|
+
closeBtn.textContent = '✕';
|
|
374
|
+
closeBtn.style.cssText = 'background: none; border: none; font-size: 18px; cursor: pointer; color: #999;';
|
|
375
|
+
closeBtn.addEventListener('click', () => {
|
|
376
|
+
backdrop.remove();
|
|
377
|
+
modal.remove();
|
|
378
|
+
});
|
|
379
|
+
header.appendChild(closeBtn);
|
|
380
|
+
|
|
381
|
+
// Content (scrollable, contains the actual filterArea with event listeners)
|
|
382
|
+
const content = doc.createElement('div');
|
|
383
|
+
content.style.cssText = `
|
|
384
|
+
flex: 1;
|
|
385
|
+
overflow-y: auto;
|
|
386
|
+
padding: 12px 16px;
|
|
387
|
+
font-size: 13px;
|
|
388
|
+
`;
|
|
389
|
+
// Move the actual filterArea into the modal (keeps event listeners intact)
|
|
390
|
+
content.appendChild(filterAreaContent);
|
|
391
|
+
|
|
392
|
+
modal.appendChild(header);
|
|
393
|
+
modal.appendChild(content);
|
|
394
|
+
|
|
395
|
+
backdrop.addEventListener('click', () => {
|
|
396
|
+
backdrop.remove();
|
|
397
|
+
modal.remove();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
doc.body.appendChild(backdrop);
|
|
401
|
+
doc.body.appendChild(modal);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Tree container
|
|
405
|
+
const treeDiv = doc.createElement('div');
|
|
406
|
+
treeDiv.id = treeDivId;
|
|
407
|
+
treeDiv.style.cssText = `
|
|
408
|
+
flex: 1;
|
|
409
|
+
overflow-y: auto;
|
|
410
|
+
border: 1px solid #ccc;
|
|
411
|
+
border-radius: 4px;
|
|
412
|
+
padding: 4px;
|
|
413
|
+
display: flex;
|
|
414
|
+
flex-direction: column;
|
|
415
|
+
`;
|
|
416
|
+
|
|
417
|
+
panel.appendChild(treeDiv);
|
|
418
|
+
// An element leaf (TreeBuilder marks it with data-element-id) drags its element's id; an editor's drop handler takes it.
|
|
419
|
+
treeDiv.addEventListener('dragstart', (event) => {
|
|
420
|
+
const node = event.target.closest?.('[data-element-id]');
|
|
421
|
+
if (!node) return;
|
|
422
|
+
event.dataTransfer.effectAllowed = 'copy';
|
|
423
|
+
event.dataTransfer.setData(MODEL_ELEMENT_MIME, node.getAttribute('data-element-id'));
|
|
424
|
+
node.style.opacity = '0.5';
|
|
425
|
+
});
|
|
426
|
+
treeDiv.addEventListener('dragend', (event) => {
|
|
427
|
+
const node = event.target.closest?.('[data-element-id]');
|
|
428
|
+
if (node) node.style.opacity = '';
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Initialize managers
|
|
432
|
+
const contextMenuManager = new ContextMenuManager(doc);
|
|
433
|
+
const treeStateManager = new TreeStateManager();
|
|
434
|
+
|
|
435
|
+
// Helper: clean type labels for better UX (strip layer prefixes)
|
|
436
|
+
// E.g., "business-actor" → "actor", "application-service" → "service"
|
|
437
|
+
function _cleanTreeLabels(nodes) {
|
|
438
|
+
return nodes.map(node => {
|
|
439
|
+
let label = node.label;
|
|
440
|
+
// For types (e.g., 'business-actor'), strip the layer prefix
|
|
441
|
+
if (node.id && node.id.startsWith('type:')) {
|
|
442
|
+
const typeName = node.id.substring(5); // remove 'type:'
|
|
443
|
+
const parts = typeName.split('-');
|
|
444
|
+
// Skip the first part if it looks like a layer name (single word)
|
|
445
|
+
if (parts.length > 1 && ['business', 'application', 'technology', 'implementation', 'motivation', 'strategy', 'physical'].includes(parts[0])) {
|
|
446
|
+
label = parts.slice(1).join('-');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Recursively clean children
|
|
450
|
+
if (node.children) {
|
|
451
|
+
return { ...node, label, children: _cleanTreeLabels(node.children) };
|
|
452
|
+
}
|
|
453
|
+
return { ...node, label };
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Prepare data: fetch views, folders, elements from model.
|
|
459
|
+
* Returns { views, model, folders, foldersByParent, topLevelFolders, allElements, elementsByLayer }
|
|
460
|
+
*/
|
|
461
|
+
function prepareData() {
|
|
462
|
+
const model = modelTree.model;
|
|
463
|
+
// Every view but the templates (they have a folder of their own), canvases too, as Archi's Views folder holds them.
|
|
464
|
+
const templates = model?.templates?.() ?? null;
|
|
465
|
+
const views = templates ? model.views().filter(v => !v.template) : modelTree.getAvailableViews();
|
|
466
|
+
|
|
467
|
+
// Get ALL folders including nested ones
|
|
468
|
+
const allFolders = model?.allFolders?.() || [];
|
|
469
|
+
const foldersByParent = {};
|
|
470
|
+
const topLevelFolders = [];
|
|
471
|
+
for (const folder of allFolders) {
|
|
472
|
+
if (!folder.parentId) {
|
|
473
|
+
topLevelFolders.push(folder);
|
|
474
|
+
} else {
|
|
475
|
+
if (!foldersByParent[folder.parentId]) {
|
|
476
|
+
foldersByParent[folder.parentId] = [];
|
|
477
|
+
}
|
|
478
|
+
foldersByParent[folder.parentId].push(folder);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const allElements = model?.elements?.() || [];
|
|
483
|
+
const elementsByLayer = {};
|
|
484
|
+
for (const element of allElements) {
|
|
485
|
+
const layer = treeLayerOf(element);
|
|
486
|
+
if (!elementsByLayer[layer]) {
|
|
487
|
+
elementsByLayer[layer] = [];
|
|
488
|
+
}
|
|
489
|
+
elementsByLayer[layer].push(element);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
views,
|
|
494
|
+
templates,
|
|
495
|
+
model,
|
|
496
|
+
allFolders,
|
|
497
|
+
foldersByParent,
|
|
498
|
+
topLevelFolders,
|
|
499
|
+
allElements,
|
|
500
|
+
elementsByLayer,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Build tree structure: create rootNodes from prepared data.
|
|
506
|
+
*/
|
|
507
|
+
function buildTreeStructure(preparedData) {
|
|
508
|
+
const { views, templates, elementsByLayer, topLevelFolders, foldersByParent } = preparedData;
|
|
509
|
+
|
|
510
|
+
const rootNodes = [];
|
|
511
|
+
|
|
512
|
+
// Views category: the views in Archi's sub-folders
|
|
513
|
+
rootNodes.push({
|
|
514
|
+
id: 'category-views',
|
|
515
|
+
label: 'Views',
|
|
516
|
+
icon: '📄',
|
|
517
|
+
type: 'category-views',
|
|
518
|
+
_views: views,
|
|
519
|
+
_emptyFolders: preparedData.model?.emptyViewFolders ?? [],
|
|
520
|
+
_depth: 0,
|
|
521
|
+
_topLevelFolders: topLevelFolders
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Templates (the model's, then the library's)
|
|
525
|
+
if (templates) {
|
|
526
|
+
// Closed until opened: the library's templates are many, and not what a model is about.
|
|
527
|
+
rootNodes.push({ id: 'category-templates', label: 'Templates', icon: '📋', type: 'category-templates', _views: templates, collapsed: true });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Layers category
|
|
531
|
+
rootNodes.push({
|
|
532
|
+
id: 'category-layers',
|
|
533
|
+
label: 'Layers',
|
|
534
|
+
icon: '📚',
|
|
535
|
+
type: 'category-layers',
|
|
536
|
+
_elementsByLayer: elementsByLayer
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Return data needed for getNodeChildren
|
|
540
|
+
return {
|
|
541
|
+
rootNodes,
|
|
542
|
+
foldersByParent,
|
|
543
|
+
topLevelFolders,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Render tree: use TreeBuilder to render the structure.
|
|
549
|
+
*/
|
|
550
|
+
function renderTree(treeStructure, preparedData) {
|
|
551
|
+
const { rootNodes, foldersByParent, topLevelFolders } = treeStructure;
|
|
552
|
+
const { model } = preparedData;
|
|
553
|
+
|
|
554
|
+
treeDiv.replaceChildren();
|
|
555
|
+
|
|
556
|
+
const categoriesContainer = doc.createElement('div');
|
|
557
|
+
categoriesContainer.className = 'categories-tree-container';
|
|
558
|
+
categoriesContainer.style.cssText = 'flex: 1; overflow-y: auto; overflow-x: hidden; font-size: 12px;';
|
|
559
|
+
treeDiv.appendChild(categoriesContainer);
|
|
560
|
+
|
|
561
|
+
const categoriesBuilder = new TreeBuilder(doc, {
|
|
562
|
+
container: categoriesContainer,
|
|
563
|
+
rootNodes: rootNodes,
|
|
564
|
+
openState: modelTree.openFolders ?? new Map(),
|
|
565
|
+
// What the filters keep (none on: everything), and the leaves a folder shows before "more".
|
|
566
|
+
filter: treeFilter(),
|
|
567
|
+
leafLimit: modelTree.leafLimit ?? Infinity,
|
|
568
|
+
shownAll: modelTree.foldersShownAll ??= new Set(),
|
|
569
|
+
// An element shows its type's glyph (as the palette does), and so does a folder of one type.
|
|
570
|
+
iconFor: (node) => {
|
|
571
|
+
const type = node.type === 'element' ? node._element?.type : node.type === 'type-group' ? node._elements?.[0]?.type : null;
|
|
572
|
+
return type && (ArchimateNotation.elementType(type, version) || ArchimateNotation.elementType(type, '3.2')) ? typeGlyph(type, { size: 11, version, document: doc }) : null;
|
|
573
|
+
},
|
|
574
|
+
onNodeClick: (nodeId, node) => {
|
|
575
|
+
if (node.type === 'element' && node._element) {
|
|
576
|
+
treeStateManager.setState({ selectedNodeId: nodeId });
|
|
577
|
+
modelTree.select(node._element.id, 'element');
|
|
578
|
+
if (onSelectElement) {
|
|
579
|
+
onSelectElement({ id: node._element.id, kind: 'element', data: node._element });
|
|
580
|
+
}
|
|
581
|
+
} else if (node.type === 'view' && node._view) {
|
|
582
|
+
treeStateManager.setState({ selectedNodeId: nodeId });
|
|
583
|
+
modelTree.select(node._view.id, 'view');
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
onNodeContext: (event, node) => {
|
|
587
|
+
if (node.type === 'folder') {
|
|
588
|
+
_showFolderContextMenu(event, node._model, { model });
|
|
589
|
+
} else if (node.type === 'element' && node._element) {
|
|
590
|
+
_showElementContextMenu(event, node._element, { model });
|
|
591
|
+
} else if (node.type === 'view' && node._view && (callbacks.viewMenu || callbacks.templateMenu)) {
|
|
592
|
+
const id = node._view.id;
|
|
593
|
+
const own = callbacks.viewMenu?.items(id) ?? [];
|
|
594
|
+
const templateItems = callbacks.templateMenu
|
|
595
|
+
? [...callbacks.templateMenu.saveAsTemplateItems(id), ...callbacks.templateMenu.modelTemplateItems(id)]
|
|
596
|
+
: [];
|
|
597
|
+
contextMenuManager.show(event, [...templateItems, ...(templateItems.length && own.length ? [{ separator: true }] : []), ...own]);
|
|
598
|
+
} else if (node.type === 'view-folder') {
|
|
599
|
+
_showViewsCategoryMenu(event, { model, folder: true });
|
|
600
|
+
} else if (node.type === 'category-templates' || node.type === 'template-library') {
|
|
601
|
+
const items = [...(callbacks.templateMenu?.newCanvasItems().slice(1) ?? []), callbacks.templateMenu?.importItem()].filter(Boolean);
|
|
602
|
+
if (items.length) contextMenuManager.show(event, items);
|
|
603
|
+
} else if (node.type === 'library-template') {
|
|
604
|
+
contextMenuManager.show(event, callbacks.templateMenu.libraryEntryItems(node._entry.id));
|
|
605
|
+
} else if (node.type === 'category-views') {
|
|
606
|
+
_showViewsCategoryMenu(event, { model });
|
|
607
|
+
} else if (node.type === 'category-layers') {
|
|
608
|
+
// Layers category is read-only, no context menu
|
|
609
|
+
event.preventDefault();
|
|
610
|
+
event.stopPropagation();
|
|
611
|
+
} else if (node.type === 'layer') {
|
|
612
|
+
// Allow creating folders under layers
|
|
613
|
+
_showLayerContextMenu(event, node, { model });
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
getNodeChildren: (node) => {
|
|
617
|
+
if (node.type === 'category-views') {
|
|
618
|
+
const children = viewFolderNodes(node._views, node._depth, node._emptyFolders);
|
|
619
|
+
for (const folder of topLevelFolders) {
|
|
620
|
+
children.push({
|
|
621
|
+
id: folder.id,
|
|
622
|
+
label: folder.name || 'Untitled Folder',
|
|
623
|
+
icon: '📁',
|
|
624
|
+
type: 'folder',
|
|
625
|
+
_model: folder
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return children.length > 0 ? children : [];
|
|
629
|
+
} else if (node.type === 'view-folder') {
|
|
630
|
+
return viewFolderNodes(node._views, node._depth, node._emptyFolders);
|
|
631
|
+
} else if (node.type === 'category-templates') {
|
|
632
|
+
const children = node._views.map(view => ({ id: view.id, label: view.name || 'Untitled Template', icon: '📋', type: 'view', _view: view }));
|
|
633
|
+
if (callbacks.templateMenu?.library) {
|
|
634
|
+
children.push({ id: 'template-library', label: 'Library', icon: '📚', type: 'template-library' });
|
|
635
|
+
}
|
|
636
|
+
return children;
|
|
637
|
+
} else if (node.type === 'template-library') {
|
|
638
|
+
return callbacks.templateMenu.library.list().map(entry => ({
|
|
639
|
+
id: `library-${entry.id}`, label: entry.name || 'Untitled Template', icon: '📋', type: 'library-template', _entry: entry,
|
|
640
|
+
}));
|
|
641
|
+
} else if (node.type === 'category-layers') {
|
|
642
|
+
const children = [];
|
|
643
|
+
for (const layerName of TREE_LAYERS) {
|
|
644
|
+
if (node._elementsByLayer[layerName] && node._elementsByLayer[layerName].length > 0) {
|
|
645
|
+
const layerLabel = layerName.charAt(0).toUpperCase() + layerName.slice(1);
|
|
646
|
+
const elementsByType = {};
|
|
647
|
+
for (const element of node._elementsByLayer[layerName]) {
|
|
648
|
+
const typeKey = element.type || 'other';
|
|
649
|
+
if (!elementsByType[typeKey]) {
|
|
650
|
+
elementsByType[typeKey] = [];
|
|
651
|
+
}
|
|
652
|
+
elementsByType[typeKey].push(element);
|
|
653
|
+
}
|
|
654
|
+
children.push({
|
|
655
|
+
id: `layer-${layerName}`,
|
|
656
|
+
label: layerLabel,
|
|
657
|
+
icon: '📁',
|
|
658
|
+
type: 'layer',
|
|
659
|
+
_layerName: layerName,
|
|
660
|
+
_elementsByType: elementsByType
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return children;
|
|
665
|
+
} else if (node.type === 'layer') {
|
|
666
|
+
const children = [];
|
|
667
|
+
|
|
668
|
+
// Add folders that belong to this layer
|
|
669
|
+
const allFolders = preparedData.allFolders || [];
|
|
670
|
+
for (const folder of allFolders) {
|
|
671
|
+
if (folder._layerName === node._layerName) {
|
|
672
|
+
children.push({
|
|
673
|
+
id: folder.id,
|
|
674
|
+
label: folder.name || 'Untitled Folder',
|
|
675
|
+
icon: '📁',
|
|
676
|
+
type: 'folder',
|
|
677
|
+
_model: folder
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Add type groups for elements
|
|
683
|
+
for (const typeKey in node._elementsByType) {
|
|
684
|
+
const elements = node._elementsByType[typeKey];
|
|
685
|
+
const parts = typeKey.split('-');
|
|
686
|
+
const words = parts.length > 1 ? parts.slice(1) : parts;
|
|
687
|
+
const typeName = words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
688
|
+
children.push({
|
|
689
|
+
id: `type-${typeKey}`,
|
|
690
|
+
label: typeName,
|
|
691
|
+
icon: '📁',
|
|
692
|
+
type: 'type-group',
|
|
693
|
+
_elements: elements
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
return children.length > 0 ? children : null;
|
|
697
|
+
} else if (node.type === 'type-group') {
|
|
698
|
+
const children = [];
|
|
699
|
+
for (const element of node._elements) {
|
|
700
|
+
children.push({
|
|
701
|
+
id: element.id,
|
|
702
|
+
label: element.name || 'Untitled',
|
|
703
|
+
icon: '●',
|
|
704
|
+
type: 'element',
|
|
705
|
+
_element: element
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return children.length > 0 ? children : null;
|
|
709
|
+
} else if (node.type === 'folder') {
|
|
710
|
+
const children = foldersByParent[node.id];
|
|
711
|
+
if (children) {
|
|
712
|
+
return children.map(f => ({
|
|
713
|
+
id: f.id,
|
|
714
|
+
label: f.name || 'Untitled Folder',
|
|
715
|
+
icon: '📁',
|
|
716
|
+
type: 'folder',
|
|
717
|
+
_model: f
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
categoriesBuilder.render();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Rebuild tree: data fetch → structure build → render.
|
|
731
|
+
*/
|
|
732
|
+
function rebuildTree() {
|
|
733
|
+
const preparedData = prepareData();
|
|
734
|
+
const treeStructure = buildTreeStructure(preparedData);
|
|
735
|
+
renderTree(treeStructure, preparedData);
|
|
736
|
+
showSearchState();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Which leaves the tree's filters keep, or null with none on: the elements
|
|
741
|
+
* they match; a view, template or empty folder whose name (or documentation,
|
|
742
|
+
* with descriptions searched) has the search text.
|
|
743
|
+
*/
|
|
744
|
+
function treeFilter() {
|
|
745
|
+
const keep = modelTree.matchingElementIds?.() ?? null;
|
|
746
|
+
if (!keep) return null;
|
|
747
|
+
return (node) => {
|
|
748
|
+
if (node.type === 'element') return keep.has(node._element?.id);
|
|
749
|
+
if (node.type === 'view') return modelTree.textMatches(node._view?.name ?? node.label, node._view?.documentation);
|
|
750
|
+
return modelTree.textMatches(node.label);
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Show context menu for layer (right-click on a layer).
|
|
756
|
+
* Allows creating folders under this layer.
|
|
757
|
+
*/
|
|
758
|
+
function _showLayerContextMenu(event, layerNode, { model }) {
|
|
759
|
+
const items = [
|
|
760
|
+
{
|
|
761
|
+
label: 'Add Folder',
|
|
762
|
+
action: () => {
|
|
763
|
+
const folderName = window.prompt('Folder name:', 'New Folder');
|
|
764
|
+
if (folderName && folderName.trim()) {
|
|
765
|
+
const folderId = `folder-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
766
|
+
// Mark this folder as belonging to a specific layer
|
|
767
|
+
const newFolder = {
|
|
768
|
+
id: folderId,
|
|
769
|
+
name: folderName.trim(),
|
|
770
|
+
documentation: '',
|
|
771
|
+
parentId: undefined,
|
|
772
|
+
children: [],
|
|
773
|
+
_layerName: layerNode._layerName // Track which layer owns this folder
|
|
774
|
+
};
|
|
775
|
+
model._folders.set(folderId, newFolder);
|
|
776
|
+
rebuildTree();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
];
|
|
781
|
+
|
|
782
|
+
contextMenuManager.show(event, items);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Show context menu for folder (right-click on a folder item).
|
|
787
|
+
* Uses ContextMenuManager for consistent handling.
|
|
788
|
+
*/
|
|
789
|
+
function _showFolderContextMenu(event, folder, { model }) {
|
|
790
|
+
const items = [
|
|
791
|
+
{
|
|
792
|
+
label: 'New Subfolder…',
|
|
793
|
+
action: () => {
|
|
794
|
+
const subfolderName = window.prompt('Subfolder name:', 'New Folder');
|
|
795
|
+
if (subfolderName && subfolderName.trim()) {
|
|
796
|
+
const folderId = `folder-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
797
|
+
const newFolder = {
|
|
798
|
+
id: folderId,
|
|
799
|
+
name: subfolderName.trim(),
|
|
800
|
+
documentation: '',
|
|
801
|
+
parentId: folder.id,
|
|
802
|
+
children: []
|
|
803
|
+
};
|
|
804
|
+
model._folders.set(folderId, newFolder);
|
|
805
|
+
// Update parent's children array for proper traversal
|
|
806
|
+
folder.children = folder.children || [];
|
|
807
|
+
folder.children.push(folderId);
|
|
808
|
+
rebuildTree();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
label: 'Rename…',
|
|
814
|
+
action: () => {
|
|
815
|
+
const newName = window.prompt('Rename folder:', folder.name || 'Folder');
|
|
816
|
+
if (newName && newName.trim() && newName.trim() !== folder.name) {
|
|
817
|
+
folder.name = newName.trim();
|
|
818
|
+
rebuildTree();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
label: 'Delete…',
|
|
824
|
+
action: () => {
|
|
825
|
+
if (window.confirm(`Delete folder "${folder.name || 'Untitled Folder'}"? Elements will not be deleted.`)) {
|
|
826
|
+
const parentId = folder.parentId;
|
|
827
|
+
const allFolders = model.allFolders?.() || [];
|
|
828
|
+
for (const f of allFolders) {
|
|
829
|
+
if (f.parentId === folder.id) {
|
|
830
|
+
f.parentId = parentId || undefined;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
model._folders.delete(folder.id);
|
|
834
|
+
rebuildTree();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
];
|
|
839
|
+
|
|
840
|
+
contextMenuManager.show(event, items);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Show context menu for Views category (right-click on "Views", or one
|
|
845
|
+
* of its sub-folders): new views and canvases; on Views itself, new
|
|
846
|
+
* top-level folders too.
|
|
847
|
+
*/
|
|
848
|
+
function _showViewsCategoryMenu(event, { model, folder = false }) {
|
|
849
|
+
const items = [
|
|
850
|
+
callbacks.viewMenu ? callbacks.viewMenu.newViewItem() : {
|
|
851
|
+
label: 'New View',
|
|
852
|
+
action: () => {
|
|
853
|
+
const viewName = window.prompt('View name:', 'New View');
|
|
854
|
+
if (viewName && viewName.trim()) {
|
|
855
|
+
const viewId = `view-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
856
|
+
const newView = model.addView({
|
|
857
|
+
id: viewId,
|
|
858
|
+
name: viewName.trim(),
|
|
859
|
+
elementIds: [], // Start with empty canvas, not all elements
|
|
860
|
+
viewpoint: '',
|
|
861
|
+
viewpointMode: '',
|
|
862
|
+
layout: {},
|
|
863
|
+
edgeLayout: {},
|
|
864
|
+
folderLayout: {}
|
|
865
|
+
});
|
|
866
|
+
rebuildTree();
|
|
867
|
+
// Call the onNewView callback to create a tab for this view
|
|
868
|
+
callbacks.onNewView?.(newView);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
// With a CanvasTemplateMenu: a new canvas, blank or from a template, here too.
|
|
873
|
+
...(callbacks.templateMenu?.newCanvasItems() ?? []),
|
|
874
|
+
callbacks.viewMenu && !folder && { separator: true },
|
|
875
|
+
!folder && {
|
|
876
|
+
label: 'New Folder',
|
|
877
|
+
action: () => {
|
|
878
|
+
const folderName = window.prompt('Folder name:', 'New Folder');
|
|
879
|
+
if (folderName && folderName.trim()) {
|
|
880
|
+
const folderId = `folder-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
881
|
+
const newFolder = {
|
|
882
|
+
id: folderId,
|
|
883
|
+
name: folderName.trim(),
|
|
884
|
+
documentation: '',
|
|
885
|
+
parentId: undefined,
|
|
886
|
+
children: []
|
|
887
|
+
};
|
|
888
|
+
model._folders.set(folderId, newFolder);
|
|
889
|
+
rebuildTree();
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
].filter(Boolean);
|
|
894
|
+
|
|
895
|
+
contextMenuManager.show(event, items);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Show context menu for element (right-click on an element in tree).
|
|
900
|
+
* Uses ContextMenuManager with submenu support for "Move to Folder".
|
|
901
|
+
*/
|
|
902
|
+
function _showElementContextMenu(event, element, { model }) {
|
|
903
|
+
const folders = model?.folders?.() || [];
|
|
904
|
+
const currentFolder = model?.getElementFolder?.(element.id);
|
|
905
|
+
|
|
906
|
+
const own = callbacks.elementMenu?.(element) ?? [];
|
|
907
|
+
const items = [...own, ...(own.length ? [{ separator: true }] : [])];
|
|
908
|
+
|
|
909
|
+
// "Move to Folder" with submenu if folders exist
|
|
910
|
+
if (folders.length > 0) {
|
|
911
|
+
const folderItems = [];
|
|
912
|
+
|
|
913
|
+
for (const folder of folders) {
|
|
914
|
+
folderItems.push({
|
|
915
|
+
label: folder.name || 'Untitled Folder',
|
|
916
|
+
isActive: currentFolder === folder.id,
|
|
917
|
+
action: () => {
|
|
918
|
+
model.setElementFolder?.(element.id, folder.id);
|
|
919
|
+
rebuildTree();
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// "Remove from folder" option
|
|
925
|
+
folderItems.push({
|
|
926
|
+
label: '(No Folder)',
|
|
927
|
+
isActive: !currentFolder,
|
|
928
|
+
isSeparator: true,
|
|
929
|
+
action: () => {
|
|
930
|
+
model.setElementFolder?.(element.id, undefined);
|
|
931
|
+
rebuildTree();
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
items.push({
|
|
936
|
+
label: 'Move to Folder',
|
|
937
|
+
submenu: { items: folderItems }
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// No rule at the end (when there were no folders to move to).
|
|
942
|
+
if (items.at(-1)?.separator) items.pop();
|
|
943
|
+
if (items.length) contextMenuManager.show(event, items);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Initial render (deferred to ensure model is ready and filters are reset)
|
|
947
|
+
setTimeout(() => {
|
|
948
|
+
modelTree.setTypeFilter(null);
|
|
949
|
+
modelTree.setSpecializationFilter(null);
|
|
950
|
+
modelTree.setPropertyFilter(null);
|
|
951
|
+
rebuildTree();
|
|
952
|
+
}, 0);
|
|
953
|
+
panel.refresh = () => rebuildTree();
|
|
954
|
+
return panel;
|
|
955
|
+
}
|