Source: effect/drag.js

/**
 * Class for dragging DOM elements.
 @namespace ludo.effect
 @class ludo.effect.Drag
 @augments ludo.effect.Effect

 @param {Object} config
 @param {Number} config.minX Optional minimum left coordinate
 @param {Number} config.maxX Optional maximum left coordinate
 @param {Number} config.minY Optional minimum top coordinate
 @param {Number} config.maxY Optional maximum top coordinate
 @param {Number} config.maxY Optional maximum top coordinate
 @param {String|HTMLElement} config.el This element is draggable.
 @param {String|HTMLElement} config.handle Optional dom element. Mouse down on this element will initiate the drag process. example: A title bar above a view. If not set, Mouse down on this.el will initiate dragging.
 @param {String} config.directions Accept dragging in these directions, default: "XY". For horizontal dragging only, use "X" and for vertical "Y".
 @param {Number} config.minPos Alternative to minX and minY when you only accepts dragging along the X or Y-axis.
 @param {Number} config.maxPos Alternative to maxX and maxY when you only accepts dragging along the X or Y-axis.
 @param {Number} config.delay Optional delay in seconds from mouse down to dragging starts. Default: 0
 @param {Boolean} config.useShim True to drag a "ghost" DOM element while dragging, default: false
 @param {String} config.shimCls Name of css class to add to the shim
 @param {Boolean} config.autoHideShim True to automatically hide shim on drag end, default: true
 @param {Number} config.mouseYOffset While dragging, always show dragged element this amount of pixels below mouse cursor.
 @param {Number} config.mouseXOffset While dragging, always show dragged element this amount of pixels right of mouse cursor.
 @param {String} config.unit Unit used while dragging, default: "px"


 @fires ludo.effect.Drag#before Event fired before drag starts. Params: 1) Dom element to be dragged, 2) ludo.effect.Drag, 3) {x,y}
 @fires ludo.effect.Drag#start Event when drag starts. Params: 1) Dom element to be dragged, 2) ludo.effect.Drag, 3) {x,y}
 @fires ludo.effect.Drag#drag' Event when drag ends. Params: 1) Dom element to be dragged, 2) ludo.effect.Drag, 3) {x,y}
 @fires ludo.effect.Drag#end' Event when drag ends. Params: 1) {x,y}, 2) dragged node 3) ludo.effect.Drag
 @fires ludo.effect.Drag#showShim' Event fired when shim DOM node is shown. Argument: 1) Shim DOM Node, 2) ludo.effect.Drag
 @fires ludo.effect.Drag#flyToShim' Event fired after flyBack animation is complete. Arguments: 1) ludo.effect.Drag, 2) Shim DOM node
 @fires ludo.effect.Drag#flyBack' Event fired when shim DOM node is shown. Argument: Arguments: 1) ludo.effect.Drag, 2) Shim DOM node


 @example
 <style type="text/css">
 .ludo-shim {
		 border: 15px solid #AAA;
		 background-color: #DEF;
		 margin: 5;
		 opacity: .5;
		 border-radius: 5px;
	}
 .draggable{
		width:150px;
		z-index:1000;
		height:150px;
		border-radius:5px;
		border:1px solid #555;
		background-color:#DEF
	}
 </style>
 <div id="draggable" class="draggable">
 I am draggable
 </div>
 <script type="text/javascript">
 var d = new ludo.effect.Drag({
		useShim:true,
		 listeners:{
			 endDrag:function(dragged, dragEffect){
				 dragEffect.getEl().setStyles({
					 left : dragEffect.getX(),
					 top: dragEffect.getY()
				 });
			 },
			 drag:function(pos, dragEffect){
				 dragEffect.setShimText(dragEffect.getX() + 'x' + dragEffect.getY());
			 }
		 }
	 });
 d.add('draggable'); // "draggable" is the id of the div
 </script>

 */
ludo.effect.Drag = new Class({
    Extends: ludo.effect.Effect,


    handle: undefined,

    el: undefined,


    minX: undefined,

    minY: undefined,


    maxX: undefined,

    maxY: undefined,


    minPos: undefined,

    maxPos: undefined,

    directions: 'XY',


    unit: 'px',

    dragProcess: {
        active: false
    },

    coordinatesToDrag: undefined,

    delay: 0,

    inDelayMode: false,

    els: {},


    useShim: false,


    autoHideShim: true,


    shimCls: undefined,

    mouseYOffset: undefined,


    mouseXOffset: undefined,

    fireEffectEvents: true,

    __construct: function (config) {
        this.parent(config);
        if (config.el !== undefined) {
            this.add({
                el: config.el,
                handle: config.handle
            });
        }

        this.setConfigParams(config, ['useShim', 'autoHideShim', 'directions', 'delay', 'minX', 'maxX', 'minY', 'maxY',
            'minPos', 'maxPos', 'unit', 'shimCls', 'mouseYOffset', 'mouseXOffset', 'fireEffectEvents']);
    },

    ludoEvents: function () {
        this.parent();
        this.getEventEl().on(ludo.util.getDragMoveEvent(), this.drag.bind(this));
        this.getEventEl().on(ludo.util.getDragEndEvent(), this.endDrag.bind(this));
        if (this.useShim) {
            this.addEvent('start', this.showShim.bind(this));
            if (this.autoHideShim) {
                this.addEvent('end', this.hideShim.bind(this));
            }
        }
    },

    /**
     Add draggable object
     @function add
     @param {effect.DraggableNode|String|HTMLDivElement} node
     @memberof ludo.effect.Effect.prototype
     @return {effect.DraggableNode}
     @example
     dragObject.add({
			el: 'myDiv',
			handle : 'myHandle'
		});
     handle is optional.

     @example
     dragObject.add('idOfMyDiv');

     You can also add custom properties:

     @example
     dragobject.add({
	 		id: "myReference',
			el: 'myDiv',
			column: 'city'
		});
     ...
     ...
     dragobject.addEvent('before', beforeDrag);
     ...
     ...
     function beforeDrag(dragged){
	 		console.log(dragged.el);
	 		console.log(dragged.column);
	 	}
     */
    add: function (node) {
        node = this.getValidNode(node);
        var el = $(node.el);
        this.setPositioning(el);

        var handle = node.handle ? $(node.handle) : el;

        handle.attr("id",  handle.id || 'ludo-' + String.uniqueID());
        handle.addClass("ludo-drag");

        handle.on(ludo.util.getDragStartEvent(), this.startDrag.bind(this));
        handle.attr('forId', node.id);
        this.els[node.id] = Object.merge(node, {
            el: $(el),
            handle: handle
        });
        return this.els[node.id];
    },

    /**
     * Remove node
     * @function remove
     * @param {String} id
     * @return {Boolean} success
     * @memberof ludo.effect.Effect.prototype
     */
    remove: function (id) {
        if (this.els[id] !== undefined) {
            var el = $("#" + this.els[id].handle);
            el.off(ludo.util.getDragStartEvent(), this.startDrag.bind(this));
            this.els[id] = undefined;
            return true;
        }
        return false;
    },

    removeAll: function () {
        var keys = Object.keys(this.els);
        for (var i = 0; i < keys.length; i++) {
            this.remove(keys[i]);
        }
        this.els = {};
    },

    getValidNode: function (node) {
        if (!this.isElConfigObject(node)) {
            node = {
                el: $(node)
            };
        }
        if (typeof node.el === 'string') {
            if (node.el.substr(0, 1) != "#")node.el = "#" + node.el;
            node.el = $(node.el);
        }
        node.id = node.id || node.el.attr("id") || 'ludo-' + String.uniqueID();
        if (!node.el.attr("id"))node.el.attr("id", node.id);
        node.el.attr('forId', node.id);
        return node;
    },

    isElConfigObject: function (config) {
        return config.el !== undefined || config.handle !== undefined;
    },

    setPositioning: function (el) {
        if (!this.useShim) {
            el.css('position', 'absolute');
        } else {
            var pos = el.css('position');
            if (!pos || (pos != 'relative' && pos != 'absolute')) {
                el.css('position', 'relative');
            }
        }
    },

    getById: function (id) {
        return this.els[id];
    },

    getIdByEvent: function (e) {
        var el = $(e.target);
        if (!el.hasClass('ludo-drag')) {
            el = el.closest('.ludo-drag');
        }
        return el.attr('forId');
    },


    getDragged: function () {
        return this.els[this.dragProcess.dragged];
    },

    /**
     * Returns reference to draggable DOM node
     * @function getEl
     * @return {HTMLElement} DOMNode
     * @memberof ludo.effect.Effect.prototype
     */
    getEl: function () {
        return this.els[this.dragProcess.dragged].el;
    },

    getShimOrEl: function () {
        return this.useShim ? this.getShim() : this.getEl();
    },

    getSizeOf: function (el) {
        return el.outerWidth !== undefined ? {x: el.outerWidth(), y: el.outerHeight()} : {x: 0, y: 0};
    },

    getPositionOf: function (el) {

        return $(el).position();
    },

    setDragCoordinates: function () {
        this.coordinatesToDrag = {
            x: 'x', y: 'y'
        };
    },
    startDrag: function (e) {
        var id = this.getIdByEvent(e);

        var el = this.getById(id).el;

        var size = this.getSizeOf(el);
        var pos;
        if (this.useShim) {
            pos = el.position();
        } else {
            var parent = this.getPositionedParent(el);
            pos = parent ? el.getPosition(parent) : this.getPositionOf(el)
        }

        var x = pos.left;
        var y = pos.top;

        var p = e.touches != undefined && e.touches.length > 0 ? e.touches[0] : e;

        this.dragProcess = {
            active: true,
            dragged: id,
            currentX: x,
            currentY: y,
            elX: x,
            elY: y,
            width: size.x,
            height: size.y,
            mouseX: p.pageX,
            mouseY: p.pageY
        };

        var dp = this.dragProcess;

        dp.el = this.getShimOrEl();

        this.fireEvent('before', [this.els[id], this, {x: x, y: y}]);

        if (!this.isActive()) {
            return undefined;
        }

        dp.minX = this.getMinX();
        dp.maxX = this.getMaxX();
        dp.minY = this.getMinY();
        dp.maxY = this.getMaxY();
        dp.dragX = this.canDragAlongX();
        dp.dragY = this.canDragAlongY();

        if (this.delay) {
            this.setActiveAfterDelay();
        } else {

            this.fireEvent('start', [this.els[id], this, {x: x, y: y}]);

            if (this.fireEffectEvents)ludo.EffectObject.start();
        }

        if(!ludo.util.isTabletOrMobile()){
            return false;
        }


    },

    getPositionedParent: function (el) {

        var parent = el.parentNode;
        while (parent) {
            var pos = parent.getStyle('position');
            if (pos === 'relative' || pos === 'absolute')return parent;
            parent = parent.getParent();
        }
        return undefined;
    },

    /**
     Cancel drag. This method is designed to be called from an event handler
     attached to the "beforeDrag" event.
     @function cancelDrag
     @memberof ludo.effect.Effect.prototype
     @example
     // Here, dd is a {{#crossLink "effect.Drag"}}{{/crossLink}} object
     dd.on('before', function(draggable, dd, pos){
	 		if(pos.x > 1000 || pos.y > 500){
	 			dd.cancelDrag();
			}
	 	});
     In this example, dragging will be cancelled when the x position of the mouse
     is greater than 1000 or if the y position is greater than 500. Another more
     useful example is this:
     @example
     dd.on('before', function(draggable, dd){
		 	if(!this.isDraggable(draggable)){
		 		dd.cancelDrag()
		 	}
		});
     Here, we assume that we have an isDraggable method which returns true or false
     for whether the given node is draggable or not. "draggable" in this example
     is one of the {{#crossLink "effect.DraggableNode"}}{{/crossLink}} objects added
     using the {{#crossLink "effect.Drag/add"}}{{/crossLink}} method.
     */

    cancelDrag: function () {
        this.dragProcess.active = false;
        this.dragProcess.el = undefined;
        if (this.fireEffectEvents)ludo.EffectObject.end();
    },

    getShimFor: function (el) {
        return el;
    },

    setActiveAfterDelay: function () {
        this.inDelayMode = true;
        this.dragProcess.active = false;
        this.startIfMouseNotReleased.delay(this.delay * 1000, this);
    },

    startIfMouseNotReleased: function () {
        if (this.inDelayMode) {
            this.dragProcess.active = true;
            this.inDelayMode = false;
            this.fireEvent('start', [this.getDragged(), this, {x: this.getX(), y: this.getY()}]);
            ludo.EffectObject.start();
        }
    },

    drag: function (e) {
        if (this.dragProcess.active && this.dragProcess.el) {
            var pos = {
                x: undefined,
                y: undefined
            };
            if (this.dragProcess.dragX) {
                pos.x = this.getXDrag(e);

            }

            if (this.dragProcess.dragY) {
                pos.y = this.getYDrag(e);
            }


            this.move(pos);

            /*
             * Event fired while dragging. Sends position, example {x:100,y:50}
             * and reference to effect.Drag as arguments
             * @event drag
             * @param {Object} x and y
             * @param {effect.Drag} this
             */
            this.fireEvent('drag', [pos, this.els[this.dragProcess.dragged], this]);
            return false;

        }
        return undefined;
    },

    move: function (pos) {
        if (pos.x !== undefined) {
            this.dragProcess.currentX = pos.x;
            this.dragProcess.el.css('left', pos.x + this.unit);
        }
        if (pos.y !== undefined) {
            this.dragProcess.currentY = pos.y;
            this.dragProcess.el.css('top', pos.y + this.unit);
        }
    },

    /**
     * Return current x pos
     * @function getX
     * @return {Number} x
     * @memberof ludo.effect.Effect.prototype
     */
    getX: function () {
        return this.dragProcess.currentX;
    },
    /**
     * Return current y pos
     * @function getY
     * @return {Number} y
     * @memberof ludo.effect.Effect.prototype
     */
    getY: function () {
        return this.dragProcess.currentY;
    },

    getXDrag: function (e) {
        var posX;

        var p = e.touches != undefined && e.touches.length > 0 ? e.touches[0] : e;

        if (this.mouseXOffset) {
            posX = p.pageX + this.mouseXOffset;
        } else {
            posX = p.pageX - this.dragProcess.mouseX + this.dragProcess.elX;
        }

        if (posX < this.dragProcess.minX) {
            posX = this.dragProcess.minX;
        }
        if (posX > this.dragProcess.maxX) {
            posX = this.dragProcess.maxX;
        }
        return posX;
    },

    getYDrag: function (e) {
        var posY;
        var p = e.touches != undefined && e.touches.length > 0 ? e.touches[0] : e;

        if (this.mouseYOffset) {
            posY = p.pageY + this.mouseYOffset;
        } else {
            posY = p.pageY - this.dragProcess.mouseY + this.dragProcess.elY;
        }

        if (posY < this.dragProcess.minY) {
            posY = this.dragProcess.minY;
        }
        if (posY > this.dragProcess.maxY) {
            posY = this.dragProcess.maxY;
        }
        return posY;
    },

    endDrag: function () {
        if (this.dragProcess.active) {
            this.cancelDrag();

            this.fireEvent('end', [
                this.getDragged(),
                this,
                {
                    x: this.getX(),
                    y: this.getY()
                }
            ]);

        }
        if (this.inDelayMode)this.inDelayMode = false;

    },

    /**
     * Set new max X pos
     * @function setMaxX
     * @param {Number} x
     * @memberof ludo.effect.Effect.prototype
     */
    setMaxX: function (x) {
        this.maxX = x;
    },
    /**
     * Set new min X pos
     * @function setMinX
     * @param {Number} x
     * @memberof ludo.effect.Effect.prototype
     */
    setMinX: function (x) {
        this.minX = x;
    },
    /**
     * Set new min Y pos
     * @function setMinY
     * @param {Number} y
     * @memberof ludo.effect.Effect.prototype
     */
    setMinY: function (y) {
        this.minY = y;
    },
    /**
     * Set new max Y pos
     * @function setMaxY
     * @param {Number} y
     * @memberof ludo.effect.Effect.prototype
     */
    setMaxY: function (y) {
        this.maxY = y;
    },
    /**
     * Set new min pos
     * @function setMinPos
     * @param {Number} pos
     * @memberof ludo.effect.Effect.prototype
     */
    setMinPos: function (pos) {
        this.minPos = pos;
    },
    /**
     * Set new max pos
     * @function setMaxPos
     * @param {Number} pos
     * @memberof ludo.effect.Effect.prototype
     */
    setMaxPos: function (pos) {
        this.maxPos = pos;
    },

    getMaxX: function () {
        return this.getMaxPos('maxX');
    },

    getMaxY: function () {
        return this.getMaxPos('maxY');
    },

    getMaxPos: function (key) {
        var max = this.getConfigProperty(key);
        return max !== undefined ? max : this.maxPos !== undefined ? this.maxPos : 100000;
    },

    getMinX: function () {
        var minX = this.getConfigProperty('minX');
        return minX !== undefined ? minX : this.minPos;
    },

    getMinY: function () {
        var dragged = this.getDragged();
        return dragged && dragged.minY !== undefined ? dragged.minY : this.minY !== undefined ? this.minY : this.minPos;
    },
    /**
     * Return amount dragged in x direction
     * @function getDraggedX
     * @return {Number} x
     * @memberof ludo.effect.Effect.prototype
     */
    getDraggedX: function () {
        return this.getX() - this.dragProcess.elX;
    },
    /**
     * Return amount dragged in y direction
     * @function getDraggedY
     * @return {Number} y
     * @memberof ludo.effect.Effect.prototype
     */
    getDraggedY: function () {
        return this.getY() - this.dragProcess.elY;
    },

    canDragAlongX: function () {
        return this.getConfigProperty('directions').indexOf('X') >= 0;
    },
    canDragAlongY: function () {
        return this.getConfigProperty('directions').indexOf('Y') >= 0;
    },

    getConfigProperty: function (property) {
        var dragged = this.getDragged();
        return dragged && dragged[property] !== undefined ? dragged[property] : this[property];
    },

    /**
     * Returns width of dragged element
     * @function getHeight
     * @return {Number}
     * @memberof ludo.effect.Effect.prototype
     */
    getWidth: function () {
        return this.dragProcess.width;
    },

    /**
     * Returns height of dragged element
     * @function getHeight
     * @return {Number}
     * @memberof ludo.effect.Effect.prototype
     */
    getHeight: function () {
        return this.dragProcess.height;
    },
    /**
     * Returns current left position of dragged
     * @function getLeft
     * @return {Number}
     * @memberof ludo.effect.Effect.prototype
     */
    getLeft: function () {
        return this.dragProcess.currentX;
    },

    /**
     * Returns current top/y position of dragged.
     * @function getTop
     * @return {Number}
     * @memberof ludo.effect.Effect.prototype
     */
    getTop: function () {
        return this.dragProcess.currentY;
    },

    /**
     * Returns reference to DOM element of shim
     * @function getShim
     * @return {HTMLDivElement} shim
     * @memberof ludo.effect.Effect.prototype
     */
    getShim: function () {
        if (this.shim === undefined) {
            this.shim = $('<div>');
            this.shim.addClass('ludo-shim');
            this.shim.css({
                position: 'absolute',
                'z-index': 50000,
                display: 'none'
            });
            $(document.body).append(this.shim);

            if (this.shimCls) {
                for (var i = 0; i < this.shimCls.length; i++) {
                    this.shim.addClass(this.shimCls[i]);
                }
            }

            this.fireEvent('createShim', this.shim);
        }
        return this.shim;
    },


    showShim: function () {
        this.getShim().css({
            display: '',
            left: this.getShimX(),
            top: this.getShimY(),
            width: this.getWidth() + this.getShimWidthDiff(),
            height: this.getHeight() + this.getShimHeightDiff()
        });

        this.fireEvent('showShim', [this.getShim(), this]);
    },

    getShimY: function () {
        if (this.mouseYOffset) {
            return this.dragProcess.mouseY + this.mouseYOffset;
        } else {
            return this.getTop() + ludo.dom.getMH(this.getEl()) - ludo.dom.getMW(this.shim);
        }
    },

    getShimX: function () {
        if (this.mouseXOffset) {
            return this.dragProcess.mouseX + this.mouseXOffset;
        } else {
            return this.getLeft() + ludo.dom.getMW(this.getEl()) - ludo.dom.getMW(this.shim);
        }
    },

    getShimWidthDiff: function () {
        return ludo.dom.getMW(this.getEl()) - ludo.dom.getMBPW(this.shim);
    },
    getShimHeightDiff: function () {
        return ludo.dom.getMH(this.getEl()) - ludo.dom.getMBPH(this.shim);
    },

    /**
     * Hide shim
     * @function hideShim
     * @memberof ludo.effect.Effect.prototype
     */
    hideShim: function () {
        this.getShim().css('display', 'none');
    },

    /**
     * Set text content of shim
     * @function setShimText
     * @param {String} text
     * @memberof ludo.effect.Effect.prototype
     */
    setShimText: function (text) {
        this.getShim().html(text);
    },
    

    isActive: function () {
        return this.dragProcess.active;
    }
});