Sortable.js 14.8 KB
Newer Older
R
RubaXa 已提交
1 2 3 4 5 6 7 8 9 10 11
/**!
 * Sortable
 * @author	RubaXa   <trash@rubaxa.org>
 * @license MIT
 */


(function (factory){
	"use strict";

	if( typeof define === "function" && define.amd ){
R
RubaXa 已提交
12
		define(factory);
R
RubaXa 已提交
13
	}
S
Scott Nelson 已提交
14 15 16
	else if( typeof module != "undefined" && typeof module.exports != "undefined" ){
		module.exports = factory();
	}
R
RubaXa 已提交
17 18 19 20 21 22 23
	else {
		window["Sortable"] = factory();
	}
})(function (){
	"use strict";

	var
M
Marius Petcu 已提交
24
		  dragEl
R
RubaXa 已提交
25 26 27 28 29 30
		, ghostEl
		, rootEl
		, nextEl

		, lastEl
		, lastCSS
R
RubaXa 已提交
31
		, lastRect
R
RubaXa 已提交
32 33 34 35 36 37 38 39 40 41 42

		, activeGroup

		, tapEvt
		, touchEvt

		, expando = 'Sortable' + (new Date).getTime()

		, win = window
		, document = win.document
		, parseInt = win.parseInt
R
RubaXa 已提交
43 44
		, supportIEdnd = !!document.createElement('div').dragDrop

R
RubaXa 已提交
45
		, _silent = false
R
RubaXa 已提交
46

R
RubaXa 已提交
47 48 49 50 51 52
		, _createEvent = function (event/**String*/, item/**HTMLElement*/){
			var evt = document.createEvent('Event');
			evt.initEvent(event, true, true);
			evt.item = item;
			return evt;
		}
R
RubaXa 已提交
53 54 55 56 57

		, noop = function (){}
		, slice = [].slice

		, touchDragOverListeners = []
M
Marius Petcu 已提交
58

M
Marius Petcu 已提交
59 60 61 62
		, pointerdown
		, pointerup
		, pointermove
		, pointercancel
R
RubaXa 已提交
63 64 65
	;


66

R
RubaXa 已提交
67 68 69
	/**
	 * @class  Sortable
	 * @param  {HTMLElement}  el
70
	 * @param  {Object}       [options]
R
RubaXa 已提交
71 72 73 74 75 76 77 78
	 */
	function Sortable(el, options){
		this.el = el; // root element
		this.options = options = (options || {});


		// Defaults
		options.group = options.group || Math.random();
79
		options.store = options.store || null;
R
RubaXa 已提交
80
		options.handle = options.handle || null;
R
RubaXa 已提交
81
		options.draggable = options.draggable || el.children[0] && el.children[0].nodeName || (/[uo]l/i.test(el.nodeName) ? 'li' : '*');
R
RubaXa 已提交
82
		options.ghostClass = options.ghostClass || 'sortable-ghost';
Z
ziflex 已提交
83
		options.ignore = options.ignore || 'a, img';
R
RubaXa 已提交
84 85 86 87

		options.onAdd = _bind(this, options.onAdd || noop);
		options.onUpdate = _bind(this, options.onUpdate || noop);
		options.onRemove = _bind(this, options.onRemove || noop);
S
skaczorowski 已提交
88 89
		options.onStart = _bind(this, options.onStart || noop);
		options.onEnd = _bind(this, options.onEnd || noop);
R
RubaXa 已提交
90 91


R
* JSDoc  
RubaXa 已提交
92
		// Export group name
R
RubaXa 已提交
93 94 95
		el[expando] = options.group;


R
* JSDoc  
RubaXa 已提交
96
		// Bind all private methods
R
RubaXa 已提交
97 98 99 100 101 102
		for( var fn in this ){
			if( fn.charAt(0) === '_' ){
				this[fn] = _bind(this, this[fn]);
			}
		}

M
Marius Petcu 已提交
103 104 105 106 107 108 109 110 111 112 113 114
		// Detect IE10/IE11+
		if (window.onpointerdown !== undefined) {
			pointerdown = 'pointerdown';
			pointerup = 'pointerup';
			pointermove = 'pointermove';
			pointercancel = 'pointercancel';
		} else {
			pointerdown = 'MSPointerDown';
			pointerup = 'MSPointerUp';
			pointermove = 'MSPointerMove';
			pointercancel = 'MSPointerCancel';
		}
R
RubaXa 已提交
115 116 117 118 119

		// Bind events
		_on(el, 'add', options.onAdd);
		_on(el, 'update', options.onUpdate);
		_on(el, 'remove', options.onRemove);
S
skaczorowski 已提交
120 121
		_on(el, 'start', options.onStart);
		_on(el, 'stop', options.onEnd);
R
RubaXa 已提交
122 123 124

		_on(el, 'mousedown', this._onTapStart);
		_on(el, 'touchstart', this._onTapStart);
R
RubaXa 已提交
125
		supportIEdnd && _on(el, 'selectstart', this._onTapStart);
R
RubaXa 已提交
126 127 128

		_on(el, 'dragover', this._onDragOver);
		_on(el, 'dragenter', this._onDragOver);
M
Marius Petcu 已提交
129
		_on(el, pointerdown, this._onTapStart);
M
Marius Petcu 已提交
130

M
Marius Petcu 已提交
131 132
		_css(el, 'touch-action', 'none');
		_css(el, '-ms-touch-action', 'none');
R
RubaXa 已提交
133 134

		touchDragOverListeners.push(this._onDragOver);
135 136 137

		// Restore sorting
		options.store && this.sort(options.store.get(this));
R
RubaXa 已提交
138 139 140
	}


141
	Sortable.prototype = /** @lends Sortable.prototype */ {
R
RubaXa 已提交
142 143 144 145 146 147 148 149
		constructor: Sortable,


		_applyEffects: function (){
			_toggleClass(dragEl, this.options.ghostClass, true);
		},


M
Marius Petcu 已提交
150
		_onTapStart: function (evt/**Event|TouchEvent|PointerEvent*/){
R
RubaXa 已提交
151
			var
M
Marius Petcu 已提交
152
				  touch = evt.touches && evt.touches[0]
R
RubaXa 已提交
153 154
				, target = (touch || evt).target
				, options =  this.options
R
RubaXa 已提交
155
				, el = this.el
R
RubaXa 已提交
156 157 158
			;

			if( options.handle ){
R
RubaXa 已提交
159
				target = _closest(target, options.handle, el);
R
RubaXa 已提交
160 161
			}

R
RubaXa 已提交
162
			target = _closest(target, options.draggable, el);
R
RubaXa 已提交
163

R
RubaXa 已提交
164
			// IE 9 Support
165 166 167 168 169
			if( target && evt.type == 'selectstart' ){
				if( target.tagName != 'A' && target.tagName != 'IMG'){
					target.dragDrop();
				}
			}
N
Nicolas 已提交
170

R
RubaXa 已提交
171
			if( target && !dragEl && (target.parentNode === el) ){
R
RubaXa 已提交
172 173 174 175
				tapEvt = evt;
				target.draggable = true;

				// Disable "draggable"
Z
ziflex 已提交
176 177 178
				Array.prototype.forEach.call(options.ignore.split(','), function (criteria) {
					_find(target, criteria.trim(), _disableDraggable);
				});
R
RubaXa 已提交
179 180 181 182

				if( touch ){
					// Touch device support
					tapEvt = {
M
Marius Petcu 已提交
183
						  target:  target
R
RubaXa 已提交
184 185 186 187 188 189
						, clientX: touch.clientX
						, clientY: touch.clientY
					};
					this._onDragStart(tapEvt, true);
					evt.preventDefault();
				}
M
Marius Petcu 已提交
190 191 192 193 194
				
				if (evt.type == 'pointerdown' || evt.type == 'MSPointerDown') {
					this._onDragStart(tapEvt, true);
					evt.preventDefault();
				}
R
RubaXa 已提交
195 196

				_on(this.el, 'dragstart', this._onDragStart);
R
RubaXa 已提交
197
				_on(this.el, 'dragend', this._onDrop);
R
RubaXa 已提交
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
				_on(document, 'dragover', _globalDragOver);


				try {
					if( document.selection ){
						document.selection.empty();
					} else {
						window.getSelection().removeAllRanges()
					}
				} catch (err){ }
			}
		},


		_emulateDragOver: function (){
			if( touchEvt ){
				_css(ghostEl, 'display', 'none');

				var
M
Marius Petcu 已提交
217
					  target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY)
R
RubaXa 已提交
218 219 220 221 222
					, parent = target
					, group = this.options.group
					, i = touchDragOverListeners.length
				;

L
Larry Davis 已提交
223 224 225 226 227 228 229 230 231 232 233 234
				if( parent ){
					do {
						if( parent[expando] === group ){
							while( i-- ){
								touchDragOverListeners[i]({
									clientX: touchEvt.clientX,
									clientY: touchEvt.clientY,
									target: target,
									rootEl: parent
								});
							}
							break;
R
RubaXa 已提交
235
						}
R
RubaXa 已提交
236

L
Larry Davis 已提交
237 238 239
						target = parent; // store last element
					}
					while( parent = parent.parentNode );
R
RubaXa 已提交
240 241 242 243 244 245 246
				}

				_css(ghostEl, 'display', '');
			}
		},


M
Marius Petcu 已提交
247
		_onTouchMove: function (evt/**TouchEvent|PointerEvent*/){
R
RubaXa 已提交
248 249
			if( tapEvt ){
				var
M
Marius Petcu 已提交
250
					  touch = evt.touches[0]
R
RubaXa 已提交
251 252 253 254 255 256
					, dx = touch.clientX - tapEvt.clientX
					, dy = touch.clientY - tapEvt.clientY
				;

				touchEvt = touch;
				_css(ghostEl, 'webkitTransform', 'translate3d('+dx+'px,'+dy+'px,0)');
M
Marius Petcu 已提交
257 258 259
				_css(ghostEl, 'mozTransform', 'translate3d('+dx+'px,'+dy+'px,0)');
				_css(ghostEl, 'msTransform', 'translate3d('+dx+'px,'+dy+'px,0)');
				_css(ghostEl, 'transform', 'translate3d('+dx+'px,'+dy+'px,0)');
M
Marius Petcu 已提交
260
				evt.preventDefault();
R
RubaXa 已提交
261 262 263 264
			}
		},


R
* JSDoc  
RubaXa 已提交
265
		_onDragStart: function (evt/**Event*/, isTouch/**Boolean*/){
R
RubaXa 已提交
266
			var
M
Marius Petcu 已提交
267
				  target = evt.target
R
RubaXa 已提交
268 269 270 271 272 273 274 275 276
				, dataTransfer = evt.dataTransfer
			;

			rootEl = this.el;
			dragEl = target;
			nextEl = target.nextSibling;
			activeGroup = this.options.group;

			if( isTouch ){
R
RubaXa 已提交
277
				var
M
Marius Petcu 已提交
278
					  rect = target.getBoundingClientRect()
R
RubaXa 已提交
279 280 281
					, css = _css(target)
					, ghostRect
				;
R
RubaXa 已提交
282 283 284 285 286

				ghostEl = target.cloneNode(true);

				_css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
				_css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
R
RubaXa 已提交
287 288
				_css(ghostEl, 'width', rect.width);
				_css(ghostEl, 'height', rect.height);
R
RubaXa 已提交
289 290 291 292
				_css(ghostEl, 'opacity', '0.8');
				_css(ghostEl, 'position', 'fixed');
				_css(ghostEl, 'zIndex', '100000');

R
RubaXa 已提交
293 294 295 296 297 298
				rootEl.appendChild(ghostEl);

				// Fixing dimensions.
				ghostRect = ghostEl.getBoundingClientRect();
				_css(ghostEl, 'width', rect.width*2 - ghostRect.width);
				_css(ghostEl, 'height', rect.height*2 - ghostRect.height);
R
RubaXa 已提交
299 300 301 302

				// Bind touch events
				_on(document, 'touchmove', this._onTouchMove);
				_on(document, 'touchend', this._onDrop);
M
Marius Petcu 已提交
303
				_on(document, 'touchcancel', this._onDrop);
M
Marius Petcu 已提交
304 305 306
				_on(document, pointermove, this._onTouchMove);
				_on(document, pointerup, this._onDrop);
				_on(document, pointercancel, this._onDrop);
R
RubaXa 已提交
307

R
RubaXa 已提交
308
				this._loopId = setInterval(this._emulateDragOver, 150);
R
RubaXa 已提交
309 310 311 312 313 314 315 316
			}
			else {
				dataTransfer.effectAllowed = 'move';
				dataTransfer.setData('Text', target.textContent);

				_on(document, 'drop', this._onDrop);
			}

S
skaczorowski 已提交
317
			dragEl.dispatchEvent(_createEvent('start', dragEl));
R
RubaXa 已提交
318 319 320 321
			setTimeout(this._applyEffects);
		},


R
* JSDoc  
RubaXa 已提交
322
		_onDragOver: function (evt/**Event*/){
R
RubaXa 已提交
323
			if( !_silent && (activeGroup === this.options.group) && (evt.rootEl === void 0 || evt.rootEl === this.el) ){
R
RubaXa 已提交
324
				var
M
Marius Petcu 已提交
325
					  el = this.el
R
RubaXa 已提交
326 327 328
					, target = _closest(evt.target, this.options.draggable, el)
				;

R
RubaXa 已提交
329
				if( el.children.length === 0 || el.children[0] === ghostEl || (el === evt.target) && _ghostInBottom(el, evt) ){
R
RubaXa 已提交
330 331 332 333 334
					el.appendChild(dragEl);
				}
				else if( target && target !== dragEl && (target.parentNode[expando] !== void 0) ){
					if( lastEl !== target ){
						lastEl = target;
R
RubaXa 已提交
335 336
						lastCSS = _css(target);
						lastRect = target.getBoundingClientRect();
R
RubaXa 已提交
337 338 339
					}


R
RubaXa 已提交
340
					var
M
Marius Petcu 已提交
341
						  rect = lastRect
R
RubaXa 已提交
342 343 344 345 346 347 348 349 350
						, width = rect.right - rect.left
						, height = rect.bottom - rect.top
						, floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display)
						, skew = (floating ? (evt.clientX - rect.left)/width : (evt.clientY - rect.top)/height) > .5
						, isWide = (target.offsetWidth > dragEl.offsetWidth)
						, isLong = (target.offsetHeight > dragEl.offsetHeight)
						, nextSibling = target.nextSibling
						, after
					;
R
RubaXa 已提交
351

R
RubaXa 已提交
352 353 354 355 356 357 358 359 360 361 362 363 364
					_silent = true;
					setTimeout(_unsilent, 30);

					if( floating ){
						after = (target.previousElementSibling === dragEl) && !isWide || (skew > .5) && isWide
					} else {
						after = (target.nextElementSibling !== dragEl) && !isLong || (skew > .5) && isLong;
					}

					if( after && !nextSibling ){
						el.appendChild(dragEl);
					} else {
						target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
R
RubaXa 已提交
365 366 367 368 369 370 371 372 373 374 375 376 377
					}
				}
			}
		},


		_onDrop: function (evt/**Event*/){
			clearInterval(this._loopId);

			// Unbind events
			_off(document, 'drop', this._onDrop);
			_off(document, 'dragover', _globalDragOver);

R
RubaXa 已提交
378
			_off(this.el, 'dragend', this._onDrop);
R
RubaXa 已提交
379
			_off(this.el, 'dragstart', this._onDragStart);
N
Nicolas 已提交
380
			_off(this.el, 'selectstart', this._onTapStart);
R
RubaXa 已提交
381 382 383

			_off(document, 'touchmove', this._onTouchMove);
			_off(document, 'touchend', this._onDrop);
M
Marius Petcu 已提交
384
			_off(document, 'touchcancel', this._onDrop);
M
Marius Petcu 已提交
385 386 387
			_off(document, pointermove, this._onTouchMove);
			_off(document, pointerup, this._onDrop);
			_off(document, pointercancel, this._onDrop);
R
RubaXa 已提交
388 389 390 391


			if( evt ){
				evt.preventDefault();
R
RubaXa 已提交
392
				evt.stopPropagation();
R
RubaXa 已提交
393

R
RubaXa 已提交
394 395 396 397
				if( ghostEl ){
					ghostEl.parentNode.removeChild(ghostEl);
				}

R
RubaXa 已提交
398
				if( dragEl ){
399
					_disableDraggable(dragEl);
R
RubaXa 已提交
400 401 402 403
					_toggleClass(dragEl, this.options.ghostClass, false);

					if( !rootEl.contains(dragEl) ){
						// Remove event
R
RubaXa 已提交
404
						rootEl.dispatchEvent(_createEvent('remove', dragEl));
R
RubaXa 已提交
405 406

						// Add event
R
RubaXa 已提交
407
						dragEl.dispatchEvent(_createEvent('add', dragEl));
R
RubaXa 已提交
408 409 410
					}
					else if( dragEl.nextSibling !== nextEl ){
						// Update event
R
RubaXa 已提交
411
						dragEl.dispatchEvent(_createEvent('update', dragEl));
R
RubaXa 已提交
412
					}
413

S
skaczorowski 已提交
414
					dragEl.dispatchEvent(_createEvent('stop', dragEl));
R
RubaXa 已提交
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
				}

				// Set NULL
				rootEl =
				dragEl =
				ghostEl =
				nextEl =

				tapEvt =
				touchEvt =

				lastEl =
				lastCSS =

				activeGroup = null;
430 431 432

				// Save sorting
				this.options.store && this.options.store.set(this);
R
RubaXa 已提交
433 434 435 436
			}
		},


437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481
		/**
		 * Serializes the item into an array of string.
		 * @returns {String[]}
		 */
		toArray: function () {
			var order = [],
				el,
				children = this.el.children,
				i = 0,
				n = children.length
			;

			for (; i < n; i++) {
				el = children[i];
				order.push(el.getAttribute('data-id') || _generateId(el));
			}

			return order;
		},


		/**
		 * Sorts the elements according to the array.
		 * @param  {String[]}  order  order of the items
		 */
		sort: function (order) {
			var items = {}, el = this.el;

			this.toArray().forEach(function (id, i) {
				items[id] = el.children[i];
			});

			order.forEach(function (id) {
				if (items[id]) {
					el.removeChild(items[id]);
					el.appendChild(items[id]);
				}
			});
		},


		/**
		 * Destroy
		 */
		destroy: function () {
R
RubaXa 已提交
482 483 484 485 486
			var el = this.el, options = this.options;

			_off(el, 'add', options.onAdd);
			_off(el, 'update', options.onUpdate);
			_off(el, 'remove', options.onRemove);
S
skaczorowski 已提交
487 488
			_off(el, 'start', options.onStart);
			_off(el, 'stop', options.onEnd);
R
RubaXa 已提交
489 490
			_off(el, 'mousedown', this._onTapStart);
			_off(el, 'touchstart', this._onTapStart);
N
Nicolas 已提交
491
			_off(el, 'selectstart', this._onTapStart);
M
Marius Petcu 已提交
492
			_off(el, pointerdown, this._onTapStart);
R
RubaXa 已提交
493 494 495 496

			_off(el, 'dragover', this._onDragOver);
			_off(el, 'dragenter', this._onDragOver);

497 498 499 500 501
			//remove draggable attributes
			Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function(el) {
				el.removeAttribute('draggable');
			});

R
RubaXa 已提交
502 503 504 505 506 507 508 509
			touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);

			this._onDrop();

			this.el = null;
		}
	};

510

R
RubaXa 已提交
511 512 513 514 515 516 517 518 519
	function _bind(ctx, fn){
		var args = slice.call(arguments, 2);
		return	fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function (){
			return fn.apply(ctx, args.concat(slice.call(arguments)));
		};
	}


	function _closest(el, selector, ctx){
R
RubaXa 已提交
520 521 522 523
		if( selector === '*' ){
			return el;
		}
		else if( el ){
R
RubaXa 已提交
524 525 526 527
			ctx = ctx || document;
			selector = selector.split('.');

			var
M
Marius Petcu 已提交
528
				  tag = selector.shift().toUpperCase()
R
RubaXa 已提交
529
				, re = new RegExp('\\s('+selector.join('|')+')\\s', 'g')
R
RubaXa 已提交
530 531 532 533
			;

			do {
				if(
M
Marius Petcu 已提交
534
					   (tag === '' || el.nodeName == tag)
R
RubaXa 已提交
535
					&& (!selector.length || ((' '+el.className+' ').match(re) || []).length == selector.length)
R
RubaXa 已提交
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568
				){
					return	el;
				}
			}
			while( el !== ctx && (el = el.parentNode) );
		}

		return	null;
	}


	function _globalDragOver(evt){
		evt.dataTransfer.dropEffect = 'move';
		evt.preventDefault();
	}


	function _on(el, event, fn){
		el.addEventListener(event, fn, false);
	}


	function _off(el, event, fn){
		el.removeEventListener(event, fn, false);
	}


	function _toggleClass(el, name, state){
		if( el ){
			if( el.classList ){
				el.classList[state ? 'add' : 'remove'](name);
			}
			else {
R
RubaXa 已提交
569
				var className = (' '+el.className+' ').replace(/\s+/g, ' ').replace(' '+name+' ', '');
R
RubaXa 已提交
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
				el.className = className + (state ? ' '+name : '')
			}
		}
	}


	function _css(el, prop, val){
		if( el && el.style ){
			if( val === void 0 ){
				if( document.defaultView && document.defaultView.getComputedStyle ){
					val = document.defaultView.getComputedStyle(el, '');
				}
				else if( el.currentStyle ){
					val	= el.currentStyle;
				}
				return	prop === void 0 ? val : val[prop];
			} else {
				el.style[prop] = val + (typeof val === 'string' ? '' : 'px');
			}
		}
	}


	function _find(ctx, tagName, iterator){
		if( ctx ){
			var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
			if( iterator ){
				for( ; i < n; i++ ){
					iterator(list[i], i);
				}
			}
			return	list;
		}
		return	[];
	}


	function _disableDraggable(el){
		return el.draggable = false;
	}


R
RubaXa 已提交
612 613 614 615 616
	function _unsilent(){
		_silent = false;
	}


R
RubaXa 已提交
617 618 619 620 621 622
	function _ghostInBottom(el, evt){
		var last = el.lastElementChild.getBoundingClientRect();
		return evt.clientY - (last.top + last.height) > 5; // min delta
	}


623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639
	/**
	 * Generate id
	 * @param   {HTMLElement} el
	 * @returns {String}
	 * @private
	 */
	function _generateId(el) {
		var str = el.innerHTML + el.className + el.src,
			i = str.length,
			sum = 0
		;
		while (i--) {
			sum += str.charCodeAt(i);
		}
		return sum.toString(36);
	}

R
RubaXa 已提交
640 641 642 643 644 645 646 647 648 649 650 651 652

	// Export utils
	Sortable.utils = {
		on: _on,
		off: _off,
		css: _css,
		find: _find,
		bind: _bind,
		closest: _closest,
		toggleClass: _toggleClass
	};


653 654
	Sortable.version = '0.4.0';

R
RubaXa 已提交
655 656 657 658

	// Export
	return	Sortable;
});