Как вы уже догадались из названия, библиотека предназначена для сортировки элементов при помощи 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, спасибо за внимание.
Также вы можете следить за нашими проектами через:
github.com/mailru — FileAPI, Tarantool, Fest и многое другое
github.com/rubaxa — мой github
@ibnRubaXa
ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/207048/
Добавить комментарий