@peter.prib/renderengine 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +219 -213
  2. package/REFERENCE.md +272 -6
  3. package/dist/renderengine-workspace.umd.js +11980 -7421
  4. package/dist/renderengine.umd.js +8991 -5784
  5. package/examples/archimate.html +68 -66
  6. package/examples/data-model.html +534 -0
  7. package/package.json +70 -68
  8. package/src/bian.js +182 -0
  9. package/src/charting/CheckboxTreeSelector.js +373 -304
  10. package/src/charting/DiagramEditor.js +53 -32
  11. package/src/charting/DiagramTypeRegistry.js +13 -3
  12. package/src/charting/ModelCheckDialog.js +63 -0
  13. package/src/charting/ModelChecker.js +26 -0
  14. package/src/charting/ViewTabStrip.js +94 -81
  15. package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
  16. package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
  17. package/src/charting/datamodel/BianLandscapeReader.js +185 -0
  18. package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
  19. package/src/charting/datamodel/DataModel.js +645 -0
  20. package/src/charting/datamodel/DataModelChecker.js +92 -0
  21. package/src/charting/datamodel/DataModelCompared.js +127 -0
  22. package/src/charting/datamodel/DataModelContext.js +108 -0
  23. package/src/charting/datamodel/DataModelFileFormat.js +33 -0
  24. package/src/charting/datamodel/DataModelPerspective.js +129 -0
  25. package/src/charting/datamodel/DataType.js +108 -0
  26. package/src/charting/datamodel/EnumerationType.js +51 -0
  27. package/src/charting/datamodel/LevelMapping.js +38 -0
  28. package/src/charting/datamodel/LevelSchema.js +41 -0
  29. package/src/charting/datamodel/ServiceDomain.js +177 -0
  30. package/src/charting/datamodel/ServiceLandscape.js +104 -0
  31. package/src/charting/datamodel/StructuredType.js +63 -0
  32. package/src/charting/datamodel/index.js +15 -0
  33. package/src/charting/diagramModel/MergeModelCommand.js +43 -37
  34. package/src/charting/diagramModel/OpenViewList.js +75 -0
  35. package/src/charting/diagramModel/ViewSetChecker.js +22 -34
  36. package/src/charting/diagrams/DataModelDiagram.js +160 -86
  37. package/src/charting/diagrams/DataModelRelationshipStrategy.js +94 -38
  38. package/src/charting/diagrams/EdgeGeometry.js +578 -553
  39. package/src/charting/diagrams/GraphDiagram.js +11 -1
  40. package/src/charting/diagrams/NodeGeometry.js +169 -158
  41. package/src/charting/diagrams/nesting.js +31 -0
  42. package/src/charting/glyphs/PersonGlyph.js +24 -23
  43. package/src/charting/modelTree/ModelTree.js +202 -164
  44. package/src/charting/modelTree/ModelTreePanel.js +36 -3
  45. package/src/charting/modelTree/TreeViewMode.js +131 -110
  46. package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
  47. package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
  48. package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
  49. package/src/datamodel.js +132 -122
  50. package/src/diagramCatalog.js +35 -30
  51. package/src/index.js +2666 -2659
  52. package/src/workspace/EditorShell.js +166 -67
  53. package/src/workspace/PanelLayout.js +56 -0
  54. package/src/workspace/index.js +1 -0
  55. package/src/workspace/layout/TabsNode.js +205 -191
  56. package/src/workspace/parts/ArchimateCanvasPart.js +10 -9
  57. package/src/workspace/parts/ChartPart.js +6 -1
  58. package/src/workspace/parts/DataModelPart.js +544 -0
  59. package/src/workspace/parts/DiagramEditorPart.js +2 -8
  60. package/src/workspace/parts/EditorPart.js +461 -407
  61. package/src/workspace/parts/ModelFile.js +216 -187
  62. package/src/workspace/parts/ModelsPart.js +483 -192
  63. package/src/workspace/storage/BackupFolder.js +145 -0
  64. package/src/workspace/storage/KeyValueStore.js +92 -0
  65. package/src/workspace/storage/ModelLibrary.js +65 -0
  66. package/src/workspace/workArea.js +417 -413
  67. package/src/yaml/YamlReader.js +372 -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
- export class CheckboxTreeSelector {
19
- constructor({ doc, nodes = [], checkedIds = null } = {}) {
20
- this.doc = doc;
21
- this.nodes = nodes;
22
- this._checkedIds = checkedIds || new Set(); // null (default) = everything checked
23
- this._listeners = [];
24
- this._checkboxesByNodeId = new Map(); // nodeId -> input[type=checkbox]
25
- }
26
-
27
- build() {
28
- const container = this.doc.createElement('div');
29
- container.style.cssText = 'font-family: sans-serif; font-size: 13px; padding: 4px 0;';
30
-
31
- for (const node of this.nodes) {
32
- this._buildNode(node, container, 0);
33
- }
34
-
35
- return container;
36
- }
37
-
38
- /**
39
- * Recursively build DOM for a node and its children.
40
- * @private
41
- */
42
- _buildNode(node, parent, depth = 0) {
43
- if (!node.children || node.children.length === 0) {
44
- // Leaf: checkbox + label
45
- this._buildLeaf(node, parent, depth);
46
- } else {
47
- // Branch: <details>/<summary> + checkbox
48
- this._buildBranch(node, parent, depth);
49
- }
50
- }
51
-
52
- /**
53
- * Build a leaf node (no children).
54
- * @private
55
- */
56
- _buildLeaf(node, parent, depth) {
57
- const row = this.doc.createElement('div');
58
- row.style.cssText = `display: flex; align-items: center; gap: 6px; margin-bottom: 4px; padding-left: ${depth * 16}px;`;
59
-
60
- const checkbox = this.doc.createElement('input');
61
- checkbox.type = 'checkbox';
62
- checkbox.id = `checkbox-${node.id}`;
63
- checkbox.checked = this._checkedIds.size === 0 || this._checkedIds.has(node.id);
64
- checkbox.style.cssText = 'flex-shrink: 0; cursor: pointer;';
65
-
66
- checkbox.addEventListener('change', () => {
67
- this._updateChecked(node.id, checkbox.checked, true);
68
- });
69
-
70
- this._checkboxesByNodeId.set(node.id, checkbox);
71
-
72
- const label = this.doc.createElement('label');
73
- label.htmlFor = checkbox.id;
74
- label.textContent = node.label;
75
- label.style.cssText = 'flex: 1; cursor: pointer; user-select: none;';
76
-
77
- row.appendChild(checkbox);
78
- row.appendChild(label);
79
- parent.appendChild(row);
80
- }
81
-
82
- /**
83
- * Build a branch node (has children, collapsible).
84
- * @private
85
- */
86
- _buildBranch(node, parent, depth) {
87
- const details = this.doc.createElement('details');
88
- details.style.cssText = `margin-bottom: 4px; padding-left: ${depth * 16}px;`;
89
- details.open = true;
90
-
91
- const summary = this.doc.createElement('summary');
92
- summary.style.cssText = 'display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; padding: 2px 0;';
93
-
94
- const checkbox = this.doc.createElement('input');
95
- checkbox.type = 'checkbox';
96
- checkbox.id = `checkbox-${node.id}`;
97
- const isChecked = this._checkedIds.size === 0 || this._checkedIds.has(node.id);
98
- const allChildrenChecked = this._allChildrenChecked(node);
99
- const someChildrenChecked = this._someChildrenChecked(node);
100
-
101
- checkbox.checked = isChecked && allChildrenChecked;
102
- checkbox.indeterminate = isChecked && someChildrenChecked && !allChildrenChecked;
103
- checkbox.style.cssText = 'flex-shrink: 0; cursor: pointer;';
104
-
105
- checkbox.addEventListener('change', () => {
106
- this._updateBranch(node, checkbox.checked);
107
- });
108
-
109
- this._checkboxesByNodeId.set(node.id, checkbox);
110
-
111
- const label = this.doc.createElement('span');
112
- label.textContent = node.label;
113
- label.style.cssText = 'flex: 1; cursor: pointer; font-weight: 500;';
114
-
115
- summary.appendChild(checkbox);
116
- summary.appendChild(label);
117
- details.appendChild(summary);
118
-
119
- const childrenContainer = this.doc.createElement('div');
120
- for (const child of node.children) {
121
- this._buildNode(child, childrenContainer, depth + 1);
122
- }
123
- details.appendChild(childrenContainer);
124
-
125
- parent.appendChild(details);
126
- }
127
-
128
- /**
129
- * Check/uncheck a single leaf node and notify.
130
- * @private
131
- */
132
- _updateChecked(nodeId, checked, notify = true) {
133
- if (checked) {
134
- this._checkedIds.add(nodeId);
135
- } else {
136
- this._checkedIds.delete(nodeId);
137
- }
138
-
139
- // Update parent indeterminate state (walk up tree — not used for flat leaves, but cleaner)
140
- this._updateBranchStates();
141
-
142
- if (notify) {
143
- this._notifyListeners();
144
- }
145
- }
146
-
147
- /**
148
- * Check/uncheck a branch and all its descendants.
149
- * @private
150
- */
151
- _updateBranch(node, checked) {
152
- const allIds = this._getAllNodeIds(node);
153
- if (checked) {
154
- allIds.forEach(id => this._checkedIds.add(id));
155
- } else {
156
- allIds.forEach(id => this._checkedIds.delete(id));
157
- }
158
-
159
- // Update DOM state for all affected checkboxes
160
- this._updateAllCheckboxes();
161
- this._updateBranchStates();
162
- this._notifyListeners();
163
- }
164
-
165
- /**
166
- * Collect all descendant node ids (including the node itself).
167
- * @private
168
- */
169
- _getAllNodeIds(node) {
170
- const ids = [node.id];
171
- if (node.children) {
172
- for (const child of node.children) {
173
- ids.push(...this._getAllNodeIds(child));
174
- }
175
- }
176
- return ids;
177
- }
178
-
179
- /**
180
- * Check if all children (direct and nested) are checked.
181
- * @private
182
- */
183
- _allChildrenChecked(node) {
184
- if (!node.children || node.children.length === 0) return true;
185
- return node.children.every(child => {
186
- const childId = child.id;
187
- const hasDescendants = child.children && child.children.length > 0;
188
- if (hasDescendants) {
189
- return this._allChildrenChecked(child);
190
- } else {
191
- return this._checkedIds.size === 0 || this._checkedIds.has(childId);
192
- }
193
- });
194
- }
195
-
196
- /**
197
- * Check if at least one child (direct or nested) is checked.
198
- * @private
199
- */
200
- _someChildrenChecked(node) {
201
- if (!node.children || node.children.length === 0) return false;
202
- return node.children.some(child => {
203
- const childId = child.id;
204
- const hasDescendants = child.children && child.children.length > 0;
205
- if (hasDescendants) {
206
- return this._someChildrenChecked(child) || (this._checkedIds.size === 0 || this._checkedIds.has(childId));
207
- } else {
208
- return this._checkedIds.size === 0 || this._checkedIds.has(childId);
209
- }
210
- });
211
- }
212
-
213
- /**
214
- * Update checkbox states (indeterminate flags) for all branches.
215
- * @private
216
- */
217
- _updateBranchStates() {
218
- for (const [nodeId, checkbox] of this._checkboxesByNodeId) {
219
- const node = this._findNode(nodeId);
220
- if (node && node.children && node.children.length > 0) {
221
- const allChecked = this._allChildrenChecked(node);
222
- const someChecked = this._someChildrenChecked(node);
223
- checkbox.checked = allChecked;
224
- checkbox.indeterminate = someChecked && !allChecked;
225
- }
226
- }
227
- }
228
-
229
- /**
230
- * Find a node by id in the tree.
231
- * @private
232
- */
233
- _findNode(id, nodes = this.nodes) {
234
- for (const node of nodes) {
235
- if (node.id === id) return node;
236
- if (node.children) {
237
- const found = this._findNode(id, node.children);
238
- if (found) return found;
239
- }
240
- }
241
- return null;
242
- }
243
-
244
- /**
245
- * Get the set of currently-checked node ids.
246
- */
247
- getCheckedIds() {
248
- return new Set(this._checkedIds);
249
- }
250
-
251
- /**
252
- * Set which nodes are checked (Set<string> | null = everything checked).
253
- */
254
- setCheckedIds(ids) {
255
- this._checkedIds = ids ? new Set(ids) : new Set();
256
- // Update all checkbox states (checked + indeterminate) to reflect new state
257
- this._updateAllCheckboxes();
258
- this._updateBranchStates();
259
- }
260
-
261
- /**
262
- * Update all checkboxes to match current _checkedIds state.
263
- * @private
264
- */
265
- _updateAllCheckboxes() {
266
- for (const [nodeId, checkbox] of this._checkboxesByNodeId) {
267
- const node = this._findNode(nodeId);
268
- if (!node) continue;
269
- if (node.children && node.children.length > 0) {
270
- // Branch: set checked if all children checked, indeterminate if some checked
271
- const allChecked = this._allChildrenChecked(node);
272
- const someChecked = this._someChildrenChecked(node);
273
- checkbox.checked = allChecked;
274
- checkbox.indeterminate = someChecked && !allChecked;
275
- } else {
276
- // Leaf: set checked if in _checkedIds
277
- checkbox.checked = this._checkedIds.size === 0 || this._checkedIds.has(nodeId);
278
- checkbox.indeterminate = false;
279
- }
280
- }
281
- }
282
-
283
- /**
284
- * Register a change listener. Called with Set<string> of checked ids.
285
- */
286
- onChange(callback) {
287
- this._listeners.push(callback);
288
- }
289
-
290
- /**
291
- * Notify all listeners of the current checked state.
292
- * @private
293
- */
294
- _notifyListeners() {
295
- const ids = this.getCheckedIds();
296
- for (const cb of this._listeners) {
297
- try {
298
- cb(ids);
299
- } catch (err) {
300
- console.warn('CheckboxTreeSelector listener error:', err);
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
+ }