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/CHANGELOG.md +15 -0
- package/README.md +12 -1
- package/docs/icon-catalog.html +68 -66
- package/docs/icon-catalog.md +111 -111
- package/docs/performance/dynamic-updates-0.7.1.md +38 -0
- package/package.json +3 -2
- package/src/assets/icons.js +10 -4
- package/src/core/dynamic-state.js +19 -29
- package/src/core/patch-batcher.js +4 -0
- package/src/core/theme-manager.js +238 -64
- package/src/core/tree-cell-layout.js +4 -2
- package/src/core/tree-column-model.js +72 -24
- package/src/core/tree-model.js +4 -2
- package/src/core/view-options.js +98 -25
- package/src/core/visible-row-model.js +50 -20
- package/src/index.d.ts +22 -0
- package/src/input/row-actions.js +224 -83
- package/src/input/row-reorder-input.js +112 -39
- package/src/input/tree-tooltip.js +39 -5
- package/src/input/tree-view-input-controller.js +33 -10
- package/src/inspector/cell-editor-manager.js +110 -31
- package/src/inspector/pane-layout.js +11 -3
- package/src/renderers/checkbox.js +12 -13
- package/src/renderers/tree-row-renderer.js +325 -68
- package/src/tree-view-controller.js +919 -213
- package/src/tree-view.js +215 -53
- package/src/view/styles.js +83 -12
|
@@ -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)
|
|
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
|
-
/**
|
|
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 =>
|
|
48
|
-
|
|
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 = {
|
|
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, {
|
|
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(
|
|
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:
|
|
130
|
-
|
|
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({
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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({
|
|
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
|
|
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 {
|
|
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(
|
|
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(
|
|
251
|
-
|
|
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 {
|
|
312
|
+
return {
|
|
313
|
+
...rect,
|
|
314
|
+
x: rect.x + layout.numberLeft,
|
|
315
|
+
width: layout.valueWidth
|
|
316
|
+
};
|
|
260
317
|
}
|
|
261
|
-
if (layout.unit)
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|