virtual-tree-canvas 0.3.5 → 0.3.6
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
CHANGED
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);
|
|
@@ -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
|
}
|
|
@@ -1201,6 +1261,18 @@ function nativeScrollbarSize() {
|
|
|
1201
1261
|
return size;
|
|
1202
1262
|
}
|
|
1203
1263
|
|
|
1264
|
+
function assertPositiveNumber(value, name) {
|
|
1265
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
1266
|
+
throw new TypeError(`${name} must be a positive number`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function assertNonNegativeNumber(value, name) {
|
|
1271
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
1272
|
+
throw new TypeError(`${name} must be a non-negative number`);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1204
1276
|
function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
|
|
1205
1277
|
const safeWidth = Math.max(1, width);
|
|
1206
1278
|
const rightPadding = 14;
|