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/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 { treeViewDefaults, treeViewOptionNames, validateTreeViewOptions } from './core/view-options.js';
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
- /** Mount a tree or inspector with a canvas, filter controls and editors. */
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 = { ...treeViewDefaults, nodes: [], model: {}, meta: {} };
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, host: this.canvasHost, autoRender: true, tooltip: true,
35
- iconsBaseUrl: options.iconsBaseUrl, iconRegistry: options.iconRegistry,
36
- nativeScrollbars: options.nativeScrollbars,
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() { this.flush(); return this._controller; }
63
+ get controller() {
64
+ this.flush();
65
+ return this._controller;
66
+ }
49
67
 
50
- /** Merge options; several changes in one task are applied once. */
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 = { ...(this.pending ?? this.options) };
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(() => { if (!this.destroyed) this.flush(); });
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, previous = this.options;
69
- const changed = key => !Object.is(next[key], previous[key]);
70
- const replaceData = this.refreshPending || changed('mode') ||
71
- (next.mode === 'tree' ? changed('nodes') : changed('model') || changed('meta') || changed('presentation') || changed('flatRoot') || changed('enforceMeta'));
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')) controller.setLayoutMetrics({ rowHeight: next.rowHeight, indentWidth: next.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') controller.setData(next.nodes ?? [], { iconResolver: next.iconResolver });
87
- else controller.setModel(next.model, next.meta ?? {}, { presentation: next.presentation, flatRoot: next.flatRoot, enforceMeta: next.enforceMeta, markUpdated: next.markUpdated });
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') controller.setIconResolver(next.iconResolver);
90
- if (changed('markUpdated')) controller.setInspectorOptions({ markUpdated: next.markUpdated });
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)) controller.setColumns(next.columns);
93
- if (replaceData || ['filter', 'filterPlacement', 'headerHeight', 'showHeader'].some(changed)) this.syncLayout();
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 = filter && (!showHeader || filterPlacement === 'bar' || (filterPlacement === 'auto' && (mode === 'tree' || presentation === 'pane')));
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({ headerHeight: !showHeader || (bar && mode === 'inspector' && presentation === 'pane') ? 0 : headerHeight });
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 = this.options.fontFamily === 'inherit' ? this.window.getComputedStyle(this.host).fontFamily : this.options.fontFamily;
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(family ? { ...theme, font: `12px ${family}`, monoFont: `12px ${family}` } : theme);
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 = this.options.fontFamily === 'inherit' ? this.window.getComputedStyle(this.host).fontFamily : this.options.fontFamily;
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({ background: theme.colors.background, text: theme.colors.text, border: theme.colors.border, focus: theme.colors.focus, selected: theme.colors.rowSelected })) this.element.style.setProperty(`--vtc-${key}`, value);
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({ nodes, mode: 'tree' });
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({ model, meta, mode: 'inspector' });
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({ columns });
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) { this.configure({ theme }); this.flush(); }
150
- on(type, listener) { return this._controller.on(type, listener); }
151
- off(type, listener) { this._controller.off(type, listener); }
152
- setDynamicState(patches) { this.controller.setDynamicState(patches); }
153
- setInspectorValue(path, value, options) { return this.controller.setInspectorValue(path, value, options); }
154
- setFilter(query, options) { this.controller.setFilter(query, options); }
155
- clearFilter() { this.controller.clearFilter(); }
156
- getRowOrder(parentId) { return this.controller.getRowOrder(parentId); }
157
- moveRow(id, index, options) { return this.controller.moveRow(id, index, options); }
158
- moveRowBy(id, offset, options) { return this.controller.moveRowBy(id, offset, options); }
159
- getSelection() { return this.controller.getSelection(); }
160
- setSelection(ids) { this.controller.setSelection(ids); }
161
- clearSelection() { this.controller.clearSelection(); }
162
- search(query, options) { return this.controller.search(query, options); }
163
- clearSearch() { this.controller.clearSearch(); }
164
- getSearchState() { return this.controller.getSearchState(); }
165
- nextSearchResult() { return this.controller.nextSearchResult(); }
166
- previousSearchResult() { return this.controller.previousSearchResult(); }
167
- focusNode(id, options) { return this.controller.focusNode(id, options); }
168
- scrollToNode(id, align) { return this.controller.scrollToNode(id, align); }
169
- expandAll() { this.controller.expandAll(); }
170
- collapseAll() { this.controller.collapseAll(); }
171
- registerIcon(name, source) { return this.controller.registerIcon(name, source); }
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
  }
@@ -1,14 +1,85 @@
1
1
  export const treeViewStyles = `
2
- .vtc-view { position:relative; display:flex; flex-direction:column; width:100%; height:100%; min-width:0; min-height:0; overflow:hidden; box-sizing:border-box; background:var(--vtc-background); color:var(--vtc-text); }
3
- .vtc-view .vtc-canvas-host { position:relative; flex:1 1 0; min-width:0; min-height:0; overflow:hidden; }
4
- .vtc-view .vtc-canvas-host > canvas { display:block; width:100%; height:100%; box-sizing:border-box; }
5
- .vtc-view .vtc-canvas-host > canvas:focus-visible { outline:1px solid var(--vtc-border); outline-offset:-1px; }
6
- .vtc-view .vtc-filter { display:flex; flex:0 0 30px; box-sizing:border-box; gap:4px; padding:4px 8px; min-width:0; }
7
- .vtc-view .vtc-filter[hidden] { display:none; }
8
- .vtc-view .vtc-filter input, .vtc-view .vtc-filter button { height:22px; box-sizing:border-box; border:1px solid var(--vtc-border); border-radius:4px; background:var(--vtc-background); color:var(--vtc-text); font:12px var(--vtc-font-family, ui-monospace, monospace); }
9
- .vtc-view .vtc-filter input { flex:1 1 auto; min-width:0; padding:0 8px; }
10
- .vtc-view .vtc-filter button { flex:0 0 24px; padding:0; cursor:pointer; }
11
- .vtc-view .vtc-filter button:hover { border-color:var(--vtc-focus); }
12
- .vtc-view .vtc-filter :focus-visible { outline:1px solid var(--vtc-focus); outline-offset:1px; }
13
- .vtc-view .vtc-filter [aria-pressed="true"] { border-color:var(--vtc-focus); background:var(--vtc-selected); }
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
  `;