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