Управление 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);
};