You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

846 lines
19 KiB

11 years ago
/**!
* Sortable
* @author RubaXa <trash@rubaxa.org>
* @license MIT
*/
10 years ago
(function (factory) {
11 years ago
"use strict";
10 years ago
if (typeof define === "function" && define.amd) {
11 years ago
define(factory);
11 years ago
}
10 years ago
else if (typeof module != "undefined" && typeof module.exports != "undefined") {
module.exports = factory();
}
10 years ago
else if (typeof Package !== "undefined") {
Sortable = factory(); // export for Meteor.js
}
11 years ago
else {
10 years ago
/* jshint sub:true */
11 years ago
window["Sortable"] = factory();
}
10 years ago
})(function () {
11 years ago
"use strict";
10 years ago
var dragEl,
startIndex,
10 years ago
ghostEl,
cloneEl,
rootEl,
nextEl,
11 years ago
10 years ago
lastEl,
lastCSS,
11 years ago
10 years ago
activeGroup,
11 years ago
10 years ago
tapEvt,
touchEvt,
11 years ago
10 years ago
expando = 'Sortable' + (new Date).getTime(),
11 years ago
10 years ago
win = window,
document = win.document,
parseInt = win.parseInt,
supportIEdnd = !!document.createElement('div').dragDrop,
10 years ago
_silent = false,
_dispatchEvent = function (rootEl, name, targetEl, fromEl, startIndex, newIndex) {
var evt = document.createEvent('Event');
11 years ago
evt.initEvent(name, true, true);
evt.item = targetEl || rootEl;
evt.from = fromEl || rootEl;
if (startIndex !== undefined) evt.oldIndex = startIndex;
if (newIndex !== undefined) evt.newIndex = newIndex;
rootEl.dispatchEvent(evt);
10 years ago
},
10 years ago
_customEvents = 'onAdd onUpdate onRemove onStart onEnd onFilter onSort'.split(' '),
11 years ago
10 years ago
noop = function () {},
slice = [].slice,
11 years ago
10 years ago
touchDragOverListeners = []
11 years ago
;
11 years ago
/**
* @class Sortable
* @param {HTMLElement} el
* @param {Object} [options]
11 years ago
*/
10 years ago
function Sortable(el, options) {
11 years ago
this.el = el; // root element
this.options = options = (options || {});
// Default options
var defaults = {
group: Math.random(),
sort: true,
disabled: false,
store: null,
handle: null,
draggable: el.children[0] && el.children[0].nodeName || (/[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);
}
};
// Set default options
for (var name in defaults) {
!(name in options) && (options[name] = defaults[name]);
}
11 years ago
if (!options.group.name) {
options.group = { name: options.group };
}
['pull', 'put'].forEach(function (key) {
if (!(key in options.group)) {
options.group[key] = true;
}
});
// Define events
_customEvents.forEach(function (name) {
options[name] = _bind(this, options[name] || noop);
_on(el, name.substr(2).toLowerCase(), options[name]);
}, this);
11 years ago
11 years ago
// Export group name
el[expando] = options.group.name;
11 years ago
11 years ago
// Bind all private methods
10 years ago
for (var fn in this) {
if (fn.charAt(0) === '_') {
11 years ago
this[fn] = _bind(this, this[fn]);
}
}
// Bind events
_on(el, 'mousedown', this._onTapStart);
_on(el, 'touchstart', this._onTapStart);
supportIEdnd && _on(el, 'selectstart', this._onTapStart);
11 years ago
_on(el, 'dragover', this._onDragOver);
_on(el, 'dragenter', this._onDragOver);
touchDragOverListeners.push(this._onDragOver);
// Restore sorting
options.store && this.sort(options.store.get(this));
11 years ago
}
Sortable.prototype = /** @lends Sortable.prototype */ {
11 years ago
constructor: Sortable,
10 years ago
_applyEffects: function () {
11 years ago
_toggleClass(dragEl, this.options.ghostClass, true);
},
10 years ago
_onTapStart: function (/**Event|TouchEvent*/evt) {
var touch = evt.touches && evt.touches[0],
target = (touch || evt).target,
options = this.options,
el = this.el,
filter = options.filter;
11 years ago
// get the index of the dragged element within its parent
startIndex = _index(target);
11 years ago
if (evt.type === 'mousedown' && evt.button !== 0 || options.disabled) {
return; // only left button or enabled
}
// Check filter
10 years ago
if (typeof filter === 'function') {
if (filter.call(this, target, this)) {
_dispatchEvent(el, 'filter', target);
return; // cancel dnd
}
}
10 years ago
else if (filter) {
filter = filter.split(',').filter(function (criteria) {
return _closest(target, criteria.trim(), el);
});
if (filter.length) {
_dispatchEvent(el, 'filter', target);
return; // cancel dnd
}
}
10 years ago
if (options.handle) {
11 years ago
target = _closest(target, options.handle, el);
11 years ago
}
11 years ago
target = _closest(target, options.draggable, el);
11 years ago
// IE 9 Support
10 years ago
if (target && evt.type == 'selectstart') {
if (target.tagName != 'A' && target.tagName != 'IMG') {
target.dragDrop();
}
}
10 years ago
if (target && !dragEl && (target.parentNode === el)) {
11 years ago
tapEvt = evt;
rootEl = this.el;
dragEl = target;
nextEl = dragEl.nextSibling;
activeGroup = this.options.group;
dragEl.draggable = true;
11 years ago
// Disable "draggable"
options.ignore.split(',').forEach(function (criteria) {
_find(target, criteria.trim(), _disableDraggable);
});
11 years ago
10 years ago
if (touch) {
11 years ago
// Touch device support
tapEvt = {
10 years ago
target: target,
clientX: touch.clientX,
clientY: touch.clientY
11 years ago
};
11 years ago
this._onDragStart(tapEvt, true);
evt.preventDefault();
}
11 years ago
_on(document, 'mouseup', this._onDrop);
_on(document, 'touchend', this._onDrop);
_on(document, 'touchcancel', this._onDrop);
11 years ago
_on(this.el, 'dragstart', this._onDragStart);
11 years ago
_on(this.el, 'dragend', this._onDrop);
11 years ago
_on(document, 'dragover', _globalDragOver);
try {
10 years ago
if (document.selection) {
11 years ago
document.selection.empty();
} else {
10 years ago
window.getSelection().removeAllRanges();
11 years ago
}
10 years ago
} catch (err) {
}
_dispatchEvent(dragEl, 'start', undefined, undefined, startIndex);
10 years ago
if (activeGroup.pull == 'clone') {
cloneEl = dragEl.cloneNode(true);
_css(cloneEl, 'display', 'none');
rootEl.insertBefore(cloneEl, dragEl);
}
11 years ago
}
},
10 years ago
_emulateDragOver: function () {
if (touchEvt) {
11 years ago
_css(ghostEl, 'display', 'none');
10 years ago
var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
parent = target,
groupName = this.options.group.name,
i = touchDragOverListeners.length;
11 years ago
10 years ago
if (parent) {
11 years ago
do {
10 years ago
if (parent[expando] === groupName) {
while (i--) {
11 years ago
touchDragOverListeners[i]({
clientX: touchEvt.clientX,
clientY: touchEvt.clientY,
target: target,
rootEl: parent
});
}
10 years ago
11 years ago
break;
11 years ago
}
11 years ago
target = parent; // store last element
}
10 years ago
/* jshint boss:true */
while (parent = parent.parentNode);
11 years ago
}
_css(ghostEl, 'display', '');
}
},
10 years ago
_onTouchMove: function (/**TouchEvent*/evt) {
if (tapEvt) {
var touch = evt.touches[0],
dx = touch.clientX - tapEvt.clientX,
dy = touch.clientY - tapEvt.clientY,
translate3d = 'translate3d(' + dx + 'px,' + dy + 'px,0)';
11 years ago
touchEvt = touch;
11 years ago
_css(ghostEl, 'webkitTransform', translate3d);
_css(ghostEl, 'mozTransform', translate3d);
_css(ghostEl, 'msTransform', translate3d);
_css(ghostEl, 'transform', translate3d);
11 years ago
evt.preventDefault();
11 years ago
}
},
10 years ago
_onDragStart: function (/**Event*/evt, /**boolean*/isTouch) {
var dataTransfer = evt.dataTransfer,
options = this.options;
11 years ago
this._offUpEvents();
11 years ago
10 years ago
if (isTouch) {
var rect = dragEl.getBoundingClientRect(),
css = _css(dragEl),
ghostRect;
11 years ago
ghostEl = dragEl.cloneNode(true);
11 years ago
_css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
_css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
11 years ago
_css(ghostEl, 'width', rect.width);
_css(ghostEl, 'height', rect.height);
11 years ago
_css(ghostEl, 'opacity', '0.8');
_css(ghostEl, 'position', 'fixed');
_css(ghostEl, 'zIndex', '100000');
11 years ago
rootEl.appendChild(ghostEl);
// Fixing dimensions.
ghostRect = ghostEl.getBoundingClientRect();
10 years ago
_css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
_css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
11 years ago
// Bind touch events
_on(document, 'touchmove', this._onTouchMove);
_on(document, 'touchend', this._onDrop);
_on(document, 'touchcancel', this._onDrop);
11 years ago
this._loopId = setInterval(this._emulateDragOver, 150);
11 years ago
}
else {
dataTransfer.effectAllowed = 'move';
options.setData && options.setData.call(this, dataTransfer, dragEl);
11 years ago
_on(document, 'drop', this._onDrop);
}
setTimeout(this._applyEffects);
},
10 years ago
_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;
10 years ago
if (!_silent &&
(isOwner
? canSort || (revert = !rootEl.contains(dragEl))
: 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)
10 years ago
) {
target = _closest(evt.target, options.draggable, el);
dragRect = dragEl.getBoundingClientRect();
if (cloneEl && (cloneEl.state !== isOwner)) {
_css(cloneEl, 'display', isOwner ? 'none' : '');
!isOwner && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
cloneEl.state = isOwner;
}
if (revert) {
if (cloneEl || nextEl) {
rootEl.insertBefore(dragEl, cloneEl || nextEl);
}
else if (!canSort) {
rootEl.appendChild(dragEl);
}
return;
}
11 years ago
10 years ago
if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
(el === evt.target) && (target = _ghostInBottom(el, evt))
10 years ago
) {
if (target) {
if (target.animated) {
return;
}
targetRect = target.getBoundingClientRect();
}
11 years ago
el.appendChild(dragEl);
10 years ago
this._animate(dragRect, dragEl);
10 years ago
target && this._animate(targetRect, target);
}
10 years ago
else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
if (lastEl !== target) {
lastEl = target;
lastCSS = _css(target);
11 years ago
}
10 years ago
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,
after
;
_silent = true;
setTimeout(_unsilent, 30);
10 years ago
if (floating) {
after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
} else {
11 years ago
after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
}
10 years ago
if (after && !nextSibling) {
el.appendChild(dragEl);
} else {
target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
11 years ago
}
10 years ago
this._animate(dragRect, dragEl);
this._animate(targetRect, target);
11 years ago
}
}
},
_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)');
10 years ago
clearTimeout(target.animated);
target.animated = setTimeout(function () {
_css(target, 'transition', '');
target.animated = false;
}, ms);
}
},
_offUpEvents: function () {
_off(document, 'mouseup', this._onDrop);
_off(document, 'touchmove', this._onTouchMove);
_off(document, 'touchend', this._onDrop);
_off(document, 'touchcancel', this._onDrop);
},
11 years ago
10 years ago
_onDrop: function (/**Event*/evt) {
11 years ago
clearInterval(this._loopId);
// Unbind events
_off(document, 'drop', this._onDrop);
_off(document, 'dragover', _globalDragOver);
11 years ago
_off(this.el, 'dragend', this._onDrop);
11 years ago
_off(this.el, 'dragstart', this._onDragStart);
_off(this.el, 'selectstart', this._onTapStart);
11 years ago
this._offUpEvents();
11 years ago
10 years ago
if (evt) {
11 years ago
evt.preventDefault();
evt.stopPropagation();
11 years ago
ghostEl && ghostEl.parentNode.removeChild(ghostEl);
11 years ago
10 years ago
if (dragEl) {
// get the index of the dragged element within its parent
var newIndex = _index(dragEl);
_disableDraggable(dragEl);
11 years ago
_toggleClass(dragEl, this.options.ghostClass, false);
10 years ago
if (!rootEl.contains(dragEl)) {
// drag from one list and drop into another
_dispatchEvent(dragEl, 'sort', dragEl, dragEl.parentNode, startIndex, newIndex);
_dispatchEvent(rootEl, 'sort', dragEl, undefined, startIndex, newIndex);
11 years ago
// Add event
_dispatchEvent(dragEl, 'add', dragEl, rootEl, startIndex, newIndex);
// Remove event
_dispatchEvent(rootEl, 'remove', dragEl, undefined, startIndex, newIndex);
11 years ago
}
10 years ago
else if (dragEl.nextSibling !== nextEl) {
// drag & drop within the same list
_dispatchEvent(dragEl, 'update', undefined, undefined, startIndex, newIndex);
_dispatchEvent(dragEl, 'sort', undefined, undefined, startIndex, newIndex);
10 years ago
cloneEl && cloneEl.parentNode.removeChild(cloneEl);
11 years ago
}
_dispatchEvent(rootEl, 'end', undefined, undefined, startIndex, newIndex);
11 years ago
}
// Set NULL
rootEl =
dragEl =
ghostEl =
nextEl =
cloneEl =
11 years ago
tapEvt =
touchEvt =
lastEl =
lastCSS =
activeGroup = null;
// Save sorting
this.options.store && this.options.store.set(this);
11 years ago
}
},
/**
* Serializes the item into an array of string.
* @returns {String[]}
*/
toArray: function () {
var order = [],
el,
children = this.el.children,
i = 0,
10 years ago
n = children.length;
for (; i < n; i++) {
el = children[i];
if (_closest(el, this.options.draggable, this.el)) {
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 = {}, 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]);
}
});
},
/**
* 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 () {
11 years ago
var el = this.el, options = this.options;
_customEvents.forEach(function (name) {
_off(el, name.substr(2).toLowerCase(), options[name]);
});
11 years ago
_off(el, 'mousedown', this._onTapStart);
_off(el, 'touchstart', this._onTapStart);
_off(el, 'selectstart', this._onTapStart);
11 years ago
_off(el, 'dragover', this._onDragOver);
_off(el, 'dragenter', this._onDragOver);
//remove draggable attributes
10 years ago
Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
el.removeAttribute('draggable');
});
11 years ago
touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);
this._onDrop();
this.el = null;
}
};
10 years ago
function _bind(ctx, fn) {
11 years ago
var args = slice.call(arguments, 2);
10 years ago
return fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function () {
11 years ago
return fn.apply(ctx, args.concat(slice.call(arguments)));
};
}
10 years ago
function _closest(el, selector, ctx) {
if (selector === '*') {
return el;
}
10 years ago
else if (el) {
11 years ago
ctx = ctx || document;
selector = selector.split('.');
10 years ago
var tag = selector.shift().toUpperCase(),
re = new RegExp('\\s(' + selector.join('|') + ')\\s', 'g');
11 years ago
do {
10 years ago
if (
(tag === '' || el.nodeName == tag) &&
(!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
) {
return el;
11 years ago
}
}
10 years ago
while (el !== ctx && (el = el.parentNode));
11 years ago
}
10 years ago
return null;
11 years ago
}
10 years ago
function _globalDragOver(evt) {
11 years ago
evt.dataTransfer.dropEffect = 'move';
evt.preventDefault();
}
10 years ago
function _on(el, event, fn) {
11 years ago
el.addEventListener(event, fn, false);
}
10 years ago
function _off(el, event, fn) {
11 years ago
el.removeEventListener(event, fn, false);
}
10 years ago
function _toggleClass(el, name, state) {
if (el) {
if (el.classList) {
11 years ago
el.classList[state ? 'add' : 'remove'](name);
}
else {
10 years ago
var className = (' ' + el.className + ' ').replace(/\s+/g, ' ').replace(' ' + name + ' ', '');
el.className = className + (state ? ' ' + name : '');
11 years ago
}
}
}
10 years ago
function _css(el, prop, val) {
var style = el && el.style;
10 years ago
if (style) {
if (val === void 0) {
if (document.defaultView && document.defaultView.getComputedStyle) {
11 years ago
val = document.defaultView.getComputedStyle(el, '');
}
10 years ago
else if (el.currentStyle) {
val = el.currentStyle;
11 years ago
}
return prop === void 0 ? val : val[prop];
}
else {
if (!(prop in style)) {
prop = '-webkit-' + prop;
}
style[prop] = val + (typeof val === 'string' ? '' : 'px');
11 years ago
}
}
}
10 years ago
function _find(ctx, tagName, iterator) {
if (ctx) {
11 years ago
var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
10 years ago
if (iterator) {
for (; i < n; i++) {
11 years ago
iterator(list[i], i);
}
}
10 years ago
return list;
11 years ago
}
10 years ago
return [];
11 years ago
}
10 years ago
function _disableDraggable(el) {
el.draggable = false;
11 years ago
}
10 years ago
function _unsilent() {
_silent = false;
}
/** @returns {HTMLElement|false} */
10 years ago
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,
10 years ago
sum = 0;
while (i--) {
sum += str.charCodeAt(i);
}
return sum.toString(36);
}
/**
* Returns the index of an element within its parent
* @param el
* @returns {HTMLElement}
*/
function _index(/**HTMLElement*/el) {
var index = 0;
while ((el = el.previousElementSibling)) {
index++;
}
return index;
}
11 years ago
// Export utils
Sortable.utils = {
on: _on,
off: _off,
css: _css,
find: _find,
bind: _bind,
closest: _closest,
toggleClass: _toggleClass,
dispatchEvent: _dispatchEvent,
index: _index
11 years ago
};
Sortable.version = '0.7.2';
11 years ago
/**
* Create sortable instance
* @param {HTMLElement} el
* @param {Object} [options]
*/
Sortable.create = function (el, options) {
10 years ago
return new Sortable(el, options);
};
11 years ago
// Export
return Sortable;
11 years ago
});