virtual-tree-canvas 0.6.0 → 0.7.1
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 +32 -0
- package/README.md +90 -3
- package/docs/icon-catalog.html +68 -66
- package/docs/icon-catalog.json +12 -0
- package/docs/icon-catalog.md +111 -107
- package/docs/performance/dynamic-updates-0.7.1.md +38 -0
- package/package.json +3 -2
- package/resources/icons/star-filled.svg +1 -0
- package/resources/icons/trash.svg +1 -0
- package/src/assets/icons.js +12 -4
- package/src/core/dynamic-state.js +19 -29
- package/src/core/patch-batcher.js +4 -0
- package/src/core/theme-manager.js +238 -61
- package/src/core/tree-cell-layout.js +11 -0
- package/src/core/tree-column-model.js +81 -22
- package/src/core/tree-model.js +4 -2
- package/src/core/types.js +1 -0
- package/src/core/view-options.js +98 -18
- package/src/core/visible-row-model.js +51 -21
- package/src/index.d.ts +70 -1
- package/src/input/row-actions.js +248 -0
- package/src/input/row-reorder-input.js +134 -36
- package/src/input/tree-tooltip.js +39 -5
- package/src/input/tree-view-input-controller.js +35 -10
- package/src/inspector/cell-editor-manager.js +128 -35
- package/src/inspector/pane-layout.js +20 -0
- package/src/renderers/checkbox.js +13 -0
- package/src/renderers/tree-row-renderer.js +356 -107
- package/src/tree-view-controller.js +968 -236
- package/src/tree-view.js +218 -54
- 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,108 +89,247 @@ 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);
|
|
120
|
+
if (changed('rowActions')) controller.setRowActions(next.rowActions);
|
|
121
|
+
if (changed('rowDrag')) controller.setRowDrag(next.rowDrag);
|
|
82
122
|
if (changed('rowReorder')) controller.setRowReorder(next.rowReorder);
|
|
83
123
|
if (replaceData) {
|
|
84
|
-
if (next.mode === 'tree')
|
|
85
|
-
|
|
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
|
+
});
|
|
86
135
|
} else {
|
|
87
|
-
if (changed('iconResolver') && next.mode === 'tree')
|
|
88
|
-
|
|
136
|
+
if (changed('iconResolver') && next.mode === 'tree')
|
|
137
|
+
controller.setIconResolver(next.iconResolver);
|
|
138
|
+
if (changed('markUpdated'))
|
|
139
|
+
controller.setInspectorOptions({
|
|
140
|
+
markUpdated: next.markUpdated
|
|
141
|
+
});
|
|
89
142
|
}
|
|
90
|
-
if (changed('columns') || (replaceData && next.columns !== null))
|
|
91
|
-
|
|
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();
|
|
92
147
|
} finally {
|
|
93
148
|
this.flushing = false;
|
|
94
149
|
}
|
|
95
150
|
}
|
|
96
151
|
|
|
97
152
|
syncLayout() {
|
|
98
|
-
const { filter, filterPlacement, headerHeight, mode, presentation } = this.options;
|
|
99
|
-
const bar =
|
|
153
|
+
const { filter, filterPlacement, headerHeight, showHeader, mode, presentation } = this.options;
|
|
154
|
+
const bar =
|
|
155
|
+
filter &&
|
|
156
|
+
(!showHeader ||
|
|
157
|
+
filterPlacement === 'bar' ||
|
|
158
|
+
(filterPlacement === 'auto' && (mode === 'tree' || presentation === 'pane')));
|
|
100
159
|
this.filterBar.setVisible(bar);
|
|
101
160
|
this._controller.setHeaderFilter(filter && !bar);
|
|
102
|
-
this._controller.setLayoutMetrics({
|
|
161
|
+
this._controller.setLayoutMetrics({
|
|
162
|
+
headerHeight:
|
|
163
|
+
!showHeader || (bar && mode === 'inspector' && presentation === 'pane') ? 0 : headerHeight
|
|
164
|
+
});
|
|
103
165
|
}
|
|
104
166
|
|
|
105
167
|
applyTheme() {
|
|
106
168
|
const theme = resolveTheme(this.options.theme);
|
|
107
|
-
const family =
|
|
169
|
+
const family =
|
|
170
|
+
this.options.fontFamily === 'inherit'
|
|
171
|
+
? this.window.getComputedStyle(this.host).fontFamily
|
|
172
|
+
: this.options.fontFamily;
|
|
108
173
|
this.fontFamily = family;
|
|
109
|
-
this._controller.setTheme(
|
|
174
|
+
this._controller.setTheme(
|
|
175
|
+
family
|
|
176
|
+
? {
|
|
177
|
+
...theme,
|
|
178
|
+
font: `12px ${family}`,
|
|
179
|
+
monoFont: `12px ${family}`
|
|
180
|
+
}
|
|
181
|
+
: theme
|
|
182
|
+
);
|
|
110
183
|
}
|
|
111
184
|
|
|
112
185
|
refreshFont = () => {
|
|
113
186
|
if (this.destroyed) return;
|
|
114
187
|
this.flush();
|
|
115
|
-
const family =
|
|
188
|
+
const family =
|
|
189
|
+
this.options.fontFamily === 'inherit'
|
|
190
|
+
? this.window.getComputedStyle(this.host).fontFamily
|
|
191
|
+
: this.options.fontFamily;
|
|
116
192
|
if (family !== this.fontFamily) this.applyTheme();
|
|
117
193
|
else this._controller.requestRender();
|
|
118
194
|
};
|
|
119
195
|
|
|
120
196
|
syncTheme() {
|
|
121
197
|
const theme = this._controller.themeManager.get();
|
|
122
|
-
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);
|
|
123
206
|
if (this.fontFamily) this.element.style.setProperty('--vtc-font-family', this.fontFamily);
|
|
124
207
|
else this.element.style.removeProperty('--vtc-font-family');
|
|
125
208
|
}
|
|
126
209
|
|
|
127
210
|
setData(nodes) {
|
|
128
211
|
if (!Array.isArray(nodes)) throw new TypeError('nodes must be an array');
|
|
129
|
-
this.configure({
|
|
212
|
+
this.configure({
|
|
213
|
+
nodes,
|
|
214
|
+
mode: 'tree'
|
|
215
|
+
});
|
|
130
216
|
this.refreshPending = true;
|
|
131
217
|
this.flush();
|
|
132
218
|
}
|
|
133
219
|
|
|
134
220
|
setModel(model, meta = (this.pending ?? this.options).meta) {
|
|
135
|
-
this.configure({
|
|
221
|
+
this.configure({
|
|
222
|
+
model,
|
|
223
|
+
meta,
|
|
224
|
+
mode: 'inspector'
|
|
225
|
+
});
|
|
136
226
|
this.refreshPending = true;
|
|
137
227
|
this.flush();
|
|
138
228
|
}
|
|
139
229
|
|
|
140
230
|
setColumns(columns) {
|
|
141
231
|
const changed = columns !== (this.pending ?? this.options).columns;
|
|
142
|
-
this.configure({
|
|
232
|
+
this.configure({
|
|
233
|
+
columns
|
|
234
|
+
});
|
|
143
235
|
this.flush();
|
|
144
236
|
if (!changed) this._controller.setColumns(columns);
|
|
145
237
|
}
|
|
146
238
|
|
|
147
|
-
setTheme(theme) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
}
|
|
170
333
|
|
|
171
334
|
destroy() {
|
|
172
335
|
if (this.destroyed) return;
|
|
@@ -178,4 +341,5 @@ export class TreeView {
|
|
|
178
341
|
this._controller.destroy();
|
|
179
342
|
this.element.remove();
|
|
180
343
|
}
|
|
344
|
+
|
|
181
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
|
`;
|