![](https://habrastorage.org/getpro/habr/post_images/e0f/693/c28/e0f693c288df05f1d3956ea804ab944b.jpg)
Как вы уже догадались из названия, библиотека предназначена для сортировки элементов при помощи drag’n’drop. Стандартным решением в таких случаях является jQuery UI/Sortable, а это ни много, ни мало 64 кб + 10 кб. Итого 75 кб gzipped в проекте, где jQuery не используется совсем. Относительно недавно на Хабре уже была статья о том, как реализовать похожий функционал, но опять же на jQuery, да и touch-устройства в предлагаемом решении не поддерживаются.
Помимо проблем с весом, все найденные мной, библиотеки не умели работать с динамически изменяемым списком. В момент инициализации плагина они определяли позиции всех элементов, и, чтобы обновить их, нужно было переинициализировать плагин, либо вызвать метод
$(‘...’).sortable(‘refresh’)
, что весьма неудобно.Так как в моей задаче не требовалась поддержка старых браузеров, я решил попробовать сделать нужный мне функционал на чистом JS, с использование HTML5 Drag’n’Drop.
После прочтения статей на эту тему, оказалось, что сейчас создать подобный функционал очень просто, можно даже уложиться в 25 строк (если убрать комментарии и отбивку):
http://jsfiddle.net/RubaXa/zLq5J/
function sortable(rootEl, onUpdate){
var dragEl;
// Делаем всех детей перетаскиваемыми
[].slice.call(rootEl.children).forEach(function (itemEl){
itemEl.draggable = true;
});
// Функция отвечающая за сортировку
function _onDragOver(evt){
evt.preventDefault();
evt.dataTransfer.dropEffect = 'move';
var target = evt.target;
if( target && target !== dragEl && target.nodeName == 'LI' ){
// Сортируем
rootEl.insertBefore(dragEl, target.nextSibling || target);
}
}
// Завершение сортировки
function _onDragEnd(evt){
evt.preventDefault();
dragEl.classList.remove('ghost');
rootEl.removeEventListener('dragover', _onDragOver, false);
rootEl.removeEventListener('dragend', _onDragEnd, false);
// Сообщаем об окончании сортировки
onUpdate(dragEl);
}
// Начало сортировки
rootEl.addEventListener('dragstart', function (evt){
dragEl = evt.target; // Запоминаем элемент который будет перемещать
// Ограничиваем тип перетаскивания
evt.dataTransfer.effectAllowed = 'move';
evt.dataTransfer.setData('Text', dragEl.textContent);
// Подписываемся на события при dnd
rootEl.addEventListener('dragover', _onDragOver, false);
rootEl.addEventListener('dragend', _onDragEnd, false);
setTimeout(function (){
// Если выполнить данное действие без setTimeout, то
// перетаскиваемый объект, будет иметь этот класс.
dragEl.classList.add('ghost');
}, 0)
}, false);
}
// Используем
sortable( document.getElementById('list'), function (item){
console.log(item);
});
Как видно из кода, вся сортировка состоит из простого перемещения перетаскиваемого элемента при помощи
rootEl.insertBefore(dragEl, target.nextSibling || target)
, где target
— это элемент, на который навелись. Если вы уже протестировали пример, то наверняка заметили, что нельзя перетащить элемент на первую позицию. Еще один нюанс метода — onUpdate
вызывается каждый раз, даже если элемент не был перемещен.Для того, чтобы избавиться от первой проблемы, достаточно добавить проверку при сортировке. Вставлять элемент после
target.nextSibling
нужно только в том случае, если это не первый элемент списка:http://jsfiddle.net/RubaXa/zLq5J/3/
if( target && target !== dragEl && target.nodeName == 'LI' ){
// Сортируем
rootEl.insertBefore(dragEl, rootEl.children[0] !== target && target.nextSibling || target);
}
Помимо этого, простое сохранение ссылки на следующий элемент (
nextEl = dragEl.nextSibling
) на момент dragstart
, позволяет избавиться от второй проблемы (http://jsfiddle.net/RubaXa/zLq5J/4/ 29 и 38 строка).На первый взгляд, все выглядит хорошо, получился компактный и понятный код, который поддерживают большинство браузеров, а если добавить поддержку
attachEvent
и убрать dragEl.classList.add/remove
, то код будет работать даже в IE5.5 :]Но, если мы немного изменим пример, просто увеличив высоту элементов списка, то получим третью проблему. Сортировка нормально работает сверху вниз, а вот наоборот — уже плохо. Поэтому логику выбора вставки элемента, «перед» или «после», нужно переписать, чтобы она учитывала, в какой половине находится курсор мыши, «верхней» или «нижней». Для этого на onDragOver получаем координаты элемента относительно экрана и проверяем, в какой половине находится курсор:
http://jsfiddle.net/RubaXa/zLq5J/6/
var rect = target.getBoundingClientRect();
var next = (evt.clientY - rect.top)/(rect.bottom - rect.top) > .5;
rootEl.insertBefore(dragEl, next && target.nextSibling || target);
Помимо этого, ещё пришлось доработать работу с inline-элементами и float-блоками.
Touch support
Увы, но drag’n’drop не работает на touch-устройствах, поэтому как-то надо было сделать эмуляцию на основе touch-событий. Я долго ломал голову, читал документацию, но ответа так и не нашёл. В итоге, ещё немного покопавшись, я вспомнил о замечательном методе document.elementFromPoint, который позволяет получить ссылку на элемент по координатам.В итоге на
touchstart
я клонирую элемент, который будет выполнять роль «призрака» под пальцем и на touchmove
перемещаю его при помощи translate3d
:var
touch = evt.touches[0]
, dx = touch.clientX - tapEvt.clientX
, dy = touch.clientY - tapEvt.clientY
;
Кроме этого, я запускаю
setInterval
, в котором каждые 100ms проверяю элемент, над котором в данный момент находится палец:_emulateDragOver: function (){
if( touchEvt ){
// Скрываем “призрака” под пальцем
_css(ghostEl, 'display', 'none');
// Получаем элемент, который находится под пальцем
var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
// Проверяем полученный элемент и если он принадлежит rootEl,
// вызываем метод onDragOver:
this._onDragOver({
target: target
, clientX: touchEvt.clientX
, clientY: touchEvt.clientY
});
// Обратно показываем “призрака”
_css(ghostEl, 'display', '');
}
}
Вот и всё, ничего сверхъестественного, как видите, нет. Оформляем код, пишем небольшую документацию и микро библиотечка готова.
Sortable
Библиотека получилось размером 2 кб gzipped и обладает следующими возможностями:- Сортировка вертикальных и горизонтальных списков;
- Возможность задать элементы для сортировки (css-селектор);
- Объединение в группы;
- Возможность задать handle (элемент, за который можно перетаскивать);
- Класс, который добавляется к перемещаемому элементу;
- События onAdd, onUpdate, onRemove;
- Работа с динамически изменяемым списком.
Пример кода:
// Простой список, например ul > li
var list = document.getElementById("my-ui-list");
new Sortable(list); // И всё.
// Группировка
var foo = document.getElementById("foo");
new Sortable(foo, { group: "omega" });
var bar = document.getElementById("bar");
new Sortable(bar, { group: "omega" });
// handle + event
var container = document.getElementById("multi");
new Sortable(container, {
handle: ".tile__title", // css-селектор, за который можно таскать
dragabble: ".tile", // css-селектор элементов, которые можно сортировать
onUpdate: function (evt/**Event*/){
var item = evt.detail; // ссылка на элемент, который переместили
}
});
На данный момент, есть только базовый функционал, буду рад любым отзывам или pull request, спасибо за внимание.
Demo | Source
Также вы можете следить за нашими проектами через:
github.com/mailru — FileAPI, Tarantool, Fest и многое другоеgithub.com/rubaxa — мой github
@ibnRubaXa