@peter.prib/renderengine 0.2.6 → 0.2.10
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 +70 -9
- package/REFERENCE.md +303 -2
- package/dist/renderengine-workspace.umd.js +8851 -3305
- package/dist/renderengine.umd.js +5621 -1382
- package/examples/archimate.html +68 -66
- package/examples/data-model.html +251 -19
- 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/FormDialog.js +398 -385
- package/src/charting/ModelCheckDialog.js +63 -0
- package/src/charting/ModelChecker.js +26 -0
- package/src/charting/RowsField.js +127 -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/ClassificationLens.js +33 -0
- package/src/charting/datamodel/ClassificationScheme.js +98 -0
- package/src/charting/datamodel/DataModel.js +574 -12
- package/src/charting/datamodel/DataModelChecker.js +338 -0
- package/src/charting/datamodel/DataModelCompared.js +127 -0
- package/src/charting/datamodel/DataModelContext.js +40 -10
- package/src/charting/datamodel/DataModelEditCommand.js +47 -0
- package/src/charting/datamodel/DataModelFileFormat.js +33 -0
- package/src/charting/datamodel/DataModelPerspective.js +55 -0
- package/src/charting/datamodel/DataType.js +108 -0
- package/src/charting/datamodel/EnumerationType.js +64 -0
- package/src/charting/datamodel/ServiceDomain.js +186 -0
- package/src/charting/datamodel/ServiceLandscape.js +104 -0
- package/src/charting/datamodel/SqlDdl.js +80 -0
- package/src/charting/datamodel/SqlDdlFileFormat.js +23 -0
- package/src/charting/datamodel/StructuredType.js +63 -0
- package/src/charting/datamodel/index.js +21 -5
- package/src/charting/datamodel/naming/CamelCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/LowerWordsStyle.js +13 -0
- package/src/charting/datamodel/naming/NameStyle.js +57 -0
- package/src/charting/datamodel/naming/NamingStandard.js +132 -0
- package/src/charting/datamodel/naming/PascalCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/SnakeCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/TitleCaseStyle.js +25 -0
- package/src/charting/datamodel/naming/UpperSnakeStyle.js +13 -0
- package/src/charting/datamodel/naming/index.js +8 -0
- package/src/charting/datamodel/scorecard/CompletenessCategory.js +30 -0
- package/src/charting/datamodel/scorecard/ConsistencyCategory.js +33 -0
- package/src/charting/datamodel/scorecard/DefinitionsCategory.js +33 -0
- package/src/charting/datamodel/scorecard/ModelScorecard.js +113 -0
- package/src/charting/datamodel/scorecard/ReadabilityCategory.js +37 -0
- package/src/charting/datamodel/scorecard/ScorecardCategory.js +46 -0
- package/src/charting/datamodel/scorecard/StandardsCategory.js +29 -0
- package/src/charting/datamodel/scorecard/StructureCategory.js +32 -0
- package/src/charting/datamodel/scorecard/index.js +8 -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/DataModelDiagram.js +4 -11
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +19 -5
- package/src/charting/diagrams/EdgeGeometry.js +578 -553
- package/src/charting/diagrams/GraphDiagram.js +11 -1
- package/src/charting/diagrams/NodeGeometry.js +173 -158
- package/src/charting/glyphs/PersonGlyph.js +24 -23
- package/src/charting/toolbar/ToolbarSelect.js +6 -0
- package/src/datamodel.js +402 -4
- package/src/index.js +6 -4
- 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 +534 -33
- 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,413 +1,417 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WorkArea — the IDE-style editor area: several views open at once, in tab
|
|
3
|
-
* groups that can sit side by side.
|
|
4
|
-
*
|
|
5
|
-
* The whole point is the separation the plain `tabs`/`split` layout does not
|
|
6
|
-
* make on its own: a view being OPEN (it has a tab) is not the same as it being
|
|
7
|
-
* VISIBLE (its tab is selected in a group that is on screen). A shell that
|
|
8
|
-
* rebuilds a fixed layout per view can only ever show one; a work area lets you
|
|
9
|
-
* put the data model next to the notebook that queries it.
|
|
10
|
-
*
|
|
11
|
-
* Structure lives in `workAreaModel.js` as pure functions over plain JSON, and
|
|
12
|
-
* this class is the thin rendering shell over them:
|
|
13
|
-
*
|
|
14
|
-
* model (JSON) --createLayout--> live layout tree --serialize--> model
|
|
15
|
-
*
|
|
16
|
-
* The live tree is authoritative for the things the user adjusts directly —
|
|
17
|
-
* splitter positions, which tab is selected — so every structural change starts
|
|
18
|
-
* by reading them back out of it (`_syncFromTree`). Rebuilding the subtree
|
|
19
|
-
* afterwards is cheap and non-destructive: `PartNode.dispose()` only detaches a
|
|
20
|
-
* part and `Part.mount()` re-parents it, so a part keeps its DOM, its state and
|
|
21
|
-
* its bus subscriptions across the rebuild.
|
|
22
|
-
*
|
|
23
|
-
* Selecting a tab does NOT rebuild, though — the pane elements are what scroll,
|
|
24
|
-
* and replacing them would throw away scroll position on every tab click.
|
|
25
|
-
*/
|
|
26
|
-
import { createLayout } from './layout/index.js';
|
|
27
|
-
import { el } from './dom.js';
|
|
28
|
-
import {
|
|
29
|
-
emptyWorkArea, listGroups, listOpenParts, findPart,
|
|
30
|
-
openPart, closePart, focusPart,
|
|
31
|
-
} from './workAreaModel.js';
|
|
32
|
-
|
|
33
|
-
export class WorkArea {
|
|
34
|
-
/**
|
|
35
|
-
* @param {object} [options]
|
|
36
|
-
* @param {object} [options.area] serialized area to restore
|
|
37
|
-
* @param {string} [options.activeGroupId] group new views open into
|
|
38
|
-
* @param {Function} [options.onChange] ({ area, activeGroupId, reason }) — persist here
|
|
39
|
-
* @param {*} [options.placeholder] el() spec shown when nothing is open
|
|
40
|
-
* @param {Element} [options.actionsHost] filled with the focused view's `actions`
|
|
41
|
-
* @param {Element} [options.outlineHost] filled with the focused view's outline part
|
|
42
|
-
* @param {Function} [options.outlineFor] (partId) => outline partId | null
|
|
43
|
-
* @param {*} [options.outlinePlaceholder] el() spec when a view has no outline
|
|
44
|
-
* @param {Function} [options.beforeClose] (partId) => false to keep a tab its close button was
|
|
45
|
-
* pressed on (unsaved work, say); the handler may close it later
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
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
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (action.
|
|
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
|
-
this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
this.outlineHost.
|
|
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
|
-
this.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
this.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.
|
|
332
|
-
this.
|
|
333
|
-
this.
|
|
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
|
-
this.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
this.
|
|
374
|
-
this.
|
|
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
|
-
this
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
this.
|
|
404
|
-
this.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
this.
|
|
411
|
-
this.
|
|
412
|
-
|
|
413
|
-
|
|
1
|
+
/**
|
|
2
|
+
* WorkArea — the IDE-style editor area: several views open at once, in tab
|
|
3
|
+
* groups that can sit side by side.
|
|
4
|
+
*
|
|
5
|
+
* The whole point is the separation the plain `tabs`/`split` layout does not
|
|
6
|
+
* make on its own: a view being OPEN (it has a tab) is not the same as it being
|
|
7
|
+
* VISIBLE (its tab is selected in a group that is on screen). A shell that
|
|
8
|
+
* rebuilds a fixed layout per view can only ever show one; a work area lets you
|
|
9
|
+
* put the data model next to the notebook that queries it.
|
|
10
|
+
*
|
|
11
|
+
* Structure lives in `workAreaModel.js` as pure functions over plain JSON, and
|
|
12
|
+
* this class is the thin rendering shell over them:
|
|
13
|
+
*
|
|
14
|
+
* model (JSON) --createLayout--> live layout tree --serialize--> model
|
|
15
|
+
*
|
|
16
|
+
* The live tree is authoritative for the things the user adjusts directly —
|
|
17
|
+
* splitter positions, which tab is selected — so every structural change starts
|
|
18
|
+
* by reading them back out of it (`_syncFromTree`). Rebuilding the subtree
|
|
19
|
+
* afterwards is cheap and non-destructive: `PartNode.dispose()` only detaches a
|
|
20
|
+
* part and `Part.mount()` re-parents it, so a part keeps its DOM, its state and
|
|
21
|
+
* its bus subscriptions across the rebuild.
|
|
22
|
+
*
|
|
23
|
+
* Selecting a tab does NOT rebuild, though — the pane elements are what scroll,
|
|
24
|
+
* and replacing them would throw away scroll position on every tab click.
|
|
25
|
+
*/
|
|
26
|
+
import { createLayout } from './layout/index.js';
|
|
27
|
+
import { el } from './dom.js';
|
|
28
|
+
import {
|
|
29
|
+
emptyWorkArea, listGroups, listOpenParts, findPart,
|
|
30
|
+
openPart, closePart, focusPart,
|
|
31
|
+
} from './workAreaModel.js';
|
|
32
|
+
|
|
33
|
+
export class WorkArea {
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} [options]
|
|
36
|
+
* @param {object} [options.area] serialized area to restore
|
|
37
|
+
* @param {string} [options.activeGroupId] group new views open into
|
|
38
|
+
* @param {Function} [options.onChange] ({ area, activeGroupId, reason }) — persist here
|
|
39
|
+
* @param {*} [options.placeholder] el() spec shown when nothing is open
|
|
40
|
+
* @param {Element} [options.actionsHost] filled with the focused view's `actions`
|
|
41
|
+
* @param {Element} [options.outlineHost] filled with the focused view's outline part
|
|
42
|
+
* @param {Function} [options.outlineFor] (partId) => outline partId | null
|
|
43
|
+
* @param {*} [options.outlinePlaceholder] el() spec when a view has no outline
|
|
44
|
+
* @param {Function} [options.beforeClose] (partId) => false to keep a tab its close button was
|
|
45
|
+
* pressed on (unsaved work, say); the handler may close it later
|
|
46
|
+
* @param {Function} [options.tabMenu] (partId, event) => true when it showed a menu for that
|
|
47
|
+
* part's tab (right-clicked); the browser's own shows otherwise
|
|
48
|
+
*/
|
|
49
|
+
constructor({
|
|
50
|
+
area, activeGroupId, onChange, placeholder,
|
|
51
|
+
actionsHost, outlineHost, outlineFor, outlinePlaceholder, beforeClose, tabMenu,
|
|
52
|
+
} = {}) {
|
|
53
|
+
this.area = area ?? emptyWorkArea();
|
|
54
|
+
this.activeGroupId = activeGroupId ?? listGroups(this.area)[0]?.id ?? null;
|
|
55
|
+
this.onChange = onChange ?? null;
|
|
56
|
+
this.beforeClose = beforeClose ?? null;
|
|
57
|
+
this.tabMenu = tabMenu ?? null;
|
|
58
|
+
this.placeholder = placeholder ?? { cls: 'ws-workarea-empty', text: 'Nothing open' };
|
|
59
|
+
this.host = null;
|
|
60
|
+
this.ctx = null;
|
|
61
|
+
this._tree = null;
|
|
62
|
+
this._outerCtx = null;
|
|
63
|
+
|
|
64
|
+
// Frame slots. Both are filled from whichever view has FOCUS, which is
|
|
65
|
+
// the whole reason they live here: the work area is the only thing that
|
|
66
|
+
// knows what that is.
|
|
67
|
+
this.actionsHost = actionsHost ?? null;
|
|
68
|
+
this.outlineHost = outlineHost ?? null;
|
|
69
|
+
this.outlineFor = outlineFor ?? null;
|
|
70
|
+
this.outlinePlaceholder = outlinePlaceholder ?? null;
|
|
71
|
+
this._outlinePart = null;
|
|
72
|
+
this._slotPartId = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ── Queries ────────────────────────────────────────────────────────── */
|
|
76
|
+
|
|
77
|
+
/** Every open part id, in group order. */
|
|
78
|
+
openParts() { return listOpenParts(this.area); }
|
|
79
|
+
|
|
80
|
+
isOpen(partId) { return !!findPart(this.area, partId); }
|
|
81
|
+
|
|
82
|
+
/** The part id selected in each group — i.e. what is actually on screen. */
|
|
83
|
+
visibleParts() {
|
|
84
|
+
return listGroups(this.area)
|
|
85
|
+
.map(g => g.children?.[g.active ?? 0]?.partId)
|
|
86
|
+
.filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The view with focus: the selected tab of the active group. This is what a
|
|
91
|
+
* shell keys its chrome off — window title, breadcrumb, which toolbar
|
|
92
|
+
* belongs to what — and it is not the same as "the last thing opened",
|
|
93
|
+
* because clicking into another group changes it.
|
|
94
|
+
*/
|
|
95
|
+
activePart() {
|
|
96
|
+
const groups = listGroups(this.area);
|
|
97
|
+
const group = groups.find(g => g.id === this.activeGroupId) ?? groups[0];
|
|
98
|
+
return group?.children?.[group.active ?? 0]?.partId ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
groupCount() { return listGroups(this.area).length; }
|
|
102
|
+
|
|
103
|
+
/** `{ area, activeGroupId }` — hand straight to a store, pass back to the ctor. */
|
|
104
|
+
serialize() {
|
|
105
|
+
this._syncFromTree();
|
|
106
|
+
return { area: this.area, activeGroupId: this.activeGroupId };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ── Rendering ──────────────────────────────────────────────────────── */
|
|
110
|
+
|
|
111
|
+
render(host, ctx = {}) {
|
|
112
|
+
this.host = host;
|
|
113
|
+
this._outerCtx = ctx;
|
|
114
|
+
this.ctx = {
|
|
115
|
+
...ctx,
|
|
116
|
+
// Closing and group focus are decisions about the AREA, not about
|
|
117
|
+
// one tab strip, so they are intercepted here rather than left to
|
|
118
|
+
// TabsNode's own handling.
|
|
119
|
+
onTabClose: ({ partId }) => { if (partId && this.beforeClose?.(partId) !== false) this.close(partId); },
|
|
120
|
+
onTabMenu: ({ partId, event }) => !!(partId && this.tabMenu?.(partId, event)),
|
|
121
|
+
onGroupFocus: (id) => this._setActiveGroup(id),
|
|
122
|
+
onLayoutChange: () => this._changed('layout'),
|
|
123
|
+
};
|
|
124
|
+
this._build();
|
|
125
|
+
// Both slots explicitly: nothing has focus yet as far as they are
|
|
126
|
+
// concerned, so a restored area would otherwise come back with an empty
|
|
127
|
+
// action bar and an empty rail.
|
|
128
|
+
this._syncSlots({ actions: true, outline: true });
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_build() {
|
|
133
|
+
if (!this.host) return;
|
|
134
|
+
this._tree?.dispose();
|
|
135
|
+
this._tree = null;
|
|
136
|
+
this.host.textContent = '';
|
|
137
|
+
|
|
138
|
+
if (!listOpenParts(this.area).length) {
|
|
139
|
+
if (this.placeholder) this.host.appendChild(el(this.placeholder));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this._tree = createLayout(this.area);
|
|
144
|
+
this._tree.render(this.host, this.ctx);
|
|
145
|
+
this._tree.activate();
|
|
146
|
+
this._markActiveGroup();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ── Frame slots ────────────────────────────────────────────────────── */
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Point the action bar and outline rail at whatever now has focus.
|
|
153
|
+
*
|
|
154
|
+
* A slot is re-read when focus MOVED, or when the caller asks for that slot
|
|
155
|
+
* specifically. The two are asked for separately because their costs
|
|
156
|
+
* differ: re-reading actions is a handful of buttons, whereas re-showing an
|
|
157
|
+
* outline re-parents an element, which resets its scroll position — so a
|
|
158
|
+
* `refreshActions()` must not quietly scroll a long tree back to the top.
|
|
159
|
+
*/
|
|
160
|
+
_syncSlots({ actions = false, outline = false } = {}) {
|
|
161
|
+
const partId = this.activePart();
|
|
162
|
+
const moved = partId !== this._slotPartId;
|
|
163
|
+
if (!moved && !actions && !outline) return;
|
|
164
|
+
this._slotPartId = partId;
|
|
165
|
+
if (moved || actions) this._renderActions(partId);
|
|
166
|
+
if (moved || outline) this._showOutline(partId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_renderActions(partId) {
|
|
170
|
+
if (!this.actionsHost) return;
|
|
171
|
+
this.actionsHost.textContent = '';
|
|
172
|
+
const actions = partId ? (this.ctx?.registry?.get(partId)?.actions ?? []) : [];
|
|
173
|
+
this.actionsHost.classList.toggle('ws-actionbar-empty', !actions.length);
|
|
174
|
+
for (const action of actions) {
|
|
175
|
+
if (!action) continue;
|
|
176
|
+
// An already-built element is passed straight through. This is the
|
|
177
|
+
// AdapterPart bargain again: a panel that already has a working
|
|
178
|
+
// toolbar — with selects, labels and its own rebuild logic — joins
|
|
179
|
+
// the frame's action bar by handing it over, not by being rewritten
|
|
180
|
+
// into a button array that could not express it anyway.
|
|
181
|
+
if (typeof action.nodeType === 'number' || typeof action.appendChild === 'function') {
|
|
182
|
+
this.actionsHost.appendChild(action);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (action.separator) { this.actionsHost.appendChild(el({ cls: 'ws-action-sep' })); continue; }
|
|
186
|
+
this.actionsHost.appendChild(el({
|
|
187
|
+
tag: 'button',
|
|
188
|
+
cls: ['ws-action', action.cls],
|
|
189
|
+
text: action.label ?? '',
|
|
190
|
+
attrs: {
|
|
191
|
+
type: 'button',
|
|
192
|
+
title: action.title ?? action.label ?? null,
|
|
193
|
+
disabled: action.disabled || null,
|
|
194
|
+
},
|
|
195
|
+
on: { click: (e) => action.onClick?.(e, this) },
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Show the focused view's outline.
|
|
202
|
+
*
|
|
203
|
+
* An outline is an ordinary Part, resolved through the registry — not a new
|
|
204
|
+
* kind of thing. That is what gets it pooling, mount-once-then-re-parent and
|
|
205
|
+
* lifecycle for free: a tree keeps its expanded nodes and scroll position
|
|
206
|
+
* while you work in another view and come back.
|
|
207
|
+
*/
|
|
208
|
+
_showOutline(partId) {
|
|
209
|
+
if (!this.outlineHost) return;
|
|
210
|
+
const outlineId = partId && this.outlineFor ? this.outlineFor(partId) : null;
|
|
211
|
+
const next = outlineId ? this.ctx?.registry?.get(outlineId) : null;
|
|
212
|
+
|
|
213
|
+
// Detached by name rather than by clearing the host: the outgoing
|
|
214
|
+
// outline is a pooled part, and `detach()` is the call that says so.
|
|
215
|
+
this._outlinePart?.detach();
|
|
216
|
+
this._outlinePart = null;
|
|
217
|
+
this._placeholderEl?.remove();
|
|
218
|
+
this._placeholderEl = null;
|
|
219
|
+
this.outlineHost.classList.toggle('ws-outline-empty', !next);
|
|
220
|
+
|
|
221
|
+
if (!next) {
|
|
222
|
+
if (this.outlinePlaceholder) {
|
|
223
|
+
this._placeholderEl = this.outlineHost.appendChild(el(this.outlinePlaceholder));
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
next.mount(this.outlineHost, this.ctx);
|
|
229
|
+
this._outlinePart = next;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(`WorkArea: outline part "${outlineId}" failed to mount`, err);
|
|
232
|
+
this.outlineHost.classList.add('ws-outline-empty');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Re-read the focused view's actions — call when a panel's state changes. */
|
|
237
|
+
refreshActions() {
|
|
238
|
+
this._syncSlots({ actions: true });
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Re-read both slots. For the case a lazily-mounted view creates the very
|
|
244
|
+
* things the slots are meant to show *after* it took focus — the action bar
|
|
245
|
+
* and outline were resolved against a panel that did not exist yet.
|
|
246
|
+
*/
|
|
247
|
+
refreshSlots() {
|
|
248
|
+
this._syncSlots({ actions: true, outline: true });
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The outline part currently on show, if any. */
|
|
253
|
+
get outlinePart() { return this._outlinePart; }
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Read the live tree back into the model. Splitter drags and tab selections
|
|
257
|
+
* only ever happened to the DOM nodes; skipping this would silently undo
|
|
258
|
+
* them the next time anything opens or closes.
|
|
259
|
+
*/
|
|
260
|
+
_syncFromTree() {
|
|
261
|
+
if (this._tree) this.area = this._tree.serialize();
|
|
262
|
+
return this.area;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Show every tab's label as its part's title is now (a file name that changed, say). */
|
|
266
|
+
refreshLabels() {
|
|
267
|
+
const walk = (node) => {
|
|
268
|
+
if (node?.constructor?.type === 'tabs') node.refreshLabels?.();
|
|
269
|
+
for (const child of node?.children ?? []) walk(child);
|
|
270
|
+
};
|
|
271
|
+
if (this._tree) walk(this._tree);
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Live TabsNode for a group id, or null when nothing is rendered. */
|
|
276
|
+
_liveGroup(groupId) {
|
|
277
|
+
const walk = (node) => {
|
|
278
|
+
if (node?.id === groupId && node.constructor?.type === 'tabs') return node;
|
|
279
|
+
for (const child of node?.children ?? []) {
|
|
280
|
+
const hit = walk(child);
|
|
281
|
+
if (hit) return hit;
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
};
|
|
285
|
+
return this._tree ? walk(this._tree) : null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Tint the active group's tab strip, but only once there is more than one
|
|
290
|
+
* group — with a single group "where the next view opens" is not a question
|
|
291
|
+
* anyone is asking, and the accent would just be noise.
|
|
292
|
+
*/
|
|
293
|
+
_markActiveGroup() {
|
|
294
|
+
const many = this.groupCount() > 1;
|
|
295
|
+
for (const group of listGroups(this.area)) {
|
|
296
|
+
const live = this._liveGroup(group.id);
|
|
297
|
+
live?.el?.classList.toggle('ws-group-active', many && group.id === this.activeGroupId);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_setActiveGroup(id) {
|
|
302
|
+
if (!id || id === this.activeGroupId) return;
|
|
303
|
+
this.activeGroupId = id;
|
|
304
|
+
this._markActiveGroup();
|
|
305
|
+
this._changed('focus-group');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_changed(reason) {
|
|
309
|
+
this._syncFromTree();
|
|
310
|
+
// Before onChange, so a consumer reacting to the change (updating its
|
|
311
|
+
// own chrome, persisting) sees a frame that already agrees with focus.
|
|
312
|
+
this._syncSlots();
|
|
313
|
+
this.onChange?.({ area: this.area, activeGroupId: this.activeGroupId, reason });
|
|
314
|
+
this._outerCtx?.onLayoutChange?.();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* ── Commands ───────────────────────────────────────────────────────── */
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Open a view.
|
|
321
|
+
*
|
|
322
|
+
* `where`: `'tab'` (default) adds it to the active group; `'right'`,
|
|
323
|
+
* `'left'`, `'below'` or `'above'` opens a new group beside it — which is
|
|
324
|
+
* what actually gets two views on screen together.
|
|
325
|
+
*
|
|
326
|
+
* Already open anywhere -> focused, not duplicated: a part instance owns one
|
|
327
|
+
* DOM element, so a second tab for it would necessarily be empty.
|
|
328
|
+
*/
|
|
329
|
+
open(partId, { where = 'tab' } = {}) {
|
|
330
|
+
if (!partId) return this;
|
|
331
|
+
if (this.isOpen(partId)) return this.focus(partId);
|
|
332
|
+
this._syncFromTree();
|
|
333
|
+
const next = openPart(this.area, partId, { where, activeGroupId: this.activeGroupId });
|
|
334
|
+
this.area = next.area;
|
|
335
|
+
this.activeGroupId = next.activeGroupId;
|
|
336
|
+
this._build();
|
|
337
|
+
this._changed('open');
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Open beside the active group — the "split editor" gesture. */
|
|
342
|
+
openToSide(partId, side = 'right') { return this.open(partId, { where: side }); }
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Select an already-open view. Deliberately does not rebuild: it drives the
|
|
346
|
+
* live TabsNode, so pane scroll positions and the DOM survive a tab click.
|
|
347
|
+
*/
|
|
348
|
+
focus(partId) {
|
|
349
|
+
const found = findPart(this._syncFromTree(), partId);
|
|
350
|
+
if (!found) return this;
|
|
351
|
+
const live = this._liveGroup(found.group.id);
|
|
352
|
+
if (live) {
|
|
353
|
+
live.select(found.index, true);
|
|
354
|
+
this.activeGroupId = found.group.id;
|
|
355
|
+
this._markActiveGroup();
|
|
356
|
+
this._changed('focus');
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
const next = focusPart(this.area, partId, { activeGroupId: this.activeGroupId });
|
|
360
|
+
this.area = next.area;
|
|
361
|
+
this.activeGroupId = next.activeGroupId;
|
|
362
|
+
this._build();
|
|
363
|
+
this._changed('focus');
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Close a view. Its group disappears when it held the last tab, and a split
|
|
369
|
+
* left with one child collapses into it — so closing back down to one view
|
|
370
|
+
* leaves a plain tab strip rather than a split with an empty half.
|
|
371
|
+
*/
|
|
372
|
+
close(partId) {
|
|
373
|
+
this._syncFromTree();
|
|
374
|
+
const next = closePart(this.area, partId, { activeGroupId: this.activeGroupId });
|
|
375
|
+
if (!next.closed) return this;
|
|
376
|
+
this.area = next.area;
|
|
377
|
+
this.activeGroupId = next.activeGroupId;
|
|
378
|
+
this._build();
|
|
379
|
+
this._changed('close');
|
|
380
|
+
return this;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Close everything, leaving one empty group. */
|
|
384
|
+
closeAll() {
|
|
385
|
+
for (const partId of this.openParts()) this.close(partId);
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Replace the whole area — restoring a saved one, or switching perspective.
|
|
391
|
+
* Parts the new area does not reference are left in the registry pool, so
|
|
392
|
+
* switching back finds them as they were.
|
|
393
|
+
*/
|
|
394
|
+
setArea(area, activeGroupId) {
|
|
395
|
+
this.area = area ?? emptyWorkArea();
|
|
396
|
+
this.activeGroupId = activeGroupId ?? listGroups(this.area)[0]?.id ?? null;
|
|
397
|
+
this._build();
|
|
398
|
+
this._changed('set-area');
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
dispose() {
|
|
403
|
+
this._tree?.dispose();
|
|
404
|
+
this._tree = null;
|
|
405
|
+
// Detached, not disposed — the outline is a pooled part like any other,
|
|
406
|
+
// and destroying it here would take it out of the registry for good.
|
|
407
|
+
this._outlinePart?.detach();
|
|
408
|
+
this._outlinePart = null;
|
|
409
|
+
this._placeholderEl = null;
|
|
410
|
+
this._slotPartId = null;
|
|
411
|
+
if (this.host) this.host.textContent = '';
|
|
412
|
+
if (this.actionsHost) this.actionsHost.textContent = '';
|
|
413
|
+
if (this.outlineHost) this.outlineHost.textContent = '';
|
|
414
|
+
this.host = null;
|
|
415
|
+
this.ctx = null;
|
|
416
|
+
}
|
|
417
|
+
}
|