tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
20
fork
atom
overview
issues
pulls
pipelines
improve dragging
Florian
3 weeks ago
63bfeba4
da7f8e54
+180
-22
3 changed files
expand all
collapse all
unified
split
.claude
settings.local.json
src
lib
helper.ts
website
EditableWebsite.svelte
+7
.claude/settings.local.json
···
1
1
+
{
2
2
+
"permissions": {
3
3
+
"allow": [
4
4
+
"Bash(pnpm check:*)"
5
5
+
]
6
6
+
}
7
7
+
}
+4
-2
src/lib/helper.ts
···
38
38
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
39
39
};
40
40
41
41
-
export function fixCollisions(items: Item[], movedItem: Item, mobile: boolean = false) {
41
41
+
export function fixCollisions(items: Item[], movedItem: Item, mobile: boolean = false, skipCompact: boolean = false) {
42
42
const clampX = (item: Item) => {
43
43
if (mobile) item.mobileX = clamp(item.mobileX, 0, COLUMNS - item.mobileW);
44
44
else item.x = clamp(item.x, 0, COLUMNS - item.w);
···
93
93
else it.x = clamp(it.x, 0, COLUMNS - it.w);
94
94
}
95
95
96
96
-
compactItems(items, mobile);
96
96
+
if (!skipCompact) {
97
97
+
compactItems(items, mobile);
98
98
+
}
97
99
}
98
100
99
101
// Fix all collisions between items (not just one moved item)
+169
-20
src/lib/website/EditableWebsite.svelte
···
66
66
y: number;
67
67
mouseDeltaX: number;
68
68
mouseDeltaY: number;
69
69
+
// For hysteresis - track last decision to prevent flickering
70
70
+
lastTargetId: string | null;
71
71
+
lastPlacement: 'above' | 'below' | null;
72
72
+
// Store original positions to reset from during drag
73
73
+
originalPositions: Map<string, { x: number; y: number; mobileX: number; mobileY: number }>;
69
74
} = $state({
70
75
element: null,
71
76
item: null,
···
74
79
x: -1,
75
80
y: -1,
76
81
mouseDeltaX: 0,
77
77
-
mouseDeltaY: 0
82
82
+
mouseDeltaY: 0,
83
83
+
lastTargetId: null,
84
84
+
lastPlacement: null,
85
85
+
originalPositions: new Map()
78
86
});
79
87
80
88
let showingMobileView = $state(false);
···
249
257
e: DragEvent & {
250
258
currentTarget: EventTarget & HTMLDivElement;
251
259
}
252
252
-
) {
253
253
-
if (!container) return;
260
260
+
): { x: number; y: number; swapWithId: string | null; placement: 'above' | 'below' | null } | undefined {
261
261
+
if (!container || !activeDragElement.item) return;
254
262
263
263
+
// x, y represent the top-left corner of the dragged card
255
264
const x = e.clientX + activeDragElement.mouseDeltaX;
256
265
const y = e.clientY + activeDragElement.mouseDeltaY;
257
266
258
267
const rect = container.getBoundingClientRect();
268
268
+
const currentMargin = isMobile ? mobileMargin : margin;
269
269
+
const cellSize = (rect.width - currentMargin * 2) / COLUMNS;
259
270
260
260
-
debugPoint.x = x - rect.left;
261
261
-
debugPoint.y = y - rect.top + margin;
262
262
-
console.log(rect.top);
271
271
+
// Get card dimensions based on current view mode
272
272
+
const cardW = isMobile ? (activeDragElement.item?.mobileW ?? activeDragElement.w) : activeDragElement.w;
273
273
+
const cardH = isMobile ? (activeDragElement.item?.mobileH ?? activeDragElement.h) : activeDragElement.h;
263
274
275
275
+
// Get dragged card's original position
276
276
+
const draggedOrigPos = activeDragElement.originalPositions.get(activeDragElement.item.id);
277
277
+
const draggedOrigX = draggedOrigPos ? (isMobile ? draggedOrigPos.mobileX : draggedOrigPos.x) : 0;
278
278
+
const draggedOrigY = draggedOrigPos ? (isMobile ? draggedOrigPos.mobileY : draggedOrigPos.y) : 0;
279
279
+
280
280
+
// Calculate raw grid position based on top-left of dragged card
264
281
let gridX = clamp(
265
265
-
Math.floor(((x - rect.left) / rect.width) * 8),
282
282
+
Math.round((x - rect.left - currentMargin) / cellSize),
266
283
0,
267
267
-
COLUMNS - (activeDragElement.w ?? 0)
284
284
+
COLUMNS - cardW
268
285
);
269
286
gridX = Math.floor(gridX / 2) * 2;
287
287
+
270
288
let gridY = Math.max(
271
271
-
Math.round(((y - rect.top + margin) / (rect.width - margin)) * COLUMNS),
289
289
+
Math.round((y - rect.top - currentMargin) / cellSize),
272
290
0
273
291
);
292
292
+
274
293
if (isMobile) {
275
294
gridX = Math.floor(gridX / 2) * 2;
276
295
gridY = Math.floor(gridY / 2) * 2;
277
296
}
278
278
-
return { x: gridX, y: gridY };
297
297
+
298
298
+
// Find if we're hovering over another card (using ORIGINAL positions)
299
299
+
const centerGridY = gridY + cardH / 2;
300
300
+
const centerGridX = gridX + cardW / 2;
301
301
+
302
302
+
let swapWithId: string | null = null;
303
303
+
let placement: 'above' | 'below' | null = null;
304
304
+
305
305
+
for (const other of items) {
306
306
+
if (other === activeDragElement.item) continue;
307
307
+
308
308
+
// Use original positions for hit testing
309
309
+
const origPos = activeDragElement.originalPositions.get(other.id);
310
310
+
if (!origPos) continue;
311
311
+
312
312
+
const otherX = isMobile ? origPos.mobileX : origPos.x;
313
313
+
const otherY = isMobile ? origPos.mobileY : origPos.y;
314
314
+
const otherW = isMobile ? other.mobileW : other.w;
315
315
+
const otherH = isMobile ? other.mobileH : other.h;
316
316
+
317
317
+
// Check if dragged card's center point is within this card's original bounds
318
318
+
if (centerGridX >= otherX && centerGridX < otherX + otherW &&
319
319
+
centerGridY >= otherY && centerGridY < otherY + otherH) {
320
320
+
321
321
+
// Check if this is a swap situation:
322
322
+
// Cards have the same dimensions and are on the same row
323
323
+
const canSwap = cardW === otherW && cardH === otherH && draggedOrigY === otherY;
324
324
+
325
325
+
if (canSwap) {
326
326
+
// Swap positions
327
327
+
swapWithId = other.id;
328
328
+
gridX = otherX;
329
329
+
gridY = otherY;
330
330
+
placement = null;
331
331
+
332
332
+
activeDragElement.lastTargetId = other.id;
333
333
+
activeDragElement.lastPlacement = null;
334
334
+
} else {
335
335
+
// Vertical placement (above/below)
336
336
+
// Detect drag direction: if dragging up, always place above
337
337
+
const isDraggingUp = gridY < draggedOrigY;
338
338
+
339
339
+
if (isDraggingUp) {
340
340
+
// When dragging up, always place above
341
341
+
placement = 'above';
342
342
+
} else {
343
343
+
// When dragging down, use top/bottom half logic
344
344
+
const midpointY = otherY + otherH / 2;
345
345
+
const hysteresis = 0.3;
346
346
+
347
347
+
if (activeDragElement.lastTargetId === other.id && activeDragElement.lastPlacement) {
348
348
+
if (activeDragElement.lastPlacement === 'above') {
349
349
+
placement = centerGridY > midpointY + hysteresis ? 'below' : 'above';
350
350
+
} else {
351
351
+
placement = centerGridY < midpointY - hysteresis ? 'above' : 'below';
352
352
+
}
353
353
+
} else {
354
354
+
placement = centerGridY < midpointY ? 'above' : 'below';
355
355
+
}
356
356
+
}
357
357
+
358
358
+
activeDragElement.lastTargetId = other.id;
359
359
+
activeDragElement.lastPlacement = placement;
360
360
+
361
361
+
if (placement === 'above') {
362
362
+
gridY = otherY;
363
363
+
} else {
364
364
+
gridY = otherY + otherH;
365
365
+
}
366
366
+
}
367
367
+
break;
368
368
+
}
369
369
+
}
370
370
+
371
371
+
// If we're not over any card, clear the tracking
372
372
+
if (!swapWithId && !placement) {
373
373
+
activeDragElement.lastTargetId = null;
374
374
+
activeDragElement.lastPlacement = null;
375
375
+
}
376
376
+
377
377
+
debugPoint.x = x - rect.left;
378
378
+
debugPoint.y = y - rect.top + currentMargin;
379
379
+
380
380
+
return { x: gridX, y: gridY, swapWithId, placement };
279
381
}
280
382
281
383
let linkValue = $state('');
···
402
504
ondragover={(e) => {
403
505
e.preventDefault();
404
506
405
405
-
const cell = getDragXY(e);
406
406
-
if (!cell) return;
507
507
+
const result = getDragXY(e);
508
508
+
if (!result) return;
407
509
408
408
-
activeDragElement.x = cell.x;
409
409
-
activeDragElement.y = cell.y;
510
510
+
activeDragElement.x = result.x;
511
511
+
activeDragElement.y = result.y;
410
512
411
513
if (activeDragElement.item) {
514
514
+
// Get dragged card's original position for swapping
515
515
+
const draggedOrigPos = activeDragElement.originalPositions.get(activeDragElement.item.id);
516
516
+
517
517
+
// Reset all items to original positions first
518
518
+
for (const it of items) {
519
519
+
const origPos = activeDragElement.originalPositions.get(it.id);
520
520
+
if (origPos && it !== activeDragElement.item) {
521
521
+
if (isMobile) {
522
522
+
it.mobileX = origPos.mobileX;
523
523
+
it.mobileY = origPos.mobileY;
524
524
+
} else {
525
525
+
it.x = origPos.x;
526
526
+
it.y = origPos.y;
527
527
+
}
528
528
+
}
529
529
+
}
530
530
+
531
531
+
// Update dragged item position
412
532
if (isMobile) {
413
413
-
activeDragElement.item.mobileX = cell.x;
414
414
-
activeDragElement.item.mobileY = cell.y;
533
533
+
activeDragElement.item.mobileX = result.x;
534
534
+
activeDragElement.item.mobileY = result.y;
415
535
} else {
416
416
-
activeDragElement.item.x = cell.x;
417
417
-
activeDragElement.item.y = cell.y;
536
536
+
activeDragElement.item.x = result.x;
537
537
+
activeDragElement.item.y = result.y;
538
538
+
}
539
539
+
540
540
+
// Handle horizontal swap
541
541
+
if (result.swapWithId && draggedOrigPos) {
542
542
+
const swapTarget = items.find(it => it.id === result.swapWithId);
543
543
+
if (swapTarget) {
544
544
+
// Move swap target to dragged card's original position
545
545
+
if (isMobile) {
546
546
+
swapTarget.mobileX = draggedOrigPos.mobileX;
547
547
+
swapTarget.mobileY = draggedOrigPos.mobileY;
548
548
+
} else {
549
549
+
swapTarget.x = draggedOrigPos.x;
550
550
+
swapTarget.y = draggedOrigPos.y;
551
551
+
}
552
552
+
}
418
553
}
419
554
555
555
+
// Now fix collisions (with compacting)
420
556
fixCollisions(items, activeDragElement.item, isMobile);
421
557
}
422
558
···
449
585
activeDragElement.item.y = cell.y;
450
586
}
451
587
588
588
+
// Fix collisions and compact items after drag ends
452
589
fixCollisions(items, activeDragElement.item, isMobile);
453
590
}
454
591
activeDragElement.x = -1;
455
592
activeDragElement.y = -1;
456
593
activeDragElement.element = null;
594
594
+
activeDragElement.item = null;
595
595
+
activeDragElement.lastTargetId = null;
596
596
+
activeDragElement.lastPlacement = null;
457
597
return true;
458
598
}}
459
599
class="@container/grid relative col-span-3 px-2 py-8 @5xl/wrapper:px-8"
···
484
624
activeDragElement.h = item.h;
485
625
activeDragElement.item = item;
486
626
627
627
+
// Store original positions of all items
628
628
+
activeDragElement.originalPositions = new Map();
629
629
+
for (const it of items) {
630
630
+
activeDragElement.originalPositions.set(it.id, {
631
631
+
x: it.x,
632
632
+
y: it.y,
633
633
+
mobileX: it.mobileX,
634
634
+
mobileY: it.mobileY
635
635
+
});
636
636
+
}
637
637
+
487
638
const rect = target.getBoundingClientRect();
488
639
activeDragElement.mouseDeltaX = rect.left - e.clientX;
489
640
activeDragElement.mouseDeltaY = rect.top - e.clientY;
490
490
-
console.log(activeDragElement.mouseDeltaY);
491
491
-
console.log(rect.width);
492
641
}}
493
642
>
494
643
<EditingCard bind:item={items[i]} />