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