@griddle/vue 0.1.6 → 0.1.8
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/dist/GriddleGrid.vue.d.ts.map +1 -1
- package/dist/LoopGrid.vue.d.ts.map +1 -1
- package/dist/index.js +590 -532
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +49 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +82 -0
- package/dist/interaction.js.map +1 -0
- package/package.json +3 -3
- package/src/GriddleGrid.vue +76 -29
- package/src/LoopGrid.vue +22 -13
- package/src/interaction.ts +153 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@griddle/vue",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Vue 3 bindings for Griddle — a headless, zero-dependency grid/canvas engine.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"grid",
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"vue": "^3.3.0",
|
|
56
|
-
"@griddle/core": "^0.1.
|
|
56
|
+
"@griddle/core": "^0.1.8"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"vue": "^3.4.0",
|
|
60
|
-
"@griddle/core": "^0.1.
|
|
60
|
+
"@griddle/core": "^0.1.8",
|
|
61
61
|
"vite": "^5.4.0",
|
|
62
62
|
"@vitejs/plugin-vue": "^5.2.0",
|
|
63
63
|
"vue-tsc": "^2.2.0"
|
package/src/GriddleGrid.vue
CHANGED
|
@@ -112,6 +112,13 @@ import {
|
|
|
112
112
|
import type { TileLayout } from '@griddle/core';
|
|
113
113
|
import type { GriddleApi } from './useGriddle.js';
|
|
114
114
|
import { animateReposition, liftTransition } from './animation.js';
|
|
115
|
+
import {
|
|
116
|
+
measureInteractionScale,
|
|
117
|
+
clampInteractionCell,
|
|
118
|
+
resolveResizePreview,
|
|
119
|
+
toLocalInteractionDelta,
|
|
120
|
+
type InteractionScale,
|
|
121
|
+
} from './interaction.js';
|
|
115
122
|
|
|
116
123
|
const props = defineProps<{
|
|
117
124
|
api: GriddleApi;
|
|
@@ -208,6 +215,7 @@ interface DragVisual {
|
|
|
208
215
|
const drag = ref<DragVisual | null>(null);
|
|
209
216
|
let dragStartPointerX = 0;
|
|
210
217
|
let dragStartPointerY = 0;
|
|
218
|
+
let interactionScale: InteractionScale = { x: 1, y: 1 };
|
|
211
219
|
|
|
212
220
|
interface GroupDragVisual {
|
|
213
221
|
tileIds: string[];
|
|
@@ -277,6 +285,7 @@ function beginPendingDrag(pending: PendingDragState): boolean {
|
|
|
277
285
|
|
|
278
286
|
dragStartPointerX = pending.startPointerX;
|
|
279
287
|
dragStartPointerY = pending.startPointerY;
|
|
288
|
+
interactionScale = measureInteractionScale(scrollEl.value);
|
|
280
289
|
|
|
281
290
|
if (pending.mode === 'pin') {
|
|
282
291
|
const layout = computeTileLayout({
|
|
@@ -399,11 +408,20 @@ function onBackgroundPointerDown(e: PointerEvent) {
|
|
|
399
408
|
|
|
400
409
|
const el = scrollEl.value;
|
|
401
410
|
if (!el) return;
|
|
411
|
+
interactionScale = measureInteractionScale(el);
|
|
402
412
|
const rect = el.getBoundingClientRect();
|
|
403
|
-
const x = e.clientX - rect.left + el.scrollLeft;
|
|
404
|
-
const y = e.clientY - rect.top + el.scrollTop;
|
|
405
|
-
const col =
|
|
406
|
-
|
|
413
|
+
const x = (e.clientX - rect.left) / interactionScale.x + el.scrollLeft;
|
|
414
|
+
const y = (e.clientY - rect.top) / interactionScale.y + el.scrollTop;
|
|
415
|
+
const col = clampInteractionCell(
|
|
416
|
+
Math.floor(x / colSize.value),
|
|
417
|
+
config.value.cols,
|
|
418
|
+
config.value.infiniteX ?? config.value.cols === Infinity,
|
|
419
|
+
);
|
|
420
|
+
const row = clampInteractionCell(
|
|
421
|
+
Math.floor(y / rowSize.value),
|
|
422
|
+
config.value.rows,
|
|
423
|
+
config.value.infiniteY ?? config.value.rows === Infinity,
|
|
424
|
+
);
|
|
407
425
|
drawState.value = { anchorCol: col, anchorRow: row, currentCol: col, currentRow: row };
|
|
408
426
|
(el as HTMLDivElement).setPointerCapture(e.pointerId);
|
|
409
427
|
}
|
|
@@ -411,6 +429,7 @@ function onBackgroundPointerDown(e: PointerEvent) {
|
|
|
411
429
|
function onResizeHandleDown(e: PointerEvent, tile: Tile, c: Corner) {
|
|
412
430
|
if (tile.resizable === false) return;
|
|
413
431
|
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
|
|
432
|
+
interactionScale = measureInteractionScale(scrollEl.value);
|
|
414
433
|
resize.value = {
|
|
415
434
|
tileId: tile.id,
|
|
416
435
|
corner: c,
|
|
@@ -434,10 +453,18 @@ function onPointerMove(e: PointerEvent) {
|
|
|
434
453
|
const el = scrollEl.value;
|
|
435
454
|
if (el) {
|
|
436
455
|
const rect = el.getBoundingClientRect();
|
|
437
|
-
const x = e.clientX - rect.left + el.scrollLeft;
|
|
438
|
-
const y = e.clientY - rect.top + el.scrollTop;
|
|
439
|
-
const col =
|
|
440
|
-
|
|
456
|
+
const x = (e.clientX - rect.left) / interactionScale.x + el.scrollLeft;
|
|
457
|
+
const y = (e.clientY - rect.top) / interactionScale.y + el.scrollTop;
|
|
458
|
+
const col = clampInteractionCell(
|
|
459
|
+
Math.floor(x / colSize.value),
|
|
460
|
+
config.value.cols,
|
|
461
|
+
config.value.infiniteX ?? config.value.cols === Infinity,
|
|
462
|
+
);
|
|
463
|
+
const row = clampInteractionCell(
|
|
464
|
+
Math.floor(y / rowSize.value),
|
|
465
|
+
config.value.rows,
|
|
466
|
+
config.value.infiniteY ?? config.value.rows === Infinity,
|
|
467
|
+
);
|
|
441
468
|
drawState.value = { ...drawState.value, currentCol: col, currentRow: row };
|
|
442
469
|
}
|
|
443
470
|
return;
|
|
@@ -461,15 +488,21 @@ function onPointerMove(e: PointerEvent) {
|
|
|
461
488
|
const d = drag.value;
|
|
462
489
|
const r = resize.value;
|
|
463
490
|
if (pd) {
|
|
464
|
-
const dx
|
|
465
|
-
|
|
491
|
+
const { dx, dy } = toLocalInteractionDelta(
|
|
492
|
+
e.clientX - pd.startPointerX,
|
|
493
|
+
e.clientY - pd.startPointerY,
|
|
494
|
+
interactionScale,
|
|
495
|
+
);
|
|
466
496
|
const newPinPx = { x: pd.startPinPx.x + dx, y: pd.startPinPx.y + dy };
|
|
467
497
|
const newPin = pixelsToPin(newPinPx, config.value);
|
|
468
498
|
props.api.grid.setTilePinned(pd.tileId, newPin);
|
|
469
499
|
}
|
|
470
500
|
if (gd) {
|
|
471
|
-
const dx
|
|
472
|
-
|
|
501
|
+
const { dx, dy } = toLocalInteractionDelta(
|
|
502
|
+
e.clientX - dragStartPointerX,
|
|
503
|
+
e.clientY - dragStartPointerY,
|
|
504
|
+
interactionScale,
|
|
505
|
+
);
|
|
473
506
|
const dcol = Math.round(dx / colSize.value);
|
|
474
507
|
const drow = Math.round(dy / rowSize.value);
|
|
475
508
|
const result = groupDragController.update({ dcol, drow });
|
|
@@ -483,8 +516,11 @@ function onPointerMove(e: PointerEvent) {
|
|
|
483
516
|
if (result.changed) syncTiles();
|
|
484
517
|
}
|
|
485
518
|
if (d) {
|
|
486
|
-
const dx
|
|
487
|
-
|
|
519
|
+
const { dx, dy } = toLocalInteractionDelta(
|
|
520
|
+
e.clientX - dragStartPointerX,
|
|
521
|
+
e.clientY - dragStartPointerY,
|
|
522
|
+
interactionScale,
|
|
523
|
+
);
|
|
488
524
|
const candidateCol = d.pickupCol + Math.round(dx / colSize.value);
|
|
489
525
|
const candidateRow = d.pickupRow + Math.round(dy / rowSize.value);
|
|
490
526
|
const result = dragController.update({ col: candidateCol, row: candidateRow });
|
|
@@ -501,24 +537,35 @@ function onPointerMove(e: PointerEvent) {
|
|
|
501
537
|
if (result.changed) syncTiles();
|
|
502
538
|
}
|
|
503
539
|
if (r) {
|
|
504
|
-
const dx
|
|
505
|
-
|
|
506
|
-
|
|
540
|
+
const { dx, dy } = toLocalInteractionDelta(
|
|
541
|
+
e.clientX - r.startPointerX,
|
|
542
|
+
e.clientY - r.startPointerY,
|
|
543
|
+
interactionScale,
|
|
544
|
+
);
|
|
507
545
|
const stepsX = Math.round(dx / colSize.value);
|
|
508
546
|
const stepsY = Math.round(dy / rowSize.value);
|
|
509
|
-
if (r.corner === 'se' || r.corner === 'ne') dw = stepsX;
|
|
510
|
-
if (r.corner === 'se' || r.corner === 'sw') dh = stepsY;
|
|
511
|
-
if (r.corner === 'sw' || r.corner === 'nw') { dw = -stepsX; dcol = stepsX; }
|
|
512
|
-
if (r.corner === 'ne' || r.corner === 'nw') { dh = -stepsY; drow = stepsY; }
|
|
513
547
|
const tile = props.api.grid.getTile(r.tileId);
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
548
|
+
const preview = resolveResizePreview({
|
|
549
|
+
corner: r.corner,
|
|
550
|
+
startCol: r.startCol,
|
|
551
|
+
startRow: r.startRow,
|
|
552
|
+
startW: r.startW,
|
|
553
|
+
startH: r.startH,
|
|
554
|
+
stepsX,
|
|
555
|
+
stepsY,
|
|
556
|
+
minW: tile?.minW ?? 1,
|
|
557
|
+
minH: tile?.minH ?? 1,
|
|
558
|
+
maxW: tile?.maxW ?? Infinity,
|
|
559
|
+
maxH: tile?.maxH ?? Infinity,
|
|
560
|
+
cols: config.value.cols,
|
|
561
|
+
rows: config.value.rows,
|
|
562
|
+
infiniteX: config.value.infiniteX ?? config.value.cols === Infinity,
|
|
563
|
+
infiniteY: config.value.infiniteY ?? config.value.rows === Infinity,
|
|
564
|
+
});
|
|
565
|
+
const nW = preview.w;
|
|
566
|
+
const nH = preview.h;
|
|
567
|
+
const nC = preview.col;
|
|
568
|
+
const nR = preview.row;
|
|
522
569
|
if (nW !== r.previewW || nH !== r.previewH || nC !== r.previewCol || nR !== r.previewRow) {
|
|
523
570
|
resize.value = { ...r, previewW: nW, previewH: nH, previewCol: nC, previewRow: nR };
|
|
524
571
|
}
|
package/src/LoopGrid.vue
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
import type { LoopTileInstance } from '@griddle/core';
|
|
81
81
|
import type { GriddleApi } from './useGriddle.js';
|
|
82
82
|
import { animateReposition } from './animation.js';
|
|
83
|
+
import { resolveResizePreview } from './interaction.js';
|
|
83
84
|
|
|
84
85
|
const props = defineProps<{
|
|
85
86
|
api: GriddleApi;
|
|
@@ -359,22 +360,30 @@ function onPointerMove(e: PointerEvent) {
|
|
|
359
360
|
if (r) {
|
|
360
361
|
const dx = e.clientX - r.startPointerX;
|
|
361
362
|
const dy = e.clientY - r.startPointerY;
|
|
362
|
-
let dw = 0, dh = 0, dcol = 0, drow = 0;
|
|
363
363
|
const stepsX = Math.round(dx / colSize.value);
|
|
364
364
|
const stepsY = Math.round(dy / rowSize.value);
|
|
365
|
-
if (r.corner === 'se' || r.corner === 'ne') dw = stepsX;
|
|
366
|
-
if (r.corner === 'se' || r.corner === 'sw') dh = stepsY;
|
|
367
|
-
if (r.corner === 'sw' || r.corner === 'nw') { dw = -stepsX; dcol = stepsX; }
|
|
368
|
-
if (r.corner === 'ne' || r.corner === 'nw') { dh = -stepsY; drow = stepsY; }
|
|
369
365
|
const tile = props.api.grid.getTile(r.tileId);
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
366
|
+
const preview = resolveResizePreview({
|
|
367
|
+
corner: r.corner,
|
|
368
|
+
startCol: r.startCol,
|
|
369
|
+
startRow: r.startRow,
|
|
370
|
+
startW: r.startW,
|
|
371
|
+
startH: r.startH,
|
|
372
|
+
stepsX,
|
|
373
|
+
stepsY,
|
|
374
|
+
minW: tile?.minW ?? 1,
|
|
375
|
+
minH: tile?.minH ?? 1,
|
|
376
|
+
maxW: tile?.maxW ?? Infinity,
|
|
377
|
+
maxH: tile?.maxH ?? Infinity,
|
|
378
|
+
cols: config.value.cols,
|
|
379
|
+
rows: config.value.rows,
|
|
380
|
+
infiniteX: config.value.infiniteX ?? config.value.cols === Infinity,
|
|
381
|
+
infiniteY: config.value.infiniteY ?? config.value.rows === Infinity,
|
|
382
|
+
});
|
|
383
|
+
const nW = preview.w;
|
|
384
|
+
const nH = preview.h;
|
|
385
|
+
const nC = preview.col;
|
|
386
|
+
const nR = preview.row;
|
|
378
387
|
if (nW !== r.previewW || nH !== r.previewH || nC !== r.previewCol || nR !== r.previewRow) {
|
|
379
388
|
resize.value = { ...r, previewW: nW, previewH: nH, previewCol: nC, previewRow: nR };
|
|
380
389
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export interface InteractionScale {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ResizePreviewInput {
|
|
7
|
+
corner: 'nw' | 'ne' | 'sw' | 'se';
|
|
8
|
+
startCol: number;
|
|
9
|
+
startRow: number;
|
|
10
|
+
startW: number;
|
|
11
|
+
startH: number;
|
|
12
|
+
stepsX: number;
|
|
13
|
+
stepsY: number;
|
|
14
|
+
minW: number;
|
|
15
|
+
minH: number;
|
|
16
|
+
maxW: number;
|
|
17
|
+
maxH: number;
|
|
18
|
+
cols: number;
|
|
19
|
+
rows: number;
|
|
20
|
+
infiniteX: boolean;
|
|
21
|
+
infiniteY: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResizePreview {
|
|
25
|
+
col: number;
|
|
26
|
+
row: number;
|
|
27
|
+
w: number;
|
|
28
|
+
h: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type MeasurableElement = Pick<
|
|
32
|
+
HTMLElement,
|
|
33
|
+
'getBoundingClientRect' | 'offsetWidth' | 'offsetHeight'
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
function validScale(value: number): number | null {
|
|
37
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Measure how CSS transforms map an element's local coordinate space into
|
|
42
|
+
* viewport pixels. PointerEvent.clientX/clientY are viewport coordinates,
|
|
43
|
+
* while Griddle's cells and inline transforms use the element's local CSS
|
|
44
|
+
* pixels, so drag math must cross this boundary explicitly.
|
|
45
|
+
*/
|
|
46
|
+
export function measureInteractionScale(
|
|
47
|
+
element: MeasurableElement | null,
|
|
48
|
+
): InteractionScale {
|
|
49
|
+
if (!element) return { x: 1, y: 1 };
|
|
50
|
+
|
|
51
|
+
const rect = element.getBoundingClientRect();
|
|
52
|
+
const measuredX = validScale(
|
|
53
|
+
element.offsetWidth > 0 ? rect.width / element.offsetWidth : NaN,
|
|
54
|
+
);
|
|
55
|
+
const measuredY = validScale(
|
|
56
|
+
element.offsetHeight > 0 ? rect.height / element.offsetHeight : NaN,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// A content-sized grid can temporarily report a zero height while mounting.
|
|
60
|
+
// Prefer the other measured axis before falling back to an unscaled value;
|
|
61
|
+
// this also handles the common uniform transform: scale(...) host.
|
|
62
|
+
return {
|
|
63
|
+
x: measuredX ?? measuredY ?? 1,
|
|
64
|
+
y: measuredY ?? measuredX ?? 1,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Convert a viewport-space pointer delta into the grid's local CSS pixels. */
|
|
69
|
+
export function toLocalInteractionDelta(
|
|
70
|
+
dx: number,
|
|
71
|
+
dy: number,
|
|
72
|
+
scale: InteractionScale,
|
|
73
|
+
): { dx: number; dy: number } {
|
|
74
|
+
return {
|
|
75
|
+
dx: dx / scale.x,
|
|
76
|
+
dy: dy / scale.y,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function clamp(value: number, lower: number, upper: number): number {
|
|
81
|
+
if (upper < lower) return upper;
|
|
82
|
+
return Math.min(upper, Math.max(lower, value));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve a corner resize while preserving the opposite edges and trimming
|
|
87
|
+
* the dragged edges at finite grid bounds.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveResizePreview({
|
|
90
|
+
corner,
|
|
91
|
+
startCol,
|
|
92
|
+
startRow,
|
|
93
|
+
startW,
|
|
94
|
+
startH,
|
|
95
|
+
stepsX,
|
|
96
|
+
stepsY,
|
|
97
|
+
minW,
|
|
98
|
+
minH,
|
|
99
|
+
maxW,
|
|
100
|
+
maxH,
|
|
101
|
+
cols,
|
|
102
|
+
rows,
|
|
103
|
+
infiniteX,
|
|
104
|
+
infiniteY,
|
|
105
|
+
}: ResizePreviewInput): ResizePreview {
|
|
106
|
+
const startRight = startCol + startW;
|
|
107
|
+
const startBottom = startRow + startH;
|
|
108
|
+
const east = corner === 'ne' || corner === 'se';
|
|
109
|
+
const south = corner === 'se' || corner === 'sw';
|
|
110
|
+
|
|
111
|
+
let col = startCol;
|
|
112
|
+
let right = startRight;
|
|
113
|
+
if (east) {
|
|
114
|
+
const maxRight = Math.min(
|
|
115
|
+
startCol + maxW,
|
|
116
|
+
infiniteX ? Infinity : cols,
|
|
117
|
+
);
|
|
118
|
+
right = clamp(startRight + stepsX, startCol + minW, maxRight);
|
|
119
|
+
} else {
|
|
120
|
+
const minLeft = Math.max(startRight - maxW, 0);
|
|
121
|
+
col = clamp(startCol + stepsX, minLeft, startRight - minW);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let row = startRow;
|
|
125
|
+
let bottom = startBottom;
|
|
126
|
+
if (south) {
|
|
127
|
+
const maxBottom = Math.min(
|
|
128
|
+
startRow + maxH,
|
|
129
|
+
infiniteY ? Infinity : rows,
|
|
130
|
+
);
|
|
131
|
+
bottom = clamp(startBottom + stepsY, startRow + minH, maxBottom);
|
|
132
|
+
} else {
|
|
133
|
+
const minTop = Math.max(startBottom - maxH, 0);
|
|
134
|
+
row = clamp(startRow + stepsY, minTop, startBottom - minH);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
col,
|
|
139
|
+
row,
|
|
140
|
+
w: right - col,
|
|
141
|
+
h: bottom - row,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Clamp a pointer-derived cell index to the cells addressable by the grid. */
|
|
146
|
+
export function clampInteractionCell(
|
|
147
|
+
cell: number,
|
|
148
|
+
extent: number,
|
|
149
|
+
infinite: boolean,
|
|
150
|
+
): number {
|
|
151
|
+
const nonNegative = Math.max(0, cell);
|
|
152
|
+
return infinite ? nonNegative : Math.min(extent - 1, nonNegative);
|
|
153
|
+
}
|