fix: free-form drag positioning instead of swap-only
Widgets can now be dragged anywhere on the grid, not just swapped. Drop position is calculated from mouse coordinates relative to the grid, snapped to grid units. Overlapping widgets are pushed down automatically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -235,7 +235,7 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string
|
||||
document.addEventListener("mouseup", onUp);
|
||||
};
|
||||
|
||||
// Drag: HTML5 DnD
|
||||
// Drag: free-form grid positioning
|
||||
const handleDragStart = (e: React.DragEvent, widgetId: string) => {
|
||||
setDraggingId(widgetId);
|
||||
e.dataTransfer.setData("text/plain", widgetId);
|
||||
@@ -247,29 +247,65 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
};
|
||||
|
||||
const handleDrop = async (e: React.DragEvent, targetId: string) => {
|
||||
const handleGridDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
const sourceId = e.dataTransfer.getData("text/plain");
|
||||
if (!sourceId || sourceId === targetId) { setDraggingId(null); return; }
|
||||
if (!sourceId) { setDraggingId(null); return; }
|
||||
if (!gridRef.current) { setDraggingId(null); return; }
|
||||
|
||||
const source = widgets.find((w) => w.id === sourceId);
|
||||
const target = widgets.find((w) => w.id === targetId);
|
||||
if (!source || !target) { setDraggingId(null); return; }
|
||||
const gridRect = gridRef.current.getBoundingClientRect();
|
||||
const unitSize = gridRect.width / 12;
|
||||
// Calculate grid position from mouse coordinates
|
||||
const relX = e.clientX - gridRect.left;
|
||||
const relY = e.clientY - gridRect.top;
|
||||
const sourceWidget = widgets.find((w) => w.id === sourceId);
|
||||
if (!sourceWidget) { setDraggingId(null); return; }
|
||||
|
||||
// Swap positions
|
||||
const sourcePos = { ...source.position };
|
||||
const targetPos = { ...target.position };
|
||||
// Snap to nearest grid unit, clamp to bounds
|
||||
const newX = Math.max(0, Math.min(12 - sourceWidget.position.w, Math.round(relX / unitSize)));
|
||||
const newY = Math.max(0, Math.round(relY / (unitSize * 0.6))); // rows are shorter
|
||||
|
||||
setWidgets((prev) =>
|
||||
prev.map((w) => {
|
||||
if (w.id === sourceId) return { ...w, position: targetPos };
|
||||
if (w.id === targetId) return { ...w, position: sourcePos };
|
||||
return w;
|
||||
})
|
||||
);
|
||||
setWidgets((current) => {
|
||||
let resolved = current.map((w) => {
|
||||
if (w.id === sourceId) {
|
||||
return { ...w, position: { ...w.position, x: newX, y: newY } };
|
||||
}
|
||||
return { ...w, position: { ...w.position } };
|
||||
});
|
||||
|
||||
// Push overlapping widgets down (skip the moved widget)
|
||||
for (let pass = 0; pass < 10; pass++) {
|
||||
let hasOverlap = false;
|
||||
for (let i = 0; i < resolved.length; i++) {
|
||||
for (let j = i + 1; j < resolved.length; j++) {
|
||||
const a = resolved[i].position;
|
||||
const b = resolved[j].position;
|
||||
if (a.x + a.w > b.x && a.x < b.x + b.w && a.y + a.h > b.y && a.y < b.y + b.h) {
|
||||
hasOverlap = true;
|
||||
// Push the widget that wasn't just moved (or the lower one)
|
||||
const moveIdx = resolved[i].id === sourceId ? j : i;
|
||||
const fixedIdx = moveIdx === i ? j : i;
|
||||
resolved[moveIdx] = {
|
||||
...resolved[moveIdx],
|
||||
position: { ...resolved[moveIdx].position, y: resolved[fixedIdx].position.y + resolved[fixedIdx].position.h },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasOverlap) break;
|
||||
}
|
||||
|
||||
// Persist
|
||||
for (const w of resolved) {
|
||||
const orig = current.find((o) => o.id === w.id);
|
||||
if (orig && (orig.position.x !== w.position.x || orig.position.y !== w.position.y)) {
|
||||
updateWidget(id, w.id, { position: w.position });
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
});
|
||||
|
||||
await updateWidget(id, sourceId, { position: targetPos });
|
||||
await updateWidget(id, targetId, { position: sourcePos });
|
||||
setDraggingId(null);
|
||||
};
|
||||
|
||||
@@ -412,6 +448,8 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string
|
||||
<div
|
||||
ref={gridRef}
|
||||
className="grid auto-rows-[minmax(100px,auto)] grid-cols-6 gap-3 md:grid-cols-12 md:gap-4"
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleGridDrop}
|
||||
>
|
||||
{widgets.map((widget) => (
|
||||
<div
|
||||
@@ -425,8 +463,6 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string
|
||||
style={widgetGridStyle(widget.position)}
|
||||
draggable={editMode}
|
||||
onDragStart={(e) => editMode && handleDragStart(e, widget.id)}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleDrop(e, widget.id)}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{/* Drag handle */}
|
||||
|
||||
Reference in New Issue
Block a user