virtual-tree-canvas 0.5.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 +105 -27
- 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 +14 -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 +162 -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 +19 -4
- package/src/renderers/tree-row-renderer.js +58 -9
- package/src/tree-view-controller.js +257 -31
- 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
|
@@ -10,7 +10,8 @@ export class TreeRowRenderer {
|
|
|
10
10
|
this.scene = null;
|
|
11
11
|
this.iconRegistry = iconRegistry ?? new IconRegistry();
|
|
12
12
|
this.renderedRows = 0;
|
|
13
|
-
this.
|
|
13
|
+
this.iconRenderFrame = null;
|
|
14
|
+
this.invalidate = null;
|
|
14
15
|
this.preparedIconThemeKey = null;
|
|
15
16
|
this.stopIconListener = this.iconRegistry.onChange?.(() => this.#scheduleIconRender()) ?? null;
|
|
16
17
|
}
|
|
@@ -26,7 +27,15 @@ export class TreeRowRenderer {
|
|
|
26
27
|
this.scene = scene;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
setInvalidationHandler(invalidate) { this.invalidate = invalidate; }
|
|
31
|
+
|
|
29
32
|
destroy() {
|
|
33
|
+
if (this.iconRenderFrame !== null) {
|
|
34
|
+
const view = this.canvas?.ownerDocument?.defaultView ?? globalThis;
|
|
35
|
+
(view.cancelAnimationFrame?.bind(view) ?? clearTimeout)(this.iconRenderFrame);
|
|
36
|
+
}
|
|
37
|
+
this.iconRenderFrame = null;
|
|
38
|
+
this.invalidate = null;
|
|
30
39
|
this.stopIconListener?.();
|
|
31
40
|
this.stopIconListener = null;
|
|
32
41
|
this.canvas = null;
|
|
@@ -65,15 +74,18 @@ export class TreeRowRenderer {
|
|
|
65
74
|
this.#drawHeader(ctx);
|
|
66
75
|
this.#drawRows(ctx);
|
|
67
76
|
this.#drawStickyRows(ctx);
|
|
77
|
+
this.#drawRowDrop(ctx);
|
|
68
78
|
ctx.restore();
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
#scheduleIconRender() {
|
|
72
|
-
if (this.
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
if (!this.canvas) return;
|
|
83
|
+
if (this.invalidate) return this.invalidate();
|
|
84
|
+
if (this.iconRenderFrame !== null) return;
|
|
85
|
+
const view = this.canvas.ownerDocument?.defaultView ?? globalThis;
|
|
86
|
+
const schedule = view.requestAnimationFrame?.bind(view) ?? (callback => setTimeout(callback, 0));
|
|
87
|
+
this.iconRenderFrame = schedule(() => {
|
|
88
|
+
this.iconRenderFrame = null;
|
|
77
89
|
this.render();
|
|
78
90
|
});
|
|
79
91
|
}
|
|
@@ -107,7 +119,7 @@ export class TreeRowRenderer {
|
|
|
107
119
|
ctx.textBaseline = 'middle';
|
|
108
120
|
|
|
109
121
|
for (const column of columns) {
|
|
110
|
-
if (headerFilter && column.kind
|
|
122
|
+
if (headerFilter && column === columns.find(item => item.kind !== 'rowOrder')) {
|
|
111
123
|
this.#drawHeaderFilter(ctx, column, viewport, theme, filterQuery);
|
|
112
124
|
} else {
|
|
113
125
|
ctx.fillStyle = colors.textMuted;
|
|
@@ -145,7 +157,7 @@ export class TreeRowRenderer {
|
|
|
145
157
|
ctx.strokeStyle = colors.border;
|
|
146
158
|
ctx.stroke();
|
|
147
159
|
ctx.fillStyle = filterQuery ? colors.text : colors.textMuted;
|
|
148
|
-
drawTruncatedText(ctx, filterQuery || 'Filter
|
|
160
|
+
drawTruncatedText(ctx, filterQuery || 'Filter rows', x + 8, viewport.headerHeight / 2, width - 16);
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
#drawSortIndicator(ctx, x, y, direction, theme) {
|
|
@@ -268,7 +280,8 @@ export class TreeRowRenderer {
|
|
|
268
280
|
cell.column.render(ctx, cell);
|
|
269
281
|
return;
|
|
270
282
|
}
|
|
271
|
-
if (cell.column.kind === '
|
|
283
|
+
if (cell.column.kind === 'rowOrder') this.#drawRowOrder(ctx, cell);
|
|
284
|
+
else if (cell.column.kind === 'tree') this.#drawTreeCell(ctx, cell);
|
|
272
285
|
else if (cell.column.kind === 'inspectorPane') this.#drawInspectorPaneCell(ctx, cell);
|
|
273
286
|
else if (cell.column.kind === 'inspectorValue') this.#drawInspectorValueCell(ctx, cell);
|
|
274
287
|
else if (cell.column.kind === 'inspectorType') this.#drawInspectorTypeCell(ctx, cell);
|
|
@@ -278,6 +291,42 @@ export class TreeRowRenderer {
|
|
|
278
291
|
else this.#drawTextCell(ctx, cell);
|
|
279
292
|
}
|
|
280
293
|
|
|
294
|
+
#drawRowOrder(ctx, { node, row, rect, theme }) {
|
|
295
|
+
const siblings = this.scene.childrenByParent?.get(node.parentId ?? null) ?? [];
|
|
296
|
+
const enabled = this.scene.canReorderRows && siblings.length > 1;
|
|
297
|
+
const cy = rect.y + row.height / 2;
|
|
298
|
+
ctx.save();
|
|
299
|
+
ctx.strokeStyle = theme.colors.textMuted;
|
|
300
|
+
ctx.fillStyle = theme.colors.textMuted;
|
|
301
|
+
ctx.lineWidth = 1.5;
|
|
302
|
+
ctx.globalAlpha = enabled ? 1 : 0.3;
|
|
303
|
+
for (const dx of [10, 14]) for (const dy of [-4, 0, 4]) ctx.fillRect(rect.x + dx, cy + dy, 1.5, 1.5);
|
|
304
|
+
for (const [center, direction, available] of [[36, -1, siblings[0] !== node.id], [60, 1, siblings.at(-1) !== node.id]]) {
|
|
305
|
+
ctx.globalAlpha = enabled && available ? 1 : 0.3;
|
|
306
|
+
ctx.beginPath();
|
|
307
|
+
ctx.moveTo(rect.x + center - 4, cy - direction * 2);
|
|
308
|
+
ctx.lineTo(rect.x + center, cy + direction * 2);
|
|
309
|
+
ctx.lineTo(rect.x + center + 4, cy - direction * 2);
|
|
310
|
+
ctx.stroke();
|
|
311
|
+
}
|
|
312
|
+
ctx.restore();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#drawRowDrop(ctx) {
|
|
316
|
+
const { rowDrop, viewport, theme } = this.scene;
|
|
317
|
+
if (!rowDrop) return;
|
|
318
|
+
const y = viewport.headerHeight + rowDrop.y - viewport.scrollY;
|
|
319
|
+
if (y < viewport.headerHeight || y > viewport.headerHeight + viewport.rowViewportHeight) return;
|
|
320
|
+
ctx.save();
|
|
321
|
+
ctx.strokeStyle = theme.colors.focus;
|
|
322
|
+
ctx.lineWidth = 2;
|
|
323
|
+
ctx.beginPath();
|
|
324
|
+
ctx.moveTo(3, y);
|
|
325
|
+
ctx.lineTo(viewport.contentViewportWidth - 3, y);
|
|
326
|
+
ctx.stroke();
|
|
327
|
+
ctx.restore();
|
|
328
|
+
}
|
|
329
|
+
|
|
281
330
|
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style, hovered, hoverPart, activePart }) {
|
|
282
331
|
const visibleRight = this.scene.viewport.scrollX + (this.scene.viewport.contentViewportWidth ?? this.scene.viewport.viewportWidth);
|
|
283
332
|
rect = { ...rect, width: Math.max(1, Math.min(rect.x + rect.width, visibleRight) - rect.x) };
|
|
@@ -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,
|
|
@@ -14,12 +17,17 @@ import {
|
|
|
14
17
|
} from './core/index.js';
|
|
15
18
|
import { numericLayout } from './inspector/numeric-layout.js';
|
|
16
19
|
import { CellEditorManager } from './inspector/cell-editor-manager.js';
|
|
17
|
-
import { formatInspectorValue, getAtPath, inspectorColumns, inspectorPaneColumns, ModelInspectorBuilder, setAtPath } from './inspector/index.js';
|
|
20
|
+
import { formatInspectorValue, getAtPath, inspectorNodeId, inspectorColumns, inspectorPaneColumns, ModelInspectorBuilder, setAtPath } from './inspector/index.js';
|
|
18
21
|
import { TreeViewInputController } from './input/tree-view-input-controller.js';
|
|
19
22
|
import { TreeRowRenderer } from './renderers/index.js';
|
|
20
23
|
|
|
21
24
|
export class TreeViewController {
|
|
22
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;
|
|
23
31
|
this.events = new EventEmitter();
|
|
24
32
|
this.model = new TreeModel();
|
|
25
33
|
this.expansion = new TreeExpansionManager(this.model);
|
|
@@ -37,11 +45,18 @@ export class TreeViewController {
|
|
|
37
45
|
this.viewport.renderInsetX = options.renderInsetX ?? 0;
|
|
38
46
|
this.viewport.renderInsetY = options.renderInsetY ?? 0;
|
|
39
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;
|
|
40
55
|
this.searchIndex = new TreeSearchIndex();
|
|
41
56
|
this.selection = new TreeSelectionManager();
|
|
42
57
|
this.patchBatcher = new PatchBatcher();
|
|
43
|
-
this.themeManager = options.themeManager ?? new ThemeManager();
|
|
44
|
-
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 });
|
|
45
60
|
this.renderer = options.renderer ?? new TreeRowRenderer({ themeManager: this.themeManager, iconRegistry: this.iconRegistry });
|
|
46
61
|
this.nativeScrollbars = options.nativeScrollbars !== false;
|
|
47
62
|
this.initialExpandDepth = options.initialExpandDepth ?? 1;
|
|
@@ -64,10 +79,17 @@ export class TreeViewController {
|
|
|
64
79
|
this.inspector = null;
|
|
65
80
|
this.inputController = null;
|
|
66
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;
|
|
67
85
|
this.scene = this.createRenderScene();
|
|
86
|
+
this.renderer.setInvalidationHandler?.(() => this.requestRender(true));
|
|
68
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
|
+
}
|
|
69
91
|
if (options.canvas) this.initialize(options.canvas);
|
|
70
|
-
if (options.
|
|
92
|
+
if (options.canvas && (options.host || options.editable)) this.attachCellEditor({ host: options.host });
|
|
71
93
|
if (options.canvas && options.input !== false) this.attachInput();
|
|
72
94
|
}
|
|
73
95
|
|
|
@@ -81,6 +103,8 @@ export class TreeViewController {
|
|
|
81
103
|
this.#resizeToCanvasClientSize();
|
|
82
104
|
this.#observeCanvasSize();
|
|
83
105
|
this.#setupNativeScrollbars();
|
|
106
|
+
if (this.tooltipEnabled) this.attachTooltip({ host: this.host });
|
|
107
|
+
this.requestRender();
|
|
84
108
|
return this;
|
|
85
109
|
}
|
|
86
110
|
|
|
@@ -91,12 +115,32 @@ export class TreeViewController {
|
|
|
91
115
|
controller: this,
|
|
92
116
|
host: options.host,
|
|
93
117
|
});
|
|
118
|
+
this.cellEditor.setEditable(this.editable);
|
|
119
|
+
if (this.inputController) this.inputController.cellEditor = this.cellEditor;
|
|
94
120
|
return this.cellEditor;
|
|
95
121
|
}
|
|
96
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
|
+
|
|
97
139
|
attachInput(options = {}) {
|
|
98
140
|
if (!this.canvas) throw new Error('TreeViewController.attachInput requires an initialized canvas');
|
|
99
141
|
this.inputController?.destroy?.();
|
|
142
|
+
this.rowReorderInput?.destroy();
|
|
143
|
+
this.rowReorderInput = new RowReorderInput(this);
|
|
100
144
|
this.inputController = new TreeViewInputController({
|
|
101
145
|
controller: this,
|
|
102
146
|
cellEditor: options.cellEditor ?? this.cellEditor,
|
|
@@ -105,6 +149,12 @@ export class TreeViewController {
|
|
|
105
149
|
}
|
|
106
150
|
|
|
107
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();
|
|
108
158
|
this.inputController?.destroy?.();
|
|
109
159
|
this.cellEditor?.destroy?.();
|
|
110
160
|
this.disableWorkers();
|
|
@@ -116,6 +166,7 @@ export class TreeViewController {
|
|
|
116
166
|
this.cellEditor = null;
|
|
117
167
|
this.renderer?.destroy?.();
|
|
118
168
|
this.canvas = null;
|
|
169
|
+
this.events.listeners.clear();
|
|
119
170
|
}
|
|
120
171
|
|
|
121
172
|
on(type, listener) {
|
|
@@ -126,9 +177,16 @@ export class TreeViewController {
|
|
|
126
177
|
this.events.off(type, listener);
|
|
127
178
|
}
|
|
128
179
|
|
|
129
|
-
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;
|
|
130
187
|
this.inspector = null;
|
|
131
|
-
|
|
188
|
+
this.columnModel.setRowReorder(this.rowReorder);
|
|
189
|
+
if (this.columnModel.columns.some(column => column.kind?.startsWith('inspector'))) {
|
|
132
190
|
this.columnModel.setColumns([]);
|
|
133
191
|
}
|
|
134
192
|
this.model.setTree(nodes);
|
|
@@ -136,13 +194,134 @@ export class TreeViewController {
|
|
|
136
194
|
this.searchIndex.rebuild(this.model);
|
|
137
195
|
this.workerClient?.setData(this.model.nodes);
|
|
138
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;
|
|
139
315
|
}
|
|
140
316
|
|
|
141
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);
|
|
142
321
|
const builder = new ModelInspectorBuilder();
|
|
143
322
|
const presentation = options.presentation ?? options.mode ?? 'table';
|
|
144
323
|
const previousExpanded = new Set(this.model.expanded);
|
|
145
|
-
const
|
|
324
|
+
const previousBranches = new Set(this.inspector ? this.model.nodes.filter(node => this.expansion.hasChildren(node.id)).map(node => node.id) : []);
|
|
146
325
|
const inspectorOptions = {
|
|
147
326
|
presentation,
|
|
148
327
|
flatRoot: Boolean(options.flatRoot),
|
|
@@ -151,17 +330,23 @@ export class TreeViewController {
|
|
|
151
330
|
markUpdated: options.markUpdated !== false,
|
|
152
331
|
};
|
|
153
332
|
const nodes = builder.build(model, meta, inspectorOptions);
|
|
333
|
+
this.rawNodes.clear();
|
|
154
334
|
this.inspector = { model, meta, builder, presentation, options: inspectorOptions };
|
|
155
335
|
this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
|
|
156
336
|
this.model.setTree(nodes);
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
343
|
this.searchIndex.rebuild(this.model);
|
|
160
344
|
this.#rebuildRows();
|
|
345
|
+
this.events.emit('datachange', {});
|
|
161
346
|
this.events.emit('modelchange', { model, meta, structural: true });
|
|
162
347
|
}
|
|
163
348
|
|
|
164
|
-
updateInspectorValue(nodeId, newValue, editorType = 'unknown') {
|
|
349
|
+
updateInspectorValue(nodeId, newValue, editorType = 'unknown', options = {}) {
|
|
165
350
|
const node = this.model.index.getNode(nodeId);
|
|
166
351
|
if (!node?.data?.inspector || !this.inspector) return false;
|
|
167
352
|
const data = node.data;
|
|
@@ -175,17 +360,25 @@ export class TreeViewController {
|
|
|
175
360
|
if (this.inspector.options.markUpdated !== false) {
|
|
176
361
|
this.setDynamicState([{ id: nodeId, state: { updated: true } }]);
|
|
177
362
|
}
|
|
178
|
-
const detail = { path: data.path, oldValue, newValue, nodeId, editorType };
|
|
179
|
-
|
|
180
|
-
|
|
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();
|
|
181
369
|
return true;
|
|
182
370
|
}
|
|
183
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
|
+
|
|
184
377
|
triggerInspectorAction(nodeId) {
|
|
185
378
|
const node = this.model.index.getNode(nodeId);
|
|
186
379
|
if (!node?.data?.inspector) return false;
|
|
187
380
|
const label = node.data.meta?.button ?? node.label;
|
|
188
|
-
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' });
|
|
189
382
|
return true;
|
|
190
383
|
}
|
|
191
384
|
|
|
@@ -230,7 +423,11 @@ export class TreeViewController {
|
|
|
230
423
|
}
|
|
231
424
|
|
|
232
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();
|
|
233
429
|
this.columnModel.setColumns(columns);
|
|
430
|
+
this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
|
|
234
431
|
this.#syncContentSize();
|
|
235
432
|
this.events.emit('columnschange', { columns: this.columnModel.columns });
|
|
236
433
|
}
|
|
@@ -281,6 +478,8 @@ export class TreeViewController {
|
|
|
281
478
|
}
|
|
282
479
|
|
|
283
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();
|
|
284
483
|
this.filterQuery = typeof queryOrPredicate === 'string' ? queryOrPredicate : '';
|
|
285
484
|
this.filterOptions = typeof queryOrPredicate === 'string'
|
|
286
485
|
? { caseSensitive: Boolean(options.caseSensitive), wholeWord: Boolean(options.wholeWord) }
|
|
@@ -320,10 +519,12 @@ export class TreeViewController {
|
|
|
320
519
|
}
|
|
321
520
|
|
|
322
521
|
setDynamicState(patches) {
|
|
522
|
+
if (!Array.isArray(patches)) throw new TypeError('patches must be an array');
|
|
323
523
|
this.lastPatchCount = patches.length;
|
|
324
524
|
this.lastDirtyNodeCount = new Set(patches.map((patch) => patch.id)).size;
|
|
325
525
|
this.model.applyDynamicPatches(patches);
|
|
326
526
|
this.renderer.updateDynamicState(patches);
|
|
527
|
+
this.requestRender();
|
|
327
528
|
}
|
|
328
529
|
|
|
329
530
|
setTheme(theme) {
|
|
@@ -331,11 +532,10 @@ export class TreeViewController {
|
|
|
331
532
|
const beforeIndentWidth = this.rowModel.indentWidth;
|
|
332
533
|
this.themeManager.setTheme(theme);
|
|
333
534
|
const nextTheme = this.themeManager.get();
|
|
334
|
-
this.rowModel.rowHeight = nextTheme.rowHeight;
|
|
335
|
-
this.rowModel.indentWidth = nextTheme.indentWidth;
|
|
336
|
-
this.
|
|
337
|
-
|
|
338
|
-
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();
|
|
339
539
|
this.#rebuildRows();
|
|
340
540
|
}
|
|
341
541
|
this.#nativeScroll?.applyTheme?.(nextTheme);
|
|
@@ -343,13 +543,17 @@ export class TreeViewController {
|
|
|
343
543
|
}
|
|
344
544
|
|
|
345
545
|
setLayoutMetrics(options = {}) {
|
|
346
|
-
const rowHeight = options.rowHeight ?? this.rowModel.rowHeight;
|
|
347
|
-
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;
|
|
348
548
|
const headerHeight = options.headerHeight ?? this.viewport.headerHeight;
|
|
349
549
|
assertPositiveNumber(rowHeight, 'rowHeight');
|
|
350
550
|
assertPositiveNumber(indentWidth, 'indentWidth');
|
|
351
551
|
assertNonNegativeNumber(headerHeight, 'headerHeight');
|
|
552
|
+
for (const key of ['rowHeight', 'indentWidth']) {
|
|
553
|
+
if (Object.hasOwn(options, key)) this.layoutOverrides[key] = options[key];
|
|
554
|
+
}
|
|
352
555
|
const rowsChanged = this.rowModel.rowHeight !== rowHeight || this.rowModel.indentWidth !== indentWidth;
|
|
556
|
+
if (rowsChanged || headerHeight !== this.viewport.headerHeight) this.closeEditor();
|
|
353
557
|
this.rowModel.rowHeight = rowHeight;
|
|
354
558
|
this.rowModel.indentWidth = indentWidth;
|
|
355
559
|
this.viewport.rowHeight = rowHeight;
|
|
@@ -543,6 +747,7 @@ export class TreeViewController {
|
|
|
543
747
|
}
|
|
544
748
|
|
|
545
749
|
setSelection(ids) {
|
|
750
|
+
if (!Array.isArray(ids)) throw new TypeError('ids must be an array');
|
|
546
751
|
this.selection.selected.clear();
|
|
547
752
|
for (const id of ids) this.selection.selected.add(id);
|
|
548
753
|
this.selection.focused = ids[ids.length - 1] ?? null;
|
|
@@ -578,6 +783,7 @@ export class TreeViewController {
|
|
|
578
783
|
if (this.activeId === nodeId && this.activePart === part) return;
|
|
579
784
|
this.activeId = nodeId;
|
|
580
785
|
this.activePart = part;
|
|
786
|
+
this.requestRender();
|
|
581
787
|
}
|
|
582
788
|
|
|
583
789
|
clickNode(nodeId, event = {}) {
|
|
@@ -621,6 +827,11 @@ export class TreeViewController {
|
|
|
621
827
|
|
|
622
828
|
handleKey(event) {
|
|
623
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
|
+
}
|
|
624
835
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
|
|
625
836
|
this.selection.selected.clear();
|
|
626
837
|
for (const row of this.rowModel.rows) this.selection.selected.add(row.nodeId);
|
|
@@ -692,9 +903,13 @@ export class TreeViewController {
|
|
|
692
903
|
createRenderScene() {
|
|
693
904
|
const visibleRange = this.rowModel.getVisibleRange(this.viewport, 4);
|
|
694
905
|
return {
|
|
906
|
+
rowReorder: this.rowReorder,
|
|
907
|
+
canReorderRows: this.canReorderRows(),
|
|
908
|
+
rowDrop: this.rowDrop,
|
|
909
|
+
childrenByParent: this.model.index.childrenByParent,
|
|
695
910
|
rows: this.rowModel.rows,
|
|
696
911
|
visibleRange,
|
|
697
|
-
stickyRows: this.rowModel.getStickyRows(this.viewport),
|
|
912
|
+
stickyRows: this.rowReorder ? [] : this.rowModel.getStickyRows(this.viewport),
|
|
698
913
|
viewport: this.viewport,
|
|
699
914
|
columns: this.columnModel.columns,
|
|
700
915
|
theme: this.themeManager.get(),
|
|
@@ -711,12 +926,13 @@ export class TreeViewController {
|
|
|
711
926
|
sortValues: this.sortValueSnapshot ? Array.from(this.sortValueSnapshot) : null,
|
|
712
927
|
inspectorPaneLabelEnd: this.#computeInspectorPaneLabelEnd(visibleRange),
|
|
713
928
|
filterQuery: this.filterQuery,
|
|
714
|
-
headerFilter: Boolean(this.inspector?.options?.filter),
|
|
929
|
+
headerFilter: this.headerFilter ?? Boolean(this.inspector?.options?.filter),
|
|
715
930
|
stats: this.getStats(),
|
|
716
931
|
};
|
|
717
932
|
}
|
|
718
933
|
|
|
719
934
|
closeEditor() {
|
|
935
|
+
this.cellEditor?.close?.();
|
|
720
936
|
this.events.emit('editorclose', {});
|
|
721
937
|
}
|
|
722
938
|
|
|
@@ -730,7 +946,7 @@ export class TreeViewController {
|
|
|
730
946
|
const resizeColumn = this.columnModel.getResizeHandleAt(x);
|
|
731
947
|
if (resizeColumn) return { area: 'header', part: 'resize', column: resizeColumn, x, y: localY };
|
|
732
948
|
const column = this.columnModel.getColumnAt(x);
|
|
733
|
-
if (column
|
|
949
|
+
if (column && (this.headerFilter ?? Boolean(this.inspector?.options?.filter)) && column === this.columnModel.columns.find(item => item.kind !== 'rowOrder')) {
|
|
734
950
|
return { area: 'header', part: 'filter', column, x, y: localY };
|
|
735
951
|
}
|
|
736
952
|
return column ? { area: 'header', part: 'label', column, x, y: localY } : { area: 'header', part: 'header', column: null, x, y: localY };
|
|
@@ -744,6 +960,10 @@ export class TreeViewController {
|
|
|
744
960
|
const column = this.columnModel.getColumnAt(x);
|
|
745
961
|
if (!column) return { area: 'row', part: 'row', row, column: null, x, y: rowY };
|
|
746
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
|
+
}
|
|
747
967
|
let part = 'cell';
|
|
748
968
|
if (column.kind === 'inspectorPane') {
|
|
749
969
|
const localX = x - column.x;
|
|
@@ -880,6 +1100,7 @@ export class TreeViewController {
|
|
|
880
1100
|
}
|
|
881
1101
|
|
|
882
1102
|
resize(width, height) {
|
|
1103
|
+
if (width !== this.viewport.viewportWidth || height !== this.viewport.viewportHeight) this.closeEditor();
|
|
883
1104
|
this.viewport.resize(width, height);
|
|
884
1105
|
this.#fitInspectorPaneColumn(width);
|
|
885
1106
|
this.#syncContentSize();
|
|
@@ -921,6 +1142,7 @@ export class TreeViewController {
|
|
|
921
1142
|
this.#syncContentSize();
|
|
922
1143
|
this.#restoreScrollAnchor(scrollAnchor);
|
|
923
1144
|
this.rebuildCount++;
|
|
1145
|
+
this.requestRender();
|
|
924
1146
|
}
|
|
925
1147
|
|
|
926
1148
|
#syncContentSize() {
|
|
@@ -938,13 +1160,6 @@ export class TreeViewController {
|
|
|
938
1160
|
this.setDynamicState(this.patchBatcher.flush());
|
|
939
1161
|
}
|
|
940
1162
|
|
|
941
|
-
#restoreExpansion(expandedIds) {
|
|
942
|
-
this.model.expanded.clear();
|
|
943
|
-
for (const id of expandedIds) {
|
|
944
|
-
if (this.model.index.getNode(id) && this.expansion.hasChildren(id)) this.model.expanded.add(id);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
1163
|
#focusedRowIndex() {
|
|
949
1164
|
if (this.focusedId) {
|
|
950
1165
|
const row = this.rowModel.getRowById(this.focusedId);
|
|
@@ -1450,3 +1665,14 @@ function isProbablyTruncated(text, width) {
|
|
|
1450
1665
|
function now() {
|
|
1451
1666
|
return globalThis.performance?.now?.() ?? Date.now();
|
|
1452
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
|
+
}
|