virtual-tree-canvas 0.6.0 → 0.7.1

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.
@@ -1,3 +1,6 @@
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';
1
4
  import { validateTreeViewOptions } from './core/view-options.js';
2
5
  import { RowReorderInput } from './input/row-reorder-input.js';
3
6
  import { TreeTooltip } from './input/tree-tooltip.js';
@@ -13,18 +16,30 @@ import {
13
16
  TreeSelectionManager,
14
17
  TreeWorkerClient,
15
18
  TreeViewViewport,
16
- VisibleRowModel,
19
+ VisibleRowModel
17
20
  } from './core/index.js';
18
21
  import { numericLayout } from './inspector/numeric-layout.js';
19
22
  import { CellEditorManager } from './inspector/cell-editor-manager.js';
20
- import { formatInspectorValue, getAtPath, inspectorNodeId, inspectorColumns, inspectorPaneColumns, ModelInspectorBuilder, setAtPath } from './inspector/index.js';
23
+ import {
24
+ formatInspectorValue,
25
+ getAtPath,
26
+ inspectorNodeId,
27
+ inspectorColumns,
28
+ inspectorPaneColumns,
29
+ ModelInspectorBuilder,
30
+ setAtPath
31
+ } from './inspector/index.js';
21
32
  import { TreeViewInputController } from './input/tree-view-input-controller.js';
22
33
  import { TreeRowRenderer } from './renderers/index.js';
34
+ import { mergeDynamicStateChanged } from './core/dynamic-state.js';
23
35
 
24
36
  export class TreeViewController {
25
37
  constructor(options = {}) {
26
38
  validateTreeViewOptions(options);
27
- this.layoutOverrides = { rowHeight: options.rowHeight, indentWidth: options.indentWidth };
39
+ this.layoutOverrides = {
40
+ rowHeight: options.rowHeight,
41
+ indentWidth: options.indentWidth
42
+ };
28
43
  this.rawNodes = new Map();
29
44
  this.editable = options.editable !== false;
30
45
  this.destroyed = false;
@@ -35,16 +50,19 @@ export class TreeViewController {
35
50
  model: this.model,
36
51
  expansion: this.expansion,
37
52
  rowHeight: options.rowHeight ?? 28,
38
- indentWidth: options.indentWidth ?? 18,
53
+ indentWidth: options.indentWidth ?? 18
39
54
  });
40
55
  this.viewport = new TreeViewViewport({
41
56
  rowHeight: this.rowModel.rowHeight,
42
57
  indentWidth: this.rowModel.indentWidth,
43
- headerHeight: options.headerHeight ?? 28,
58
+ headerHeight: options.headerHeight ?? 28
44
59
  });
45
60
  this.viewport.renderInsetX = options.renderInsetX ?? 0;
46
61
  this.viewport.renderInsetY = options.renderInsetY ?? 0;
47
62
  this.columnModel = new TreeColumnModel(options.columns);
63
+ this.rowActions = options.rowActions ?? [];
64
+ this.rowDrag = options.rowDrag ?? null;
65
+ this.columnModel.setRowActions(this.rowActions.length);
48
66
  this.rowReorder = Boolean(options.rowReorder);
49
67
  this.columnModel.setRowReorder(this.rowReorder);
50
68
  this.rowDrop = null;
@@ -55,14 +73,29 @@ export class TreeViewController {
55
73
  this.searchIndex = new TreeSearchIndex();
56
74
  this.selection = new TreeSelectionManager();
57
75
  this.patchBatcher = new PatchBatcher();
76
+ this.dynamicPatchQueue = new PatchBatcher();
77
+ this.invalidation = { full: true, dirtyNodeIds: new Set() };
78
+ this.renderForced = false;
58
79
  this.themeManager = options.themeManager ?? new ThemeManager(options.theme);
59
- this.iconRegistry = options.iconRegistry ?? new IconRegistry({ iconsBaseUrl: options.iconsBaseUrl });
60
- this.renderer = options.renderer ?? new TreeRowRenderer({ themeManager: this.themeManager, iconRegistry: this.iconRegistry });
80
+ this.iconRegistry =
81
+ options.iconRegistry ??
82
+ new IconRegistry({
83
+ iconsBaseUrl: options.iconsBaseUrl
84
+ });
85
+ this.renderer =
86
+ options.renderer ??
87
+ new TreeRowRenderer({
88
+ themeManager: this.themeManager,
89
+ iconRegistry: this.iconRegistry
90
+ });
61
91
  this.nativeScrollbars = options.nativeScrollbars !== false;
62
92
  this.initialExpandDepth = options.initialExpandDepth ?? 1;
63
93
  this.searchHighlights = new Set();
64
94
  this.filterQuery = '';
65
- this.filterOptions = { caseSensitive: false, wholeWord: false };
95
+ this.filterOptions = {
96
+ caseSensitive: false,
97
+ wholeWord: false
98
+ };
66
99
  this.hoverId = null;
67
100
  this.hoverPart = null;
68
101
  this.activeId = null;
@@ -73,6 +106,16 @@ export class TreeViewController {
73
106
  this.lastDirtyNodeCount = 0;
74
107
  this.lastRenderedRows = 0;
75
108
  this.rebuildCount = 0;
109
+ this.performanceStats = {
110
+ setDynamicStateCalls: 0,
111
+ patchesReceived: 0,
112
+ uniqueNodesReceived: 0,
113
+ nodesChanged: 0,
114
+ rendersRequested: 0,
115
+ rendersExecuted: 0,
116
+ rendersAvoidedNoChanges: 0,
117
+ rendersAvoidedOffscreen: 0
118
+ };
76
119
  this.workerClient = null;
77
120
  this.workerRevision = 0;
78
121
  this.sortValueSnapshot = null;
@@ -81,15 +124,36 @@ export class TreeViewController {
81
124
  this.cellEditor = null;
82
125
  const initialTheme = this.themeManager.get();
83
126
  this.rowModel.rowHeight = this.viewport.rowHeight = options.rowHeight ?? initialTheme.rowHeight;
84
- this.rowModel.indentWidth = this.viewport.indentWidth = options.indentWidth ?? initialTheme.indentWidth;
127
+ this.rowModel.indentWidth = this.viewport.indentWidth =
128
+ options.indentWidth ?? initialTheme.indentWidth;
85
129
  this.scene = this.createRenderScene();
86
130
  this.renderer.setInvalidationHandler?.(() => this.requestRender(true));
87
131
 
88
- for (const type of ['viewportchange', 'columnschange', 'sortchange', 'filterchange', 'themechange', 'layoutchange', 'expand', 'collapse', 'searchchange', 'nodehover', 'selectionchange', 'focuschange', 'rowreorder', 'rowdrag', 'rowreorderchange', 'modelchange']) {
132
+ for (const type of [
133
+ 'viewportchange',
134
+ 'columnschange',
135
+ 'sortchange',
136
+ 'filterchange',
137
+ 'themechange',
138
+ 'layoutchange',
139
+ 'expand',
140
+ 'collapse',
141
+ 'searchchange',
142
+ 'nodehover',
143
+ 'selectionchange',
144
+ 'focuschange',
145
+ 'rowreorder',
146
+ 'rowdrag',
147
+ 'rowreorderchange',
148
+ 'modelchange'
149
+ ]) {
89
150
  this.events.on(type, () => this.requestRender());
90
151
  }
91
152
  if (options.canvas) this.initialize(options.canvas);
92
- if (options.canvas && (options.host || options.editable)) this.attachCellEditor({ host: options.host });
153
+ if (options.canvas && (options.host || options.editable))
154
+ this.attachCellEditor({
155
+ host: options.host
156
+ });
93
157
  if (options.canvas && options.input !== false) this.attachInput();
94
158
  }
95
159
 
@@ -100,61 +164,102 @@ export class TreeViewController {
100
164
  this.canvas = canvas;
101
165
  this.renderer.initialize(canvas);
102
166
  this.renderer.setScene(this.scene);
167
+ this.actionOverlay?.destroy();
168
+ this.actionOverlay =
169
+ canvas.ownerDocument?.createElement && canvas.parentElement ? new RowActions(this) : null;
103
170
  this.#resizeToCanvasClientSize();
104
171
  this.#observeCanvasSize();
105
172
  this.#setupNativeScrollbars();
106
- if (this.tooltipEnabled) this.attachTooltip({ host: this.host });
173
+ if (this.tooltipEnabled)
174
+ this.attachTooltip({
175
+ host: this.host
176
+ });
107
177
  this.requestRender();
108
178
  return this;
109
179
  }
110
180
 
111
181
  attachCellEditor(options = {}) {
112
- if (!this.canvas) throw new Error('TreeViewController.attachCellEditor requires an initialized canvas');
182
+ if (!this.canvas)
183
+ throw new Error('TreeViewController.attachCellEditor requires an initialized canvas');
113
184
  this.cellEditor?.destroy?.();
114
185
  this.cellEditor = new CellEditorManager({
115
186
  controller: this,
116
- host: options.host,
187
+ host: options.host
117
188
  });
118
189
  this.cellEditor.setEditable(this.editable);
119
190
  if (this.inputController) this.inputController.cellEditor = this.cellEditor;
120
191
  return this.cellEditor;
121
192
  }
122
193
 
194
+ setRowActions(actions) {
195
+ validateTreeViewOptions({
196
+ rowActions: actions
197
+ });
198
+ this.rowActions = actions;
199
+ this.columnModel.setRowActions(actions.length);
200
+ this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
201
+ this.#syncContentSize();
202
+ this.requestRender();
203
+ }
204
+
205
+ setRowDrag(resolver) {
206
+ validateTreeViewOptions({
207
+ rowDrag: resolver
208
+ });
209
+ this.rowReorderInput?.cancel();
210
+ this.rowDrag = resolver;
211
+ this.rowReorderInput?.syncMode();
212
+ }
213
+
123
214
  setEditable(enabled) {
124
- validateTreeViewOptions({ editable: enabled });
215
+ validateTreeViewOptions({
216
+ editable: enabled
217
+ });
125
218
  if (this.editable === enabled) return;
126
219
  this.editable = enabled;
127
- if (!this.cellEditor && this.canvas) this.attachCellEditor({ host: this.host });
220
+ if (!this.cellEditor && this.canvas)
221
+ this.attachCellEditor({
222
+ host: this.host
223
+ });
128
224
  this.cellEditor?.setEditable(enabled);
129
- this.events.emit('editablechange', { editable: enabled });
225
+ this.events.emit('editablechange', {
226
+ editable: enabled
227
+ });
130
228
  this.requestRender();
131
229
  }
132
230
 
133
231
  setInitialExpandDepth(depth) {
134
- validateTreeViewOptions({ initialExpandDepth: depth });
232
+ validateTreeViewOptions({
233
+ initialExpandDepth: depth
234
+ });
135
235
  this.initialExpandDepth = depth;
136
236
  }
137
237
 
138
-
139
238
  attachInput(options = {}) {
140
- if (!this.canvas) throw new Error('TreeViewController.attachInput requires an initialized canvas');
239
+ if (!this.canvas)
240
+ throw new Error('TreeViewController.attachInput requires an initialized canvas');
141
241
  this.inputController?.destroy?.();
142
242
  this.rowReorderInput?.destroy();
143
243
  this.rowReorderInput = new RowReorderInput(this);
144
244
  this.inputController = new TreeViewInputController({
145
245
  controller: this,
146
- cellEditor: options.cellEditor ?? this.cellEditor,
246
+ cellEditor: options.cellEditor ?? this.cellEditor
147
247
  });
148
248
  return this.inputController;
149
249
  }
150
250
 
151
251
  destroy() {
152
252
  this.destroyed = true;
253
+ this.actionOverlay?.destroy();
153
254
  this.rowReorderInput?.destroy();
154
255
  this.rowReorderInput = null;
155
256
  this.tooltip?.destroy();
156
257
  this.tooltip = null;
157
258
  this.cancelRender();
259
+ this.patchBatcher.clear();
260
+ this.dynamicPatchQueue.clear();
261
+ this.invalidation.dirtyNodeIds.clear();
262
+ this.invalidation.full = false;
158
263
  this.inputController?.destroy?.();
159
264
  this.cellEditor?.destroy?.();
160
265
  this.disableWorkers();
@@ -178,29 +283,50 @@ export class TreeViewController {
178
283
  }
179
284
 
180
285
  setData(nodes, options = {}) {
286
+ this.flushDynamicState();
181
287
  if (!Array.isArray(nodes)) throw new TypeError('nodes must be an array');
182
- validateTreeViewOptions({ iconResolver: options.iconResolver ?? null });
288
+ validateTreeViewOptions({
289
+ iconResolver: options.iconResolver ?? null
290
+ });
183
291
  const resolved = resolveNodeIcons(nodes, options.iconResolver);
184
292
  this.closeEditor();
185
- this.rawNodes = new Map(nodes.map(node => [node.id, node]));
293
+ this.rawNodes = new Map(nodes.map((node) => [node.id, node]));
186
294
  nodes = resolved;
295
+ const preserveExpansion = !this.inspector;
187
296
  this.inspector = null;
188
297
  this.columnModel.setRowReorder(this.rowReorder);
189
- if (this.columnModel.columns.some(column => column.kind?.startsWith('inspector'))) {
298
+ if (this.columnModel.columns.some((column) => column.kind?.startsWith('inspector'))) {
190
299
  this.columnModel.setColumns([]);
191
300
  }
192
- this.model.setTree(nodes);
193
- this.expansion.expandToDepth(this.initialExpandDepth);
301
+ this.#replaceTree(nodes, preserveExpansion);
194
302
  this.searchIndex.rebuild(this.model);
195
303
  this.workerClient?.setData(this.model.nodes);
196
304
  this.#rebuildRows();
197
305
  this.events.emit('datachange', {});
198
306
  }
199
307
 
308
+ #replaceTree(nodes, preserveExpansion) {
309
+ const branches = preserveExpansion
310
+ ? this.model.nodes
311
+ .filter((node) => this.expansion.hasChildren(node.id))
312
+ .map((node) => node.id)
313
+ : [];
314
+ const expanded = new Set(this.model.expanded);
315
+ this.model.setTree(nodes);
316
+ this.expansion.expandToDepth(this.initialExpandDepth);
317
+ for (const id of branches) {
318
+ if (!this.model.index.getNode(id)) continue;
319
+ if (expanded.has(id)) this.model.expanded.add(id);
320
+ else this.model.expanded.delete(id);
321
+ }
322
+ }
323
+
200
324
  setIconResolver(resolver) {
201
- validateTreeViewOptions({ iconResolver: resolver });
325
+ validateTreeViewOptions({
326
+ iconResolver: resolver
327
+ });
202
328
  if (this.inspector) return;
203
- const source = this.model.nodes.map(node => this.rawNodes.get(node.id) ?? node);
329
+ const source = this.model.nodes.map((node) => this.rawNodes.get(node.id) ?? node);
204
330
  const nodes = resolveNodeIcons(source, resolver);
205
331
  this.model.nodes = nodes;
206
332
  this.model.index.rebuild(nodes);
@@ -225,16 +351,26 @@ export class TreeViewController {
225
351
  }
226
352
 
227
353
  setRowReorder(enabled) {
228
- validateTreeViewOptions({ rowReorder: enabled });
354
+ validateTreeViewOptions({
355
+ rowReorder: enabled
356
+ });
229
357
  this.rowReorder = Boolean(enabled);
230
358
  this.rowDrop = null;
231
359
  this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
232
360
  this.#syncContentSize();
233
- this.events.emit('rowreorderchange', { enabled: this.rowReorder });
361
+ this.events.emit('rowreorderchange', {
362
+ enabled: this.rowReorder
363
+ });
234
364
  }
235
365
 
236
- canReorderRows() {
237
- return this.rowReorder && !this.inspector && !this.columnModel.sort.direction && !this.rowModel.filterPredicate;
366
+ canReorderRows(nodeId) {
367
+ return (
368
+ this.rowReorder &&
369
+ !this.inspector &&
370
+ !this.columnModel.sort.direction &&
371
+ !this.rowModel.filterPredicate &&
372
+ (nodeId == null || this.model.index.getNode(nodeId)?.reorderable !== false)
373
+ );
238
374
  }
239
375
 
240
376
  getRowOrder(parentId = null) {
@@ -242,7 +378,8 @@ export class TreeViewController {
242
378
  }
243
379
 
244
380
  moveRow(nodeId, targetIndex, options = {}) {
245
- if (!this.canReorderRows()) return false;
381
+ this.flushDynamicState();
382
+ if (!this.canReorderRows(nodeId)) return false;
246
383
  const anchorId = this.rowModel.getRow(this.anchorRowIndex)?.nodeId;
247
384
  const detail = this.model.moveNode(nodeId, targetIndex);
248
385
  if (!detail) return false;
@@ -255,8 +392,13 @@ export class TreeViewController {
255
392
  this.selection.focused = nodeId;
256
393
  this.cellEditor?.close?.();
257
394
  this.scrollToNode(nodeId);
258
- this.events.emit('rowreorder', { ...detail, source: options.source ?? 'api' });
259
- this.events.emit('focuschange', { nodeId });
395
+ this.events.emit('rowreorder', {
396
+ ...detail,
397
+ source: options.source ?? 'api'
398
+ });
399
+ this.events.emit('focuschange', {
400
+ nodeId
401
+ });
260
402
  return true;
261
403
  }
262
404
 
@@ -268,13 +410,20 @@ export class TreeViewController {
268
410
  }
269
411
 
270
412
  getRowDropTarget(nodeId, clientX, clientY) {
271
- if (!this.canReorderRows()) return null;
413
+ if (!this.canReorderRows(nodeId)) return null;
272
414
  const x = clientX - this.viewport.renderInsetX;
273
415
  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;
416
+ if (
417
+ x < 0 ||
418
+ x >= this.viewport.contentViewportWidth ||
419
+ y < this.viewport.headerHeight ||
420
+ y >= this.viewport.headerHeight + this.viewport.rowViewportHeight
421
+ )
422
+ return null;
276
423
  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)));
424
+ const row = this.rowModel.getRow(
425
+ Math.min(this.rowModel.rows.length - 1, Math.floor(contentY / this.rowModel.rowHeight))
426
+ );
278
427
  const source = this.model.index.getNode(nodeId);
279
428
  const target = row && this.model.index.getNode(row.nodeId);
280
429
  if (!source || !target || (source.parentId ?? null) !== (target.parentId ?? null)) return null;
@@ -283,21 +432,46 @@ export class TreeViewController {
283
432
  const from = siblings.indexOf(nodeId);
284
433
  let index = siblings.indexOf(target.id) + Number(after);
285
434
  if (index > from) index--;
286
- return index === from ? null : { nodeId, targetId: target.id, index, y: row.y + (after ? row.height : 0) };
435
+ return index === from
436
+ ? null
437
+ : {
438
+ nodeId,
439
+ targetId: target.id,
440
+ index,
441
+ y: row.y + (after ? row.height : 0)
442
+ };
287
443
  }
288
444
 
289
445
  setRowDrop(target) {
290
446
  this.rowDrop = target;
291
- this.events.emit('rowdrag', { target });
447
+ this.events.emit('rowdrag', {
448
+ target
449
+ });
292
450
  }
293
451
 
294
452
  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));
453
+ if (this.destroyed) return;
454
+ this.invalidation.full = true;
455
+ this.invalidation.dirtyNodeIds.clear();
456
+ if ((!this.autoRender && !force) || !this.canvas) return;
457
+ if (this.renderFrame === null) this.performanceStats.rendersRequested++;
458
+ this.renderForced ||= force;
459
+ this.#scheduleFrame();
460
+ }
461
+
462
+ #scheduleFrame() {
463
+ if (this.destroyed || this.renderFrame !== null) return;
464
+ const view = this.canvas?.ownerDocument?.defaultView ?? globalThis;
465
+ const schedule =
466
+ view.requestAnimationFrame?.bind(view) ?? ((callback) => setTimeout(callback, 0));
298
467
  this.renderFrame = schedule(() => {
299
468
  this.renderFrame = null;
300
- if (this.canvas) this.render();
469
+ const forced = this.renderForced;
470
+ this.renderForced = false;
471
+ this.flushDynamicState();
472
+ if (this.canvas && (this.autoRender || forced) && this.#hasInvalidation()) {
473
+ this.render();
474
+ }
301
475
  });
302
476
  }
303
477
 
@@ -306,44 +480,64 @@ export class TreeViewController {
306
480
  const view = this.canvas?.ownerDocument?.defaultView ?? globalThis;
307
481
  (view.cancelAnimationFrame?.bind(view) ?? clearTimeout)(this.renderFrame);
308
482
  this.renderFrame = null;
483
+ this.renderForced = false;
309
484
  }
310
485
 
311
486
  attachTooltip(options = {}) {
312
487
  this.tooltip?.destroy();
313
- this.tooltip = new TreeTooltip({ controller: this, host: options.host ?? this.canvas?.parentElement });
488
+ this.tooltip = new TreeTooltip({
489
+ controller: this,
490
+ host: options.host ?? this.canvas?.parentElement
491
+ });
314
492
  return this.tooltip;
315
493
  }
316
494
 
317
495
  setModel(model, meta = {}, options = {}) {
318
- validateTreeViewOptions({ ...options, mode: 'inspector', model, meta, presentation: options.presentation ?? options.mode ?? 'table' });
319
- this.closeEditor();
496
+ this.flushDynamicState();
497
+ validateTreeViewOptions({
498
+ ...options,
499
+ mode: 'inspector',
500
+ model,
501
+ meta,
502
+ presentation: options.presentation ?? options.mode ?? 'table'
503
+ });
320
504
  this.columnModel.setRowReorder(false);
321
505
  const builder = new ModelInspectorBuilder();
322
506
  const presentation = options.presentation ?? options.mode ?? 'table';
323
- const previousExpanded = new Set(this.model.expanded);
324
- const previousBranches = new Set(this.inspector ? this.model.nodes.filter(node => this.expansion.hasChildren(node.id)).map(node => node.id) : []);
507
+ const preserveExpansion = Boolean(this.inspector);
325
508
  const inspectorOptions = {
326
509
  presentation,
327
510
  flatRoot: Boolean(options.flatRoot),
328
511
  enforceMeta: Boolean(options.enforceMeta),
329
512
  filter: Boolean(options.filter),
330
- markUpdated: options.markUpdated !== false,
513
+ markUpdated: options.markUpdated !== false
331
514
  };
332
515
  const nodes = builder.build(model, meta, inspectorOptions);
516
+ const sameLayout =
517
+ this.inspector?.presentation === presentation &&
518
+ nodes.length === this.model.nodes.length &&
519
+ nodes.every((node, index) => node.id === this.model.nodes[index].id);
520
+ if (!sameLayout) this.closeEditor();
521
+ else this.cellEditor?.reconcile(nodes);
333
522
  this.rawNodes.clear();
334
- this.inspector = { model, meta, builder, presentation, options: inspectorOptions };
335
- this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
336
- this.model.setTree(nodes);
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
- }
523
+ this.inspector = {
524
+ model,
525
+ meta,
526
+ builder,
527
+ presentation,
528
+ options: inspectorOptions
529
+ };
530
+ if (!sameLayout)
531
+ this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
532
+ this.#replaceTree(nodes, preserveExpansion);
343
533
  this.searchIndex.rebuild(this.model);
344
534
  this.#rebuildRows();
345
535
  this.events.emit('datachange', {});
346
- this.events.emit('modelchange', { model, meta, structural: true });
536
+ this.events.emit('modelchange', {
537
+ model,
538
+ meta,
539
+ structural: true
540
+ });
347
541
  }
348
542
 
349
543
  updateInspectorValue(nodeId, newValue, editorType = 'unknown', options = {}) {
@@ -355,15 +549,39 @@ export class TreeViewController {
355
549
  if (Object.is(oldValue, newValue)) return true;
356
550
  setAtPath(this.inspector.model, data.path, newValue);
357
551
  data.value = newValue;
358
- data.valueType = Array.isArray(newValue) ? 'array' : newValue === null ? 'null' : typeof newValue === 'object' ? 'object' : typeof newValue;
552
+ data.valueType = Array.isArray(newValue)
553
+ ? 'array'
554
+ : newValue === null
555
+ ? 'null'
556
+ : typeof newValue === 'object'
557
+ ? 'object'
558
+ : typeof newValue;
359
559
  data.valueText = formatInspectorValue(newValue, data.meta);
360
560
  if (this.inspector.options.markUpdated !== false) {
361
- this.setDynamicState([{ id: nodeId, state: { updated: true } }]);
561
+ this.setDynamicState([
562
+ {
563
+ id: nodeId,
564
+ state: {
565
+ updated: true
566
+ }
567
+ }
568
+ ]);
569
+ this.flushDynamicState();
362
570
  }
363
- const detail = { path: data.path, oldValue, newValue, nodeId, editorType, model: this.inspector.model, source: options.source ?? 'user' };
571
+ const detail = {
572
+ path: data.path,
573
+ oldValue,
574
+ newValue,
575
+ nodeId,
576
+ editorType,
577
+ model: this.inspector.model,
578
+ source: options.source ?? 'user'
579
+ };
364
580
  if (options.emit !== false) {
365
581
  this.events.emit('valuechange', detail);
366
- this.events.emit('modelchange', { ...detail });
582
+ this.events.emit('modelchange', {
583
+ ...detail
584
+ });
367
585
  }
368
586
  this.requestRender();
369
587
  return true;
@@ -371,14 +589,23 @@ export class TreeViewController {
371
589
 
372
590
  setInspectorValue(path, value, options = {}) {
373
591
  if (typeof path !== 'string') throw new TypeError('path must be a string');
374
- return this.updateInspectorValue(inspectorNodeId(path === '$' ? '' : path), value, 'api', { ...options, source: 'api' });
592
+ return this.updateInspectorValue(inspectorNodeId(path === '$' ? '' : path), value, 'api', {
593
+ ...options,
594
+ source: 'api'
595
+ });
375
596
  }
376
597
 
377
598
  triggerInspectorAction(nodeId) {
378
599
  const node = this.model.index.getNode(nodeId);
379
600
  if (!node?.data?.inspector) return false;
380
601
  const label = node.data.meta?.button ?? node.label;
381
- this.events.emit('action', { path: node.data.path, label, nodeId, model: this.inspector?.model, source: 'user' });
602
+ this.events.emit('action', {
603
+ path: node.data.path,
604
+ label,
605
+ nodeId,
606
+ model: this.inspector?.model,
607
+ source: 'user'
608
+ });
382
609
  return true;
383
610
  }
384
611
 
@@ -390,7 +617,13 @@ export class TreeViewController {
390
617
  const item = createDefaultArrayItem(node.data.meta);
391
618
  array.push(item);
392
619
  this.#rebuildInspectorModel(node.data.path);
393
- this.events.emit('modelchange', { model: this.inspector.model, path: node.data.path, structural: true, action: 'array-add', value: item });
620
+ this.events.emit('modelchange', {
621
+ model: this.inspector.model,
622
+ path: node.data.path,
623
+ structural: true,
624
+ action: 'array-add',
625
+ value: item
626
+ });
394
627
  return true;
395
628
  }
396
629
 
@@ -401,7 +634,13 @@ export class TreeViewController {
401
634
  if (!Array.isArray(array) || !array.length) return false;
402
635
  const value = array.pop();
403
636
  this.#rebuildInspectorModel(node.data.path);
404
- this.events.emit('modelchange', { model: this.inspector.model, path: node.data.path, structural: true, action: 'array-remove', value });
637
+ this.events.emit('modelchange', {
638
+ model: this.inspector.model,
639
+ path: node.data.path,
640
+ structural: true,
641
+ action: 'array-remove',
642
+ value
643
+ });
405
644
  return true;
406
645
  }
407
646
 
@@ -409,7 +648,8 @@ export class TreeViewController {
409
648
  if (this.workerClient) return Promise.resolve(this);
410
649
  const client = new TreeWorkerClient(workerUrl);
411
650
  this.workerClient = client;
412
- return client.setData(this.model.nodes)
651
+ return client
652
+ .setData(this.model.nodes)
413
653
  .then(() => this)
414
654
  .catch((error) => {
415
655
  if (this.workerClient === client) this.disableWorkers();
@@ -423,30 +663,42 @@ export class TreeViewController {
423
663
  }
424
664
 
425
665
  setColumns(columns) {
426
- if (columns == null) columns = this.inspector ? (this.inspector.presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns()) : [];
666
+ if (columns == null)
667
+ columns = this.inspector
668
+ ? this.inspector.presentation === 'pane'
669
+ ? inspectorPaneColumns()
670
+ : inspectorColumns()
671
+ : [];
427
672
  if (!Array.isArray(columns)) throw new TypeError('columns must be an array or null');
428
673
  this.closeEditor();
429
674
  this.columnModel.setColumns(columns);
430
675
  this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
431
676
  this.#syncContentSize();
432
- this.events.emit('columnschange', { columns: this.columnModel.columns });
677
+ this.events.emit('columnschange', {
678
+ columns: this.columnModel.columns
679
+ });
433
680
  }
434
681
 
435
682
  resizeColumn(columnId, width) {
436
683
  if (!this.columnModel.resizeColumn(columnId, width)) return false;
437
684
  this.#syncContentSize();
438
- this.events.emit('columnschange', { columns: this.columnModel.columns });
685
+ this.events.emit('columnschange', {
686
+ columns: this.columnModel.columns
687
+ });
439
688
  return true;
440
689
  }
441
690
 
442
691
  moveColumn(columnId, targetIndex) {
443
692
  if (!this.columnModel.moveColumn(columnId, targetIndex)) return false;
444
693
  this.#syncContentSize();
445
- this.events.emit('columnschange', { columns: this.columnModel.columns });
694
+ this.events.emit('columnschange', {
695
+ columns: this.columnModel.columns
696
+ });
446
697
  return true;
447
698
  }
448
699
 
449
700
  sortBy(columnId, direction = 'toggle') {
701
+ this.flushDynamicState();
450
702
  const column = this.columnModel.getColumn(columnId);
451
703
  if (!column || column.sortable === false) return false;
452
704
  const current = this.columnModel.sort;
@@ -462,10 +714,21 @@ export class TreeViewController {
462
714
  return true;
463
715
  }
464
716
  this.columnModel.setSort(columnId, nextDirection);
465
- this.sortValueSnapshot = createSortValueSnapshot(column, this.model.nodes, this.model.dynamicState);
466
- this.rowModel.setSortComparator((a, b) => compareColumnValues(column, a, b, this.model.dynamicState, this.sortValueSnapshot) * (nextDirection === 'desc' ? -1 : 1));
717
+ this.sortValueSnapshot = createSortValueSnapshot(
718
+ column,
719
+ this.model.nodes,
720
+ this.model.dynamicState
721
+ );
722
+ this.rowModel.setSortComparator(
723
+ (a, b) =>
724
+ compareColumnValues(column, a, b, this.model.dynamicState, this.sortValueSnapshot) *
725
+ (nextDirection === 'desc' ? -1 : 1)
726
+ );
467
727
  this.#rebuildRows();
468
- this.events.emit('sortchange', { columnId, direction: nextDirection });
728
+ this.events.emit('sortchange', {
729
+ columnId,
730
+ direction: nextDirection
731
+ });
469
732
  return true;
470
733
  }
471
734
 
@@ -474,24 +737,51 @@ export class TreeViewController {
474
737
  this.sortValueSnapshot = null;
475
738
  this.rowModel.setSortComparator(null);
476
739
  this.#rebuildRows();
477
- this.events.emit('sortchange', { columnId: null, direction: null });
740
+ this.events.emit('sortchange', {
741
+ columnId: null,
742
+ direction: null
743
+ });
478
744
  }
479
745
 
480
746
  setFilter(queryOrPredicate = '', options = {}) {
481
- if (typeof queryOrPredicate !== 'string' && typeof queryOrPredicate !== 'function') throw new TypeError('filter must be a string or predicate');
747
+ this.flushDynamicState();
748
+ if (typeof queryOrPredicate !== 'string' && typeof queryOrPredicate !== 'function')
749
+ throw new TypeError('filter must be a string or predicate');
482
750
  if (this.cellEditor?.overlayKind !== 'filter') this.closeEditor();
483
751
  this.filterQuery = typeof queryOrPredicate === 'string' ? queryOrPredicate : '';
484
- this.filterOptions = typeof queryOrPredicate === 'string'
485
- ? { caseSensitive: Boolean(options.caseSensitive), wholeWord: Boolean(options.wholeWord) }
486
- : { caseSensitive: false, wholeWord: false };
752
+ this.filterOptions =
753
+ typeof queryOrPredicate === 'string'
754
+ ? {
755
+ caseSensitive: Boolean(options.caseSensitive),
756
+ wholeWord: Boolean(options.wholeWord)
757
+ }
758
+ : {
759
+ caseSensitive: false,
760
+ wholeWord: false
761
+ };
487
762
  if (typeof queryOrPredicate === 'function') {
488
763
  this.rowModel.setFilterPredicate(queryOrPredicate);
489
764
  } else {
490
765
  const query = normalizeFilterValue(queryOrPredicate.trim(), this.filterOptions);
491
- this.rowModel.setFilterPredicate(query ? (node, state) => matchesFilter(node, state, this.model.index.pathById.get(node.id) ?? '', query, this.filterOptions) : null);
766
+ this.rowModel.setFilterPredicate(
767
+ query
768
+ ? (node, state) =>
769
+ matchesFilter(
770
+ node,
771
+ state,
772
+ this.model.index.pathById.get(node.id) ?? '',
773
+ query,
774
+ this.filterOptions
775
+ )
776
+ : null
777
+ );
492
778
  }
493
779
  this.#rebuildRows();
494
- this.events.emit('filterchange', { query: this.filterQuery, options: this.filterOptions, visibleRows: this.rowModel.rows.length });
780
+ this.events.emit('filterchange', {
781
+ query: this.filterQuery,
782
+ options: this.filterOptions,
783
+ visibleRows: this.rowModel.rows.length
784
+ });
495
785
  }
496
786
 
497
787
  clearFilter() {
@@ -506,45 +796,128 @@ export class TreeViewController {
506
796
  const totalStart = now();
507
797
  const revision = ++this.workerRevision;
508
798
  this.filterQuery = query;
509
- this.filterOptions = { caseSensitive: Boolean(options.caseSensitive), wholeWord: Boolean(options.wholeWord) };
799
+ this.filterOptions = {
800
+ caseSensitive: Boolean(options.caseSensitive),
801
+ wholeWord: Boolean(options.wholeWord)
802
+ };
510
803
  const normalized = normalizeFilterValue(query.trim(), this.filterOptions);
511
- this.rowModel.setFilterPredicate(normalized ? (node, state) => matchesFilter(node, state, this.model.index.pathById.get(node.id) ?? '', normalized, this.filterOptions) : null);
804
+ this.rowModel.setFilterPredicate(
805
+ normalized
806
+ ? (node, state) =>
807
+ matchesFilter(
808
+ node,
809
+ state,
810
+ this.model.index.pathById.get(node.id) ?? '',
811
+ normalized,
812
+ this.filterOptions
813
+ )
814
+ : null
815
+ );
512
816
  const workerStart = now();
513
- const result = await this.workerClient.rebuildRows(this.#workerRowOptions({ filterQuery: query, filterOptions: this.filterOptions }));
817
+ const result = await this.workerClient.rebuildRows(
818
+ this.#workerRowOptions({
819
+ filterQuery: query,
820
+ filterOptions: this.filterOptions
821
+ })
822
+ );
514
823
  const workerMs = now() - workerStart;
515
824
  if (revision !== this.workerRevision) return this.rowModel.rows;
516
825
  this.#applyWorkerRows(result);
517
- this.events.emit('filterchange', { query: this.filterQuery, options: this.filterOptions, visibleRows: this.rowModel.rows.length, worker: true, workerMs, totalMs: now() - totalStart });
826
+ this.events.emit('filterchange', {
827
+ query: this.filterQuery,
828
+ options: this.filterOptions,
829
+ visibleRows: this.rowModel.rows.length,
830
+ worker: true,
831
+ workerMs,
832
+ totalMs: now() - totalStart
833
+ });
518
834
  return this.rowModel.rows;
519
835
  }
520
836
 
521
837
  setDynamicState(patches) {
522
838
  if (!Array.isArray(patches)) throw new TypeError('patches must be an array');
523
- this.lastPatchCount = patches.length;
524
- this.lastDirtyNodeCount = new Set(patches.map((patch) => patch.id)).size;
525
- this.model.applyDynamicPatches(patches);
526
- this.renderer.updateDynamicState(patches);
527
- this.requestRender();
839
+ if (this.destroyed || patches.length === 0) return;
840
+ this.performanceStats.setDynamicStateCalls++;
841
+ this.performanceStats.patchesReceived += patches.length;
842
+ this.lastPatchCount += patches.length;
843
+ this.dynamicPatchQueue.addMany(patches);
844
+ this.#scheduleFrame();
845
+ }
846
+
847
+ /**
848
+ * Applies queued dynamic patches immediately. Rendering, scene creation,
849
+ * sorting and filtering call this method before reading dynamic state.
850
+ * @returns {Set<string>} IDs whose stored state actually changed.
851
+ */
852
+ flushDynamicState() {
853
+ if (!this.dynamicPatchQueue.size) return new Set();
854
+ const hadInvalidation = this.#hasInvalidation();
855
+ const queued = this.dynamicPatchQueue.pending;
856
+ const changedIds = new Set();
857
+ const changedPatches = [];
858
+ this.performanceStats.uniqueNodesReceived += queued.size;
859
+ this.lastDirtyNodeCount = 0;
860
+
861
+ for (const [id, state] of queued) {
862
+ const current = this.model.dynamicState.get(id);
863
+ if (!current || !mergeDynamicStateChanged(current, state)) continue;
864
+ changedIds.add(id);
865
+ changedPatches.push({ id, state });
866
+ }
867
+ queued.clear();
868
+
869
+ if (!changedIds.size) {
870
+ if (!hadInvalidation) this.performanceStats.rendersAvoidedNoChanges++;
871
+ this.lastPatchCount = 0;
872
+ return changedIds;
873
+ }
874
+
875
+ this.lastDirtyNodeCount = changedIds.size;
876
+ this.performanceStats.nodesChanged += changedIds.size;
877
+ this.renderer.updateDynamicState(changedPatches);
878
+ if (this.#hasVisibleDirtyNode(changedIds)) {
879
+ if (!this.invalidation.full) {
880
+ const requestsRender = this.invalidation.dirtyNodeIds.size === 0;
881
+ for (const id of changedIds) this.invalidation.dirtyNodeIds.add(id);
882
+ if (requestsRender) this.performanceStats.rendersRequested++;
883
+ }
884
+ } else {
885
+ if (!hadInvalidation) this.performanceStats.rendersAvoidedOffscreen++;
886
+ this.lastPatchCount = 0;
887
+ }
888
+ return changedIds;
528
889
  }
529
890
 
530
891
  setTheme(theme) {
892
+ this.flushDynamicState();
531
893
  const beforeRowHeight = this.rowModel.rowHeight;
532
894
  const beforeIndentWidth = this.rowModel.indentWidth;
533
895
  this.themeManager.setTheme(theme);
534
896
  const nextTheme = this.themeManager.get();
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) {
897
+ this.rowModel.rowHeight = this.viewport.rowHeight =
898
+ this.layoutOverrides.rowHeight ?? nextTheme.rowHeight;
899
+ this.rowModel.indentWidth = this.viewport.indentWidth =
900
+ this.layoutOverrides.indentWidth ?? nextTheme.indentWidth;
901
+ if (
902
+ beforeRowHeight !== this.rowModel.rowHeight ||
903
+ beforeIndentWidth !== this.rowModel.indentWidth
904
+ ) {
538
905
  this.closeEditor();
539
906
  this.#rebuildRows();
540
907
  }
541
908
  this.#nativeScroll?.applyTheme?.(nextTheme);
542
- this.events.emit('themechange', { theme: nextTheme });
909
+ this.events.emit('themechange', {
910
+ theme: nextTheme
911
+ });
543
912
  }
544
913
 
545
914
  setLayoutMetrics(options = {}) {
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;
915
+ const rowHeight = Object.hasOwn(options, 'rowHeight')
916
+ ? (options.rowHeight ?? this.themeManager.get().rowHeight)
917
+ : this.rowModel.rowHeight;
918
+ const indentWidth = Object.hasOwn(options, 'indentWidth')
919
+ ? (options.indentWidth ?? this.themeManager.get().indentWidth)
920
+ : this.rowModel.indentWidth;
548
921
  const headerHeight = options.headerHeight ?? this.viewport.headerHeight;
549
922
  assertPositiveNumber(rowHeight, 'rowHeight');
550
923
  assertPositiveNumber(indentWidth, 'indentWidth');
@@ -552,7 +925,8 @@ export class TreeViewController {
552
925
  for (const key of ['rowHeight', 'indentWidth']) {
553
926
  if (Object.hasOwn(options, key)) this.layoutOverrides[key] = options[key];
554
927
  }
555
- const rowsChanged = this.rowModel.rowHeight !== rowHeight || this.rowModel.indentWidth !== indentWidth;
928
+ const rowsChanged =
929
+ this.rowModel.rowHeight !== rowHeight || this.rowModel.indentWidth !== indentWidth;
556
930
  if (rowsChanged || headerHeight !== this.viewport.headerHeight) this.closeEditor();
557
931
  this.rowModel.rowHeight = rowHeight;
558
932
  this.rowModel.indentWidth = indentWidth;
@@ -571,12 +945,17 @@ export class TreeViewController {
571
945
  expand(nodeId) {
572
946
  if (this.filterQuery && this.rowModel.expandFilterBranch(nodeId)) {
573
947
  this.#rebuildRows();
574
- this.events.emit('expand', { nodeId, filter: true });
948
+ this.events.emit('expand', {
949
+ nodeId,
950
+ filter: true
951
+ });
575
952
  return true;
576
953
  }
577
954
  if (!this.expansion.expand(nodeId)) return false;
578
955
  this.#rebuildRows();
579
- this.events.emit('expand', { nodeId });
956
+ this.events.emit('expand', {
957
+ nodeId
958
+ });
580
959
  return true;
581
960
  }
582
961
 
@@ -587,51 +966,76 @@ export class TreeViewController {
587
966
  if (shouldCollapseFilterBranch) this.rowModel.collapseFilterBranch(nodeId);
588
967
  if (!changed && !shouldCollapseFilterBranch) return false;
589
968
  this.#rebuildRows();
590
- this.events.emit('collapse', { nodeId, filter: shouldCollapseFilterBranch });
969
+ this.events.emit('collapse', {
970
+ nodeId,
971
+ filter: shouldCollapseFilterBranch
972
+ });
591
973
  return true;
592
974
  }
593
975
 
594
976
  toggle(nodeId) {
595
977
  const row = this.rowModel.getRowById(nodeId);
596
- return (row?.expanded ?? this.expansion.isExpanded(nodeId)) ? this.collapse(nodeId) : this.expand(nodeId);
978
+ return (row?.expanded ?? this.expansion.isExpanded(nodeId))
979
+ ? this.collapse(nodeId)
980
+ : this.expand(nodeId);
597
981
  }
598
982
 
599
983
  expandAll() {
600
984
  this.expansion.expandAll();
601
985
  this.#rebuildRows();
602
- this.events.emit('expand', { nodeId: null, all: true });
986
+ this.events.emit('expand', {
987
+ nodeId: null,
988
+ all: true
989
+ });
603
990
  }
604
991
 
605
992
  collapseAll() {
606
993
  this.expansion.collapseAll();
607
994
  this.#rebuildRows();
608
- this.events.emit('collapse', { nodeId: null, all: true });
995
+ this.events.emit('collapse', {
996
+ nodeId: null,
997
+ all: true
998
+ });
609
999
  }
610
1000
 
611
1001
  search(query, options = {}) {
612
- for (const id of this.searchHighlights) this.patchBatcher.set(id, { highlighted: false });
1002
+ for (const id of this.searchHighlights)
1003
+ this.patchBatcher.set(id, {
1004
+ highlighted: false
1005
+ });
613
1006
  const results = this.searchIndex.search(query, {
614
1007
  limit: options.limit ?? 500,
615
1008
  fields: options.fields,
616
1009
  caseSensitive: options.caseSensitive,
617
- wholeWord: options.wholeWord,
1010
+ wholeWord: options.wholeWord
618
1011
  });
619
1012
  this.searchHighlights = new Set(results);
620
1013
  const expandedSizeBefore = this.expansion.model.expanded.size;
621
1014
  for (const id of results) {
622
1015
  if (options.expand !== false) this.expansion.expandAncestors(id);
623
- this.patchBatcher.set(id, { highlighted: true });
1016
+ this.patchBatcher.set(id, {
1017
+ highlighted: true
1018
+ });
624
1019
  }
625
- if (options.expand !== false && this.expansion.model.expanded.size !== expandedSizeBefore) this.#rebuildRows();
1020
+ if (options.expand !== false && this.expansion.model.expanded.size !== expandedSizeBefore)
1021
+ this.#rebuildRows();
626
1022
  this.setDynamicState(this.patchBatcher.flush());
1023
+ this.flushDynamicState();
627
1024
  if (results[0] && options.focus !== false) {
628
1025
  this.scrollToNode(results[0], options.align ?? 'nearest');
629
1026
  this.focusedId = results[0];
630
1027
  this.selection.focused = results[0];
631
1028
  if (options.select) this.setSelection([results[0]]);
632
- this.events.emit('focuschange', { nodeId: results[0] });
1029
+ this.events.emit('focuschange', {
1030
+ nodeId: results[0]
1031
+ });
633
1032
  }
634
- this.events.emit('searchchange', { query, results, cursor: this.searchIndex.cursor, current: this.searchIndex.currentSearchResult() });
1033
+ this.events.emit('searchchange', {
1034
+ query,
1035
+ results,
1036
+ cursor: this.searchIndex.cursor,
1037
+ current: this.searchIndex.currentSearchResult()
1038
+ });
635
1039
  return results;
636
1040
  }
637
1041
 
@@ -639,13 +1043,16 @@ export class TreeViewController {
639
1043
  if (!this.workerClient) return this.search(query, options);
640
1044
  const totalStart = now();
641
1045
  const revision = ++this.workerRevision;
642
- for (const id of this.searchHighlights) this.patchBatcher.set(id, { highlighted: false });
1046
+ for (const id of this.searchHighlights)
1047
+ this.patchBatcher.set(id, {
1048
+ highlighted: false
1049
+ });
643
1050
  const searchStart = now();
644
1051
  const results = await this.workerClient.search(query, {
645
1052
  limit: options.limit ?? 500,
646
1053
  fields: options.fields,
647
1054
  caseSensitive: options.caseSensitive,
648
- wholeWord: options.wholeWord,
1055
+ wholeWord: options.wholeWord
649
1056
  });
650
1057
  const searchMs = now() - searchStart;
651
1058
  if (revision !== this.workerRevision) return this.searchIndex.results;
@@ -656,24 +1063,34 @@ export class TreeViewController {
656
1063
  const expandedSizeBefore = this.expansion.model.expanded.size;
657
1064
  for (const id of results) {
658
1065
  if (options.expand !== false) this.expansion.expandAncestors(id);
659
- this.patchBatcher.set(id, { highlighted: true });
1066
+ this.patchBatcher.set(id, {
1067
+ highlighted: true
1068
+ });
660
1069
  }
661
1070
  let rowMs = 0;
662
- const expansionChanged = options.expand !== false && this.expansion.model.expanded.size !== expandedSizeBefore;
1071
+ const expansionChanged =
1072
+ options.expand !== false && this.expansion.model.expanded.size !== expandedSizeBefore;
663
1073
  if (expansionChanged || this.filterQuery) {
664
1074
  const rowStart = now();
665
- const rowResult = await this.workerClient.rebuildRows(this.#workerRowOptions({ filterQuery: this.filterQuery }));
1075
+ const rowResult = await this.workerClient.rebuildRows(
1076
+ this.#workerRowOptions({
1077
+ filterQuery: this.filterQuery
1078
+ })
1079
+ );
666
1080
  rowMs = now() - rowStart;
667
1081
  if (revision !== this.workerRevision) return this.searchIndex.results;
668
1082
  this.#applyWorkerRows(rowResult);
669
1083
  }
670
1084
  this.setDynamicState(this.patchBatcher.flush());
1085
+ this.flushDynamicState();
671
1086
  if (results[0] && options.focus !== false) {
672
1087
  this.scrollToNode(results[0], options.align ?? 'nearest');
673
1088
  this.focusedId = results[0];
674
1089
  this.selection.focused = results[0];
675
1090
  if (options.select) this.setSelection([results[0]]);
676
- this.events.emit('focuschange', { nodeId: results[0] });
1091
+ this.events.emit('focuschange', {
1092
+ nodeId: results[0]
1093
+ });
677
1094
  }
678
1095
  this.events.emit('searchchange', {
679
1096
  query,
@@ -684,18 +1101,27 @@ export class TreeViewController {
684
1101
  searchMs,
685
1102
  rowMs,
686
1103
  workerMs: searchMs + rowMs,
687
- totalMs: now() - totalStart,
1104
+ totalMs: now() - totalStart
688
1105
  });
689
1106
  return results;
690
1107
  }
691
1108
 
692
1109
  clearSearch() {
693
1110
  this.workerRevision++;
694
- for (const id of this.searchHighlights) this.patchBatcher.set(id, { highlighted: false });
1111
+ for (const id of this.searchHighlights)
1112
+ this.patchBatcher.set(id, {
1113
+ highlighted: false
1114
+ });
695
1115
  this.searchHighlights.clear();
696
1116
  this.searchIndex.clear();
697
1117
  this.setDynamicState(this.patchBatcher.flush());
698
- this.events.emit('searchchange', { query: '', results: [], cursor: -1, current: null });
1118
+ this.flushDynamicState();
1119
+ this.events.emit('searchchange', {
1120
+ query: '',
1121
+ results: [],
1122
+ cursor: -1,
1123
+ current: null
1124
+ });
699
1125
  }
700
1126
 
701
1127
  getSearchState() {
@@ -704,21 +1130,37 @@ export class TreeViewController {
704
1130
  results: this.searchIndex.results.slice(),
705
1131
  cursor: this.searchIndex.cursor,
706
1132
  current: this.searchIndex.currentSearchResult(),
707
- count: this.searchIndex.results.length,
1133
+ count: this.searchIndex.results.length
708
1134
  };
709
1135
  }
710
1136
 
711
1137
  nextSearchResult() {
712
1138
  const id = this.searchIndex.nextSearchResult();
713
- if (id) this.focusNode(id, { select: false });
714
- this.events.emit('searchchange', { query: this.searchIndex.lastQuery, results: this.searchIndex.results.slice(), cursor: this.searchIndex.cursor, current: id });
1139
+ if (id)
1140
+ this.focusNode(id, {
1141
+ select: false
1142
+ });
1143
+ this.events.emit('searchchange', {
1144
+ query: this.searchIndex.lastQuery,
1145
+ results: this.searchIndex.results.slice(),
1146
+ cursor: this.searchIndex.cursor,
1147
+ current: id
1148
+ });
715
1149
  return id;
716
1150
  }
717
1151
 
718
1152
  previousSearchResult() {
719
1153
  const id = this.searchIndex.previousSearchResult();
720
- if (id) this.focusNode(id, { select: false });
721
- this.events.emit('searchchange', { query: this.searchIndex.lastQuery, results: this.searchIndex.results.slice(), cursor: this.searchIndex.cursor, current: id });
1154
+ if (id)
1155
+ this.focusNode(id, {
1156
+ select: false
1157
+ });
1158
+ this.events.emit('searchchange', {
1159
+ query: this.searchIndex.lastQuery,
1160
+ results: this.searchIndex.results.slice(),
1161
+ cursor: this.searchIndex.cursor,
1162
+ current: id
1163
+ });
722
1164
  return id;
723
1165
  }
724
1166
 
@@ -730,7 +1172,9 @@ export class TreeViewController {
730
1172
  this.focusedId = nodeId;
731
1173
  this.selection.focused = nodeId;
732
1174
  if (options.select) this.setSelection([nodeId]);
733
- this.events.emit('focuschange', { nodeId });
1175
+ this.events.emit('focuschange', {
1176
+ nodeId
1177
+ });
734
1178
  return true;
735
1179
  }
736
1180
 
@@ -752,9 +1196,14 @@ export class TreeViewController {
752
1196
  for (const id of ids) this.selection.selected.add(id);
753
1197
  this.selection.focused = ids[ids.length - 1] ?? null;
754
1198
  this.focusedId = this.selection.focused;
755
- this.anchorRowIndex = this.focusedId ? this.rowModel.getRowById(this.focusedId)?.rowIndex ?? null : null;
1199
+ this.anchorRowIndex = this.focusedId
1200
+ ? (this.rowModel.getRowById(this.focusedId)?.rowIndex ?? null)
1201
+ : null;
756
1202
  this.#syncSelectionState();
757
- this.events.emit('selectionchange', { selection: this.getSelection(), focusedId: this.focusedId });
1203
+ this.events.emit('selectionchange', {
1204
+ selection: this.getSelection(),
1205
+ focusedId: this.focusedId
1206
+ });
758
1207
  }
759
1208
 
760
1209
  clearSelection() {
@@ -765,7 +1214,9 @@ export class TreeViewController {
765
1214
  if (this.hoverId === nodeId) return;
766
1215
  this.hoverId = nodeId;
767
1216
  this.hoverPart = null;
768
- this.events.emit('nodehover', { nodeId });
1217
+ this.events.emit('nodehover', {
1218
+ nodeId
1219
+ });
769
1220
  }
770
1221
 
771
1222
  setHoverHit(hit) {
@@ -774,7 +1225,10 @@ export class TreeViewController {
774
1225
  if (this.hoverId === nodeId && this.hoverPart === part) return;
775
1226
  this.hoverId = nodeId;
776
1227
  this.hoverPart = part;
777
- this.events.emit('nodehover', { nodeId, part });
1228
+ this.events.emit('nodehover', {
1229
+ nodeId,
1230
+ part
1231
+ });
778
1232
  }
779
1233
 
780
1234
  setActiveHit(hit) {
@@ -790,14 +1244,22 @@ export class TreeViewController {
790
1244
  const row = this.rowModel.getRowById(nodeId);
791
1245
  if (!row) return;
792
1246
  this.selectRow(row.rowIndex, event);
793
- this.events.emit('nodeclick', { nodeId, row, originalEvent: event });
1247
+ this.events.emit('nodeclick', {
1248
+ nodeId,
1249
+ row,
1250
+ originalEvent: event
1251
+ });
794
1252
  }
795
1253
 
796
1254
  doubleClickNode(nodeId, event = {}) {
797
1255
  const row = this.rowModel.getRowById(nodeId);
798
1256
  if (!row) return;
799
1257
  if (row.hasChildren) this.toggle(nodeId);
800
- this.events.emit('nodedblclick', { nodeId, row, originalEvent: event });
1258
+ this.events.emit('nodedblclick', {
1259
+ nodeId,
1260
+ row,
1261
+ originalEvent: event
1262
+ });
801
1263
  }
802
1264
 
803
1265
  selectRow(rowIndex, event = {}) {
@@ -820,8 +1282,13 @@ export class TreeViewController {
820
1282
  this.focusedId = row.nodeId;
821
1283
  this.selection.focused = row.nodeId;
822
1284
  this.#syncSelectionState();
823
- this.events.emit('selectionchange', { selection: this.getSelection(), focusedId: this.focusedId });
824
- this.events.emit('focuschange', { nodeId: this.focusedId });
1285
+ this.events.emit('selectionchange', {
1286
+ selection: this.getSelection(),
1287
+ focusedId: this.focusedId
1288
+ });
1289
+ this.events.emit('focuschange', {
1290
+ nodeId: this.focusedId
1291
+ });
825
1292
  return true;
826
1293
  }
827
1294
 
@@ -829,7 +1296,9 @@ export class TreeViewController {
829
1296
  if (!this.rowModel.rows.length) return false;
830
1297
  if (this.rowReorder && event.altKey && ['ArrowUp', 'ArrowDown'].includes(event.key)) {
831
1298
  const id = this.focusedId ?? this.rowModel.getRow(this.#focusedRowIndex())?.nodeId;
832
- this.moveRowBy(id, event.key === 'ArrowUp' ? -1 : 1, { source: 'keyboard' });
1299
+ this.moveRowBy(id, event.key === 'ArrowUp' ? -1 : 1, {
1300
+ source: 'keyboard'
1301
+ });
833
1302
  return true;
834
1303
  }
835
1304
  if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
@@ -839,15 +1308,28 @@ export class TreeViewController {
839
1308
  this.focusedId = focusedRow?.nodeId ?? this.focusedId;
840
1309
  this.selection.focused = this.focusedId;
841
1310
  this.#syncSelectionState();
842
- this.events.emit('selectionchange', { selection: this.getSelection(), focusedId: this.focusedId });
1311
+ this.events.emit('selectionchange', {
1312
+ selection: this.getSelection(),
1313
+ focusedId: this.focusedId
1314
+ });
843
1315
  return true;
844
1316
  }
845
1317
  const currentRow = this.#focusedRowIndex();
846
1318
  let target = currentRow;
847
1319
  if (event.key === 'ArrowDown') target = Math.min(this.rowModel.rows.length - 1, currentRow + 1);
848
1320
  else if (event.key === 'ArrowUp') target = Math.max(0, currentRow - 1);
849
- else if (event.key === 'PageDown') target = Math.min(this.rowModel.rows.length - 1, currentRow + Math.max(1, Math.floor(this.viewport.rowViewportHeight / this.rowModel.rowHeight) - 1));
850
- else if (event.key === 'PageUp') target = Math.max(0, currentRow - Math.max(1, Math.floor(this.viewport.rowViewportHeight / this.rowModel.rowHeight) - 1));
1321
+ else if (event.key === 'PageDown')
1322
+ target = Math.min(
1323
+ this.rowModel.rows.length - 1,
1324
+ currentRow +
1325
+ Math.max(1, Math.floor(this.viewport.rowViewportHeight / this.rowModel.rowHeight) - 1)
1326
+ );
1327
+ else if (event.key === 'PageUp')
1328
+ target = Math.max(
1329
+ 0,
1330
+ currentRow -
1331
+ Math.max(1, Math.floor(this.viewport.rowViewportHeight / this.rowModel.rowHeight) - 1)
1332
+ );
851
1333
  else if (event.key === 'Home') target = 0;
852
1334
  else if (event.key === 'End') target = this.rowModel.rows.length - 1;
853
1335
  else if (event.key === 'ArrowRight') {
@@ -862,13 +1344,20 @@ export class TreeViewController {
862
1344
  if (parentRow) target = parentRow.rowIndex;
863
1345
  } else if (event.key === 'Enter' || event.key === ' ') {
864
1346
  const row = this.rowModel.getRow(currentRow);
865
- if (row) this.selectRow(row.rowIndex, { ctrlKey: true });
1347
+ if (row)
1348
+ this.selectRow(row.rowIndex, {
1349
+ ctrlKey: true
1350
+ });
866
1351
  return true;
867
1352
  } else return false;
868
1353
 
869
1354
  const row = this.rowModel.getRow(target);
870
1355
  if (!row) return false;
871
- this.selectRow(target, { shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey });
1356
+ this.selectRow(target, {
1357
+ shiftKey: event.shiftKey,
1358
+ ctrlKey: event.ctrlKey,
1359
+ metaKey: event.metaKey
1360
+ });
872
1361
  this.viewport.scrollRowIntoView(target, 'nearest');
873
1362
  this.events.emit('viewportchange', this.getViewportState());
874
1363
  return true;
@@ -885,24 +1374,40 @@ export class TreeViewController {
885
1374
  }
886
1375
 
887
1376
  render(time = performance.now()) {
1377
+ this.cancelRender();
888
1378
  this.renderMeasured(time);
889
1379
  }
890
1380
 
891
1381
  renderMeasured(time = performance.now()) {
1382
+ if (this.renderFrame !== null) this.cancelRender();
1383
+ this.flushDynamicState();
892
1384
  const sceneStart = performance.now();
893
1385
  this.scene = this.createRenderScene();
894
1386
  const sceneMs = performance.now() - sceneStart;
895
1387
  this.renderer.setScene(this.scene);
896
1388
  const renderStart = performance.now();
897
1389
  this.renderer.render(this.scene, time);
1390
+ this.actionOverlay?.render(this.scene, this.invalidation);
1391
+ this.tooltip?.refresh();
898
1392
  const renderMs = performance.now() - renderStart;
899
1393
  this.lastRenderedRows = this.renderer.renderedRows ?? 0;
900
- return { scene: this.scene, sceneMs, renderMs, renderedRows: this.lastRenderedRows };
1394
+ this.performanceStats.rendersExecuted++;
1395
+ this.lastPatchCount = 0;
1396
+ this.invalidation.full = false;
1397
+ this.invalidation.dirtyNodeIds.clear();
1398
+ return {
1399
+ scene: this.scene,
1400
+ sceneMs,
1401
+ renderMs,
1402
+ renderedRows: this.lastRenderedRows
1403
+ };
901
1404
  }
902
1405
 
903
1406
  createRenderScene() {
1407
+ this.flushDynamicState();
904
1408
  const visibleRange = this.rowModel.getVisibleRange(this.viewport, 4);
905
1409
  return {
1410
+ rowDrag: this.rowDrag,
906
1411
  rowReorder: this.rowReorder,
907
1412
  canReorderRows: this.canReorderRows(),
908
1413
  rowDrop: this.rowDrop,
@@ -923,11 +1428,10 @@ export class TreeViewController {
923
1428
  focusNodeId: this.focusedId,
924
1429
  searchMatches: this.searchHighlights,
925
1430
  sort: this.columnModel.sort,
926
- sortValues: this.sortValueSnapshot ? Array.from(this.sortValueSnapshot) : null,
927
1431
  inspectorPaneLabelEnd: this.#computeInspectorPaneLabelEnd(visibleRange),
928
1432
  filterQuery: this.filterQuery,
929
1433
  headerFilter: this.headerFilter ?? Boolean(this.inspector?.options?.filter),
930
- stats: this.getStats(),
1434
+ stats: this.getStats()
931
1435
  };
932
1436
  }
933
1437
 
@@ -944,12 +1448,46 @@ export class TreeViewController {
944
1448
  const x = localX + this.viewport.scrollX;
945
1449
  if (localY < this.viewport.headerHeight) {
946
1450
  const resizeColumn = this.columnModel.getResizeHandleAt(x);
947
- if (resizeColumn) return { area: 'header', part: 'resize', column: resizeColumn, x, y: localY };
1451
+ if (resizeColumn)
1452
+ return {
1453
+ area: 'header',
1454
+ part: 'resize',
1455
+ column: resizeColumn,
1456
+ x,
1457
+ y: localY
1458
+ };
948
1459
  const column = this.columnModel.getColumnAt(x);
949
- if (column && (this.headerFilter ?? Boolean(this.inspector?.options?.filter)) && column === this.columnModel.columns.find(item => item.kind !== 'rowOrder')) {
950
- return { area: 'header', part: 'filter', column, x, y: localY };
1460
+ if (
1461
+ column &&
1462
+ (this.headerFilter ?? Boolean(this.inspector?.options?.filter)) &&
1463
+ column ===
1464
+ this.columnModel.columns.find(
1465
+ (item) => item.kind !== 'rowOrder' && item.id !== '__vtc_actions'
1466
+ )
1467
+ ) {
1468
+ return {
1469
+ area: 'header',
1470
+ part: 'filter',
1471
+ column,
1472
+ x,
1473
+ y: localY
1474
+ };
951
1475
  }
952
- return column ? { area: 'header', part: 'label', column, x, y: localY } : { area: 'header', part: 'header', column: null, x, y: localY };
1476
+ return column
1477
+ ? {
1478
+ area: 'header',
1479
+ part: 'label',
1480
+ column,
1481
+ x,
1482
+ y: localY
1483
+ }
1484
+ : {
1485
+ area: 'header',
1486
+ part: 'header',
1487
+ column: null,
1488
+ x,
1489
+ y: localY
1490
+ };
953
1491
  }
954
1492
 
955
1493
  if (localY >= this.viewport.headerHeight + this.viewport.rowViewportHeight) return null;
@@ -958,11 +1496,26 @@ export class TreeViewController {
958
1496
  const row = this.rowModel.getRow(rowIndex);
959
1497
  if (!row) return null;
960
1498
  const column = this.columnModel.getColumnAt(x);
961
- if (!column) return { area: 'row', part: 'row', row, column: null, x, y: rowY };
1499
+ if (!column)
1500
+ return {
1501
+ area: 'row',
1502
+ part: 'row',
1503
+ row,
1504
+ column: null,
1505
+ x,
1506
+ y: rowY
1507
+ };
962
1508
 
963
1509
  if (column.kind === 'rowOrder') {
964
1510
  const offset = x - column.x;
965
- return { area: 'row', part: offset < 24 ? 'rowDrag' : offset < 48 ? 'rowUp' : 'rowDown', row, column, x, y: rowY };
1511
+ return {
1512
+ area: 'row',
1513
+ part: offset < 24 ? 'rowDrag' : offset < 48 ? 'rowUp' : 'rowDown',
1514
+ row,
1515
+ column,
1516
+ x,
1517
+ y: rowY
1518
+ };
966
1519
  }
967
1520
  let part = 'cell';
968
1521
  if (column.kind === 'inspectorPane') {
@@ -971,23 +1524,46 @@ export class TreeViewController {
971
1524
  if (node?.data?.valueType === 'object') {
972
1525
  const treeX = row.depth * this.rowModel.indentWidth;
973
1526
  part = localX >= treeX + 4 && localX <= treeX + 22 ? 'chevron' : 'label';
974
- return { area: 'row', part, row, column, x, y: rowY };
1527
+ return {
1528
+ area: 'row',
1529
+ part,
1530
+ row,
1531
+ column,
1532
+ x,
1533
+ y: rowY
1534
+ };
975
1535
  }
976
1536
  if (node?.data?.valueType === 'array') {
977
1537
  if (localX >= column.width - 54 && localX <= column.width - 32) part = 'arrayAdd';
978
1538
  else if (localX >= column.width - 28 && localX <= column.width - 6) part = 'arrayRemove';
979
1539
  else {
980
- const editorLeft = this.getInspectorPaneLayout(this.#visibleInspectorPaneWidth(column), row, node?.data?.editorType).editorLeft;
1540
+ const editorLeft = this.getInspectorPaneLayout(
1541
+ this.#visibleInspectorPaneWidth(column),
1542
+ row,
1543
+ node?.data?.editorType
1544
+ ).editorLeft;
981
1545
  if (localX >= editorLeft) part = 'editor';
982
1546
  else {
983
1547
  const treeX = row.depth * this.rowModel.indentWidth;
984
1548
  part = localX >= treeX + 4 && localX <= treeX + 22 ? 'chevron' : 'label';
985
1549
  }
986
1550
  }
987
- return { area: 'row', part, row, column, x, y: rowY };
1551
+ return {
1552
+ area: 'row',
1553
+ part,
1554
+ row,
1555
+ column,
1556
+ x,
1557
+ y: rowY
1558
+ };
988
1559
  }
989
- const layout = this.getInspectorPaneLayout(this.#visibleInspectorPaneWidth(column), row, node?.data?.editorType);
990
- if (localX >= layout.editorLeft) part = this.#inspectorEditorPart(row, localX - layout.editorLeft, layout.editorWidth);
1560
+ const layout = this.getInspectorPaneLayout(
1561
+ this.#visibleInspectorPaneWidth(column),
1562
+ row,
1563
+ node?.data?.editorType
1564
+ );
1565
+ if (localX >= layout.editorLeft)
1566
+ part = this.#inspectorEditorPart(row, localX - layout.editorLeft, layout.editorWidth);
991
1567
  else {
992
1568
  const treeX = row.depth * this.rowModel.indentWidth;
993
1569
  if (localX >= treeX + 4 && localX <= treeX + 22) part = 'chevron';
@@ -995,15 +1571,27 @@ export class TreeViewController {
995
1571
  }
996
1572
  } else if (column.kind === 'tree') {
997
1573
  const localX = x - column.x;
998
- const treeX = row.depth * this.rowModel.indentWidth;
999
- if (localX >= treeX + 4 && localX <= treeX + 22) part = 'chevron';
1000
- else if (localX >= treeX + 26 && localX <= treeX + 44) part = 'icon';
1001
- else if (localX >= treeX + 48) part = 'label';
1574
+ const layout = treeCellLayout(
1575
+ row.depth,
1576
+ this.rowModel.indentWidth,
1577
+ this.model.index.childrenByParent.size > 1
1578
+ );
1579
+ if (row.hasChildren && localX >= layout.chevronX - 6 && localX <= layout.chevronX + 12)
1580
+ part = 'chevron';
1581
+ else if (localX >= layout.iconX - 1 && localX <= layout.iconX + 17) part = 'icon';
1582
+ else if (localX >= layout.labelX - 2) part = 'label';
1002
1583
  else part = 'cell';
1003
1584
  } else if (column.kind === 'inspectorValue') {
1004
1585
  part = this.#inspectorEditorPart(row, x - column.x, column.width);
1005
1586
  }
1006
- return { area: 'row', part, row, column, x, y: rowY };
1587
+ return {
1588
+ area: 'row',
1589
+ part,
1590
+ row,
1591
+ column,
1592
+ x,
1593
+ y: rowY
1594
+ };
1007
1595
  }
1008
1596
 
1009
1597
  #inspectorEditorPart(row, localEditorX, editorWidth) {
@@ -1022,33 +1610,62 @@ export class TreeViewController {
1022
1610
  }
1023
1611
 
1024
1612
  #visibleInspectorPaneWidth(column) {
1025
- return Math.max(1, Math.min(column.width, this.viewport.scrollX + this.viewport.contentViewportWidth - column.x));
1613
+ return Math.max(
1614
+ 1,
1615
+ Math.min(column.width, this.viewport.scrollX + this.viewport.contentViewportWidth - column.x)
1616
+ );
1026
1617
  }
1027
1618
 
1028
1619
  getInspectorPaneLayout(width, row = null, editorType = '') {
1029
- return inspectorPaneLayout(width, row?.depth ?? 0, this.rowModel.indentWidth, editorType, this.#computeInspectorPaneLabelEnd(this.rowModel.getVisibleRange(this.viewport, 4)));
1620
+ return inspectorPaneLayout(
1621
+ width,
1622
+ row?.depth ?? 0,
1623
+ this.rowModel.indentWidth,
1624
+ editorType,
1625
+ this.#computeInspectorPaneLabelEnd(this.rowModel.getVisibleRange(this.viewport, 4))
1626
+ );
1030
1627
  }
1031
1628
 
1032
1629
  getCellClientRect(hit) {
1033
- if (!hit?.row || !hit.column || !this.canvas) return { x: 0, y: 0, width: 0, height: 0 };
1630
+ if (!hit?.row || !hit.column || !this.canvas)
1631
+ return {
1632
+ x: 0,
1633
+ y: 0,
1634
+ width: 0,
1635
+ height: 0
1636
+ };
1034
1637
  const rect = this.canvas.getBoundingClientRect();
1035
1638
  return {
1036
1639
  x: rect.left + (this.viewport.renderInsetX ?? 0) + hit.column.x - this.viewport.scrollX,
1037
- y: rect.top + (this.viewport.renderInsetY ?? 0) + this.viewport.headerHeight + hit.row.y - this.viewport.scrollY,
1640
+ y:
1641
+ rect.top +
1642
+ (this.viewport.renderInsetY ?? 0) +
1643
+ this.viewport.headerHeight +
1644
+ hit.row.y -
1645
+ this.viewport.scrollY,
1038
1646
  width: hit.column.width,
1039
- height: hit.row.height,
1647
+ height: hit.row.height
1040
1648
  };
1041
1649
  }
1042
1650
 
1043
1651
  getHeaderClientRect(hit) {
1044
- if (!hit?.column || !this.canvas) return { x: 0, y: 0, width: 0, height: 0 };
1652
+ if (!hit?.column || !this.canvas)
1653
+ return {
1654
+ x: 0,
1655
+ y: 0,
1656
+ width: 0,
1657
+ height: 0
1658
+ };
1045
1659
  const rect = this.canvas.getBoundingClientRect();
1046
- const visibleWidth = Math.max(0, this.viewport.contentViewportWidth - Math.max(0, hit.column.x - this.viewport.scrollX));
1660
+ const visibleWidth = Math.max(
1661
+ 0,
1662
+ this.viewport.contentViewportWidth - Math.max(0, hit.column.x - this.viewport.scrollX)
1663
+ );
1047
1664
  return {
1048
1665
  x: rect.left + (this.viewport.renderInsetX ?? 0) + hit.column.x - this.viewport.scrollX,
1049
1666
  y: rect.top + (this.viewport.renderInsetY ?? 0),
1050
1667
  width: Math.min(hit.column.width, visibleWidth || hit.column.width),
1051
- height: this.viewport.headerHeight,
1668
+ height: this.viewport.headerHeight
1052
1669
  };
1053
1670
  }
1054
1671
 
@@ -1068,7 +1685,10 @@ export class TreeViewController {
1068
1685
  if (hit.part === 'label' || hit.part === 'chevron') {
1069
1686
  const labelX = hit.row.depth * this.rowModel.indentWidth + (hit.row.hasChildren ? 28 : 24);
1070
1687
  text = node.label ?? node.id;
1071
- width = data.valueType === 'object' ? Math.max(0, visibleWidth - labelX - 8) : Math.max(0, layout.editorLeft - labelX - 8);
1688
+ width =
1689
+ data.valueType === 'object'
1690
+ ? Math.max(0, visibleWidth - labelX - 8)
1691
+ : Math.max(0, layout.editorLeft - labelX - 8);
1072
1692
  } else if (hit.part === 'arrayAdd' || hit.part === 'arrayRemove') {
1073
1693
  return null;
1074
1694
  } else {
@@ -1076,7 +1696,11 @@ export class TreeViewController {
1076
1696
  width = Math.max(0, layout.editorWidth - 20);
1077
1697
  }
1078
1698
  } else if (hit.column.kind === 'tree') {
1079
- const labelX = hit.row.depth * this.rowModel.indentWidth + 50;
1699
+ const { labelX } = treeCellLayout(
1700
+ hit.row.depth,
1701
+ this.rowModel.indentWidth,
1702
+ this.model.index.childrenByParent.size > 1
1703
+ );
1080
1704
  text = node.label ?? node.id;
1081
1705
  width = Math.max(0, hit.column.width - labelX - 6);
1082
1706
  } else if (hit.column.kind === 'inspectorValue') {
@@ -1090,17 +1714,28 @@ export class TreeViewController {
1090
1714
  width = Math.max(0, hit.column.width - 20);
1091
1715
  } else if (typeof hit.column.value === 'function') {
1092
1716
  const value = hit.column.value(node, state);
1093
- text = value == null ? '' : String(value);
1717
+ text = hit.column.format
1718
+ ? hit.column.format(value, node, state)
1719
+ : value == null
1720
+ ? ''
1721
+ : String(value);
1094
1722
  width = Math.max(0, hit.column.width - 20);
1095
1723
  }
1096
1724
 
1097
1725
  text = String(text ?? '');
1098
1726
  if (!text || !isProbablyTruncated(text, width)) return null;
1099
- return { text, rect, nodeId: node.id, part: hit.part, columnId: hit.column.id };
1727
+ return {
1728
+ text,
1729
+ rect,
1730
+ nodeId: node.id,
1731
+ part: hit.part,
1732
+ columnId: hit.column.id
1733
+ };
1100
1734
  }
1101
1735
 
1102
1736
  resize(width, height) {
1103
- if (width !== this.viewport.viewportWidth || height !== this.viewport.viewportHeight) this.closeEditor();
1737
+ if (width !== this.viewport.viewportWidth || height !== this.viewport.viewportHeight)
1738
+ this.closeEditor();
1104
1739
  this.viewport.resize(width, height);
1105
1740
  this.#fitInspectorPaneColumn(width);
1106
1741
  this.#syncContentSize();
@@ -1108,6 +1743,7 @@ export class TreeViewController {
1108
1743
  }
1109
1744
 
1110
1745
  getStats() {
1746
+ this.flushDynamicState();
1111
1747
  return {
1112
1748
  totalNodes: this.model.nodes.length,
1113
1749
  visibleRows: this.rowModel.rows.length,
@@ -1116,6 +1752,9 @@ export class TreeViewController {
1116
1752
  dirtyNodes: this.lastDirtyNodeCount,
1117
1753
  selectedCount: this.selection.selected.size,
1118
1754
  rebuildCount: this.rebuildCount,
1755
+ ...this.performanceStats,
1756
+ rowActionsFullUpdates: this.actionOverlay?.fullUpdates ?? 0,
1757
+ rowActionsIncrementalUpdates: this.actionOverlay?.incrementalUpdates ?? 0
1119
1758
  };
1120
1759
  }
1121
1760
 
@@ -1132,10 +1771,28 @@ export class TreeViewController {
1132
1771
  viewportHeight: this.viewport.viewportHeight,
1133
1772
  contentWidth: this.viewport.contentWidth,
1134
1773
  contentHeight: this.viewport.contentHeight,
1135
- zoom: this.viewport.zoom,
1774
+ zoom: this.viewport.zoom
1136
1775
  };
1137
1776
  }
1138
1777
 
1778
+ #hasInvalidation() {
1779
+ return this.invalidation.full || this.invalidation.dirtyNodeIds.size > 0;
1780
+ }
1781
+
1782
+ /** @param {Set<string>} ids */
1783
+ #hasVisibleDirtyNode(ids) {
1784
+ const rowIndexById = this.rowModel.rowIndexById;
1785
+ if (!(rowIndexById instanceof Map) || !this.viewport) return true;
1786
+ const range = this.rowModel.getVisibleRange(this.viewport, 4);
1787
+ for (const id of ids) {
1788
+ const index = rowIndexById.get(id);
1789
+ if (index !== undefined && index >= range.first && index <= range.last) return true;
1790
+ }
1791
+ const stickyRows = this.rowReorder ? [] : this.rowModel.getStickyRows(this.viewport);
1792
+ for (const row of stickyRows) if (ids.has(row.nodeId)) return true;
1793
+ return false;
1794
+ }
1795
+
1139
1796
  #rebuildRows() {
1140
1797
  const scrollAnchor = this.#captureScrollAnchor();
1141
1798
  this.rowModel.rebuild();
@@ -1147,17 +1804,25 @@ export class TreeViewController {
1147
1804
 
1148
1805
  #syncContentSize() {
1149
1806
  this.#syncScrollbarState(this.columnModel.contentWidth, this.rowModel.contentHeight);
1150
- if (this.viewport.viewportWidth > 1) this.#fitInspectorPaneColumn(this.viewport.contentViewportWidth);
1807
+ if (this.viewport.viewportWidth > 1)
1808
+ this.#fitInspectorPaneColumn(this.viewport.contentViewportWidth);
1151
1809
  this.#syncScrollbarState(this.columnModel.contentWidth, this.rowModel.contentHeight);
1152
1810
  this.viewport.setContentSize(this.columnModel.contentWidth, this.rowModel.contentHeight);
1153
1811
  }
1154
1812
 
1155
1813
  #syncSelectionState() {
1156
1814
  for (const [id, state] of this.model.dynamicState) {
1157
- if (state.selected) this.patchBatcher.set(id, { selected: false });
1815
+ if (state.selected)
1816
+ this.patchBatcher.set(id, {
1817
+ selected: false
1818
+ });
1158
1819
  }
1159
- for (const id of this.selection.selected) this.patchBatcher.set(id, { selected: true });
1820
+ for (const id of this.selection.selected)
1821
+ this.patchBatcher.set(id, {
1822
+ selected: true
1823
+ });
1160
1824
  this.setDynamicState(this.patchBatcher.flush());
1825
+ this.flushDynamicState();
1161
1826
  }
1162
1827
 
1163
1828
  #focusedRowIndex() {
@@ -1175,15 +1840,25 @@ export class TreeViewController {
1175
1840
  const entry = entries[0];
1176
1841
  const width = Math.floor(entry?.contentRect?.width ?? 0);
1177
1842
  const height = Math.floor(entry?.contentRect?.height ?? 0);
1178
- if (width > 0 && height > 0) this.resize(width, height);
1843
+ if (width > 0 && height > 0) {
1844
+ this.resize(width, height);
1845
+ if (this.autoRender) {
1846
+ this.cancelRender();
1847
+ this.render();
1848
+ }
1849
+ }
1179
1850
  });
1180
1851
  this.#resizeObserver.observe(this.canvas);
1181
1852
  }
1182
1853
 
1183
1854
  #resizeToCanvasClientSize() {
1184
1855
  if (!this.canvas) return;
1185
- const width = Math.floor(this.canvas.clientWidth || this.canvas.getBoundingClientRect().width || 0);
1186
- const height = Math.floor(this.canvas.clientHeight || this.canvas.getBoundingClientRect().height || 0);
1856
+ const width = Math.floor(
1857
+ this.canvas.clientWidth || this.canvas.getBoundingClientRect().width || 0
1858
+ );
1859
+ const height = Math.floor(
1860
+ this.canvas.clientHeight || this.canvas.getBoundingClientRect().height || 0
1861
+ );
1187
1862
  if (width > 0 && height > 0) this.resize(width, height);
1188
1863
  }
1189
1864
 
@@ -1225,7 +1900,7 @@ export class TreeViewController {
1225
1900
  height: '1px',
1226
1901
  overflowX: 'hidden',
1227
1902
  overflowY: 'auto',
1228
- zIndex: '4',
1903
+ zIndex: '4'
1229
1904
  });
1230
1905
  Object.assign(horizontal.style, {
1231
1906
  position: 'absolute',
@@ -1235,7 +1910,7 @@ export class TreeViewController {
1235
1910
  height: `${size}px`,
1236
1911
  overflowX: 'auto',
1237
1912
  overflowY: 'hidden',
1238
- zIndex: '4',
1913
+ zIndex: '4'
1239
1914
  });
1240
1915
  Object.assign(corner.style, {
1241
1916
  position: 'absolute',
@@ -1245,15 +1920,15 @@ export class TreeViewController {
1245
1920
  height: `${size}px`,
1246
1921
  zIndex: '4',
1247
1922
  background: 'transparent',
1248
- pointerEvents: 'none',
1923
+ pointerEvents: 'none'
1249
1924
  });
1250
1925
  Object.assign(verticalSpacer.style, {
1251
1926
  width: '1px',
1252
- minHeight: '1px',
1927
+ minHeight: '1px'
1253
1928
  });
1254
1929
  Object.assign(horizontalSpacer.style, {
1255
1930
  height: '1px',
1256
- minWidth: '1px',
1931
+ minWidth: '1px'
1257
1932
  });
1258
1933
 
1259
1934
  host.appendChild(vertical);
@@ -1301,8 +1976,12 @@ export class TreeViewController {
1301
1976
  };
1302
1977
  const onViewportChange = () => sync();
1303
1978
 
1304
- vertical.addEventListener('scroll', onVerticalScroll, { passive: true });
1305
- horizontal.addEventListener('scroll', onHorizontalScroll, { passive: true });
1979
+ vertical.addEventListener('scroll', onVerticalScroll, {
1980
+ passive: true
1981
+ });
1982
+ horizontal.addEventListener('scroll', onHorizontalScroll, {
1983
+ passive: true
1984
+ });
1306
1985
  this.viewport.addEventListener('change', onViewportChange);
1307
1986
  sync();
1308
1987
 
@@ -1317,16 +1996,24 @@ export class TreeViewController {
1317
1996
  horizontal.remove();
1318
1997
  corner.remove();
1319
1998
  this.#nativeScrollbarSize = 0;
1320
- this.viewport.setScrollbarState({ size: 0, vertical: false, horizontal: false });
1999
+ this.viewport.setScrollbarState({
2000
+ size: 0,
2001
+ vertical: false,
2002
+ horizontal: false
2003
+ });
1321
2004
  if (hostPositionChanged) host.style.position = previousHostPosition;
1322
- },
2005
+ }
1323
2006
  };
1324
2007
  }
1325
2008
 
1326
2009
  #syncScrollbarState(contentWidth, contentHeight) {
1327
2010
  const size = this.nativeScrollbars ? this.#nativeScrollbarSize : 0;
1328
2011
  if (!size) {
1329
- this.viewport.setScrollbarState({ size: 0, vertical: false, horizontal: false });
2012
+ this.viewport.setScrollbarState({
2013
+ size: 0,
2014
+ vertical: false,
2015
+ horizontal: false
2016
+ });
1330
2017
  return;
1331
2018
  }
1332
2019
  let availableWidth = this.viewport.viewportWidth;
@@ -1335,16 +2022,45 @@ export class TreeViewController {
1335
2022
  let showX = contentWidth > availableWidth;
1336
2023
  if (showY && contentWidth > Math.max(1, availableWidth - size)) showX = true;
1337
2024
  if (showX && contentHeight > Math.max(1, availableRowHeight - size)) showY = true;
1338
- this.viewport.setScrollbarState({ size, vertical: showY, horizontal: showX });
2025
+ this.viewport.setScrollbarState({
2026
+ size,
2027
+ vertical: showY,
2028
+ horizontal: showX
2029
+ });
1339
2030
  }
1340
2031
 
1341
2032
  #canvasHostMetrics() {
1342
- if (!this.canvas) return { left: 0, top: 0, width: this.viewport.viewportWidth, height: this.viewport.viewportHeight };
2033
+ if (!this.canvas)
2034
+ return {
2035
+ left: 0,
2036
+ top: 0,
2037
+ width: this.viewport.viewportWidth,
2038
+ height: this.viewport.viewportHeight
2039
+ };
1343
2040
  const left = Number.isFinite(this.canvas.offsetLeft) ? this.canvas.offsetLeft : 0;
1344
2041
  const top = Number.isFinite(this.canvas.offsetTop) ? this.canvas.offsetTop : 0;
1345
- const width = Math.max(1, Math.floor(this.canvas.clientWidth || this.canvas.getBoundingClientRect?.().width || this.viewport.viewportWidth));
1346
- const height = Math.max(1, Math.floor(this.canvas.clientHeight || this.canvas.getBoundingClientRect?.().height || this.viewport.viewportHeight));
1347
- return { left, top, width, height };
2042
+ const width = Math.max(
2043
+ 1,
2044
+ Math.floor(
2045
+ this.canvas.clientWidth ||
2046
+ this.canvas.getBoundingClientRect?.().width ||
2047
+ this.viewport.viewportWidth
2048
+ )
2049
+ );
2050
+ const height = Math.max(
2051
+ 1,
2052
+ Math.floor(
2053
+ this.canvas.clientHeight ||
2054
+ this.canvas.getBoundingClientRect?.().height ||
2055
+ this.viewport.viewportHeight
2056
+ )
2057
+ );
2058
+ return {
2059
+ left,
2060
+ top,
2061
+ width,
2062
+ height
2063
+ };
1348
2064
  }
1349
2065
 
1350
2066
  #computeInspectorPaneLabelEnd(visibleRange) {
@@ -1366,7 +2082,13 @@ export class TreeViewController {
1366
2082
  }
1367
2083
 
1368
2084
  #fitInspectorPaneColumn(width) {
1369
- const column = this.columnModel.columns.length === 1 ? this.columnModel.columns[0] : null;
2085
+ const columns = this.columnModel.columns.filter(
2086
+ (c) => !['__vtc_actions', '__vtc_row_order'].includes(c.id)
2087
+ );
2088
+ const column = columns.length === 1 ? columns[0] : null;
2089
+ width -= this.columnModel.columns
2090
+ .filter((c) => !columns.includes(c))
2091
+ .reduce((sum, c) => sum + c.width, 0);
1370
2092
  if (column?.kind !== 'inspectorPane' && column?.kind !== 'tree') return;
1371
2093
  if (column.kind === 'inspectorPane' && this.inspector?.options?.presentation !== 'pane') return;
1372
2094
  const nextWidth = Math.max(column.minWidth, Math.floor(width));
@@ -1381,9 +2103,10 @@ export class TreeViewController {
1381
2103
  rowHeight: this.rowModel.rowHeight,
1382
2104
  indentWidth: this.rowModel.indentWidth,
1383
2105
  sort: this.columnModel.sort,
2106
+ sortValues: this.sortValueSnapshot ? Array.from(this.sortValueSnapshot) : null,
1384
2107
  filterQuery: this.filterQuery,
1385
2108
  filterOptions: this.filterOptions,
1386
- ...overrides,
2109
+ ...overrides
1387
2110
  };
1388
2111
  }
1389
2112
 
@@ -1400,12 +2123,18 @@ export class TreeViewController {
1400
2123
  const scrollY = this.viewport.scrollY;
1401
2124
  const rowIndex = Math.max(0, Math.floor(scrollY / this.rowModel.rowHeight));
1402
2125
  const row = this.rowModel.getRow(rowIndex);
1403
- if (!row) return { scrollX, scrollY, nodeId: null, offsetY: 0 };
2126
+ if (!row)
2127
+ return {
2128
+ scrollX,
2129
+ scrollY,
2130
+ nodeId: null,
2131
+ offsetY: 0
2132
+ };
1404
2133
  return {
1405
2134
  scrollX,
1406
2135
  scrollY,
1407
2136
  nodeId: row.nodeId,
1408
- offsetY: Math.max(0, scrollY - row.y),
2137
+ offsetY: Math.max(0, scrollY - row.y)
1409
2138
  };
1410
2139
  }
1411
2140
 
@@ -1419,7 +2148,11 @@ export class TreeViewController {
1419
2148
  if (!this.inspector) return;
1420
2149
  const expanded = new Set(this.expansion.model.expanded);
1421
2150
  const focusId = focusPath ? `model:${focusPath}` : this.focusedId;
1422
- const nodes = this.inspector.builder.build(this.inspector.model, this.inspector.meta, this.inspector.options);
2151
+ const nodes = this.inspector.builder.build(
2152
+ this.inspector.model,
2153
+ this.inspector.meta,
2154
+ this.inspector.options
2155
+ );
1423
2156
  this.model.setTree(nodes);
1424
2157
  this.expansion.model.expanded = expanded;
1425
2158
  if (focusId) this.expansion.expandAncestors(focusId);
@@ -1437,7 +2170,8 @@ const NATIVE_SCROLLBAR_STYLE_ID = 'virtual-tree-canvas-native-scrollbar-style';
1437
2170
 
1438
2171
  function ensureNativeScrollbarStyles(doc) {
1439
2172
  if (!doc || typeof doc.createElement !== 'function') return;
1440
- if (typeof doc.getElementById === 'function' && doc.getElementById(NATIVE_SCROLLBAR_STYLE_ID)) return;
2173
+ if (typeof doc.getElementById === 'function' && doc.getElementById(NATIVE_SCROLLBAR_STYLE_ID))
2174
+ return;
1441
2175
  const style = doc.createElement('style');
1442
2176
  style.id = NATIVE_SCROLLBAR_STYLE_ID;
1443
2177
  style.textContent = `
@@ -1504,7 +2238,11 @@ function ensureNativeScrollbarStyles(doc) {
1504
2238
  function applyNativeScrollbarTheme(elements, theme) {
1505
2239
  const colors = theme?.colors ?? {};
1506
2240
  const track = alphaHex(colors.row ?? colors.background ?? '#0b1020', 0.74);
1507
- const thumb = mixHex(colors.borderStrong ?? colors.border ?? '#334155', colors.textMuted ?? colors.focus ?? '#94a3b8', 0.55);
2241
+ const thumb = mixHex(
2242
+ colors.borderStrong ?? colors.border ?? '#334155',
2243
+ colors.textMuted ?? colors.focus ?? '#94a3b8',
2244
+ 0.55
2245
+ );
1508
2246
  const hover = mixHex(thumb, colors.focus ?? '#38bdf8', 0.32);
1509
2247
  const active = mixHex(thumb, colors.focus ?? '#38bdf8', 0.52);
1510
2248
  const highlight = alphaHex('#ffffff', 0.2);
@@ -1551,7 +2289,7 @@ function nativeScrollbarSize() {
1551
2289
  top: '-9999px',
1552
2290
  width: '100px',
1553
2291
  height: '100px',
1554
- overflow: 'scroll',
2292
+ overflow: 'scroll'
1555
2293
  });
1556
2294
  document.body?.appendChild(probe);
1557
2295
  const size = Math.max(10, probe.offsetWidth - probe.clientWidth || 14);
@@ -1571,22 +2309,6 @@ function assertNonNegativeNumber(value, name) {
1571
2309
  }
1572
2310
  }
1573
2311
 
1574
- function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
1575
- const safeWidth = Math.max(1, width);
1576
- const rightPadding = 14;
1577
- if (editorType === 'checkbox') {
1578
- const editorWidth = 34;
1579
- return { editorLeft: Math.max(64, safeWidth - rightPadding - editorWidth), editorWidth };
1580
- }
1581
- const minEditor = Math.min(170, Math.max(96, safeWidth * 0.45));
1582
- const minLabelEnd = Math.max(88, depth * indentWidth + 104);
1583
- const preferredLeft = Math.max(minLabelEnd, labelEnd, safeWidth * 0.32);
1584
- const maxLeft = Math.max(64, safeWidth - rightPadding - minEditor);
1585
- const editorLeft = Math.max(64, Math.min(preferredLeft, maxLeft));
1586
- const editorWidth = Math.max(56, safeWidth - rightPadding - editorLeft);
1587
- return { editorLeft, editorWidth };
1588
- }
1589
-
1590
2312
  function createDefaultArrayItem(meta = {}) {
1591
2313
  if (meta.itemFactory) return meta.itemFactory();
1592
2314
  if (meta.itemType === 'number') return 0;
@@ -1596,14 +2318,19 @@ function createDefaultArrayItem(meta = {}) {
1596
2318
  }
1597
2319
 
1598
2320
  function createSortValueSnapshot(column, nodes, dynamicState) {
1599
- return new Map(nodes.map((node) => [node.id, column.value(node, dynamicState.get(node.id) ?? {})]));
2321
+ return new Map(
2322
+ nodes.map((node) => [node.id, column.value(node, dynamicState.get(node.id) ?? {})])
2323
+ );
1600
2324
  }
1601
2325
 
1602
2326
  function compareColumnValues(column, a, b, dynamicState, snapshot = null) {
1603
2327
  const aValue = snapshot ? snapshot.get(a.id) : column.value(a, dynamicState.get(a.id) ?? {});
1604
2328
  const bValue = snapshot ? snapshot.get(b.id) : column.value(b, dynamicState.get(b.id) ?? {});
1605
2329
  if (typeof aValue === 'number' && typeof bValue === 'number') return aValue - bValue;
1606
- return String(aValue ?? '').localeCompare(String(bValue ?? ''), undefined, { numeric: true, sensitivity: 'base' });
2330
+ return String(aValue ?? '').localeCompare(String(bValue ?? ''), undefined, {
2331
+ numeric: true,
2332
+ sensitivity: 'base'
2333
+ });
1607
2334
  }
1608
2335
 
1609
2336
  function matchesFilter(node, state, path, query, options = {}) {
@@ -1616,18 +2343,12 @@ function matchesFilter(node, state, path, query, options = {}) {
1616
2343
  node.data?.key,
1617
2344
  node.data?.valueText,
1618
2345
  node.data?.meta?.description,
1619
- ...Object.keys(node.data?.meta?.options ?? {}),
2346
+ ...Object.keys(node.data?.meta?.options ?? {})
1620
2347
  ]
1621
- : [
1622
- node.id,
1623
- node.label,
1624
- node.type,
1625
- path,
1626
- ...(node.tags ?? []),
1627
- state.status,
1628
- state.value,
1629
- ];
1630
- return values.some((value) => matchesSearch(normalizeFilterValue(value, options), query, options.wholeWord));
2348
+ : [node.id, node.label, node.type, path, ...(node.tags ?? []), state.status, state.value];
2349
+ return values.some((value) =>
2350
+ matchesSearch(normalizeFilterValue(value, options), query, options.wholeWord)
2351
+ );
1631
2352
  }
1632
2353
 
1633
2354
  function normalizeFilterValue(value, options = {}) {
@@ -1668,11 +2389,22 @@ function now() {
1668
2389
 
1669
2390
  function resolveNodeIcons(nodes, resolver) {
1670
2391
  if (!resolver) return nodes;
1671
- return nodes.map(node => {
2392
+ return nodes.map((node) => {
1672
2393
  const visual = resolver(node);
1673
- if (typeof visual === 'string') return { ...node, icon: visual };
2394
+ if (typeof visual === 'string')
2395
+ return {
2396
+ ...node,
2397
+ icon: visual
2398
+ };
1674
2399
  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 };
2400
+ if (
2401
+ (visual.id !== undefined && visual.id !== node.id) ||
2402
+ (visual.parentId !== undefined && visual.parentId !== node.parentId)
2403
+ )
2404
+ throw new TypeError('iconResolver cannot change node identity or parent');
2405
+ return {
2406
+ ...node,
2407
+ ...visual
2408
+ };
1677
2409
  });
1678
2410
  }