virtual-tree-canvas 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +12 -1
- package/docs/icon-catalog.html +68 -66
- package/docs/icon-catalog.md +111 -111
- package/docs/performance/dynamic-updates-0.7.1.md +38 -0
- package/package.json +3 -2
- package/src/assets/icons.js +10 -4
- package/src/core/dynamic-state.js +19 -29
- package/src/core/patch-batcher.js +4 -0
- package/src/core/theme-manager.js +238 -64
- package/src/core/tree-cell-layout.js +4 -2
- package/src/core/tree-column-model.js +72 -24
- package/src/core/tree-model.js +4 -2
- package/src/core/view-options.js +98 -25
- package/src/core/visible-row-model.js +50 -20
- package/src/index.d.ts +22 -0
- package/src/input/row-actions.js +224 -83
- package/src/input/row-reorder-input.js +112 -39
- package/src/input/tree-tooltip.js +39 -5
- package/src/input/tree-view-input-controller.js +33 -10
- package/src/inspector/cell-editor-manager.js +110 -31
- package/src/inspector/pane-layout.js +11 -3
- package/src/renderers/checkbox.js +12 -13
- package/src/renderers/tree-row-renderer.js +325 -68
- package/src/tree-view-controller.js +919 -213
- package/src/tree-view.js +215 -53
- package/src/view/styles.js +83 -12
package/src/tree-view.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { TreeViewController } from './tree-view-controller.js';
|
|
2
2
|
import { TreeFilterBar } from './input/tree-filter-bar.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
treeViewDefaults,
|
|
5
|
+
treeViewOptionNames,
|
|
6
|
+
validateTreeViewOptions
|
|
7
|
+
} from './core/view-options.js';
|
|
4
8
|
import { resolveTheme } from './core/theme-manager.js';
|
|
5
9
|
import { treeViewStyles } from './view/styles.js';
|
|
6
10
|
|
|
7
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Mount a tree or inspector with a canvas, filter controls and editors.
|
|
13
|
+
*/
|
|
8
14
|
export class TreeView {
|
|
9
15
|
constructor(host, options = {}) {
|
|
10
16
|
if (!host?.ownerDocument) throw new TypeError('TreeView requires an HTMLElement host');
|
|
@@ -18,7 +24,12 @@ export class TreeView {
|
|
|
18
24
|
this.scheduled = false;
|
|
19
25
|
this.pending = null;
|
|
20
26
|
this.refreshPending = true;
|
|
21
|
-
this.options = {
|
|
27
|
+
this.options = {
|
|
28
|
+
...treeViewDefaults,
|
|
29
|
+
nodes: [],
|
|
30
|
+
model: {},
|
|
31
|
+
meta: {}
|
|
32
|
+
};
|
|
22
33
|
this.element = this.document.createElement('div');
|
|
23
34
|
this.element.className = 'vtc-view';
|
|
24
35
|
const style = this.document.createElement('style');
|
|
@@ -31,9 +42,13 @@ export class TreeView {
|
|
|
31
42
|
this.element.append(style, this.canvasHost);
|
|
32
43
|
host.append(this.element);
|
|
33
44
|
this._controller = new TreeViewController({
|
|
34
|
-
canvas: this.canvas,
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
canvas: this.canvas,
|
|
46
|
+
host: this.canvasHost,
|
|
47
|
+
autoRender: true,
|
|
48
|
+
tooltip: true,
|
|
49
|
+
iconsBaseUrl: options.iconsBaseUrl,
|
|
50
|
+
iconRegistry: options.iconRegistry,
|
|
51
|
+
nativeScrollbars: options.nativeScrollbars
|
|
37
52
|
});
|
|
38
53
|
this.filterBar = new TreeFilterBar(this._controller, this.document);
|
|
39
54
|
this.element.insertBefore(this.filterBar.element, this.canvasHost);
|
|
@@ -45,19 +60,28 @@ export class TreeView {
|
|
|
45
60
|
this.document.fonts?.ready.then(this.refreshFont);
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
get controller() {
|
|
63
|
+
get controller() {
|
|
64
|
+
this.flush();
|
|
65
|
+
return this._controller;
|
|
66
|
+
}
|
|
49
67
|
|
|
50
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Merge options; several changes in one task are applied once.
|
|
70
|
+
*/
|
|
51
71
|
configure(patch) {
|
|
52
72
|
if (this.destroyed) throw new Error('TreeView has been destroyed');
|
|
53
73
|
validateTreeViewOptions(patch);
|
|
54
74
|
if (Object.hasOwn(patch, 'theme')) resolveTheme(patch.theme);
|
|
55
|
-
const next = {
|
|
75
|
+
const next = {
|
|
76
|
+
...(this.pending ?? this.options)
|
|
77
|
+
};
|
|
56
78
|
for (const key of treeViewOptionNames) if (Object.hasOwn(patch, key)) next[key] = patch[key];
|
|
57
79
|
this.pending = next;
|
|
58
80
|
if (!this.scheduled) {
|
|
59
81
|
this.scheduled = true;
|
|
60
|
-
queueMicrotask(() => {
|
|
82
|
+
queueMicrotask(() => {
|
|
83
|
+
if (!this.destroyed) this.flush();
|
|
84
|
+
});
|
|
61
85
|
}
|
|
62
86
|
return this;
|
|
63
87
|
}
|
|
@@ -65,32 +89,61 @@ export class TreeView {
|
|
|
65
89
|
flush() {
|
|
66
90
|
if (this.destroyed || this.flushing || !this.pending) return;
|
|
67
91
|
this.scheduled = false;
|
|
68
|
-
const next = this.pending,
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
92
|
+
const next = this.pending,
|
|
93
|
+
previous = this.options;
|
|
94
|
+
const changed = (key) => !Object.is(next[key], previous[key]);
|
|
95
|
+
const replaceData =
|
|
96
|
+
this.refreshPending ||
|
|
97
|
+
changed('mode') ||
|
|
98
|
+
(next.mode === 'tree'
|
|
99
|
+
? changed('nodes')
|
|
100
|
+
: changed('model') ||
|
|
101
|
+
changed('meta') ||
|
|
102
|
+
changed('presentation') ||
|
|
103
|
+
changed('flatRoot') ||
|
|
104
|
+
changed('enforceMeta'));
|
|
72
105
|
this.pending = null;
|
|
73
106
|
this.refreshPending = false;
|
|
74
107
|
this.options = next;
|
|
75
108
|
this.flushing = true;
|
|
76
109
|
const controller = this._controller;
|
|
77
110
|
try {
|
|
111
|
+
controller.flushDynamicState();
|
|
78
112
|
if (changed('initialExpandDepth')) controller.setInitialExpandDepth(next.initialExpandDepth);
|
|
79
|
-
if (changed('rowHeight') || changed('indentWidth'))
|
|
113
|
+
if (changed('rowHeight') || changed('indentWidth'))
|
|
114
|
+
controller.setLayoutMetrics({
|
|
115
|
+
rowHeight: next.rowHeight,
|
|
116
|
+
indentWidth: next.indentWidth
|
|
117
|
+
});
|
|
80
118
|
if (changed('theme') || changed('fontFamily')) this.applyTheme();
|
|
81
119
|
if (changed('editable')) controller.setEditable(next.editable);
|
|
82
120
|
if (changed('rowActions')) controller.setRowActions(next.rowActions);
|
|
83
121
|
if (changed('rowDrag')) controller.setRowDrag(next.rowDrag);
|
|
84
122
|
if (changed('rowReorder')) controller.setRowReorder(next.rowReorder);
|
|
85
123
|
if (replaceData) {
|
|
86
|
-
if (next.mode === 'tree')
|
|
87
|
-
|
|
124
|
+
if (next.mode === 'tree')
|
|
125
|
+
controller.setData(next.nodes ?? [], {
|
|
126
|
+
iconResolver: next.iconResolver
|
|
127
|
+
});
|
|
128
|
+
else
|
|
129
|
+
controller.setModel(next.model, next.meta ?? {}, {
|
|
130
|
+
presentation: next.presentation,
|
|
131
|
+
flatRoot: next.flatRoot,
|
|
132
|
+
enforceMeta: next.enforceMeta,
|
|
133
|
+
markUpdated: next.markUpdated
|
|
134
|
+
});
|
|
88
135
|
} else {
|
|
89
|
-
if (changed('iconResolver') && next.mode === 'tree')
|
|
90
|
-
|
|
136
|
+
if (changed('iconResolver') && next.mode === 'tree')
|
|
137
|
+
controller.setIconResolver(next.iconResolver);
|
|
138
|
+
if (changed('markUpdated'))
|
|
139
|
+
controller.setInspectorOptions({
|
|
140
|
+
markUpdated: next.markUpdated
|
|
141
|
+
});
|
|
91
142
|
}
|
|
92
|
-
if (changed('columns') || (replaceData && next.columns !== null))
|
|
93
|
-
|
|
143
|
+
if (changed('columns') || (replaceData && next.columns !== null))
|
|
144
|
+
controller.setColumns(next.columns);
|
|
145
|
+
if (replaceData || ['filter', 'filterPlacement', 'headerHeight', 'showHeader'].some(changed))
|
|
146
|
+
this.syncLayout();
|
|
94
147
|
} finally {
|
|
95
148
|
this.flushing = false;
|
|
96
149
|
}
|
|
@@ -98,77 +151,185 @@ export class TreeView {
|
|
|
98
151
|
|
|
99
152
|
syncLayout() {
|
|
100
153
|
const { filter, filterPlacement, headerHeight, showHeader, mode, presentation } = this.options;
|
|
101
|
-
const bar =
|
|
154
|
+
const bar =
|
|
155
|
+
filter &&
|
|
156
|
+
(!showHeader ||
|
|
157
|
+
filterPlacement === 'bar' ||
|
|
158
|
+
(filterPlacement === 'auto' && (mode === 'tree' || presentation === 'pane')));
|
|
102
159
|
this.filterBar.setVisible(bar);
|
|
103
160
|
this._controller.setHeaderFilter(filter && !bar);
|
|
104
|
-
this._controller.setLayoutMetrics({
|
|
161
|
+
this._controller.setLayoutMetrics({
|
|
162
|
+
headerHeight:
|
|
163
|
+
!showHeader || (bar && mode === 'inspector' && presentation === 'pane') ? 0 : headerHeight
|
|
164
|
+
});
|
|
105
165
|
}
|
|
106
166
|
|
|
107
167
|
applyTheme() {
|
|
108
168
|
const theme = resolveTheme(this.options.theme);
|
|
109
|
-
const family =
|
|
169
|
+
const family =
|
|
170
|
+
this.options.fontFamily === 'inherit'
|
|
171
|
+
? this.window.getComputedStyle(this.host).fontFamily
|
|
172
|
+
: this.options.fontFamily;
|
|
110
173
|
this.fontFamily = family;
|
|
111
|
-
this._controller.setTheme(
|
|
174
|
+
this._controller.setTheme(
|
|
175
|
+
family
|
|
176
|
+
? {
|
|
177
|
+
...theme,
|
|
178
|
+
font: `12px ${family}`,
|
|
179
|
+
monoFont: `12px ${family}`
|
|
180
|
+
}
|
|
181
|
+
: theme
|
|
182
|
+
);
|
|
112
183
|
}
|
|
113
184
|
|
|
114
185
|
refreshFont = () => {
|
|
115
186
|
if (this.destroyed) return;
|
|
116
187
|
this.flush();
|
|
117
|
-
const family =
|
|
188
|
+
const family =
|
|
189
|
+
this.options.fontFamily === 'inherit'
|
|
190
|
+
? this.window.getComputedStyle(this.host).fontFamily
|
|
191
|
+
: this.options.fontFamily;
|
|
118
192
|
if (family !== this.fontFamily) this.applyTheme();
|
|
119
193
|
else this._controller.requestRender();
|
|
120
194
|
};
|
|
121
195
|
|
|
122
196
|
syncTheme() {
|
|
123
197
|
const theme = this._controller.themeManager.get();
|
|
124
|
-
for (const [key, value] of Object.entries({
|
|
198
|
+
for (const [key, value] of Object.entries({
|
|
199
|
+
background: theme.colors.background,
|
|
200
|
+
text: theme.colors.text,
|
|
201
|
+
border: theme.colors.border,
|
|
202
|
+
focus: theme.colors.focus,
|
|
203
|
+
selected: theme.colors.rowSelected
|
|
204
|
+
}))
|
|
205
|
+
this.element.style.setProperty(`--vtc-${key}`, value);
|
|
125
206
|
if (this.fontFamily) this.element.style.setProperty('--vtc-font-family', this.fontFamily);
|
|
126
207
|
else this.element.style.removeProperty('--vtc-font-family');
|
|
127
208
|
}
|
|
128
209
|
|
|
129
210
|
setData(nodes) {
|
|
130
211
|
if (!Array.isArray(nodes)) throw new TypeError('nodes must be an array');
|
|
131
|
-
this.configure({
|
|
212
|
+
this.configure({
|
|
213
|
+
nodes,
|
|
214
|
+
mode: 'tree'
|
|
215
|
+
});
|
|
132
216
|
this.refreshPending = true;
|
|
133
217
|
this.flush();
|
|
134
218
|
}
|
|
135
219
|
|
|
136
220
|
setModel(model, meta = (this.pending ?? this.options).meta) {
|
|
137
|
-
this.configure({
|
|
221
|
+
this.configure({
|
|
222
|
+
model,
|
|
223
|
+
meta,
|
|
224
|
+
mode: 'inspector'
|
|
225
|
+
});
|
|
138
226
|
this.refreshPending = true;
|
|
139
227
|
this.flush();
|
|
140
228
|
}
|
|
141
229
|
|
|
142
230
|
setColumns(columns) {
|
|
143
231
|
const changed = columns !== (this.pending ?? this.options).columns;
|
|
144
|
-
this.configure({
|
|
232
|
+
this.configure({
|
|
233
|
+
columns
|
|
234
|
+
});
|
|
145
235
|
this.flush();
|
|
146
236
|
if (!changed) this._controller.setColumns(columns);
|
|
147
237
|
}
|
|
148
238
|
|
|
149
|
-
setTheme(theme) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
239
|
+
setTheme(theme) {
|
|
240
|
+
this.configure({
|
|
241
|
+
theme
|
|
242
|
+
});
|
|
243
|
+
this.flush();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
on(type, listener) {
|
|
247
|
+
return this._controller.on(type, listener);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
off(type, listener) {
|
|
251
|
+
this._controller.off(type, listener);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setDynamicState(patches) {
|
|
255
|
+
this.controller.setDynamicState(patches);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setInspectorValue(path, value, options) {
|
|
259
|
+
return this.controller.setInspectorValue(path, value, options);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
setFilter(query, options) {
|
|
263
|
+
this.controller.setFilter(query, options);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
clearFilter() {
|
|
267
|
+
this.controller.clearFilter();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
getRowOrder(parentId) {
|
|
271
|
+
return this.controller.getRowOrder(parentId);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
moveRow(id, index, options) {
|
|
275
|
+
return this.controller.moveRow(id, index, options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
moveRowBy(id, offset, options) {
|
|
279
|
+
return this.controller.moveRowBy(id, offset, options);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
getSelection() {
|
|
283
|
+
return this.controller.getSelection();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
setSelection(ids) {
|
|
287
|
+
this.controller.setSelection(ids);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
clearSelection() {
|
|
291
|
+
this.controller.clearSelection();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
search(query, options) {
|
|
295
|
+
return this.controller.search(query, options);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
clearSearch() {
|
|
299
|
+
this.controller.clearSearch();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
getSearchState() {
|
|
303
|
+
return this.controller.getSearchState();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
nextSearchResult() {
|
|
307
|
+
return this.controller.nextSearchResult();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
previousSearchResult() {
|
|
311
|
+
return this.controller.previousSearchResult();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
focusNode(id, options) {
|
|
315
|
+
return this.controller.focusNode(id, options);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
scrollToNode(id, align) {
|
|
319
|
+
return this.controller.scrollToNode(id, align);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
expandAll() {
|
|
323
|
+
this.controller.expandAll();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
collapseAll() {
|
|
327
|
+
this.controller.collapseAll();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
registerIcon(name, source) {
|
|
331
|
+
return this.controller.registerIcon(name, source);
|
|
332
|
+
}
|
|
172
333
|
|
|
173
334
|
destroy() {
|
|
174
335
|
if (this.destroyed) return;
|
|
@@ -180,4 +341,5 @@ export class TreeView {
|
|
|
180
341
|
this._controller.destroy();
|
|
181
342
|
this.element.remove();
|
|
182
343
|
}
|
|
344
|
+
|
|
183
345
|
}
|
package/src/view/styles.js
CHANGED
|
@@ -1,14 +1,85 @@
|
|
|
1
1
|
export const treeViewStyles = `
|
|
2
|
-
.vtc-view {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
.vtc-view {
|
|
3
|
+
position: relative;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
width: 100%;
|
|
7
|
+
height: 100%;
|
|
8
|
+
min-width: 0;
|
|
9
|
+
min-height: 0;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
background: var(--vtc-background);
|
|
13
|
+
color: var(--vtc-text);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.vtc-view .vtc-canvas-host {
|
|
17
|
+
position: relative;
|
|
18
|
+
flex: 1 1 0;
|
|
19
|
+
min-width: 0;
|
|
20
|
+
min-height: 0;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.vtc-view .vtc-canvas-host > canvas {
|
|
25
|
+
display: block;
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 100%;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.vtc-view .vtc-canvas-host > canvas:focus-visible {
|
|
32
|
+
outline: 1px solid var(--vtc-border);
|
|
33
|
+
outline-offset: -1px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.vtc-view .vtc-filter {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex: 0 0 30px;
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
gap: 4px;
|
|
41
|
+
padding: 4px 8px;
|
|
42
|
+
min-width: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.vtc-view .vtc-filter[hidden] {
|
|
46
|
+
display: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.vtc-view .vtc-filter input,
|
|
50
|
+
.vtc-view .vtc-filter button {
|
|
51
|
+
height: 22px;
|
|
52
|
+
box-sizing: border-box;
|
|
53
|
+
border: 1px solid var(--vtc-border);
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
background: var(--vtc-background);
|
|
56
|
+
color: var(--vtc-text);
|
|
57
|
+
font: 12px var(--vtc-font-family, ui-monospace, monospace);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.vtc-view .vtc-filter input {
|
|
61
|
+
flex: 1 1 auto;
|
|
62
|
+
min-width: 0;
|
|
63
|
+
padding: 0 8px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.vtc-view .vtc-filter button {
|
|
67
|
+
flex: 0 0 24px;
|
|
68
|
+
padding: 0;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.vtc-view .vtc-filter button:hover {
|
|
73
|
+
border-color: var(--vtc-focus);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.vtc-view .vtc-filter :focus-visible {
|
|
77
|
+
outline: 1px solid var(--vtc-focus);
|
|
78
|
+
outline-offset: 1px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.vtc-view .vtc-filter [aria-pressed='true'] {
|
|
82
|
+
border-color: var(--vtc-focus);
|
|
83
|
+
background: var(--vtc-selected);
|
|
84
|
+
}
|
|
14
85
|
`;
|