-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Sorting with the help of HTML5 Drag'n'Drop API
Sortable.js is a minimalistic library for modern browsers and touch devices that doesn't require jQuery. As you may have guessed from its name, the library was developed for sorting elements by means of drag’n’drop. Standard solution in this case is jQuery UI/Sortable, and that’s 64kB + 10kB, neither more and no less. The result is 75kB of gzipped in a project, where jQuery is not used at all.
Apart from problems with weight, all libraries that I’ve found were unable to work with a dynamically varying list. At the moment of plug-in initialization, they were defining positions of all elements; to refresh them, we had to reinitialize the plug-in, or to call $('...').sortable('refresh')
method, which is quite inconvenient.
Since my task didn’t require supporting old browsers, I’ve tried to create the functionality I needed in pure JS with the use of HTML5 Drag’n’Drop.
After reading the related articles, it turned out that today it is very simple to create such functionality, it can even be made with 25 lines only (without commentaries and spacing):
http://jsfiddle.net/RubaXa/zLq5J/
function sortable(rootEl, onUpdate) {
var dragEl;
// Making all siblings movable
[].slice.call(rootEl.children).forEach(function (itemEl) {
itemEl.draggable = true;
});
// Function responsible for sorting
function _onDragOver(evt) {
evt.preventDefault();
evt.dataTransfer.dropEffect = 'move';
var target = evt.target;
if (target && target !== dragEl && target.nodeName == 'LI') {
// Sorting
rootEl.insertBefore(dragEl, target.nextSibling || target);
}
}
// End of sorting
function _onDragEnd(evt){
evt.preventDefault();
dragEl.classList.remove('ghost');
rootEl.removeEventListener('dragover', _onDragOver, false);
rootEl.removeEventListener('dragend', _onDragEnd, false);
// Notification about the end of sorting
onUpdate(dragEl);
}
// Sorting starts
rootEl.addEventListener('dragstart', function (evt){
dragEl = evt.target; // Remembering an element that will be moved
// Limiting the movement type
evt.dataTransfer.effectAllowed = 'move';
evt.dataTransfer.setData('Text', dragEl.textContent);
// Subscribing to the events at dnd
rootEl.addEventListener('dragover', _onDragOver, false);
rootEl.addEventListener('dragend', _onDragEnd, false);
setTimeout(function () {
// If this action is performed without setTimeout, then
// the moved object will be of this class.
dragEl.classList.add('ghost');
}, 0)
}, false);
}
// Using
sortable(document.getElementById('list'), function (item) {
console.log(item);
});
As it may be noticed from the code, the whole sorting process consists of simple movement of the dragged element by means of rootEl.insertBefore(dragEl, target.nextSibling || target)
, where target
is an element that was targeted. If you have already tested the example, you must have noticed that it is impossible to move an element to the first position. One more peculiarity of this method is that onUpdate
is called each time, even if the element was not moved.
In order to fix the first problem, all we have to do is to add testing during sorting. It is necessary to insert an element after target.nextSibling only in case it is not the first element of the list:
http://jsfiddle.net/RubaXa/zLq5J/3/
if (target && target !== dragEl && target.nodeName == 'LI') {
// Sorting
rootEl.insertBefore(dragEl, rootEl.children[0] !== target && target.nextSibling || target);
}
Besides that, simply saving a link to the next element (nextEl = dragEl.nextSibling)
in the moment dragstart
allows us to get rid of the second problem (http://jsfiddle.net/RubaXa/zLq5J/4/, lines 29 and 38).
On the face of it, everything looks fine, we have a compact and intelligible code that is supported by the majority of browsers, and if we add the support of attachEvent and remove dragEl.classList.add/remove
, then the code will work even in IE5.5 :]
But if we change the example a little bit by simply increasing height of list elements, we will have a third problem. Sorting works fine from the top downward, but it works poorly from the bottom upwards. That’s why we need to rewrite the logic of element inserting before
or after
so that it would consider, in which half the mouse cursor is located (upper or lower). For this purpose, we acquire element coordinates against the screen at onDragOver
and check, in which half the cursor is located:
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);
Unfortunately, drag’n’drop doesn’t work on touch devices. That’s why we needed to create some sort of emulation based on touch-events. I have been scratching my had over this for a long time, read documentation, but never found an answer. Finally, after digging a little bit more, I remembered one more excellent method document.elementFromPoint, which allows to obtain a link to an element by coordinates.
As the result, I clone the element that will play the role of a “ghost” under the finger at touchstart
, and move it by means of translate3d
at touchmove
:
var
touch = evt.touches[0],
dx = touch.clientX - tapEvt.clientX,
dy = touch.clientY - tapEvt.clientY
;
Besides that, I initiate setInterval
, where I check the current element under the finger every 100ms:
_emulateDragOver: function () {
if (touchEvt) {
// Hiding a “ghost” under the finger
_css(ghostEl, 'display', 'none');
// Obtaining an element under the finger
var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
// Checking the obtained element, and if it belongs to rootEl,
// we call `onDragOver` method:
this._onDragOver({
target: target,
clientX: touchEvt.clientX,
clientY: touchEvt.clientY,
});
// Showing the “ghost” again
_css(ghostEl, 'display', '');
}
}
That’s it, as you can see, there’s nothing supernatural. Now, we need to draw up a code, write some documentation, and the micro library is ready.
The library turned out to weight 2kB gzipped and has the following capabilities:
- Sorting of vertical and horizontal lists;
- Ability to set the elements to be sorted (css-selector);
- Combining into groups;
- Ability to set handle (an element that can be dragged);
- A class that is added to the moved element;
-
onAdd
,onUpdate
,onRemove
events; - Working with dynamically varied lists.
// Simple list, e.g. ul > li
var list = document.getElementById("my-ui-list");
new Sortable(list); // That's all.
// Grouping
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-selector, which can be used to drag
draggable: ".tile", // css-selector of elements, which can be sorted
onUpdate: function (/**Event*/evt){
var item = evt.item; // a link to an element that was moved
}
});
Today, only basic functionality is available. I would be glad to receive any feedback or pull request, thanks for your attention.