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.
- package/CHANGELOG.md +32 -0
- package/README.md +90 -3
- package/docs/icon-catalog.html +68 -66
- package/docs/icon-catalog.json +12 -0
- package/docs/icon-catalog.md +111 -107
- package/docs/performance/dynamic-updates-0.7.1.md +38 -0
- package/package.json +3 -2
- package/resources/icons/star-filled.svg +1 -0
- package/resources/icons/trash.svg +1 -0
- package/src/assets/icons.js +12 -4
- package/src/core/dynamic-state.js +19 -29
- package/src/core/patch-batcher.js +4 -0
- package/src/core/theme-manager.js +238 -61
- package/src/core/tree-cell-layout.js +11 -0
- package/src/core/tree-column-model.js +81 -22
- package/src/core/tree-model.js +4 -2
- package/src/core/types.js +1 -0
- package/src/core/view-options.js +98 -18
- package/src/core/visible-row-model.js +51 -21
- package/src/index.d.ts +70 -1
- package/src/input/row-actions.js +248 -0
- package/src/input/row-reorder-input.js +134 -36
- package/src/input/tree-tooltip.js +39 -5
- package/src/input/tree-view-input-controller.js +35 -10
- package/src/inspector/cell-editor-manager.js +128 -35
- package/src/inspector/pane-layout.js +20 -0
- package/src/renderers/checkbox.js +13 -0
- package/src/renderers/tree-row-renderer.js +356 -107
- package/src/tree-view-controller.js +968 -236
- package/src/tree-view.js +218 -54
- package/src/view/styles.js +83 -12
|
@@ -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 {
|
|
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 = {
|
|
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 =
|
|
60
|
-
|
|
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 = {
|
|
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 =
|
|
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 [
|
|
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))
|
|
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)
|
|
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)
|
|
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({
|
|
215
|
+
validateTreeViewOptions({
|
|
216
|
+
editable: enabled
|
|
217
|
+
});
|
|
125
218
|
if (this.editable === enabled) return;
|
|
126
219
|
this.editable = enabled;
|
|
127
|
-
if (!this.cellEditor && this.canvas)
|
|
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', {
|
|
225
|
+
this.events.emit('editablechange', {
|
|
226
|
+
editable: enabled
|
|
227
|
+
});
|
|
130
228
|
this.requestRender();
|
|
131
229
|
}
|
|
132
230
|
|
|
133
231
|
setInitialExpandDepth(depth) {
|
|
134
|
-
validateTreeViewOptions({
|
|
232
|
+
validateTreeViewOptions({
|
|
233
|
+
initialExpandDepth: depth
|
|
234
|
+
});
|
|
135
235
|
this.initialExpandDepth = depth;
|
|
136
236
|
}
|
|
137
237
|
|
|
138
|
-
|
|
139
238
|
attachInput(options = {}) {
|
|
140
|
-
if (!this.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({
|
|
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
|
|
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({
|
|
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({
|
|
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', {
|
|
361
|
+
this.events.emit('rowreorderchange', {
|
|
362
|
+
enabled: this.rowReorder
|
|
363
|
+
});
|
|
234
364
|
}
|
|
235
365
|
|
|
236
|
-
canReorderRows() {
|
|
237
|
-
return
|
|
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
|
-
|
|
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', {
|
|
259
|
-
|
|
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 (
|
|
275
|
-
|
|
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(
|
|
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
|
|
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', {
|
|
447
|
+
this.events.emit('rowdrag', {
|
|
448
|
+
target
|
|
449
|
+
});
|
|
292
450
|
}
|
|
293
451
|
|
|
294
452
|
requestRender(force = false) {
|
|
295
|
-
if (this.destroyed
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
319
|
-
|
|
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
|
|
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 = {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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', {
|
|
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)
|
|
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([
|
|
561
|
+
this.setDynamicState([
|
|
562
|
+
{
|
|
563
|
+
id: nodeId,
|
|
564
|
+
state: {
|
|
565
|
+
updated: true
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
]);
|
|
569
|
+
this.flushDynamicState();
|
|
362
570
|
}
|
|
363
|
-
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
|
+
};
|
|
364
580
|
if (options.emit !== false) {
|
|
365
581
|
this.events.emit('valuechange', detail);
|
|
366
|
-
this.events.emit('modelchange', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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
|
|
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)
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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(
|
|
466
|
-
|
|
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', {
|
|
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', {
|
|
740
|
+
this.events.emit('sortchange', {
|
|
741
|
+
columnId: null,
|
|
742
|
+
direction: null
|
|
743
|
+
});
|
|
478
744
|
}
|
|
479
745
|
|
|
480
746
|
setFilter(queryOrPredicate = '', options = {}) {
|
|
481
|
-
|
|
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 =
|
|
485
|
-
|
|
486
|
-
|
|
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(
|
|
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', {
|
|
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 = {
|
|
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(
|
|
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(
|
|
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', {
|
|
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.
|
|
524
|
-
this.
|
|
525
|
-
this.
|
|
526
|
-
this.
|
|
527
|
-
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;
|
|
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 =
|
|
536
|
-
|
|
537
|
-
|
|
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', {
|
|
909
|
+
this.events.emit('themechange', {
|
|
910
|
+
theme: nextTheme
|
|
911
|
+
});
|
|
543
912
|
}
|
|
544
913
|
|
|
545
914
|
setLayoutMetrics(options = {}) {
|
|
546
|
-
const rowHeight = Object.hasOwn(options, 'rowHeight')
|
|
547
|
-
|
|
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 =
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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))
|
|
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', {
|
|
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', {
|
|
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)
|
|
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, {
|
|
1016
|
+
this.patchBatcher.set(id, {
|
|
1017
|
+
highlighted: true
|
|
1018
|
+
});
|
|
624
1019
|
}
|
|
625
|
-
if (options.expand !== false && this.expansion.model.expanded.size !== expandedSizeBefore)
|
|
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', {
|
|
1029
|
+
this.events.emit('focuschange', {
|
|
1030
|
+
nodeId: results[0]
|
|
1031
|
+
});
|
|
633
1032
|
}
|
|
634
|
-
this.events.emit('searchchange', {
|
|
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)
|
|
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, {
|
|
1066
|
+
this.patchBatcher.set(id, {
|
|
1067
|
+
highlighted: true
|
|
1068
|
+
});
|
|
660
1069
|
}
|
|
661
1070
|
let rowMs = 0;
|
|
662
|
-
const expansionChanged =
|
|
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(
|
|
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', {
|
|
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)
|
|
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.
|
|
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)
|
|
714
|
-
|
|
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)
|
|
721
|
-
|
|
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', {
|
|
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
|
|
1199
|
+
this.anchorRowIndex = this.focusedId
|
|
1200
|
+
? (this.rowModel.getRowById(this.focusedId)?.rowIndex ?? null)
|
|
1201
|
+
: null;
|
|
756
1202
|
this.#syncSelectionState();
|
|
757
|
-
this.events.emit('selectionchange', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
824
|
-
|
|
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, {
|
|
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', {
|
|
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')
|
|
850
|
-
|
|
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)
|
|
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, {
|
|
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
|
-
|
|
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)
|
|
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 (
|
|
950
|
-
|
|
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
|
|
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)
|
|
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 {
|
|
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 {
|
|
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(
|
|
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 {
|
|
1551
|
+
return {
|
|
1552
|
+
area: 'row',
|
|
1553
|
+
part,
|
|
1554
|
+
row,
|
|
1555
|
+
column,
|
|
1556
|
+
x,
|
|
1557
|
+
y: rowY
|
|
1558
|
+
};
|
|
988
1559
|
}
|
|
989
|
-
const layout = this.getInspectorPaneLayout(
|
|
990
|
-
|
|
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
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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 {
|
|
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(
|
|
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(
|
|
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)
|
|
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:
|
|
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)
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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)
|
|
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)
|
|
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)
|
|
1815
|
+
if (state.selected)
|
|
1816
|
+
this.patchBatcher.set(id, {
|
|
1817
|
+
selected: false
|
|
1818
|
+
});
|
|
1158
1819
|
}
|
|
1159
|
-
for (const id of this.selection.selected)
|
|
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)
|
|
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(
|
|
1186
|
-
|
|
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, {
|
|
1305
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
2025
|
+
this.viewport.setScrollbarState({
|
|
2026
|
+
size,
|
|
2027
|
+
vertical: showY,
|
|
2028
|
+
horizontal: showX
|
|
2029
|
+
});
|
|
1339
2030
|
}
|
|
1340
2031
|
|
|
1341
2032
|
#canvasHostMetrics() {
|
|
1342
|
-
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
|
+
};
|
|
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(
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
|
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)
|
|
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(
|
|
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))
|
|
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(
|
|
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(
|
|
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, {
|
|
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
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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')
|
|
2394
|
+
if (typeof visual === 'string')
|
|
2395
|
+
return {
|
|
2396
|
+
...node,
|
|
2397
|
+
icon: visual
|
|
2398
|
+
};
|
|
1674
2399
|
if (!visual || typeof visual !== 'object') return node;
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
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
|
}
|