Управление DOM с помощью JavaScript в современных браузерах и IE 11+
В этом примере мы создадим сортируемый список, элементы которого можно перетаскивать в него:
<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>
Каждый элемент имеет класс draggable, указывающий, что пользователь может перетащить его:
.draggable {
cursor: move;
user-select: none;
}
Используя аналогичный подход, упомянутый в публикации Сделать перетаскиваемый элемент, мы можем превратить каждый элемент в перетаскиваемый элемент:
// The current dragging item
let draggingEle;
// The current position of mouse relative to the dragging element
let x = 0;
let y = 0;
const mouseDownHandler = function(e) {
const draggingEle = e.target;
// Calculate the mouse position
const rect = draggingEle.getBoundingClientRect();
x = e.pageX - rect.left;
y = e.pageY - rect.top;
// Attach the listeners to `document`
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
const mouseMoveHandler = function(e) {
// Set position for dragging element
draggingEle.style.position = 'absolute';
draggingEle.style.top = `${e.pageY - y}px`;
draggingEle.style.left = `${e.pageX - x}px`;
};
Обработчик события mouseup будет удалить стили позиционных перетаскиваний элемента и очищают обработчик событий:
const mouseUpHandler = function() {
// Remove the position styles
draggingEle.style.removeProperty('top');
draggingEle.style.removeProperty('left');
draggingEle.style.removeProperty('position');
x = null;
y = null;
draggingEle = null;
// Remove the handlers of `mousemove` and `mouseup`
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
Теперь мы можем прикрепить событие mousedown к каждому элементу, просматривая список элементов:
// Query the list element
const list = document.getElementById('list');
// Query all items
[].slice.call(list.querySelectorAll('.draggable')).forEach(function(item) {
item.addEventListener('mousedown', mouseDownHandler);
});
Давайте еще раз посмотрим на список предметов:
A
B
C
D
E
Например, когда мы перетаскиваем элемент C, следующий элемент (D) перемещается вверх и занимает область перетаскиваемого элемента (C). Чтобы это исправить, мы создаем динамический элемент-заполнитель и вставляем его прямо перед перетаскиваемым элементом. Высота заполнителя должна быть такой же, как у перетаскиваемого элемента.
Заполнитель создается один раз при перемещении мыши, поэтому мы добавляем новый флаг isDraggingStarted для его отслеживания:
let placeholder;
let isDraggingStarted = false;
const mouseMoveHandler = function(e) {
const draggingRect = draggingEle.getBoundingClientRect();
if (!isDraggingStarted) {
// Update the flag
isDraggingStarted = true;
// Let the placeholder take the height of dragging element
// So the next element won't move up
placeholder = document.createElement('div');
placeholder.classList.add('placeholder');
draggingEle.parentNode.insertBefore(
placeholder,
draggingEle.nextSibling
);
// Set the placeholder's height
placeholder.style.height = `${draggingRect.height}px`;
}
...
}
Заполнитель будет удален, как только пользователи уронят элемент:
const mouseUpHandler = function() {
// Remove the placeholder
placeholder && placeholder.parentNode.removeChild(placeholder);
...
};
Вот порядок элементов, когда пользователь перетаскивает элемент:
A
B
placeholder <- The dynamic placeholder
C <- The dragging item
D
E
Прежде всего, нам нужна вспомогательная функция, чтобы проверить, находится ли элемент выше или ниже другого.
nodeA обрабатывается, как указано выше, nodeB если горизонтальная центральная точка nodeA меньше, чем nodeB. Центральную точку узла можно рассчитать, взяв сумму его вершины и половины его высоты:
const isAbove = function(nodeA, nodeB) {
// Get the bounding rectangle of nodes
const rectA = nodeA.getBoundingClientRect();
const rectB = nodeB.getBoundingClientRect();
return (rectA.top + rectA.height / 2 < rectB.top + rectB.height / 2);
};
Когда пользователь перемещает элемент, мы определяем предыдущий и следующий элементы родственного элемента:
const mouseMoveHandler = function(e) {
// The current order:
// prevEle
// draggingEle
// placeholder
// nextEle
const prevEle = draggingEle.previousElementSibling;
const nextEle = placeholder.nextElementSibling;
};
Если пользователь переместит элемент в начало, мы поменяем местами заполнитель и предыдущий элемент:
const mouseMoveHandler = function(e) {
...
// User moves item to the top
if (prevEle && isAbove(draggingEle, prevEle)) {
// The current order -> The new order
// prevEle -> placeholder
// draggingEle -> draggingEle
// placeholder -> prevEle
swap(placeholder, draggingEle);
swap(placeholder, prevEle);
return;
}
};
Точно так же мы поменяем следующий и перетаскивающий элемент, если обнаружим, что пользователь перемещает элемент вниз:
const mouseMoveHandler = function(e) {
...
// User moves the dragging element to the bottom
if (nextEle && isAbove(nextEle, draggingEle)) {
// The current order -> The new order
// draggingEle -> nextEle
// placeholder -> placeholder
// nextEle -> draggingEle
swap(nextEle, placeholder);
swap(nextEle, draggingEle);
}
};
Вот небольшая функция swap для замены двух узлов:
const swap = function(nodeA, nodeB) {
const parentA = nodeA.parentNode;
const siblingA = nodeA.nextSibling === nodeB ? nodeA : nodeA.nextSibling;
// Move `nodeA` to before the `nodeB`
nodeB.parentNode.insertBefore(nodeA, nodeB);
// Move `nodeB` to before the sibling of `nodeA`
parentA.insertBefore(nodeB, siblingA);
};