@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,304 +1,373 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CheckboxTreeSelector — generic tree with checkboxes and tri-state cascading logic.
|
|
3
|
-
*
|
|
4
|
-
* Reusable across any domain: build a tree of { id, label, children? } nodes and get back a
|
|
5
|
-
* DOM container + methods to track checked/unchecked ids. Tri-state: checking a branch checks
|
|
6
|
-
* all descendants; parent shows indeterminate when some (but not all) children are checked.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* const selector = new CheckboxTreeSelector({ doc: document, nodes: [...], checkedIds: new Set(['id1']) });
|
|
10
|
-
* const container = selector.build();
|
|
11
|
-
* parent.appendChild(container);
|
|
12
|
-
* selector.onChange(ids => { ... }); // fires on any checkbox click
|
|
13
|
-
*
|
|
14
|
-
* Node shape:
|
|
15
|
-
* { id: string, label: string, children?: Node[] }
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this._updateBranchStates();
|
|
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
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* CheckboxTreeSelector — generic tree with checkboxes and tri-state cascading logic.
|
|
3
|
+
*
|
|
4
|
+
* Reusable across any domain: build a tree of { id, label, children? } nodes and get back a
|
|
5
|
+
* DOM container + methods to track checked/unchecked ids. Tri-state: checking a branch checks
|
|
6
|
+
* all descendants; parent shows indeterminate when some (but not all) children are checked.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const selector = new CheckboxTreeSelector({ doc: document, nodes: [...], checkedIds: new Set(['id1']) });
|
|
10
|
+
* const container = selector.build();
|
|
11
|
+
* parent.appendChild(container);
|
|
12
|
+
* selector.onChange(ids => { ... }); // fires on any checkbox click
|
|
13
|
+
*
|
|
14
|
+
* Node shape:
|
|
15
|
+
* { id: string, label: string, children?: Node[] }
|
|
16
|
+
*
|
|
17
|
+
* Nothing ticked means everything is (no filter): `_isChecked`. OptInCheckboxTreeSelector
|
|
18
|
+
* is the other way about. A branch with more children than `searchAbove` starts folded, with
|
|
19
|
+
* a box that narrows its children to those containing the text typed; ticking the branch then
|
|
20
|
+
* ticks only those shown.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export class CheckboxTreeSelector {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {Document} options.doc
|
|
27
|
+
* @param {Array<Object>} [options.nodes]
|
|
28
|
+
* @param {Set<string>|null} [options.checkedIds]
|
|
29
|
+
* @param {number} [options.searchAbove] - A branch with more children than this is folded and searchable
|
|
30
|
+
*/
|
|
31
|
+
constructor({ doc, nodes = [], checkedIds = null, searchAbove = Infinity } = {}) {
|
|
32
|
+
this.doc = doc;
|
|
33
|
+
this.nodes = nodes;
|
|
34
|
+
this.searchAbove = searchAbove;
|
|
35
|
+
/** The search text of each searchable branch, and its children's rows, by its id. */
|
|
36
|
+
this._searches = new Map();
|
|
37
|
+
this._searchRows = new Map();
|
|
38
|
+
this._checkedIds = checkedIds || new Set(); // null (default) = everything checked
|
|
39
|
+
this._listeners = [];
|
|
40
|
+
this._checkboxesByNodeId = new Map(); // nodeId -> input[type=checkbox]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
build() {
|
|
44
|
+
const container = this.doc.createElement('div');
|
|
45
|
+
container.style.cssText = 'font-family: sans-serif; font-size: 13px; padding: 4px 0;';
|
|
46
|
+
|
|
47
|
+
for (const node of this.nodes) {
|
|
48
|
+
this._buildNode(node, container, 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return container;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Recursively build DOM for a node and its children.
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
_buildNode(node, parent, depth = 0) {
|
|
59
|
+
if (!node.children || node.children.length === 0) {
|
|
60
|
+
// Leaf: checkbox + label
|
|
61
|
+
this._buildLeaf(node, parent, depth);
|
|
62
|
+
} else {
|
|
63
|
+
// Branch: <details>/<summary> + checkbox
|
|
64
|
+
this._buildBranch(node, parent, depth);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a leaf node (no children).
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
_buildLeaf(node, parent, depth) {
|
|
73
|
+
const row = this.doc.createElement('div');
|
|
74
|
+
row.style.cssText = `display: flex; align-items: center; gap: 6px; margin-bottom: 4px; padding-left: ${depth * 16}px;`;
|
|
75
|
+
|
|
76
|
+
const checkbox = this.doc.createElement('input');
|
|
77
|
+
checkbox.type = 'checkbox';
|
|
78
|
+
checkbox.id = `checkbox-${node.id}`;
|
|
79
|
+
checkbox.checked = this._isChecked(node.id);
|
|
80
|
+
checkbox.style.cssText = 'flex-shrink: 0; cursor: pointer;';
|
|
81
|
+
row.dataset.label = String(node.label ?? '').toLowerCase();
|
|
82
|
+
|
|
83
|
+
checkbox.addEventListener('change', () => {
|
|
84
|
+
this._updateChecked(node.id, checkbox.checked, true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this._checkboxesByNodeId.set(node.id, checkbox);
|
|
88
|
+
|
|
89
|
+
const label = this.doc.createElement('label');
|
|
90
|
+
label.htmlFor = checkbox.id;
|
|
91
|
+
label.textContent = node.label;
|
|
92
|
+
label.style.cssText = 'flex: 1; cursor: pointer; user-select: none;';
|
|
93
|
+
|
|
94
|
+
row.appendChild(checkbox);
|
|
95
|
+
row.appendChild(label);
|
|
96
|
+
parent.appendChild(row);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build a branch node (has children, collapsible).
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
_buildBranch(node, parent, depth) {
|
|
104
|
+
const details = this.doc.createElement('details');
|
|
105
|
+
details.style.cssText = `margin-bottom: 4px; padding-left: ${depth * 16}px;`;
|
|
106
|
+
const searchable = this.isSearchable(node);
|
|
107
|
+
details.open = !searchable; // a long list starts folded, like a submenu
|
|
108
|
+
|
|
109
|
+
const summary = this.doc.createElement('summary');
|
|
110
|
+
summary.style.cssText = 'display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; padding: 2px 0;';
|
|
111
|
+
|
|
112
|
+
const checkbox = this.doc.createElement('input');
|
|
113
|
+
checkbox.type = 'checkbox';
|
|
114
|
+
checkbox.id = `checkbox-${node.id}`;
|
|
115
|
+
const isChecked = this._isChecked(node.id);
|
|
116
|
+
const allChildrenChecked = this._allChildrenChecked(node);
|
|
117
|
+
const someChildrenChecked = this._someChildrenChecked(node);
|
|
118
|
+
|
|
119
|
+
checkbox.checked = isChecked && allChildrenChecked;
|
|
120
|
+
checkbox.indeterminate = isChecked && someChildrenChecked && !allChildrenChecked;
|
|
121
|
+
checkbox.style.cssText = 'flex-shrink: 0; cursor: pointer;';
|
|
122
|
+
|
|
123
|
+
checkbox.addEventListener('change', () => {
|
|
124
|
+
this._updateBranch(node, checkbox.checked);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this._checkboxesByNodeId.set(node.id, checkbox);
|
|
128
|
+
|
|
129
|
+
const label = this.doc.createElement('span');
|
|
130
|
+
label.textContent = node.label;
|
|
131
|
+
label.style.cssText = 'flex: 1; cursor: pointer; font-weight: 500;';
|
|
132
|
+
|
|
133
|
+
summary.appendChild(checkbox);
|
|
134
|
+
summary.appendChild(label);
|
|
135
|
+
details.appendChild(summary);
|
|
136
|
+
|
|
137
|
+
const childrenContainer = this.doc.createElement('div');
|
|
138
|
+
if (searchable) details.appendChild(this._searchBox(node, depth + 1));
|
|
139
|
+
for (const child of node.children) {
|
|
140
|
+
this._buildNode(child, childrenContainer, depth + 1);
|
|
141
|
+
}
|
|
142
|
+
// Each child is one element of the container: its row, shown or hidden by the search.
|
|
143
|
+
if (searchable) this._searchRows.set(node.id, new Map(node.children.map((c, i) => [c.id, childrenContainer.children[i]])));
|
|
144
|
+
details.appendChild(childrenContainer);
|
|
145
|
+
|
|
146
|
+
parent.appendChild(details);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check/uncheck a single leaf node and notify.
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
_updateChecked(nodeId, checked, notify = true) {
|
|
154
|
+
if (checked) {
|
|
155
|
+
this._checkedIds.add(nodeId);
|
|
156
|
+
} else {
|
|
157
|
+
this._checkedIds.delete(nodeId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Update parent indeterminate state (walk up tree — not used for flat leaves, but cleaner)
|
|
161
|
+
this._updateBranchStates();
|
|
162
|
+
|
|
163
|
+
if (notify) {
|
|
164
|
+
this._notifyListeners();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check/uncheck a branch and all its descendants.
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
_updateBranch(node, checked) {
|
|
173
|
+
// A searched branch ticks only the children its search shows.
|
|
174
|
+
const allIds = this._searches.get(node.id)
|
|
175
|
+
? [node.id, ...this.matchingChildren(node).flatMap(c => this._getAllNodeIds(c))]
|
|
176
|
+
: this._getAllNodeIds(node);
|
|
177
|
+
if (checked) {
|
|
178
|
+
allIds.forEach(id => this._checkedIds.add(id));
|
|
179
|
+
} else {
|
|
180
|
+
allIds.forEach(id => this._checkedIds.delete(id));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Update DOM state for all affected checkboxes
|
|
184
|
+
this._updateAllCheckboxes();
|
|
185
|
+
this._updateBranchStates();
|
|
186
|
+
this._notifyListeners();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Collect all descendant node ids (including the node itself).
|
|
191
|
+
* @private
|
|
192
|
+
*/
|
|
193
|
+
_getAllNodeIds(node) {
|
|
194
|
+
const ids = [node.id];
|
|
195
|
+
if (node.children) {
|
|
196
|
+
for (const child of node.children) {
|
|
197
|
+
ids.push(...this._getAllNodeIds(child));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return ids;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if all children (direct and nested) are checked.
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
_allChildrenChecked(node) {
|
|
208
|
+
if (!node.children || node.children.length === 0) return true;
|
|
209
|
+
return node.children.every(child => {
|
|
210
|
+
const childId = child.id;
|
|
211
|
+
const hasDescendants = child.children && child.children.length > 0;
|
|
212
|
+
if (hasDescendants) {
|
|
213
|
+
return this._allChildrenChecked(child);
|
|
214
|
+
} else {
|
|
215
|
+
return this._isChecked(childId);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if at least one child (direct or nested) is checked.
|
|
222
|
+
* @private
|
|
223
|
+
*/
|
|
224
|
+
_someChildrenChecked(node) {
|
|
225
|
+
if (!node.children || node.children.length === 0) return false;
|
|
226
|
+
return node.children.some(child => {
|
|
227
|
+
const childId = child.id;
|
|
228
|
+
const hasDescendants = child.children && child.children.length > 0;
|
|
229
|
+
if (hasDescendants) {
|
|
230
|
+
return this._someChildrenChecked(child) || this._isChecked(childId);
|
|
231
|
+
} else {
|
|
232
|
+
return this._isChecked(childId);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Update checkbox states (indeterminate flags) for all branches.
|
|
239
|
+
* @private
|
|
240
|
+
*/
|
|
241
|
+
_updateBranchStates() {
|
|
242
|
+
for (const [nodeId, checkbox] of this._checkboxesByNodeId) {
|
|
243
|
+
const node = this._findNode(nodeId);
|
|
244
|
+
if (node && node.children && node.children.length > 0) {
|
|
245
|
+
const allChecked = this._allChildrenChecked(node);
|
|
246
|
+
const someChecked = this._someChildrenChecked(node);
|
|
247
|
+
checkbox.checked = allChecked;
|
|
248
|
+
checkbox.indeterminate = someChecked && !allChecked;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Find a node by id in the tree.
|
|
255
|
+
* @private
|
|
256
|
+
*/
|
|
257
|
+
_findNode(id, nodes = this.nodes) {
|
|
258
|
+
for (const node of nodes) {
|
|
259
|
+
if (node.id === id) return node;
|
|
260
|
+
if (node.children) {
|
|
261
|
+
const found = this._findNode(id, node.children);
|
|
262
|
+
if (found) return found;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get the set of currently-checked node ids.
|
|
270
|
+
*/
|
|
271
|
+
getCheckedIds() {
|
|
272
|
+
return new Set(this._checkedIds);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Set which nodes are checked (Set<string> | null = everything checked).
|
|
277
|
+
*/
|
|
278
|
+
setCheckedIds(ids) {
|
|
279
|
+
this._checkedIds = ids ? new Set(ids) : new Set();
|
|
280
|
+
// Update all checkbox states (checked + indeterminate) to reflect new state
|
|
281
|
+
this._updateAllCheckboxes();
|
|
282
|
+
this._updateBranchStates();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Update all checkboxes to match current _checkedIds state.
|
|
287
|
+
* @private
|
|
288
|
+
*/
|
|
289
|
+
_updateAllCheckboxes() {
|
|
290
|
+
for (const [nodeId, checkbox] of this._checkboxesByNodeId) {
|
|
291
|
+
const node = this._findNode(nodeId);
|
|
292
|
+
if (!node) continue;
|
|
293
|
+
if (node.children && node.children.length > 0) {
|
|
294
|
+
// Branch: set checked if all children checked, indeterminate if some checked
|
|
295
|
+
const allChecked = this._allChildrenChecked(node);
|
|
296
|
+
const someChecked = this._someChildrenChecked(node);
|
|
297
|
+
checkbox.checked = allChecked;
|
|
298
|
+
checkbox.indeterminate = someChecked && !allChecked;
|
|
299
|
+
} else {
|
|
300
|
+
// Leaf: set checked if in _checkedIds
|
|
301
|
+
checkbox.checked = this._isChecked(nodeId);
|
|
302
|
+
checkbox.indeterminate = false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Whether a node shows ticked: nothing ticked means everything is (no filter). @protected */
|
|
308
|
+
_isChecked(id) {
|
|
309
|
+
return this._checkedIds.size === 0 || this._checkedIds.has(id);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Whether a branch is folded and searchable: more children than `searchAbove`. */
|
|
313
|
+
isSearchable(node) {
|
|
314
|
+
return (node.children?.length ?? 0) > this.searchAbove;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** A branch's children its search shows: those whose label contains the text (all with none). */
|
|
318
|
+
matchingChildren(node) {
|
|
319
|
+
const text = (this._searches.get(node.id) ?? '').toLowerCase();
|
|
320
|
+
return text ? node.children.filter(c => String(c.label ?? '').toLowerCase().includes(text)) : node.children;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Narrow a branch to the children containing `text` ('' shows them all). */
|
|
324
|
+
search(nodeId, text) {
|
|
325
|
+
this._searches.set(nodeId, text);
|
|
326
|
+
const node = this._findNode(nodeId);
|
|
327
|
+
const rows = this._searchRows.get(nodeId);
|
|
328
|
+
if (!node || !rows) return;
|
|
329
|
+
const shown = new Set(this.matchingChildren(node).map(c => c.id));
|
|
330
|
+
for (const [id, row] of rows) row.style.display = shown.has(id) ? '' : 'none';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The box that narrows a long branch's children. @private */
|
|
334
|
+
_searchBox(node, depth) {
|
|
335
|
+
const input = this.doc.createElement('input');
|
|
336
|
+
Object.assign(input, { type: 'search', placeholder: `Find among ${node.children.length}…` });
|
|
337
|
+
input.style.cssText = `margin: 2px 0 4px ${depth * 16}px; width: calc(100% - ${depth * 16}px); box-sizing: border-box; font-size: 12px; padding: 2px 4px;`;
|
|
338
|
+
input.addEventListener('input', () => this.search(node.id, input.value.trim()));
|
|
339
|
+
return input;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Register a change listener. Called with Set<string> of checked ids.
|
|
344
|
+
*/
|
|
345
|
+
onChange(callback) {
|
|
346
|
+
this._listeners.push(callback);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Notify all listeners of the current checked state.
|
|
351
|
+
* @private
|
|
352
|
+
*/
|
|
353
|
+
_notifyListeners() {
|
|
354
|
+
const ids = this.getCheckedIds();
|
|
355
|
+
for (const cb of this._listeners) {
|
|
356
|
+
try {
|
|
357
|
+
cb(ids);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.warn('CheckboxTreeSelector listener error:', err);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* OptInCheckboxTreeSelector — nothing ticked means nothing chosen (no filter):
|
|
367
|
+
* each value is ticked to choose it, as a property filter wants.
|
|
368
|
+
*/
|
|
369
|
+
export class OptInCheckboxTreeSelector extends CheckboxTreeSelector {
|
|
370
|
+
_isChecked(id) {
|
|
371
|
+
return this._checkedIds.has(id);
|
|
372
|
+
}
|
|
373
|
+
}
|