@peter.prib/renderengine 0.2.5 → 0.2.6
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 +215 -213
- package/REFERENCE.md +94 -6
- package/dist/renderengine-workspace.umd.js +3254 -2089
- package/dist/renderengine.umd.js +3016 -2086
- package/examples/data-model.html +324 -0
- package/package.json +70 -68
- package/src/charting/CheckboxTreeSelector.js +373 -304
- package/src/charting/DiagramEditor.js +48 -6
- package/src/charting/DiagramTypeRegistry.js +5 -1
- package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
- package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
- package/src/charting/datamodel/DataModel.js +299 -0
- package/src/charting/datamodel/DataModelContext.js +108 -0
- package/src/charting/datamodel/DataModelPerspective.js +84 -0
- package/src/charting/datamodel/LevelMapping.js +38 -0
- package/src/charting/datamodel/LevelSchema.js +41 -0
- package/src/charting/datamodel/index.js +5 -0
- package/src/charting/diagrams/DataModelDiagram.js +160 -86
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +86 -38
- package/src/charting/diagrams/nesting.js +31 -0
- package/src/charting/modelTree/ModelTree.js +202 -164
- package/src/charting/modelTree/ModelTreePanel.js +36 -3
- package/src/charting/modelTree/TreeViewMode.js +131 -110
- package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
- package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
- package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
- package/src/datamodel.js +132 -122
- package/src/diagramCatalog.js +35 -30
- package/src/index.js +6 -1
- package/src/workspace/index.js +1 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +5 -2
- package/src/workspace/parts/ChartPart.js +6 -1
- package/src/workspace/parts/DataModelPart.js +234 -0
|
@@ -1,164 +1,202 @@
|
|
|
1
|
-
import { treeModeRegistry } from '../../treeModeCatalog.js';
|
|
2
|
-
import { buildTypeCategoryTree, buildPropertyTree } from './buildFilterTrees.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Pure-logic model tree orchestrator. Holds model, manages grouping mode,
|
|
6
|
-
* filter text, and selection. DOM shells (ModelTreePanel/AnalysisPanel)
|
|
7
|
-
* use this via getters/setters and onSelectionChange callbacks.
|
|
8
|
-
*/
|
|
9
|
-
export class ModelTree {
|
|
10
|
-
constructor(model, { mode = 'byLayer', viewId = null } = {}) {
|
|
11
|
-
this.model = model;
|
|
12
|
-
this._filterText = '';
|
|
13
|
-
this._typeFilter = null; // Set<string> | null (null = no filter = all types shown)
|
|
14
|
-
this._specializationFilter = null;
|
|
15
|
-
this._propertyFilter = null;
|
|
16
|
-
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for (const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
1
|
+
import { treeModeRegistry } from '../../treeModeCatalog.js';
|
|
2
|
+
import { buildTypeCategoryTree, buildPropertyTree } from './buildFilterTrees.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure-logic model tree orchestrator. Holds model, manages grouping mode,
|
|
6
|
+
* filter text, and selection. DOM shells (ModelTreePanel/AnalysisPanel)
|
|
7
|
+
* use this via getters/setters and onSelectionChange callbacks.
|
|
8
|
+
*/
|
|
9
|
+
export class ModelTree {
|
|
10
|
+
constructor(model, { mode = 'byLayer', viewId = null } = {}) {
|
|
11
|
+
this.model = model;
|
|
12
|
+
this._filterText = '';
|
|
13
|
+
this._typeFilter = null; // Set<string> | null (null = no filter = all types shown)
|
|
14
|
+
this._specializationFilter = null;
|
|
15
|
+
this._propertyFilter = null;
|
|
16
|
+
/** Whether the search text is looked for in descriptions too. */
|
|
17
|
+
this._inDescriptions = false;
|
|
18
|
+
/** What the search covers: 'model' (every element) or 'view' (those on the diagram in front). */
|
|
19
|
+
this._searchScope = 'model';
|
|
20
|
+
this._selectedId = null;
|
|
21
|
+
this._selectedKind = null;
|
|
22
|
+
this._onSelectionChange = [];
|
|
23
|
+
/** Which tree folders are open, by node id (TreeBuilder openState), kept while the tree is redrawn. */
|
|
24
|
+
this.openFolders = new Map();
|
|
25
|
+
this._selectedViewId = viewId || (model.views().length > 0 ? model.views()[0].id : null);
|
|
26
|
+
this.setMode(mode);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getSelectedView() {
|
|
30
|
+
return this._selectedViewId ? this.model.getView?.(this._selectedViewId) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setView(viewId) {
|
|
34
|
+
// null means "show all elements from the model"
|
|
35
|
+
if (viewId !== null) {
|
|
36
|
+
const view = this.model.getView?.(viewId);
|
|
37
|
+
if (!view) {
|
|
38
|
+
throw new Error(`Unknown view: "${viewId}"`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this._selectedViewId = viewId;
|
|
42
|
+
this._rebuild();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getAvailableViews() {
|
|
46
|
+
return this.model.views();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setMode(key) {
|
|
50
|
+
const entry = treeModeRegistry.get(key);
|
|
51
|
+
if (!entry) {
|
|
52
|
+
throw new Error(`Unknown tree mode: "${key}"`);
|
|
53
|
+
}
|
|
54
|
+
this._mode = new entry.TreeModeClass();
|
|
55
|
+
this._rebuild();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setFilter(text) {
|
|
59
|
+
this._filterText = text;
|
|
60
|
+
this._rebuild();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setTypeFilter(typeIds) {
|
|
64
|
+
this._typeFilter = typeIds instanceof Set ? typeIds : (typeIds ? new Set(typeIds) : null);
|
|
65
|
+
this._rebuild();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setSpecializationFilter(specIds) {
|
|
69
|
+
this._specializationFilter = specIds instanceof Set ? specIds : (specIds ? new Set(specIds) : null);
|
|
70
|
+
this._rebuild();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setPropertyFilter(propIds) {
|
|
74
|
+
this._propertyFilter = propIds instanceof Set ? propIds : (propIds ? new Set(propIds) : null);
|
|
75
|
+
this._rebuild();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Look for the search text in descriptions (documentation) too, or not. */
|
|
79
|
+
setSearchInDescriptions(on) {
|
|
80
|
+
this._inDescriptions = !!on;
|
|
81
|
+
this._rebuild();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getSearchInDescriptions() { return this._inDescriptions; }
|
|
85
|
+
|
|
86
|
+
/** What the search covers: 'model' (every element) or 'view' (only those on the diagram in front). */
|
|
87
|
+
setSearchScope(scope) {
|
|
88
|
+
if (!ModelTree.SEARCH_SCOPES.some(([key]) => key === scope)) throw new Error(`Unknown search scope: "${scope}"`);
|
|
89
|
+
this._searchScope = scope;
|
|
90
|
+
this._rebuild();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getSearchScope() { return this._searchScope; }
|
|
94
|
+
|
|
95
|
+
/** The filters on now (null: none), for a filter dialog to show. */
|
|
96
|
+
getTypeFilter() { return this._typeFilter; }
|
|
97
|
+
getSpecializationFilter() { return this._specializationFilter; }
|
|
98
|
+
getPropertyFilter() { return this._propertyFilter; }
|
|
99
|
+
|
|
100
|
+
select(id, kind = 'element') {
|
|
101
|
+
this._selectedId = id;
|
|
102
|
+
this._selectedKind = kind;
|
|
103
|
+
this._notifySelectionChange();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getSelectedId() { return this._selectedId; }
|
|
107
|
+
getSelectedKind() { return this._selectedKind; }
|
|
108
|
+
getFilterText() { return this._filterText; }
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get the type/specialization hierarchy tree for this model.
|
|
112
|
+
* Ready to pass to CheckboxTreeSelector.
|
|
113
|
+
*/
|
|
114
|
+
getAvailableTypeTree() {
|
|
115
|
+
return buildTypeCategoryTree(this.model);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the property name/value hierarchy tree for this model.
|
|
120
|
+
* Ready to pass to CheckboxTreeSelector.
|
|
121
|
+
*/
|
|
122
|
+
getAvailablePropertyTree() {
|
|
123
|
+
return buildPropertyTree(this.model);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
buildGroups() {
|
|
127
|
+
const view = this.getSelectedView();
|
|
128
|
+
const dataSource = view || this.model;
|
|
129
|
+
return this._mode.buildGroups(dataSource, this._filters());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onSelectionChange(callback) {
|
|
133
|
+
this._onSelectionChange.push(callback);
|
|
134
|
+
return () => {
|
|
135
|
+
const idx = this._onSelectionChange.indexOf(callback);
|
|
136
|
+
if (idx !== -1) this._onSelectionChange.splice(idx, 1);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_rebuild() {
|
|
141
|
+
// Consumers call buildGroups() as needed; listeners hear that the filters changed.
|
|
142
|
+
for (const cb of this._onFilterChange ?? []) cb(this);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Whether any filter is on (text, type, specialization or property). */
|
|
146
|
+
get filtering() {
|
|
147
|
+
return !!(this._filterText || this._typeFilter?.size || this._specializationFilter?.size || this._propertyFilter?.size);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** What every mode filters by. @private */
|
|
151
|
+
_filters() {
|
|
152
|
+
return {
|
|
153
|
+
filterText: this._filterText, typeIds: this._typeFilter, specializationIds: this._specializationFilter,
|
|
154
|
+
propertyIds: this._propertyFilter, inDescriptions: this._inDescriptions,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The model elements the tree's filters keep, or null when no filter is
|
|
160
|
+
* on: over the whole model, or with the 'view' scope only those on the
|
|
161
|
+
* diagram in front.
|
|
162
|
+
* @returns {Set<string>|null}
|
|
163
|
+
*/
|
|
164
|
+
matchingElementIds() {
|
|
165
|
+
if (!this.filtering) return null;
|
|
166
|
+
const groups = this._mode.buildGroups(this.model, this._filters());
|
|
167
|
+
const ids = new Set();
|
|
168
|
+
const walk = (items) => { for (const item of items ?? []) { if (item.kind === 'element') ids.add(item.id); walk(item.items); walk(item.children); } };
|
|
169
|
+
for (const group of groups) walk(group.items);
|
|
170
|
+
const onView = this._searchScope === 'view' ? this._elementIdsOnView() : null;
|
|
171
|
+
return onView ? new Set([...ids].filter(id => onView.has(id))) : ids;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The model elements drawn on the diagram in front, or null when there is none. @private */
|
|
175
|
+
_elementIdsOnView() {
|
|
176
|
+
const view = this.model.currentView;
|
|
177
|
+
if (!view?.elementInstances) return null;
|
|
178
|
+
return new Set([...view.elementInstances.values()].map(i => i.elementId).filter(Boolean));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Call `fn(tree)` whenever a filter changes. Returns an unsubscribe function. */
|
|
182
|
+
onFilterChange(fn) {
|
|
183
|
+
(this._onFilterChange ??= []).push(fn);
|
|
184
|
+
return () => { this._onFilterChange = this._onFilterChange.filter(f => f !== fn); };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_notifySelectionChange() {
|
|
188
|
+
const data = {
|
|
189
|
+
id: this._selectedId,
|
|
190
|
+
kind: this._selectedKind,
|
|
191
|
+
element: this._selectedKind === 'element' ? this.model.getElement(this._selectedId) : null,
|
|
192
|
+
relationship: this._selectedKind === 'relationship' ? this.model.getRelationship(this._selectedId) : null,
|
|
193
|
+
view: this._selectedKind === 'view' ? this.model.getView?.(this._selectedId) : null,
|
|
194
|
+
};
|
|
195
|
+
for (const cb of this._onSelectionChange) {
|
|
196
|
+
cb(data);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** What the search can cover: [key, label]. */
|
|
202
|
+
ModelTree.SEARCH_SCOPES = Object.freeze([['model', 'Whole model'], ['view', 'This diagram']]);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { treeModeRegistry } from '../../treeModeCatalog.js';
|
|
2
|
-
import { CheckboxTreeSelector } from '../CheckboxTreeSelector.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;
|
|
3
7
|
import { TreeBuilder } from './TreeBuilder.js';
|
|
4
8
|
import { typeGlyph } from '../archimate/typeGlyph.js';
|
|
5
9
|
import { ArchimateNotation } from '../../archimate.js';
|
|
@@ -125,6 +129,28 @@ export function buildModelTreePanel(doc, { modelTree, onSelectElement = null, on
|
|
|
125
129
|
box.style.cssText = 'width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid #ccc; border-radius: 3px; font-size: 13px;';
|
|
126
130
|
box.addEventListener('input', () => { modelTree.setFilter(box.value.trim()); showMatches(); });
|
|
127
131
|
panel.appendChild(box);
|
|
132
|
+
|
|
133
|
+
// What the search covers (the whole model, or the diagram in front) and whether it reads descriptions.
|
|
134
|
+
const options = doc.createElement('div');
|
|
135
|
+
options.style.cssText = 'display: flex; gap: 8px; align-items: center; font-size: 12px; color: #555;';
|
|
136
|
+
const scope = doc.createElement('select');
|
|
137
|
+
scope.setAttribute('aria-label', 'Search in');
|
|
138
|
+
scope.style.cssText = 'font-size: 12px; padding: 1px 2px;';
|
|
139
|
+
for (const [key, label] of ModelTree.SEARCH_SCOPES) {
|
|
140
|
+
const option = doc.createElement('option');
|
|
141
|
+
Object.assign(option, { value: key, textContent: label, selected: key === modelTree.getSearchScope?.() });
|
|
142
|
+
scope.appendChild(option);
|
|
143
|
+
}
|
|
144
|
+
scope.addEventListener('change', () => { modelTree.setSearchScope(scope.value); showMatches(); });
|
|
145
|
+
const described = doc.createElement('label');
|
|
146
|
+
described.style.cssText = 'display: flex; gap: 4px; align-items: center; cursor: pointer;';
|
|
147
|
+
const tick = doc.createElement('input');
|
|
148
|
+
Object.assign(tick, { type: 'checkbox', checked: !!modelTree.getSearchInDescriptions?.() });
|
|
149
|
+
tick.setAttribute('data-search-descriptions', '');
|
|
150
|
+
tick.addEventListener('change', () => { modelTree.setSearchInDescriptions(tick.checked); showMatches(); });
|
|
151
|
+
described.append(tick, 'Descriptions');
|
|
152
|
+
options.append('In:', scope, described);
|
|
153
|
+
panel.appendChild(options);
|
|
128
154
|
}
|
|
129
155
|
|
|
130
156
|
// Filter button opens modal dialog
|
|
@@ -168,7 +194,9 @@ export function buildModelTreePanel(doc, { modelTree, onSelectElement = null, on
|
|
|
168
194
|
// Rename labels for better UX (strip common prefixes)
|
|
169
195
|
const cleanTreeData = _cleanTreeLabels(typeTreeData);
|
|
170
196
|
|
|
171
|
-
|
|
197
|
+
// Showing the filter on now (none: every type ticked).
|
|
198
|
+
const typeOn = [...(modelTree.getTypeFilter?.() ?? []), ...(modelTree.getSpecializationFilter?.() ?? [])];
|
|
199
|
+
const typeSelector = new CheckboxTreeSelector({ doc, nodes: cleanTreeData, checkedIds: typeOn.length ? new Set(typeOn) : null });
|
|
172
200
|
const typeContainer = typeSelector.build();
|
|
173
201
|
typeContainer.style.cssText = 'padding-left: 8px; font-size: 12px;';
|
|
174
202
|
typeDetails.appendChild(typeContainer);
|
|
@@ -218,7 +246,12 @@ export function buildModelTreePanel(doc, { modelTree, onSelectElement = null, on
|
|
|
218
246
|
propSummary.textContent = '▼ Properties';
|
|
219
247
|
propDetails.appendChild(propSummary);
|
|
220
248
|
|
|
221
|
-
|
|
249
|
+
// Nothing ticked to start (no property filter); a value is ticked to keep what has it.
|
|
250
|
+
// A property with many values is folded, with a box to find one.
|
|
251
|
+
const propOn = modelTree.getPropertyFilter?.();
|
|
252
|
+
const propSelector = new OptInCheckboxTreeSelector({
|
|
253
|
+
doc, nodes: propTreeData, checkedIds: propOn ? new Set(propOn) : null, searchAbove: PROPERTY_VALUES_FOLDED_ABOVE,
|
|
254
|
+
});
|
|
222
255
|
const propContainer = propSelector.build();
|
|
223
256
|
propContainer.style.cssText = 'padding-left: 8px; font-size: 12px;';
|
|
224
257
|
propDetails.appendChild(propContainer);
|