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 +16 -0
- package/README.md +10 -4
- package/package.json +1 -1
- package/src/index.d.ts +9 -0
- package/src/input/row-actions.js +8 -0
- package/src/input/row-reorder-input.js +91 -9
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`.
|
|
437
|
-
|
|
438
|
-
|
|
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
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;
|
package/src/input/row-actions.js
CHANGED
|
@@ -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
|
|
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() && !(
|
|
79
|
-
:
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|