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/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,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, 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);
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') controller.setData(next.nodes ?? [], { iconResolver: next.iconResolver });
85
- 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
+ });
86
135
  } else {
87
- if (changed('iconResolver') && next.mode === 'tree') controller.setIconResolver(next.iconResolver);
88
- 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
+ });
89
142
  }
90
- if (changed('columns') || (replaceData && next.columns !== null)) controller.setColumns(next.columns);
91
- if (replaceData || ['filter', 'filterPlacement', 'headerHeight'].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();
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 = filter && (filterPlacement === 'bar' || (filterPlacement === 'auto' && (mode === 'tree' || presentation === 'pane')));
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({ headerHeight: bar && mode === 'inspector' && presentation === 'pane' ? 0 : headerHeight });
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 = 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;
108
173
  this.fontFamily = family;
109
- 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
+ );
110
183
  }
111
184
 
112
185
  refreshFont = () => {
113
186
  if (this.destroyed) return;
114
187
  this.flush();
115
- 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;
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({ 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);
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({ nodes, mode: 'tree' });
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({ model, meta, mode: 'inspector' });
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({ columns });
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) { this.configure({ theme }); this.flush(); }
148
- on(type, listener) { return this._controller.on(type, listener); }
149
- off(type, listener) { this._controller.off(type, listener); }
150
- setDynamicState(patches) { this.controller.setDynamicState(patches); }
151
- setInspectorValue(path, value, options) { return this.controller.setInspectorValue(path, value, options); }
152
- setFilter(query, options) { this.controller.setFilter(query, options); }
153
- clearFilter() { this.controller.clearFilter(); }
154
- getRowOrder(parentId) { return this.controller.getRowOrder(parentId); }
155
- moveRow(id, index, options) { return this.controller.moveRow(id, index, options); }
156
- moveRowBy(id, offset, options) { return this.controller.moveRowBy(id, offset, options); }
157
- getSelection() { return this.controller.getSelection(); }
158
- setSelection(ids) { this.controller.setSelection(ids); }
159
- clearSelection() { this.controller.clearSelection(); }
160
- search(query, options) { return this.controller.search(query, options); }
161
- clearSearch() { this.controller.clearSearch(); }
162
- getSearchState() { return this.controller.getSearchState(); }
163
- nextSearchResult() { return this.controller.nextSearchResult(); }
164
- previousSearchResult() { return this.controller.previousSearchResult(); }
165
- focusNode(id, options) { return this.controller.focusNode(id, options); }
166
- scrollToNode(id, align) { return this.controller.scrollToNode(id, align); }
167
- expandAll() { this.controller.expandAll(); }
168
- collapseAll() { this.controller.collapseAll(); }
169
- 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
+ }
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
  }
@@ -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 canvas { display:block; width:100%; height:100%; box-sizing:border-box; }
5
- .vtc-view canvas:focus-visible { outline:1px solid var(--vtc-focus); 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
  `;