virtual-tree-canvas 0.7.1 → 0.7.3

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Added
6
+
7
+ - External row dragging now includes every draggable row in the active selection through `items`, `nodeIds` and `count`, with a count badge for multi-row gestures.
8
+
9
+ ### Changed
10
+
11
+ - Starting a drag on an unselected row selects and drags only that row, matching common multi-selection behavior.
12
+
13
+ ## 0.7.2 — 2026-09-13
14
+
15
+ ### Fixed
16
+
17
+ - Repaint initially visible row-action icons when their asynchronous SVG raster finishes loading.
18
+
3
19
  ## 0.7.1
4
20
 
5
21
  - Coalesce dynamic state patches by node until the next frame and ignore repeated values.
package/README.md CHANGED
@@ -430,12 +430,18 @@ value updates preserve their identity and focus.
430
430
 
431
431
  `rowDrag(node, dynamicState)` returns a payload, or `null` to disable dragging
432
432
  that row. Drag from a row label or the row-order handle. Editors, checkboxes,
433
- action buttons and expand/collapse controls retain their normal behavior.
433
+ action buttons and expand/collapse controls retain their normal behavior. When
434
+ the source row belongs to a multi-selection, all selected draggable rows take
435
+ part in the gesture. Starting on an unselected row selects and drags only that
436
+ row. A small count badge identifies multi-row drags.
434
437
 
435
438
  The controller emits `rowdragstart`, `rowdragmove`, `rowdragend` and
436
- `rowdragcancel`. Details contain `nodeId`, `payload`, `label` and, except on
437
- cancellation, `originalEvent` with client coordinates. Connect these events to
438
- your application's drop manager to highlight destinations and handle the drop.
439
+ `rowdragcancel`. The backward-compatible `nodeId`, `payload` and `label` fields
440
+ describe the source row. `items`, `nodeIds` and `count` describe the complete
441
+ drag selection; every item contains its row's `nodeId`, `payload` and `label`.
442
+ Rows whose `rowDrag` resolver returns `null` are omitted. Except on cancellation,
443
+ `originalEvent` provides client coordinates. Connect these events to your
444
+ application's drop manager to highlight destinations and handle the drop.
439
445
  The payload is captured at pointer-down; use a stable reference instead of a
440
446
  snapshot of a live value. The library never moves or deletes exported data.
441
447
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "virtual-tree-canvas",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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",
package/src/index.d.ts CHANGED
@@ -328,8 +328,17 @@ export interface RowDragDetail {
328
328
  nodeId: string;
329
329
  payload: any;
330
330
  label: string;
331
+ /** Draggable selected rows captured when the pointer gesture starts. */
332
+ items: RowDragItem[];
333
+ nodeIds: string[];
334
+ count: number;
331
335
  originalEvent: PointerEvent;
332
336
  }
337
+ export interface RowDragItem {
338
+ nodeId: string;
339
+ payload: any;
340
+ label: string;
341
+ }
333
342
  export interface TreeViewEvents {
334
343
  rowaction: {checked?: boolean; actionId: string; nodeId: string; node: TreeNode; originalEvent: MouseEvent};
335
344
  rowdragstart: RowDragDetail;
@@ -13,6 +13,12 @@ export class RowActions {
13
13
  this.incrementalUpdates = 0;
14
14
  this.themeToken = 0;
15
15
  this.theme = null;
16
+ this.stopIconListener = this.controller.iconRegistry.onChange?.(() => {
17
+ for (const button of this.buttons.values()) {
18
+ if (button._vtcKind !== 'checkbox') button.firstChild._vtcDrawKey = null;
19
+ }
20
+ this.controller.requestRender(true);
21
+ }) ?? null;
16
22
  this.element = controller.canvas.ownerDocument.createElement('div');
17
23
  Object.assign(this.element.style, {
18
24
  position: 'absolute', overflow: 'hidden', pointerEvents: 'none', zIndex: '2'
@@ -215,6 +221,8 @@ export class RowActions {
215
221
  }
216
222
 
217
223
  destroy() {
224
+ this.stopIconListener?.();
225
+ this.stopIconListener = null;
218
226
  this.element.remove();
219
227
  this.buttons.clear();
220
228
  this.buttonsByNodeId.clear();
@@ -51,6 +51,29 @@ export class RowReorderInput {
51
51
  });
52
52
  this.canvas.parentElement.appendChild(this.status);
53
53
  }
54
+ if (doc?.createElement && doc.body) {
55
+ this.dragBadge = doc.createElement('span');
56
+ this.dragBadge.className = 'vtc-row-drag-badge';
57
+ this.dragBadge.hidden = true;
58
+ this.dragBadge.setAttribute('aria-hidden', 'true');
59
+ Object.assign(this.dragBadge.style, {
60
+ position: 'fixed',
61
+ zIndex: '2147483647',
62
+ pointerEvents: 'none',
63
+ minWidth: '20px',
64
+ height: '20px',
65
+ padding: '0 6px',
66
+ boxSizing: 'border-box',
67
+ border: '1px solid rgba(255, 255, 255, 0.35)',
68
+ borderRadius: '10px',
69
+ background: '#1f2937',
70
+ color: '#fff',
71
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
72
+ font: '600 12px/18px system-ui, sans-serif',
73
+ textAlign: 'center'
74
+ });
75
+ doc.body.appendChild(this.dragBadge);
76
+ }
54
77
  this.syncMode();
55
78
  }
56
79
 
@@ -68,17 +91,21 @@ export class RowReorderInput {
68
91
  const hit = this.controller.hitTest(point.x, point.y);
69
92
  if (!hit?.row) return;
70
93
  const node = this.controller.model.index.getNode(hit.row.nodeId);
71
- const payload =
94
+ const sourcePayload =
72
95
  this.controller.rowDrag?.(node, this.controller.model.dynamicState.get(node.id) ?? {}) ??
73
96
  null;
74
97
  const orderPart = ['rowDrag', 'rowUp', 'rowDown'].includes(hit.part);
75
98
  if (orderPart && node.reorderable === false) return;
76
99
  if (
77
100
  orderPart
78
- ? !this.controller.canReorderRows() && !(payload != null && hit.part === 'rowDrag')
79
- : payload == null || !['label', 'row', 'icon', 'cell'].includes(hit.part)
101
+ ? !this.controller.canReorderRows() && !(sourcePayload != null && hit.part === 'rowDrag')
102
+ : sourcePayload == null || !['label', 'row', 'icon', 'cell'].includes(hit.part)
80
103
  )
81
104
  return;
105
+ const items =
106
+ sourcePayload != null && (!orderPart || hit.part === 'rowDrag')
107
+ ? this.resolveDragItems(node, sourcePayload)
108
+ : [];
82
109
  this.canvas.focus({
83
110
  preventScroll: true
84
111
  });
@@ -87,7 +114,9 @@ export class RowReorderInput {
87
114
  this.gesture = {
88
115
  id: hit.row.nodeId,
89
116
  part: orderPart ? hit.part : 'rowDrag',
90
- payload,
117
+ payload: sourcePayload,
118
+ items,
119
+ sourceSelected: this.controller.selection.selected.has(hit.row.nodeId),
91
120
  pointerId: event.pointerId,
92
121
  startX: point.x,
93
122
  startY: point.y,
@@ -109,9 +138,16 @@ export class RowReorderInput {
109
138
  Math.hypot(gesture.point.x - gesture.startX, gesture.point.y - gesture.startY) < 5
110
139
  )
111
140
  return;
112
- if (!gesture.dragging && gesture.payload != null) this.emitDrag('rowdragstart', event);
141
+ if (!gesture.dragging && gesture.payload != null) {
142
+ if (!gesture.sourceSelected) this.controller.setSelection([gesture.id]);
143
+ this.updateDragBadge(event);
144
+ this.emitDrag('rowdragstart', event);
145
+ }
113
146
  gesture.dragging = true;
114
- if (gesture.payload != null) this.emitDrag('rowdragmove', event);
147
+ if (gesture.payload != null) {
148
+ this.updateDragBadge(event);
149
+ this.emitDrag('rowdragmove', event);
150
+ }
115
151
  this.canvas.style.cursor = 'grabbing';
116
152
  this.updateTarget();
117
153
  this.scheduleScroll();
@@ -201,13 +237,57 @@ export class RowReorderInput {
201
237
 
202
238
  emitDrag(type, originalEvent) {
203
239
  const gesture = this.gesture;
204
- if (gesture)
205
- this.controller.events.emit(type, {
240
+ if (gesture) {
241
+ const detail = {
206
242
  nodeId: gesture.id,
207
243
  payload: gesture.payload,
208
244
  label: this.controller.model.index.getNode(gesture.id)?.label ?? gesture.id,
209
- originalEvent
245
+ items: gesture.items,
246
+ nodeIds: gesture.items.map((item) => item.nodeId),
247
+ count: gesture.items.length
248
+ };
249
+ if (originalEvent) detail.originalEvent = originalEvent;
250
+ this.controller.events.emit(type, detail);
251
+ }
252
+ }
253
+
254
+ resolveDragItems(source, sourcePayload) {
255
+ const selectedIds = this.controller.getSelection();
256
+ const ids = selectedIds.includes(source.id) ? selectedIds : [source.id];
257
+ const items = [];
258
+ for (const id of ids) {
259
+ const node = this.controller.model.index.getNode(id);
260
+ if (!node) continue;
261
+ const payload =
262
+ id === source.id
263
+ ? sourcePayload
264
+ : (this.controller.rowDrag?.(
265
+ node,
266
+ this.controller.model.dynamicState.get(node.id) ?? {}
267
+ ) ?? null);
268
+ if (payload == null) continue;
269
+ items.push({
270
+ nodeId: id,
271
+ payload,
272
+ label: node.label ?? id
210
273
  });
274
+ }
275
+ return items;
276
+ }
277
+
278
+ updateDragBadge(event) {
279
+ const count = this.gesture?.items.length ?? 0;
280
+ if (!this.dragBadge || count <= 1) return;
281
+ const colors = this.controller.themeManager.get().colors;
282
+ this.dragBadge.textContent = `×${count}`;
283
+ this.dragBadge.style.background = colors.badgeFill;
284
+ this.dragBadge.style.color = colors.badgeText;
285
+ this.dragBadge.style.borderColor = colors.borderStrong;
286
+ this.dragBadge.style.boxShadow = `0 2px 8px ${colors.shadow}`;
287
+ this.dragBadge.hidden = false;
288
+ this.dragBadge.style.left = `${event.clientX + 12}px`;
289
+ this.dragBadge.style.top = `${event.clientY + 12}px`;
290
+ if (this.status) this.status.textContent = `${count} rows dragging`;
211
291
  }
212
292
 
213
293
  cancel = (notify = true) => {
@@ -221,6 +301,7 @@ export class RowReorderInput {
221
301
  if (gesture && this.canvas.hasPointerCapture?.(gesture.pointerId))
222
302
  this.canvas.releasePointerCapture(gesture.pointerId);
223
303
  this.canvas.style.cursor = '';
304
+ if (this.dragBadge) this.dragBadge.hidden = true;
224
305
  this.controller.setRowDrop(null);
225
306
  };
226
307
 
@@ -236,6 +317,7 @@ export class RowReorderInput {
236
317
  this.canvas.style.touchAction = this.previousTouchAction;
237
318
  for (const stop of this.stops) stop();
238
319
  this.status?.remove();
320
+ this.dragBadge?.remove();
239
321
  }
240
322
 
241
323
  }