virtual-tree-canvas 0.4.0 → 0.6.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +115 -28
  3. package/docs/icon-catalog.html +67 -0
  4. package/docs/icon-catalog.json +380 -0
  5. package/docs/icon-catalog.md +167 -0
  6. package/examples/generate-tree.js +86 -0
  7. package/examples/reorder-table.html +33 -0
  8. package/examples/units.html +43 -0
  9. package/package.json +7 -3
  10. package/resources/icons/bicycle.svg +1 -0
  11. package/resources/icons/bird.svg +1 -0
  12. package/resources/icons/box.svg +1 -0
  13. package/resources/icons/building.svg +1 -0
  14. package/resources/icons/calendar.svg +1 -0
  15. package/resources/icons/car.svg +1 -0
  16. package/resources/icons/cat.svg +1 -0
  17. package/resources/icons/child.svg +1 -0
  18. package/resources/icons/clock.svg +1 -0
  19. package/resources/icons/cloud.svg +1 -0
  20. package/resources/icons/data-bus.svg +1 -0
  21. package/resources/icons/database.svg +1 -0
  22. package/resources/icons/dog.svg +1 -0
  23. package/resources/icons/drone.svg +1 -0
  24. package/resources/icons/fish.svg +1 -0
  25. package/resources/icons/flower.svg +1 -0
  26. package/resources/icons/fog.svg +1 -0
  27. package/resources/icons/globe.svg +1 -0
  28. package/resources/icons/grip.svg +1 -0
  29. package/resources/icons/heart.svg +1 -0
  30. package/resources/icons/helicopter.svg +1 -0
  31. package/resources/icons/insect.svg +1 -0
  32. package/resources/icons/leaf.svg +1 -0
  33. package/resources/icons/link.svg +1 -0
  34. package/resources/icons/lock.svg +1 -0
  35. package/resources/icons/moon.svg +1 -0
  36. package/resources/icons/network.svg +1 -0
  37. package/resources/icons/people.svg +1 -0
  38. package/resources/icons/person.svg +1 -0
  39. package/resources/icons/pin.svg +1 -0
  40. package/resources/icons/radar.svg +1 -0
  41. package/resources/icons/rain.svg +1 -0
  42. package/resources/icons/seedling.svg +1 -0
  43. package/resources/icons/sensor.svg +1 -0
  44. package/resources/icons/server.svg +1 -0
  45. package/resources/icons/ship.svg +1 -0
  46. package/resources/icons/snow.svg +1 -0
  47. package/resources/icons/star.svg +1 -0
  48. package/resources/icons/storm.svg +1 -0
  49. package/resources/icons/sun.svg +1 -0
  50. package/resources/icons/thermometer.svg +1 -0
  51. package/resources/icons/tree.svg +1 -0
  52. package/resources/icons/wind.svg +1 -0
  53. package/src/assets/icons.js +73 -25
  54. package/src/core/icon-registry.js +6 -8
  55. package/src/core/theme-manager.js +15 -4
  56. package/src/core/tree-column-model.js +16 -3
  57. package/src/core/tree-model.js +20 -0
  58. package/src/core/view-options.js +30 -0
  59. package/src/index.d.ts +166 -5
  60. package/src/index.js +3 -0
  61. package/src/input/index.js +1 -0
  62. package/src/input/row-reorder-input.js +143 -0
  63. package/src/input/tree-filter-bar.js +55 -0
  64. package/src/input/tree-tooltip.js +55 -0
  65. package/src/input/tree-view-input-controller.js +5 -0
  66. package/src/inspector/cell-editor-manager.js +42 -22
  67. package/src/inspector/editor-resolver.js +6 -1
  68. package/src/inspector/numeric-layout.js +16 -0
  69. package/src/renderers/tree-row-renderer.js +79 -13
  70. package/src/tree-view-controller.js +261 -34
  71. package/src/tree-view.js +181 -0
  72. package/src/view/styles.js +14 -0
  73. package/src/assets/icons/air.svg +0 -1
  74. package/src/assets/icons/radar.svg +0 -1
  75. /package/{src/assets → resources}/icons/aircraft.svg +0 -0
  76. /package/{src/assets/icons/bus.svg → resources/icons/bus-vehicle.svg} +0 -0
  77. /package/{src/assets → resources}/icons/control.svg +0 -0
  78. /package/{src/assets → resources}/icons/damage.svg +0 -0
  79. /package/{src/assets → resources}/icons/error.svg +0 -0
  80. /package/{src/assets → resources}/icons/folder.svg +0 -0
  81. /package/{src/assets → resources}/icons/ground.svg +0 -0
  82. /package/{src/assets → resources}/icons/inspector-array.svg +0 -0
  83. /package/{src/assets → resources}/icons/inspector-object.svg +0 -0
  84. /package/{src/assets → resources}/icons/inspector-value.svg +0 -0
  85. /package/{src/assets → resources}/icons/munition.svg +0 -0
  86. /package/{src/assets → resources}/icons/placeholder.svg +0 -0
  87. /package/{src/assets → resources}/icons/point.svg +0 -0
  88. /package/{src/assets → resources}/icons/situation.svg +0 -0
  89. /package/{src/assets → resources}/icons/space.svg +0 -0
  90. /package/{src/assets → resources}/icons/subsurface.svg +0 -0
  91. /package/{src/assets → resources}/icons/surface.svg +0 -0
  92. /package/{src/assets → resources}/icons/task.svg +0 -0
  93. /package/{src/assets → resources}/icons/track.svg +0 -0
  94. /package/{src/assets → resources}/icons/warning.svg +0 -0
@@ -1,3 +1,6 @@
1
+ import { validateTreeViewOptions } from './core/view-options.js';
2
+ import { RowReorderInput } from './input/row-reorder-input.js';
3
+ import { TreeTooltip } from './input/tree-tooltip.js';
1
4
  import {
2
5
  EventEmitter,
3
6
  IconRegistry,
@@ -12,13 +15,19 @@ import {
12
15
  TreeViewViewport,
13
16
  VisibleRowModel,
14
17
  } from './core/index.js';
18
+ import { numericLayout } from './inspector/numeric-layout.js';
15
19
  import { CellEditorManager } from './inspector/cell-editor-manager.js';
16
- import { formatInspectorValue, getAtPath, inspectorColumns, inspectorPaneColumns, ModelInspectorBuilder, setAtPath } from './inspector/index.js';
20
+ import { formatInspectorValue, getAtPath, inspectorNodeId, inspectorColumns, inspectorPaneColumns, ModelInspectorBuilder, setAtPath } from './inspector/index.js';
17
21
  import { TreeViewInputController } from './input/tree-view-input-controller.js';
18
22
  import { TreeRowRenderer } from './renderers/index.js';
19
23
 
20
24
  export class TreeViewController {
21
25
  constructor(options = {}) {
26
+ validateTreeViewOptions(options);
27
+ this.layoutOverrides = { rowHeight: options.rowHeight, indentWidth: options.indentWidth };
28
+ this.rawNodes = new Map();
29
+ this.editable = options.editable !== false;
30
+ this.destroyed = false;
22
31
  this.events = new EventEmitter();
23
32
  this.model = new TreeModel();
24
33
  this.expansion = new TreeExpansionManager(this.model);
@@ -36,11 +45,18 @@ export class TreeViewController {
36
45
  this.viewport.renderInsetX = options.renderInsetX ?? 0;
37
46
  this.viewport.renderInsetY = options.renderInsetY ?? 0;
38
47
  this.columnModel = new TreeColumnModel(options.columns);
48
+ this.rowReorder = Boolean(options.rowReorder);
49
+ this.columnModel.setRowReorder(this.rowReorder);
50
+ this.rowDrop = null;
51
+ this.autoRender = Boolean(options.autoRender);
52
+ this.renderFrame = null;
53
+ this.tooltipEnabled = Boolean(options.tooltip);
54
+ this.host = options.host ?? null;
39
55
  this.searchIndex = new TreeSearchIndex();
40
56
  this.selection = new TreeSelectionManager();
41
57
  this.patchBatcher = new PatchBatcher();
42
- this.themeManager = options.themeManager ?? new ThemeManager();
43
- this.iconRegistry = options.iconRegistry ?? new IconRegistry();
58
+ this.themeManager = options.themeManager ?? new ThemeManager(options.theme);
59
+ this.iconRegistry = options.iconRegistry ?? new IconRegistry({ iconsBaseUrl: options.iconsBaseUrl });
44
60
  this.renderer = options.renderer ?? new TreeRowRenderer({ themeManager: this.themeManager, iconRegistry: this.iconRegistry });
45
61
  this.nativeScrollbars = options.nativeScrollbars !== false;
46
62
  this.initialExpandDepth = options.initialExpandDepth ?? 1;
@@ -63,10 +79,17 @@ export class TreeViewController {
63
79
  this.inspector = null;
64
80
  this.inputController = null;
65
81
  this.cellEditor = null;
82
+ const initialTheme = this.themeManager.get();
83
+ this.rowModel.rowHeight = this.viewport.rowHeight = options.rowHeight ?? initialTheme.rowHeight;
84
+ this.rowModel.indentWidth = this.viewport.indentWidth = options.indentWidth ?? initialTheme.indentWidth;
66
85
  this.scene = this.createRenderScene();
86
+ this.renderer.setInvalidationHandler?.(() => this.requestRender(true));
67
87
 
88
+ for (const type of ['viewportchange', 'columnschange', 'sortchange', 'filterchange', 'themechange', 'layoutchange', 'expand', 'collapse', 'searchchange', 'nodehover', 'selectionchange', 'focuschange', 'rowreorder', 'rowdrag', 'rowreorderchange', 'modelchange']) {
89
+ this.events.on(type, () => this.requestRender());
90
+ }
68
91
  if (options.canvas) this.initialize(options.canvas);
69
- if (options.editable !== false && (options.host || options.editable)) this.attachCellEditor({ host: options.host });
92
+ if (options.canvas && (options.host || options.editable)) this.attachCellEditor({ host: options.host });
70
93
  if (options.canvas && options.input !== false) this.attachInput();
71
94
  }
72
95
 
@@ -80,6 +103,8 @@ export class TreeViewController {
80
103
  this.#resizeToCanvasClientSize();
81
104
  this.#observeCanvasSize();
82
105
  this.#setupNativeScrollbars();
106
+ if (this.tooltipEnabled) this.attachTooltip({ host: this.host });
107
+ this.requestRender();
83
108
  return this;
84
109
  }
85
110
 
@@ -90,12 +115,32 @@ export class TreeViewController {
90
115
  controller: this,
91
116
  host: options.host,
92
117
  });
118
+ this.cellEditor.setEditable(this.editable);
119
+ if (this.inputController) this.inputController.cellEditor = this.cellEditor;
93
120
  return this.cellEditor;
94
121
  }
95
122
 
123
+ setEditable(enabled) {
124
+ validateTreeViewOptions({ editable: enabled });
125
+ if (this.editable === enabled) return;
126
+ this.editable = enabled;
127
+ if (!this.cellEditor && this.canvas) this.attachCellEditor({ host: this.host });
128
+ this.cellEditor?.setEditable(enabled);
129
+ this.events.emit('editablechange', { editable: enabled });
130
+ this.requestRender();
131
+ }
132
+
133
+ setInitialExpandDepth(depth) {
134
+ validateTreeViewOptions({ initialExpandDepth: depth });
135
+ this.initialExpandDepth = depth;
136
+ }
137
+
138
+
96
139
  attachInput(options = {}) {
97
140
  if (!this.canvas) throw new Error('TreeViewController.attachInput requires an initialized canvas');
98
141
  this.inputController?.destroy?.();
142
+ this.rowReorderInput?.destroy();
143
+ this.rowReorderInput = new RowReorderInput(this);
99
144
  this.inputController = new TreeViewInputController({
100
145
  controller: this,
101
146
  cellEditor: options.cellEditor ?? this.cellEditor,
@@ -104,6 +149,12 @@ export class TreeViewController {
104
149
  }
105
150
 
106
151
  destroy() {
152
+ this.destroyed = true;
153
+ this.rowReorderInput?.destroy();
154
+ this.rowReorderInput = null;
155
+ this.tooltip?.destroy();
156
+ this.tooltip = null;
157
+ this.cancelRender();
107
158
  this.inputController?.destroy?.();
108
159
  this.cellEditor?.destroy?.();
109
160
  this.disableWorkers();
@@ -115,6 +166,7 @@ export class TreeViewController {
115
166
  this.cellEditor = null;
116
167
  this.renderer?.destroy?.();
117
168
  this.canvas = null;
169
+ this.events.listeners.clear();
118
170
  }
119
171
 
120
172
  on(type, listener) {
@@ -125,9 +177,16 @@ export class TreeViewController {
125
177
  this.events.off(type, listener);
126
178
  }
127
179
 
128
- setData(nodes) {
180
+ setData(nodes, options = {}) {
181
+ if (!Array.isArray(nodes)) throw new TypeError('nodes must be an array');
182
+ validateTreeViewOptions({ iconResolver: options.iconResolver ?? null });
183
+ const resolved = resolveNodeIcons(nodes, options.iconResolver);
184
+ this.closeEditor();
185
+ this.rawNodes = new Map(nodes.map(node => [node.id, node]));
186
+ nodes = resolved;
129
187
  this.inspector = null;
130
- if (this.columnModel.columns.length === 1 && this.columnModel.columns[0]?.kind === 'inspectorPane') {
188
+ this.columnModel.setRowReorder(this.rowReorder);
189
+ if (this.columnModel.columns.some(column => column.kind?.startsWith('inspector'))) {
131
190
  this.columnModel.setColumns([]);
132
191
  }
133
192
  this.model.setTree(nodes);
@@ -135,13 +194,134 @@ export class TreeViewController {
135
194
  this.searchIndex.rebuild(this.model);
136
195
  this.workerClient?.setData(this.model.nodes);
137
196
  this.#rebuildRows();
197
+ this.events.emit('datachange', {});
198
+ }
199
+
200
+ setIconResolver(resolver) {
201
+ validateTreeViewOptions({ iconResolver: resolver });
202
+ if (this.inspector) return;
203
+ const source = this.model.nodes.map(node => this.rawNodes.get(node.id) ?? node);
204
+ const nodes = resolveNodeIcons(source, resolver);
205
+ this.model.nodes = nodes;
206
+ this.model.index.rebuild(nodes);
207
+ this.searchIndex.rebuild(this.model);
208
+ this.workerClient?.setData(nodes);
209
+ this.#rebuildRows();
210
+ }
211
+
212
+ setHeaderFilter(enabled) {
213
+ if (this.headerFilter !== Boolean(enabled)) this.closeEditor();
214
+ this.headerFilter = Boolean(enabled);
215
+ this.requestRender();
216
+ }
217
+
218
+ setInspectorOptions(options = {}) {
219
+ validateTreeViewOptions(options);
220
+ if (!this.inspector) return;
221
+ for (const key of ['filter', 'markUpdated']) {
222
+ if (options[key] !== undefined) this.inspector.options[key] = options[key];
223
+ }
224
+ this.requestRender();
225
+ }
226
+
227
+ setRowReorder(enabled) {
228
+ validateTreeViewOptions({ rowReorder: enabled });
229
+ this.rowReorder = Boolean(enabled);
230
+ this.rowDrop = null;
231
+ this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
232
+ this.#syncContentSize();
233
+ this.events.emit('rowreorderchange', { enabled: this.rowReorder });
234
+ }
235
+
236
+ canReorderRows() {
237
+ return this.rowReorder && !this.inspector && !this.columnModel.sort.direction && !this.rowModel.filterPredicate;
238
+ }
239
+
240
+ getRowOrder(parentId = null) {
241
+ return this.model.index.getChildren(parentId).slice();
242
+ }
243
+
244
+ moveRow(nodeId, targetIndex, options = {}) {
245
+ if (!this.canReorderRows()) return false;
246
+ const anchorId = this.rowModel.getRow(this.anchorRowIndex)?.nodeId;
247
+ const detail = this.model.moveNode(nodeId, targetIndex);
248
+ if (!detail) return false;
249
+ this.workerRevision++;
250
+ this.workerClient?.setData(this.model.nodes);
251
+ this.searchIndex.rebuild(this.model);
252
+ this.#rebuildRows();
253
+ this.anchorRowIndex = this.rowModel.getRowById(anchorId)?.rowIndex ?? null;
254
+ this.focusedId = nodeId;
255
+ this.selection.focused = nodeId;
256
+ this.cellEditor?.close?.();
257
+ this.scrollToNode(nodeId);
258
+ this.events.emit('rowreorder', { ...detail, source: options.source ?? 'api' });
259
+ this.events.emit('focuschange', { nodeId });
260
+ return true;
261
+ }
262
+
263
+ moveRowBy(nodeId, offset, options = {}) {
264
+ const node = this.model.index.getNode(nodeId);
265
+ if (!node || !Number.isInteger(offset)) return false;
266
+ const siblings = this.model.index.getChildren(node.parentId ?? null);
267
+ return this.moveRow(nodeId, siblings.indexOf(nodeId) + offset, options);
268
+ }
269
+
270
+ getRowDropTarget(nodeId, clientX, clientY) {
271
+ if (!this.canReorderRows()) return null;
272
+ const x = clientX - this.viewport.renderInsetX;
273
+ const y = clientY - this.viewport.renderInsetY;
274
+ if (x < 0 || x >= this.viewport.contentViewportWidth || y < this.viewport.headerHeight
275
+ || y >= this.viewport.headerHeight + this.viewport.rowViewportHeight) return null;
276
+ const contentY = y - this.viewport.headerHeight + this.viewport.scrollY;
277
+ const row = this.rowModel.getRow(Math.min(this.rowModel.rows.length - 1, Math.floor(contentY / this.rowModel.rowHeight)));
278
+ const source = this.model.index.getNode(nodeId);
279
+ const target = row && this.model.index.getNode(row.nodeId);
280
+ if (!source || !target || (source.parentId ?? null) !== (target.parentId ?? null)) return null;
281
+ const siblings = this.model.index.getChildren(source.parentId ?? null);
282
+ const after = contentY >= row.y + row.height / 2;
283
+ const from = siblings.indexOf(nodeId);
284
+ let index = siblings.indexOf(target.id) + Number(after);
285
+ if (index > from) index--;
286
+ return index === from ? null : { nodeId, targetId: target.id, index, y: row.y + (after ? row.height : 0) };
287
+ }
288
+
289
+ setRowDrop(target) {
290
+ this.rowDrop = target;
291
+ this.events.emit('rowdrag', { target });
292
+ }
293
+
294
+ requestRender(force = false) {
295
+ if (this.destroyed || (!this.autoRender && !force) || !this.canvas || this.renderFrame !== null) return;
296
+ const view = this.canvas.ownerDocument?.defaultView ?? globalThis;
297
+ const schedule = view.requestAnimationFrame?.bind(view) ?? (callback => setTimeout(callback, 0));
298
+ this.renderFrame = schedule(() => {
299
+ this.renderFrame = null;
300
+ if (this.canvas) this.render();
301
+ });
302
+ }
303
+
304
+ cancelRender() {
305
+ if (this.renderFrame === null) return;
306
+ const view = this.canvas?.ownerDocument?.defaultView ?? globalThis;
307
+ (view.cancelAnimationFrame?.bind(view) ?? clearTimeout)(this.renderFrame);
308
+ this.renderFrame = null;
309
+ }
310
+
311
+ attachTooltip(options = {}) {
312
+ this.tooltip?.destroy();
313
+ this.tooltip = new TreeTooltip({ controller: this, host: options.host ?? this.canvas?.parentElement });
314
+ return this.tooltip;
138
315
  }
139
316
 
140
317
  setModel(model, meta = {}, options = {}) {
318
+ validateTreeViewOptions({ ...options, mode: 'inspector', model, meta, presentation: options.presentation ?? options.mode ?? 'table' });
319
+ this.closeEditor();
320
+ this.columnModel.setRowReorder(false);
141
321
  const builder = new ModelInspectorBuilder();
142
322
  const presentation = options.presentation ?? options.mode ?? 'table';
143
323
  const previousExpanded = new Set(this.model.expanded);
144
- const shouldPreserveExpansion = Boolean(this.inspector);
324
+ const previousBranches = new Set(this.inspector ? this.model.nodes.filter(node => this.expansion.hasChildren(node.id)).map(node => node.id) : []);
145
325
  const inspectorOptions = {
146
326
  presentation,
147
327
  flatRoot: Boolean(options.flatRoot),
@@ -150,17 +330,23 @@ export class TreeViewController {
150
330
  markUpdated: options.markUpdated !== false,
151
331
  };
152
332
  const nodes = builder.build(model, meta, inspectorOptions);
333
+ this.rawNodes.clear();
153
334
  this.inspector = { model, meta, builder, presentation, options: inspectorOptions };
154
335
  this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
155
336
  this.model.setTree(nodes);
156
- if (shouldPreserveExpansion) this.#restoreExpansion(previousExpanded);
157
- else this.expansion.expandToDepth(this.initialExpandDepth);
337
+ this.expansion.expandToDepth(this.initialExpandDepth);
338
+ for (const id of previousBranches) {
339
+ if (!this.model.index.getNode(id)) continue;
340
+ if (previousExpanded.has(id)) this.model.expanded.add(id);
341
+ else this.model.expanded.delete(id);
342
+ }
158
343
  this.searchIndex.rebuild(this.model);
159
344
  this.#rebuildRows();
345
+ this.events.emit('datachange', {});
160
346
  this.events.emit('modelchange', { model, meta, structural: true });
161
347
  }
162
348
 
163
- updateInspectorValue(nodeId, newValue, editorType = 'unknown') {
349
+ updateInspectorValue(nodeId, newValue, editorType = 'unknown', options = {}) {
164
350
  const node = this.model.index.getNode(nodeId);
165
351
  if (!node?.data?.inspector || !this.inspector) return false;
166
352
  const data = node.data;
@@ -174,17 +360,25 @@ export class TreeViewController {
174
360
  if (this.inspector.options.markUpdated !== false) {
175
361
  this.setDynamicState([{ id: nodeId, state: { updated: true } }]);
176
362
  }
177
- const detail = { path: data.path, oldValue, newValue, nodeId, editorType };
178
- this.events.emit('valuechange', detail);
179
- this.events.emit('modelchange', { model: this.inspector.model, path: data.path, oldValue, newValue, nodeId });
363
+ const detail = { path: data.path, oldValue, newValue, nodeId, editorType, model: this.inspector.model, source: options.source ?? 'user' };
364
+ if (options.emit !== false) {
365
+ this.events.emit('valuechange', detail);
366
+ this.events.emit('modelchange', { ...detail });
367
+ }
368
+ this.requestRender();
180
369
  return true;
181
370
  }
182
371
 
372
+ setInspectorValue(path, value, options = {}) {
373
+ if (typeof path !== 'string') throw new TypeError('path must be a string');
374
+ return this.updateInspectorValue(inspectorNodeId(path === '$' ? '' : path), value, 'api', { ...options, source: 'api' });
375
+ }
376
+
183
377
  triggerInspectorAction(nodeId) {
184
378
  const node = this.model.index.getNode(nodeId);
185
379
  if (!node?.data?.inspector) return false;
186
380
  const label = node.data.meta?.button ?? node.label;
187
- this.events.emit('action', { path: node.data.path, label, nodeId });
381
+ this.events.emit('action', { path: node.data.path, label, nodeId, model: this.inspector?.model, source: 'user' });
188
382
  return true;
189
383
  }
190
384
 
@@ -229,7 +423,11 @@ export class TreeViewController {
229
423
  }
230
424
 
231
425
  setColumns(columns) {
426
+ if (columns == null) columns = this.inspector ? (this.inspector.presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns()) : [];
427
+ if (!Array.isArray(columns)) throw new TypeError('columns must be an array or null');
428
+ this.closeEditor();
232
429
  this.columnModel.setColumns(columns);
430
+ this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
233
431
  this.#syncContentSize();
234
432
  this.events.emit('columnschange', { columns: this.columnModel.columns });
235
433
  }
@@ -280,6 +478,8 @@ export class TreeViewController {
280
478
  }
281
479
 
282
480
  setFilter(queryOrPredicate = '', options = {}) {
481
+ if (typeof queryOrPredicate !== 'string' && typeof queryOrPredicate !== 'function') throw new TypeError('filter must be a string or predicate');
482
+ if (this.cellEditor?.overlayKind !== 'filter') this.closeEditor();
283
483
  this.filterQuery = typeof queryOrPredicate === 'string' ? queryOrPredicate : '';
284
484
  this.filterOptions = typeof queryOrPredicate === 'string'
285
485
  ? { caseSensitive: Boolean(options.caseSensitive), wholeWord: Boolean(options.wholeWord) }
@@ -319,10 +519,12 @@ export class TreeViewController {
319
519
  }
320
520
 
321
521
  setDynamicState(patches) {
522
+ if (!Array.isArray(patches)) throw new TypeError('patches must be an array');
322
523
  this.lastPatchCount = patches.length;
323
524
  this.lastDirtyNodeCount = new Set(patches.map((patch) => patch.id)).size;
324
525
  this.model.applyDynamicPatches(patches);
325
526
  this.renderer.updateDynamicState(patches);
527
+ this.requestRender();
326
528
  }
327
529
 
328
530
  setTheme(theme) {
@@ -330,11 +532,10 @@ export class TreeViewController {
330
532
  const beforeIndentWidth = this.rowModel.indentWidth;
331
533
  this.themeManager.setTheme(theme);
332
534
  const nextTheme = this.themeManager.get();
333
- this.rowModel.rowHeight = nextTheme.rowHeight;
334
- this.rowModel.indentWidth = nextTheme.indentWidth;
335
- this.viewport.rowHeight = nextTheme.rowHeight;
336
- this.viewport.indentWidth = nextTheme.indentWidth;
337
- if (beforeRowHeight !== nextTheme.rowHeight || beforeIndentWidth !== nextTheme.indentWidth) {
535
+ this.rowModel.rowHeight = this.viewport.rowHeight = this.layoutOverrides.rowHeight ?? nextTheme.rowHeight;
536
+ this.rowModel.indentWidth = this.viewport.indentWidth = this.layoutOverrides.indentWidth ?? nextTheme.indentWidth;
537
+ if (beforeRowHeight !== this.rowModel.rowHeight || beforeIndentWidth !== this.rowModel.indentWidth) {
538
+ this.closeEditor();
338
539
  this.#rebuildRows();
339
540
  }
340
541
  this.#nativeScroll?.applyTheme?.(nextTheme);
@@ -342,13 +543,17 @@ export class TreeViewController {
342
543
  }
343
544
 
344
545
  setLayoutMetrics(options = {}) {
345
- const rowHeight = options.rowHeight ?? this.rowModel.rowHeight;
346
- const indentWidth = options.indentWidth ?? this.rowModel.indentWidth;
546
+ const rowHeight = Object.hasOwn(options, 'rowHeight') ? options.rowHeight ?? this.themeManager.get().rowHeight : this.rowModel.rowHeight;
547
+ const indentWidth = Object.hasOwn(options, 'indentWidth') ? options.indentWidth ?? this.themeManager.get().indentWidth : this.rowModel.indentWidth;
347
548
  const headerHeight = options.headerHeight ?? this.viewport.headerHeight;
348
549
  assertPositiveNumber(rowHeight, 'rowHeight');
349
550
  assertPositiveNumber(indentWidth, 'indentWidth');
350
551
  assertNonNegativeNumber(headerHeight, 'headerHeight');
552
+ for (const key of ['rowHeight', 'indentWidth']) {
553
+ if (Object.hasOwn(options, key)) this.layoutOverrides[key] = options[key];
554
+ }
351
555
  const rowsChanged = this.rowModel.rowHeight !== rowHeight || this.rowModel.indentWidth !== indentWidth;
556
+ if (rowsChanged || headerHeight !== this.viewport.headerHeight) this.closeEditor();
352
557
  this.rowModel.rowHeight = rowHeight;
353
558
  this.rowModel.indentWidth = indentWidth;
354
559
  this.viewport.rowHeight = rowHeight;
@@ -542,6 +747,7 @@ export class TreeViewController {
542
747
  }
543
748
 
544
749
  setSelection(ids) {
750
+ if (!Array.isArray(ids)) throw new TypeError('ids must be an array');
545
751
  this.selection.selected.clear();
546
752
  for (const id of ids) this.selection.selected.add(id);
547
753
  this.selection.focused = ids[ids.length - 1] ?? null;
@@ -577,6 +783,7 @@ export class TreeViewController {
577
783
  if (this.activeId === nodeId && this.activePart === part) return;
578
784
  this.activeId = nodeId;
579
785
  this.activePart = part;
786
+ this.requestRender();
580
787
  }
581
788
 
582
789
  clickNode(nodeId, event = {}) {
@@ -620,6 +827,11 @@ export class TreeViewController {
620
827
 
621
828
  handleKey(event) {
622
829
  if (!this.rowModel.rows.length) return false;
830
+ if (this.rowReorder && event.altKey && ['ArrowUp', 'ArrowDown'].includes(event.key)) {
831
+ const id = this.focusedId ?? this.rowModel.getRow(this.#focusedRowIndex())?.nodeId;
832
+ this.moveRowBy(id, event.key === 'ArrowUp' ? -1 : 1, { source: 'keyboard' });
833
+ return true;
834
+ }
623
835
  if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
624
836
  this.selection.selected.clear();
625
837
  for (const row of this.rowModel.rows) this.selection.selected.add(row.nodeId);
@@ -691,9 +903,13 @@ export class TreeViewController {
691
903
  createRenderScene() {
692
904
  const visibleRange = this.rowModel.getVisibleRange(this.viewport, 4);
693
905
  return {
906
+ rowReorder: this.rowReorder,
907
+ canReorderRows: this.canReorderRows(),
908
+ rowDrop: this.rowDrop,
909
+ childrenByParent: this.model.index.childrenByParent,
694
910
  rows: this.rowModel.rows,
695
911
  visibleRange,
696
- stickyRows: this.rowModel.getStickyRows(this.viewport),
912
+ stickyRows: this.rowReorder ? [] : this.rowModel.getStickyRows(this.viewport),
697
913
  viewport: this.viewport,
698
914
  columns: this.columnModel.columns,
699
915
  theme: this.themeManager.get(),
@@ -710,12 +926,13 @@ export class TreeViewController {
710
926
  sortValues: this.sortValueSnapshot ? Array.from(this.sortValueSnapshot) : null,
711
927
  inspectorPaneLabelEnd: this.#computeInspectorPaneLabelEnd(visibleRange),
712
928
  filterQuery: this.filterQuery,
713
- headerFilter: Boolean(this.inspector?.options?.filter),
929
+ headerFilter: this.headerFilter ?? Boolean(this.inspector?.options?.filter),
714
930
  stats: this.getStats(),
715
931
  };
716
932
  }
717
933
 
718
934
  closeEditor() {
935
+ this.cellEditor?.close?.();
719
936
  this.events.emit('editorclose', {});
720
937
  }
721
938
 
@@ -729,7 +946,7 @@ export class TreeViewController {
729
946
  const resizeColumn = this.columnModel.getResizeHandleAt(x);
730
947
  if (resizeColumn) return { area: 'header', part: 'resize', column: resizeColumn, x, y: localY };
731
948
  const column = this.columnModel.getColumnAt(x);
732
- if (column?.kind === 'inspectorPane' && this.inspector?.options?.filter) {
949
+ if (column && (this.headerFilter ?? Boolean(this.inspector?.options?.filter)) && column === this.columnModel.columns.find(item => item.kind !== 'rowOrder')) {
733
950
  return { area: 'header', part: 'filter', column, x, y: localY };
734
951
  }
735
952
  return column ? { area: 'header', part: 'label', column, x, y: localY } : { area: 'header', part: 'header', column: null, x, y: localY };
@@ -743,6 +960,10 @@ export class TreeViewController {
743
960
  const column = this.columnModel.getColumnAt(x);
744
961
  if (!column) return { area: 'row', part: 'row', row, column: null, x, y: rowY };
745
962
 
963
+ if (column.kind === 'rowOrder') {
964
+ const offset = x - column.x;
965
+ return { area: 'row', part: offset < 24 ? 'rowDrag' : offset < 48 ? 'rowUp' : 'rowDown', row, column, x, y: rowY };
966
+ }
746
967
  let part = 'cell';
747
968
  if (column.kind === 'inspectorPane') {
748
969
  const localX = x - column.x;
@@ -789,11 +1010,11 @@ export class TreeViewController {
789
1010
  const node = this.model.nodes[row.nodeIndex];
790
1011
  const data = node?.data;
791
1012
  if (!data?.inspector) return 'cell';
1013
+ const numeric = numericLayout(Math.max(24, editorWidth - 20), data);
1014
+ if (numeric.unit && localEditorX >= 10 + numeric.unitLeft) return 'unit';
792
1015
  if (data.editorType === 'checkbox') return 'checkbox';
793
1016
  if (data.editorType === 'range') {
794
- const width = Math.max(24, editorWidth - 20);
795
- const valueWidth = Math.min(64, Math.max(42, width * 0.28));
796
- const numberLeft = editorWidth - valueWidth - 10;
1017
+ const numberLeft = 10 + numeric.numberLeft;
797
1018
  return localEditorX >= numberLeft ? 'number' : 'range';
798
1019
  }
799
1020
  if (data.editorType === 'button') return 'button';
@@ -879,6 +1100,7 @@ export class TreeViewController {
879
1100
  }
880
1101
 
881
1102
  resize(width, height) {
1103
+ if (width !== this.viewport.viewportWidth || height !== this.viewport.viewportHeight) this.closeEditor();
882
1104
  this.viewport.resize(width, height);
883
1105
  this.#fitInspectorPaneColumn(width);
884
1106
  this.#syncContentSize();
@@ -920,6 +1142,7 @@ export class TreeViewController {
920
1142
  this.#syncContentSize();
921
1143
  this.#restoreScrollAnchor(scrollAnchor);
922
1144
  this.rebuildCount++;
1145
+ this.requestRender();
923
1146
  }
924
1147
 
925
1148
  #syncContentSize() {
@@ -937,13 +1160,6 @@ export class TreeViewController {
937
1160
  this.setDynamicState(this.patchBatcher.flush());
938
1161
  }
939
1162
 
940
- #restoreExpansion(expandedIds) {
941
- this.model.expanded.clear();
942
- for (const id of expandedIds) {
943
- if (this.model.index.getNode(id) && this.expansion.hasChildren(id)) this.model.expanded.add(id);
944
- }
945
- }
946
-
947
1163
  #focusedRowIndex() {
948
1164
  if (this.focusedId) {
949
1165
  const row = this.rowModel.getRowById(this.focusedId);
@@ -1449,3 +1665,14 @@ function isProbablyTruncated(text, width) {
1449
1665
  function now() {
1450
1666
  return globalThis.performance?.now?.() ?? Date.now();
1451
1667
  }
1668
+
1669
+ function resolveNodeIcons(nodes, resolver) {
1670
+ if (!resolver) return nodes;
1671
+ return nodes.map(node => {
1672
+ const visual = resolver(node);
1673
+ if (typeof visual === 'string') return { ...node, icon: visual };
1674
+ if (!visual || typeof visual !== 'object') return node;
1675
+ 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');
1676
+ return { ...node, ...visual };
1677
+ });
1678
+ }