virtual-tree-canvas 0.4.0 → 0.6.0

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +115 -28
  3. package/docs/icon-catalog.html +67 -0
  4. package/docs/icon-catalog.json +380 -0
  5. package/docs/icon-catalog.md +167 -0
  6. package/examples/generate-tree.js +86 -0
  7. package/examples/reorder-table.html +33 -0
  8. package/examples/units.html +43 -0
  9. package/package.json +7 -3
  10. package/resources/icons/bicycle.svg +1 -0
  11. package/resources/icons/bird.svg +1 -0
  12. package/resources/icons/box.svg +1 -0
  13. package/resources/icons/building.svg +1 -0
  14. package/resources/icons/calendar.svg +1 -0
  15. package/resources/icons/car.svg +1 -0
  16. package/resources/icons/cat.svg +1 -0
  17. package/resources/icons/child.svg +1 -0
  18. package/resources/icons/clock.svg +1 -0
  19. package/resources/icons/cloud.svg +1 -0
  20. package/resources/icons/data-bus.svg +1 -0
  21. package/resources/icons/database.svg +1 -0
  22. package/resources/icons/dog.svg +1 -0
  23. package/resources/icons/drone.svg +1 -0
  24. package/resources/icons/fish.svg +1 -0
  25. package/resources/icons/flower.svg +1 -0
  26. package/resources/icons/fog.svg +1 -0
  27. package/resources/icons/globe.svg +1 -0
  28. package/resources/icons/grip.svg +1 -0
  29. package/resources/icons/heart.svg +1 -0
  30. package/resources/icons/helicopter.svg +1 -0
  31. package/resources/icons/insect.svg +1 -0
  32. package/resources/icons/leaf.svg +1 -0
  33. package/resources/icons/link.svg +1 -0
  34. package/resources/icons/lock.svg +1 -0
  35. package/resources/icons/moon.svg +1 -0
  36. package/resources/icons/network.svg +1 -0
  37. package/resources/icons/people.svg +1 -0
  38. package/resources/icons/person.svg +1 -0
  39. package/resources/icons/pin.svg +1 -0
  40. package/resources/icons/radar.svg +1 -0
  41. package/resources/icons/rain.svg +1 -0
  42. package/resources/icons/seedling.svg +1 -0
  43. package/resources/icons/sensor.svg +1 -0
  44. package/resources/icons/server.svg +1 -0
  45. package/resources/icons/ship.svg +1 -0
  46. package/resources/icons/snow.svg +1 -0
  47. package/resources/icons/star.svg +1 -0
  48. package/resources/icons/storm.svg +1 -0
  49. package/resources/icons/sun.svg +1 -0
  50. package/resources/icons/thermometer.svg +1 -0
  51. package/resources/icons/tree.svg +1 -0
  52. package/resources/icons/wind.svg +1 -0
  53. package/src/assets/icons.js +73 -25
  54. package/src/core/icon-registry.js +6 -8
  55. package/src/core/theme-manager.js +15 -4
  56. package/src/core/tree-column-model.js +16 -3
  57. package/src/core/tree-model.js +20 -0
  58. package/src/core/view-options.js +30 -0
  59. package/src/index.d.ts +166 -5
  60. package/src/index.js +3 -0
  61. package/src/input/index.js +1 -0
  62. package/src/input/row-reorder-input.js +143 -0
  63. package/src/input/tree-filter-bar.js +55 -0
  64. package/src/input/tree-tooltip.js +55 -0
  65. package/src/input/tree-view-input-controller.js +5 -0
  66. package/src/inspector/cell-editor-manager.js +42 -22
  67. package/src/inspector/editor-resolver.js +6 -1
  68. package/src/inspector/numeric-layout.js +16 -0
  69. package/src/renderers/tree-row-renderer.js +79 -13
  70. package/src/tree-view-controller.js +261 -34
  71. package/src/tree-view.js +181 -0
  72. package/src/view/styles.js +14 -0
  73. package/src/assets/icons/air.svg +0 -1
  74. package/src/assets/icons/radar.svg +0 -1
  75. /package/{src/assets → resources}/icons/aircraft.svg +0 -0
  76. /package/{src/assets/icons/bus.svg → resources/icons/bus-vehicle.svg} +0 -0
  77. /package/{src/assets → resources}/icons/control.svg +0 -0
  78. /package/{src/assets → resources}/icons/damage.svg +0 -0
  79. /package/{src/assets → resources}/icons/error.svg +0 -0
  80. /package/{src/assets → resources}/icons/folder.svg +0 -0
  81. /package/{src/assets → resources}/icons/ground.svg +0 -0
  82. /package/{src/assets → resources}/icons/inspector-array.svg +0 -0
  83. /package/{src/assets → resources}/icons/inspector-object.svg +0 -0
  84. /package/{src/assets → resources}/icons/inspector-value.svg +0 -0
  85. /package/{src/assets → resources}/icons/munition.svg +0 -0
  86. /package/{src/assets → resources}/icons/placeholder.svg +0 -0
  87. /package/{src/assets → resources}/icons/point.svg +0 -0
  88. /package/{src/assets → resources}/icons/situation.svg +0 -0
  89. /package/{src/assets → resources}/icons/space.svg +0 -0
  90. /package/{src/assets → resources}/icons/subsurface.svg +0 -0
  91. /package/{src/assets → resources}/icons/surface.svg +0 -0
  92. /package/{src/assets → resources}/icons/task.svg +0 -0
  93. /package/{src/assets → resources}/icons/track.svg +0 -0
  94. /package/{src/assets → resources}/icons/warning.svg +0 -0
@@ -0,0 +1,181 @@
1
+ import { TreeViewController } from './tree-view-controller.js';
2
+ import { TreeFilterBar } from './input/tree-filter-bar.js';
3
+ import { treeViewDefaults, treeViewOptionNames, validateTreeViewOptions } from './core/view-options.js';
4
+ import { resolveTheme } from './core/theme-manager.js';
5
+ import { treeViewStyles } from './view/styles.js';
6
+
7
+ /** Mount a tree or inspector with a canvas, filter controls and editors. */
8
+ export class TreeView {
9
+ constructor(host, options = {}) {
10
+ if (!host?.ownerDocument) throw new TypeError('TreeView requires an HTMLElement host');
11
+ validateTreeViewOptions(options);
12
+ resolveTheme(options.theme ?? treeViewDefaults.theme);
13
+ this.host = host;
14
+ this.document = host.ownerDocument;
15
+ this.window = this.document.defaultView;
16
+ this.destroyed = false;
17
+ this.flushing = false;
18
+ this.scheduled = false;
19
+ this.pending = null;
20
+ this.refreshPending = true;
21
+ this.options = { ...treeViewDefaults, nodes: [], model: {}, meta: {} };
22
+ this.element = this.document.createElement('div');
23
+ this.element.className = 'vtc-view';
24
+ const style = this.document.createElement('style');
25
+ style.textContent = treeViewStyles;
26
+ this.canvasHost = this.document.createElement('div');
27
+ this.canvasHost.className = 'vtc-canvas-host';
28
+ this.canvas = this.document.createElement('canvas');
29
+ this.canvas.setAttribute('aria-label', 'Tree table');
30
+ this.canvasHost.append(this.canvas);
31
+ this.element.append(style, this.canvasHost);
32
+ host.append(this.element);
33
+ 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,
37
+ });
38
+ this.filterBar = new TreeFilterBar(this._controller, this.document);
39
+ this.element.insertBefore(this.filterBar.element, this.canvasHost);
40
+ this.stopTheme = this._controller.on('themechange', () => this.syncTheme());
41
+ this.configure(options);
42
+ this.flush();
43
+ this.syncTheme();
44
+ this.document.fonts?.addEventListener('loadingdone', this.refreshFont);
45
+ this.document.fonts?.ready.then(this.refreshFont);
46
+ }
47
+
48
+ get controller() { this.flush(); return this._controller; }
49
+
50
+ /** Merge options; several changes in one task are applied once. */
51
+ configure(patch) {
52
+ if (this.destroyed) throw new Error('TreeView has been destroyed');
53
+ validateTreeViewOptions(patch);
54
+ if (Object.hasOwn(patch, 'theme')) resolveTheme(patch.theme);
55
+ const next = { ...(this.pending ?? this.options) };
56
+ for (const key of treeViewOptionNames) if (Object.hasOwn(patch, key)) next[key] = patch[key];
57
+ this.pending = next;
58
+ if (!this.scheduled) {
59
+ this.scheduled = true;
60
+ queueMicrotask(() => { if (!this.destroyed) this.flush(); });
61
+ }
62
+ return this;
63
+ }
64
+
65
+ flush() {
66
+ if (this.destroyed || this.flushing || !this.pending) return;
67
+ 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'));
72
+ this.pending = null;
73
+ this.refreshPending = false;
74
+ this.options = next;
75
+ this.flushing = true;
76
+ const controller = this._controller;
77
+ try {
78
+ if (changed('initialExpandDepth')) controller.setInitialExpandDepth(next.initialExpandDepth);
79
+ if (changed('rowHeight') || changed('indentWidth')) controller.setLayoutMetrics({ rowHeight: next.rowHeight, indentWidth: next.indentWidth });
80
+ if (changed('theme') || changed('fontFamily')) this.applyTheme();
81
+ if (changed('editable')) controller.setEditable(next.editable);
82
+ if (changed('rowReorder')) controller.setRowReorder(next.rowReorder);
83
+ 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 });
86
+ } else {
87
+ if (changed('iconResolver') && next.mode === 'tree') controller.setIconResolver(next.iconResolver);
88
+ if (changed('markUpdated')) controller.setInspectorOptions({ markUpdated: next.markUpdated });
89
+ }
90
+ if (changed('columns') || (replaceData && next.columns !== null)) controller.setColumns(next.columns);
91
+ if (replaceData || ['filter', 'filterPlacement', 'headerHeight'].some(changed)) this.syncLayout();
92
+ } finally {
93
+ this.flushing = false;
94
+ }
95
+ }
96
+
97
+ syncLayout() {
98
+ const { filter, filterPlacement, headerHeight, mode, presentation } = this.options;
99
+ const bar = filter && (filterPlacement === 'bar' || (filterPlacement === 'auto' && (mode === 'tree' || presentation === 'pane')));
100
+ this.filterBar.setVisible(bar);
101
+ this._controller.setHeaderFilter(filter && !bar);
102
+ this._controller.setLayoutMetrics({ headerHeight: bar && mode === 'inspector' && presentation === 'pane' ? 0 : headerHeight });
103
+ }
104
+
105
+ applyTheme() {
106
+ const theme = resolveTheme(this.options.theme);
107
+ const family = this.options.fontFamily === 'inherit' ? this.window.getComputedStyle(this.host).fontFamily : this.options.fontFamily;
108
+ this.fontFamily = family;
109
+ this._controller.setTheme(family ? { ...theme, font: `12px ${family}`, monoFont: `12px ${family}` } : theme);
110
+ }
111
+
112
+ refreshFont = () => {
113
+ if (this.destroyed) return;
114
+ this.flush();
115
+ const family = this.options.fontFamily === 'inherit' ? this.window.getComputedStyle(this.host).fontFamily : this.options.fontFamily;
116
+ if (family !== this.fontFamily) this.applyTheme();
117
+ else this._controller.requestRender();
118
+ };
119
+
120
+ syncTheme() {
121
+ 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);
123
+ if (this.fontFamily) this.element.style.setProperty('--vtc-font-family', this.fontFamily);
124
+ else this.element.style.removeProperty('--vtc-font-family');
125
+ }
126
+
127
+ setData(nodes) {
128
+ if (!Array.isArray(nodes)) throw new TypeError('nodes must be an array');
129
+ this.configure({ nodes, mode: 'tree' });
130
+ this.refreshPending = true;
131
+ this.flush();
132
+ }
133
+
134
+ setModel(model, meta = (this.pending ?? this.options).meta) {
135
+ this.configure({ model, meta, mode: 'inspector' });
136
+ this.refreshPending = true;
137
+ this.flush();
138
+ }
139
+
140
+ setColumns(columns) {
141
+ const changed = columns !== (this.pending ?? this.options).columns;
142
+ this.configure({ columns });
143
+ this.flush();
144
+ if (!changed) this._controller.setColumns(columns);
145
+ }
146
+
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); }
170
+
171
+ destroy() {
172
+ if (this.destroyed) return;
173
+ this.destroyed = true;
174
+ this.pending = null;
175
+ this.document.fonts?.removeEventListener('loadingdone', this.refreshFont);
176
+ this.stopTheme();
177
+ this.filterBar.destroy();
178
+ this._controller.destroy();
179
+ this.element.remove();
180
+ }
181
+ }
@@ -0,0 +1,14 @@
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); }
14
+ `;
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.5l1.4 8.1 7.2 4.2-7.9-1.3L12 21.5l-.7-8-7.9 1.3 7.2-4.2z"/><path d="M9.8 18.8L6.5 21M14.2 18.8l3.3 2.2" fill="none"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 12l6.5-5M12 12a7 7 0 1 0 7 7"/><path d="M12 5a7 7 0 0 1 7 7"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/></svg>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes