-
-
Save fabien/20f31a2e3f676849d7ca to your computer and use it in GitHub Desktop.
This file contains 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
'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