В этой статье расскажу о том, как менять местами элементы <div> на странице (наверно, это можно назвать drag-and-drop). Долго откладывал эту фичу, которую хотел добавить в свой пет-проект, но оказалось, что зря — реализация оказалась очень простой.
Описание страницы
Для начала опишу страницу, с которой буду работать, так как всегда предпочитаю показывать практические, а не абстрактные примеры.
У меня имеется 40 строк с полями для заполнения, но если при вводе допущена ошибка с порядком записей, то нужно легко её исправить перетаскиванием строки на правильное место.
<form method="POST" action="/chartadd">
<div id="songs">
<c:forEach begin="1" end="40" var="val">
<div class="song-row">
<div style="display:none;"><input name="idSong[]" type="text" /></div>
<div class="flex1" name="num"><c:out value="${val}"/></div>
<div class="flex9"><input style="width:100%;" name="artists[]" type="text" /></div>
<div style="text-align:center; width: 30px;"> - </div>
<div class="flex9"><input style="width:100%;" name="name[]" type="text" /></div>
</div>
</c:forEach>
</div>
<div style="text-align: center;">
<input style="margin-top:10px;" type="submit" value="Сохранить" />
</div>
</div>
</form>
Здесь я в цикле создаю сорок строчек в форме, а внизу размещена кнопка Сохранить. Нас будут интересовать <div> с id songs, как контейнер, внутри которого мы и будем перемещать другие дивы с классом song-row, которые мы будем менять местами.

Перетаскивание элементов на чистом JavaScript
Чтобы перетаскивание работало, добавляем в <div>, которые хотим перетаскивать и менять местами draggable=»true»:
<form method="POST" action="/chartadd">
<div id="songs">
<c:forEach begin="1" end="40" var="val">
<div class="song-row draggable" draggable="true">
<...>
</div>
</c:forEach>
</div>
<div style="text-align: center;">
<input style="margin-top:10px;" type="submit" value="Сохранить" />
</div>
</div>
</form>
Я также добавил в них класс draggable, чтобы было легче с ними работать и можно было добавлять стили.
Скрипт для этой страницы будет выглядеть следующим образом:
<script>
const draggables = document.querySelectorAll('.draggable');
const container = document.getElementById('songs');
let draggedItem = null;
draggables.forEach(item => {
item.addEventListener('dragstart', () => {
draggedItem = item;
});
item.addEventListener('dragover', e => {
e.preventDefault();
});
item.addEventListener('drop', () => {
if (item !== draggedItem) {
container.insertBefore(draggedItem, item.nextSibling);
}
});
});
</script>
В этом коде первом делом находим контейнер songs и все элементы, которые можем перемещать, чтобы потом для каждого из них в цикле forEach добавить слушателей событий. Находим их по классу .draggable.
Переменная draggedItem хранит элемент, который мы перемещаем. Изначально значение null, но при вызове события dragstart присваиваем этой переменной новое значение.
При событии drop проверяем, действительно ли мы кидаем поверх элемента другой элемент. Если это тот же элемент, то никаких перемещений делать не нужно. Иначе вызываем метод insertBefore(). Он автоматически убирает элемент с прошлого места перед вставкой в новое. Первый аргумент здесь отвечает за перемещаемый элемент (draggedItem), а второй указывает элемент, перед которым нужно его вставить. Мне нужно вставить div не до, а после (а функции insertAfter() нет), поэтому вставляю его перед следующим сиблингом (item.nextSibling).
Теперь поиграем со стилями. Выделим элемент, после которого будет вставляться наш перетаскиваемый контейнер пунктирной границей и поменяем курсор при наведении на draggable элемент (это может быть move для стрелочек или grab для руки)
<style>
.draggable {
cursor: move;
}
.drag-over {
border: 2px dashed #000;
}
</style>
Класс .draggable и так уже есть у каждой строки в моей таблице, а вот класс .drag-over нужно добавлять при наведении на другой div во время перетаскивания. И убирать, когда курсор с него уходит. Для этого добавим код в событие dragover и создадим ещё одного слушателя ивента dragleave.
<script>
const draggables = document.querySelectorAll('.draggable');
const container = document.getElementById('songs');
let draggedItem = null;
draggables.forEach(item => {
item.addEventListener('dragstart', () => {
draggedItem = item;
});
item.addEventListener('dragover', e => {
e.preventDefault();
if (item !== draggedItem) {
item.classList.add('drag-over');
}
});
item.addEventListener('dragleave', () => {
item.classList.remove('drag-over');
});
item.addEventListener('drop', () => {
if (item !== draggedItem) {
container.insertBefore(draggedItem, item.nextSibling);
}
item.classList.remove('drag-over');
});
});
</script>
Готово.
Меняем элементы местами на jQuery
Я в своём проекте предпочёл использовать jQuery, потому что он компактнее (и потому что я и так использую его для всех других скриптов).
Здесь всё действительно намного проще:
$('#songs').sortable();
$('#songs').disableSelection();
sortable() позволяет сортировать элементы внутри контейнера (у меня он с id songs), и по умолчанию они все становятся draggable, поэтому явно указывать это в коде уже не нужно.
disableSelection() не обязателен, но нужен, чтобы при перетаскивании элемента браузер не выделял текст.
Можно также добавить дополнительные функции, которые вызываются после обновления списка. Например, мне нужно, чтобы нумерация строчек менялась после каждого перетаскивания (номера от 1 до 40 должны оставаться в таком же порядке, несмотря ни на что). За обновление нумерации у меня отвечает функция updateNum():
function updateNum() {
var num = 1;
$(document).find('#songs').find('.song-row').each(function() {
$(this).find('[name="num"]').html(num);
num++;
})
}
Эта функция находит в каждой из 40 строк колонку num и забивает туда порядковый номер. Вызываем её после обновления списка так:
$('#songs').sortable({
update: updateNum
});
$('#songs').disableSelection();
Перетаскивание элементов с jQuery — droppable()
Ну и так как прошлая секция получилась очень компактной, хочу ещё немного поговорить про возможности jQuery. В моём примере мне нужно было менять местами элементы, которые лежат в контейнере — их позиция на экране задана стилями, и должна таковой сохраняться. Но что если нужно позволить пользователю таскать элемент по экрану в любое место? Тогда нужно сказать, что этот элемент draggable():
$('#songs').find('.draggable').draggable();
После этого все строчки в моей таблице можно перемещать в любое место на экране и даже выносить за пределы контейнера.
Обратите внимание, что в прошлом примере мы объявляем sortable() сам контейнер, а элементы в нём вообще не трогаем. А здесь работаем именно с элементами, но не трогаем контейнер.

В моём проекте это смысла не имеет, но кому-то может быть более полезно.
Если мы не хотим позволять выносить объект за пределы контейнера (объекты будут упираться в его «стенки»), то прописываем свойство containment.
$('#songs').find('.draggable').draggable({
containment: '#songs'
});
Можно также ограничить движение лишь по сетке с помощью grid, и задать ось движения с axis:
$('#songs').find('.draggable').draggable({
grid: [10, 10],
axis: 'y'
});
Более сложные случаи, вроде движения под заданным углом, описывать не буду, так как в моём проекте это не требуется.
Итак, в этой статье разобрались с тем, как менять местами элементы в контейнере с помощью Javascript и jQuery, и также взглянули на некоторые базовые настройки перемещаемых элементов в jQuery.