virtual-tree-canvas 0.3.5 → 0.3.7
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/index.d.ts +9 -2
- package/src/input/tree-view-input-controller.js +24 -133
- package/src/inspector/cell-editor-manager.js +8 -6
- package/src/renderers/tree-row-renderer.js +10 -5
- package/src/tree-view-controller.js +126 -14
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
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export class TreeRowRenderer {
|
|
|
62
62
|
|
|
63
63
|
export class TreeViewController {
|
|
64
64
|
canvas?: HTMLCanvasElement;
|
|
65
|
+
inputController?: TreeViewInputController | null;
|
|
66
|
+
cellEditor?: CellEditorManager | null;
|
|
65
67
|
viewport: {
|
|
66
68
|
viewportWidth: number;
|
|
67
69
|
viewportHeight: number;
|
|
@@ -72,6 +74,10 @@ export class TreeViewController {
|
|
|
72
74
|
expansion: any;
|
|
73
75
|
selection: any;
|
|
74
76
|
constructor(options?: Record<string, any> & { nativeScrollbars?: boolean });
|
|
77
|
+
initialize(canvas: HTMLCanvasElement): this;
|
|
78
|
+
attachCellEditor(options?: { host?: HTMLElement | null }): CellEditorManager;
|
|
79
|
+
attachInput(options?: { cellEditor?: CellEditorManager | null }): TreeViewInputController;
|
|
80
|
+
destroy(): void;
|
|
75
81
|
on(type: string, listener: (event: any) => void): any;
|
|
76
82
|
off(type: string, listener: (event: any) => void): void;
|
|
77
83
|
setData(nodes: TreeNode[]): void;
|
|
@@ -79,6 +85,7 @@ export class TreeViewController {
|
|
|
79
85
|
setColumns(columns: Column[]): void;
|
|
80
86
|
setDynamicState(patches: DynamicPatch[]): void;
|
|
81
87
|
setTheme(theme: any): void;
|
|
88
|
+
setLayoutMetrics(options?: { rowHeight?: number; indentWidth?: number; headerHeight?: number }): void;
|
|
82
89
|
resize(width: number, height: number): void;
|
|
83
90
|
render(time?: number): void;
|
|
84
91
|
renderMeasured(time?: number): any;
|
|
@@ -98,12 +105,12 @@ export class TreeViewController {
|
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
export class TreeViewInputController {
|
|
101
|
-
constructor(options?:
|
|
108
|
+
constructor(options: { controller: TreeViewController; cellEditor?: CellEditorManager | null });
|
|
102
109
|
destroy(): void;
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
export class CellEditorManager {
|
|
106
|
-
constructor(options?:
|
|
113
|
+
constructor(options: { controller: TreeViewController; host?: HTMLElement | null });
|
|
107
114
|
destroy(): void;
|
|
108
115
|
close(): void;
|
|
109
116
|
}
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
export class TreeViewInputController {
|
|
2
2
|
/**
|
|
3
3
|
* @param {{
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* rowModel: import('../core/visible-row-model.js').VisibleRowModel,
|
|
7
|
-
* expansion: import('../core/tree-expansion-manager.js').TreeExpansionManager,
|
|
8
|
-
* selection: import('../core/selection-manager.js').TreeSelectionManager,
|
|
9
|
-
* controller?: import('../tree-view-controller.js').TreeViewController,
|
|
10
|
-
* onRowsChanged?: () => void,
|
|
11
|
-
* onSelectionChanged?: () => void,
|
|
12
|
-
* onHoverChanged?: (id: string | null) => void
|
|
4
|
+
* controller: import('../tree-view-controller.js').TreeViewController,
|
|
5
|
+
* cellEditor?: import('../inspector/cell-editor-manager.js').CellEditorManager | null
|
|
13
6
|
* }} options
|
|
14
7
|
*/
|
|
15
8
|
constructor(options) {
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
if (!options?.controller) throw new TypeError('TreeViewInputController requires a TreeViewController');
|
|
10
|
+
if (!options.controller.canvas) throw new Error('TreeViewInputController requires an initialized TreeViewController canvas');
|
|
11
|
+
this.controller = options.controller;
|
|
12
|
+
this.canvas = options.controller.canvas;
|
|
18
13
|
this.hoveredId = null;
|
|
19
14
|
this.resizeDrag = null;
|
|
20
15
|
this.cellEditor = options.cellEditor ?? null;
|
|
@@ -44,11 +39,7 @@ export class TreeViewInputController {
|
|
|
44
39
|
#onWheel = (event) => {
|
|
45
40
|
event.preventDefault();
|
|
46
41
|
this.cellEditor?.close?.();
|
|
47
|
-
|
|
48
|
-
this.controller.scrollBy(event.shiftKey ? event.deltaY : event.deltaX, event.deltaY);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
this.viewport.scrollBy(event.shiftKey ? event.deltaY : event.deltaX, event.deltaY);
|
|
42
|
+
this.controller.scrollBy(event.shiftKey ? event.deltaY : event.deltaX, event.deltaY);
|
|
52
43
|
};
|
|
53
44
|
|
|
54
45
|
#onMouseMove = (event) => {
|
|
@@ -56,7 +47,7 @@ export class TreeViewInputController {
|
|
|
56
47
|
const rect = this.canvas.getBoundingClientRect();
|
|
57
48
|
const clientX = event.clientX - rect.left;
|
|
58
49
|
const nextWidth = this.resizeDrag.startWidth + (clientX - this.resizeDrag.startX);
|
|
59
|
-
this.controller
|
|
50
|
+
this.controller.resizeColumn(this.resizeDrag.columnId, nextWidth);
|
|
60
51
|
event.preventDefault();
|
|
61
52
|
return;
|
|
62
53
|
}
|
|
@@ -67,19 +58,15 @@ export class TreeViewInputController {
|
|
|
67
58
|
if (id === this.hoveredId && key === this.hoveredHitKey) return;
|
|
68
59
|
this.hoveredId = id;
|
|
69
60
|
this.hoveredHitKey = key;
|
|
70
|
-
|
|
71
|
-
else this.controller?.setHover(id);
|
|
72
|
-
this.onHoverChanged?.(id);
|
|
61
|
+
this.controller.setHoverHit(hit);
|
|
73
62
|
};
|
|
74
63
|
|
|
75
64
|
#onMouseLeave = () => {
|
|
76
65
|
if (!this.resizeDrag) this.canvas.style.cursor = '';
|
|
77
66
|
this.hoveredId = null;
|
|
78
67
|
this.hoveredHitKey = null;
|
|
79
|
-
this.controller
|
|
80
|
-
|
|
81
|
-
else this.controller?.setHover(null);
|
|
82
|
-
this.onHoverChanged?.(null);
|
|
68
|
+
this.controller.setActiveHit(null);
|
|
69
|
+
this.controller.setHoverHit(null);
|
|
83
70
|
};
|
|
84
71
|
|
|
85
72
|
#onClick = (event) => {
|
|
@@ -88,43 +75,30 @@ export class TreeViewInputController {
|
|
|
88
75
|
if (!hit) return;
|
|
89
76
|
if (hit.area === 'header') {
|
|
90
77
|
if (hit.part === 'filter' && this.cellEditor?.handleHeaderClick(event, hit)) return;
|
|
91
|
-
if (!this.resizeDrag && hit.part === 'label' && hit.column) this.controller
|
|
78
|
+
if (!this.resizeDrag && hit.part === 'label' && hit.column) this.controller.sortBy(hit.column.id);
|
|
92
79
|
return;
|
|
93
80
|
}
|
|
94
81
|
if (hit.part === 'chevron' && hit.row.hasChildren) {
|
|
95
|
-
|
|
96
|
-
this.controller.toggle(hit.row.nodeId);
|
|
97
|
-
this.onRowsChanged?.();
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
this.expansion.toggle(hit.row.nodeId);
|
|
101
|
-
this.rowModel.rebuild();
|
|
102
|
-
this.viewport.setContentSize(this.rowModel.contentWidth, this.rowModel.contentHeight);
|
|
103
|
-
this.onRowsChanged?.();
|
|
82
|
+
this.controller.toggle(hit.row.nodeId);
|
|
104
83
|
return;
|
|
105
84
|
}
|
|
106
85
|
if (this.cellEditor?.handleClick(event, hit)) return;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
this.onSelectionChanged?.();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
this.#selectRow(hit.row.rowIndex, event);
|
|
86
|
+
this.controller.clickNode(hit.row.nodeId, {
|
|
87
|
+
shiftKey: event.shiftKey,
|
|
88
|
+
ctrlKey: event.ctrlKey,
|
|
89
|
+
metaKey: event.metaKey,
|
|
90
|
+
multi: this.canvas.dataset.multi === 'true',
|
|
91
|
+
});
|
|
118
92
|
};
|
|
119
93
|
|
|
120
94
|
#onDoubleClick = (event) => {
|
|
121
95
|
const hit = this.#hitTest(event);
|
|
122
|
-
if (hit?.area === 'row') this.controller
|
|
96
|
+
if (hit?.area === 'row') this.controller.doubleClickNode(hit.row.nodeId, event);
|
|
123
97
|
};
|
|
124
98
|
|
|
125
99
|
#onMouseDown = (event) => {
|
|
126
100
|
const hit = this.#hitTest(event);
|
|
127
|
-
this.controller
|
|
101
|
+
this.controller.setActiveHit(hit);
|
|
128
102
|
if (this.cellEditor?.handlePointerDown(event, hit)) {
|
|
129
103
|
event.preventDefault();
|
|
130
104
|
return;
|
|
@@ -143,103 +117,20 @@ export class TreeViewInputController {
|
|
|
143
117
|
#onMouseUp = () => {
|
|
144
118
|
this.resizeDrag = null;
|
|
145
119
|
this.canvas.style.cursor = '';
|
|
146
|
-
this.controller
|
|
120
|
+
this.controller.setActiveHit(null);
|
|
147
121
|
};
|
|
148
122
|
|
|
149
123
|
#onKeyDown = (event) => {
|
|
150
|
-
if (this.controller
|
|
124
|
+
if (this.controller.handleKey(event)) {
|
|
151
125
|
event.preventDefault();
|
|
152
|
-
this.onSelectionChanged?.();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (!this.rowModel.rows.length) return;
|
|
156
|
-
const currentRow = this.#focusedRowIndex();
|
|
157
|
-
let target = currentRow;
|
|
158
|
-
if (event.key === 'ArrowDown') target = Math.min(this.rowModel.rows.length - 1, currentRow + 1);
|
|
159
|
-
else if (event.key === 'ArrowUp') target = Math.max(0, currentRow - 1);
|
|
160
|
-
else if (event.key === 'Home') target = 0;
|
|
161
|
-
else if (event.key === 'End') target = this.rowModel.rows.length - 1;
|
|
162
|
-
else if (event.key === 'ArrowRight') {
|
|
163
|
-
const row = this.rowModel.getRow(currentRow);
|
|
164
|
-
if (row?.hasChildren && !row.expanded) this.#toggleAndRebuild(row.nodeId);
|
|
165
|
-
else if (row?.expanded) target = Math.min(this.rowModel.rows.length - 1, currentRow + 1);
|
|
166
|
-
} else if (event.key === 'ArrowLeft') {
|
|
167
|
-
const row = this.rowModel.getRow(currentRow);
|
|
168
|
-
if (row?.expanded) this.#toggleAndRebuild(row.nodeId);
|
|
169
|
-
else {
|
|
170
|
-
const node = row ? this.rowModel.model.index.getNode(row.nodeId) : null;
|
|
171
|
-
const parentRow = node?.parentId ? this.rowModel.getRowById(node.parentId) : null;
|
|
172
|
-
if (parentRow) target = parentRow.rowIndex;
|
|
173
|
-
}
|
|
174
|
-
} else if (event.key === 'Enter' || event.key === ' ') {
|
|
175
|
-
const row = this.rowModel.getRow(currentRow);
|
|
176
|
-
if (row) this.selection.toggle(row.nodeId);
|
|
177
|
-
this.onSelectionChanged?.();
|
|
178
|
-
event.preventDefault();
|
|
179
|
-
return;
|
|
180
|
-
} else return;
|
|
181
|
-
|
|
182
|
-
const row = this.rowModel.getRow(target);
|
|
183
|
-
if (row) {
|
|
184
|
-
this.selection.select(row.nodeId);
|
|
185
|
-
this.anchorRowIndex = target;
|
|
186
|
-
this.viewport.scrollRowIntoView(target);
|
|
187
|
-
this.onSelectionChanged?.();
|
|
188
126
|
}
|
|
189
|
-
event.preventDefault();
|
|
190
127
|
};
|
|
191
128
|
|
|
192
|
-
#selectRow(rowIndex, event) {
|
|
193
|
-
const row = this.rowModel.getRow(rowIndex);
|
|
194
|
-
if (!row) return;
|
|
195
|
-
if (event.shiftKey && this.anchorRowIndex !== null) {
|
|
196
|
-
this.selection.selected.clear();
|
|
197
|
-
const start = Math.min(this.anchorRowIndex, rowIndex);
|
|
198
|
-
const end = Math.max(this.anchorRowIndex, rowIndex);
|
|
199
|
-
for (let i = start; i <= end; i++) this.selection.selected.add(this.rowModel.rows[i].nodeId);
|
|
200
|
-
this.selection.focused = row.nodeId;
|
|
201
|
-
this.selection.dispatchEvent(new Event('change'));
|
|
202
|
-
} else if (event.ctrlKey || event.metaKey || this.canvas.dataset.multi === 'true') {
|
|
203
|
-
this.selection.toggle(row.nodeId);
|
|
204
|
-
this.anchorRowIndex = rowIndex;
|
|
205
|
-
} else {
|
|
206
|
-
this.selection.select(row.nodeId);
|
|
207
|
-
this.anchorRowIndex = rowIndex;
|
|
208
|
-
}
|
|
209
|
-
this.onSelectionChanged?.();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
#focusedRowIndex() {
|
|
213
|
-
if (this.selection.focused) {
|
|
214
|
-
const row = this.rowModel.getRowById(this.selection.focused);
|
|
215
|
-
if (row) return row.rowIndex;
|
|
216
|
-
}
|
|
217
|
-
return Math.max(0, Math.floor(this.viewport.scrollY / this.rowModel.rowHeight));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
#toggleAndRebuild(id) {
|
|
221
|
-
this.expansion.toggle(id);
|
|
222
|
-
this.rowModel.rebuild();
|
|
223
|
-
this.viewport.setContentSize(this.rowModel.contentWidth, this.rowModel.contentHeight);
|
|
224
|
-
this.onRowsChanged?.();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
129
|
#hitTest(event) {
|
|
228
130
|
const rect = this.canvas.getBoundingClientRect();
|
|
229
131
|
const clientX = event.clientX - rect.left;
|
|
230
132
|
const clientY = event.clientY - rect.top;
|
|
231
|
-
|
|
232
|
-
const x = clientX + this.viewport.scrollX;
|
|
233
|
-
const y = clientY - (this.viewport.headerHeight ?? 0) + this.viewport.scrollY;
|
|
234
|
-
if (clientY < (this.viewport.headerHeight ?? 0)) return { area: 'header', part: 'header', x, y: clientY };
|
|
235
|
-
const rowIndex = Math.floor(y / this.rowModel.rowHeight);
|
|
236
|
-
const row = this.rowModel.getRow(rowIndex);
|
|
237
|
-
if (!row) return null;
|
|
238
|
-
const rowX = row.depth * this.rowModel.indentWidth;
|
|
239
|
-
const chevronLeft = rowX + 4;
|
|
240
|
-
const chevronRight = chevronLeft + 18;
|
|
241
|
-
const part = x >= chevronLeft && x <= chevronRight ? 'chevron' : x <= rowX + 42 ? 'icon' : 'body';
|
|
242
|
-
return { area: 'row', row, x, y, part };
|
|
133
|
+
return this.controller.hitTest(clientX, clientY);
|
|
243
134
|
}
|
|
244
135
|
}
|
|
245
136
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export class CellEditorManager {
|
|
2
|
-
constructor({ controller,
|
|
2
|
+
constructor({ controller, host = null }) {
|
|
3
|
+
if (!controller) throw new TypeError('CellEditorManager requires a TreeViewController');
|
|
4
|
+
if (!controller.canvas) throw new Error('CellEditorManager requires an initialized TreeViewController canvas');
|
|
3
5
|
this.controller = controller;
|
|
4
|
-
this.canvas = canvas;
|
|
5
|
-
this.host = host ?? canvas.parentElement ?? document.body;
|
|
6
|
+
this.canvas = controller.canvas;
|
|
7
|
+
this.host = host ?? controller.canvas.parentElement ?? document.body;
|
|
6
8
|
this.overlay = null;
|
|
7
9
|
this.rangeDrag = null;
|
|
8
10
|
this.onMouseMove = this.#onMouseMove.bind(this);
|
|
@@ -139,7 +141,7 @@ export class CellEditorManager {
|
|
|
139
141
|
const rect = this.#clampRectToHost({
|
|
140
142
|
x: headerRect.x + 8,
|
|
141
143
|
y: headerRect.y + 5,
|
|
142
|
-
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),
|
|
143
145
|
height: Math.max(20, headerRect.height - 10),
|
|
144
146
|
}, 0);
|
|
145
147
|
const hostRect = this.host.getBoundingClientRect();
|
|
@@ -212,7 +214,7 @@ export class CellEditorManager {
|
|
|
212
214
|
#overlayRect(hit) {
|
|
213
215
|
if (hit.column?.kind !== 'inspectorPane') return this.controller.getCellClientRect(hit);
|
|
214
216
|
const rect = this.controller.getCellClientRect(hit);
|
|
215
|
-
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));
|
|
216
218
|
const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
|
|
217
219
|
const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
|
|
218
220
|
if (hit.part === 'number') {
|
|
@@ -225,7 +227,7 @@ export class CellEditorManager {
|
|
|
225
227
|
#rangeBarRect(hit) {
|
|
226
228
|
const rect = this.controller.getCellClientRect(hit);
|
|
227
229
|
if (hit.column?.kind === 'inspectorPane') {
|
|
228
|
-
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));
|
|
229
231
|
const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
|
|
230
232
|
const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
|
|
231
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;
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
TreeViewViewport,
|
|
13
13
|
VisibleRowModel,
|
|
14
14
|
} from './core/index.js';
|
|
15
|
+
import { CellEditorManager } from './inspector/cell-editor-manager.js';
|
|
15
16
|
import { formatInspectorValue, getAtPath, inspectorColumns, inspectorPaneColumns, ModelInspectorBuilder, setAtPath } from './inspector/index.js';
|
|
17
|
+
import { TreeViewInputController } from './input/tree-view-input-controller.js';
|
|
16
18
|
import { TreeRowRenderer } from './renderers/index.js';
|
|
17
19
|
|
|
18
20
|
export class TreeViewController {
|
|
@@ -58,12 +60,19 @@ export class TreeViewController {
|
|
|
58
60
|
this.workerRevision = 0;
|
|
59
61
|
this.sortValueSnapshot = null;
|
|
60
62
|
this.inspector = null;
|
|
63
|
+
this.inputController = null;
|
|
64
|
+
this.cellEditor = null;
|
|
61
65
|
this.scene = this.createRenderScene();
|
|
62
66
|
|
|
63
67
|
if (options.canvas) this.initialize(options.canvas);
|
|
68
|
+
if (options.editable !== false && (options.host || options.editable)) this.attachCellEditor({ host: options.host });
|
|
69
|
+
if (options.canvas && options.input !== false) this.attachInput();
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
initialize(canvas) {
|
|
73
|
+
if (!canvas || typeof canvas.getContext !== 'function') {
|
|
74
|
+
throw new TypeError('TreeViewController.initialize requires an HTMLCanvasElement');
|
|
75
|
+
}
|
|
67
76
|
this.canvas = canvas;
|
|
68
77
|
this.renderer.initialize(canvas);
|
|
69
78
|
this.renderer.setScene(this.scene);
|
|
@@ -73,6 +82,39 @@ export class TreeViewController {
|
|
|
73
82
|
return this;
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
attachCellEditor(options = {}) {
|
|
86
|
+
if (!this.canvas) throw new Error('TreeViewController.attachCellEditor requires an initialized canvas');
|
|
87
|
+
this.cellEditor?.destroy?.();
|
|
88
|
+
this.cellEditor = new CellEditorManager({
|
|
89
|
+
controller: this,
|
|
90
|
+
host: options.host,
|
|
91
|
+
});
|
|
92
|
+
return this.cellEditor;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
attachInput(options = {}) {
|
|
96
|
+
if (!this.canvas) throw new Error('TreeViewController.attachInput requires an initialized canvas');
|
|
97
|
+
this.inputController?.destroy?.();
|
|
98
|
+
this.inputController = new TreeViewInputController({
|
|
99
|
+
controller: this,
|
|
100
|
+
cellEditor: options.cellEditor ?? this.cellEditor,
|
|
101
|
+
});
|
|
102
|
+
return this.inputController;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
destroy() {
|
|
106
|
+
this.inputController?.destroy?.();
|
|
107
|
+
this.cellEditor?.destroy?.();
|
|
108
|
+
this.disableWorkers();
|
|
109
|
+
this.#resizeObserver?.disconnect();
|
|
110
|
+
this.#resizeObserver = null;
|
|
111
|
+
this.#nativeScroll?.destroy?.();
|
|
112
|
+
this.#nativeScroll = null;
|
|
113
|
+
this.inputController = null;
|
|
114
|
+
this.cellEditor = null;
|
|
115
|
+
this.canvas = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
76
118
|
on(type, listener) {
|
|
77
119
|
return this.events.on(type, listener);
|
|
78
120
|
}
|
|
@@ -287,6 +329,24 @@ export class TreeViewController {
|
|
|
287
329
|
this.events.emit('themechange', { theme: nextTheme });
|
|
288
330
|
}
|
|
289
331
|
|
|
332
|
+
setLayoutMetrics(options = {}) {
|
|
333
|
+
const rowHeight = options.rowHeight ?? this.rowModel.rowHeight;
|
|
334
|
+
const indentWidth = options.indentWidth ?? this.rowModel.indentWidth;
|
|
335
|
+
const headerHeight = options.headerHeight ?? this.viewport.headerHeight;
|
|
336
|
+
assertPositiveNumber(rowHeight, 'rowHeight');
|
|
337
|
+
assertPositiveNumber(indentWidth, 'indentWidth');
|
|
338
|
+
assertNonNegativeNumber(headerHeight, 'headerHeight');
|
|
339
|
+
const rowsChanged = this.rowModel.rowHeight !== rowHeight || this.rowModel.indentWidth !== indentWidth;
|
|
340
|
+
this.rowModel.rowHeight = rowHeight;
|
|
341
|
+
this.rowModel.indentWidth = indentWidth;
|
|
342
|
+
this.viewport.rowHeight = rowHeight;
|
|
343
|
+
this.viewport.indentWidth = indentWidth;
|
|
344
|
+
this.viewport.headerHeight = headerHeight;
|
|
345
|
+
if (rowsChanged) this.#rebuildRows();
|
|
346
|
+
else this.#syncContentSize();
|
|
347
|
+
this.events.emit('layoutchange', this.getViewportState());
|
|
348
|
+
}
|
|
349
|
+
|
|
290
350
|
registerIcon(name, imageOrUrl) {
|
|
291
351
|
return this.iconRegistry.register(name, imageOrUrl);
|
|
292
352
|
}
|
|
@@ -640,6 +700,7 @@ export class TreeViewController {
|
|
|
640
700
|
const localX = clientX - (this.viewport.renderInsetX ?? 0);
|
|
641
701
|
const localY = clientY - (this.viewport.renderInsetY ?? 0);
|
|
642
702
|
if (localX < 0 || localY < 0) return null;
|
|
703
|
+
if (localX >= this.viewport.contentViewportWidth) return null;
|
|
643
704
|
const x = localX + this.viewport.scrollX;
|
|
644
705
|
if (localY < this.viewport.headerHeight) {
|
|
645
706
|
const resizeColumn = this.columnModel.getResizeHandleAt(x);
|
|
@@ -651,6 +712,7 @@ export class TreeViewController {
|
|
|
651
712
|
return column ? { area: 'header', part: 'label', column, x, y: localY } : { area: 'header', part: 'header', column: null, x, y: localY };
|
|
652
713
|
}
|
|
653
714
|
|
|
715
|
+
if (localY >= this.viewport.headerHeight + this.viewport.rowViewportHeight) return null;
|
|
654
716
|
const rowY = localY - this.viewport.headerHeight + this.viewport.scrollY;
|
|
655
717
|
const rowIndex = Math.floor(rowY / this.rowModel.rowHeight);
|
|
656
718
|
const row = this.rowModel.getRow(rowIndex);
|
|
@@ -716,7 +778,7 @@ export class TreeViewController {
|
|
|
716
778
|
}
|
|
717
779
|
|
|
718
780
|
#visibleInspectorPaneWidth(column) {
|
|
719
|
-
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));
|
|
720
782
|
}
|
|
721
783
|
|
|
722
784
|
getInspectorPaneLayout(width, row = null, editorType = '') {
|
|
@@ -737,10 +799,11 @@ export class TreeViewController {
|
|
|
737
799
|
getHeaderClientRect(hit) {
|
|
738
800
|
if (!hit?.column || !this.canvas) return { x: 0, y: 0, width: 0, height: 0 };
|
|
739
801
|
const rect = this.canvas.getBoundingClientRect();
|
|
802
|
+
const visibleWidth = Math.max(0, this.viewport.contentViewportWidth - Math.max(0, hit.column.x - this.viewport.scrollX));
|
|
740
803
|
return {
|
|
741
804
|
x: rect.left + (this.viewport.renderInsetX ?? 0) + hit.column.x - this.viewport.scrollX,
|
|
742
805
|
y: rect.top + (this.viewport.renderInsetY ?? 0),
|
|
743
|
-
width: hit.column.width,
|
|
806
|
+
width: Math.min(hit.column.width, visibleWidth || hit.column.width),
|
|
744
807
|
height: this.viewport.headerHeight,
|
|
745
808
|
};
|
|
746
809
|
}
|
|
@@ -838,7 +901,9 @@ export class TreeViewController {
|
|
|
838
901
|
}
|
|
839
902
|
|
|
840
903
|
#syncContentSize() {
|
|
841
|
-
|
|
904
|
+
this.#syncScrollbarState(this.columnModel.contentWidth, this.rowModel.contentHeight);
|
|
905
|
+
if (this.viewport.viewportWidth > 1) this.#fitInspectorPaneColumn(this.viewport.contentViewportWidth);
|
|
906
|
+
this.#syncScrollbarState(this.columnModel.contentWidth, this.rowModel.contentHeight);
|
|
842
907
|
this.viewport.setContentSize(this.columnModel.contentWidth, this.rowModel.contentHeight);
|
|
843
908
|
}
|
|
844
909
|
|
|
@@ -913,12 +978,13 @@ export class TreeViewController {
|
|
|
913
978
|
applyNativeScrollbarTheme([vertical, horizontal, corner], this.themeManager.get());
|
|
914
979
|
|
|
915
980
|
const size = nativeScrollbarSize();
|
|
981
|
+
this.#nativeScrollbarSize = size;
|
|
916
982
|
Object.assign(vertical.style, {
|
|
917
983
|
position: 'absolute',
|
|
918
|
-
top:
|
|
919
|
-
|
|
920
|
-
bottom: '0',
|
|
984
|
+
top: '0',
|
|
985
|
+
left: '0',
|
|
921
986
|
width: `${size}px`,
|
|
987
|
+
height: '1px',
|
|
922
988
|
overflowX: 'hidden',
|
|
923
989
|
overflowY: 'auto',
|
|
924
990
|
zIndex: '4',
|
|
@@ -926,8 +992,8 @@ export class TreeViewController {
|
|
|
926
992
|
Object.assign(horizontal.style, {
|
|
927
993
|
position: 'absolute',
|
|
928
994
|
left: '0',
|
|
929
|
-
|
|
930
|
-
|
|
995
|
+
top: '0',
|
|
996
|
+
width: '1px',
|
|
931
997
|
height: `${size}px`,
|
|
932
998
|
overflowX: 'auto',
|
|
933
999
|
overflowY: 'hidden',
|
|
@@ -935,8 +1001,8 @@ export class TreeViewController {
|
|
|
935
1001
|
});
|
|
936
1002
|
Object.assign(corner.style, {
|
|
937
1003
|
position: 'absolute',
|
|
938
|
-
|
|
939
|
-
|
|
1004
|
+
left: '0',
|
|
1005
|
+
top: '0',
|
|
940
1006
|
width: `${size}px`,
|
|
941
1007
|
height: `${size}px`,
|
|
942
1008
|
zIndex: '4',
|
|
@@ -958,17 +1024,24 @@ export class TreeViewController {
|
|
|
958
1024
|
|
|
959
1025
|
let syncing = false;
|
|
960
1026
|
const sync = () => {
|
|
961
|
-
|
|
1027
|
+
this.#syncScrollbarState(this.viewport.contentWidth, this.viewport.contentHeight);
|
|
1028
|
+
const maxX = Math.max(0, this.viewport.contentWidth - this.viewport.contentViewportWidth);
|
|
962
1029
|
const maxY = Math.max(0, this.viewport.contentHeight - this.viewport.rowViewportHeight);
|
|
963
1030
|
const showX = maxX > 0;
|
|
964
1031
|
const showY = maxY > 0;
|
|
1032
|
+
const canvasMetrics = this.#canvasHostMetrics();
|
|
965
1033
|
|
|
966
1034
|
vertical.style.display = showY ? 'block' : 'none';
|
|
967
1035
|
horizontal.style.display = showX ? 'block' : 'none';
|
|
968
1036
|
corner.style.display = showX && showY ? 'block' : 'none';
|
|
969
|
-
vertical.style.
|
|
970
|
-
|
|
971
|
-
vertical.style.
|
|
1037
|
+
vertical.style.left = `${canvasMetrics.left + canvasMetrics.width - size}px`;
|
|
1038
|
+
vertical.style.top = `${canvasMetrics.top + this.viewport.headerHeight}px`;
|
|
1039
|
+
vertical.style.height = `${Math.max(1, canvasMetrics.height - this.viewport.headerHeight - (showX ? size : 0))}px`;
|
|
1040
|
+
horizontal.style.left = `${canvasMetrics.left}px`;
|
|
1041
|
+
horizontal.style.top = `${canvasMetrics.top + canvasMetrics.height - size}px`;
|
|
1042
|
+
horizontal.style.width = `${Math.max(1, canvasMetrics.width - (showY ? size : 0))}px`;
|
|
1043
|
+
corner.style.left = `${canvasMetrics.left + canvasMetrics.width - size}px`;
|
|
1044
|
+
corner.style.top = `${canvasMetrics.top + canvasMetrics.height - size}px`;
|
|
972
1045
|
|
|
973
1046
|
verticalSpacer.style.height = `${Math.ceil(maxY + vertical.clientHeight)}px`;
|
|
974
1047
|
horizontalSpacer.style.width = `${Math.ceil(maxX + horizontal.clientWidth)}px`;
|
|
@@ -1005,11 +1078,37 @@ export class TreeViewController {
|
|
|
1005
1078
|
vertical.remove();
|
|
1006
1079
|
horizontal.remove();
|
|
1007
1080
|
corner.remove();
|
|
1081
|
+
this.#nativeScrollbarSize = 0;
|
|
1082
|
+
this.viewport.setScrollbarState({ size: 0, vertical: false, horizontal: false });
|
|
1008
1083
|
if (hostPositionChanged) host.style.position = previousHostPosition;
|
|
1009
1084
|
},
|
|
1010
1085
|
};
|
|
1011
1086
|
}
|
|
1012
1087
|
|
|
1088
|
+
#syncScrollbarState(contentWidth, contentHeight) {
|
|
1089
|
+
const size = this.nativeScrollbars ? this.#nativeScrollbarSize : 0;
|
|
1090
|
+
if (!size) {
|
|
1091
|
+
this.viewport.setScrollbarState({ size: 0, vertical: false, horizontal: false });
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
let availableWidth = this.viewport.viewportWidth;
|
|
1095
|
+
let availableRowHeight = Math.max(1, this.viewport.viewportHeight - this.viewport.headerHeight);
|
|
1096
|
+
let showY = contentHeight > availableRowHeight;
|
|
1097
|
+
let showX = contentWidth > availableWidth;
|
|
1098
|
+
if (showY && contentWidth > Math.max(1, availableWidth - size)) showX = true;
|
|
1099
|
+
if (showX && contentHeight > Math.max(1, availableRowHeight - size)) showY = true;
|
|
1100
|
+
this.viewport.setScrollbarState({ size, vertical: showY, horizontal: showX });
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
#canvasHostMetrics() {
|
|
1104
|
+
if (!this.canvas) return { left: 0, top: 0, width: this.viewport.viewportWidth, height: this.viewport.viewportHeight };
|
|
1105
|
+
const left = Number.isFinite(this.canvas.offsetLeft) ? this.canvas.offsetLeft : 0;
|
|
1106
|
+
const top = Number.isFinite(this.canvas.offsetTop) ? this.canvas.offsetTop : 0;
|
|
1107
|
+
const width = Math.max(1, Math.floor(this.canvas.clientWidth || this.canvas.getBoundingClientRect?.().width || this.viewport.viewportWidth));
|
|
1108
|
+
const height = Math.max(1, Math.floor(this.canvas.clientHeight || this.canvas.getBoundingClientRect?.().height || this.viewport.viewportHeight));
|
|
1109
|
+
return { left, top, width, height };
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1013
1112
|
#computeInspectorPaneLabelEnd(visibleRange) {
|
|
1014
1113
|
const column = this.columnModel.columns.find((item) => item.kind === 'inspectorPane');
|
|
1015
1114
|
if (!column) return 0;
|
|
@@ -1073,6 +1172,7 @@ export class TreeViewController {
|
|
|
1073
1172
|
|
|
1074
1173
|
#resizeObserver = null;
|
|
1075
1174
|
#nativeScroll = null;
|
|
1175
|
+
#nativeScrollbarSize = 0;
|
|
1076
1176
|
}
|
|
1077
1177
|
|
|
1078
1178
|
const NATIVE_SCROLLBAR_STYLE_ID = 'virtual-tree-canvas-native-scrollbar-style';
|
|
@@ -1201,6 +1301,18 @@ function nativeScrollbarSize() {
|
|
|
1201
1301
|
return size;
|
|
1202
1302
|
}
|
|
1203
1303
|
|
|
1304
|
+
function assertPositiveNumber(value, name) {
|
|
1305
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
1306
|
+
throw new TypeError(`${name} must be a positive number`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function assertNonNegativeNumber(value, name) {
|
|
1311
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
1312
|
+
throw new TypeError(`${name} must be a non-negative number`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1204
1316
|
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
1205
1317
|
const safeWidth = Math.max(1, width);
|
|
1206
1318
|
const rightPadding = 14;
|