blob: 524e118e2f2e2d98ce669dcc9507f950acd48fd6 [file] [log] [blame]
AdriĆ 6ae49e72014-11-09 22:21:19 +01001/**!
2 * Sortable
3 * @author RubaXa <trash@rubaxa.org>
4 * @license MIT
5 */
6
7
8(function (factory){
9 "use strict";
10
11 if( typeof define === "function" && define.amd ){
12 define(factory);
13 }
14 else if( typeof module != "undefined" && typeof module.exports != "undefined" ){
15 module.exports = factory();
16 }
17 else {
18 window["Sortable"] = factory();
19 }
20})(function (){
21 "use strict";
22
23 var
24 dragEl
25 , ghostEl
26 , cloneEl
27 , rootEl
28 , nextEl
29
30 , lastEl
31 , lastCSS
32
33 , activeGroup
34
35 , tapEvt
36 , touchEvt
37
38 , expando = 'Sortable' + (new Date).getTime()
39
40 , win = window
41 , document = win.document
42 , parseInt = win.parseInt
43 , supportIEdnd = !!document.createElement('div').dragDrop
44
45 , _silent = false
46
47 , _dispatchEvent = function (rootEl, name, targetEl, fromEl) {
48 var evt = document.createEvent('Event');
49
50 evt.initEvent(name, true, true);
51 evt.item = targetEl || rootEl;
52 evt.from = fromEl || rootEl;
53
54 rootEl.dispatchEvent(evt);
55 }
56
57 , _customEvents = 'onAdd onUpdate onRemove onStart onEnd onFilter onSort'.split(' ')
58
59 , noop = function (){}
60 , slice = [].slice
61
62 , touchDragOverListeners = []
63 ;
64
65
66
67 /**
68 * @class Sortable
69 * @param {HTMLElement} el
70 * @param {Object} [options]
71 */
72 function Sortable(el, options){
73 this.el = el; // root element
74 this.options = options = (options || {});
75
76
77 // Default options
78 var defaults = {
79 group: Math.random(),
80 sort: true,
81 store: null,
82 handle: null,
83 draggable: el.children[0] && el.children[0].nodeName || (/[uo]l/i.test(el.nodeName) ? 'li' : '*'),
84 ghostClass: 'sortable-ghost',
85 ignore: 'a, img',
86 filter: null,
87 animation: 0
88 };
89
90
91 // Set default options
92 for (var name in defaults) {
93 !(name in options) && (options[name] = defaults[name]);
94 }
95
96
97 if (!options.group.name) {
98 options.group = { name: options.group };
99 }
100
101
102 ['pull', 'put'].forEach(function (key) {
103 if (!(key in options.group)) {
104 options.group[key] = true;
105 }
106 });
107
108
109 // Define events
110 _customEvents.forEach(function (name) {
111 options[name] = _bind(this, options[name] || noop);
112 _on(el, name.substr(2).toLowerCase(), options[name]);
113 }, this);
114
115
116 // Export group name
117 el[expando] = options.group.name;
118
119
120 // Bind all private methods
121 for( var fn in this ){
122 if( fn.charAt(0) === '_' ){
123 this[fn] = _bind(this, this[fn]);
124 }
125 }
126
127
128 // Bind events
129 _on(el, 'mousedown', this._onTapStart);
130 _on(el, 'touchstart', this._onTapStart);
131 supportIEdnd && _on(el, 'selectstart', this._onTapStart);
132
133 _on(el, 'dragover', this._onDragOver);
134 _on(el, 'dragenter', this._onDragOver);
135
136 touchDragOverListeners.push(this._onDragOver);
137
138 // Restore sorting
139 options.store && this.sort(options.store.get(this));
140 }
141
142
143 Sortable.prototype = /** @lends Sortable.prototype */ {
144 constructor: Sortable,
145
146
147 _applyEffects: function (){
148 _toggleClass(dragEl, this.options.ghostClass, true);
149 },
150
151
152 _onTapStart: function (evt/**Event|TouchEvent*/){
153 var
154 touch = evt.touches && evt.touches[0]
155 , target = (touch || evt).target
156 , options = this.options
157 , el = this.el
158 , filter = options.filter
159 ;
160
161 if( evt.type === 'mousedown' && evt.button !== 0 ) {
162 return; // only left button
163 }
164
165 // Check filter
166 if( typeof filter === 'function' ){
167 if( filter.call(this, target, this) ){
168 _dispatchEvent(el, 'filter', target);
169 return; // cancel dnd
170 }
171 }
172 else if( filter ){
173 filter = filter.split(',').filter(function (criteria) {
174 return _closest(target, criteria.trim(), el);
175 });
176
177 if (filter.length) {
178 _dispatchEvent(el, 'filter', target);
179 return; // cancel dnd
180 }
181 }
182
183 if( options.handle ){
184 target = _closest(target, options.handle, el);
185 }
186
187 target = _closest(target, options.draggable, el);
188
189 // IE 9 Support
190 if( target && evt.type == 'selectstart' ){
191 if( target.tagName != 'A' && target.tagName != 'IMG'){
192 target.dragDrop();
193 }
194 }
195
196 if( target && !dragEl && (target.parentNode === el) ){
197 tapEvt = evt;
198
199 rootEl = this.el;
200 dragEl = target;
201 nextEl = dragEl.nextSibling;
202 activeGroup = this.options.group;
203
204 dragEl.draggable = true;
205
206 // Disable "draggable"
207 options.ignore.split(',').forEach(function (criteria) {
208 _find(target, criteria.trim(), _disableDraggable);
209 });
210
211 if( touch ){
212 // Touch device support
213 tapEvt = {
214 target: target
215 , clientX: touch.clientX
216 , clientY: touch.clientY
217 };
218
219 this._onDragStart(tapEvt, true);
220 evt.preventDefault();
221 }
222
223 _on(document, 'mouseup', this._onDrop);
224 _on(document, 'touchend', this._onDrop);
225 _on(document, 'touchcancel', this._onDrop);
226
227 _on(this.el, 'dragstart', this._onDragStart);
228 _on(this.el, 'dragend', this._onDrop);
229 _on(document, 'dragover', _globalDragOver);
230
231
232 try {
233 if( document.selection ){
234 document.selection.empty();
235 } else {
236 window.getSelection().removeAllRanges()
237 }
238 } catch (err){ }
239
240
241 _dispatchEvent(dragEl, 'start');
242
243
244 cloneEl = dragEl.cloneNode(true);
245 _css(cloneEl, 'display', 'none');
246 rootEl.insertBefore(cloneEl, dragEl);
247 }
248 },
249
250 _emulateDragOver: function (){
251 if( touchEvt ){
252 _css(ghostEl, 'display', 'none');
253
254 var
255 target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY)
256 , parent = target
257 , groupName = this.options.group.name
258 , i = touchDragOverListeners.length
259 ;
260
261 if( parent ){
262 do {
263 if( parent[expando] === groupName ){
264 while( i-- ){
265 touchDragOverListeners[i]({
266 clientX: touchEvt.clientX,
267 clientY: touchEvt.clientY,
268 target: target,
269 rootEl: parent
270 });
271 }
272 break;
273 }
274
275 target = parent; // store last element
276 }
277 while( parent = parent.parentNode );
278 }
279
280 _css(ghostEl, 'display', '');
281 }
282 },
283
284
285 _onTouchMove: function (evt/**TouchEvent*/){
286 if( tapEvt ){
287 var
288 touch = evt.touches[0]
289 , dx = touch.clientX - tapEvt.clientX
290 , dy = touch.clientY - tapEvt.clientY
291 , translate3d = 'translate3d(' + dx + 'px,' + dy + 'px,0)'
292 ;
293
294 touchEvt = touch;
295
296 _css(ghostEl, 'webkitTransform', translate3d);
297 _css(ghostEl, 'mozTransform', translate3d);
298 _css(ghostEl, 'msTransform', translate3d);
299 _css(ghostEl, 'transform', translate3d);
300
301 evt.preventDefault();
302 }
303 },
304
305
306 _onDragStart: function (evt/**Event*/, isTouch/**Boolean*/){
307 var dataTransfer = evt.dataTransfer;
308
309 this._offUpEvents();
310
311 if( isTouch ){
312 var
313 rect = dragEl.getBoundingClientRect()
314 , css = _css(dragEl)
315 , ghostRect
316 ;
317
318 ghostEl = dragEl.cloneNode(true);
319
320 _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
321 _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
322 _css(ghostEl, 'width', rect.width);
323 _css(ghostEl, 'height', rect.height);
324 _css(ghostEl, 'opacity', '0.8');
325 _css(ghostEl, 'position', 'fixed');
326 _css(ghostEl, 'zIndex', '100000');
327
328 rootEl.appendChild(ghostEl);
329
330 // Fixing dimensions.
331 ghostRect = ghostEl.getBoundingClientRect();
332 _css(ghostEl, 'width', rect.width*2 - ghostRect.width);
333 _css(ghostEl, 'height', rect.height*2 - ghostRect.height);
334
335 // Bind touch events
336 _on(document, 'touchmove', this._onTouchMove);
337 _on(document, 'touchend', this._onDrop);
338 _on(document, 'touchcancel', this._onDrop);
339
340 this._loopId = setInterval(this._emulateDragOver, 150);
341 }
342 else {
343 dataTransfer.effectAllowed = 'move';
344 dataTransfer.setData('Text', dragEl.textContent);
345
346 _on(document, 'drop', this._onDrop);
347 }
348
349 setTimeout(this._applyEffects);
350 },
351
352
353 _onDragOver: function (evt/**Event*/){
354 var el = this.el,
355 target,
356 dragRect,
357 revert,
358 options = this.options,
359 group = options.group,
360 groupPut = group.put,
361 isOwner = (activeGroup === group);
362
363 if( !_silent &&
364 (activeGroup.name === group.name || groupPut && groupPut.indexOf && groupPut.indexOf(activeGroup.name) > -1) &&
365 (isOwner && (options.sort || (revert = !rootEl.contains(dragEl))) || groupPut && activeGroup.pull) &&
366 (evt.rootEl === void 0 || evt.rootEl === this.el)
367 ){
368 target = _closest(evt.target, this.options.draggable, el);
369 dragRect = dragEl.getBoundingClientRect();
370
371 if ((activeGroup.pull == 'clone') && (cloneEl.state !== isOwner)) {
372 _css(cloneEl, 'display', isOwner ? 'none' : '');
373 !isOwner && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
374 cloneEl.state = isOwner;
375 }
376
377 if (revert) {
378 rootEl.insertBefore(dragEl, cloneEl);
379 return;
380 }
381
382 if( (el.children.length === 0) || (el.children[0] === ghostEl) ||
383 (el === evt.target) && _ghostInBottom(el, evt)
384 ){
385 target && (targetRect = target.getBoundingClientRect());
386
387 el.appendChild(dragEl);
388 this._animate(dragRect, dragEl);
389 target && this._animate(targetRect, target);
390 }
391 else if( target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0) ){
392 if( lastEl !== target ){
393 lastEl = target;
394 lastCSS = _css(target);
395 }
396
397
398 var targetRect = target.getBoundingClientRect()
399 , width = targetRect.right - targetRect.left
400 , height = targetRect.bottom - targetRect.top
401 , floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display)
402 , isWide = (target.offsetWidth > dragEl.offsetWidth)
403 , isLong = (target.offsetHeight > dragEl.offsetHeight)
404 , halfway = (floating ? (evt.clientX - targetRect.left)/width : (evt.clientY - targetRect.top)/height) > .5
405 , nextSibling = target.nextElementSibling
406 , after
407 ;
408
409 _silent = true;
410 setTimeout(_unsilent, 30);
411
412 if( floating ){
413 after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide
414 } else {
415 after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
416 }
417
418 if( after && !nextSibling ){
419 el.appendChild(dragEl);
420 } else {
421 target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
422 }
423
424 this._animate(dragRect, dragEl);
425 this._animate(targetRect, target);
426 }
427 }
428 },
429
430 _animate: function (prevRect, target) {
431 var ms = this.options.animation;
432
433 if (ms) {
434 var currentRect = target.getBoundingClientRect();
435
436 _css(target, 'transition', 'none');
437 _css(target, 'transform', 'translate3d('
438 + (prevRect.left - currentRect.left) + 'px,'
439 + (prevRect.top - currentRect.top) + 'px,0)'
440 );
441
442 target.offsetWidth; // repaint
443
444 _css(target, 'transition', 'all ' + ms + 'ms');
445 _css(target, 'transform', 'translate3d(0,0,0)');
446
447 clearTimeout(target.animated);
448 target.animated = setTimeout(function () {
449 _css(target, 'transition', '');
450 target.animated = false;
451 }, ms);
452 }
453 },
454
455 _offUpEvents: function () {
456 _off(document, 'mouseup', this._onDrop);
457 _off(document, 'touchmove', this._onTouchMove);
458 _off(document, 'touchend', this._onDrop);
459 _off(document, 'touchcancel', this._onDrop);
460 },
461
462 _onDrop: function (evt/**Event*/){
463 clearInterval(this._loopId);
464
465 // Unbind events
466 _off(document, 'drop', this._onDrop);
467 _off(document, 'dragover', _globalDragOver);
468
469 _off(this.el, 'dragend', this._onDrop);
470 _off(this.el, 'dragstart', this._onDragStart);
471 _off(this.el, 'selectstart', this._onTapStart);
472
473 this._offUpEvents();
474
475 if( evt ){
476 evt.preventDefault();
477 evt.stopPropagation();
478
479 cloneEl.parentNode.removeChild(cloneEl);
480 ghostEl && ghostEl.parentNode.removeChild(ghostEl);
481
482 if( dragEl ){
483 _disableDraggable(dragEl);
484 _toggleClass(dragEl, this.options.ghostClass, false);
485
486 if( !rootEl.contains(dragEl) ){
487 _dispatchEvent(dragEl, 'sort');
488 _dispatchEvent(rootEl, 'sort');
489
490 // Add event
491 _dispatchEvent(dragEl, 'add', dragEl, rootEl);
492
493 // Remove event
494 _dispatchEvent(rootEl, 'remove', dragEl);
495 }
496 else if( dragEl.nextSibling !== nextEl ){
497 // Update event
498 _dispatchEvent(dragEl, 'update');
499 _dispatchEvent(dragEl, 'sort');
500 }
501
502 _dispatchEvent(rootEl, 'end');
503 }
504
505 // Set NULL
506 rootEl =
507 dragEl =
508 ghostEl =
509 nextEl =
510 cloneEl =
511
512 tapEvt =
513 touchEvt =
514
515 lastEl =
516 lastCSS =
517
518 activeGroup = null;
519
520 // Save sorting
521 this.options.store && this.options.store.set(this);
522 }
523 },
524
525
526 /**
527 * Serializes the item into an array of string.
528 * @returns {String[]}
529 */
530 toArray: function () {
531 var order = [],
532 el,
533 children = this.el.children,
534 i = 0,
535 n = children.length
536 ;
537
538 for (; i < n; i++) {
539 el = children[i];
540 if (_closest(el, this.options.draggable, this.el)) {
541 order.push(el.getAttribute('data-id') || _generateId(el));
542 }
543 }
544
545 return order;
546 },
547
548
549 /**
550 * Sorts the elements according to the array.
551 * @param {String[]} order order of the items
552 */
553 sort: function (order) {
554 var items = {}, rootEl = this.el;
555
556 this.toArray().forEach(function (id, i) {
557 var el = rootEl.children[i];
558
559 if (_closest(el, this.options.draggable, rootEl)) {
560 items[id] = el;
561 }
562 }, this);
563
564
565 order.forEach(function (id) {
566 if (items[id]) {
567 rootEl.removeChild(items[id]);
568 rootEl.appendChild(items[id]);
569 }
570 });
571 },
572
573
574 /**
575 * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
576 * @param {HTMLElement} el
577 * @param {String} [selector] default: `options.draggable`
578 * @returns {HTMLElement|null}
579 */
580 closest: function (el, selector) {
581 return _closest(el, selector || this.options.draggable, this.el);
582 },
583
584
585 /**
586 * Destroy
587 */
588 destroy: function () {
589 var el = this.el, options = this.options;
590
591 _customEvents.forEach(function (name) {
592 _off(el, name.substr(2).toLowerCase(), options[name]);
593 });
594
595 _off(el, 'mousedown', this._onTapStart);
596 _off(el, 'touchstart', this._onTapStart);
597 _off(el, 'selectstart', this._onTapStart);
598
599 _off(el, 'dragover', this._onDragOver);
600 _off(el, 'dragenter', this._onDragOver);
601
602 //remove draggable attributes
603 Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function(el) {
604 el.removeAttribute('draggable');
605 });
606
607 touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);
608
609 this._onDrop();
610
611 this.el = null;
612 }
613 };
614
615
616 function _bind(ctx, fn){
617 var args = slice.call(arguments, 2);
618 return fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function (){
619 return fn.apply(ctx, args.concat(slice.call(arguments)));
620 };
621 }
622
623
624 function _closest(el, selector, ctx){
625 if( selector === '*' ){
626 return el;
627 }
628 else if( el ){
629 ctx = ctx || document;
630 selector = selector.split('.');
631
632 var
633 tag = selector.shift().toUpperCase()
634 , re = new RegExp('\\s('+selector.join('|')+')\\s', 'g')
635 ;
636
637 do {
638 if(
639 (tag === '' || el.nodeName == tag)
640 && (!selector.length || ((' '+el.className+' ').match(re) || []).length == selector.length)
641 ){
642 return el;
643 }
644 }
645 while( el !== ctx && (el = el.parentNode) );
646 }
647
648 return null;
649 }
650
651
652 function _globalDragOver(evt){
653 evt.dataTransfer.dropEffect = 'move';
654 evt.preventDefault();
655 }
656
657
658 function _on(el, event, fn){
659 el.addEventListener(event, fn, false);
660 }
661
662
663 function _off(el, event, fn){
664 el.removeEventListener(event, fn, false);
665 }
666
667
668 function _toggleClass(el, name, state){
669 if( el ){
670 if( el.classList ){
671 el.classList[state ? 'add' : 'remove'](name);
672 }
673 else {
674 var className = (' '+el.className+' ').replace(/\s+/g, ' ').replace(' '+name+' ', '');
675 el.className = className + (state ? ' '+name : '')
676 }
677 }
678 }
679
680
681 function _css(el, prop, val){
682 var style = el && el.style;
683
684 if( style ){
685 if( val === void 0 ){
686 if( document.defaultView && document.defaultView.getComputedStyle ){
687 val = document.defaultView.getComputedStyle(el, '');
688 }
689 else if( el.currentStyle ){
690 val = el.currentStyle;
691 }
692
693 return prop === void 0 ? val : val[prop];
694 }
695 else {
696 if (!(prop in style)) {
697 prop = '-webkit-' + prop;
698 }
699
700 style[prop] = val + (typeof val === 'string' ? '' : 'px');
701 }
702 }
703 }
704
705
706 function _find(ctx, tagName, iterator){
707 if( ctx ){
708 var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
709 if( iterator ){
710 for( ; i < n; i++ ){
711 iterator(list[i], i);
712 }
713 }
714 return list;
715 }
716 return [];
717 }
718
719
720 function _disableDraggable(el){
721 return el.draggable = false;
722 }
723
724
725 function _unsilent(){
726 _silent = false;
727 }
728
729
730 function _ghostInBottom(el, evt){
731 var last = el.lastElementChild.getBoundingClientRect();
732 return evt.clientY - (last.top + last.height) > 5; // min delta
733 }
734
735
736 /**
737 * Generate id
738 * @param {HTMLElement} el
739 * @returns {String}
740 * @private
741 */
742 function _generateId(el) {
743 var str = el.tagName + el.className + el.src + el.href + el.textContent,
744 i = str.length,
745 sum = 0
746 ;
747
748 while (i--) {
749 sum += str.charCodeAt(i);
750 }
751
752 return sum.toString(36);
753 }
754
755
756 // Export utils
757 Sortable.utils = {
758 on: _on,
759 off: _off,
760 css: _css,
761 find: _find,
762 bind: _bind,
763 closest: _closest,
764 toggleClass: _toggleClass,
765 dispatchEvent: _dispatchEvent
766 };
767
768
769 Sortable.version = '0.6.0';
770
771
772 /**
773 * Create sortable instance
774 * @param {HTMLElement} el
775 * @param {Object} [options]
776 */
777 Sortable.create = function (el, options) {
778 return new Sortable(el, options)
779 };
780
781 // Export
782 return Sortable;
783});