Skip to content

Instantly share code, notes, and snippets.

@fabien
Created April 19, 2015 16:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fabien/20f31a2e3f676849d7ca to your computer and use it in GitHub Desktop.
Save fabien/20f31a2e3f676849d7ca to your computer and use it in GitHub Desktop.
'use strict';
var emitter = require('contra.emitter');
var crossvent = require('crossvent');
var body = document.body;
var documentElement = document.documentElement;
function dragula (containers, options) {
var _mirror; // mirror image
var _source; // source container
var _item; // item being dragged
var _offsetX; // reference x
var _offsetY; // reference y
var _initialSibling; // reference sibling when grabbed
var _currentSibling; // reference sibling now
var _copy; // item used for copying
var o = options || {};
if (o.moves === void 0) { o.moves = always; }
if (o.accepts === void 0) { o.accepts = always; }
if (o.copy === void 0) { o.copy = false; }
if (o.revertOnSpill === void 0) { o.revertOnSpill = false; }
if (o.removeOnSpill === void 0) { o.removeOnSpill = false; }
if (o.direction === void 0) { o.direction = 'vertical'; }
var api = emitter({
addContainer: manipulateContainers('add'),
removeContainer: manipulateContainers('remove'),
start: start,
end: end,
cancel: cancel,
remove: remove,
destroy: destroy,
dragging: false
});
events();
return api;
function manipulateContainers (op) {
return function addOrRemove (all) {
var containers = Array.isArray(all) ? all : [all];
containers.forEach(track);
function track (container) {
touchy(container, op, 'mousedown', grab);
}
};
}
function events (remove) {
var op = remove ? 'remove' : 'add';
touchy(documentElement, op, 'mouseup', release);
api[op + 'Container'](containers);
}
function destroy () {
events(true);
release({});
}
function grab (e) {
var item = e.target;
if ((e.which !== 0 && e.which !== 1) || e.metaKey || e.ctrlKey) {
return; // we only care about honest-to-god left clicks and touch events
}
if (start(item) !== true) {
return;
}
var offset = getOffset(_item);
_offsetX = getCoord('pageX', e) - offset.left;
_offsetY = getCoord('pageY', e) - offset.top;
renderMirrorImage();
drag(e);
e.preventDefault();
}
function start (item) {
if (api.dragging && _mirror) {
return;
}
if (containers.indexOf(item) !== -1) {
return; // don't drag container itself
}
if (typeof o.handle === 'string' && !isHandle(item, o.handle)) {
return;
}
while (containers.indexOf(item.parentElement) === -1) {
if (invalidTarget(item)) {
return;
}
item = item.parentElement; // drag target should be a top element
}
if (invalidTarget(item)) {
return;
}
var container = item.parentElement;
var movable = o.moves(item, container);
if (!movable) {
return;
}
end();
if (o.copy) {
_copy = item.cloneNode(true);
addClass(_copy, 'gu-transit');
} else {
addClass(item, 'gu-transit');
}
_source = container;
_item = item;
_initialSibling = _currentSibling = nextEl(item);
api.dragging = true;
api.emit('drag', _item, _source);
return true;
}
function invalidTarget (el) {
return el.tagName === 'A' || el.tagName === 'BUTTON';
}
function isHandle (el, className) {
return (' ' + el.className + ' ').indexOf(' ' + className + ' ') > -1;
}
function end () {
if (!api.dragging) {
return;
}
var item = _copy || _item;
drop(item, item.parentElement);
}
function release (e) {
if (!api.dragging) {
return;
}
var item = _copy || _item;
var clientX = getCoord('clientX', e);
var clientY = getCoord('clientY', e);
var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
if (dropTarget && (o.copy === false || dropTarget !== _source)) {
drop(item, dropTarget);
} else if (o.removeOnSpill) {
remove();
} else {
cancel();
}
}
function drop (item, target) {
if (isInitialPlacement(target)) {
api.emit('cancel', item, _source);
} else {
api.emit('drop', item, target, _source);
}
cleanup();
}
function remove () {
if (!api.dragging) {
return;
}
var item = _copy || _item;
var parent = item.parentElement;
if (parent) {
parent.removeChild(item);
}
api.emit(o.copy ? 'cancel' : 'remove', item, parent);
cleanup();
}
function cancel (revert) {
if (!api.dragging) {
return;
}
var reverts = arguments.length > 0 ? revert : o.revertOnSpill;
var item = _copy || _item;
var parent = item.parentElement;
if (parent === _source && o.copy) {
parent.removeChild(_copy);
}
var initial = isInitialPlacement(parent);
if (initial === false && o.copy === false && reverts) {
_source.insertBefore(item, _initialSibling);
}
if (initial || reverts) {
api.emit('cancel', item, _source);
} else {
api.emit('drop', item, parent, _source);
}
cleanup();
}
function cleanup () {
var item = _copy || _item;
removeMirrorImage();
rmClass(item, 'gu-transit');
_source = _item = _copy = _initialSibling = _currentSibling = null;
api.dragging = false;
api.emit('dragend', item);
}
function isInitialPlacement (target, s) {
var sibling;
if (s !== void 0) {
sibling = s;
} else if (_mirror) {
sibling = _currentSibling;
} else {
sibling = nextEl(_item || _copy);
}
return target === _source && sibling === _initialSibling;
}
function findDropTarget (elementBehindCursor, clientX, clientY) {
var target = elementBehindCursor;
while (target && !accepted()) {
target = target.parentElement;
}
return target;
function accepted () {
var droppable = containers.indexOf(target) !== -1;
if (droppable === false) {
return false;
}
var immediate = getImmediateChild(target, elementBehindCursor);
var reference = getReference(target, immediate, clientX, clientY);
var initial = isInitialPlacement(target, reference);
if (initial) {
return true; // should always be able to drop it right back where it was
}
return o.accepts(_item, target, _source, reference);
}
}
function drag (e) {
if (!_mirror) {
return;
}
var clientX = getCoord('clientX', e);
var clientY = getCoord('clientY', e);
var x = clientX - _offsetX;
var y = clientY - _offsetY;
_mirror.style.left = x + 'px';
_mirror.style.top = y + 'px';
var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
if (dropTarget === _source && o.copy) {
return;
}
var item = _copy || _item;
var immediate = getImmediateChild(dropTarget, elementBehindCursor);
if (immediate === null) {
return;
}
var reference = getReference(dropTarget, immediate, clientX, clientY);
if (reference === null || reference !== item && reference !== nextEl(item)) {
_currentSibling = reference;
dropTarget.insertBefore(item, reference);
api.emit('shadow', item, dropTarget);
}
}
function renderMirrorImage () {
if (_mirror) {
return;
}
var rect = _item.getBoundingClientRect();
_mirror = _item.cloneNode(true);
_mirror.style.width = rect.width + 'px';
_mirror.style.height = rect.height + 'px';
rmClass(_mirror, 'gu-transit');
addClass(_mirror, ' gu-mirror');
body.appendChild(_mirror);
touchy(documentElement, 'add', 'mousemove', drag);
addClass(body, 'gu-unselectable');
}
function removeMirrorImage () {
if (_mirror) {
rmClass(body, 'gu-unselectable');
touchy(documentElement, 'remove', 'mousemove', drag);
_mirror.parentElement.removeChild(_mirror);
_mirror = null;
}
}
function getImmediateChild (dropTarget, target) {
var immediate = target;
while (immediate !== dropTarget && immediate.parentElement !== dropTarget) {
immediate = immediate.parentElement;
}
if (immediate === documentElement) {
return null;
}
return immediate;
}
function getReference (dropTarget, target, x, y) {
var horizontal = o.direction === 'horizontal';
var reference = target !== dropTarget ? inside() : outside();
return reference;
function outside () { // slower, but able to figure out any position
var len = dropTarget.children.length;
var i;
var el;
var rect;
for (i = 0; i < len; i++) {
el = dropTarget.children[i];
rect = el.getBoundingClientRect();
if (horizontal && rect.left > x) { return el; }
if (!horizontal && rect.top > y) { return el; }
}
return null;
}
function inside () { // faster, but only available if dropped inside a child element
var rect = target.getBoundingClientRect();
if (horizontal) {
return resolve(x > rect.left + rect.width / 2);
}
return resolve(y > rect.top + rect.height / 2);
}
function resolve (after) {
return after ? nextEl(target) : target;
}
}
}
function touchy (el, op, type, fn) {
var touch = {
mouseup: 'touchend',
mousedown: 'touchstart',
mousemove: 'touchmove'
};
var microsoft = {
mouseup: 'MSPointerUp',
mousedown: 'MSPointerDown',
mousemove: 'MSPointerMove'
};
if (global.navigator.msPointerEnabled) {
crossvent[op](el, microsoft[type], fn);
}
crossvent[op](el, touch[type], fn);
crossvent[op](el, type, fn);
}
function getOffset (el) {
var rect = el.getBoundingClientRect();
return {
left: rect.left + getScroll('scrollLeft', 'pageXOffset'),
top: rect.top + getScroll('scrollTop', 'pageYOffset')
};
}
function getScroll (scrollProp, offsetProp) {
if (typeof global[offsetProp] !== 'undefined') {
return global[offsetProp];
}
if (documentElement.clientHeight) {
return documentElement[scrollProp];
}
return body[scrollProp];
}
function getElementBehindPoint (point, x, y) {
if (!x && !y) {
return null;
}
var p = point || {};
var state = p.className;
var el;
p.className += ' gu-hide';
el = document.elementFromPoint(x, y);
p.className = state;
return el;
}
function always () {
return true;
}
function nextEl (el) {
return el.nextElementSibling || manually();
function manually () {
var sibling = el;
do {
sibling = sibling.nextSibling;
} while (sibling && sibling.nodeType !== 1);
return sibling;
}
}
function addClass (el, className) {
if (el.className.indexOf(' ' + className) === -1) {
el.className += ' ' + className;
}
}
function rmClass (el, className) {
el.className = el.className.replace(new RegExp(' ' + className, 'g'), '');
}
function getCoord (coord, e) {
if (typeof e.targetTouches === 'undefined') {
return e[coord];
}
return e.targetTouches && e.targetTouches.length && e.targetTouches[0][coord] || 0;
}
module.exports = dragula;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment