virtual-tree-canvas 0.6.0 → 0.7.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 +23 -0
- package/README.md +79 -3
- package/docs/icon-catalog.json +12 -0
- package/docs/icon-catalog.md +5 -1
- package/package.json +1 -1
- package/resources/icons/star-filled.svg +1 -0
- package/resources/icons/trash.svg +1 -0
- package/src/assets/icons.js +2 -0
- package/src/core/theme-manager.js +3 -0
- package/src/core/tree-cell-layout.js +9 -0
- package/src/core/tree-column-model.js +13 -2
- package/src/core/types.js +1 -0
- package/src/core/view-options.js +10 -3
- package/src/core/visible-row-model.js +1 -1
- package/src/index.d.ts +48 -1
- package/src/input/row-actions.js +115 -0
- package/src/input/row-reorder-input.js +34 -9
- package/src/input/tree-view-input-controller.js +5 -3
- package/src/inspector/cell-editor-manager.js +23 -9
- package/src/inspector/pane-layout.js +12 -0
- package/src/renderers/checkbox.js +14 -0
- package/src/renderers/tree-row-renderer.js +47 -55
- package/src/tree-view-controller.js +67 -41
- package/src/tree-view.js +6 -4
- package/src/view/styles.js +2 -2
|
@@ -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';
|
|
@@ -45,6 +48,9 @@ export class TreeViewController {
|
|
|
45
48
|
this.viewport.renderInsetX = options.renderInsetX ?? 0;
|
|
46
49
|
this.viewport.renderInsetY = options.renderInsetY ?? 0;
|
|
47
50
|
this.columnModel = new TreeColumnModel(options.columns);
|
|
51
|
+
this.rowActions = options.rowActions ?? [];
|
|
52
|
+
this.rowDrag = options.rowDrag ?? null;
|
|
53
|
+
this.columnModel.setRowActions(this.rowActions.length);
|
|
48
54
|
this.rowReorder = Boolean(options.rowReorder);
|
|
49
55
|
this.columnModel.setRowReorder(this.rowReorder);
|
|
50
56
|
this.rowDrop = null;
|
|
@@ -100,6 +106,8 @@ export class TreeViewController {
|
|
|
100
106
|
this.canvas = canvas;
|
|
101
107
|
this.renderer.initialize(canvas);
|
|
102
108
|
this.renderer.setScene(this.scene);
|
|
109
|
+
this.actionOverlay?.destroy();
|
|
110
|
+
this.actionOverlay = canvas.ownerDocument?.createElement && canvas.parentElement ? new RowActions(this) : null;
|
|
103
111
|
this.#resizeToCanvasClientSize();
|
|
104
112
|
this.#observeCanvasSize();
|
|
105
113
|
this.#setupNativeScrollbars();
|
|
@@ -120,6 +128,22 @@ export class TreeViewController {
|
|
|
120
128
|
return this.cellEditor;
|
|
121
129
|
}
|
|
122
130
|
|
|
131
|
+
setRowActions(actions) {
|
|
132
|
+
validateTreeViewOptions({ rowActions: actions });
|
|
133
|
+
this.rowActions = actions;
|
|
134
|
+
this.columnModel.setRowActions(actions.length);
|
|
135
|
+
this.columnModel.setRowReorder(this.rowReorder && !this.inspector);
|
|
136
|
+
this.#syncContentSize();
|
|
137
|
+
this.requestRender();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setRowDrag(resolver) {
|
|
141
|
+
validateTreeViewOptions({ rowDrag: resolver });
|
|
142
|
+
this.rowReorderInput?.cancel();
|
|
143
|
+
this.rowDrag = resolver;
|
|
144
|
+
this.rowReorderInput?.syncMode();
|
|
145
|
+
}
|
|
146
|
+
|
|
123
147
|
setEditable(enabled) {
|
|
124
148
|
validateTreeViewOptions({ editable: enabled });
|
|
125
149
|
if (this.editable === enabled) return;
|
|
@@ -150,6 +174,7 @@ export class TreeViewController {
|
|
|
150
174
|
|
|
151
175
|
destroy() {
|
|
152
176
|
this.destroyed = true;
|
|
177
|
+
this.actionOverlay?.destroy();
|
|
153
178
|
this.rowReorderInput?.destroy();
|
|
154
179
|
this.rowReorderInput = null;
|
|
155
180
|
this.tooltip?.destroy();
|
|
@@ -184,19 +209,31 @@ export class TreeViewController {
|
|
|
184
209
|
this.closeEditor();
|
|
185
210
|
this.rawNodes = new Map(nodes.map(node => [node.id, node]));
|
|
186
211
|
nodes = resolved;
|
|
212
|
+
const preserveExpansion = !this.inspector;
|
|
187
213
|
this.inspector = null;
|
|
188
214
|
this.columnModel.setRowReorder(this.rowReorder);
|
|
189
215
|
if (this.columnModel.columns.some(column => column.kind?.startsWith('inspector'))) {
|
|
190
216
|
this.columnModel.setColumns([]);
|
|
191
217
|
}
|
|
192
|
-
this
|
|
193
|
-
this.expansion.expandToDepth(this.initialExpandDepth);
|
|
218
|
+
this.#replaceTree(nodes, preserveExpansion);
|
|
194
219
|
this.searchIndex.rebuild(this.model);
|
|
195
220
|
this.workerClient?.setData(this.model.nodes);
|
|
196
221
|
this.#rebuildRows();
|
|
197
222
|
this.events.emit('datachange', {});
|
|
198
223
|
}
|
|
199
224
|
|
|
225
|
+
#replaceTree(nodes, preserveExpansion) {
|
|
226
|
+
const branches = preserveExpansion ? this.model.nodes.filter(node => this.expansion.hasChildren(node.id)).map(node => node.id) : [];
|
|
227
|
+
const expanded = new Set(this.model.expanded);
|
|
228
|
+
this.model.setTree(nodes);
|
|
229
|
+
this.expansion.expandToDepth(this.initialExpandDepth);
|
|
230
|
+
for (const id of branches) {
|
|
231
|
+
if (!this.model.index.getNode(id)) continue;
|
|
232
|
+
if (expanded.has(id)) this.model.expanded.add(id);
|
|
233
|
+
else this.model.expanded.delete(id);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
200
237
|
setIconResolver(resolver) {
|
|
201
238
|
validateTreeViewOptions({ iconResolver: resolver });
|
|
202
239
|
if (this.inspector) return;
|
|
@@ -233,8 +270,9 @@ export class TreeViewController {
|
|
|
233
270
|
this.events.emit('rowreorderchange', { enabled: this.rowReorder });
|
|
234
271
|
}
|
|
235
272
|
|
|
236
|
-
canReorderRows() {
|
|
237
|
-
return this.rowReorder && !this.inspector && !this.columnModel.sort.direction && !this.rowModel.filterPredicate
|
|
273
|
+
canReorderRows(nodeId) {
|
|
274
|
+
return this.rowReorder && !this.inspector && !this.columnModel.sort.direction && !this.rowModel.filterPredicate
|
|
275
|
+
&& (nodeId == null || this.model.index.getNode(nodeId)?.reorderable !== false);
|
|
238
276
|
}
|
|
239
277
|
|
|
240
278
|
getRowOrder(parentId = null) {
|
|
@@ -242,7 +280,7 @@ export class TreeViewController {
|
|
|
242
280
|
}
|
|
243
281
|
|
|
244
282
|
moveRow(nodeId, targetIndex, options = {}) {
|
|
245
|
-
if (!this.canReorderRows()) return false;
|
|
283
|
+
if (!this.canReorderRows(nodeId)) return false;
|
|
246
284
|
const anchorId = this.rowModel.getRow(this.anchorRowIndex)?.nodeId;
|
|
247
285
|
const detail = this.model.moveNode(nodeId, targetIndex);
|
|
248
286
|
if (!detail) return false;
|
|
@@ -268,7 +306,7 @@ export class TreeViewController {
|
|
|
268
306
|
}
|
|
269
307
|
|
|
270
308
|
getRowDropTarget(nodeId, clientX, clientY) {
|
|
271
|
-
if (!this.canReorderRows()) return null;
|
|
309
|
+
if (!this.canReorderRows(nodeId)) return null;
|
|
272
310
|
const x = clientX - this.viewport.renderInsetX;
|
|
273
311
|
const y = clientY - this.viewport.renderInsetY;
|
|
274
312
|
if (x < 0 || x >= this.viewport.contentViewportWidth || y < this.viewport.headerHeight
|
|
@@ -316,12 +354,10 @@ export class TreeViewController {
|
|
|
316
354
|
|
|
317
355
|
setModel(model, meta = {}, options = {}) {
|
|
318
356
|
validateTreeViewOptions({ ...options, mode: 'inspector', model, meta, presentation: options.presentation ?? options.mode ?? 'table' });
|
|
319
|
-
this.closeEditor();
|
|
320
357
|
this.columnModel.setRowReorder(false);
|
|
321
358
|
const builder = new ModelInspectorBuilder();
|
|
322
359
|
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) : []);
|
|
360
|
+
const preserveExpansion = Boolean(this.inspector);
|
|
325
361
|
const inspectorOptions = {
|
|
326
362
|
presentation,
|
|
327
363
|
flatRoot: Boolean(options.flatRoot),
|
|
@@ -330,16 +366,14 @@ export class TreeViewController {
|
|
|
330
366
|
markUpdated: options.markUpdated !== false,
|
|
331
367
|
};
|
|
332
368
|
const nodes = builder.build(model, meta, inspectorOptions);
|
|
369
|
+
const sameLayout = this.inspector?.presentation === presentation &&
|
|
370
|
+
nodes.length === this.model.nodes.length && nodes.every((node, index) => node.id === this.model.nodes[index].id);
|
|
371
|
+
if (!sameLayout) this.closeEditor();
|
|
372
|
+
else this.cellEditor?.reconcile(nodes);
|
|
333
373
|
this.rawNodes.clear();
|
|
334
374
|
this.inspector = { model, meta, builder, presentation, options: inspectorOptions };
|
|
335
|
-
this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
|
|
336
|
-
this
|
|
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
|
-
}
|
|
375
|
+
if (!sameLayout) this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
|
|
376
|
+
this.#replaceTree(nodes, preserveExpansion);
|
|
343
377
|
this.searchIndex.rebuild(this.model);
|
|
344
378
|
this.#rebuildRows();
|
|
345
379
|
this.events.emit('datachange', {});
|
|
@@ -895,6 +929,7 @@ export class TreeViewController {
|
|
|
895
929
|
this.renderer.setScene(this.scene);
|
|
896
930
|
const renderStart = performance.now();
|
|
897
931
|
this.renderer.render(this.scene, time);
|
|
932
|
+
this.actionOverlay?.render(this.scene);
|
|
898
933
|
const renderMs = performance.now() - renderStart;
|
|
899
934
|
this.lastRenderedRows = this.renderer.renderedRows ?? 0;
|
|
900
935
|
return { scene: this.scene, sceneMs, renderMs, renderedRows: this.lastRenderedRows };
|
|
@@ -903,6 +938,7 @@ export class TreeViewController {
|
|
|
903
938
|
createRenderScene() {
|
|
904
939
|
const visibleRange = this.rowModel.getVisibleRange(this.viewport, 4);
|
|
905
940
|
return {
|
|
941
|
+
rowDrag: this.rowDrag,
|
|
906
942
|
rowReorder: this.rowReorder,
|
|
907
943
|
canReorderRows: this.canReorderRows(),
|
|
908
944
|
rowDrop: this.rowDrop,
|
|
@@ -946,7 +982,7 @@ export class TreeViewController {
|
|
|
946
982
|
const resizeColumn = this.columnModel.getResizeHandleAt(x);
|
|
947
983
|
if (resizeColumn) return { area: 'header', part: 'resize', column: resizeColumn, x, y: localY };
|
|
948
984
|
const column = this.columnModel.getColumnAt(x);
|
|
949
|
-
if (column && (this.headerFilter ?? Boolean(this.inspector?.options?.filter)) && column === this.columnModel.columns.find(item => item.kind !== 'rowOrder')) {
|
|
985
|
+
if (column && (this.headerFilter ?? Boolean(this.inspector?.options?.filter)) && column === this.columnModel.columns.find(item => item.kind !== 'rowOrder' && item.id !== '__vtc_actions')) {
|
|
950
986
|
return { area: 'header', part: 'filter', column, x, y: localY };
|
|
951
987
|
}
|
|
952
988
|
return column ? { area: 'header', part: 'label', column, x, y: localY } : { area: 'header', part: 'header', column: null, x, y: localY };
|
|
@@ -995,10 +1031,10 @@ export class TreeViewController {
|
|
|
995
1031
|
}
|
|
996
1032
|
} else if (column.kind === 'tree') {
|
|
997
1033
|
const localX = x - column.x;
|
|
998
|
-
const
|
|
999
|
-
if (localX >=
|
|
1000
|
-
else if (localX >=
|
|
1001
|
-
else if (localX >=
|
|
1034
|
+
const layout = treeCellLayout(row.depth, this.rowModel.indentWidth, this.model.index.childrenByParent.size > 1);
|
|
1035
|
+
if (row.hasChildren && localX >= layout.chevronX - 6 && localX <= layout.chevronX + 12) part = 'chevron';
|
|
1036
|
+
else if (localX >= layout.iconX - 1 && localX <= layout.iconX + 17) part = 'icon';
|
|
1037
|
+
else if (localX >= layout.labelX - 2) part = 'label';
|
|
1002
1038
|
else part = 'cell';
|
|
1003
1039
|
} else if (column.kind === 'inspectorValue') {
|
|
1004
1040
|
part = this.#inspectorEditorPart(row, x - column.x, column.width);
|
|
@@ -1076,7 +1112,7 @@ export class TreeViewController {
|
|
|
1076
1112
|
width = Math.max(0, layout.editorWidth - 20);
|
|
1077
1113
|
}
|
|
1078
1114
|
} else if (hit.column.kind === 'tree') {
|
|
1079
|
-
const labelX = hit.row.depth
|
|
1115
|
+
const { labelX } = treeCellLayout(hit.row.depth, this.rowModel.indentWidth, this.model.index.childrenByParent.size > 1);
|
|
1080
1116
|
text = node.label ?? node.id;
|
|
1081
1117
|
width = Math.max(0, hit.column.width - labelX - 6);
|
|
1082
1118
|
} else if (hit.column.kind === 'inspectorValue') {
|
|
@@ -1090,7 +1126,7 @@ export class TreeViewController {
|
|
|
1090
1126
|
width = Math.max(0, hit.column.width - 20);
|
|
1091
1127
|
} else if (typeof hit.column.value === 'function') {
|
|
1092
1128
|
const value = hit.column.value(node, state);
|
|
1093
|
-
text = value == null ? '' : String(value);
|
|
1129
|
+
text = hit.column.format ? hit.column.format(value, node, state) : value == null ? '' : String(value);
|
|
1094
1130
|
width = Math.max(0, hit.column.width - 20);
|
|
1095
1131
|
}
|
|
1096
1132
|
|
|
@@ -1175,7 +1211,10 @@ export class TreeViewController {
|
|
|
1175
1211
|
const entry = entries[0];
|
|
1176
1212
|
const width = Math.floor(entry?.contentRect?.width ?? 0);
|
|
1177
1213
|
const height = Math.floor(entry?.contentRect?.height ?? 0);
|
|
1178
|
-
if (width > 0 && height > 0)
|
|
1214
|
+
if (width > 0 && height > 0) {
|
|
1215
|
+
this.resize(width, height);
|
|
1216
|
+
if (this.autoRender) { this.cancelRender(); this.render(); }
|
|
1217
|
+
}
|
|
1179
1218
|
});
|
|
1180
1219
|
this.#resizeObserver.observe(this.canvas);
|
|
1181
1220
|
}
|
|
@@ -1366,7 +1405,9 @@ export class TreeViewController {
|
|
|
1366
1405
|
}
|
|
1367
1406
|
|
|
1368
1407
|
#fitInspectorPaneColumn(width) {
|
|
1369
|
-
const
|
|
1408
|
+
const columns = this.columnModel.columns.filter(c => !['__vtc_actions', '__vtc_row_order'].includes(c.id));
|
|
1409
|
+
const column = columns.length === 1 ? columns[0] : null;
|
|
1410
|
+
width -= this.columnModel.columns.filter(c => !columns.includes(c)).reduce((sum, c) => sum + c.width, 0);
|
|
1370
1411
|
if (column?.kind !== 'inspectorPane' && column?.kind !== 'tree') return;
|
|
1371
1412
|
if (column.kind === 'inspectorPane' && this.inspector?.options?.presentation !== 'pane') return;
|
|
1372
1413
|
const nextWidth = Math.max(column.minWidth, Math.floor(width));
|
|
@@ -1571,21 +1612,6 @@ function assertNonNegativeNumber(value, name) {
|
|
|
1571
1612
|
}
|
|
1572
1613
|
}
|
|
1573
1614
|
|
|
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
1615
|
|
|
1590
1616
|
function createDefaultArrayItem(meta = {}) {
|
|
1591
1617
|
if (meta.itemFactory) return meta.itemFactory();
|
package/src/tree-view.js
CHANGED
|
@@ -79,6 +79,8 @@ export class TreeView {
|
|
|
79
79
|
if (changed('rowHeight') || changed('indentWidth')) controller.setLayoutMetrics({ rowHeight: next.rowHeight, indentWidth: next.indentWidth });
|
|
80
80
|
if (changed('theme') || changed('fontFamily')) this.applyTheme();
|
|
81
81
|
if (changed('editable')) controller.setEditable(next.editable);
|
|
82
|
+
if (changed('rowActions')) controller.setRowActions(next.rowActions);
|
|
83
|
+
if (changed('rowDrag')) controller.setRowDrag(next.rowDrag);
|
|
82
84
|
if (changed('rowReorder')) controller.setRowReorder(next.rowReorder);
|
|
83
85
|
if (replaceData) {
|
|
84
86
|
if (next.mode === 'tree') controller.setData(next.nodes ?? [], { iconResolver: next.iconResolver });
|
|
@@ -88,18 +90,18 @@ export class TreeView {
|
|
|
88
90
|
if (changed('markUpdated')) controller.setInspectorOptions({ markUpdated: next.markUpdated });
|
|
89
91
|
}
|
|
90
92
|
if (changed('columns') || (replaceData && next.columns !== null)) controller.setColumns(next.columns);
|
|
91
|
-
if (replaceData || ['filter', 'filterPlacement', 'headerHeight'].some(changed)) this.syncLayout();
|
|
93
|
+
if (replaceData || ['filter', 'filterPlacement', 'headerHeight', 'showHeader'].some(changed)) this.syncLayout();
|
|
92
94
|
} finally {
|
|
93
95
|
this.flushing = false;
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
syncLayout() {
|
|
98
|
-
const { filter, filterPlacement, headerHeight, mode, presentation } = this.options;
|
|
99
|
-
const bar = filter && (filterPlacement === 'bar' || (filterPlacement === 'auto' && (mode === 'tree' || presentation === 'pane')));
|
|
100
|
+
const { filter, filterPlacement, headerHeight, showHeader, mode, presentation } = this.options;
|
|
101
|
+
const bar = filter && (!showHeader || filterPlacement === 'bar' || (filterPlacement === 'auto' && (mode === 'tree' || presentation === 'pane')));
|
|
100
102
|
this.filterBar.setVisible(bar);
|
|
101
103
|
this._controller.setHeaderFilter(filter && !bar);
|
|
102
|
-
this._controller.setLayoutMetrics({ headerHeight: bar && mode === 'inspector' && presentation === 'pane' ? 0 : headerHeight });
|
|
104
|
+
this._controller.setLayoutMetrics({ headerHeight: !showHeader || (bar && mode === 'inspector' && presentation === 'pane') ? 0 : headerHeight });
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
applyTheme() {
|
package/src/view/styles.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const treeViewStyles = `
|
|
2
2
|
.vtc-view { position:relative; display:flex; flex-direction:column; width:100%; height:100%; min-width:0; min-height:0; overflow:hidden; box-sizing:border-box; background:var(--vtc-background); color:var(--vtc-text); }
|
|
3
3
|
.vtc-view .vtc-canvas-host { position:relative; flex:1 1 0; min-width:0; min-height:0; overflow:hidden; }
|
|
4
|
-
.vtc-view canvas { display:block; width:100%; height:100%; box-sizing:border-box; }
|
|
5
|
-
.vtc-view canvas:focus-visible { outline:1px solid var(--vtc-
|
|
4
|
+
.vtc-view .vtc-canvas-host > canvas { display:block; width:100%; height:100%; box-sizing:border-box; }
|
|
5
|
+
.vtc-view .vtc-canvas-host > canvas:focus-visible { outline:1px solid var(--vtc-border); outline-offset:-1px; }
|
|
6
6
|
.vtc-view .vtc-filter { display:flex; flex:0 0 30px; box-sizing:border-box; gap:4px; padding:4px 8px; min-width:0; }
|
|
7
7
|
.vtc-view .vtc-filter[hidden] { display:none; }
|
|
8
8
|
.vtc-view .vtc-filter input, .vtc-view .vtc-filter button { height:22px; box-sizing:border-box; border:1px solid var(--vtc-border); border-radius:4px; background:var(--vtc-background); color:var(--vtc-text); font:12px var(--vtc-font-family, ui-monospace, monospace); }
|