virtual-tree-canvas 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +181 -27
- package/docs/icon-catalog.html +67 -0
- package/docs/icon-catalog.json +392 -0
- package/docs/icon-catalog.md +171 -0
- package/examples/generate-tree.js +86 -0
- package/examples/reorder-table.html +33 -0
- package/examples/units.html +43 -0
- package/package.json +7 -3
- package/resources/icons/bicycle.svg +1 -0
- package/resources/icons/bird.svg +1 -0
- package/resources/icons/box.svg +1 -0
- package/resources/icons/building.svg +1 -0
- package/resources/icons/calendar.svg +1 -0
- package/resources/icons/car.svg +1 -0
- package/resources/icons/cat.svg +1 -0
- package/resources/icons/child.svg +1 -0
- package/resources/icons/clock.svg +1 -0
- package/resources/icons/cloud.svg +1 -0
- package/resources/icons/data-bus.svg +1 -0
- package/resources/icons/database.svg +1 -0
- package/resources/icons/dog.svg +1 -0
- package/resources/icons/drone.svg +1 -0
- package/resources/icons/fish.svg +1 -0
- package/resources/icons/flower.svg +1 -0
- package/resources/icons/fog.svg +1 -0
- package/resources/icons/globe.svg +1 -0
- package/resources/icons/grip.svg +1 -0
- package/resources/icons/heart.svg +1 -0
- package/resources/icons/helicopter.svg +1 -0
- package/resources/icons/insect.svg +1 -0
- package/resources/icons/leaf.svg +1 -0
- package/resources/icons/link.svg +1 -0
- package/resources/icons/lock.svg +1 -0
- package/resources/icons/moon.svg +1 -0
- package/resources/icons/network.svg +1 -0
- package/resources/icons/people.svg +1 -0
- package/resources/icons/person.svg +1 -0
- package/resources/icons/pin.svg +1 -0
- package/resources/icons/radar.svg +1 -0
- package/resources/icons/rain.svg +1 -0
- package/resources/icons/seedling.svg +1 -0
- package/resources/icons/sensor.svg +1 -0
- package/resources/icons/server.svg +1 -0
- package/resources/icons/ship.svg +1 -0
- package/resources/icons/snow.svg +1 -0
- package/resources/icons/star-filled.svg +1 -0
- package/resources/icons/star.svg +1 -0
- package/resources/icons/storm.svg +1 -0
- package/resources/icons/sun.svg +1 -0
- package/resources/icons/thermometer.svg +1 -0
- package/resources/icons/trash.svg +1 -0
- package/resources/icons/tree.svg +1 -0
- package/resources/icons/wind.svg +1 -0
- package/src/assets/icons.js +75 -25
- package/src/core/icon-registry.js +6 -8
- package/src/core/theme-manager.js +17 -4
- package/src/core/tree-cell-layout.js +9 -0
- package/src/core/tree-column-model.js +27 -3
- package/src/core/tree-model.js +20 -0
- package/src/core/types.js +1 -0
- package/src/core/view-options.js +37 -0
- package/src/core/visible-row-model.js +1 -1
- package/src/index.d.ts +209 -5
- package/src/index.js +3 -0
- package/src/input/index.js +1 -0
- package/src/input/row-actions.js +115 -0
- package/src/input/row-reorder-input.js +168 -0
- package/src/input/tree-filter-bar.js +55 -0
- package/src/input/tree-tooltip.js +55 -0
- package/src/input/tree-view-input-controller.js +10 -3
- package/src/inspector/cell-editor-manager.js +42 -13
- package/src/inspector/pane-layout.js +12 -0
- package/src/renderers/checkbox.js +14 -0
- package/src/renderers/tree-row-renderer.js +103 -62
- package/src/tree-view-controller.js +309 -57
- package/src/tree-view.js +183 -0
- package/src/view/styles.js +14 -0
- package/src/assets/icons/air.svg +0 -1
- package/src/assets/icons/radar.svg +0 -1
- /package/{src/assets → resources}/icons/aircraft.svg +0 -0
- /package/{src/assets/icons/bus.svg → resources/icons/bus-vehicle.svg} +0 -0
- /package/{src/assets → resources}/icons/control.svg +0 -0
- /package/{src/assets → resources}/icons/damage.svg +0 -0
- /package/{src/assets → resources}/icons/error.svg +0 -0
- /package/{src/assets → resources}/icons/folder.svg +0 -0
- /package/{src/assets → resources}/icons/ground.svg +0 -0
- /package/{src/assets → resources}/icons/inspector-array.svg +0 -0
- /package/{src/assets → resources}/icons/inspector-object.svg +0 -0
- /package/{src/assets → resources}/icons/inspector-value.svg +0 -0
- /package/{src/assets → resources}/icons/munition.svg +0 -0
- /package/{src/assets → resources}/icons/placeholder.svg +0 -0
- /package/{src/assets → resources}/icons/point.svg +0 -0
- /package/{src/assets → resources}/icons/situation.svg +0 -0
- /package/{src/assets → resources}/icons/space.svg +0 -0
- /package/{src/assets → resources}/icons/subsurface.svg +0 -0
- /package/{src/assets → resources}/icons/surface.svg +0 -0
- /package/{src/assets → resources}/icons/task.svg +0 -0
- /package/{src/assets → resources}/icons/track.svg +0 -0
- /package/{src/assets → resources}/icons/warning.svg +0 -0
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { treeCellLayout } from './core/tree-cell-layout.js';
|
|
2
|
+
import { inspectorPaneLayout } from './inspector/pane-layout.js';
|
|
3
|
+
import { RowActions } from './input/row-actions.js';
|
|
4
|
+
import { validateTreeViewOptions } from './core/view-options.js';
|
|
5
|
+
import { RowReorderInput } from './input/row-reorder-input.js';
|
|
6
|
+
import { TreeTooltip } from './input/tree-tooltip.js';
|
|
1
7
|
import {
|
|
2
8
|
EventEmitter,
|
|
3
9
|
IconRegistry,
|
|
@@ -14,12 +20,17 @@ import {
|
|
|
14
20
|
} from './core/index.js';
|
|
15
21
|
import { numericLayout } from './inspector/numeric-layout.js';
|
|
16
22
|
import { CellEditorManager } from './inspector/cell-editor-manager.js';
|
|
17
|
-
import { formatInspectorValue, getAtPath, inspectorColumns, inspectorPaneColumns, ModelInspectorBuilder, setAtPath } from './inspector/index.js';
|
|
23
|
+
import { formatInspectorValue, getAtPath, inspectorNodeId, inspectorColumns, inspectorPaneColumns, ModelInspectorBuilder, setAtPath } from './inspector/index.js';
|
|
18
24
|
import { TreeViewInputController } from './input/tree-view-input-controller.js';
|
|
19
25
|
import { TreeRowRenderer } from './renderers/index.js';
|
|
20
26
|
|
|
21
27
|
export class TreeViewController {
|
|
22
28
|
constructor(options = {}) {
|
|
29
|
+
validateTreeViewOptions(options);
|
|
30
|
+
this.layoutOverrides = { rowHeight: options.rowHeight, indentWidth: options.indentWidth };
|
|
31
|
+
this.rawNodes = new Map();
|
|
32
|
+
this.editable = options.editable !== false;
|
|
33
|
+
this.destroyed = false;
|
|
23
34
|
this.events = new EventEmitter();
|
|
24
35
|
this.model = new TreeModel();
|
|
25
36
|
this.expansion = new TreeExpansionManager(this.model);
|
|
@@ -37,11 +48,21 @@ export class TreeViewController {
|
|
|
37
48
|
this.viewport.renderInsetX = options.renderInsetX ?? 0;
|
|
38
49
|
this.viewport.renderInsetY = options.renderInsetY ?? 0;
|
|
39
50
|
this.columnModel = new TreeColumnModel(options.columns);
|
|
51
|
+
this.rowActions = options.rowActions ?? [];
|
|
52
|
+
this.rowDrag = options.rowDrag ?? null;
|
|
53
|
+
this.columnModel.setRowActions(this.rowActions.length);
|
|
54
|
+
this.rowReorder = Boolean(options.rowReorder);
|
|
55
|
+
this.columnModel.setRowReorder(this.rowReorder);
|
|
56
|
+
this.rowDrop = null;
|
|
57
|
+
this.autoRender = Boolean(options.autoRender);
|
|
58
|
+
this.renderFrame = null;
|
|
59
|
+
this.tooltipEnabled = Boolean(options.tooltip);
|
|
60
|
+
this.host = options.host ?? null;
|
|
40
61
|
this.searchIndex = new TreeSearchIndex();
|
|
41
62
|
this.selection = new TreeSelectionManager();
|
|
42
63
|
this.patchBatcher = new PatchBatcher();
|
|
43
|
-
this.themeManager = options.themeManager ?? new ThemeManager();
|
|
44
|
-
this.iconRegistry = options.iconRegistry ?? new IconRegistry();
|
|
64
|
+
this.themeManager = options.themeManager ?? new ThemeManager(options.theme);
|
|
65
|
+
this.iconRegistry = options.iconRegistry ?? new IconRegistry({ iconsBaseUrl: options.iconsBaseUrl });
|
|
45
66
|
this.renderer = options.renderer ?? new TreeRowRenderer({ themeManager: this.themeManager, iconRegistry: this.iconRegistry });
|
|
46
67
|
this.nativeScrollbars = options.nativeScrollbars !== false;
|
|
47
68
|
this.initialExpandDepth = options.initialExpandDepth ?? 1;
|
|
@@ -64,10 +85,17 @@ export class TreeViewController {
|
|
|
64
85
|
this.inspector = null;
|
|
65
86
|
this.inputController = null;
|
|
66
87
|
this.cellEditor = null;
|
|
88
|
+
const initialTheme = this.themeManager.get();
|
|
89
|
+
this.rowModel.rowHeight = this.viewport.rowHeight = options.rowHeight ?? initialTheme.rowHeight;
|
|
90
|
+
this.rowModel.indentWidth = this.viewport.indentWidth = options.indentWidth ?? initialTheme.indentWidth;
|
|
67
91
|
this.scene = this.createRenderScene();
|
|
92
|
+
this.renderer.setInvalidationHandler?.(() => this.requestRender(true));
|
|
68
93
|
|
|
94
|
+
for (const type of ['viewportchange', 'columnschange', 'sortchange', 'filterchange', 'themechange', 'layoutchange', 'expand', 'collapse', 'searchchange', 'nodehover', 'selectionchange', 'focuschange', 'rowreorder', 'rowdrag', 'rowreorderchange', 'modelchange']) {
|
|
95
|
+
this.events.on(type, () => this.requestRender());
|
|
96
|
+
}
|
|
69
97
|
if (options.canvas) this.initialize(options.canvas);
|
|
70
|
-
if (options.
|
|
98
|
+
if (options.canvas && (options.host || options.editable)) this.attachCellEditor({ host: options.host });
|
|
71
99
|
if (options.canvas && options.input !== false) this.attachInput();
|
|
72
100
|
}
|
|
73
101
|
|
|
@@ -78,9 +106,13 @@ export class TreeViewController {
|
|
|
78
106
|
this.canvas = canvas;
|
|
79
107
|
this.renderer.initialize(canvas);
|
|
80
108
|
this.renderer.setScene(this.scene);
|
|
109
|
+
this.actionOverlay?.destroy();
|
|
110
|
+
this.actionOverlay = canvas.ownerDocument?.createElement && canvas.parentElement ? new RowActions(this) : null;
|
|
81
111
|
this.#resizeToCanvasClientSize();
|
|
82
112
|
this.#observeCanvasSize();
|
|
83
113
|
this.#setupNativeScrollbars();
|
|
114
|
+
if (this.tooltipEnabled) this.attachTooltip({ host: this.host });
|
|
115
|
+
this.requestRender();
|
|
84
116
|
return this;
|
|
85
117
|
}
|
|
86
118
|
|
|
@@ -91,12 +123,48 @@ export class TreeViewController {
|
|
|
91
123
|
controller: this,
|
|
92
124
|
host: options.host,
|
|
93
125
|
});
|
|
126
|
+
this.cellEditor.setEditable(this.editable);
|
|
127
|
+
if (this.inputController) this.inputController.cellEditor = this.cellEditor;
|
|
94
128
|
return this.cellEditor;
|
|
95
129
|
}
|
|
96
130
|
|
|
131
|
+
setRowActions(actions) {
|
|
132
|
+
validateTreeViewOptions({ rowActions: actions });
|
|
133
|
+
this.rowActions = actions;
|
|
134
|
+
this.columnModel.setRowActions(actions.length);
|
|
135
|
+
this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
|
|
136
|
+
this.#syncContentSize();
|
|
137
|
+
this.requestRender();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setRowDrag(resolver) {
|
|
141
|
+
validateTreeViewOptions({ rowDrag: resolver });
|
|
142
|
+
this.rowReorderInput?.cancel();
|
|
143
|
+
this.rowDrag = resolver;
|
|
144
|
+
this.rowReorderInput?.syncMode();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
setEditable(enabled) {
|
|
148
|
+
validateTreeViewOptions({ editable: enabled });
|
|
149
|
+
if (this.editable === enabled) return;
|
|
150
|
+
this.editable = enabled;
|
|
151
|
+
if (!this.cellEditor && this.canvas) this.attachCellEditor({ host: this.host });
|
|
152
|
+
this.cellEditor?.setEditable(enabled);
|
|
153
|
+
this.events.emit('editablechange', { editable: enabled });
|
|
154
|
+
this.requestRender();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setInitialExpandDepth(depth) {
|
|
158
|
+
validateTreeViewOptions({ initialExpandDepth: depth });
|
|
159
|
+
this.initialExpandDepth = depth;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
97
163
|
attachInput(options = {}) {
|
|
98
164
|
if (!this.canvas) throw new Error('TreeViewController.attachInput requires an initialized canvas');
|
|
99
165
|
this.inputController?.destroy?.();
|
|
166
|
+
this.rowReorderInput?.destroy();
|
|
167
|
+
this.rowReorderInput = new RowReorderInput(this);
|
|
100
168
|
this.inputController = new TreeViewInputController({
|
|
101
169
|
controller: this,
|
|
102
170
|
cellEditor: options.cellEditor ?? this.cellEditor,
|
|
@@ -105,6 +173,13 @@ export class TreeViewController {
|
|
|
105
173
|
}
|
|
106
174
|
|
|
107
175
|
destroy() {
|
|
176
|
+
this.destroyed = true;
|
|
177
|
+
this.actionOverlay?.destroy();
|
|
178
|
+
this.rowReorderInput?.destroy();
|
|
179
|
+
this.rowReorderInput = null;
|
|
180
|
+
this.tooltip?.destroy();
|
|
181
|
+
this.tooltip = null;
|
|
182
|
+
this.cancelRender();
|
|
108
183
|
this.inputController?.destroy?.();
|
|
109
184
|
this.cellEditor?.destroy?.();
|
|
110
185
|
this.disableWorkers();
|
|
@@ -116,6 +191,7 @@ export class TreeViewController {
|
|
|
116
191
|
this.cellEditor = null;
|
|
117
192
|
this.renderer?.destroy?.();
|
|
118
193
|
this.canvas = null;
|
|
194
|
+
this.events.listeners.clear();
|
|
119
195
|
}
|
|
120
196
|
|
|
121
197
|
on(type, listener) {
|
|
@@ -126,23 +202,162 @@ export class TreeViewController {
|
|
|
126
202
|
this.events.off(type, listener);
|
|
127
203
|
}
|
|
128
204
|
|
|
129
|
-
setData(nodes) {
|
|
205
|
+
setData(nodes, options = {}) {
|
|
206
|
+
if (!Array.isArray(nodes)) throw new TypeError('nodes must be an array');
|
|
207
|
+
validateTreeViewOptions({ iconResolver: options.iconResolver ?? null });
|
|
208
|
+
const resolved = resolveNodeIcons(nodes, options.iconResolver);
|
|
209
|
+
this.closeEditor();
|
|
210
|
+
this.rawNodes = new Map(nodes.map(node => [node.id, node]));
|
|
211
|
+
nodes = resolved;
|
|
212
|
+
const preserveExpansion = !this.inspector;
|
|
130
213
|
this.inspector = null;
|
|
131
|
-
|
|
214
|
+
this.columnModel.setRowReorder(this.rowReorder);
|
|
215
|
+
if (this.columnModel.columns.some(column => column.kind?.startsWith('inspector'))) {
|
|
132
216
|
this.columnModel.setColumns([]);
|
|
133
217
|
}
|
|
218
|
+
this.#replaceTree(nodes, preserveExpansion);
|
|
219
|
+
this.searchIndex.rebuild(this.model);
|
|
220
|
+
this.workerClient?.setData(this.model.nodes);
|
|
221
|
+
this.#rebuildRows();
|
|
222
|
+
this.events.emit('datachange', {});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#replaceTree(nodes, preserveExpansion) {
|
|
226
|
+
const branches = preserveExpansion ? this.model.nodes.filter(node => this.expansion.hasChildren(node.id)).map(node => node.id) : [];
|
|
227
|
+
const expanded = new Set(this.model.expanded);
|
|
134
228
|
this.model.setTree(nodes);
|
|
135
229
|
this.expansion.expandToDepth(this.initialExpandDepth);
|
|
230
|
+
for (const id of branches) {
|
|
231
|
+
if (!this.model.index.getNode(id)) continue;
|
|
232
|
+
if (expanded.has(id)) this.model.expanded.add(id);
|
|
233
|
+
else this.model.expanded.delete(id);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
setIconResolver(resolver) {
|
|
238
|
+
validateTreeViewOptions({ iconResolver: resolver });
|
|
239
|
+
if (this.inspector) return;
|
|
240
|
+
const source = this.model.nodes.map(node => this.rawNodes.get(node.id) ?? node);
|
|
241
|
+
const nodes = resolveNodeIcons(source, resolver);
|
|
242
|
+
this.model.nodes = nodes;
|
|
243
|
+
this.model.index.rebuild(nodes);
|
|
136
244
|
this.searchIndex.rebuild(this.model);
|
|
245
|
+
this.workerClient?.setData(nodes);
|
|
246
|
+
this.#rebuildRows();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setHeaderFilter(enabled) {
|
|
250
|
+
if (this.headerFilter !== Boolean(enabled)) this.closeEditor();
|
|
251
|
+
this.headerFilter = Boolean(enabled);
|
|
252
|
+
this.requestRender();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
setInspectorOptions(options = {}) {
|
|
256
|
+
validateTreeViewOptions(options);
|
|
257
|
+
if (!this.inspector) return;
|
|
258
|
+
for (const key of ['filter', 'markUpdated']) {
|
|
259
|
+
if (options[key] !== undefined) this.inspector.options[key] = options[key];
|
|
260
|
+
}
|
|
261
|
+
this.requestRender();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setRowReorder(enabled) {
|
|
265
|
+
validateTreeViewOptions({ rowReorder: enabled });
|
|
266
|
+
this.rowReorder = Boolean(enabled);
|
|
267
|
+
this.rowDrop = null;
|
|
268
|
+
this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
|
|
269
|
+
this.#syncContentSize();
|
|
270
|
+
this.events.emit('rowreorderchange', { enabled: this.rowReorder });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
canReorderRows(nodeId) {
|
|
274
|
+
return this.rowReorder && !this.inspector && !this.columnModel.sort.direction && !this.rowModel.filterPredicate
|
|
275
|
+
&& (nodeId == null || this.model.index.getNode(nodeId)?.reorderable !== false);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
getRowOrder(parentId = null) {
|
|
279
|
+
return this.model.index.getChildren(parentId).slice();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
moveRow(nodeId, targetIndex, options = {}) {
|
|
283
|
+
if (!this.canReorderRows(nodeId)) return false;
|
|
284
|
+
const anchorId = this.rowModel.getRow(this.anchorRowIndex)?.nodeId;
|
|
285
|
+
const detail = this.model.moveNode(nodeId, targetIndex);
|
|
286
|
+
if (!detail) return false;
|
|
287
|
+
this.workerRevision++;
|
|
137
288
|
this.workerClient?.setData(this.model.nodes);
|
|
289
|
+
this.searchIndex.rebuild(this.model);
|
|
138
290
|
this.#rebuildRows();
|
|
291
|
+
this.anchorRowIndex = this.rowModel.getRowById(anchorId)?.rowIndex ?? null;
|
|
292
|
+
this.focusedId = nodeId;
|
|
293
|
+
this.selection.focused = nodeId;
|
|
294
|
+
this.cellEditor?.close?.();
|
|
295
|
+
this.scrollToNode(nodeId);
|
|
296
|
+
this.events.emit('rowreorder', { ...detail, source: options.source ?? 'api' });
|
|
297
|
+
this.events.emit('focuschange', { nodeId });
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
moveRowBy(nodeId, offset, options = {}) {
|
|
302
|
+
const node = this.model.index.getNode(nodeId);
|
|
303
|
+
if (!node || !Number.isInteger(offset)) return false;
|
|
304
|
+
const siblings = this.model.index.getChildren(node.parentId ?? null);
|
|
305
|
+
return this.moveRow(nodeId, siblings.indexOf(nodeId) + offset, options);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getRowDropTarget(nodeId, clientX, clientY) {
|
|
309
|
+
if (!this.canReorderRows(nodeId)) return null;
|
|
310
|
+
const x = clientX - this.viewport.renderInsetX;
|
|
311
|
+
const y = clientY - this.viewport.renderInsetY;
|
|
312
|
+
if (x < 0 || x >= this.viewport.contentViewportWidth || y < this.viewport.headerHeight
|
|
313
|
+
|| y >= this.viewport.headerHeight + this.viewport.rowViewportHeight) return null;
|
|
314
|
+
const contentY = y - this.viewport.headerHeight + this.viewport.scrollY;
|
|
315
|
+
const row = this.rowModel.getRow(Math.min(this.rowModel.rows.length - 1, Math.floor(contentY / this.rowModel.rowHeight)));
|
|
316
|
+
const source = this.model.index.getNode(nodeId);
|
|
317
|
+
const target = row && this.model.index.getNode(row.nodeId);
|
|
318
|
+
if (!source || !target || (source.parentId ?? null) !== (target.parentId ?? null)) return null;
|
|
319
|
+
const siblings = this.model.index.getChildren(source.parentId ?? null);
|
|
320
|
+
const after = contentY >= row.y + row.height / 2;
|
|
321
|
+
const from = siblings.indexOf(nodeId);
|
|
322
|
+
let index = siblings.indexOf(target.id) + Number(after);
|
|
323
|
+
if (index > from) index--;
|
|
324
|
+
return index === from ? null : { nodeId, targetId: target.id, index, y: row.y + (after ? row.height : 0) };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
setRowDrop(target) {
|
|
328
|
+
this.rowDrop = target;
|
|
329
|
+
this.events.emit('rowdrag', { target });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
requestRender(force = false) {
|
|
333
|
+
if (this.destroyed || (!this.autoRender && !force) || !this.canvas || this.renderFrame !== null) return;
|
|
334
|
+
const view = this.canvas.ownerDocument?.defaultView ?? globalThis;
|
|
335
|
+
const schedule = view.requestAnimationFrame?.bind(view) ?? (callback => setTimeout(callback, 0));
|
|
336
|
+
this.renderFrame = schedule(() => {
|
|
337
|
+
this.renderFrame = null;
|
|
338
|
+
if (this.canvas) this.render();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
cancelRender() {
|
|
343
|
+
if (this.renderFrame === null) return;
|
|
344
|
+
const view = this.canvas?.ownerDocument?.defaultView ?? globalThis;
|
|
345
|
+
(view.cancelAnimationFrame?.bind(view) ?? clearTimeout)(this.renderFrame);
|
|
346
|
+
this.renderFrame = null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
attachTooltip(options = {}) {
|
|
350
|
+
this.tooltip?.destroy();
|
|
351
|
+
this.tooltip = new TreeTooltip({ controller: this, host: options.host ?? this.canvas?.parentElement });
|
|
352
|
+
return this.tooltip;
|
|
139
353
|
}
|
|
140
354
|
|
|
141
355
|
setModel(model, meta = {}, options = {}) {
|
|
356
|
+
validateTreeViewOptions({ ...options, mode: 'inspector', model, meta, presentation: options.presentation ?? options.mode ?? 'table' });
|
|
357
|
+
this.columnModel.setRowReorder(false);
|
|
142
358
|
const builder = new ModelInspectorBuilder();
|
|
143
359
|
const presentation = options.presentation ?? options.mode ?? 'table';
|
|
144
|
-
const
|
|
145
|
-
const shouldPreserveExpansion = Boolean(this.inspector);
|
|
360
|
+
const preserveExpansion = Boolean(this.inspector);
|
|
146
361
|
const inspectorOptions = {
|
|
147
362
|
presentation,
|
|
148
363
|
flatRoot: Boolean(options.flatRoot),
|
|
@@ -151,17 +366,21 @@ export class TreeViewController {
|
|
|
151
366
|
markUpdated: options.markUpdated !== false,
|
|
152
367
|
};
|
|
153
368
|
const nodes = builder.build(model, meta, inspectorOptions);
|
|
369
|
+
const sameLayout = this.inspector?.presentation === presentation &&
|
|
370
|
+
nodes.length === this.model.nodes.length && nodes.every((node, index) => node.id === this.model.nodes[index].id);
|
|
371
|
+
if (!sameLayout) this.closeEditor();
|
|
372
|
+
else this.cellEditor?.reconcile(nodes);
|
|
373
|
+
this.rawNodes.clear();
|
|
154
374
|
this.inspector = { model, meta, builder, presentation, options: inspectorOptions };
|
|
155
|
-
this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
|
|
156
|
-
this
|
|
157
|
-
if (shouldPreserveExpansion) this.#restoreExpansion(previousExpanded);
|
|
158
|
-
else this.expansion.expandToDepth(this.initialExpandDepth);
|
|
375
|
+
if (!sameLayout) this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
|
|
376
|
+
this.#replaceTree(nodes, preserveExpansion);
|
|
159
377
|
this.searchIndex.rebuild(this.model);
|
|
160
378
|
this.#rebuildRows();
|
|
379
|
+
this.events.emit('datachange', {});
|
|
161
380
|
this.events.emit('modelchange', { model, meta, structural: true });
|
|
162
381
|
}
|
|
163
382
|
|
|
164
|
-
updateInspectorValue(nodeId, newValue, editorType = 'unknown') {
|
|
383
|
+
updateInspectorValue(nodeId, newValue, editorType = 'unknown', options = {}) {
|
|
165
384
|
const node = this.model.index.getNode(nodeId);
|
|
166
385
|
if (!node?.data?.inspector || !this.inspector) return false;
|
|
167
386
|
const data = node.data;
|
|
@@ -175,17 +394,25 @@ export class TreeViewController {
|
|
|
175
394
|
if (this.inspector.options.markUpdated !== false) {
|
|
176
395
|
this.setDynamicState([{ id: nodeId, state: { updated: true } }]);
|
|
177
396
|
}
|
|
178
|
-
const detail = { path: data.path, oldValue, newValue, nodeId, editorType };
|
|
179
|
-
|
|
180
|
-
|
|
397
|
+
const detail = { path: data.path, oldValue, newValue, nodeId, editorType, model: this.inspector.model, source: options.source ?? 'user' };
|
|
398
|
+
if (options.emit !== false) {
|
|
399
|
+
this.events.emit('valuechange', detail);
|
|
400
|
+
this.events.emit('modelchange', { ...detail });
|
|
401
|
+
}
|
|
402
|
+
this.requestRender();
|
|
181
403
|
return true;
|
|
182
404
|
}
|
|
183
405
|
|
|
406
|
+
setInspectorValue(path, value, options = {}) {
|
|
407
|
+
if (typeof path !== 'string') throw new TypeError('path must be a string');
|
|
408
|
+
return this.updateInspectorValue(inspectorNodeId(path === '$' ? '' : path), value, 'api', { ...options, source: 'api' });
|
|
409
|
+
}
|
|
410
|
+
|
|
184
411
|
triggerInspectorAction(nodeId) {
|
|
185
412
|
const node = this.model.index.getNode(nodeId);
|
|
186
413
|
if (!node?.data?.inspector) return false;
|
|
187
414
|
const label = node.data.meta?.button ?? node.label;
|
|
188
|
-
this.events.emit('action', { path: node.data.path, label, nodeId });
|
|
415
|
+
this.events.emit('action', { path: node.data.path, label, nodeId, model: this.inspector?.model, source: 'user' });
|
|
189
416
|
return true;
|
|
190
417
|
}
|
|
191
418
|
|
|
@@ -230,7 +457,11 @@ export class TreeViewController {
|
|
|
230
457
|
}
|
|
231
458
|
|
|
232
459
|
setColumns(columns) {
|
|
460
|
+
if (columns == null) columns = this.inspector ? (this.inspector.presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns()) : [];
|
|
461
|
+
if (!Array.isArray(columns)) throw new TypeError('columns must be an array or null');
|
|
462
|
+
this.closeEditor();
|
|
233
463
|
this.columnModel.setColumns(columns);
|
|
464
|
+
this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
|
|
234
465
|
this.#syncContentSize();
|
|
235
466
|
this.events.emit('columnschange', { columns: this.columnModel.columns });
|
|
236
467
|
}
|
|
@@ -281,6 +512,8 @@ export class TreeViewController {
|
|
|
281
512
|
}
|
|
282
513
|
|
|
283
514
|
setFilter(queryOrPredicate = '', options = {}) {
|
|
515
|
+
if (typeof queryOrPredicate !== 'string' && typeof queryOrPredicate !== 'function') throw new TypeError('filter must be a string or predicate');
|
|
516
|
+
if (this.cellEditor?.overlayKind !== 'filter') this.closeEditor();
|
|
284
517
|
this.filterQuery = typeof queryOrPredicate === 'string' ? queryOrPredicate : '';
|
|
285
518
|
this.filterOptions = typeof queryOrPredicate === 'string'
|
|
286
519
|
? { caseSensitive: Boolean(options.caseSensitive), wholeWord: Boolean(options.wholeWord) }
|
|
@@ -320,10 +553,12 @@ export class TreeViewController {
|
|
|
320
553
|
}
|
|
321
554
|
|
|
322
555
|
setDynamicState(patches) {
|
|
556
|
+
if (!Array.isArray(patches)) throw new TypeError('patches must be an array');
|
|
323
557
|
this.lastPatchCount = patches.length;
|
|
324
558
|
this.lastDirtyNodeCount = new Set(patches.map((patch) => patch.id)).size;
|
|
325
559
|
this.model.applyDynamicPatches(patches);
|
|
326
560
|
this.renderer.updateDynamicState(patches);
|
|
561
|
+
this.requestRender();
|
|
327
562
|
}
|
|
328
563
|
|
|
329
564
|
setTheme(theme) {
|
|
@@ -331,11 +566,10 @@ export class TreeViewController {
|
|
|
331
566
|
const beforeIndentWidth = this.rowModel.indentWidth;
|
|
332
567
|
this.themeManager.setTheme(theme);
|
|
333
568
|
const nextTheme = this.themeManager.get();
|
|
334
|
-
this.rowModel.rowHeight = nextTheme.rowHeight;
|
|
335
|
-
this.rowModel.indentWidth = nextTheme.indentWidth;
|
|
336
|
-
this.
|
|
337
|
-
|
|
338
|
-
if (beforeRowHeight !== nextTheme.rowHeight || beforeIndentWidth !== nextTheme.indentWidth) {
|
|
569
|
+
this.rowModel.rowHeight = this.viewport.rowHeight = this.layoutOverrides.rowHeight ?? nextTheme.rowHeight;
|
|
570
|
+
this.rowModel.indentWidth = this.viewport.indentWidth = this.layoutOverrides.indentWidth ?? nextTheme.indentWidth;
|
|
571
|
+
if (beforeRowHeight !== this.rowModel.rowHeight || beforeIndentWidth !== this.rowModel.indentWidth) {
|
|
572
|
+
this.closeEditor();
|
|
339
573
|
this.#rebuildRows();
|
|
340
574
|
}
|
|
341
575
|
this.#nativeScroll?.applyTheme?.(nextTheme);
|
|
@@ -343,13 +577,17 @@ export class TreeViewController {
|
|
|
343
577
|
}
|
|
344
578
|
|
|
345
579
|
setLayoutMetrics(options = {}) {
|
|
346
|
-
const rowHeight = options.rowHeight ?? this.rowModel.rowHeight;
|
|
347
|
-
const indentWidth = options.indentWidth ?? this.rowModel.indentWidth;
|
|
580
|
+
const rowHeight = Object.hasOwn(options, 'rowHeight') ? options.rowHeight ?? this.themeManager.get().rowHeight : this.rowModel.rowHeight;
|
|
581
|
+
const indentWidth = Object.hasOwn(options, 'indentWidth') ? options.indentWidth ?? this.themeManager.get().indentWidth : this.rowModel.indentWidth;
|
|
348
582
|
const headerHeight = options.headerHeight ?? this.viewport.headerHeight;
|
|
349
583
|
assertPositiveNumber(rowHeight, 'rowHeight');
|
|
350
584
|
assertPositiveNumber(indentWidth, 'indentWidth');
|
|
351
585
|
assertNonNegativeNumber(headerHeight, 'headerHeight');
|
|
586
|
+
for (const key of ['rowHeight', 'indentWidth']) {
|
|
587
|
+
if (Object.hasOwn(options, key)) this.layoutOverrides[key] = options[key];
|
|
588
|
+
}
|
|
352
589
|
const rowsChanged = this.rowModel.rowHeight !== rowHeight || this.rowModel.indentWidth !== indentWidth;
|
|
590
|
+
if (rowsChanged || headerHeight !== this.viewport.headerHeight) this.closeEditor();
|
|
353
591
|
this.rowModel.rowHeight = rowHeight;
|
|
354
592
|
this.rowModel.indentWidth = indentWidth;
|
|
355
593
|
this.viewport.rowHeight = rowHeight;
|
|
@@ -543,6 +781,7 @@ export class TreeViewController {
|
|
|
543
781
|
}
|
|
544
782
|
|
|
545
783
|
setSelection(ids) {
|
|
784
|
+
if (!Array.isArray(ids)) throw new TypeError('ids must be an array');
|
|
546
785
|
this.selection.selected.clear();
|
|
547
786
|
for (const id of ids) this.selection.selected.add(id);
|
|
548
787
|
this.selection.focused = ids[ids.length - 1] ?? null;
|
|
@@ -578,6 +817,7 @@ export class TreeViewController {
|
|
|
578
817
|
if (this.activeId === nodeId && this.activePart === part) return;
|
|
579
818
|
this.activeId = nodeId;
|
|
580
819
|
this.activePart = part;
|
|
820
|
+
this.requestRender();
|
|
581
821
|
}
|
|
582
822
|
|
|
583
823
|
clickNode(nodeId, event = {}) {
|
|
@@ -621,6 +861,11 @@ export class TreeViewController {
|
|
|
621
861
|
|
|
622
862
|
handleKey(event) {
|
|
623
863
|
if (!this.rowModel.rows.length) return false;
|
|
864
|
+
if (this.rowReorder && event.altKey && ['ArrowUp', 'ArrowDown'].includes(event.key)) {
|
|
865
|
+
const id = this.focusedId ?? this.rowModel.getRow(this.#focusedRowIndex())?.nodeId;
|
|
866
|
+
this.moveRowBy(id, event.key === 'ArrowUp' ? -1 : 1, { source: 'keyboard' });
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
624
869
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
|
|
625
870
|
this.selection.selected.clear();
|
|
626
871
|
for (const row of this.rowModel.rows) this.selection.selected.add(row.nodeId);
|
|
@@ -684,6 +929,7 @@ export class TreeViewController {
|
|
|
684
929
|
this.renderer.setScene(this.scene);
|
|
685
930
|
const renderStart = performance.now();
|
|
686
931
|
this.renderer.render(this.scene, time);
|
|
932
|
+
this.actionOverlay?.render(this.scene);
|
|
687
933
|
const renderMs = performance.now() - renderStart;
|
|
688
934
|
this.lastRenderedRows = this.renderer.renderedRows ?? 0;
|
|
689
935
|
return { scene: this.scene, sceneMs, renderMs, renderedRows: this.lastRenderedRows };
|
|
@@ -692,9 +938,14 @@ export class TreeViewController {
|
|
|
692
938
|
createRenderScene() {
|
|
693
939
|
const visibleRange = this.rowModel.getVisibleRange(this.viewport, 4);
|
|
694
940
|
return {
|
|
941
|
+
rowDrag: this.rowDrag,
|
|
942
|
+
rowReorder: this.rowReorder,
|
|
943
|
+
canReorderRows: this.canReorderRows(),
|
|
944
|
+
rowDrop: this.rowDrop,
|
|
945
|
+
childrenByParent: this.model.index.childrenByParent,
|
|
695
946
|
rows: this.rowModel.rows,
|
|
696
947
|
visibleRange,
|
|
697
|
-
stickyRows: this.rowModel.getStickyRows(this.viewport),
|
|
948
|
+
stickyRows: this.rowReorder ? [] : this.rowModel.getStickyRows(this.viewport),
|
|
698
949
|
viewport: this.viewport,
|
|
699
950
|
columns: this.columnModel.columns,
|
|
700
951
|
theme: this.themeManager.get(),
|
|
@@ -711,12 +962,13 @@ export class TreeViewController {
|
|
|
711
962
|
sortValues: this.sortValueSnapshot ? Array.from(this.sortValueSnapshot) : null,
|
|
712
963
|
inspectorPaneLabelEnd: this.#computeInspectorPaneLabelEnd(visibleRange),
|
|
713
964
|
filterQuery: this.filterQuery,
|
|
714
|
-
headerFilter: Boolean(this.inspector?.options?.filter),
|
|
965
|
+
headerFilter: this.headerFilter ?? Boolean(this.inspector?.options?.filter),
|
|
715
966
|
stats: this.getStats(),
|
|
716
967
|
};
|
|
717
968
|
}
|
|
718
969
|
|
|
719
970
|
closeEditor() {
|
|
971
|
+
this.cellEditor?.close?.();
|
|
720
972
|
this.events.emit('editorclose', {});
|
|
721
973
|
}
|
|
722
974
|
|
|
@@ -730,7 +982,7 @@ export class TreeViewController {
|
|
|
730
982
|
const resizeColumn = this.columnModel.getResizeHandleAt(x);
|
|
731
983
|
if (resizeColumn) return { area: 'header', part: 'resize', column: resizeColumn, x, y: localY };
|
|
732
984
|
const column = this.columnModel.getColumnAt(x);
|
|
733
|
-
if (column
|
|
985
|
+
if (column && (this.headerFilter ?? Boolean(this.inspector?.options?.filter)) && column === this.columnModel.columns.find(item => item.kind !== 'rowOrder' && item.id !== '__vtc_actions')) {
|
|
734
986
|
return { area: 'header', part: 'filter', column, x, y: localY };
|
|
735
987
|
}
|
|
736
988
|
return column ? { area: 'header', part: 'label', column, x, y: localY } : { area: 'header', part: 'header', column: null, x, y: localY };
|
|
@@ -744,6 +996,10 @@ export class TreeViewController {
|
|
|
744
996
|
const column = this.columnModel.getColumnAt(x);
|
|
745
997
|
if (!column) return { area: 'row', part: 'row', row, column: null, x, y: rowY };
|
|
746
998
|
|
|
999
|
+
if (column.kind === 'rowOrder') {
|
|
1000
|
+
const offset = x - column.x;
|
|
1001
|
+
return { area: 'row', part: offset < 24 ? 'rowDrag' : offset < 48 ? 'rowUp' : 'rowDown', row, column, x, y: rowY };
|
|
1002
|
+
}
|
|
747
1003
|
let part = 'cell';
|
|
748
1004
|
if (column.kind === 'inspectorPane') {
|
|
749
1005
|
const localX = x - column.x;
|
|
@@ -775,10 +1031,10 @@ export class TreeViewController {
|
|
|
775
1031
|
}
|
|
776
1032
|
} else if (column.kind === 'tree') {
|
|
777
1033
|
const localX = x - column.x;
|
|
778
|
-
const
|
|
779
|
-
if (localX >=
|
|
780
|
-
else if (localX >=
|
|
781
|
-
else if (localX >=
|
|
1034
|
+
const layout = treeCellLayout(row.depth, this.rowModel.indentWidth, this.model.index.childrenByParent.size > 1);
|
|
1035
|
+
if (row.hasChildren && localX >= layout.chevronX - 6 && localX <= layout.chevronX + 12) part = 'chevron';
|
|
1036
|
+
else if (localX >= layout.iconX - 1 && localX <= layout.iconX + 17) part = 'icon';
|
|
1037
|
+
else if (localX >= layout.labelX - 2) part = 'label';
|
|
782
1038
|
else part = 'cell';
|
|
783
1039
|
} else if (column.kind === 'inspectorValue') {
|
|
784
1040
|
part = this.#inspectorEditorPart(row, x - column.x, column.width);
|
|
@@ -856,7 +1112,7 @@ export class TreeViewController {
|
|
|
856
1112
|
width = Math.max(0, layout.editorWidth - 20);
|
|
857
1113
|
}
|
|
858
1114
|
} else if (hit.column.kind === 'tree') {
|
|
859
|
-
const labelX = hit.row.depth
|
|
1115
|
+
const { labelX } = treeCellLayout(hit.row.depth, this.rowModel.indentWidth, this.model.index.childrenByParent.size > 1);
|
|
860
1116
|
text = node.label ?? node.id;
|
|
861
1117
|
width = Math.max(0, hit.column.width - labelX - 6);
|
|
862
1118
|
} else if (hit.column.kind === 'inspectorValue') {
|
|
@@ -870,7 +1126,7 @@ export class TreeViewController {
|
|
|
870
1126
|
width = Math.max(0, hit.column.width - 20);
|
|
871
1127
|
} else if (typeof hit.column.value === 'function') {
|
|
872
1128
|
const value = hit.column.value(node, state);
|
|
873
|
-
text = value == null ? '' : String(value);
|
|
1129
|
+
text = hit.column.format ? hit.column.format(value, node, state) : value == null ? '' : String(value);
|
|
874
1130
|
width = Math.max(0, hit.column.width - 20);
|
|
875
1131
|
}
|
|
876
1132
|
|
|
@@ -880,6 +1136,7 @@ export class TreeViewController {
|
|
|
880
1136
|
}
|
|
881
1137
|
|
|
882
1138
|
resize(width, height) {
|
|
1139
|
+
if (width !== this.viewport.viewportWidth || height !== this.viewport.viewportHeight) this.closeEditor();
|
|
883
1140
|
this.viewport.resize(width, height);
|
|
884
1141
|
this.#fitInspectorPaneColumn(width);
|
|
885
1142
|
this.#syncContentSize();
|
|
@@ -921,6 +1178,7 @@ export class TreeViewController {
|
|
|
921
1178
|
this.#syncContentSize();
|
|
922
1179
|
this.#restoreScrollAnchor(scrollAnchor);
|
|
923
1180
|
this.rebuildCount++;
|
|
1181
|
+
this.requestRender();
|
|
924
1182
|
}
|
|
925
1183
|
|
|
926
1184
|
#syncContentSize() {
|
|
@@ -938,13 +1196,6 @@ export class TreeViewController {
|
|
|
938
1196
|
this.setDynamicState(this.patchBatcher.flush());
|
|
939
1197
|
}
|
|
940
1198
|
|
|
941
|
-
#restoreExpansion(expandedIds) {
|
|
942
|
-
this.model.expanded.clear();
|
|
943
|
-
for (const id of expandedIds) {
|
|
944
|
-
if (this.model.index.getNode(id) && this.expansion.hasChildren(id)) this.model.expanded.add(id);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
1199
|
#focusedRowIndex() {
|
|
949
1200
|
if (this.focusedId) {
|
|
950
1201
|
const row = this.rowModel.getRowById(this.focusedId);
|
|
@@ -960,7 +1211,10 @@ export class TreeViewController {
|
|
|
960
1211
|
const entry = entries[0];
|
|
961
1212
|
const width = Math.floor(entry?.contentRect?.width ?? 0);
|
|
962
1213
|
const height = Math.floor(entry?.contentRect?.height ?? 0);
|
|
963
|
-
if (width > 0 && height > 0)
|
|
1214
|
+
if (width > 0 && height > 0) {
|
|
1215
|
+
this.resize(width, height);
|
|
1216
|
+
if (this.autoRender) { this.cancelRender(); this.render(); }
|
|
1217
|
+
}
|
|
964
1218
|
});
|
|
965
1219
|
this.#resizeObserver.observe(this.canvas);
|
|
966
1220
|
}
|
|
@@ -1151,7 +1405,9 @@ export class TreeViewController {
|
|
|
1151
1405
|
}
|
|
1152
1406
|
|
|
1153
1407
|
#fitInspectorPaneColumn(width) {
|
|
1154
|
-
const
|
|
1408
|
+
const columns = this.columnModel.columns.filter(c => !['__vtc_actions', '__vtc_row_order'].includes(c.id));
|
|
1409
|
+
const column = columns.length === 1 ? columns[0] : null;
|
|
1410
|
+
width -= this.columnModel.columns.filter(c => !columns.includes(c)).reduce((sum, c) => sum + c.width, 0);
|
|
1155
1411
|
if (column?.kind !== 'inspectorPane' && column?.kind !== 'tree') return;
|
|
1156
1412
|
if (column.kind === 'inspectorPane' && this.inspector?.options?.presentation !== 'pane') return;
|
|
1157
1413
|
const nextWidth = Math.max(column.minWidth, Math.floor(width));
|
|
@@ -1356,21 +1612,6 @@ function assertNonNegativeNumber(value, name) {
|
|
|
1356
1612
|
}
|
|
1357
1613
|
}
|
|
1358
1614
|
|
|
1359
|
-
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
1360
|
-
const safeWidth = Math.max(1, width);
|
|
1361
|
-
const rightPadding = 14;
|
|
1362
|
-
if (editorType === 'checkbox') {
|
|
1363
|
-
const editorWidth = 34;
|
|
1364
|
-
return { editorLeft: Math.max(64, safeWidth - rightPadding - editorWidth), editorWidth };
|
|
1365
|
-
}
|
|
1366
|
-
const minEditor = Math.min(170, Math.max(96, safeWidth * 0.45));
|
|
1367
|
-
const minLabelEnd = Math.max(88, depth * indentWidth + 104);
|
|
1368
|
-
const preferredLeft = Math.max(minLabelEnd, labelEnd, safeWidth * 0.32);
|
|
1369
|
-
const maxLeft = Math.max(64, safeWidth - rightPadding - minEditor);
|
|
1370
|
-
const editorLeft = Math.max(64, Math.min(preferredLeft, maxLeft));
|
|
1371
|
-
const editorWidth = Math.max(56, safeWidth - rightPadding - editorLeft);
|
|
1372
|
-
return { editorLeft, editorWidth };
|
|
1373
|
-
}
|
|
1374
1615
|
|
|
1375
1616
|
function createDefaultArrayItem(meta = {}) {
|
|
1376
1617
|
if (meta.itemFactory) return meta.itemFactory();
|
|
@@ -1450,3 +1691,14 @@ function isProbablyTruncated(text, width) {
|
|
|
1450
1691
|
function now() {
|
|
1451
1692
|
return globalThis.performance?.now?.() ?? Date.now();
|
|
1452
1693
|
}
|
|
1694
|
+
|
|
1695
|
+
function resolveNodeIcons(nodes, resolver) {
|
|
1696
|
+
if (!resolver) return nodes;
|
|
1697
|
+
return nodes.map(node => {
|
|
1698
|
+
const visual = resolver(node);
|
|
1699
|
+
if (typeof visual === 'string') return { ...node, icon: visual };
|
|
1700
|
+
if (!visual || typeof visual !== 'object') return node;
|
|
1701
|
+
if ((visual.id !== undefined && visual.id !== node.id) || (visual.parentId !== undefined && visual.parentId !== node.parentId)) throw new TypeError('iconResolver cannot change node identity or parent');
|
|
1702
|
+
return { ...node, ...visual };
|
|
1703
|
+
});
|
|
1704
|
+
}
|