Drag and drop
Nam eget elit vel felis aliquam dapibus. Nulla fermentum ultricies auctor. Nunc sit amet purus tempus, rhoncus erat in, pulvinar magna. Nulla pellentesque, odio ac semper pretium, ex nisi ullamcorper justo, quis ultrices libero felis id felis. Cras eget dapibus erat, pharetra fringilla sem. Phasellus nec lobortis nisi.
DraggableGrid.jsx
"use client";
import React, { useState, useRef } from "react";
export default function DraggableGrid(props) {
const { data, classes, DragIcon } = props;
const [gridItems, setGridItems] = useState(data);
const [draggedItemId, setDraggedItemId] = useState(null);
const [draggedTargetId, setDraggedTargetId] = useState(null);
const currentDragEnterTarget = useRef(null);
const newGridItems = useRef(null);
const dragged = useRef(null);
const translateData = useRef({});
const duration = 400;
let start;
const firstFrame = (timeStamp) => {
start = timeStamp;
transformAnimationStep(timeStamp);
};
const transformAnimationStep = (timeStamp) => {
let elapsed = timeStamp - start;
// Use linear interpolation to calculate the positions of the elements
// over the elapsed time.
const lerp = (startValue, endValue, t = elapsed / duration) => {
return startValue + (endValue - startValue) * t;
};
const smoothStep = (t) => t * t * (3 - 2 * t);
let draggedX = lerp(
translateData.current.draggedX,
translateData.current.targetX,
);
let draggedY = lerp(
translateData.current.draggedY,
translateData.current.targetY,
);
let targetX = lerp(
translateData.current.targetX,
translateData.current.draggedX,
);
let targetY = lerp(
translateData.current.targetY,
translateData.current.draggedY,
);
let t = smoothStep(elapsed / duration);
// Trying to resolve relative displacements from the initial positions by
// setting the startValue to 0, without this the transformations seem to
// start 'outward' from the initial positions.
let targetXDelta = lerp(0, targetX - draggedX, t);
let targetYDelta = lerp(0, targetY - draggedY, t);
let draggedXDelta = lerp(0, draggedX - targetX, t);
let draggedYDelta = lerp(0, draggedY - targetY, t);
currentDragEnterTarget.current.style.transform = `translate3d(${targetXDelta}px, ${targetYDelta}px, 0)`;
dragged.current.style.transform = `translate3d(${draggedXDelta}px, ${draggedYDelta}px, 0)`;
if (elapsed < duration) {
// The animation hasn't finished, ask for a new frame.
requestAnimationFrame((timeStamp) =>
transformAnimationStep(timeStamp),
);
} else {
// The absolute value of the delta + the linear interpolation should
// be equal to the target value, request a new frame to remove the
// transformations and commit the new gridItems.
requestAnimationFrame(() => {
currentDragEnterTarget.current.style.transform = "";
dragged.current.style.transform = "";
});
commitNewGridItems();
}
};
const findWrapper = (el, className) => {
while (el) {
if (el.classList.contains(className)) {
break;
}
el = el.parentNode;
}
return el;
};
const onDragEnter = (e, draggedEnterItem) => {
// It's necessary to store the current target in a ref, so the
// current 'dragenter' target is accessible in 'dragleave'.
// useState can't be used as a) this isn't needed for rendering and
// b) a re-render doesn't occur so currentDragEnterTarget would be
// a step behind when we need it.
currentDragEnterTarget.current = findWrapper(e.target, "draggable-wrapper");
// If we 'enter' the same item we're dragging, return early as we don't
// want to apply any visual changes to it.
if (draggedEnterItem.id === draggedItemId) {
return;
}
setDraggedTargetId(draggedEnterItem.id);
};
const onDragStart = (e, draggedItem) => {
e.dataTransfer.setData("draggedItemId", draggedItem.id);
let parentNode = e.target.parentNode;
dragged.current = parentNode;
let xOffset = parentNode.clientWidth - 20;
let yOffset = 25;
// The current target is the svg wrapper element, so set the
// image to the parent node which is what were actually dragging.
e.dataTransfer.setDragImage(parentNode, xOffset, yOffset);
setDraggedItemId(draggedItem.id);
};
const onDrop = (e, droppedItem) => {
let draggedItemId = e.dataTransfer.getData("draggedItemId");
let draggedItemIndex = gridItems.findIndex(
(item) => item.id === draggedItemId,
);
let droppedItemIndex = gridItems.findIndex(
(item) => item.id === droppedItem.id,
);
// Now that we know the indexes of the dragged and dropped items, copy
// gridItems and swap the dragged and dropped items position in the
// copied gridItems.
newGridItems.current = [...gridItems];
newGridItems.current[draggedItemIndex] = gridItems[droppedItemIndex];
newGridItems.current[droppedItemIndex] = gridItems[draggedItemIndex];
setAxis();
requestAnimationFrame(firstFrame);
};
const commitNewGridItems = () => {
setGridItems(newGridItems.current);
setDraggedItemId(null);
setDraggedTargetId(null);
newGridItems.current = null;
};
const setAxis = () => {
translateData.current.draggedRect = dragged.current.getBoundingClientRect();
translateData.current.targetRect =
currentDragEnterTarget.current.getBoundingClientRect();
translateData.current.draggedX =
translateData.current.draggedRect.left + window.scrollX;
translateData.current.draggedY =
translateData.current.draggedRect.top + window.scrollY;
translateData.current.targetX =
translateData.current.targetRect.left + window.scrollX;
translateData.current.targetY =
translateData.current.targetRect.top + window.scrollY;
};
const onDragLeave = (e, item) => {
let relatedIsDescendant = e.relatedTarget.parentNode.id == item.id;
let targetisDescendant = e.target.parentNode.id == item.id;
if (relatedIsDescendant || targetisDescendant) {
// We're dragging over the same drop candidate, so return early.
// This is necessary because the dragleave event is fired when
// dragging over a child element of the candidate.
return;
}
if (
currentDragEnterTarget.current.nodeName == "svg" ||
currentDragEnterTarget.current.nodeName == "path"
) {
// The drag icon is absolute and sits inside the drop target, so
// if we're dragging over the icon return early as we haven't left
// the drop candidates container yet.
return;
}
setDraggedTargetId(null);
};
return (
<div className="grid items-start gap-5 sm:grid-cols-2 md:grid-cols-3">
{gridItems.map((item) => (
<div key={item.id} className="draggable-wrapper relative">
<div
id={item.id}
onDragOver={(e) => e.preventDefault()}
onDragEnter={(e) => onDragEnter(e, item)}
onDragLeave={(e) => onDragLeave(e, item)}
onDrop={(e) => onDrop(e, item)}
className={`draggable ${classes}${draggedTargetId == item.id ? " dragging-target" : ""}`}
dangerouslySetInnerHTML={{ __html: item.html }}
></div>
<DragIcon
draggable
onDragStart={(e) => onDragStart(e, item)}
onDragOver={(e) => e.preventDefault()}
onDragEnter={(e) => onDragEnter(e, item)}
onDragLeave={(e) => onDragLeave(e, item)}
onDrop={(e) => onDrop(e, item)}
className="absolute right-[10px] top-[15px] text-stone-400 hover:cursor-pointer"
/>
</div>
))}
</div>
);
}