virtual-tree-canvas 0.3.6 → 0.3.8
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 +1 -1
- package/src/core/search-index.js +63 -11
- package/src/core/tree-view-viewport.js +24 -2
- package/src/core/tree-worker-operations.js +55 -4
- package/src/inspector/cell-editor-manager.js +3 -3
- package/src/renderers/tree-row-renderer.js +10 -5
- package/src/tree-view-controller.js +78 -20
package/package.json
CHANGED
package/src/core/search-index.js
CHANGED
|
@@ -8,13 +8,21 @@ export class TreeSearchIndex {
|
|
|
8
8
|
|
|
9
9
|
/** @param {import('./tree-model.js').TreeModel} model */
|
|
10
10
|
rebuild(model) {
|
|
11
|
-
this.records = model.nodes.map((node) =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
this.records = model.nodes.map((node) => {
|
|
12
|
+
const path = model.index.pathById.get(node.id) ?? '';
|
|
13
|
+
const searchId = searchableNodeId(node);
|
|
14
|
+
const record = {
|
|
15
|
+
id: node.id,
|
|
16
|
+
searchId: normalize(searchId),
|
|
17
|
+
label: normalize(node.label ?? ''),
|
|
18
|
+
path: normalize(path),
|
|
19
|
+
tags: normalize((node.tags ?? []).join(' ')),
|
|
20
|
+
type: normalize(node.type ?? ''),
|
|
21
|
+
value: normalize(searchableNodeValue(node)),
|
|
22
|
+
};
|
|
23
|
+
record.searchText = defaultSearchText(node, record);
|
|
24
|
+
return record;
|
|
25
|
+
});
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
/**
|
|
@@ -32,11 +40,16 @@ export class TreeSearchIndex {
|
|
|
32
40
|
const fields = options.fields ?? ['label', 'id', 'path', 'tags', 'type'];
|
|
33
41
|
const limit = options.limit ?? 100;
|
|
34
42
|
const results = [];
|
|
43
|
+
const defaultFields = isDefaultSearchFields(fields);
|
|
35
44
|
for (const record of this.records) {
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
if (defaultFields) {
|
|
46
|
+
if (record.searchText.includes(q)) results.push(record.id);
|
|
47
|
+
} else {
|
|
48
|
+
for (const field of fields) {
|
|
49
|
+
if (searchFieldValue(record, field).includes(q)) {
|
|
50
|
+
results.push(record.id);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
55
|
if (results.length >= limit) break;
|
|
@@ -68,3 +81,42 @@ export class TreeSearchIndex {
|
|
|
68
81
|
this.lastQuery = '';
|
|
69
82
|
}
|
|
70
83
|
}
|
|
84
|
+
|
|
85
|
+
function isDefaultSearchFields(fields) {
|
|
86
|
+
return fields.length === 5 && fields.includes('label') && fields.includes('id') && fields.includes('path') && fields.includes('tags') && fields.includes('type');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function searchFieldValue(record, field) {
|
|
90
|
+
if (field === 'id') return record.searchId || normalize(record.id);
|
|
91
|
+
return normalize(record[field] ?? '');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function searchableNodeId(node) {
|
|
95
|
+
if (node?.data?.inspector) return node.data.key ?? node.label ?? node.id;
|
|
96
|
+
return node?.id ?? '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function searchableNodeValue(node) {
|
|
100
|
+
if (!node?.data?.inspector) return '';
|
|
101
|
+
return node.data.valueText ?? node.data.value ?? '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function defaultSearchText(node, record) {
|
|
105
|
+
if (node?.data?.inspector) {
|
|
106
|
+
const data = node.data;
|
|
107
|
+
return normalize([
|
|
108
|
+
record.searchId,
|
|
109
|
+
node.label ?? '',
|
|
110
|
+
data.key ?? '',
|
|
111
|
+
data.valueText ?? '',
|
|
112
|
+
data.valueType ?? '',
|
|
113
|
+
record.tags,
|
|
114
|
+
record.type,
|
|
115
|
+
].join(' '));
|
|
116
|
+
}
|
|
117
|
+
return normalize(`${record.searchId} ${record.label} ${record.path} ${record.tags} ${record.type}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalize(value) {
|
|
121
|
+
return String(value ?? '').toLowerCase();
|
|
122
|
+
}
|
|
@@ -13,10 +13,17 @@ export class TreeViewViewport extends EventTarget {
|
|
|
13
13
|
this.contentWidth = 1;
|
|
14
14
|
this.contentHeight = 1;
|
|
15
15
|
this.zoom = 1;
|
|
16
|
+
this.scrollbarSize = 0;
|
|
17
|
+
this.verticalScrollbarVisible = false;
|
|
18
|
+
this.horizontalScrollbarVisible = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get contentViewportWidth() {
|
|
22
|
+
return Math.max(1, this.viewportWidth - (this.verticalScrollbarVisible ? this.scrollbarSize : 0));
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
get rowViewportHeight() {
|
|
19
|
-
return Math.max(1, this.viewportHeight - this.headerHeight);
|
|
26
|
+
return Math.max(1, this.viewportHeight - this.headerHeight - (this.horizontalScrollbarVisible ? this.scrollbarSize : 0));
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
resize(width, height) {
|
|
@@ -33,6 +40,21 @@ export class TreeViewViewport extends EventTarget {
|
|
|
33
40
|
this.dispatchEvent(new Event('change'));
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
setScrollbarState({ size = this.scrollbarSize, vertical = this.verticalScrollbarVisible, horizontal = this.horizontalScrollbarVisible } = {}) {
|
|
44
|
+
const nextSize = Math.max(0, size);
|
|
45
|
+
const nextVertical = Boolean(vertical && nextSize > 0);
|
|
46
|
+
const nextHorizontal = Boolean(horizontal && nextSize > 0);
|
|
47
|
+
const changed =
|
|
48
|
+
this.scrollbarSize !== nextSize ||
|
|
49
|
+
this.verticalScrollbarVisible !== nextVertical ||
|
|
50
|
+
this.horizontalScrollbarVisible !== nextHorizontal;
|
|
51
|
+
this.scrollbarSize = nextSize;
|
|
52
|
+
this.verticalScrollbarVisible = nextVertical;
|
|
53
|
+
this.horizontalScrollbarVisible = nextHorizontal;
|
|
54
|
+
this.clamp();
|
|
55
|
+
return changed;
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
scrollBy(dx, dy) {
|
|
37
59
|
this.scrollX += dx;
|
|
38
60
|
this.scrollY += dy;
|
|
@@ -61,7 +83,7 @@ export class TreeViewViewport extends EventTarget {
|
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
clamp() {
|
|
64
|
-
const maxX = Math.max(0, this.contentWidth - this.
|
|
86
|
+
const maxX = Math.max(0, this.contentWidth - this.contentViewportWidth);
|
|
65
87
|
const maxY = Math.max(0, this.contentHeight - this.rowViewportHeight);
|
|
66
88
|
this.scrollX = Math.max(0, Math.min(maxX, this.scrollX));
|
|
67
89
|
this.scrollY = Math.max(0, Math.min(maxY, this.scrollY));
|
|
@@ -27,14 +27,18 @@ export function createWorkerTreeState(nodes) {
|
|
|
27
27
|
const records = nodes.map((node) => {
|
|
28
28
|
const path = pathById.get(node.id) ?? '';
|
|
29
29
|
const tags = (node.tags ?? []).join(' ');
|
|
30
|
-
|
|
30
|
+
const record = {
|
|
31
31
|
id: node.id,
|
|
32
|
+
searchId: searchableNodeId(node),
|
|
32
33
|
label: node.label ?? '',
|
|
33
34
|
path,
|
|
34
35
|
tags,
|
|
35
36
|
type: node.type ?? '',
|
|
36
|
-
|
|
37
|
+
value: searchableNodeValue(node),
|
|
37
38
|
};
|
|
39
|
+
record.searchText = defaultSearchText(node, record);
|
|
40
|
+
record.filterText = defaultFilterText(node, record);
|
|
41
|
+
return record;
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
return { nodes, idToIndex, childrenByParent, parentById, roots, pathById, records };
|
|
@@ -47,7 +51,7 @@ export function searchWorkerTree(state, query, { fields = ['label', 'id', 'path'
|
|
|
47
51
|
const defaultFields = fields.length === 5 && fields.includes('label') && fields.includes('id') && fields.includes('path') && fields.includes('tags') && fields.includes('type');
|
|
48
52
|
|
|
49
53
|
for (const record of state.records) {
|
|
50
|
-
const matches = defaultFields ? record.searchText.includes(q) : fields.some((field) =>
|
|
54
|
+
const matches = defaultFields ? record.searchText.includes(q) : fields.some((field) => searchFieldValue(record, field).includes(q));
|
|
51
55
|
if (!matches) continue;
|
|
52
56
|
results.push(record.id);
|
|
53
57
|
if (results.length >= limit) break;
|
|
@@ -115,7 +119,7 @@ export function getIncludedIdsForQuery(state, query, candidateIds = null) {
|
|
|
115
119
|
const records = candidateIds ? idsToRecords(state, candidateIds) : state.records;
|
|
116
120
|
|
|
117
121
|
for (const record of records) {
|
|
118
|
-
if (!record.searchText.includes(q)) continue;
|
|
122
|
+
if (!(record.filterText ?? record.searchText).includes(q)) continue;
|
|
119
123
|
matchingIds.push(record.id);
|
|
120
124
|
let id = record.id;
|
|
121
125
|
while (id !== null && id !== undefined && !includedIds.has(id)) {
|
|
@@ -152,3 +156,50 @@ function columnValue(node, columnId) {
|
|
|
152
156
|
function normalize(value) {
|
|
153
157
|
return String(value ?? '').toLowerCase();
|
|
154
158
|
}
|
|
159
|
+
|
|
160
|
+
function searchFieldValue(record, field) {
|
|
161
|
+
if (field === 'id') return normalize(record.searchId || record.id);
|
|
162
|
+
return normalize(record[field]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function searchableNodeId(node) {
|
|
166
|
+
if (node?.data?.inspector) return node.data.key ?? node.label ?? node.id;
|
|
167
|
+
return node?.id ?? '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function searchableNodeValue(node) {
|
|
171
|
+
if (!node?.data?.inspector) return '';
|
|
172
|
+
return node.data.valueText ?? node.data.value ?? '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function defaultSearchText(node, record) {
|
|
176
|
+
if (node?.data?.inspector) {
|
|
177
|
+
const data = node.data;
|
|
178
|
+
return normalize([
|
|
179
|
+
record.searchId,
|
|
180
|
+
node.label ?? '',
|
|
181
|
+
data.key ?? '',
|
|
182
|
+
data.valueText ?? '',
|
|
183
|
+
data.valueType ?? '',
|
|
184
|
+
record.tags,
|
|
185
|
+
record.type,
|
|
186
|
+
].join(' '));
|
|
187
|
+
}
|
|
188
|
+
return normalize(`${record.searchId} ${record.label} ${record.path} ${record.tags} ${record.type}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function defaultFilterText(node, record) {
|
|
192
|
+
if (node?.data?.inspector) {
|
|
193
|
+
const data = node.data;
|
|
194
|
+
return normalize([
|
|
195
|
+
node.label ?? '',
|
|
196
|
+
node.type ?? '',
|
|
197
|
+
data.path ?? '',
|
|
198
|
+
data.key ?? '',
|
|
199
|
+
data.valueText ?? '',
|
|
200
|
+
data.meta?.description ?? '',
|
|
201
|
+
...Object.keys(data.meta?.options ?? {}),
|
|
202
|
+
].join(' '));
|
|
203
|
+
}
|
|
204
|
+
return record.searchText;
|
|
205
|
+
}
|
|
@@ -141,7 +141,7 @@ export class CellEditorManager {
|
|
|
141
141
|
const rect = this.#clampRectToHost({
|
|
142
142
|
x: headerRect.x + 8,
|
|
143
143
|
y: headerRect.y + 5,
|
|
144
|
-
width: Math.max(24, Math.min(headerRect.width, this.controller.viewport.
|
|
144
|
+
width: Math.max(24, Math.min(headerRect.width, this.controller.viewport.contentViewportWidth) - 16),
|
|
145
145
|
height: Math.max(20, headerRect.height - 10),
|
|
146
146
|
}, 0);
|
|
147
147
|
const hostRect = this.host.getBoundingClientRect();
|
|
@@ -214,7 +214,7 @@ export class CellEditorManager {
|
|
|
214
214
|
#overlayRect(hit) {
|
|
215
215
|
if (hit.column?.kind !== 'inspectorPane') return this.controller.getCellClientRect(hit);
|
|
216
216
|
const rect = this.controller.getCellClientRect(hit);
|
|
217
|
-
const visibleWidth = Math.max(1, Math.min(rect.width, this.controller.viewport.
|
|
217
|
+
const visibleWidth = Math.max(1, Math.min(rect.width, this.controller.viewport.contentViewportWidth));
|
|
218
218
|
const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
|
|
219
219
|
const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
|
|
220
220
|
if (hit.part === 'number') {
|
|
@@ -227,7 +227,7 @@ export class CellEditorManager {
|
|
|
227
227
|
#rangeBarRect(hit) {
|
|
228
228
|
const rect = this.controller.getCellClientRect(hit);
|
|
229
229
|
if (hit.column?.kind === 'inspectorPane') {
|
|
230
|
-
const visibleWidth = Math.max(1, Math.min(rect.width, this.controller.viewport.
|
|
230
|
+
const visibleWidth = Math.max(1, Math.min(rect.width, this.controller.viewport.contentViewportWidth));
|
|
231
231
|
const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
|
|
232
232
|
const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
|
|
233
233
|
const valueWidth = Math.min(64, Math.max(42, (editorWidth - 20) * 0.28));
|
|
@@ -58,9 +58,13 @@ export class TreeRowRenderer {
|
|
|
58
58
|
const { viewport, columns, theme, sort, headerFilter, filterQuery } = this.scene;
|
|
59
59
|
if (viewport.headerHeight <= 0) return;
|
|
60
60
|
const colors = theme.colors;
|
|
61
|
+
const visibleWidth = viewport.contentViewportWidth ?? viewport.viewportWidth;
|
|
61
62
|
ctx.save();
|
|
62
63
|
ctx.fillStyle = colors.row;
|
|
63
64
|
ctx.fillRect(0, 0, viewport.viewportWidth, viewport.headerHeight);
|
|
65
|
+
ctx.beginPath();
|
|
66
|
+
ctx.rect(0, 0, visibleWidth, viewport.headerHeight);
|
|
67
|
+
ctx.clip();
|
|
64
68
|
ctx.translate(-viewport.scrollX, 0);
|
|
65
69
|
ctx.font = theme.font;
|
|
66
70
|
ctx.textBaseline = 'middle';
|
|
@@ -86,7 +90,7 @@ export class TreeRowRenderer {
|
|
|
86
90
|
ctx.strokeStyle = colors.border;
|
|
87
91
|
ctx.beginPath();
|
|
88
92
|
ctx.moveTo(0, viewport.headerHeight + 0.5);
|
|
89
|
-
ctx.lineTo(
|
|
93
|
+
ctx.lineTo(visibleWidth, viewport.headerHeight + 0.5);
|
|
90
94
|
ctx.stroke();
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -94,7 +98,7 @@ export class TreeRowRenderer {
|
|
|
94
98
|
const colors = theme.colors;
|
|
95
99
|
const x = column.x + 8;
|
|
96
100
|
const y = 5;
|
|
97
|
-
const visibleRight = viewport.scrollX + viewport.viewportWidth;
|
|
101
|
+
const visibleRight = viewport.scrollX + (viewport.contentViewportWidth ?? viewport.viewportWidth);
|
|
98
102
|
const visibleWidth = Math.max(1, Math.min(column.x + column.width, visibleRight) - column.x);
|
|
99
103
|
const width = Math.max(40, visibleWidth - 16);
|
|
100
104
|
const height = Math.max(18, viewport.headerHeight - 10);
|
|
@@ -126,9 +130,10 @@ export class TreeRowRenderer {
|
|
|
126
130
|
#drawRows(ctx) {
|
|
127
131
|
const { rows, visibleRange, viewport } = this.scene;
|
|
128
132
|
this.renderedRows = visibleRange.count;
|
|
133
|
+
const visibleWidth = viewport.contentViewportWidth ?? viewport.viewportWidth;
|
|
129
134
|
ctx.save();
|
|
130
135
|
ctx.beginPath();
|
|
131
|
-
ctx.rect(0, viewport.headerHeight,
|
|
136
|
+
ctx.rect(0, viewport.headerHeight, visibleWidth, viewport.rowViewportHeight);
|
|
132
137
|
ctx.clip();
|
|
133
138
|
ctx.translate(-viewport.scrollX, viewport.headerHeight - viewport.scrollY);
|
|
134
139
|
for (let i = visibleRange.first; i <= visibleRange.last; i++) {
|
|
@@ -150,7 +155,7 @@ export class TreeRowRenderer {
|
|
|
150
155
|
const focused = focusNodeId === row.nodeId;
|
|
151
156
|
const y = row.y;
|
|
152
157
|
const visibleX = viewport.scrollX;
|
|
153
|
-
const visibleWidth = viewport.viewportWidth;
|
|
158
|
+
const visibleWidth = viewport.contentViewportWidth ?? viewport.viewportWidth;
|
|
154
159
|
|
|
155
160
|
ctx.fillStyle = selected ? colors.rowSelected : highlighted ? colors.rowHighlighted : hovered ? colors.rowHover : colors.row;
|
|
156
161
|
ctx.fillRect(visibleX, y, visibleWidth, row.height);
|
|
@@ -209,7 +214,7 @@ export class TreeRowRenderer {
|
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
#drawInspectorPaneCell(ctx, { node, row, rect, theme, style, hovered, hoverPart, activePart }) {
|
|
212
|
-
const visibleRight = this.scene.viewport.scrollX + this.scene.viewport.viewportWidth;
|
|
217
|
+
const visibleRight = this.scene.viewport.scrollX + (this.scene.viewport.contentViewportWidth ?? this.scene.viewport.viewportWidth);
|
|
213
218
|
rect = { ...rect, width: Math.max(1, Math.min(rect.x + rect.width, visibleRight) - rect.x) };
|
|
214
219
|
const data = node.data ?? {};
|
|
215
220
|
const colors = theme.colors;
|
|
@@ -700,6 +700,7 @@ export class TreeViewController {
|
|
|
700
700
|
const localX = clientX - (this.viewport.renderInsetX ?? 0);
|
|
701
701
|
const localY = clientY - (this.viewport.renderInsetY ?? 0);
|
|
702
702
|
if (localX < 0 || localY < 0) return null;
|
|
703
|
+
if (localX >= this.viewport.contentViewportWidth) return null;
|
|
703
704
|
const x = localX + this.viewport.scrollX;
|
|
704
705
|
if (localY < this.viewport.headerHeight) {
|
|
705
706
|
const resizeColumn = this.columnModel.getResizeHandleAt(x);
|
|
@@ -711,6 +712,7 @@ export class TreeViewController {
|
|
|
711
712
|
return column ? { area: 'header', part: 'label', column, x, y: localY } : { area: 'header', part: 'header', column: null, x, y: localY };
|
|
712
713
|
}
|
|
713
714
|
|
|
715
|
+
if (localY >= this.viewport.headerHeight + this.viewport.rowViewportHeight) return null;
|
|
714
716
|
const rowY = localY - this.viewport.headerHeight + this.viewport.scrollY;
|
|
715
717
|
const rowIndex = Math.floor(rowY / this.rowModel.rowHeight);
|
|
716
718
|
const row = this.rowModel.getRow(rowIndex);
|
|
@@ -776,7 +778,7 @@ export class TreeViewController {
|
|
|
776
778
|
}
|
|
777
779
|
|
|
778
780
|
#visibleInspectorPaneWidth(column) {
|
|
779
|
-
return Math.max(1, Math.min(column.width, this.viewport.scrollX + this.viewport.
|
|
781
|
+
return Math.max(1, Math.min(column.width, this.viewport.scrollX + this.viewport.contentViewportWidth - column.x));
|
|
780
782
|
}
|
|
781
783
|
|
|
782
784
|
getInspectorPaneLayout(width, row = null, editorType = '') {
|
|
@@ -797,10 +799,11 @@ export class TreeViewController {
|
|
|
797
799
|
getHeaderClientRect(hit) {
|
|
798
800
|
if (!hit?.column || !this.canvas) return { x: 0, y: 0, width: 0, height: 0 };
|
|
799
801
|
const rect = this.canvas.getBoundingClientRect();
|
|
802
|
+
const visibleWidth = Math.max(0, this.viewport.contentViewportWidth - Math.max(0, hit.column.x - this.viewport.scrollX));
|
|
800
803
|
return {
|
|
801
804
|
x: rect.left + (this.viewport.renderInsetX ?? 0) + hit.column.x - this.viewport.scrollX,
|
|
802
805
|
y: rect.top + (this.viewport.renderInsetY ?? 0),
|
|
803
|
-
width: hit.column.width,
|
|
806
|
+
width: Math.min(hit.column.width, visibleWidth || hit.column.width),
|
|
804
807
|
height: this.viewport.headerHeight,
|
|
805
808
|
};
|
|
806
809
|
}
|
|
@@ -889,16 +892,17 @@ export class TreeViewController {
|
|
|
889
892
|
}
|
|
890
893
|
|
|
891
894
|
#rebuildRows() {
|
|
892
|
-
const
|
|
893
|
-
const scrollY = this.viewport.scrollY;
|
|
895
|
+
const scrollAnchor = this.#captureScrollAnchor();
|
|
894
896
|
this.rowModel.rebuild();
|
|
895
897
|
this.#syncContentSize();
|
|
896
|
-
this
|
|
898
|
+
this.#restoreScrollAnchor(scrollAnchor);
|
|
897
899
|
this.rebuildCount++;
|
|
898
900
|
}
|
|
899
901
|
|
|
900
902
|
#syncContentSize() {
|
|
901
|
-
|
|
903
|
+
this.#syncScrollbarState(this.columnModel.contentWidth, this.rowModel.contentHeight);
|
|
904
|
+
if (this.viewport.viewportWidth > 1) this.#fitInspectorPaneColumn(this.viewport.contentViewportWidth);
|
|
905
|
+
this.#syncScrollbarState(this.columnModel.contentWidth, this.rowModel.contentHeight);
|
|
902
906
|
this.viewport.setContentSize(this.columnModel.contentWidth, this.rowModel.contentHeight);
|
|
903
907
|
}
|
|
904
908
|
|
|
@@ -973,12 +977,13 @@ export class TreeViewController {
|
|
|
973
977
|
applyNativeScrollbarTheme([vertical, horizontal, corner], this.themeManager.get());
|
|
974
978
|
|
|
975
979
|
const size = nativeScrollbarSize();
|
|
980
|
+
this.#nativeScrollbarSize = size;
|
|
976
981
|
Object.assign(vertical.style, {
|
|
977
982
|
position: 'absolute',
|
|
978
|
-
top:
|
|
979
|
-
|
|
980
|
-
bottom: '0',
|
|
983
|
+
top: '0',
|
|
984
|
+
left: '0',
|
|
981
985
|
width: `${size}px`,
|
|
986
|
+
height: '1px',
|
|
982
987
|
overflowX: 'hidden',
|
|
983
988
|
overflowY: 'auto',
|
|
984
989
|
zIndex: '4',
|
|
@@ -986,8 +991,8 @@ export class TreeViewController {
|
|
|
986
991
|
Object.assign(horizontal.style, {
|
|
987
992
|
position: 'absolute',
|
|
988
993
|
left: '0',
|
|
989
|
-
|
|
990
|
-
|
|
994
|
+
top: '0',
|
|
995
|
+
width: '1px',
|
|
991
996
|
height: `${size}px`,
|
|
992
997
|
overflowX: 'auto',
|
|
993
998
|
overflowY: 'hidden',
|
|
@@ -995,8 +1000,8 @@ export class TreeViewController {
|
|
|
995
1000
|
});
|
|
996
1001
|
Object.assign(corner.style, {
|
|
997
1002
|
position: 'absolute',
|
|
998
|
-
|
|
999
|
-
|
|
1003
|
+
left: '0',
|
|
1004
|
+
top: '0',
|
|
1000
1005
|
width: `${size}px`,
|
|
1001
1006
|
height: `${size}px`,
|
|
1002
1007
|
zIndex: '4',
|
|
@@ -1018,17 +1023,24 @@ export class TreeViewController {
|
|
|
1018
1023
|
|
|
1019
1024
|
let syncing = false;
|
|
1020
1025
|
const sync = () => {
|
|
1021
|
-
|
|
1026
|
+
this.#syncScrollbarState(this.viewport.contentWidth, this.viewport.contentHeight);
|
|
1027
|
+
const maxX = Math.max(0, this.viewport.contentWidth - this.viewport.contentViewportWidth);
|
|
1022
1028
|
const maxY = Math.max(0, this.viewport.contentHeight - this.viewport.rowViewportHeight);
|
|
1023
1029
|
const showX = maxX > 0;
|
|
1024
1030
|
const showY = maxY > 0;
|
|
1031
|
+
const canvasMetrics = this.#canvasHostMetrics();
|
|
1025
1032
|
|
|
1026
1033
|
vertical.style.display = showY ? 'block' : 'none';
|
|
1027
1034
|
horizontal.style.display = showX ? 'block' : 'none';
|
|
1028
1035
|
corner.style.display = showX && showY ? 'block' : 'none';
|
|
1029
|
-
vertical.style.
|
|
1030
|
-
|
|
1031
|
-
vertical.style.
|
|
1036
|
+
vertical.style.left = `${canvasMetrics.left + canvasMetrics.width - size}px`;
|
|
1037
|
+
vertical.style.top = `${canvasMetrics.top + this.viewport.headerHeight}px`;
|
|
1038
|
+
vertical.style.height = `${Math.max(1, canvasMetrics.height - this.viewport.headerHeight - (showX ? size : 0))}px`;
|
|
1039
|
+
horizontal.style.left = `${canvasMetrics.left}px`;
|
|
1040
|
+
horizontal.style.top = `${canvasMetrics.top + canvasMetrics.height - size}px`;
|
|
1041
|
+
horizontal.style.width = `${Math.max(1, canvasMetrics.width - (showY ? size : 0))}px`;
|
|
1042
|
+
corner.style.left = `${canvasMetrics.left + canvasMetrics.width - size}px`;
|
|
1043
|
+
corner.style.top = `${canvasMetrics.top + canvasMetrics.height - size}px`;
|
|
1032
1044
|
|
|
1033
1045
|
verticalSpacer.style.height = `${Math.ceil(maxY + vertical.clientHeight)}px`;
|
|
1034
1046
|
horizontalSpacer.style.width = `${Math.ceil(maxX + horizontal.clientWidth)}px`;
|
|
@@ -1065,11 +1077,37 @@ export class TreeViewController {
|
|
|
1065
1077
|
vertical.remove();
|
|
1066
1078
|
horizontal.remove();
|
|
1067
1079
|
corner.remove();
|
|
1080
|
+
this.#nativeScrollbarSize = 0;
|
|
1081
|
+
this.viewport.setScrollbarState({ size: 0, vertical: false, horizontal: false });
|
|
1068
1082
|
if (hostPositionChanged) host.style.position = previousHostPosition;
|
|
1069
1083
|
},
|
|
1070
1084
|
};
|
|
1071
1085
|
}
|
|
1072
1086
|
|
|
1087
|
+
#syncScrollbarState(contentWidth, contentHeight) {
|
|
1088
|
+
const size = this.nativeScrollbars ? this.#nativeScrollbarSize : 0;
|
|
1089
|
+
if (!size) {
|
|
1090
|
+
this.viewport.setScrollbarState({ size: 0, vertical: false, horizontal: false });
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
let availableWidth = this.viewport.viewportWidth;
|
|
1094
|
+
let availableRowHeight = Math.max(1, this.viewport.viewportHeight - this.viewport.headerHeight);
|
|
1095
|
+
let showY = contentHeight > availableRowHeight;
|
|
1096
|
+
let showX = contentWidth > availableWidth;
|
|
1097
|
+
if (showY && contentWidth > Math.max(1, availableWidth - size)) showX = true;
|
|
1098
|
+
if (showX && contentHeight > Math.max(1, availableRowHeight - size)) showY = true;
|
|
1099
|
+
this.viewport.setScrollbarState({ size, vertical: showY, horizontal: showX });
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
#canvasHostMetrics() {
|
|
1103
|
+
if (!this.canvas) return { left: 0, top: 0, width: this.viewport.viewportWidth, height: this.viewport.viewportHeight };
|
|
1104
|
+
const left = Number.isFinite(this.canvas.offsetLeft) ? this.canvas.offsetLeft : 0;
|
|
1105
|
+
const top = Number.isFinite(this.canvas.offsetTop) ? this.canvas.offsetTop : 0;
|
|
1106
|
+
const width = Math.max(1, Math.floor(this.canvas.clientWidth || this.canvas.getBoundingClientRect?.().width || this.viewport.viewportWidth));
|
|
1107
|
+
const height = Math.max(1, Math.floor(this.canvas.clientHeight || this.canvas.getBoundingClientRect?.().height || this.viewport.viewportHeight));
|
|
1108
|
+
return { left, top, width, height };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1073
1111
|
#computeInspectorPaneLabelEnd(visibleRange) {
|
|
1074
1112
|
const column = this.columnModel.columns.find((item) => item.kind === 'inspectorPane');
|
|
1075
1113
|
if (!column) return 0;
|
|
@@ -1110,14 +1148,33 @@ export class TreeViewController {
|
|
|
1110
1148
|
}
|
|
1111
1149
|
|
|
1112
1150
|
#applyWorkerRows(result) {
|
|
1113
|
-
const
|
|
1114
|
-
const scrollY = this.viewport.scrollY;
|
|
1151
|
+
const scrollAnchor = this.#captureScrollAnchor();
|
|
1115
1152
|
this.rowModel.applyRows(result);
|
|
1116
1153
|
this.#syncContentSize();
|
|
1117
|
-
this
|
|
1154
|
+
this.#restoreScrollAnchor(scrollAnchor);
|
|
1118
1155
|
this.rebuildCount++;
|
|
1119
1156
|
}
|
|
1120
1157
|
|
|
1158
|
+
#captureScrollAnchor() {
|
|
1159
|
+
const scrollX = this.viewport.scrollX;
|
|
1160
|
+
const scrollY = this.viewport.scrollY;
|
|
1161
|
+
const rowIndex = Math.max(0, Math.floor(scrollY / this.rowModel.rowHeight));
|
|
1162
|
+
const row = this.rowModel.getRow(rowIndex);
|
|
1163
|
+
if (!row) return { scrollX, scrollY, nodeId: null, offsetY: 0 };
|
|
1164
|
+
return {
|
|
1165
|
+
scrollX,
|
|
1166
|
+
scrollY,
|
|
1167
|
+
nodeId: row.nodeId,
|
|
1168
|
+
offsetY: Math.max(0, scrollY - row.y),
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
#restoreScrollAnchor(anchor) {
|
|
1173
|
+
const row = anchor.nodeId ? this.rowModel.getRowById(anchor.nodeId) : null;
|
|
1174
|
+
const scrollY = row ? row.y + anchor.offsetY : anchor.scrollY;
|
|
1175
|
+
this.viewport.scrollTo(anchor.scrollX, scrollY);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1121
1178
|
#rebuildInspectorModel(focusPath = '') {
|
|
1122
1179
|
if (!this.inspector) return;
|
|
1123
1180
|
const expanded = new Set(this.expansion.model.expanded);
|
|
@@ -1133,6 +1190,7 @@ export class TreeViewController {
|
|
|
1133
1190
|
|
|
1134
1191
|
#resizeObserver = null;
|
|
1135
1192
|
#nativeScroll = null;
|
|
1193
|
+
#nativeScrollbarSize = 0;
|
|
1136
1194
|
}
|
|
1137
1195
|
|
|
1138
1196
|
const NATIVE_SCROLLBAR_STYLE_ID = 'virtual-tree-canvas-native-scrollbar-style';
|