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

от автора

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

ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/207048/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *