(function (root, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define([], factory); } else { root.Draggable = factory(); } }(this, function () { 'use strict'; var defaults = { // settings grid: 0, // grid cell size for snapping to on drag filterTarget: null, // disallow drag when target passes this test limit: { // limit the drag bounds x: null, // [minimum position, maximum position] || position y: null // [minimum position, maximum position] || position }, threshold: 0, // threshold to move before drag begins (in px) // flags setCursor: false, // change cursor to reflect draggable? setPosition: true, // change draggable position to absolute? smoothDrag: true, // snap to grid when dropped, but not during useGPU: true, // move graphics calculation/composition to the GPU // event hooks onDrag: noop, // function(element, X, Y, event) onDragStart: noop, // function(element, X, Y, event) onDragEnd: noop // function(element, X, Y, event) }; var env = { // CSS vendor-prefixed transform property transform: (function(){ var prefixes = ' -o- -ms- -moz- -webkit-'.split(' '); var style = document.body.style; for (var n = prefixes.length; n--;) { var property = prefixes[n] + 'transform'; if (property in style) { return property; } } })() }; var util = { assign: function () { var obj = arguments[0]; var count = arguments.length; for ( var n = 1; n < count; n++ ) { var argument = arguments[n]; for ( var key in argument ) { obj[key] = argument[key]; } } return obj; }, bind: function (fn, context) { return function() { fn.apply(context, arguments); } }, on: function (element, e, fn) { if (e && fn) { util.addEvent (element, e, fn); } else if (e) { for (var ee in e) { util.addEvent (element, ee, e[ee]); } } }, off: function (element, e, fn) { if (e && fn) { util.removeEvent (element, e, fn); } else if (e) { for (var ee in e) { util.removeEvent (element, ee, e[ee]); } } }, // Example: // // util.limit(x, limit.x) limit: function (n, limit) { // {Array} limit.x if (isArray(limit)) { limit = [+limit[0], +limit[1]]; if (n < limit[0]) n = limit[0]; else if (n > limit[1]) n = limit[1]; // {Number} limit.x } else { n = +limit; } return n; }, addEvent: ('attachEvent' in Element.prototype) ? function (element, e, fn) { element.attachEvent('on'+e, fn) } : function (element, e, fn) { element.addEventListener(e, fn, false) }, removeEvent: ('attachEvent' in Element.prototype) ? function (element, e, fn) { element.detachEvent('on'+e, fn) } : function (element, e, fn) { element.removeEventListener(e, fn) } }; /* usage: new Draggable (element, options) - or - new Draggable (element) */ function Draggable (element, options) { var me = this, start = util.bind(me.start, me), drag = util.bind(me.drag, me), stop = util.bind(me.stop, me); // sanity check if (!isElement(element)) { throw new TypeError('Draggable expects argument 0 to be an Element'); } options = util.assign({}, defaults, options); // set instance properties util.assign(me, { // DOM element element: element, handle: (options.handle && isElement(options.handle)) ? options.handle : element, // DOM event handlers handlers: { start: { mousedown: start, touchstart: start }, move: { mousemove: drag, mouseup: stop, touchmove: drag, touchend: stop } }, // options options: options }); // initialize me.initialize(); } util.assign (Draggable.prototype, { // public setOption: function (property, value) { var me = this; me.options[property] = value; me.initialize(); return me; }, get: function() { var dragEvent = this.dragEvent; return { x: dragEvent.x, y: dragEvent.y }; }, set: function (x, y) { var me = this, dragEvent = me.dragEvent; dragEvent.original = { x: dragEvent.x, y: dragEvent.y }; me.move(x, y); return me; }, // internal dragEvent: { started: false, x: 0, y: 0 }, initialize: function() { var me = this, element = me.element, handle = me.handle, style = element.style, compStyle = getStyle(element), options = me.options, transform = env.transform, oldTransform; // cache element dimensions (for performance) var _dimensions = me._dimensions = { height: element.offsetHeight, left: element.offsetLeft, top: element.offsetTop, width: element.offsetWidth }; // shift compositing over to the GPU if the browser supports it (for performance) if (options.useGPU && transform) { // concatenate to any existing transform // so we don't accidentally override it oldTransform = compStyle[transform]; if (oldTransform === 'none') { oldTransform = ''; } style[transform] = oldTransform + ' translate3d(0,0,0)'; } // optional styling if (options.setPosition) { style.display = 'block'; style.left = _dimensions.left + 'px'; style.top = _dimensions.top + 'px'; style.bottom = style.right = 'auto'; style.margin = 0; style.position = 'absolute'; } if (options.setCursor) { style.cursor = 'move'; } // set limit me.setLimit(options.limit); // set position in model util.assign(me.dragEvent, { x: _dimensions.left, y: _dimensions.top }); // attach mousedown event util.on(me.handle, me.handlers.start); }, start: function (e) { var me = this; var cursor = me.getCursor(e); var element = me.element; // filter the target? if (!me.useTarget(e.target || e.srcElement)) { return; } // prevent browsers from visually dragging the element's outline if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; // IE10 } // set a high z-index, just in case me.dragEvent.oldZindex = element.style.zIndex; element.style.zIndex = 10000; // set initial position me.setCursor(cursor); me.setPosition(); me.setZoom(); // add event listeners util.on(document, me.handlers.move); }, drag: function (e) { var me = this, dragEvent = me.dragEvent, element = me.element, initialCursor = me._cursor, initialPosition = me._dimensions, options = me.options, zoom = initialPosition.zoom, cursor = me.getCursor(e), threshold = options.threshold, x = (cursor.x - initialCursor.x)/zoom + initialPosition.left, y = (cursor.y - initialCursor.y)/zoom + initialPosition.top; // check threshold if (!dragEvent.started && threshold && (Math.abs(initialCursor.x - cursor.x) < threshold) && (Math.abs(initialCursor.y - cursor.y) < threshold) ) { return; } // save original position? if (!dragEvent.original) { dragEvent.original = { x: x, y: y }; } // trigger start event? if (!dragEvent.started) { options.onDragStart(element, x, y, e); dragEvent.started = true; } // move the element if (me.move(x, y)) { // trigger drag event options.onDrag(element, dragEvent.x, dragEvent.y, e); } }, move: function (x, y) { var me = this, dragEvent = me.dragEvent, options = me.options, grid = options.grid, style = me.element.style, pos = me.limit(x, y, dragEvent.original.x, dragEvent.original.y); // snap to grid? if (!options.smoothDrag && grid) { pos = me.round (pos, grid); } // move it if (pos.x !== dragEvent.x || pos.y !== dragEvent.y) { dragEvent.x = pos.x; dragEvent.y = pos.y; style.left = pos.x + 'px'; style.top = pos.y + 'px'; return true; } return false; }, stop: function (e) { var me = this, dragEvent = me.dragEvent, element = me.element, options = me.options, grid = options.grid, pos; // remove event listeners util.off(document, me.handlers.move); // resent element's z-index element.style.zIndex = dragEvent.oldZindex; // snap to grid? if (options.smoothDrag && grid) { pos = me.round({ x: dragEvent.x, y: dragEvent.y }, grid); me.move(pos.x, pos.y); util.assign(me.dragEvent, pos); } // trigger dragend event if (me.dragEvent.started) { options.onDragEnd(element, dragEvent.x, dragEvent.y, e); } // clear temp vars me.reset(); }, reset: function() { this.dragEvent.started = false; }, round: function (pos) { var grid = this.options.grid; return { x: grid * Math.round(pos.x/grid), y: grid * Math.round(pos.y/grid) }; }, getCursor: function (e) { return { x: (e.targetTouches ? e.targetTouches[0] : e).clientX, y: (e.targetTouches ? e.targetTouches[0] : e).clientY }; }, setCursor: function (xy) { this._cursor = xy; }, setLimit: function (limit) { var me = this, _true = function (x, y) { return { x:x, y:y }; }; // limit is a function if (isFunction(limit)) { me.limit = limit; } // limit is an element else if (isElement(limit)) { var draggableSize = me._dimensions, height = limit.scrollHeight - draggableSize.height, width = limit.scrollWidth - draggableSize.width; me.limit = function (x, y) { return { x: util.limit(x, [0, width]), y: util.limit(y, [0, height]) } }; } // limit is defined else if (limit) { var defined = { x: isDefined(limit.x), y: isDefined(limit.y) }; var _x, _y; // {Undefined} limit.x, {Undefined} limit.y if (!defined.x && !defined.y) { me.limit = _true; } else { me.limit = function (x, y) { return { x: defined.x ? util.limit(x, limit.x) : x, y: defined.y ? util.limit(y, limit.y) : y }; }; } } // limit is `null` or `undefined` else { me.limit = _true; } }, setPosition: function() { var me = this, element = me.element, style = element.style; util.assign(me._dimensions, { left: parse(style.left) || element.offsetLeft, top: parse(style.top) || element.offsetTop }); }, setZoom: function() { var me = this; var element = me.element; var zoom = 1; while (element = element.offsetParent) { var z = getStyle(element).zoom; if (z && z !== 'normal') { zoom = z; break; } } me._dimensions.zoom = zoom; }, useTarget: function (element) { var filterTarget = this.options.filterTarget; if (filterTarget instanceof Function) { return filterTarget(element); } return true; }, destroy: function () { util.off(this.handle, this.handlers.start); util.off(document, this.handlers.move); } }); // helpers function parse (string) { return parseInt(string, 10); } function getStyle (element) { return 'currentStyle' in element ? element.currentStyle : getComputedStyle(element); } function isArray (thing) { return thing instanceof Array; // HTMLElement } function isDefined (thing) { return thing !== void 0 && thing !== null; } function isElement (thing) { return thing instanceof Element || typeof HTMLDocument !== 'undefined' && thing instanceof HTMLDocument; } function isFunction (thing) { return thing instanceof Function; } function noop (){}; return Draggable; }));