virtual-tree-canvas 0.1.0 → 0.3.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/package.json +6 -2
- package/src/core/icon-registry.js +249 -0
- package/src/core/theme-manager.js +20 -1
- package/src/core/tree-worker-operations.js +5 -3
- package/src/core/visible-row-model.js +15 -3
- package/src/index.d.ts +108 -0
- package/src/input/tree-view-input-controller.js +1 -0
- package/src/inspector/cell-editor-manager.js +57 -15
- package/src/inspector/model-inspector-builder.js +2 -1
- package/src/renderers/tree-row-renderer.js +171 -37
- package/src/tree-view-controller.js +164 -9
|
@@ -76,6 +76,9 @@ export class TreeViewController {
|
|
|
76
76
|
|
|
77
77
|
setData(nodes) {
|
|
78
78
|
this.inspector = null;
|
|
79
|
+
if (this.columnModel.columns.length === 1 && this.columnModel.columns[0]?.kind === 'inspectorPane') {
|
|
80
|
+
this.columnModel.setColumns([]);
|
|
81
|
+
}
|
|
79
82
|
this.model.setTree(nodes);
|
|
80
83
|
this.expansion.expandToDepth(this.initialExpandDepth);
|
|
81
84
|
this.searchIndex.rebuild(this.model);
|
|
@@ -86,17 +89,21 @@ export class TreeViewController {
|
|
|
86
89
|
setModel(model, meta = {}, options = {}) {
|
|
87
90
|
const builder = new ModelInspectorBuilder();
|
|
88
91
|
const presentation = options.presentation ?? options.mode ?? 'table';
|
|
92
|
+
const previousExpanded = new Set(this.model.expanded);
|
|
93
|
+
const shouldPreserveExpansion = Boolean(this.inspector);
|
|
89
94
|
const inspectorOptions = {
|
|
90
95
|
presentation,
|
|
91
96
|
flatRoot: Boolean(options.flatRoot),
|
|
92
97
|
enforceMeta: Boolean(options.enforceMeta),
|
|
93
98
|
filter: Boolean(options.filter),
|
|
99
|
+
markUpdated: options.markUpdated !== false,
|
|
94
100
|
};
|
|
95
101
|
const nodes = builder.build(model, meta, inspectorOptions);
|
|
96
102
|
this.inspector = { model, meta, builder, presentation, options: inspectorOptions };
|
|
97
103
|
this.setColumns(presentation === 'pane' ? inspectorPaneColumns() : inspectorColumns());
|
|
98
104
|
this.model.setTree(nodes);
|
|
99
|
-
this
|
|
105
|
+
if (shouldPreserveExpansion) this.#restoreExpansion(previousExpanded);
|
|
106
|
+
else this.expansion.expandToDepth(this.initialExpandDepth);
|
|
100
107
|
this.searchIndex.rebuild(this.model);
|
|
101
108
|
this.#rebuildRows();
|
|
102
109
|
this.events.emit('modelchange', { model, meta, structural: true });
|
|
@@ -113,7 +120,9 @@ export class TreeViewController {
|
|
|
113
120
|
data.value = newValue;
|
|
114
121
|
data.valueType = Array.isArray(newValue) ? 'array' : newValue === null ? 'null' : typeof newValue === 'object' ? 'object' : typeof newValue;
|
|
115
122
|
data.valueText = formatInspectorValue(newValue, data.meta);
|
|
116
|
-
this.
|
|
123
|
+
if (this.inspector.options.markUpdated !== false) {
|
|
124
|
+
this.setDynamicState([{ id: nodeId, state: { updated: true } }]);
|
|
125
|
+
}
|
|
117
126
|
const detail = { path: data.path, oldValue, newValue, nodeId, editorType };
|
|
118
127
|
this.events.emit('valuechange', detail);
|
|
119
128
|
this.events.emit('modelchange', { model: this.inspector.model, path: data.path, oldValue, newValue, nodeId });
|
|
@@ -275,6 +284,11 @@ export class TreeViewController {
|
|
|
275
284
|
}
|
|
276
285
|
|
|
277
286
|
expand(nodeId) {
|
|
287
|
+
if (this.filterQuery && this.rowModel.expandFilterBranch(nodeId)) {
|
|
288
|
+
this.#rebuildRows();
|
|
289
|
+
this.events.emit('expand', { nodeId, filter: true });
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
278
292
|
if (!this.expansion.expand(nodeId)) return false;
|
|
279
293
|
this.#rebuildRows();
|
|
280
294
|
this.events.emit('expand', { nodeId });
|
|
@@ -282,14 +296,19 @@ export class TreeViewController {
|
|
|
282
296
|
}
|
|
283
297
|
|
|
284
298
|
collapse(nodeId) {
|
|
285
|
-
|
|
299
|
+
const row = this.rowModel.getRowById(nodeId);
|
|
300
|
+
const shouldCollapseFilterBranch = Boolean(this.filterQuery && row?.expanded);
|
|
301
|
+
const changed = this.expansion.collapse(nodeId);
|
|
302
|
+
if (shouldCollapseFilterBranch) this.rowModel.collapseFilterBranch(nodeId);
|
|
303
|
+
if (!changed && !shouldCollapseFilterBranch) return false;
|
|
286
304
|
this.#rebuildRows();
|
|
287
|
-
this.events.emit('collapse', { nodeId });
|
|
305
|
+
this.events.emit('collapse', { nodeId, filter: shouldCollapseFilterBranch });
|
|
288
306
|
return true;
|
|
289
307
|
}
|
|
290
308
|
|
|
291
309
|
toggle(nodeId) {
|
|
292
|
-
|
|
310
|
+
const row = this.rowModel.getRowById(nodeId);
|
|
311
|
+
return (row?.expanded ?? this.expansion.isExpanded(nodeId)) ? this.collapse(nodeId) : this.expand(nodeId);
|
|
293
312
|
}
|
|
294
313
|
|
|
295
314
|
expandAll() {
|
|
@@ -573,12 +592,17 @@ export class TreeViewController {
|
|
|
573
592
|
searchMatches: this.searchHighlights,
|
|
574
593
|
sort: this.columnModel.sort,
|
|
575
594
|
sortValues: this.sortValueSnapshot ? Array.from(this.sortValueSnapshot) : null,
|
|
595
|
+
inspectorPaneLabelEnd: this.#computeInspectorPaneLabelEnd(visibleRange),
|
|
576
596
|
filterQuery: this.filterQuery,
|
|
577
597
|
headerFilter: Boolean(this.inspector?.options?.filter),
|
|
578
598
|
stats: this.getStats(),
|
|
579
599
|
};
|
|
580
600
|
}
|
|
581
601
|
|
|
602
|
+
closeEditor() {
|
|
603
|
+
this.events.emit('editorclose', {});
|
|
604
|
+
}
|
|
605
|
+
|
|
582
606
|
hitTest(clientX, clientY) {
|
|
583
607
|
const localX = clientX - (this.viewport.renderInsetX ?? 0);
|
|
584
608
|
const localY = clientY - (this.viewport.renderInsetY ?? 0);
|
|
@@ -605,11 +629,16 @@ export class TreeViewController {
|
|
|
605
629
|
if (column.kind === 'inspectorPane') {
|
|
606
630
|
const localX = x - column.x;
|
|
607
631
|
const node = this.model.nodes[row.nodeIndex];
|
|
632
|
+
if (node?.data?.valueType === 'object') {
|
|
633
|
+
const treeX = row.depth * this.rowModel.indentWidth;
|
|
634
|
+
part = localX >= treeX + 4 && localX <= treeX + 22 ? 'chevron' : 'label';
|
|
635
|
+
return { area: 'row', part, row, column, x, y: rowY };
|
|
636
|
+
}
|
|
608
637
|
if (node?.data?.valueType === 'array') {
|
|
609
638
|
if (localX >= column.width - 54 && localX <= column.width - 32) part = 'arrayAdd';
|
|
610
639
|
else if (localX >= column.width - 28 && localX <= column.width - 6) part = 'arrayRemove';
|
|
611
640
|
else {
|
|
612
|
-
const editorLeft =
|
|
641
|
+
const editorLeft = this.getInspectorPaneLayout(this.#visibleInspectorPaneWidth(column), row, node?.data?.editorType).editorLeft;
|
|
613
642
|
if (localX >= editorLeft) part = 'editor';
|
|
614
643
|
else {
|
|
615
644
|
const treeX = row.depth * this.rowModel.indentWidth;
|
|
@@ -618,8 +647,8 @@ export class TreeViewController {
|
|
|
618
647
|
}
|
|
619
648
|
return { area: 'row', part, row, column, x, y: rowY };
|
|
620
649
|
}
|
|
621
|
-
const
|
|
622
|
-
if (localX >= editorLeft) part = this.#inspectorEditorPart(row, localX - editorLeft,
|
|
650
|
+
const layout = this.getInspectorPaneLayout(this.#visibleInspectorPaneWidth(column), row, node?.data?.editorType);
|
|
651
|
+
if (localX >= layout.editorLeft) part = this.#inspectorEditorPart(row, localX - layout.editorLeft, layout.editorWidth);
|
|
623
652
|
else {
|
|
624
653
|
const treeX = row.depth * this.rowModel.indentWidth;
|
|
625
654
|
if (localX >= treeX + 4 && localX <= treeX + 22) part = 'chevron';
|
|
@@ -642,13 +671,23 @@ export class TreeViewController {
|
|
|
642
671
|
if (!data?.inspector) return 'cell';
|
|
643
672
|
if (data.editorType === 'checkbox') return 'checkbox';
|
|
644
673
|
if (data.editorType === 'range') {
|
|
645
|
-
const
|
|
674
|
+
const width = Math.max(24, editorWidth - 20);
|
|
675
|
+
const valueWidth = Math.min(64, Math.max(42, width * 0.28));
|
|
676
|
+
const numberLeft = editorWidth - valueWidth - 10;
|
|
646
677
|
return localEditorX >= numberLeft ? 'number' : 'range';
|
|
647
678
|
}
|
|
648
679
|
if (data.editorType === 'button') return 'button';
|
|
649
680
|
return 'editor';
|
|
650
681
|
}
|
|
651
682
|
|
|
683
|
+
#visibleInspectorPaneWidth(column) {
|
|
684
|
+
return Math.max(1, Math.min(column.width, this.viewport.scrollX + this.viewport.viewportWidth - column.x));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
getInspectorPaneLayout(width, row = null, editorType = '') {
|
|
688
|
+
return inspectorPaneLayout(width, row?.depth ?? 0, this.rowModel.indentWidth, editorType, this.#computeInspectorPaneLabelEnd(this.rowModel.getVisibleRange(this.viewport, 4)));
|
|
689
|
+
}
|
|
690
|
+
|
|
652
691
|
getCellClientRect(hit) {
|
|
653
692
|
if (!hit?.row || !hit.column || !this.canvas) return { x: 0, y: 0, width: 0, height: 0 };
|
|
654
693
|
const rect = this.canvas.getBoundingClientRect();
|
|
@@ -671,8 +710,57 @@ export class TreeViewController {
|
|
|
671
710
|
};
|
|
672
711
|
}
|
|
673
712
|
|
|
713
|
+
getTooltipForHit(hit) {
|
|
714
|
+
if (!hit?.row || !hit.column) return null;
|
|
715
|
+
const node = this.model.nodes[hit.row.nodeIndex];
|
|
716
|
+
if (!node) return null;
|
|
717
|
+
const state = this.model.dynamicState.get(hit.row.nodeId) ?? {};
|
|
718
|
+
const rect = this.getCellClientRect(hit);
|
|
719
|
+
let text = '';
|
|
720
|
+
let width = rect.width;
|
|
721
|
+
|
|
722
|
+
if (hit.column.kind === 'inspectorPane') {
|
|
723
|
+
const data = node.data ?? {};
|
|
724
|
+
const visibleWidth = this.#visibleInspectorPaneWidth(hit.column);
|
|
725
|
+
const layout = this.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
|
|
726
|
+
if (hit.part === 'label' || hit.part === 'chevron') {
|
|
727
|
+
const labelX = hit.row.depth * this.rowModel.indentWidth + (hit.row.hasChildren ? 28 : 24);
|
|
728
|
+
text = node.label ?? node.id;
|
|
729
|
+
width = data.valueType === 'object' ? Math.max(0, visibleWidth - labelX - 8) : Math.max(0, layout.editorLeft - labelX - 8);
|
|
730
|
+
} else if (hit.part === 'arrayAdd' || hit.part === 'arrayRemove') {
|
|
731
|
+
return null;
|
|
732
|
+
} else {
|
|
733
|
+
text = inspectorTooltipValue(node);
|
|
734
|
+
width = Math.max(0, layout.editorWidth - 20);
|
|
735
|
+
}
|
|
736
|
+
} else if (hit.column.kind === 'tree') {
|
|
737
|
+
const labelX = hit.row.depth * this.rowModel.indentWidth + 50;
|
|
738
|
+
text = node.label ?? node.id;
|
|
739
|
+
width = Math.max(0, hit.column.width - labelX - 6);
|
|
740
|
+
} else if (hit.column.kind === 'inspectorValue') {
|
|
741
|
+
text = inspectorTooltipValue(node);
|
|
742
|
+
width = Math.max(0, hit.column.width - 20);
|
|
743
|
+
} else if (hit.column.kind === 'inspectorType') {
|
|
744
|
+
text = node.data?.valueType ?? '';
|
|
745
|
+
width = Math.max(0, hit.column.width - 20);
|
|
746
|
+
} else if (hit.column.kind === 'inspectorDescription') {
|
|
747
|
+
text = node.data?.meta?.description ?? '';
|
|
748
|
+
width = Math.max(0, hit.column.width - 20);
|
|
749
|
+
} else if (typeof hit.column.value === 'function') {
|
|
750
|
+
const value = hit.column.value(node, state);
|
|
751
|
+
text = value == null ? '' : String(value);
|
|
752
|
+
width = Math.max(0, hit.column.width - 20);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
text = String(text ?? '');
|
|
756
|
+
if (!text || !isProbablyTruncated(text, width)) return null;
|
|
757
|
+
return { text, rect, nodeId: node.id, part: hit.part, columnId: hit.column.id };
|
|
758
|
+
}
|
|
759
|
+
|
|
674
760
|
resize(width, height) {
|
|
675
761
|
this.viewport.resize(width, height);
|
|
762
|
+
this.#fitInspectorPaneColumn(width);
|
|
763
|
+
this.#syncContentSize();
|
|
676
764
|
this.events.emit('viewportchange', this.getViewportState());
|
|
677
765
|
}
|
|
678
766
|
|
|
@@ -715,6 +803,7 @@ export class TreeViewController {
|
|
|
715
803
|
}
|
|
716
804
|
|
|
717
805
|
#syncContentSize() {
|
|
806
|
+
if (this.viewport.viewportWidth > 1) this.#fitInspectorPaneColumn(this.viewport.viewportWidth);
|
|
718
807
|
this.viewport.setContentSize(this.columnModel.contentWidth, this.rowModel.contentHeight);
|
|
719
808
|
}
|
|
720
809
|
|
|
@@ -726,6 +815,13 @@ export class TreeViewController {
|
|
|
726
815
|
this.setDynamicState(this.patchBatcher.flush());
|
|
727
816
|
}
|
|
728
817
|
|
|
818
|
+
#restoreExpansion(expandedIds) {
|
|
819
|
+
this.model.expanded.clear();
|
|
820
|
+
for (const id of expandedIds) {
|
|
821
|
+
if (this.model.index.getNode(id) && this.expansion.hasChildren(id)) this.model.expanded.add(id);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
729
825
|
#focusedRowIndex() {
|
|
730
826
|
if (this.focusedId) {
|
|
731
827
|
const row = this.rowModel.getRowById(this.focusedId);
|
|
@@ -740,12 +836,42 @@ export class TreeViewController {
|
|
|
740
836
|
const height = this.canvas.clientHeight;
|
|
741
837
|
if (width > 0 && height > 0 && (this.viewport.viewportWidth !== width || this.viewport.viewportHeight !== height)) {
|
|
742
838
|
this.viewport.resize(width, height);
|
|
839
|
+
this.#fitInspectorPaneColumn(width);
|
|
840
|
+
this.#syncContentSize();
|
|
743
841
|
}
|
|
744
842
|
}
|
|
745
843
|
|
|
844
|
+
#computeInspectorPaneLabelEnd(visibleRange) {
|
|
845
|
+
const column = this.columnModel.columns.find((item) => item.kind === 'inspectorPane');
|
|
846
|
+
if (!column) return 0;
|
|
847
|
+
let labelEnd = 0;
|
|
848
|
+
for (let i = visibleRange.first; i <= visibleRange.last; i++) {
|
|
849
|
+
const row = this.rowModel.rows[i];
|
|
850
|
+
if (!row) continue;
|
|
851
|
+
const node = this.model.nodes[row.nodeIndex];
|
|
852
|
+
if (!node) continue;
|
|
853
|
+
if (node.data?.valueType === 'object') continue;
|
|
854
|
+
const indentX = row.depth * this.rowModel.indentWidth;
|
|
855
|
+
const labelX = indentX + (row.hasChildren ? 28 : 24);
|
|
856
|
+
const labelWidth = String(node.label ?? node.id ?? '').length * 6.4;
|
|
857
|
+
labelEnd = Math.max(labelEnd, labelX + labelWidth + 12);
|
|
858
|
+
}
|
|
859
|
+
return Math.min(labelEnd, Math.max(90, this.#visibleInspectorPaneWidth(column) - 72));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
#fitInspectorPaneColumn(width) {
|
|
863
|
+
const column = this.columnModel.columns.length === 1 ? this.columnModel.columns[0] : null;
|
|
864
|
+
if (column?.kind !== 'inspectorPane' && column?.kind !== 'tree') return;
|
|
865
|
+
if (column.kind === 'inspectorPane' && this.inspector?.options?.presentation !== 'pane') return;
|
|
866
|
+
const nextWidth = Math.max(column.minWidth, Math.floor(width));
|
|
867
|
+
if (Math.abs(column.width - nextWidth) < 1) return;
|
|
868
|
+
this.columnModel.resizeColumn(column.id, nextWidth);
|
|
869
|
+
}
|
|
870
|
+
|
|
746
871
|
#workerRowOptions(overrides = {}) {
|
|
747
872
|
return {
|
|
748
873
|
expandedIds: Array.from(this.expansion.model.expanded),
|
|
874
|
+
filterCollapsedIds: Array.from(this.rowModel.filterCollapsed ?? []),
|
|
749
875
|
rowHeight: this.rowModel.rowHeight,
|
|
750
876
|
indentWidth: this.rowModel.indentWidth,
|
|
751
877
|
sort: this.columnModel.sort,
|
|
@@ -777,6 +903,22 @@ export class TreeViewController {
|
|
|
777
903
|
}
|
|
778
904
|
}
|
|
779
905
|
|
|
906
|
+
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
907
|
+
const safeWidth = Math.max(1, width);
|
|
908
|
+
const rightPadding = 14;
|
|
909
|
+
if (editorType === 'checkbox') {
|
|
910
|
+
const editorWidth = 34;
|
|
911
|
+
return { editorLeft: Math.max(64, safeWidth - rightPadding - editorWidth), editorWidth };
|
|
912
|
+
}
|
|
913
|
+
const minEditor = Math.min(170, Math.max(96, safeWidth * 0.45));
|
|
914
|
+
const minLabelEnd = Math.max(88, depth * indentWidth + 104);
|
|
915
|
+
const preferredLeft = Math.max(minLabelEnd, labelEnd, safeWidth * 0.32);
|
|
916
|
+
const maxLeft = Math.max(64, safeWidth - rightPadding - minEditor);
|
|
917
|
+
const editorLeft = Math.max(64, Math.min(preferredLeft, maxLeft));
|
|
918
|
+
const editorWidth = Math.max(56, safeWidth - rightPadding - editorLeft);
|
|
919
|
+
return { editorLeft, editorWidth };
|
|
920
|
+
}
|
|
921
|
+
|
|
780
922
|
function createDefaultArrayItem(meta = {}) {
|
|
781
923
|
if (meta.itemFactory) return meta.itemFactory();
|
|
782
924
|
if (meta.itemType === 'number') return 0;
|
|
@@ -820,6 +962,19 @@ function matchesFilter(node, state, path, query) {
|
|
|
820
962
|
return values.some((value) => String(value ?? '').toLowerCase().includes(query));
|
|
821
963
|
}
|
|
822
964
|
|
|
965
|
+
function inspectorTooltipValue(node) {
|
|
966
|
+
const data = node.data ?? {};
|
|
967
|
+
if (data.editorType === 'checkbox') return '';
|
|
968
|
+
if (data.editorType === 'button') return data.meta?.button ?? node.label ?? '';
|
|
969
|
+
if (data.valueType === 'object') return '';
|
|
970
|
+
return data.valueText ?? data.value ?? '';
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function isProbablyTruncated(text, width) {
|
|
974
|
+
if (width <= 0) return Boolean(text);
|
|
975
|
+
return String(text).length * 6.4 > width;
|
|
976
|
+
}
|
|
977
|
+
|
|
823
978
|
function now() {
|
|
824
979
|
return globalThis.performance?.now?.() ?? Date.now();
|
|
825
980
|
}
|