Список задач с drag & drop
В этой демонстрации мы создадим список задач, которые можно будет сортировать с помощью эффекта drag & drop («потяни и брось»).
Для начала напишем разметку. Список будет состоять из заголовка и непосредственно самих задач.
Добавим элементам базовую стилизацию.
Обратите внимание на тип курсора — move
. Все элементы списка смогут перемещаться, а с помощью курсора move
мы подсказываем пользователю, что есть такая возможность.
Также зададим стилизацию для класса selected
, который чуть позже будем добавлять программно при взаимодействии с элементом.
Теперь можно приступить к реализации логики перемещения.
В стандарте HTML5 есть HTML Drag and Drop API. Он даёт возможность с помощью специальных событий контролировать захват элемента на странице мышью и его перемещение в новое положение.
По умолчанию большинство элементов не может перемещаться, поэтому присвоим им атрибут draggable
со значением true
, чтобы изменить это поведение.
Также нам нужно узнать, какой элемент на данный момент перетаскивается. Для этого будем слушать событие dragstart
. Оно срабатывает в момент начала перетаскивания элемента.
После того, как событие сработало, добавим элементу класс selected
. Мы уже задали стили для него ранее. Если попробовать перетащить элемент сейчас, можно увидеть, как изменилась прозрачность его фонового цвета. Но на данный момент она не возвращается к исходному значению после окончания перетаскивания.
В момент окончания перетаскивания нужно убирать класс selected
у элемента. Для этого будем слушать событие dragend
— оно сработает после завершения перетаскивания.
Мы добрались до основной части — перетаскивания задач. Будем отслеживать местоположение перемещаемого элемента относительно других, подписавшись на событие dragover
. Оно срабатывает каждые несколько сотен миллисекунд, пока перетаскиваемый элемент находится над зоной, в которую может быть сброшен. В данном случае это tasksListElement
, на нём и будем отслеживать dragover
.
По умолчанию большинство областей на странице недоступны для сброса. Чтобы создать область, в которую элементы могут быть сброшены, нужно внутри обработчика события dragover
отменить действие по умолчанию, переопределив поведение. Сделаем такой областью весь список задач.
Найдём выбранный элемент (с классом selected
) и тот элемент, на котором сработало событие dragover
.
Проверим, что событие dragover
сработало не на выбранном элементе, потому что иначе перемещать элемент нет смысла — он уже на нужном месте.
Также проверим, что dragover
сработало именно на одном из элементов списка. Это важно, потому что курсор может оказаться и на пустом пространстве между элементами, а оно нас не интересует.
Если условие не выполняется, прервём выполнение функции, отменив все дальнейшие действия.
Теперь найдём элемент, перед которым нужно осуществить вставку. Сделаем это, сравнив положение выбранного элемента и текущего, на который наведён курсор.
Осталось только вставить элемент на новое место. Для этого воспользуемся методом insertBefore
. Он вызывается на родительском элементе, первым параметром принимает вставляемый элемент, а вторым — элемент, перед которым нужно вставить.
Получившийся на этом этапе код — рабочий. Уже сейчас элементы можно сортировать так, как мы и планировали. Но при этом у варианта есть недостаток — перемещаемый элемент меняет положение в тот момент, когда курсор попадает на другой элемент. Такое поведение недостаточно оптимально и стабильно. С точки зрения пользователя логичнее ориентироваться на центр элемента. То есть мы должны осуществлять вставку только после того, как курсор пересечёт центральную ось, а не сразу после наведения на элемент. Чтобы реализовать это поведение, напишем функцию для получения nextElement
другим способом.
Мы уже знаем, что функция должна возвращать тот элемент, перед которым нужно сделать вставку. Тогда мы сможем без проблем интегрировать её в уже готовый код, который написали ранее.
getNextElement
должна принимать на вход вертикальную координату курсора и текущий элемент, на котором сработало событие dragover
. Мы сравним текущее положение курсора с центральной осью элемента, над которым курсор находится в момент перетаскивания. Вставка будет осуществляться в момент, когда курсор пересекает центральную ось.
Значит нас интересует вертикальная координата центра элемента, над которым находится курсор. Чтобы получить её, используем метод getBoundingClientRect()
. Он вызывается на элементе и возвращает объект, в свойствах которого находится информация о размерах и координатах элемента относительно вьюпорта. Нам понадобится координата y
, но также нужно будет учесть высоту элемента height
, потому что у
рассчитывается относительно верхнего левого угла элемента, а нам нужен центр.
Далее опишем логику получения nextElement
. Если курсор выше центра элемента, nextElement
должен быть равен текущему элементу, в ином случае — следующему DOM-элементу.
Функция готова. Теперь применим её для нахождения nextElement
, передав нужные параметры. Используем evt.clientY
— вертикальную координату курсора в момент, когда сработало событие.
Всё почти готово, но нужно ещё учесть ситуацию, когда во время перемещения курсор был наведён на какой-то элемент и при этом центральную ось так и не пересёк. Это значит, что порядок не изменился, и ничего делать не нужно. Но программа пока об этом не знает и в таких ситуациях осуществляет вставку в DOM на то же самое место при каждом срабатывании события dragover
. Оно срабатывает очень часто и каждый раз влечёт за собой ненужные операции с DOM. Изменим это поведение, добавив проверку. Если порядок не изменился, выходим из функции.
Теперь всё работает так, как нужно: отслеживаем положение курсора относительно центра, лишние операции в DOM исключили и, главное, элементы сортируются — задача выполнена!
Также вы можете прочитать подробный туториал с разбором этой задачи в блоге Академии.