1 /**
 * @author Garrett Smith, � 2008 
 * @version 1
 * @fileoverview:
 * contains: APE.drag.Draggable, DragHandlers, APE.drag.DropTarget
 *
 * @requires APE.EventPublisher, APE.dom
 * 
 * classNames:
 * <ul>
 *   <li>activeDragClassName</li>
 *   <li>focusClassName</li>
 *   <li>selectedClassName</li>
 *   <li>dragOverClassName (for dropTarget)</li>
 *   <li>focusClassName (focused drag object)</li>
 * </ul>
 *
 * APE.drag.Draggable Features:
 * <ul>
 *   <li>dragCopy</li>
 *   <li>dragMultiple</li>
 *   <li>setHandle(handle, useTree)</li>
 * </ul>
 *
 * @example Create a Draggable:
 *<pre>
 * var Draggable = APE.drag.Draggable,
 *     el = document.getElementById(<var>"box"</var>),
 *     box = Draggable.getByNode( el );
 * box.keepInContainer = true;
 * box.activeDragClassName = "boxDragging";
 * box.focusClassName = "boxFocused";
 * 
 * var bigBx = box.addDropTarget( document.getElementById("biggerBox") );
 * bigBx.dragOverClassName = "boxDragOver";
 * </pre>
 */

/** @name APE.drag.Draggable
 * @namespace */
APE.namespace("APE.drag");

/** 
 * @private
 * @description Do not call the constructor directly.
 * @param {HTMLElement} el the element to drag.
 * @param {uint} [constraint] (0 | 1 | 2) default is 0.
 * @see APE.drag.Draggable.constraints
 */
APE.drag.Draggable = function(el, constraint) {
    this.id = el.id;
    this.el = this.origEl = el;
    this.style = el.style;
    this.isRel = APE.dom.getStyle(el, "position").toLowerCase() == "relative";

    // default 'container' is the containing block.
    this.container = (this.isRel ? el.parentNode : APE.dom.getContainingBlock(el));
    this.dropTargets = [];
    this.handle = el;
    this.constraint = constraint||0;
    this.init();
};

/** 
 * @memberOf APE.drag.Draggable
 * @function
 * @return {APE.drag.Draggable} 
 * @param {HTMLElement} el 
 * @param {uint} [constraint] (0 | 1 | 2) default is 0.
 * @static
 * @description Use Draggable.getByNode, <em>not</em> new. */
APE.drag.Draggable.getByNode = APE.getByNode;    

/** @name APE.drag.instanceDestructor
 * @private
 * @param {HTMLElement} el the element to drag
 * @param {uint} constraint   
 * @see APE.drag.Draggable.constraints
 */
APE.drag.Draggable.instanceDestructor = function() {
    var x, p, dO, DragHandlers = APE.drag.DragHandlers;
    for(x in this.instances) {
        dO = this.instances[x];
        for(p in dO)
            if(dO.hasOwnProperty(p)) 
                delete dO[p];
        delete this.instances[x];
    }
    if(dO) {
        dO.constructor.draggableList = {};
        DragHandlers.focusedDO = DragHandlers.dO = null;
    }
};

/** 
 * @private
 *  Callback handler for a draggable's intrinsic focus event.
 */
APE.drag.Draggable.focused = function(e) {

    // TODO: separate concern.
    // IE will fire focus events when, in retireClone, when this.el = this.origEl.
    // This can be demonstrated by holding the mouse down on a dragObj for 2 sec, then releasing the mouse.
    if(timeStamp - arguments.callee.timeStamp < 5) return; // recurrant.
    arguments.callee.timeStamp = timeStamp; // record.
    e = e||event;

    var timeStamp = new Date-0;

    if(typeof e.stopPropagation == "function") 
        e.stopPropagation();
    else 
        e.cancelBubble = true;
    this.setFocus(true, e);
};

/** 
 * @private
 *  Callback handler for a draggable's intrinsic focus event.
 */
APE.drag.Draggable.blurred = function(e) {
    this.setFocus(false, e);
};

/** 
 * @memberOf APE.drag.Draggable
 */
APE.drag.Draggable.constraints = { NONE : 0, HORZ : 1, VERT : 2 };

APE.drag.Draggable.prototype = {
    
    /** @type {boolean} 
     * @private 
     * @description internal flag
     */
    hasFocus : false,

    /** @type {boolean} 
     * @description set to true to make a temporary "ghost" copy dragged.
     */
    dragCopy : false,

    /** @type {boolean} 
     * @description set to true to allow this to be dragged as a group.
     */
    dragMultiple : false,
 
    isSelected : false,

/**
 *  A subset of dropTargets that have ondragover or ondragout.
 *  created onmousedown, to help boost performance by reducing count for ondragover 
 *     
 **/    

    _dragOverTargets : false,

   /** @event
    * @description Has been grabbed.
    */
   onfocus : undefined,
   /** @event
    * @description Has been blurred.
    */
   onblur : undefined,

   /** @event
    * @description Is about to move.
    */
   onbeforedrag : undefined,

   /** @event
    * @description Has been grabbed.
    */
   onbeforedragstart : undefined,
   /** @event
    *  @param {Event} e dom event.
    * Mouse has moved.
    */
   ondragstart : undefined,
     
   /** @event
    *  @param {Event} e dom event.
    *  @description Being dragged
    */
   ondrag : undefined,
 
   /** @event
    *  @description Dragging stopped before it escaped its container. 
    */
   ondragstop : undefined,
 
   /** @event
    *  @description Dragging completed (as a result of mouseup). 
    */
   ondragend : undefined,
 

    /**@type {Number} 
     * @description current x position*/
    x : 0,
    /**@type {Number} 
     * @description current y position*/
    y : 0,
    /**@type {Number} 
     * @description where drag started from */
    origX : 0,
    /**@type {Number} 
     * @description where drag started from */
    origY : 0,
    /**@type {Number} 
     * @description where draggable was grabbed from */
    grabX : 0,
    /**@type {Number} 
     * @description where draggable was grabbed from */
    grabY : 0,

    /** @type {Number} 
     * @description Where it will move to next. onbeforedrag */
    newX : 0, 
    /** @type {Number} 
     * @description Where it will move to next. onbeforedrag */
    newY : 0,

    /** @type {Number} 
     * @description default: APE.drag.Draggable.constraints.NONE */
    constraint : APE.drag.Draggable.constraints.NONE,

    /** @type {boolean} 
     * @description drag object can be dragged outside of its container */
    keepInContainer : false,
    /** @type {boolean} 
     * @description drag object can be disabled by setting to this to false */
    isDragEnabled : true,
    
    /** @type {String} 
     * @description className to add when selected. */
    selectedClassName : "",
    /** @type {String} 
     * @description className to add before being dragged. */
    activeDragClassName : "",
    /** @type {String} 
     * @description className to add when focused. */
    focusClassName : "",

    init : function(){
        var EventPublisher = APE.EventPublisher,
            drag = APE.drag,
            Draggable = drag.Draggable,
            el = this.el;
        el.style.zIndex = APE.dom.getStyle(el, "zIndex") || Draggable.highestZIndex++;
        
        this._setIeTopLeft();

        EventPublisher.add(el, "onfocus", Draggable.focused, this);
        EventPublisher.add(el, "onblur", Draggable.blurred, this);

        // For IE, if the attribute is not present, 0 will be returned.
        // For Moz, Webkit, Op, if attribute is not present, null will be returned,
        // but the default value for the DOM property will be -1 (truthy), so use getAttribute. 
        if(!el.getAttribute('tabIndex')) el.tabIndex = 0; // Allow default kbd navigation.

        /** Will be dragged */
        this.onbeforeexitcontainer = function() { return !this.keepInContainer; };

        drag.DragHandlers.init();
    },

    useHandleTree : true,

    hasHandleSet : false,

    /** Sets a handle on a draggable 
     * @param {HTMLElement} el the element to use as a handle.
     * By default, the handle is the draggable.
     * @param {boolean} [setHandleTree] if true, the draggable can use anything in the 
     * handle's subtree for dragging.
     */
    setHandle : function(el, setHandleTree){
        this.handle = el;
        this.hasHandleSet = true;
        // Make sure user didn't forget the secondParam and expect true.
        this.useHandleTree = setHandleTree != false;
    },
    
    /** @param {HTMLElement} target Element that is checked.
     * @private
     */
    isInHandle : function(target) {
         return target == this.handle || (this.useHandleTree && APE.dom.contains( this.handle, target ));
     },

    /** 
     * Adds a drop target.
     * @param {HTMLElement|APE.drag.DropTarget} dropTarget either an element or a DropTarget.
     * @return {DropTarget} The drop target that was added.
     */
    addDropTarget : function(dropTarget) {
        var DropTarget = APE.drag.DropTarget, 
            el = DropTarget.getByNode(dropTarget).el,
            dropTargets = this.dropTargets;
        if(this.el === el) return;
        return dropTargets[dropTargets.length] = DropTarget.getByNode(el);
    },
    
    /** 
     * Grabs the draggable, centering it under the cursor.
     * @param {Event} e the event to grab the element from.
     * @param {int} [xOffset] amount of horizontal adjustment to apply.
     * @param {int} [xOffset] amount of vertical adjustment to apply.
     */
    grab : function(e, xOffset, yOffset) {
        if(!e) e = event;
        
        var dom = APE.dom,
            Event = dom.Event,
            target = Event.getTarget(e),
            drag = APE.drag,
            DragHandlers = drag.DragHandlers;

        if(e.preventDefault) e.preventDefault();
        e.returnValue = false;
        
        if(dom.contains(this.el, target)) return;
         
        this._fixFocus(e);

        var grabCoords = dom.getPixelCoords(this.el);
        this.grabX = grabCoords.x;
        this.grabY = grabCoords.y;
        
        // Get the container's offset.
        var eventCoords = Event.getCoords(e),
            offsetCoords = dom.getOffsetCoords(dom.getContainingBlock(this.el)),
            offsetY = eventCoords.y - offsetCoords.y,
            newY = Math.floor(offsetY - (this.handle.offsetHeight/2)),
            handleOffsetCoords = dom.getOffsetCoords(this.handle, this.el),
            constraints = drag.Draggable.constraints;
        
        if(this.constraint != constraints.VERT) {
            // Center the dragObject around the coords, but keep it inside.

            var offsetX = eventCoords.x - offsetCoords.x,
                newX = offsetX - Math.floor((this.handle.offsetWidth/2));
            if(this.keepInContainer) {
                newX = Math.max(newX, 0);
                newX = Math.min(newX, this.container.clientWidth - this.el.offsetWidth);
            }

            this.moveToX(newX- handleOffsetCoords.x + (xOffset||0));
        }
        if(this.constraint != constraints.HORZ) {
            if(this.keepInContainer) {
                newY = Math.max(newY, 0);
                newY = Math.min(newY, this.container.clientHeight - this.el.offsetHeight);
            }
            this.moveToY(newY - handleOffsetCoords.y + (yOffset||0));
        }
        DragHandlers.dragObjGrabbed(e, this);    
        DragHandlers.dO = this;
    },
    
    /** 
     * Selects the draggable, adding selectedClassName
     * @param {boolean} isSelect if false, deselects.
     */
    select : function(isSelect) {
        var APE = window.APE, Draggable = APE.drag.Draggable;
        if(isSelect) {
            if(this.selectedClassName)
                APE.dom.addClass(this.el, this.selectedClassName);
            // onselect handler would go here, if/when needed. return false to prevent.
            if(this.dragMultiple && ! (this.id in Draggable.draggableList))
                Draggable.draggableList[this.id] = this;
        }
        else {
            if(this.selectedClassName)
                APE.dom.removeClass(this.el, this.selectedClassName);
            // ondeselect handler would go here, if/when needed.
            delete Draggable.draggableList[this.id];
        }
        this.isSelected = Boolean(isSelect);
    }

    /** 
     * @param {boolean} isFocus if false, blurs.
     * @param {Event} e the DOM event.
     */
    ,setFocus : function(isFocus, e) {

        if(isFocus == this.hasFocus) return; // nothing to do.

        if(!this.isDragEnabled) return false; 

        //console.log('set ' + this + ' focus = ' + isFocus);
        var ret = true,
            DragHandlers = APE.drag.DragHandlers, dom = APE.dom;
        if(isFocus) {
            if(this.focusClassName)
                dom.addClass(this.el, this.focusClassName);
            if(typeof this.onfocus == "function") { 
                ret = this.onfocus(e);
            }
            if(ret != false) {
                if(DragHandlers.focusedDO) {
                    DragHandlers.focusedDO.setFocus(false, e);
                }
                DragHandlers.focusedDO = this; // Should be afterAdvice.
            }
        }
        else {
            if(this.focusClassName)
                dom.removeClass(this.el, this.focusClassName);
            if(typeof this.onblur == "function") 
                ret = this.onblur(e);
            if(ret != false) DragHandlers.focusedDO = null; // Should be afterAdvice.
        }
        this.hasFocus = isFocus;
        return ret;
    },
    
    /** @private
     * @type {APE.drag.DropTarget[]|boolean}
     * @description An array of DropTarget that has one of: 
     * an ondragover or ondragout handler
     * a hoverClassName
     */
    _dragOverTargets : false,

    /** @private
     */
    _fixFocus : function(e) {
        // Mozilla will not give focus to/remove focus from an element when mouseDown returns false;
        // However, it is necessary to return false onmousedown to prevent selecting 
        // an img dragObj and having the browser "grab" it.
        //
        // IE won't set focus unless the element has tabIndex. 
        // Conditionally force focus when the element was clicked, regardless of IE/Moz anomalies.

        // Mozilla still won't get the focus event.
    

        var metaKey = e.metaKey || (/Win/.test(navigator.platform) && e.ctrlKey);
        if(!this.dragMultiple && APE.drag.DragHandlers.focusedDO && metaKey) return false;

        if(!this.hasFocus) {
            this.setFocus(!this.hasFocus, e);
        }
    },

    /** @private
     * grab will create a clone here, and will be released to here. 
     */
    dragStart : function(e) {

        if(this.isBeingDragged) return;

        if(this.dragCopy) {
            this.assignClone(e);  // this.el assigned to copyEl, this.origEl stays put.
        }
        if(typeof this.ondragstart == "function")
            this.ondragstart(e);

        if(this.activeDragClassName) 
            APE.dom.addClass(this.el, this.activeDragClassName);
        // Check the coords after making the copyEl here.
        APE.drag.DragHandlers.setUpCoords(e, this);
        this.isBeingDragged = true; 
    },
    
    /**
     * releases the draggable, as if the mouse had been released.
     * @param {Event} [e] the event that triggered release
     */
    release : function(e) {
        APE.drag.DragHandlers.dragObjReleased(e, this);
        if(typeof this.onrelease == "function") 
            this.onrelease(e);
    },

    /**@private
     * creates a copyEl for dragCopy
     */
    assignClone : function(e) {

        var dom = APE.dom, addClass = "addClass",
            copyEl,
            el = this.el,
            origEl = el,
            copyElStyle;

        if(!this.copyEl) {
            this.origEl = el;
            this.copyEl = el.cloneNode(true);
        }
        copyEl = this.copyEl;
        copyElStyle = copyEl.style;

        if(this.focusClassName) 
            dom[this.hasFocus ? addClass : "removeClass"](copyEl, this.focusClassName);
        
        copyElStyle.display = "";

        if(copyEl.parentNode != el.parentNode)
        // In case the element was appened elsewhere, by external script
            el.parentNode.insertBefore(copyEl, el);

        // 100 draggable items appear above.
        copyElStyle.zIndex = parseInt(origEl.style.zIndex) + 100;
        if(this.origClassName)
            dom[addClass](el, this.origClassName);
        
        this.el = copyEl;
        this.style = copyElStyle;

        // This helps prevent copyEl from displacing other elements.
        if(this.isRel) {
            copyElStyle.marginBottom = -origEl.offsetHeight + 
                -(parseInt(dom.getStyle(origEl, "marginBottom"))||0) + "px";
            copyElStyle.marginright = -origEl.offsetWidth + 
                -(parseInt(dom.getStyle(origEl, "marginRight"))||0) + "px";
        }
    },

    /** @private
     */
    retireClone : function() {
        // this causes IE to lose focus, then fire focus for another element (IE decides which one).
        this.constructor.focused.timeStamp = new Date;

        if(this.copyEl.style.display == "none") return;
    
        this.el = this.origEl;
        this.style = this.origEl.style;
        
        // Update position of origEl, which was left behind.
        this.moveToX(this.x);
        this.moveToY(this.y);
        this.copyEl.style.display = "none";
        if(this.origClassName)
            APE.dom.removeClass(this.el, this.origClassName);
    },

    moveToX : ('pixelLeft'in document.documentElement.style ? 
        function(x) { this.style.pixelLeft = this.x = x; } :
        function(x) { this.style.left = (this.x = x) + "px"; }),
    
    moveToY : ('pixelTop'in document.documentElement.style ? 
        function(y) { this.style.pixelTop = this.y = y;} :
        function(y) { this.style.top = (this.y = y) + "px";}),

    moveToXY : ('pixelTop'in document.documentElement.style ? 
        function(x,y) { 
            var s = this.style;
            s.pixelLeft = this.x = x;
            s.pixelTop = this.y = y;
        } :
        function(x,y) {
            var s = this.style;
            s.left = (this.x = x) + "px";
            s.top = (this.y = y) + "px";
    }),
    
    /**
     * @private
     */
    glideStart : function(x, y) {
    // Would be cleaner to separate this concern; APE.drag.Draggable should not have to concern itself 
    // for animation and notifying subscribers of onglide, et c.

        if(this.animTimer) return;
        
        this.startX = x;
        this.startY = y;

        var dx = this.startX - this.grabX,
            dy = this.startY - this.grabY;

        // Calculate Hypoteneuse.
        this.GlideDist = Math.ceil(Math.sqrt((dx * dx) + (dy * dy)));

        if(this.GlideDist === 0) return;
        this.rx = Math.abs(dx)/this.GlideDist;
        this.ry = Math.abs(dy)/this.GlideDist;
        if(this.x > this.grabX)
            this.rx = -this.rx;
        if(this.y > this.grabY)
            this.ry = -this.ry;
        
        this.startTime = new Date().getTime();
        this.animTimer = window.setInterval("APE.drag.Draggable.instances['"+this.id+"'].glide()", 10);
    },

    /**
     * @private
     */
    glide : function() {
        var t = new Date - this.startTime,
        // 2px per 10ms slight acceleration 10px/s
            d = Math.ceil(2 * t + .5 * .01 * t*t);

        if(d >= this.GlideDist) {
            this.animTimer = clearInterval(this.animTimer);
            if(this.constraint != 2) 
                this.moveToX( this.grabX );
            if(this.constraint != 1)
                this.moveToY( this.grabY );
            if(this.copyEl) {
                this.el = this.origEl;
                this.style = this.origEl.style;
                this.copyEl.style.display = "none";
            }
            if(typeof this.onglide == "function")
                this.onglide();
            if(typeof this.onglideend == "function")
                this.onglideend();
            this.dragDone({});
        }
        else {
            if(this.constraint != 2) 
                 this.moveToX(this.startX + d * this.rx);
            if(this.constraint != 1)
                 this.moveToY(this.startY + d * this.ry);
            if(typeof this.onglide == "function")
                this.onglide();
        }
    },
    
    /** Starts gliding the draggable back to its original x,y coords. 
     * @param {Number} [x] x coordinate to start gliding from.
     * @param {Number} [y] y coordinate to start gliding from.
     */
    animateBack : function(x, y) {
         this.glideStart(x||this.x, y||this.y);
    },
    
    /** A dragObj does not check search for containing block each time its grabbed/dragged.
     * Instead, it reuses the container. If the container must change, this must be done 
     * manually, via dragObj.setContainer(newEl);
     */
    setContainer : function(el) {
        this.container = el;
    },
    
    /** 
     * Removes a drop target.
     * @param {HTMLElement|DropTarget} element or DropTarget to remove.
     * @return {HTMLElement} the removed dropTarget element.
     */
    removeDropTarget : function(el){
        el = document.getElementById(el.id); 
        
        for(var i = 0, len = this.dropTargets.length; i < len; i++) {
            if(this.dropTargets[i].el === el) {
                this.dropTargets.splice(i, 1);
                return el;
            }
        }
        return null;
    },
    
    /** 
     * @fires this.ondragend()
     * fires the ondragend handler.
     */
    dragDone : function(e) {
        if(this.activeDragClassName)
            APE.dom.removeClass(this.el, this.activeDragClassName);
        if(typeof this.ondragend == "function" && this.hasBeenDragged) {
            this.ondragend(e);
        }
        if(this.copyEl) { // in case user does some appending of el, et c.
            this.el.parentNode.insertBefore(this.copyEl, this.el);
        }
        this.hasBeenDragged = false;
    },
    
    // For some browsers (IE and Safari), the currentStyle/computedStyle 
    // for top/left will be "auto" when bottom and right values are set.
    _setIeTopLeft : function() { 
        // For IE, set top/left values when declared values are auto
        // and right/bottom values are given.

        var dv = document.defaultView,
            el = this.el,
            s = el.style,
            cs = el.currentStyle || 
            (dv.getComputedStyle && dv.getComputedStyle(el,"")) || s,
            cb = APE.dom.getContainingBlock(el),
            curL = cs.left, 
            curR = cs.right, 
            curT = cs.top,
            curB = cs.bottom;        
        // Calculate left when right is given pixel value and left is "auto".
        if((curL == "" || curL == "auto")) {
            curR = parseInt(curR);
            if(isFinite(curR))
                s.left = cb.clientWidth - el.offsetWidth - curR + "px";
            else s.left = "0";
        }

        // Calculate top when bottom is given pixel value and top is "auto".
        if((curT == "" || curT == "auto")) {
            curB = parseInt(curB);
            if(isFinite(curB)) {
                s.top = cb.clientHeight - el.offsetHeight - curB + "px";
            }
            else s.top = "0";
        }
    },

    toString : function() { return "APE.drag.Draggable(id=" +this.id +")"; }
};

/** @type {Number} 
 * @description a higher z-index is assigned beforedragstart. */
APE.drag.Draggable.highestZIndex = 1000;

/** @type {Object} 
 * @description Internal map of draggables */
APE.drag.Draggable.draggableList = { };

/** 
 * called before dragstart, this function checks to see if there are any droptargets 
 * that need mousemove consideration. For example, if the droptarget has a
 * dragOverClassName, or has an ondragover handler.
 * @private
 */
APE.drag.Draggable._setUpDragOver = function(dO) {
    // subset for ondragover, to help speed up dragging 
    // with multiple drop targets.
    dO._dragOverTargets = [];
    var dropTargets = dO.dropTargets,
        dt, i = 0, len = dropTargets.length;
    
    for(; i < len; i++) {
        dt = dropTargets[i];
        dt.initCoords();
        if(typeof dt.ondragover == "function" || typeof dt.ondragout == "function" || dt.dragOverClassName) 
            dO._dragOverTargets.push(dt);
    }
    // set to false, for quicker access on drag over.
    if(dO._dragOverTargets.length === 0) 
        dO._dragOverTargets = false;
};

/** APE.drag.DropTarget
 *
 * @param{HTMLElement} el 
 * private constructor - use draggable.addDropTarget(el);
 * @private 
 */
APE.drag.DropTarget = function(el) {
    this.el = el;
    this.id = el.id;
};

APE.drag.DropTarget.getByNode = APE.getByNode;

APE.drag.DropTarget.prototype = {
    
    /** @type {Object} 
     * @description {x,y} coords of DropTarget */
    coords : undefined,

    /** @type {String} 
     * @description the className to add when selected. */
    dragOverClassName : "",

    initCoords : function() {
        if(!this.coords) this.coords = {};
        APE.dom.getOffsetCoords(this.el, document, this.coords);
        this.coords.w = this.el.clientWidth;
        this.coords.h = this.el.clientHeight;
    },

    /**  checks to see if the coordinates 
     *  x and y are both inside dropTarget
     * @param {Object} curs {x,y} coordinates of the event.
     */
    containsCoords : function(curs) {
         // check for x, then y.
        var dt_x = this.coords.x, dt_y = this.coords.y;
        
        return (curs.x >= dt_x && curs.x <= dt_x + this.coords.w)
            && // now check for y.
            (curs.y >= dt_y && curs.y <= dt_y + this.coords.h);
    },

    /**
     * @event
     * Dragged over a droptarget */
    ondragover : false, 
    /**
     * @event
     * Dragged off a droptarget */
    ondragout : undefined,

    /**
     * @event
     * Hit a drop target. Fires for each object being dragged. */
    ondrop : undefined
};

/** DragHandlers 
 * 
 * @memberOf APE.drag
 *
 * methods: 
 * mouseDown - initializes dragging
 *
 * mouseMove - tracks the mouse position and updates dO
 *
 * mouseUp   - releases any dO and calls ondragend,
 *             passing the event. The event has a dropTarget 
 *             property, which may be null.
 * 
 */
APE.drag.DragHandlers = {
    
    /**@type {HTMLElement}
     * @description the element being actively dragged.
     */
    dO : null,
        
    /**@type {HTMLElement}
     * @description the element that has focus.
     */
    focusedDO : null,
        
    /** 
     * @function
     */
    getEventCoords : APE.dom.Event.getCoords,

    /** Initializes event handlers for dragging.
     * this is called once when the first dragObj is created.
     */
    init : function() {
        if(this.inited) return;
        
        var d = document,
            docEl = d.documentElement,
            ds = docEl.style,
            EventPublisher = APE.EventPublisher;

        EventPublisher.add(d, "onmousedown", this.mouseDown, this);
        EventPublisher.add(d, "onkeypress", this.keyPressed, this);
        EventPublisher.add(d, "onmousemove", this.mouseMove, this);
        EventPublisher.add(d, "onmouseup", this.mouseUp, this);

        // prevent text selection while dragging.
        if('onselectstart' in d) {
            EventPublisher.get(d, "onselectstart").addBefore(this.isInDrag, this);
        }
        else {
            EventPublisher.get(d, "onmousedown").addAfter(this.preventUserSelection, this);
            EventPublisher.get(d, "onmouseup").addAfter(this.preventUserSelection, this);
        }
        this.inited = true;

        this.userSelectType = "MozUserSelect"in ds ? "MozUserSelect" : 
            "KhtmlUserSelect"in ds ? "KhtmlUserSelect" : "userSelect"in ds ? "userSelect" : "";
    },
    
    /** 
     * @return true, if there is a draggable object.
     * we could have a focusedDO that has been released. In that case, it may still be
     * isBeingDragged = true; isBeingDragged is set to false in dragDone. This occurs after glideEnd.
     */
    isInDrag : function() { return !this.dO && !this.focusedDO; },
    
    /** 
     * Prevents selection while user is dragging.
     */
    preventUserSelection : function() {
        document.documentElement.style[this.userSelectType] = this.dO ? "none" : "";
    },
    
    /** 
     * Called from grab() and from mousemove, when first started.
     */
    dragObjGrabbed : function(e, dO) {
        if(typeof dO.onbeforedragstart == "function" && dO.onbeforedragstart(e) == false) return true;

        var DragHandlers = APE.drag.DragHandlers, dom = APE.dom,
            eventCoords = dom.Event.getCoords(e),
            elementPixelCoords;;

        DragHandlers.locked = true;
        
        DragHandlers.mousedownX = eventCoords.x;
        DragHandlers.mousedownY = eventCoords.y;
        
        elementPixelCoords = dom.getPixelCoords(dO.el);

        dO.origX = dO.grabX = elementPixelCoords.x;
        dO.origY = dO.grabY = elementPixelCoords.y;

        dO.isBeingDragged = false;
    },
    
    /** 
     * Called from dragStart. Sets initial x/y position values.
     */
    setUpCoords : function(e, dO) {
        
        var dom = APE.dom,
            container = dO.container,
            el = dO.el,
            cb = dom.getContainingBlock(el),
            coords = dom.getOffsetCoords(cb, container),
        // subtract in-flow offsets.
            pixelCoords = dom.getPixelCoords(el),

        // Due to the AVK-CSSOM Mess, offsetTop/offsetLeft are broken - DO NOT USE offset*!
        // Instead, use getOffsetCoords(el, el.parentNode);
            offsetFromParent = dom.getOffsetCoords(el, el.parentNode),
            inFlowOffsetX = offsetFromParent.x - pixelCoords.x + coords.x,
            inFlowOffsetY = offsetFromParent.y - pixelCoords.y + coords.y; 


        // Safari Bug: if el is inside a TD, safari adds the TD's offsetLeft to the 
        // el's offsetLeft, even if the TD has position: relative.

        // Impl Note: Don't use margins for absolutely positioned elements for Safari.
        // Safari calculates offsetTop from parentNode border edge (not padding edge).

          // Safari 1.3 can't read style values from styleSheets.
          // Safari 1.3 also adds parentNode border-width to offsetLeft. 
        // Safari 3 does not. TODO: test Safari 2.
        
        // Safari 1.3 adds padding-left and top to inFlowOffsets, Safari 3 does not.
        // Safari 1 can't read styles. TODO: test Safari 2.
        
        if(dO.keepInContainer) {
            dO.minX = 0 - inFlowOffsetX;
            dO.maxX = container.clientWidth - dO.el.offsetWidth - inFlowOffsetX;
            dO.minY = 0 - inFlowOffsetY;
            dO.maxY = container.clientHeight - dO.el.offsetHeight - inFlowOffsetY;
        }
    },

    /** 
     * Called on mousedown. 
     */
    mouseDown : function(e) { 

    // TODO: This is too complicated. Focus/selection. DragMultiple. draggableList. 
    // Need a way to encapsulate those complexities.
        if(!e) 
            e = event;
            
        var dom = APE.dom,
            target = dom.Event.getTarget(e),
            dO = null,
            Draggable = APE.drag.Draggable,
            instances = Draggable.instances,
            testNode = target;

        for(;dO == null && testNode; testNode = dom.findAncestorWithAttribute(testNode, "id"))
            dO = instances[testNode.id];

        var metaKey = e.metaKey || (/Win/.test(navigator.platform) && e.ctrlKey);

        if(dO) { // found. 
        
            if(!dO.isDragEnabled) {
                if(!metaKey) {
                    this.removeGroupSelection();
                    if(this.focusedDO) {
                        // if we return false (we do, to preven focus), we must explicitly
                        // call blur() for Firefox (this seems like a browser bug).
                        // this.focusedDO.el.blur();
                    }
                }
                return false; // prevent focus.
            }
            
            // If it's got a handle, make sure user clicked the handle.
            if(!metaKey && dO.hasHandleSet && !dO.isInHandle( target ) ) {

                if(!this.locked) {
                    this.removeGroupSelection();
                    this.dO = null;
                    this.locked = false;
                 }
                return;
            }
            
            else {
                if(!metaKey && !dO.isSelected) { // no metaKey, 
                    this.removeGroupSelection();
                }
             e.returnValue = false;
            }
            // In Mozilla; the intrisinc focus event will not fire when the 
            // mousedown calls preventDefault(). This is a bug in Mozilla.
             //else if(typeof e.preventDefault == "function") e.preventDefault();
         }
         else {
            if(!this.locked) {
                if(!metaKey) {
                    this.removeGroupSelection();
                    if(this.focusedDO) {
                        this.focusedDO.setFocus(false, e);
                    }
                }
                this.dO = null;
                this.locked = false;
             }
            return;

        }

        // User tried to add to selection, but can't. Just return.
        if(metaKey && this.hasGroupSelection() && !dO.dragMultiple) return false; 
        
        // amend for Mozilla not gaining/losing focus from mouseDown return false.
        dO._fixFocus(e);
 
        if(!dO.dragMultiple) {
            if(!metaKey)
                this.removeGroupSelection();

            // User tried to add to group. Just exit.
            else return false;
        }
        
        this.setGroupSelection(dO, metaKey);
        
        dO.style.zIndex = ++Draggable.highestZIndex;

        // User tried to drag a group and still had metaKey down.
        if(metaKey && dO.hasFocus) {
            ;
        }
        
        // Sets up dropTargets that have dragOverClassName | ondragover 
        Draggable._setUpDragOver(dO);

        this.dragObjGrabbed(e, dO);
       
        for(var id in Draggable.draggableList) {
            this.dragObjGrabbed(e, Draggable.draggableList[id]);
        }
        this.dO = dO;
        return target.tagName != "IMG"; // Mozilla will prevent focus events for return false;
    },
    
    setGroupSelection : function(dO, hasMetaKey) {
        
        var draggableList = APE.drag.Draggable.draggableList;

        if(hasMetaKey) {
            if(dO.id in draggableList) { // selected.
                this.deselectOnMouseup = true;
            }
            else { // not selected.
                dO.select(true);
            }
        }
        else if(!dO.isSelected) { // if not selected, deselect others.
            this.removeGroupSelection();
            dO.select(true);
        }
    },


    /**
     * When a draggable has been released (by ESC), it calls dragout from the relevant 
     * droptargets and resets any active over droptargets.
     * @param {Event} e the event that triggered the release. This gets passed back to ondragout.
     * @param {Draggable} the draggable object that was released.
     */
     dragObjReleased : function(e, dO) {
        dO.animateBack();

        var APE = window.APE, removeClass = APE.dom.removeClass,
            draggableList = APE.drag.Draggable.draggableList,
            dt, i = 0, j = dO._dragOverTargets.length, id;

        if(dO._dragOverTargets !== false) {
            for(; i < j; i++) {
                dt = dO._dragOverTargets[i];

                // Did we just move off dO dropTarget?
                if(dt.hasDropTargetOver) {
                    if(typeof dt.ondragout == "function")
                        dt.ondragout(e, dO);
                    if(dt.dragOverClassName)
                        removeClass(dt.el, dt.dragOverClassName);
                    dt.hasDropTargetOver = false;
                }
            }
        }

        for(id in draggableList) {
            draggableList[id].animateBack();
        }
        
        this.dO = null;
    },
    
    /** 
     * called on mousemove 
     */
    carryGroup : function(distX, distY) {
        var draggableList = APE.drag.Draggable.draggableList, o, id;
        for(id in draggableList) {
            o = draggableList[id];
            if(distX != null)
                o.moveToX( o.origX + distX );
            if(distY != null)
                o.moveToY( o.origY + distY );
        }
    },

    removeGroupSelection : function() {
        var id, drag = APE.drag, draggableList = drag.Draggable.draggableList;
        for(id in draggableList) {
            draggableList[id].select(false);
        }

        if(drag.DragHandlers.focusedDO) {
            drag.DragHandlers.focusedDO.select(false);
        }
    },

    /** 
     * returns true if there are any selected items.
     */
    hasGroupSelection : function() {
        for(var id in APE.drag.Draggable.draggableList)
            return true;
        return false;
    },
   
    /**
     * mousemove callback handler.
     */
    mouseMove : function(e) {

        var dO = this.dO;
        
        if(dO == null) return;
        
        if(!e)
            e = event;

        var eventCoords = this.getEventCoords(e),
            ePageX = eventCoords.x, ePageY = eventCoords.y,
            distX = ePageX - this.mousedownX,
            distY = ePageY - this.mousedownY;
        
        // drag the bitch.
        if(dO.isBeingDragged == false) {
            dO.dragStart(e);
            
            var id, draggableList = APE.drag.Draggable.draggableList;
            for(id in draggableList) {
                draggableList[id].dragStart(e);
         
            }
        }
        dO.newX = dO.origX + distX;
        dO.newY = dO.origY + distY;

        dO.hasBeenDragged = (dO.hasBeenDragged || (distX || distY));
        
        var constraints = APE.drag.Draggable.constraints,
            isLeft = dO.newX < dO.minX,
            isRight = dO.newX > dO.maxX,
            isAbove = dO.newY < dO.minY,
            isBelow = dO.newY > dO.maxY;

        if(typeof dO.onbeforedrag == "function" && dO.onbeforedrag(e) == false) return;
        
            var isOutsideContainer = dO.container != null,
            hasOnDrag = (typeof dO.ondrag == "function"),
            isBeforeExitContainerFunction = typeof dO.onbeforeexitcontainer == "function",
            planesStopped = 0;

        if(dO.constraint === constraints.NONE) { // no constraint. Life is hard.
            
            isOutsideContainer &= ( isLeft || isRight || isAbove || isBelow );
            
            if(isOutsideContainer && (isBeforeExitContainerFunction || 
                                    dO.onbeforeexitcontainer() == false)) {
                if(isLeft) {
                    if(!dO.isAtLeft) {
                        dO.moveToX( dO.minX );
                        // dO.minX - dO.origX = max possible negative distance to travel.
                        this.carryGroup(dO.minX - dO.origX, null);
                        if(hasOnDrag)
                            dO.ondrag(e);
                        dO.isAtRight = false;
                        dO.isAtLeft = true;
                        planesStopped += 1;
                    }
                }
                else if(isRight) {
                    if(!dO.isAtRight) {
                        dO.moveToX( dO.maxX );
                        // dO.maxX - dO.origX = max possible positive distance to travel.
                        this.carryGroup(dO.maxX - dO.origX, null);
                        if(hasOnDrag)
                            dO.ondrag(e);
                        dO.isAtRight = true;
                        dO.isAtLeft = false;
                        planesStopped += 1;
                    }
                }
                else {
                    dO.isAtLeft = dO.isAtRight = false;
                    dO.moveToX( dO.newX );

                    this.carryGroup(distX, null);
                }
                if(isAbove) {
                    if(!dO.isAtTop) {
                        dO.moveToY( dO.minY );
                        // dO.minY - dO.origY = max possible positive distance to travel.
                        this.carryGroup(null, dO.minY - dO.origY);
                        if(hasOnDrag)
                            dO.ondrag(e);
                        dO.isAtTop = true;
                        dO.isAtBottom = false;
                        planesStopped += 1;
                    }
                }
                else if(isBelow) {
                    if(!dO.isAtBottom) {
                        if( dO.maxY > 0 )
                            dO.moveToY( dO.maxY );
                        // dO.maxY - dO.origY = max possible positive distance to travel.
                        this.carryGroup(null, dO.maxY - dO.origY);
                        if(hasOnDrag)
                            dO.ondrag(e);
                        dO.isAtTop = false;
                        dO.isAtBottom = true;
                        planesStopped += 1;
                    }
                }
                else {
                    dO.isAtTop = dO.isAtBottom = false;
                    dO.moveToY( dO.newY );
                    this.carryGroup(null, distY);
                }
                
                dO.isDragStopped = planesStopped == 2;
                
                if(dO.isDragStopped && typeof dO.ondragstop == "function")
                    dO.ondragstop(e);
                else 
                    if(hasOnDrag)
                        dO.ondrag(e);
            }
            else {            // In container.
                dO.isDragStopped = dO.isAtLeft = dO.isAtRight =
                    dO.isAtTop = dO.isAtBottom = false;
                dO.moveToXY( dO.newX, dO.newY );
                this.carryGroup(distX, distY);
                if(hasOnDrag)
                    dO.ondrag(e);
            }
        }
        
        else {  // A constraint. 
            // A VERT type constraint? 
            if(dO.constraint % 2 == 0) {
                                
                isOutsideContainer &= (isAbove || isBelow);
                if(isOutsideContainer && (isBeforeExitContainerFunction || dO.onbeforeexitcontainer() == false)) {
                    if(isAbove) {
                        if(!dO.isAtTop) {
                            dO.moveToY( dO.minY );
                            // dO.minY - dO.origY = max possible positive distance to travel.
                            this.carryGroup(null, dO.minY - dO.origY);
                            if(hasOnDrag)
                                dO.ondrag(e);
                            dO.isAtTop = !(dO.isAtBottom = false);
                        }
                    }
                    else if(isBelow) {
                        if(!dO.isAtBottom) {
                            dO.moveToY( dO.maxY );
                            // dO.maxY - dO.origY = max possible positive distance to travel.
                            this.carryGroup(null, dO.maxY - dO.origY);
                            if(hasOnDrag)
                                dO.ondrag(e);
                            dO.isAtBottom = !(dO.isAtTop = false);
                        }
                    }

                    if(!dO.isDragStopped) {
                        if(typeof dO.ondragstop == "function")
                            dO.ondragstop(e);
                        dO.isDragStopped = true;
                    }
                }
                else { // in container.

                    dO.isAtTop = dO.isAtBottom = false;
                    dO.isDragStopped = false;
                    dO.moveToY( dO.newY );
                    this.carryGroup(null, distY);
                    if(hasOnDrag)
                        dO.ondrag(e);
                }
            }
            
            // A HORZ type constraint? 
            else {
              
                isOutsideContainer &= (isLeft || isRight);
                
                if(isOutsideContainer && (isBeforeExitContainerFunction || dO.onbeforeexitcontainer() == false)) {
                    if(isLeft) {  
                        if(!dO.isAtLeft) {
                            dO.moveToX( dO.minX );
                            // dO.minX - dO.origX = max possible negative distance to travel.
                            this.carryGroup(dO.minX - dO.origX, null);
                            dO.isAtLeft = !(dO.isAtRight = false);
                        }
                    }
                    else if(isRight) {
                        if(!dO.isAtRight) {
                            // dO.maxX - dO.origX = max possible negative distance to travel.
                            this.carryGroup(dO.maxX - dO.origX, null);
                            dO.moveToX( dO.maxX );
                            dO.isAtRight = !(dO.isAtLeft = false);
                        }
                    }
                    
                    
                    if(!dO.isDragStopped) {
                        if(typeof dO.ondragstop == "function")
                            dO.ondragstop(e);
                        dO.isDragStopped = true;
                    }
                 }
                else {            // In container.
                    dO.isAtLeft = dO.isAtRight = false;
                    dO.isDragStopped = false;
                    dO.moveToX( dO.newX );
                    this.carryGroup(distX, null);
                    if(hasOnDrag)
                        dO.ondrag(e);
                }
                
            }
        }
        
        // Handle dropTarget dragOver
        var _dragOverTargets = dO._dragOverTargets;

        if(_dragOverTargets !== false)  {
            var coords = { x:ePageX, y:ePageY },
                i = 0, 
                j = _dragOverTargets.length,
                dt,
                dom = APE.dom,
                dragEvent = {domEvent:e, dragObj:dO};
            
           for(; i < j; i++) {
                dt = _dragOverTargets[i],
                    isInTarget = dt.containsCoords(coords);
                // Did we just move over this dropTarget?

                if(!dt.hasDropTargetOver && isInTarget) {
                    dt.hasDropTargetOver = true;

                   if(typeof dt.ondragover == "function")
                        dt.ondragover(dragEvent); // typeof check now needed.
                    if(dt.dragOverClassName)
                        dom.addClass(dt.el, dt.dragOverClassName);
                }
                else { // Were we previously over this dropTarget?
                    if(dt.hasDropTargetOver && !isInTarget) { 
                        if(typeof dt.ondragout == "function")
                            dt.ondragout(dragEvent);
                        if(dt.dragOverClassName)
                            dom.removeClass(dt.el, dt.dragOverClassName);
                        dt.hasDropTargetOver = false;
                    }
                }
            }
        }
        return false;
    },
    
    /** 
     * mouseup callback handler.
     */
    mouseUp : function(e) {
    // IE will fire this event twice when mouse was held.
        
        // IE fires mousemoves randomly, usually when the mouse is held.
        // For this case, just let dragDone run it's course and fire it's event. 
        var isRandomMouseMoveEvent = (this.dO && this.dO.isBeingDragged && !this.dO.hasBeenDragged);

        if(this.dO == null || !this.dO.hasBeenDragged && !isRandomMouseMoveEvent) {
            if(this.dO && this.deselectOnMouseup && !this.dO.hasBeenDragged) {
                this.dO.select(false);
            }
            this.deselectOnMouseup = false;
            this.dO = null;
            this.locked = false;
            return;
        }

        if(!e)
            e = event;
        
        var dO = this.dO,
            draggableList = APE.drag.Draggable.draggableList,
            id, item;
        if(dO.copyEl) 
            dO.retireClone();
        for(id in draggableList) {
            item = draggableList[id];
            if(item.copyEl) 
                item.retireClone();
        }
        
        // if it's been dragged onto a dropTarget, fire that event.
        var targets = dO.dropTargets,
            len = targets.length, o, x, y;
        if(len > 0) {
            var coords = this.getEventCoords(e),
                dropTarget, i = 0;
            for(; i < len; i++) {
                dropTarget = targets[i];

                if(dropTarget.containsCoords(coords)) {
                    dropTarget.containsCoords(coords);

                    if(typeof dropTarget.ondrop == "function")
                        dropTarget.ondrop({domEvent:e, dragObj:dO, dropTarget:dropTarget});
                        
                    for(id in draggableList)  { // Assume that draggable groups share dropTargets.
                        if(id === dropTarget.id) continue;
                        if(typeof dropTarget.ondrop == "function") {
                            o = draggableList[id];
                            dropTarget.ondrop({domEvent:e, dragObj:o, dropTarget:dropTarget}); 
                        }
                    }
                    if(dropTarget.dragOverClassName)
                        APE.dom.removeClass(dropTarget.el, dropTarget.dragOverClassName);
                    break;
                }
            }
        }
        for(id in draggableList) {
            o = draggableList[id], 
                x = o.x, y = o.y;
            if(x < o.minX)
                o.moveToX(o.minX);
            else if(x > o.maxX)
                o.moveToX(o.maxX);
            if(y < o.minY)
                o.moveToY(o.minY);
            else if(y > o.maxY)
                o.moveToY(o.maxY);
            if(o.hasBeenDragged)
                o.dragDone(e);
        }
        if(dO.hasBeenDragged)
            dO.dragDone(e);
        this.locked = false;
        this.dO = null;
    },
    
    /** 
     * Key event callback handler. 
     * When ESC key is pressed, 
     * draggables are released.
     */
    keyPressed : function(e) {
        e=e||event;
        if(e.keyCode == 27) { // esc key.
            if(this.dO) {
                this.dO.release(e);
            }
        }
    },
    
    toString : function() { return"[object DragHandlers]"; }
};

if(typeof window.CollectGarbage == "function") (function(){
    // After onunload, add instanceDestructor.
    // Also add instanceDestructor for DropTarget instances.
    var drag = APE.drag, 
        Draggable = drag.Draggable, 
        instanceDestructor = Draggable.instanceDestructor;

    APE.EventPublisher.get(window, "onunload").addAfter(instanceDestructor, Draggable).
      add(instanceDestructor, drag.DropTarget);
})();