virtual-tree-canvas 0.1.0 → 0.3.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.
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "virtual-tree-canvas",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "High-performance Canvas2D virtual tree/table widget for very large hierarchical datasets.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
7
8
  "sideEffects": false,
8
9
  "exports": {
9
- ".": "./src/index.js",
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "default": "./src/index.js"
13
+ },
10
14
  "./core": "./src/core/index.js",
11
15
  "./inspector": "./src/inspector/index.js",
12
16
  "./renderers": "./src/renderers/index.js",
@@ -74,6 +74,16 @@ export class IconRegistry {
74
74
  this.register('error', drawError);
75
75
  this.register('task', drawTask);
76
76
  this.register('track', drawTrack);
77
+ this.register('point', drawPoint);
78
+ this.register('munition', drawMunition);
79
+ this.register('air', drawAircraft);
80
+ this.register('ground', drawGroundVehicle);
81
+ this.register('surface', drawSurfaceVehicle);
82
+ this.register('subsurface', drawSubsurfaceVehicle);
83
+ this.register('space', drawSpaceVehicle);
84
+ this.register('control', drawControl);
85
+ this.register('situation', drawSituation);
86
+ this.register('damage', drawDamage);
77
87
  }
78
88
  }
79
89
 
@@ -171,3 +181,242 @@ function drawTrack(ctx, x, y, size, color) {
171
181
  ctx.lineTo(x + size - 1, y + size / 2);
172
182
  ctx.stroke();
173
183
  }
184
+
185
+ function drawPoint(ctx, x, y, size, color) {
186
+ const cx = x + size / 2;
187
+ const cy = y + size / 2;
188
+ ctx.strokeStyle = color;
189
+ ctx.fillStyle = color;
190
+ ctx.lineWidth = 1.4;
191
+ ctx.beginPath();
192
+ ctx.arc(cx, cy, size * 0.22, 0, Math.PI * 2);
193
+ ctx.fill();
194
+ ctx.beginPath();
195
+ ctx.arc(cx, cy, size * 0.42, 0, Math.PI * 2);
196
+ ctx.stroke();
197
+ ctx.beginPath();
198
+ ctx.moveTo(cx, y + 1);
199
+ ctx.lineTo(cx, y + size * 0.22);
200
+ ctx.moveTo(cx, y + size * 0.78);
201
+ ctx.lineTo(cx, y + size - 1);
202
+ ctx.moveTo(x + 1, cy);
203
+ ctx.lineTo(x + size * 0.22, cy);
204
+ ctx.moveTo(x + size * 0.78, cy);
205
+ ctx.lineTo(x + size - 1, cy);
206
+ ctx.stroke();
207
+ }
208
+
209
+ function drawMunition(ctx, x, y, size, color) {
210
+ const cx = x + size * 0.5;
211
+ const bodyTop = y + size * 0.25;
212
+ const bodyBottom = y + size * 0.76;
213
+ ctx.fillStyle = color;
214
+
215
+ ctx.beginPath();
216
+ ctx.moveTo(cx, y + 1);
217
+ ctx.lineTo(cx + size * 0.17, bodyTop);
218
+ ctx.lineTo(cx - size * 0.17, bodyTop);
219
+ ctx.closePath();
220
+ ctx.fill();
221
+
222
+ ctx.fillRect(cx - size * 0.11, bodyTop, size * 0.22, bodyBottom - bodyTop);
223
+
224
+ ctx.beginPath();
225
+ ctx.moveTo(cx - size * 0.11, y + size * 0.63);
226
+ ctx.lineTo(x + 1, y + size - 2);
227
+ ctx.lineTo(cx - size * 0.11, y + size * 0.82);
228
+ ctx.closePath();
229
+ ctx.fill();
230
+
231
+ ctx.beginPath();
232
+ ctx.moveTo(cx + size * 0.11, y + size * 0.63);
233
+ ctx.lineTo(x + size - 1, y + size - 2);
234
+ ctx.lineTo(cx + size * 0.11, y + size * 0.82);
235
+ ctx.closePath();
236
+ ctx.fill();
237
+
238
+ ctx.strokeStyle = 'rgba(255,255,255,.35)';
239
+ ctx.lineWidth = 1;
240
+ ctx.beginPath();
241
+ ctx.moveTo(cx, bodyTop + 1);
242
+ ctx.lineTo(cx, bodyBottom - 1);
243
+ ctx.stroke();
244
+
245
+ ctx.strokeStyle = color;
246
+ ctx.beginPath();
247
+ ctx.moveTo(cx - size * 0.12, bodyBottom);
248
+ ctx.lineTo(cx, y + size - 1);
249
+ ctx.lineTo(cx + size * 0.12, bodyBottom);
250
+ ctx.stroke();
251
+ }
252
+
253
+ function drawGroundVehicle(ctx, x, y, size, color) {
254
+ ctx.fillStyle = color;
255
+ const bodyY = y + size * 0.42;
256
+ ctx.fillRect(x + size * 0.16, bodyY, size * 0.68, size * 0.26);
257
+ ctx.fillRect(x + size * 0.34, y + size * 0.26, size * 0.28, size * 0.2);
258
+ ctx.fillRect(x + size * 0.62, y + size * 0.34, size * 0.3, size * 0.06);
259
+ ctx.beginPath();
260
+ ctx.arc(x + size * 0.28, y + size * 0.76, size * 0.09, 0, Math.PI * 2);
261
+ ctx.arc(x + size * 0.5, y + size * 0.76, size * 0.09, 0, Math.PI * 2);
262
+ ctx.arc(x + size * 0.72, y + size * 0.76, size * 0.09, 0, Math.PI * 2);
263
+ ctx.fill();
264
+ }
265
+
266
+ function drawSurfaceVehicle(ctx, x, y, size, color) {
267
+ ctx.fillStyle = color;
268
+ ctx.beginPath();
269
+ ctx.moveTo(x + 1, y + size * 0.58);
270
+ ctx.lineTo(x + size * 0.82, y + size * 0.58);
271
+ ctx.lineTo(x + size - 1, y + size * 0.72);
272
+ ctx.lineTo(x + size * 0.18, y + size * 0.82);
273
+ ctx.closePath();
274
+ ctx.fill();
275
+
276
+ ctx.fillRect(x + size * 0.34, y + size * 0.34, size * 0.24, size * 0.2);
277
+ ctx.fillRect(x + size * 0.46, y + size * 0.18, size * 0.08, size * 0.18);
278
+ ctx.strokeStyle = color;
279
+ ctx.lineWidth = 1.2;
280
+ ctx.beginPath();
281
+ ctx.moveTo(x + size * 0.18, y + size * 0.88);
282
+ ctx.quadraticCurveTo(x + size * 0.34, y + size * 0.78, x + size * 0.5, y + size * 0.88);
283
+ ctx.quadraticCurveTo(x + size * 0.66, y + size * 0.98, x + size * 0.82, y + size * 0.88);
284
+ ctx.stroke();
285
+ }
286
+
287
+ function drawSubsurfaceVehicle(ctx, x, y, size, color) {
288
+ ctx.fillStyle = color;
289
+ ctx.beginPath();
290
+ ctx.ellipse(x + size * 0.5, y + size * 0.58, size * 0.38, size * 0.18, 0, 0, Math.PI * 2);
291
+ ctx.fill();
292
+ ctx.fillRect(x + size * 0.44, y + size * 0.28, size * 0.12, size * 0.18);
293
+ ctx.fillRect(x + size * 0.38, y + size * 0.26, size * 0.24, size * 0.06);
294
+ ctx.strokeStyle = color;
295
+ ctx.lineWidth = 1.2;
296
+ ctx.beginPath();
297
+ ctx.moveTo(x + size * 0.16, y + size * 0.86);
298
+ ctx.quadraticCurveTo(x + size * 0.32, y + size * 0.78, x + size * 0.5, y + size * 0.86);
299
+ ctx.quadraticCurveTo(x + size * 0.68, y + size * 0.94, x + size * 0.84, y + size * 0.86);
300
+ ctx.stroke();
301
+ }
302
+
303
+ function drawSpaceVehicle(ctx, x, y, size, color) {
304
+ const cx = x + size * 0.5;
305
+ const cy = y + size * 0.5;
306
+ ctx.strokeStyle = color;
307
+ ctx.fillStyle = color;
308
+ ctx.lineWidth = 1.2;
309
+ ctx.beginPath();
310
+ ctx.arc(cx, cy, size * 0.16, 0, Math.PI * 2);
311
+ ctx.fill();
312
+ ctx.strokeRect(x + size * 0.12, y + size * 0.32, size * 0.24, size * 0.36);
313
+ ctx.strokeRect(x + size * 0.64, y + size * 0.32, size * 0.24, size * 0.36);
314
+ ctx.beginPath();
315
+ ctx.moveTo(x + size * 0.36, cy);
316
+ ctx.lineTo(x + size * 0.64, cy);
317
+ ctx.stroke();
318
+ ctx.beginPath();
319
+ ctx.ellipse(cx, cy, size * 0.42, size * 0.18, -0.45, 0, Math.PI * 2);
320
+ ctx.stroke();
321
+ }
322
+
323
+ function drawControl(ctx, x, y, size, color) {
324
+ ctx.strokeStyle = color;
325
+ ctx.fillStyle = color;
326
+ ctx.lineWidth = 1.3;
327
+ const cx = x + size * 0.5;
328
+ const baseY = y + size * 0.68;
329
+
330
+ ctx.beginPath();
331
+ ctx.moveTo(x + size * 0.22, baseY);
332
+ ctx.lineTo(x + size * 0.78, baseY);
333
+ ctx.lineTo(x + size * 0.88, y + size * 0.88);
334
+ ctx.lineTo(x + size * 0.12, y + size * 0.88);
335
+ ctx.closePath();
336
+ ctx.stroke();
337
+
338
+ ctx.beginPath();
339
+ ctx.moveTo(cx, baseY);
340
+ ctx.lineTo(x + size * 0.42, y + size * 0.32);
341
+ ctx.stroke();
342
+
343
+ ctx.beginPath();
344
+ ctx.arc(x + size * 0.4, y + size * 0.28, size * 0.13, 0, Math.PI * 2);
345
+ ctx.fill();
346
+
347
+ ctx.beginPath();
348
+ ctx.arc(x + size * 0.66, y + size * 0.78, size * 0.055, 0, Math.PI * 2);
349
+ ctx.arc(x + size * 0.78, y + size * 0.78, size * 0.055, 0, Math.PI * 2);
350
+ ctx.fill();
351
+ }
352
+
353
+ function drawSituation(ctx, x, y, size, color) {
354
+ const cx = x + size * 0.5;
355
+ const cy = y + size * 0.5;
356
+ ctx.strokeStyle = color;
357
+ ctx.fillStyle = color;
358
+ ctx.lineWidth = 1.25;
359
+
360
+ ctx.beginPath();
361
+ ctx.arc(cx, cy, size * 0.42, 0, Math.PI * 2);
362
+ ctx.moveTo(cx - size * 0.42, cy);
363
+ ctx.lineTo(cx + size * 0.42, cy);
364
+ ctx.moveTo(cx, cy - size * 0.42);
365
+ ctx.lineTo(cx, cy + size * 0.42);
366
+ ctx.stroke();
367
+
368
+ ctx.globalAlpha = 0.55;
369
+ ctx.beginPath();
370
+ ctx.arc(cx, cy, size * 0.25, 0, Math.PI * 2);
371
+ ctx.stroke();
372
+ ctx.globalAlpha = 1;
373
+
374
+ ctx.beginPath();
375
+ ctx.moveTo(cx, cy);
376
+ ctx.lineTo(x + size * 0.82, y + size * 0.26);
377
+ ctx.stroke();
378
+
379
+ ctx.beginPath();
380
+ ctx.arc(x + size * 0.68, y + size * 0.38, size * 0.055, 0, Math.PI * 2);
381
+ ctx.arc(x + size * 0.36, y + size * 0.62, size * 0.045, 0, Math.PI * 2);
382
+ ctx.fill();
383
+ }
384
+
385
+ function drawDamage(ctx, x, y, size, color) {
386
+ const cx = x + size * 0.5;
387
+ const cy = y + size * 0.52;
388
+ const r = size * 0.42;
389
+
390
+ ctx.beginPath();
391
+ ctx.moveTo(cx, y + 1);
392
+ ctx.lineTo(cx + r * 0.28, cy - r * 0.28);
393
+ ctx.lineTo(x + size - 1, y + size * 0.2);
394
+ ctx.lineTo(cx + r * 0.55, cy + r * 0.05);
395
+ ctx.lineTo(x + size * 0.92, y + size * 0.76);
396
+ ctx.lineTo(cx + r * 0.22, cy + r * 0.32);
397
+ ctx.lineTo(cx + r * 0.08, y + size - 1);
398
+ ctx.lineTo(cx - r * 0.18, cy + r * 0.34);
399
+ ctx.lineTo(x + size * 0.14, y + size * 0.86);
400
+ ctx.lineTo(cx - r * 0.42, cy + r * 0.12);
401
+ ctx.lineTo(x + 1, y + size * 0.38);
402
+ ctx.lineTo(cx - r * 0.3, cy - r * 0.18);
403
+ ctx.closePath();
404
+ ctx.fillStyle = color;
405
+ ctx.fill();
406
+
407
+ ctx.fillStyle = '#fff7ed';
408
+ ctx.beginPath();
409
+ ctx.arc(cx, cy, size * 0.13, 0, Math.PI * 2);
410
+ ctx.fill();
411
+
412
+ ctx.strokeStyle = color;
413
+ ctx.lineWidth = 1.1;
414
+ ctx.beginPath();
415
+ ctx.moveTo(x + size * 0.08, y + size * 0.08);
416
+ ctx.lineTo(x + size * 0.2, y + size * 0.2);
417
+ ctx.moveTo(x + size * 0.84, y + size * 0.88);
418
+ ctx.lineTo(x + size * 0.94, y + size * 0.98);
419
+ ctx.moveTo(x + size * 0.9, y + size * 0.06);
420
+ ctx.lineTo(x + size * 0.82, y + size * 0.18);
421
+ ctx.stroke();
422
+ }
@@ -22,8 +22,18 @@ export const darkTheme = {
22
22
  root: { icon: 'folder', color: '#38bdf8' },
23
23
  system: { icon: 'folder', color: '#818cf8' },
24
24
  platform: { icon: 'aircraft', color: '#60a5fa' },
25
+ air: { icon: 'air', color: '#60a5fa' },
26
+ ground: { icon: 'ground', color: '#a3e635' },
27
+ surface: { icon: 'surface', color: '#22d3ee' },
28
+ subsurface: { icon: 'subsurface', color: '#38bdf8' },
29
+ space: { icon: 'space', color: '#c084fc' },
30
+ munition: { icon: 'munition', color: '#fb7185' },
25
31
  sensor: { icon: 'radar', color: '#34d399' },
26
32
  track: { icon: 'track', color: '#a78bfa' },
33
+ geometry: { icon: 'point', color: '#38bdf8' },
34
+ control: { icon: 'control', color: '#f59e0b' },
35
+ situation: { icon: 'situation', color: '#2dd4bf' },
36
+ damage: { icon: 'damage', color: '#fb7185' },
27
37
  warning: { icon: 'warning', color: '#facc15' },
28
38
  error: { icon: 'error', color: '#ef4444' },
29
39
  task: { icon: 'task', color: '#f97316' },
@@ -77,8 +87,18 @@ export const tacticalTheme = {
77
87
  ...darkTheme.types,
78
88
  root: { icon: 'folder', color: '#7ddc92' },
79
89
  platform: { icon: 'aircraft', color: '#93c572' },
90
+ air: { icon: 'air', color: '#93c572' },
91
+ ground: { icon: 'ground', color: '#c7f36f' },
92
+ surface: { icon: 'surface', color: '#67e8f9' },
93
+ subsurface: { icon: 'subsurface', color: '#7dd3fc' },
94
+ space: { icon: 'space', color: '#d8b4fe' },
95
+ munition: { icon: 'munition', color: '#ff8fab' },
80
96
  sensor: { icon: 'radar', color: '#45d483' },
81
97
  track: { icon: 'track', color: '#d6f264' },
98
+ geometry: { icon: 'point', color: '#7ddc92' },
99
+ control: { icon: 'control', color: '#ffd166' },
100
+ situation: { icon: 'situation', color: '#8be9d9' },
101
+ damage: { icon: 'damage', color: '#ff8fab' },
82
102
  warning: { icon: 'warning', color: '#ffd166' },
83
103
  error: { icon: 'error', color: '#ff5c5c' },
84
104
  },
@@ -159,4 +179,3 @@ function mergeTheme(base, override) {
159
179
  statuses: { ...base.statuses, ...(override.statuses ?? {}) },
160
180
  };
161
181
  }
162
-
@@ -59,6 +59,7 @@ export function rebuildWorkerRows(state, options = {}) {
59
59
  const rowHeight = options.rowHeight ?? 28;
60
60
  const indentWidth = options.indentWidth ?? 18;
61
61
  const expanded = new Set(options.expandedIds ?? []);
62
+ const filterCollapsed = new Set(options.filterCollapsedIds ?? []);
62
63
  const query = normalize(options.filterQuery ?? '');
63
64
  const sort = options.sort ?? { columnId: null, direction: null };
64
65
  const sortValues = options.sortValues ? new Map(options.sortValues) : null;
@@ -81,7 +82,8 @@ export function rebuildWorkerRows(state, options = {}) {
81
82
  if (!isIncluded(id)) return;
82
83
  const nodeIndex = state.idToIndex.get(id);
83
84
  if (nodeIndex === undefined) return;
84
- const expandedRow = expanded.has(id);
85
+ const children = sortIds(state.childrenByParent.get(id) ?? []);
86
+ const expandedRow = (expanded.has(id) || Boolean(query && children.length > 0)) && !filterCollapsed.has(id);
85
87
  const rowIndex = rows.length;
86
88
  rows.push({
87
89
  nodeId: id,
@@ -94,8 +96,8 @@ export function rebuildWorkerRows(state, options = {}) {
94
96
  hasChildren: hasChildren(id),
95
97
  });
96
98
  maxDepth = Math.max(maxDepth, depth);
97
- if (!expandedRow && !query) return;
98
- for (const childId of sortIds(state.childrenByParent.get(id) ?? [])) visit(childId, depth + 1);
99
+ if (!expandedRow) return;
100
+ for (const childId of children) visit(childId, depth + 1);
99
101
  };
100
102
 
101
103
  for (const rootId of sortIds(state.roots)) visit(rootId, 0);
@@ -33,6 +33,7 @@ export class VisibleRowModel extends EventTarget {
33
33
  this.contentHeight = 0;
34
34
  this.sortComparator = null;
35
35
  this.filterPredicate = null;
36
+ this.filterCollapsed = new Set();
36
37
  }
37
38
 
38
39
  setSortComparator(comparator) {
@@ -41,6 +42,15 @@ export class VisibleRowModel extends EventTarget {
41
42
 
42
43
  setFilterPredicate(predicate) {
43
44
  this.filterPredicate = predicate;
45
+ this.filterCollapsed.clear();
46
+ }
47
+
48
+ collapseFilterBranch(id) {
49
+ this.filterCollapsed.add(id);
50
+ }
51
+
52
+ expandFilterBranch(id) {
53
+ return this.filterCollapsed.delete(id);
44
54
  }
45
55
 
46
56
  applyRows({ rows, contentHeight, contentWidth }) {
@@ -84,8 +94,10 @@ export class VisibleRowModel extends EventTarget {
84
94
  if (!subtreeIncluded(id)) return;
85
95
  const nodeIndex = this.model.index.idToIndex.get(id);
86
96
  if (nodeIndex === undefined) return;
97
+ const children = sortedChildren(id);
87
98
  const hasChildren = this.expansion.hasChildren(id);
88
- const expanded = this.expansion.isExpanded(id);
99
+ const autoExpanded = Boolean(this.filterPredicate && children.length > 0);
100
+ const expanded = (this.expansion.isExpanded(id) || autoExpanded) && !this.filterCollapsed.has(id);
89
101
  const rowIndex = this.rows.length;
90
102
  this.rows.push({
91
103
  nodeId: id,
@@ -99,8 +111,8 @@ export class VisibleRowModel extends EventTarget {
99
111
  });
100
112
  this.rowIndexById.set(id, rowIndex);
101
113
  maxDepth = Math.max(maxDepth, depth);
102
- if (!expanded && !this.filterPredicate) return;
103
- for (const childId of sortedChildren(id)) visit(childId, depth + 1);
114
+ if (!expanded) return;
115
+ for (const childId of children) visit(childId, depth + 1);
104
116
  };
105
117
 
106
118
  const roots = this.model.index.roots.filter((rootId) => subtreeIncluded(rootId));
package/src/index.d.ts ADDED
@@ -0,0 +1,108 @@
1
+ export type TreeViewAlign = 'start' | 'center' | 'end' | 'nearest';
2
+
3
+ export type TreeNode = {
4
+ id: string;
5
+ parentId?: string | null;
6
+ label?: string;
7
+ type?: string;
8
+ icon?: string;
9
+ image?: string;
10
+ tags?: string[];
11
+ data?: any;
12
+ };
13
+
14
+ export type DynamicPatch = {
15
+ id: string;
16
+ state?: Record<string, any>;
17
+ } & Record<string, any>;
18
+
19
+ export type MetaRule = {
20
+ min?: number;
21
+ max?: number;
22
+ step?: number;
23
+ integer?: boolean;
24
+ readonly?: boolean;
25
+ disabled?: boolean;
26
+ updated?: boolean;
27
+ description?: string;
28
+ color?: boolean;
29
+ options?: Record<string, string | number | boolean>;
30
+ button?: string;
31
+ label?: string;
32
+ fullWidthButton?: boolean;
33
+ itemType?: 'object' | 'number' | 'string' | 'boolean';
34
+ itemTitle?: (i: number, item: any) => string;
35
+ itemFactory?: () => any;
36
+ type?: string;
37
+ icon?: string;
38
+ };
39
+
40
+ export type Column = {
41
+ id: string;
42
+ label?: string;
43
+ width?: number;
44
+ minWidth?: number;
45
+ align?: 'left' | 'center' | 'right';
46
+ kind?: string;
47
+ sortable?: boolean;
48
+ value?: (node: TreeNode, state: Record<string, any>) => string | number | boolean;
49
+ render?: (ctx: CanvasRenderingContext2D, cell: any) => void;
50
+ };
51
+
52
+ export const themes: Record<string, any>;
53
+
54
+ export class TreeRowRenderer {
55
+ renderedRows: number;
56
+ constructor(options?: Record<string, any>);
57
+ initialize(canvas: HTMLCanvasElement): void;
58
+ setScene(scene: any): void;
59
+ updateDynamicState(patches: DynamicPatch[]): void;
60
+ render(scene?: any, time?: number): void;
61
+ }
62
+
63
+ export class TreeViewController {
64
+ canvas?: HTMLCanvasElement;
65
+ viewport: {
66
+ viewportWidth: number;
67
+ viewportHeight: number;
68
+ [key: string]: any;
69
+ };
70
+ filterQuery: string;
71
+ rowModel: any;
72
+ expansion: any;
73
+ selection: any;
74
+ constructor(options?: Record<string, any>);
75
+ on(type: string, listener: (event: any) => void): any;
76
+ off(type: string, listener: (event: any) => void): void;
77
+ setData(nodes: TreeNode[]): void;
78
+ setModel(model: any, meta?: Record<string, MetaRule>, options?: Record<string, any>): void;
79
+ setColumns(columns: Column[]): void;
80
+ setDynamicState(patches: DynamicPatch[]): void;
81
+ setTheme(theme: any): void;
82
+ resize(width: number, height: number): void;
83
+ render(time?: number): void;
84
+ renderMeasured(time?: number): any;
85
+ hitTest(clientX: number, clientY: number): any;
86
+ getTooltipForHit(hit: any): any;
87
+ search(query: string, options?: Record<string, any>): any;
88
+ setFilter(queryOrPredicate?: string | ((node: any, state: any) => boolean)): void;
89
+ clearFilter(): void;
90
+ focusNode(nodeId: string, options?: Record<string, any>): boolean;
91
+ scrollToNode(nodeId: string, align?: TreeViewAlign): boolean;
92
+ getSelection(): string[];
93
+ setSelection(ids: string[]): void;
94
+ clearSelection(): void;
95
+ expandAll(): void;
96
+ collapseAll(): void;
97
+ }
98
+
99
+ export class TreeViewInputController {
100
+ constructor(options?: Record<string, any>);
101
+ destroy(): void;
102
+ }
103
+
104
+ export class CellEditorManager {
105
+ constructor(options?: Record<string, any>);
106
+ destroy(): void;
107
+ close(): void;
108
+ }
@@ -43,6 +43,7 @@ export class TreeViewInputController {
43
43
 
44
44
  #onWheel = (event) => {
45
45
  event.preventDefault();
46
+ this.cellEditor?.close?.();
46
47
  if (this.controller) {
47
48
  this.controller.scrollBy(event.shiftKey ? event.deltaY : event.deltaX, event.deltaY);
48
49
  return;
@@ -15,11 +15,18 @@ export class CellEditorManager {
15
15
  window.removeEventListener('mouseup', this.onMouseUp);
16
16
  }
17
17
 
18
+ close() {
19
+ this.#removeOverlay();
20
+ this.rangeDrag = null;
21
+ window.removeEventListener('mousemove', this.onMouseMove);
22
+ window.removeEventListener('mouseup', this.onMouseUp);
23
+ }
24
+
18
25
  handlePointerDown(event, hit) {
19
26
  if (!this.#isEditableHit(hit)) return false;
20
27
  const data = hit.row ? this.controller.model.nodes[hit.row.nodeIndex]?.data : null;
21
28
  if (!data || data.readonly || data.disabled) return false;
22
- if (data.editorType === 'range' && hit.part !== 'number') {
29
+ if (data.editorType === 'range' && hit.part === 'range') {
23
30
  this.rangeDrag = { hit, data };
24
31
  this.#updateRangeFromEvent(event);
25
32
  window.addEventListener('mousemove', this.onMouseMove);
@@ -47,7 +54,13 @@ export class CellEditorManager {
47
54
  this.controller.updateInspectorValue(node.id, !data.value, 'checkbox');
48
55
  return true;
49
56
  }
50
- if (data.editorType === 'range' && hit.part !== 'number') return true;
57
+ if (data.editorType === 'range') {
58
+ if (hit.part === 'range') return true;
59
+ if (hit.part !== 'number') {
60
+ this.#toggleRangeMinMax(node, data);
61
+ return true;
62
+ }
63
+ }
51
64
  if (this.overlay) return true;
52
65
  this.#showOverlay(node, hit);
53
66
  return true;
@@ -62,7 +75,7 @@ export class CellEditorManager {
62
75
  #showOverlay(node, hit, options = {}) {
63
76
  this.#removeOverlay();
64
77
  const data = node.data;
65
- const rect = this.#overlayRect(hit);
78
+ const rect = this.#clampRectToHost(this.#overlayRect(hit), 4);
66
79
  const hostRect = this.host.getBoundingClientRect();
67
80
  const element = createEditorElement(data);
68
81
  Object.assign(element.style, {
@@ -98,7 +111,7 @@ export class CellEditorManager {
98
111
  ensureOverlayHost(this.host);
99
112
  this.host.append(element);
100
113
  this.overlay = element;
101
- element.focus();
114
+ element.focus({ preventScroll: true });
102
115
  element.select?.();
103
116
  if (options.showPicker && element.showPicker) {
104
117
  requestAnimationFrame(() => {
@@ -113,7 +126,13 @@ export class CellEditorManager {
113
126
 
114
127
  #showHeaderFilterOverlay(hit) {
115
128
  this.#removeOverlay();
116
- const rect = this.controller.getHeaderClientRect(hit);
129
+ const headerRect = this.controller.getHeaderClientRect(hit);
130
+ const rect = this.#clampRectToHost({
131
+ x: headerRect.x + 8,
132
+ y: headerRect.y + 5,
133
+ width: Math.max(24, Math.min(headerRect.width, this.controller.viewport.viewportWidth) - 16),
134
+ height: Math.max(20, headerRect.height - 10),
135
+ }, 0);
117
136
  const hostRect = this.host.getBoundingClientRect();
118
137
  const element = document.createElement('input');
119
138
  element.type = 'search';
@@ -121,12 +140,12 @@ export class CellEditorManager {
121
140
  element.placeholder = 'Filter inspector';
122
141
  Object.assign(element.style, {
123
142
  position: 'absolute',
124
- left: `${rect.x - hostRect.left + 8}px`,
125
- top: `${rect.y - hostRect.top + 5}px`,
126
- width: `${Math.max(24, rect.width - 16)}px`,
127
- height: `${Math.max(20, rect.height - 10)}px`,
143
+ left: `${rect.x - hostRect.left}px`,
144
+ top: `${rect.y - hostRect.top}px`,
145
+ width: `${Math.max(24, rect.width)}px`,
146
+ height: `${Math.max(20, rect.height)}px`,
128
147
  minWidth: '0',
129
- maxWidth: `${Math.max(24, rect.width - 16)}px`,
148
+ maxWidth: `${Math.max(24, rect.width)}px`,
130
149
  zIndex: 20,
131
150
  boxSizing: 'border-box',
132
151
  margin: '0',
@@ -147,7 +166,7 @@ export class CellEditorManager {
147
166
  ensureOverlayHost(this.host);
148
167
  this.host.append(element);
149
168
  this.overlay = element;
150
- element.focus();
169
+ element.focus({ preventScroll: true });
151
170
  element.select();
152
171
  }
153
172
 
@@ -184,8 +203,9 @@ export class CellEditorManager {
184
203
  #overlayRect(hit) {
185
204
  if (hit.column?.kind !== 'inspectorPane') return this.controller.getCellClientRect(hit);
186
205
  const rect = this.controller.getCellClientRect(hit);
187
- const editorLeft = Math.min(Math.max(210, rect.width * 0.42), rect.width - 180);
188
- const editorWidth = Math.max(80, rect.width - editorLeft - 14);
206
+ const visibleWidth = Math.max(1, Math.min(rect.width, this.controller.viewport.viewportWidth));
207
+ const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
208
+ const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
189
209
  if (hit.part === 'number') {
190
210
  const valueWidth = Math.min(64, Math.max(42, (editorWidth - 20) * 0.28));
191
211
  return { x: rect.x + editorLeft + editorWidth - valueWidth - 10, y: rect.y + 4, width: valueWidth, height: rect.height - 8 };
@@ -196,8 +216,9 @@ export class CellEditorManager {
196
216
  #rangeBarRect(hit) {
197
217
  const rect = this.controller.getCellClientRect(hit);
198
218
  if (hit.column?.kind === 'inspectorPane') {
199
- const editorLeft = Math.min(Math.max(210, rect.width * 0.42), rect.width - 180);
200
- const editorWidth = Math.max(80, rect.width - editorLeft - 14);
219
+ const visibleWidth = Math.max(1, Math.min(rect.width, this.controller.viewport.viewportWidth));
220
+ const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
221
+ const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
201
222
  const valueWidth = Math.min(64, Math.max(42, (editorWidth - 20) * 0.28));
202
223
  const barWidth = Math.max(24, editorWidth - 20 - valueWidth - 8);
203
224
  return { x: rect.x + editorLeft + 10, y: rect.y + rect.height / 2 - 4, width: barWidth, height: 8 };
@@ -206,10 +227,31 @@ export class CellEditorManager {
206
227
  return { x: rect.x + 10, y: rect.y + rect.height / 2 - 4, width: Math.max(24, rect.width - 20 - valueWidth - 8), height: 8 };
207
228
  }
208
229
 
230
+ #clampRectToHost(rect, inset = 0) {
231
+ const hostRect = this.host.getBoundingClientRect();
232
+ const minX = hostRect.left + inset;
233
+ const minY = hostRect.top + inset;
234
+ const maxX = hostRect.right - inset;
235
+ const maxY = hostRect.bottom - inset;
236
+ const width = Math.max(24, Math.min(rect.width, maxX - minX));
237
+ const height = Math.max(18, Math.min(rect.height, maxY - minY));
238
+ const x = Math.max(minX, Math.min(rect.x, maxX - width));
239
+ const y = Math.max(minY, Math.min(rect.y, maxY - height));
240
+ return { x, y, width, height };
241
+ }
242
+
209
243
  #removeOverlay() {
210
244
  this.overlay?.remove();
211
245
  this.overlay = null;
212
246
  }
247
+
248
+ #toggleRangeMinMax(node, data) {
249
+ const meta = data.meta ?? {};
250
+ const min = Number.isFinite(meta.min) ? meta.min : 0;
251
+ const max = Number.isFinite(meta.max) ? meta.max : 100;
252
+ const current = typeof data.value === 'number' && Number.isFinite(data.value) ? data.value : min;
253
+ this.controller.updateInspectorValue(node.id, current <= min ? max : min, 'range');
254
+ }
213
255
  }
214
256
 
215
257
  function createEditorElement(data) {
@@ -30,7 +30,8 @@ export class ModelInspectorBuilder {
30
30
  id,
31
31
  parentId,
32
32
  label,
33
- type: inspectorNodeType(valueType, editorType),
33
+ type: rule.type ?? inspectorNodeType(valueType, editorType),
34
+ icon: rule.icon,
34
35
  data: {
35
36
  inspector: true,
36
37
  path,