Thoughts on Web Dev Simplified's HTML5 drag and drop functionality
When I was learning about HTML5 drag and drop functionality, I came across a video by Kyle at Web Dev Simplified. He has a great way of explaining things and I’ve learned a lot from him over the years. You can see the video of his solution here. However, I found myself struggling to understand one part of his solution, namely the getDragAfterElement function. See the snippet of code below, extracting only the relevant portions (for example, I’ve omitted the container code for brevity and changed the list selector):
const list = document.querySelector('[data-draggable-list"]');
list.querySelectorAll('.draggable').forEach((el) => {
el.addEventListener('dragover', (e) => {
e.preventDefault();
const afterElement = getDragAfterElement(e.clientY);
const draggingEl = list.querySelector('.dragging');
if (afterElement == null) {
list.appendChild(draggingEl);
} else {
list.insertBefore(draggingEl, afterElement);
}
});
});
function getDragAfterElement(mouseY) {
const draggableElements = [...list.querySelectorAll('.draggable:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = mouseY - box.top - box.height / 2;
if(offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}The main part I struggled with was the getDragAfterElement function. The idea is that it finds the closest element to the mouse position in order to determine where to drop the currently dragged element. It does this by checking the position of each draggable element and comparing it to the mouse position. If the mouse is above the middle of an element, it returns that element as the closest one. If not, it continues checking until it finds the closest one.
It’s probably the most succinct way of writing this logic, but when I look at that code my first response is WTF 🤯. I bow to Kyle’s superior code mind; I spent a bit of time trying to understand it, and whilst I understand theoretically what it’s doing, I really struggle to conceptualise it. And in 6 months time when I come back to this code cold, no chance.
So I rewrote it into something I can better conceptualise, using the idea of drop zones:
const list = document.querySelector('[data-draggable-list"]');
let draggingEl = null; // currently dragging element
let lastTarget = null; // last insert target
list.querySelectorAll('[data-draggable]').forEach((el) => {
el.addEventListener('dragover', (e) => {
e.preventDefault();
if (!draggingEl) return;
const target = getInsertTarget(e.clientY);
if (target === lastTarget) return;
animateReorder(list, draggingEl, target);
lastTarget = target;
});
function getInsertTarget(mouseY) {
const allItems = list.querySelectorAll('[data-draggable]');
const items = [...allItems].filter((el) => el !== draggingEl); // Exclude the currently dragging element
for (let i = 0; i < items.length - 1; i++) {
const current = items[i].getBoundingClientRect();
const next = items[i + 1].getBoundingClientRect();
// Define the vertical drop zone between current and next element
const zoneStart = current.top + current.height / 2;
const zoneEnd = next.top + next.height / 2;
// If mouse is inside the drop zone, return next item as the insert target
if (mouseY >= zoneStart && mouseY < zoneEnd) return items[i + 1];
}
// If mouse is before the first item, return first item
const first = items[0]?.getBoundingClientRect();
if (first && mouseY < first.top + first.height / 2) return items[0];
// If mouse is after the last item, return last item
return items[items.length];
}Firstly, I changed the name of the function to getInsertTarget, which I think is more descriptive of what it’s doing.
Next, I’m using a for loop to iterate through the items, which I find easier to understand than the reduce method. The drop zone is defined as the space of the bottom half of the current item and the top half of the next item. If the mouse is within that zone, we return the next item as the insert target. Finally, there are just a couple of checks to see if the mouse is above the first item or below the last item, in which case we return the first or last item respectively.
As mentioned, Kyle’s code is definitely more succinct, but I find it hard to visualise as I read the code. As as I say, in 6 months time when I come back to this cold, no chance. So for me, the few extra lines of code are worth it for the improved clarity.
That said, I don’t like the UX…
Both my code and Kyle’s can be used interchangeably to achieve the same behaviour. However, more generally I’m not a fan of the UX. I think it could be slightly more intuitive with the switch between two items being quicker and more fluid as soon as you drag over another item. Perhaps because in Kyle’s example the elements had much more gap between them than in my example. But regardless, I think its worth exploring my thoughts.
I wrote a whole post on my implementation of vanilla JS HTML5 drag and drop functionality, where I document my solution in more detail. But in short, I think the UX could be improved by adding a small transition when items switch places and to transition as soon as the mouse is over another draggable, not when its passed the center line. See example side by side with Kyle’s below:
Original UX
Improved UX
In the original UX, the items only switch places when the mouse is over the center line of the next item. This feels a bit ‘sluggish’ to me. In the improved version, the switch is much snappier, as the items switch places as soon as the mouse is over another draggable item, which makes it feel more responsive. I suppose it’s a matter of personal preference, and there really isn’t much in it, but I think mine feels slightly better.
The UX can be enhanced further by adding a small transition effect when the items switch places, so that they animate smoothly rather than jumping. Read my post on using native HTML5 drag and drop functionality for more details on how to implement this.