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