Last active
August 29, 2015 14:25
-
-
Save dux/61120ded60b2bf550903 to your computer and use it in GitHub Desktop.
Best Sortable and Draggable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Sortable test Dino</title> | |
</head> | |
<body> | |
<style> | |
ul { margin:0; padding:0; width:200px; float:left; margin-right:50px; } | |
ul li { border:1px solid #ccc; background-color:#eee; margin-bottom:10px; padding:10px; } | |
ul li.ghost { opacity:0.5; border:1px dashed #aaa; } | |
</style> | |
<ul class="sortable"> | |
<li>Item 1</li> | |
<li>Item 2</li> | |
<li>Item 3</li> | |
<li>Item 4</li> | |
<li>Item 5</li> | |
</ul> | |
<ul class="sortable"> | |
<li>Item 6</li> | |
<li>Item 7</li> | |
<li>Item 8</li> | |
<li>Item 9</li> | |
</ul> | |
<script src="./sortable.js"></script> | |
<script> | |
var update_list = function (evt) { | |
var itemEl = evt.item; // dragged HTMLElement | |
alert('update'); | |
}; | |
list = document.getElementsByClassName('sortable'); | |
for (var i = 0; i < list.length; i++) { | |
var sortable = Sortable.create(list[i], { | |
group: "words", | |
ghostClass: 'ghost', | |
onUpdate: update_list, | |
onRemove: update_list, | |
onAdd: update_list | |
}); | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/**! | |
* Sortable | |
* @author RubaXa <[email protected]> | |
* @license MIT | |
* https://github.com/RubaXa/Sortable | |
*/ | |
(function (factory) { | |
"use strict"; | |
if (typeof define === "function" && define.amd) { | |
define(factory); | |
} | |
else if (typeof module != "undefined" && typeof module.exports != "undefined") { | |
module.exports = factory(); | |
} | |
else if (typeof Package !== "undefined") { | |
Sortable = factory(); // export for Meteor.js | |
} | |
else { | |
/* jshint sub:true */ | |
window["Sortable"] = factory(); | |
} | |
})(function () { | |
"use strict"; | |
var dragEl, | |
ghostEl, | |
cloneEl, | |
rootEl, | |
nextEl, | |
scrollEl, | |
scrollParentEl, | |
lastEl, | |
lastCSS, | |
oldIndex, | |
newIndex, | |
activeGroup, | |
autoScroll = {}, | |
tapEvt, | |
touchEvt, | |
/** @const */ | |
RSPACE = /\s+/g, | |
expando = 'Sortable' + (new Date).getTime(), | |
win = window, | |
document = win.document, | |
parseInt = win.parseInt, | |
supportDraggable = !!('draggable' in document.createElement('div')), | |
_silent = false, | |
abs = Math.abs, | |
slice = [].slice, | |
touchDragOverListeners = [], | |
_autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) { | |
// Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 | |
if (rootEl && options.scroll) { | |
var el, | |
rect, | |
sens = options.scrollSensitivity, | |
speed = options.scrollSpeed, | |
x = evt.clientX, | |
y = evt.clientY, | |
winWidth = window.innerWidth, | |
winHeight = window.innerHeight, | |
vx, | |
vy | |
; | |
// Delect scrollEl | |
if (scrollParentEl !== rootEl) { | |
scrollEl = options.scroll; | |
scrollParentEl = rootEl; | |
if (scrollEl === true) { | |
scrollEl = rootEl; | |
do { | |
if ((scrollEl.offsetWidth < scrollEl.scrollWidth) || | |
(scrollEl.offsetHeight < scrollEl.scrollHeight) | |
) { | |
break; | |
} | |
/* jshint boss:true */ | |
} while (scrollEl = scrollEl.parentNode); | |
} | |
} | |
if (scrollEl) { | |
el = scrollEl; | |
rect = scrollEl.getBoundingClientRect(); | |
vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens); | |
vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens); | |
} | |
if (!(vx || vy)) { | |
vx = (winWidth - x <= sens) - (x <= sens); | |
vy = (winHeight - y <= sens) - (y <= sens); | |
/* jshint expr:true */ | |
(vx || vy) && (el = win); | |
} | |
if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) { | |
autoScroll.el = el; | |
autoScroll.vx = vx; | |
autoScroll.vy = vy; | |
clearInterval(autoScroll.pid); | |
if (el) { | |
autoScroll.pid = setInterval(function () { | |
if (el === win) { | |
win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed); | |
} else { | |
vy && (el.scrollTop += vy * speed); | |
vx && (el.scrollLeft += vx * speed); | |
} | |
}, 24); | |
} | |
} | |
} | |
}, 30) | |
; | |
/** | |
* @class Sortable | |
* @param {HTMLElement} el | |
* @param {Object} [options] | |
*/ | |
function Sortable(el, options) { | |
this.el = el; // root element | |
this.options = options = _extend({}, options); | |
// Export instance | |
el[expando] = this; | |
// Default options | |
var defaults = { | |
group: Math.random(), | |
sort: true, | |
disabled: false, | |
store: null, | |
handle: null, | |
scroll: true, | |
scrollSensitivity: 30, | |
scrollSpeed: 10, | |
draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*', | |
ghostClass: 'sortable-ghost', | |
ignore: 'a, img', | |
filter: null, | |
animation: 0, | |
setData: function (dataTransfer, dragEl) { | |
dataTransfer.setData('Text', dragEl.textContent); | |
}, | |
dropBubble: false, | |
dragoverBubble: false, | |
dataIdAttr: 'data-id', | |
delay: 0 | |
}; | |
// Set default options | |
for (var name in defaults) { | |
!(name in options) && (options[name] = defaults[name]); | |
} | |
var group = options.group; | |
if (!group || typeof group != 'object') { | |
group = options.group = { name: group }; | |
} | |
['pull', 'put'].forEach(function (key) { | |
if (!(key in group)) { | |
group[key] = true; | |
} | |
}); | |
options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' '; | |
// Bind all private methods | |
for (var fn in this) { | |
if (fn.charAt(0) === '_') { | |
this[fn] = _bind(this, this[fn]); | |
} | |
} | |
// Bind events | |
_on(el, 'mousedown', this._onTapStart); | |
_on(el, 'touchstart', this._onTapStart); | |
_on(el, 'dragover', this); | |
_on(el, 'dragenter', this); | |
touchDragOverListeners.push(this._onDragOver); | |
// Restore sorting | |
options.store && this.sort(options.store.get(this)); | |
} | |
Sortable.prototype = /** @lends Sortable.prototype */ { | |
constructor: Sortable, | |
_onTapStart: function (/** Event|TouchEvent */evt) { | |
var _this = this, | |
el = this.el, | |
options = this.options, | |
type = evt.type, | |
touch = evt.touches && evt.touches[0], | |
target = (touch || evt).target, | |
originalTarget = target, | |
filter = options.filter; | |
if (type === 'mousedown' && evt.button !== 0 || options.disabled) { | |
return; // only left button or enabled | |
} | |
target = _closest(target, options.draggable, el); | |
if (!target) { | |
return; | |
} | |
// get the index of the dragged element within its parent | |
oldIndex = _index(target); | |
// Check filter | |
if (typeof filter === 'function') { | |
if (filter.call(this, evt, target, this)) { | |
_dispatchEvent(_this, originalTarget, 'filter', target, el, oldIndex); | |
evt.preventDefault(); | |
return; // cancel dnd | |
} | |
} | |
else if (filter) { | |
filter = filter.split(',').some(function (criteria) { | |
criteria = _closest(originalTarget, criteria.trim(), el); | |
if (criteria) { | |
_dispatchEvent(_this, criteria, 'filter', target, el, oldIndex); | |
return true; | |
} | |
}); | |
if (filter) { | |
evt.preventDefault(); | |
return; // cancel dnd | |
} | |
} | |
if (options.handle && !_closest(originalTarget, options.handle, el)) { | |
return; | |
} | |
// Prepare `dragstart` | |
this._prepareDragStart(evt, touch, target); | |
}, | |
_prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target) { | |
var _this = this, | |
el = _this.el, | |
options = _this.options, | |
ownerDocument = el.ownerDocument, | |
dragStartFn; | |
if (target && !dragEl && (target.parentNode === el)) { | |
tapEvt = evt; | |
rootEl = el; | |
dragEl = target; | |
nextEl = dragEl.nextSibling; | |
activeGroup = options.group; | |
dragStartFn = function () { | |
// Delayed drag has been triggered | |
// we can re-enable the events: touchmove/mousemove | |
_this._disableDelayedDrag(); | |
// Make the element draggable | |
dragEl.draggable = true; | |
// Disable "draggable" | |
options.ignore.split(',').forEach(function (criteria) { | |
_find(dragEl, criteria.trim(), _disableDraggable); | |
}); | |
// Bind the events: dragstart/dragend | |
_this._triggerDragStart(touch); | |
}; | |
_on(ownerDocument, 'mouseup', _this._onDrop); | |
_on(ownerDocument, 'touchend', _this._onDrop); | |
_on(ownerDocument, 'touchcancel', _this._onDrop); | |
if (options.delay) { | |
// If the user moves the pointer before the delay has been reached: | |
// disable the delayed drag | |
_on(ownerDocument, 'mousemove', _this._disableDelayedDrag); | |
_on(ownerDocument, 'touchmove', _this._disableDelayedDrag); | |
_this._dragStartTimer = setTimeout(dragStartFn, options.delay); | |
} else { | |
dragStartFn(); | |
} | |
} | |
}, | |
_disableDelayedDrag: function () { | |
var ownerDocument = this.el.ownerDocument; | |
clearTimeout(this._dragStartTimer); | |
_off(ownerDocument, 'mousemove', this._disableDelayedDrag); | |
_off(ownerDocument, 'touchmove', this._disableDelayedDrag); | |
}, | |
_triggerDragStart: function (/** Touch */touch) { | |
if (touch) { | |
// Touch device support | |
tapEvt = { | |
target: dragEl, | |
clientX: touch.clientX, | |
clientY: touch.clientY | |
}; | |
this._onDragStart(tapEvt, 'touch'); | |
} | |
else if (!supportDraggable) { | |
this._onDragStart(tapEvt, true); | |
} | |
else { | |
_on(dragEl, 'dragend', this); | |
_on(rootEl, 'dragstart', this._onDragStart); | |
} | |
try { | |
if (document.selection) { | |
document.selection.empty(); | |
} else { | |
window.getSelection().removeAllRanges(); | |
} | |
} catch (err) { | |
} | |
}, | |
_dragStarted: function () { | |
if (rootEl && dragEl) { | |
// Apply effect | |
_toggleClass(dragEl, this.options.ghostClass, true); | |
Sortable.active = this; | |
// Drag start event | |
_dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex); | |
} | |
}, | |
_emulateDragOver: function () { | |
if (touchEvt) { | |
_css(ghostEl, 'display', 'none'); | |
var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY), | |
parent = target, | |
groupName = ' ' + this.options.group.name + '', | |
i = touchDragOverListeners.length; | |
if (parent) { | |
do { | |
if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) { | |
while (i--) { | |
touchDragOverListeners[i]({ | |
clientX: touchEvt.clientX, | |
clientY: touchEvt.clientY, | |
target: target, | |
rootEl: parent | |
}); | |
} | |
break; | |
} | |
target = parent; // store last element | |
} | |
/* jshint boss:true */ | |
while (parent = parent.parentNode); | |
} | |
_css(ghostEl, 'display', ''); | |
} | |
}, | |
_onTouchMove: function (/**TouchEvent*/evt) { | |
if (tapEvt) { | |
var touch = evt.touches ? evt.touches[0] : evt, | |
dx = touch.clientX - tapEvt.clientX, | |
dy = touch.clientY - tapEvt.clientY, | |
translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; | |
touchEvt = touch; | |
_css(ghostEl, 'webkitTransform', translate3d); | |
_css(ghostEl, 'mozTransform', translate3d); | |
_css(ghostEl, 'msTransform', translate3d); | |
_css(ghostEl, 'transform', translate3d); | |
evt.preventDefault(); | |
} | |
}, | |
_onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { | |
var dataTransfer = evt.dataTransfer, | |
options = this.options; | |
this._offUpEvents(); | |
if (activeGroup.pull == 'clone') { | |
cloneEl = dragEl.cloneNode(true); | |
_css(cloneEl, 'display', 'none'); | |
rootEl.insertBefore(cloneEl, dragEl); | |
} | |
if (useFallback) { | |
var rect = dragEl.getBoundingClientRect(), | |
css = _css(dragEl), | |
ghostRect; | |
ghostEl = dragEl.cloneNode(true); | |
_css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); | |
_css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); | |
_css(ghostEl, 'width', rect.width); | |
_css(ghostEl, 'height', rect.height); | |
_css(ghostEl, 'opacity', '0.8'); | |
_css(ghostEl, 'position', 'fixed'); | |
_css(ghostEl, 'zIndex', '100000'); | |
rootEl.appendChild(ghostEl); | |
// Fixing dimensions. | |
ghostRect = ghostEl.getBoundingClientRect(); | |
_css(ghostEl, 'width', rect.width * 2 - ghostRect.width); | |
_css(ghostEl, 'height', rect.height * 2 - ghostRect.height); | |
if (useFallback === 'touch') { | |
// Bind touch events | |
_on(document, 'touchmove', this._onTouchMove); | |
_on(document, 'touchend', this._onDrop); | |
_on(document, 'touchcancel', this._onDrop); | |
} else { | |
// Old brwoser | |
_on(document, 'mousemove', this._onTouchMove); | |
_on(document, 'mouseup', this._onDrop); | |
} | |
this._loopId = setInterval(this._emulateDragOver, 150); | |
} | |
else { | |
if (dataTransfer) { | |
dataTransfer.effectAllowed = 'move'; | |
options.setData && options.setData.call(this, dataTransfer, dragEl); | |
} | |
_on(document, 'drop', this); | |
} | |
setTimeout(this._dragStarted, 0); | |
}, | |
_onDragOver: function (/**Event*/evt) { | |
var el = this.el, | |
target, | |
dragRect, | |
revert, | |
options = this.options, | |
group = options.group, | |
groupPut = group.put, | |
isOwner = (activeGroup === group), | |
canSort = options.sort; | |
if (evt.preventDefault !== void 0) { | |
evt.preventDefault(); | |
!options.dragoverBubble && evt.stopPropagation(); | |
} | |
if (activeGroup && !options.disabled && | |
(isOwner | |
? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list | |
: activeGroup.pull && groupPut && ( | |
(activeGroup.name === group.name) || // by Name | |
(groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array | |
) | |
) && | |
(evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback | |
) { | |
// Smart auto-scrolling | |
_autoScroll(evt, options, this.el); | |
if (_silent) { | |
return; | |
} | |
target = _closest(evt.target, options.draggable, el); | |
dragRect = dragEl.getBoundingClientRect(); | |
if (revert) { | |
_cloneHide(true); | |
if (cloneEl || nextEl) { | |
rootEl.insertBefore(dragEl, cloneEl || nextEl); | |
} | |
else if (!canSort) { | |
rootEl.appendChild(dragEl); | |
} | |
return; | |
} | |
if ((el.children.length === 0) || (el.children[0] === ghostEl) || | |
(el === evt.target) && (target = _ghostInBottom(el, evt)) | |
) { | |
if (target) { | |
if (target.animated) { | |
return; | |
} | |
targetRect = target.getBoundingClientRect(); | |
} | |
_cloneHide(isOwner); | |
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) { | |
el.appendChild(dragEl); | |
this._animate(dragRect, dragEl); | |
target && this._animate(targetRect, target); | |
} | |
} | |
else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) { | |
if (lastEl !== target) { | |
lastEl = target; | |
lastCSS = _css(target); | |
} | |
var targetRect = target.getBoundingClientRect(), | |
width = targetRect.right - targetRect.left, | |
height = targetRect.bottom - targetRect.top, | |
floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display), | |
isWide = (target.offsetWidth > dragEl.offsetWidth), | |
isLong = (target.offsetHeight > dragEl.offsetHeight), | |
halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, | |
nextSibling = target.nextElementSibling, | |
moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect), | |
after | |
; | |
if (moveVector !== false) { | |
_silent = true; | |
setTimeout(_unsilent, 30); | |
_cloneHide(isOwner); | |
if (moveVector === 1 || moveVector === -1) { | |
after = (moveVector === 1); | |
} | |
else if (floating) { | |
after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide; | |
} else { | |
after = (nextSibling !== dragEl) && !isLong || halfway && isLong; | |
} | |
if (after && !nextSibling) { | |
el.appendChild(dragEl); | |
} else { | |
target.parentNode.insertBefore(dragEl, after ? nextSibling : target); | |
} | |
this._animate(dragRect, dragEl); | |
this._animate(targetRect, target); | |
} | |
} | |
} | |
}, | |
_animate: function (prevRect, target) { | |
var ms = this.options.animation; | |
if (ms) { | |
var currentRect = target.getBoundingClientRect(); | |
_css(target, 'transition', 'none'); | |
_css(target, 'transform', 'translate3d(' | |
+ (prevRect.left - currentRect.left) + 'px,' | |
+ (prevRect.top - currentRect.top) + 'px,0)' | |
); | |
target.offsetWidth; // repaint | |
_css(target, 'transition', 'all ' + ms + 'ms'); | |
_css(target, 'transform', 'translate3d(0,0,0)'); | |
clearTimeout(target.animated); | |
target.animated = setTimeout(function () { | |
_css(target, 'transition', ''); | |
_css(target, 'transform', ''); | |
target.animated = false; | |
}, ms); | |
} | |
}, | |
_offUpEvents: function () { | |
var ownerDocument = this.el.ownerDocument; | |
_off(document, 'touchmove', this._onTouchMove); | |
_off(ownerDocument, 'mouseup', this._onDrop); | |
_off(ownerDocument, 'touchend', this._onDrop); | |
_off(ownerDocument, 'touchcancel', this._onDrop); | |
}, | |
_onDrop: function (/**Event*/evt) { | |
var el = this.el, | |
options = this.options; | |
clearInterval(this._loopId); | |
clearInterval(autoScroll.pid); | |
clearTimeout(this.dragStartTimer); | |
// Unbind events | |
_off(document, 'drop', this); | |
_off(document, 'mousemove', this._onTouchMove); | |
_off(el, 'dragstart', this._onDragStart); | |
this._offUpEvents(); | |
if (evt) { | |
evt.preventDefault(); | |
!options.dropBubble && evt.stopPropagation(); | |
ghostEl && ghostEl.parentNode.removeChild(ghostEl); | |
if (dragEl) { | |
_off(dragEl, 'dragend', this); | |
_disableDraggable(dragEl); | |
_toggleClass(dragEl, this.options.ghostClass, false); | |
if (rootEl !== dragEl.parentNode) { | |
newIndex = _index(dragEl); | |
// drag from one list and drop into another | |
_dispatchEvent(null, dragEl.parentNode, 'sort', dragEl, rootEl, oldIndex, newIndex); | |
_dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex); | |
// Add event | |
_dispatchEvent(null, dragEl.parentNode, 'add', dragEl, rootEl, oldIndex, newIndex); | |
// Remove event | |
_dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex); | |
} | |
else { | |
// Remove clone | |
cloneEl && cloneEl.parentNode.removeChild(cloneEl); | |
if (dragEl.nextSibling !== nextEl) { | |
// Get the index of the dragged element within its parent | |
newIndex = _index(dragEl); | |
// drag & drop within the same list | |
_dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex); | |
_dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex); | |
} | |
} | |
if (Sortable.active) { | |
// Drag end event | |
_dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex); | |
// Save sorting | |
this.save(); | |
} | |
} | |
// Nulling | |
rootEl = | |
dragEl = | |
ghostEl = | |
nextEl = | |
cloneEl = | |
scrollEl = | |
scrollParentEl = | |
tapEvt = | |
touchEvt = | |
lastEl = | |
lastCSS = | |
activeGroup = | |
Sortable.active = null; | |
} | |
}, | |
handleEvent: function (/**Event*/evt) { | |
var type = evt.type; | |
if (type === 'dragover' || type === 'dragenter') { | |
if (dragEl) { | |
this._onDragOver(evt); | |
_globalDragOver(evt); | |
} | |
} | |
else if (type === 'drop' || type === 'dragend') { | |
this._onDrop(evt); | |
} | |
}, | |
/** | |
* Serializes the item into an array of string. | |
* @returns {String[]} | |
*/ | |
toArray: function () { | |
var order = [], | |
el, | |
children = this.el.children, | |
i = 0, | |
n = children.length, | |
options = this.options; | |
for (; i < n; i++) { | |
el = children[i]; | |
if (_closest(el, options.draggable, this.el)) { | |
order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); | |
} | |
} | |
return order; | |
}, | |
/** | |
* Sorts the elements according to the array. | |
* @param {String[]} order order of the items | |
*/ | |
sort: function (order) { | |
var items = {}, rootEl = this.el; | |
this.toArray().forEach(function (id, i) { | |
var el = rootEl.children[i]; | |
if (_closest(el, this.options.draggable, rootEl)) { | |
items[id] = el; | |
} | |
}, this); | |
order.forEach(function (id) { | |
if (items[id]) { | |
rootEl.removeChild(items[id]); | |
rootEl.appendChild(items[id]); | |
} | |
}); | |
}, | |
/** | |
* Save the current sorting | |
*/ | |
save: function () { | |
var store = this.options.store; | |
store && store.set(this); | |
}, | |
/** | |
* 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. | |
* @param {HTMLElement} el | |
* @param {String} [selector] default: `options.draggable` | |
* @returns {HTMLElement|null} | |
*/ | |
closest: function (el, selector) { | |
return _closest(el, selector || this.options.draggable, this.el); | |
}, | |
/** | |
* Set/get option | |
* @param {string} name | |
* @param {*} [value] | |
* @returns {*} | |
*/ | |
option: function (name, value) { | |
var options = this.options; | |
if (value === void 0) { | |
return options[name]; | |
} else { | |
options[name] = value; | |
} | |
}, | |
/** | |
* Destroy | |
*/ | |
destroy: function () { | |
var el = this.el; | |
el[expando] = null; | |
_off(el, 'mousedown', this._onTapStart); | |
_off(el, 'touchstart', this._onTapStart); | |
_off(el, 'dragover', this); | |
_off(el, 'dragenter', this); | |
// Remove draggable attributes | |
Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { | |
el.removeAttribute('draggable'); | |
}); | |
touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1); | |
this._onDrop(); | |
this.el = el = null; | |
} | |
}; | |
function _cloneHide(state) { | |
if (cloneEl && (cloneEl.state !== state)) { | |
_css(cloneEl, 'display', state ? 'none' : ''); | |
!state && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl); | |
cloneEl.state = state; | |
} | |
} | |
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(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) { | |
if (el) { | |
ctx = ctx || document; | |
selector = selector.split('.'); | |
var tag = selector.shift().toUpperCase(), | |
re = new RegExp('\\s(' + selector.join('|') + ')\\s', 'g'); | |
do { | |
if ( | |
(tag === '>*' && el.parentNode === ctx) || ( | |
(tag === '' || el.nodeName.toUpperCase() == tag) && | |
(!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length) | |
) | |
) { | |
return el; | |
} | |
} | |
while (el !== ctx && (el = el.parentNode)); | |
} | |
return null; | |
} | |
function _globalDragOver(/**Event*/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 { | |
var className = (' ' + el.className + ' ').replace(RSPACE, ' ').replace(' ' + name + ' ', ' '); | |
el.className = (className + (state ? ' ' + name : '')).replace(RSPACE, ' '); | |
} | |
} | |
} | |
function _css(el, prop, val) { | |
var style = el && el.style; | |
if (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 { | |
if (!(prop in style)) { | |
prop = '-webkit-' + prop; | |
} | |
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 _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) { | |
var evt = document.createEvent('Event'), | |
options = (sortable || rootEl[expando]).options, | |
onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); | |
evt.initEvent(name, true, true); | |
evt.to = rootEl; | |
evt.from = fromEl || rootEl; | |
evt.item = targetEl || rootEl; | |
evt.clone = cloneEl; | |
evt.oldIndex = startIndex; | |
evt.newIndex = newIndex; | |
rootEl.dispatchEvent(evt); | |
if (options[onName]) { | |
options[onName].call(sortable, evt); | |
} | |
} | |
function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect) { | |
var evt, | |
sortable = fromEl[expando], | |
onMoveFn = sortable.options.onMove, | |
retVal; | |
if (onMoveFn) { | |
evt = document.createEvent('Event'); | |
evt.initEvent('move', true, true); | |
evt.to = toEl; | |
evt.from = fromEl; | |
evt.dragged = dragEl; | |
evt.draggedRect = dragRect; | |
evt.related = targetEl || toEl; | |
evt.relatedRect = targetRect || toEl.getBoundingClientRect(); | |
retVal = onMoveFn.call(sortable, evt); | |
} | |
return retVal; | |
} | |
function _disableDraggable(el) { | |
el.draggable = false; | |
} | |
function _unsilent() { | |
_silent = false; | |
} | |
/** @returns {HTMLElement|false} */ | |
function _ghostInBottom(el, evt) { | |
var lastEl = el.lastElementChild, | |
rect = lastEl.getBoundingClientRect(); | |
return (evt.clientY - (rect.top + rect.height) > 5) && lastEl; // min delta | |
} | |
/** | |
* Generate id | |
* @param {HTMLElement} el | |
* @returns {String} | |
* @private | |
*/ | |
function _generateId(el) { | |
var str = el.tagName + el.className + el.src + el.href + el.textContent, | |
i = str.length, | |
sum = 0; | |
while (i--) { | |
sum += str.charCodeAt(i); | |
} | |
return sum.toString(36); | |
} | |
/** | |
* Returns the index of an element within its parent | |
* @param el | |
* @returns {number} | |
* @private | |
*/ | |
function _index(/**HTMLElement*/el) { | |
var index = 0; | |
while (el && (el = el.previousElementSibling)) { | |
if (el.nodeName.toUpperCase() !== 'TEMPLATE') { | |
index++; | |
} | |
} | |
return index; | |
} | |
function _throttle(callback, ms) { | |
var args, _this; | |
return function () { | |
if (args === void 0) { | |
args = arguments; | |
_this = this; | |
setTimeout(function () { | |
if (args.length === 1) { | |
callback.call(_this, args[0]); | |
} else { | |
callback.apply(_this, args); | |
} | |
args = void 0; | |
}, ms); | |
} | |
}; | |
} | |
function _extend(dst, src) { | |
if (dst && src) { | |
for (var key in src) { | |
if (src.hasOwnProperty(key)) { | |
dst[key] = src[key]; | |
} | |
} | |
} | |
return dst; | |
} | |
// Export utils | |
Sortable.utils = { | |
on: _on, | |
off: _off, | |
css: _css, | |
find: _find, | |
bind: _bind, | |
is: function (el, selector) { | |
return !!_closest(el, selector, el); | |
}, | |
extend: _extend, | |
throttle: _throttle, | |
closest: _closest, | |
toggleClass: _toggleClass, | |
index: _index | |
}; | |
Sortable.version = '1.2.0'; | |
/** | |
* Create sortable instance | |
* @param {HTMLElement} el | |
* @param {Object} [options] | |
*/ | |
Sortable.create = function (el, options) { | |
return new Sortable(el, options); | |
}; | |
// Export | |
return Sortable; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment