Drag and drop element in a list
In this example, we will create a sortable list whose items can be dragged and dropped inside it:
<div id="list">
<div class="draggable">A</div>
<div class="draggable">B</div>
<div class="draggable">C</div>
<div class="draggable">D</div>
<div class="draggable">E</div>
</div>
Each item has class of draggable
indicating that user can drag it:
.draggable {
cursor: move;
user-select: none;
}
Make items draggable
By using the similar approach mentioned in the
Make a draggable element post, we can turn each item into a draggable element:
let draggingEle;
let x = 0;
let y = 0;
const mouseDownHandler = function (e) {
draggingEle = e.target;
const rect = draggingEle.getBoundingClientRect();
x = e.pageX - rect.left;
y = e.pageY - rect.top;
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
const mouseMoveHandler = function (e) {
draggingEle.style.position = 'absolute';
draggingEle.style.top = `${e.pageY - y}px`;
draggingEle.style.left = `${e.pageX - x}px`;
};
The mouseup
event handler will remove the position styles of dragging item and cleans up the event handlers:
const mouseUpHandler = function () {
draggingEle.style.removeProperty('top');
draggingEle.style.removeProperty('left');
draggingEle.style.removeProperty('position');
x = null;
y = null;
draggingEle = null;
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
Now we can attach the
mousedown
event to each item by
looping over the list of items:
const list = document.getElementById('list');
[].slice.call(list.querySelectorAll('.draggable')).forEach(function (item) {
item.addEventListener('mousedown', mouseDownHandler);
});
Add a placeholder
Let's take a look at the list of items again:
When we drag an item,
C
for example, the next item (
D
) will move up to the top and takes the area of the dragging element (
C
).
To fix that, we create a dynamic placeholder element and
insert it right before the dragging element.
The height of placeholder must be the same as dragging element.
The placeholder is created once during the mouse moving, so we add a new flag isDraggingStarted
to track it:
let placeholder;
let isDraggingStarted = false;
const mouseMoveHandler = function(e) {
const draggingRect = draggingEle.getBoundingClientRect();
if (!isDraggingStarted) {
isDraggingStarted = true;
placeholder = document.createElement('div');
placeholder.classList.add('placeholder');
draggingEle.parentNode.insertBefore(
placeholder,
draggingEle.nextSibling
);
placeholder.style.height = `${draggingRect.height}px`;
}
...
}
The placeholder will be
removed as soon as the users drop the item:
const mouseUpHandler = function() {
placeholder && placeholder.parentNode.removeChild(placeholder);
isDraggingStarted = false;
...
};
Here is the order of element when user drags and moves an item around:
A B placeholder <- The dynamic placeholder C <- The dragging item D E
Determine if user moves item up or down
First of all, we need a helper function to check if an item is above or below another one.
A nodeA
is treated as above of nodeB
if the horizontal center point of nodeA
is less than nodeB
.
The center point of a node can be calculated by taking the sum of its top and half of its height:
const isAbove = function (nodeA, nodeB) {
const rectA = nodeA.getBoundingClientRect();
const rectB = nodeB.getBoundingClientRect();
return rectA.top + rectA.height / 2 < rectB.top + rectB.height / 2;
};
As user moves the item around, we define the previous and next
sibling items:
const mouseMoveHandler = function (e) {
const prevEle = draggingEle.previousElementSibling;
const nextEle = placeholder.nextElementSibling;
};
If user moves the item to the top, we will swap the placeholder and the previous item:
const mouseMoveHandler = function(e) {
...
if (prevEle && isAbove(draggingEle, prevEle)) {
swap(placeholder, draggingEle);
swap(placeholder, prevEle);
return;
}
};
Similarly, we will swap the next and dragging item if we detect that user moves item down to the bottom:
const mouseMoveHandler = function(e) {
...
if (nextEle && isAbove(nextEle, draggingEle)) {
swap(nextEle, placeholder);
swap(nextEle, draggingEle);
}
};
const swap = function (nodeA, nodeB) {
const parentA = nodeA.parentNode;
const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;
nodeB.parentNode.insertBefore(nodeA, nodeB);
parentA.insertBefore(nodeB, siblingA);
};
Following is the final demo. Try to drag and drop any item!
Demo
Drag and drop element in a list
See also