Pull to refresh
VK
Building the Internet

Сортировка при помощи HTML5 Drag'n'Drop API

VK corporate blog Website development *JavaScript *
Sortable.js — минималистичная библиотека для современных браузеров и touch-устройств, не требующая jQuery.

Как вы уже догадались из названия, библиотека предназначена для сортировки элементов при помощи 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
Tags:
Hubs:
Total votes 59: ↑54 and ↓5 +49
Views 33K
Comments Comments 44

Information

Founded
Location
Россия
Website
vk.com
Employees
5,001–10,000 employees
Registered
Representative
Миша Буданов