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.
@@ -3,7 +3,8 @@ import { numericLayout } from './numeric-layout.js';
3
3
  export class CellEditorManager {
4
4
  constructor({ controller, host = null }) {
5
5
  if (!controller) throw new TypeError('CellEditorManager requires a TreeViewController');
6
- if (!controller.canvas) throw new Error('CellEditorManager requires an initialized TreeViewController canvas');
6
+ if (!controller.canvas)
7
+ throw new Error('CellEditorManager requires an initialized TreeViewController canvas');
7
8
  this.controller = controller;
8
9
  this.canvas = controller.canvas;
9
10
  this.view = this.canvas.ownerDocument.defaultView;
@@ -39,13 +40,20 @@ export class CellEditorManager {
39
40
  this.view.removeEventListener('mouseup', this.onMouseUp);
40
41
  }
41
42
 
42
- /** Keep an in-progress edit while values refresh; cancel if its editor changes. */
43
+ /**
44
+ * Keep an in-progress edit while values refresh; cancel if its editor changes.
45
+ */
43
46
  reconcile(nodes) {
44
47
  const previous = this.overlayNode ?? this.rangeDrag?.node;
45
48
  if (!previous) return;
46
- const next = nodes.find(node => node.id === previous.id);
47
- const signature = data => JSON.stringify([data.editorType, data.readonly, data.disabled,
48
- ...['min', 'max', 'step', 'integer', 'options', 'unit'].map(key => data.meta?.[key])]);
49
+ const next = nodes.find((node) => node.id === previous.id);
50
+ const signature = (data) =>
51
+ JSON.stringify([
52
+ data.editorType,
53
+ data.readonly,
54
+ data.disabled,
55
+ ...['min', 'max', 'step', 'integer', 'options', 'unit'].map((key) => data.meta?.[key])
56
+ ]);
49
57
  if (!next || signature(previous.data) !== signature(next.data)) this.close();
50
58
  }
51
59
 
@@ -54,7 +62,11 @@ export class CellEditorManager {
54
62
  const data = hit.row ? this.controller.model.nodes[hit.row.nodeIndex]?.data : null;
55
63
  if (!data || data.readonly || data.disabled) return false;
56
64
  if (data.editorType === 'range' && hit.part === 'range') {
57
- this.rangeDrag = { hit, data, node: this.controller.model.nodes[hit.row.nodeIndex] };
65
+ this.rangeDrag = {
66
+ hit,
67
+ data,
68
+ node: this.controller.model.nodes[hit.row.nodeIndex]
69
+ };
58
70
  this.#updateRangeFromEvent(event);
59
71
  this.view.addEventListener('mousemove', this.onMouseMove);
60
72
  this.view.addEventListener('mouseup', this.onMouseUp);
@@ -62,7 +74,9 @@ export class CellEditorManager {
62
74
  }
63
75
  if (shouldOpenOverlayOnPointerDown(data, hit)) {
64
76
  const node = this.controller.model.nodes[hit.row.nodeIndex];
65
- this.#showOverlay(node, hit, { showPicker: true });
77
+ this.#showOverlay(node, hit, {
78
+ showPicker: true
79
+ });
66
80
  return true;
67
81
  }
68
82
  return false;
@@ -106,7 +120,10 @@ export class CellEditorManager {
106
120
  const hostRect = this.host.getBoundingClientRect();
107
121
  const element = createEditorElement(data);
108
122
  element.className = `vtc-editor vtc-editor-${data.editorType}`;
109
- element.setAttribute('aria-label', [node.label, numericLayout(rect.width, data).unit].filter(Boolean).join(' '));
123
+ element.setAttribute(
124
+ 'aria-label',
125
+ [node.label, numericLayout(rect.width, data).unit].filter(Boolean).join(' ')
126
+ );
110
127
  Object.assign(element.style, {
111
128
  position: 'absolute',
112
129
  left: `${rect.x - hostRect.left}px`,
@@ -126,8 +143,13 @@ export class CellEditorManager {
126
143
  color: '#e5e7eb',
127
144
  boxShadow: '0 0 0 1px rgba(15, 23, 42, 0.8), 0 8px 20px rgba(0, 0, 0, 0.28)',
128
145
  font: editorFont(data),
129
- padding: data.editorType === 'color' ? '0 2px' : data.editorType === 'select' ? '0 22px 0 8px' : '0 8px',
130
- textAlign: data.editorType === 'number' || data.editorType === 'range' ? 'right' : 'left',
146
+ padding:
147
+ data.editorType === 'color'
148
+ ? '0 2px'
149
+ : data.editorType === 'select'
150
+ ? '0 22px 0 8px'
151
+ : '0 8px',
152
+ textAlign: data.editorType === 'number' || data.editorType === 'range' ? 'right' : 'left'
131
153
  });
132
154
  let committed = false;
133
155
  const commit = () => {
@@ -150,7 +172,9 @@ export class CellEditorManager {
150
172
  this.overlay = element;
151
173
  this.overlayKind = 'value';
152
174
  this.overlayNode = node;
153
- element.focus({ preventScroll: true });
175
+ element.focus({
176
+ preventScroll: true
177
+ });
154
178
  element.select?.();
155
179
  if (options.showPicker && element.showPicker) {
156
180
  requestAnimationFrame(() => {
@@ -166,12 +190,18 @@ export class CellEditorManager {
166
190
  #showHeaderFilterOverlay(hit) {
167
191
  this.#removeOverlay();
168
192
  const headerRect = this.controller.getHeaderClientRect(hit);
169
- const rect = this.#clampRectToHost({
170
- x: headerRect.x + 8,
171
- y: headerRect.y + 5,
172
- width: Math.max(24, Math.min(headerRect.width, this.controller.viewport.contentViewportWidth) - 16),
173
- height: Math.max(20, headerRect.height - 10),
174
- }, 0);
193
+ const rect = this.#clampRectToHost(
194
+ {
195
+ x: headerRect.x + 8,
196
+ y: headerRect.y + 5,
197
+ width: Math.max(
198
+ 24,
199
+ Math.min(headerRect.width, this.controller.viewport.contentViewportWidth) - 16
200
+ ),
201
+ height: Math.max(20, headerRect.height - 10)
202
+ },
203
+ 0
204
+ );
175
205
  const hostRect = this.host.getBoundingClientRect();
176
206
  const element = document.createElement('input');
177
207
  element.type = 'search';
@@ -194,7 +224,7 @@ export class CellEditorManager {
194
224
  background: '#0b1020',
195
225
  color: '#e5e7eb',
196
226
  font: SANS_FONT,
197
- padding: '0 8px',
227
+ padding: '0 8px'
198
228
  });
199
229
  element.addEventListener('input', () => this.controller.setFilter(element.value));
200
230
  element.addEventListener('keydown', (event) => {
@@ -206,7 +236,9 @@ export class CellEditorManager {
206
236
  this.host.append(element);
207
237
  this.overlay = element;
208
238
  this.overlayKind = 'filter';
209
- element.focus({ preventScroll: true });
239
+ element.focus({
240
+ preventScroll: true
241
+ });
210
242
  element.select();
211
243
  }
212
244
 
@@ -237,18 +269,39 @@ export class CellEditorManager {
237
269
  }
238
270
 
239
271
  #isEditableHit(hit) {
240
- return hit?.area === 'row' && hit.part !== 'unit' && (hit.column?.kind === 'inspectorValue' || hit.column?.kind === 'inspectorPane');
272
+ return (
273
+ hit?.area === 'row' &&
274
+ hit.part !== 'unit' &&
275
+ (hit.column?.kind === 'inspectorValue' || hit.column?.kind === 'inspectorPane')
276
+ );
241
277
  }
242
278
 
243
279
  #editorContentRect(hit) {
244
280
  const rect = this.controller.getCellClientRect(hit);
245
281
  if (hit.column?.kind !== 'inspectorPane') {
246
- return { x: rect.x + 10, y: rect.y + 4, width: Math.max(24, rect.width - 20), height: rect.height - 8 };
282
+ return {
283
+ x: rect.x + 10,
284
+ y: rect.y + 4,
285
+ width: Math.max(24, rect.width - 20),
286
+ height: rect.height - 8
287
+ };
247
288
  }
248
- const visibleWidth = Math.max(1, Math.min(rect.width, this.controller.viewport.contentViewportWidth));
289
+ const visibleWidth = Math.max(
290
+ 1,
291
+ Math.min(rect.width, this.controller.viewport.contentViewportWidth)
292
+ );
249
293
  const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
250
- const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(visibleWidth, hit.row, data.editorType);
251
- return { x: rect.x + editorLeft + 10, y: rect.y + 4, width: Math.max(24, editorWidth - 20), height: rect.height - 8 };
294
+ const { editorLeft, editorWidth } = this.controller.getInspectorPaneLayout(
295
+ visibleWidth,
296
+ hit.row,
297
+ data.editorType
298
+ );
299
+ return {
300
+ x: rect.x + editorLeft + 10,
301
+ y: rect.y + 4,
302
+ width: Math.max(24, editorWidth - 20),
303
+ height: rect.height - 8
304
+ };
252
305
  }
253
306
 
254
307
  #overlayRect(hit) {
@@ -256,9 +309,17 @@ export class CellEditorManager {
256
309
  const rect = this.#editorContentRect(hit);
257
310
  const layout = numericLayout(rect.width, data);
258
311
  if (hit.part === 'number' && data.editorType === 'range') {
259
- return { ...rect, x: rect.x + layout.numberLeft, width: layout.valueWidth };
312
+ return {
313
+ ...rect,
314
+ x: rect.x + layout.numberLeft,
315
+ width: layout.valueWidth
316
+ };
260
317
  }
261
- if (layout.unit) return { ...rect, width: layout.contentWidth };
318
+ if (layout.unit)
319
+ return {
320
+ ...rect,
321
+ width: layout.contentWidth
322
+ };
262
323
  if (hit.column?.kind !== 'inspectorPane') return this.controller.getCellClientRect(hit);
263
324
  return rect;
264
325
  }
@@ -266,7 +327,12 @@ export class CellEditorManager {
266
327
  #rangeBarRect(hit) {
267
328
  const data = this.controller.model.nodes[hit.row.nodeIndex]?.data ?? {};
268
329
  const rect = this.#editorContentRect(hit);
269
- return { ...rect, y: rect.y + rect.height / 2 - 4, width: numericLayout(rect.width, data).barWidth, height: 8 };
330
+ return {
331
+ ...rect,
332
+ y: rect.y + rect.height / 2 - 4,
333
+ width: numericLayout(rect.width, data).barWidth,
334
+ height: 8
335
+ };
270
336
  }
271
337
 
272
338
  #clampRectToHost(rect, inset = 0) {
@@ -279,7 +345,12 @@ export class CellEditorManager {
279
345
  const height = Math.max(18, Math.min(rect.height, maxY - minY));
280
346
  const x = Math.max(minX, Math.min(rect.x, maxX - width));
281
347
  const y = Math.max(minY, Math.min(rect.y, maxY - height));
282
- return { x, y, width, height };
348
+ return {
349
+ x,
350
+ y,
351
+ width,
352
+ height
353
+ };
283
354
  }
284
355
 
285
356
  #removeOverlay() {
@@ -294,9 +365,11 @@ export class CellEditorManager {
294
365
  const meta = data.meta ?? {};
295
366
  const min = Number.isFinite(meta.min) ? meta.min : 0;
296
367
  const max = Number.isFinite(meta.max) ? meta.max : 100;
297
- const current = typeof data.value === 'number' && Number.isFinite(data.value) ? data.value : min;
368
+ const current =
369
+ typeof data.value === 'number' && Number.isFinite(data.value) ? data.value : min;
298
370
  this.controller.updateInspectorValue(node.id, current <= min ? max : min, 'range');
299
371
  }
372
+
300
373
  }
301
374
 
302
375
  function createEditorElement(data) {
@@ -326,7 +399,12 @@ function createEditorElement(data) {
326
399
  function shouldOpenOverlayOnPointerDown(data, hit) {
327
400
  if (data.readonly || data.disabled) return false;
328
401
  if (data.editorType === 'range') return hit.part === 'number';
329
- return data.editorType === 'text' || data.editorType === 'number' || data.editorType === 'select' || data.editorType === 'color';
402
+ return (
403
+ data.editorType === 'text' ||
404
+ data.editorType === 'number' ||
405
+ data.editorType === 'select' ||
406
+ data.editorType === 'color'
407
+ );
330
408
  }
331
409
 
332
410
  function ensureOverlayHost(host) {
@@ -352,7 +430,8 @@ function parseEditorValue(element, data) {
352
430
  }
353
431
 
354
432
  const SANS_FONT = '12px Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif';
355
- const MONO_FONT = '12px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace';
433
+ const MONO_FONT =
434
+ '12px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace';
356
435
 
357
436
  function editorFont(data) {
358
437
  return data.editorType === 'number' || data.editorType === 'range' ? MONO_FONT : SANS_FONT;
@@ -1,4 +1,10 @@
1
- export function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorType = '', labelEnd = 0) {
1
+ export function inspectorPaneLayout(
2
+ width,
3
+ depth = 0,
4
+ indentWidth = 18,
5
+ editorType = '',
6
+ labelEnd = 0
7
+ ) {
2
8
  const safeWidth = Math.max(1, width);
3
9
  const rightPadding = 14;
4
10
  const minEditor = Math.min(170, Math.max(96, safeWidth * 0.45));
@@ -7,6 +13,8 @@ export function inspectorPaneLayout(width, depth = 0, indentWidth = 18, editorTy
7
13
  const maxLeft = Math.max(64, safeWidth - rightPadding - minEditor);
8
14
  const editorLeft = Math.max(64, Math.min(preferredLeft, maxLeft));
9
15
  const editorWidth = Math.max(56, safeWidth - rightPadding - editorLeft);
10
- return { editorLeft, editorWidth };
16
+ return {
17
+ editorLeft,
18
+ editorWidth
19
+ };
11
20
  }
12
-
@@ -1,14 +1,13 @@
1
1
  export function drawCheckbox(ctx, x, y, checked, theme) {
2
- ctx.beginPath();
3
- ctx.roundRect(x, y, 16, 16, 3);
4
- ctx.fillStyle = theme.colors.progressTrack;
5
- ctx.fill();
6
- ctx.strokeStyle = checked ? theme.colors.progressFill : theme.colors.textMuted;
7
- ctx.stroke();
8
- if (!checked) return;
9
- ctx.fillStyle = theme.colors.progressFill;
10
- ctx.beginPath();
11
- ctx.roundRect(x + 4, y + 4, 8, 8, 1.5);
12
- ctx.fill();
13
- }
14
-
2
+ ctx.beginPath();
3
+ ctx.roundRect(x, y, 16, 16, 3);
4
+ ctx.fillStyle = theme.colors.progressTrack;
5
+ ctx.fill();
6
+ ctx.strokeStyle = checked ? theme.colors.progressFill : theme.colors.textMuted;
7
+ ctx.stroke();
8
+ if (!checked) return;
9
+ ctx.fillStyle = theme.colors.progressFill;
10
+ ctx.beginPath();
11
+ ctx.roundRect(x + 4, y + 4, 8, 8, 1.5);
12
+ ctx.fill();
13
+ }