DragDropMgr.js

Souce Code [top]

/* Copyright (c) 2006 Yahoo! Inc. All rights reserved. */

// Only load the library once.  Rewriting the manager class would orphan 
// existing drag and drop instances.
if (!YAHOO.util.DragDropMgr) {

    /**
     * Handles the element interaction for all DragDrop items in the 
     * window.  Generally, you will not call this class directly, but it does
     * have helper methods that could be useful in your DragDrop 
     * implementations.  This class should not be instantiated; all methods 
     * are are static.
     *
     * @constructor
     */
    YAHOO.util.DragDropMgr = new function() {
        /**
         * Flag to determine if we should prevent the default behavior of the
         * events we define. By default this is true, but this can be set to 
         * false if you need the default behavior (not recommended)
         *
         * @type boolean
         */
        this.preventDefault = true;

        /**
         * Flag to determine if we should stop the propagation of the events 
         * we generate. This is true by default but you may want to set it to
         * false if the html element contains other features that require the
         * mouse click.
         *
         * @type boolean
         */
        this.stopPropagation = true;

        /**
         * In point mode, drag and drop interaction is defined by the 
         * location of the cursor during the drag/drop
         * @type int
         */
        this.POINT     = 0;

        /**
         * In intersect mode, drag and drop interactio nis defined by the 
         * overlap of two or more drag and drop objects.
         * @type int
         */
        this.INTERSECT = 1;

        /**
         * The current drag and drop mode.  Default it point mode
         * @type int
         */
        this.mode = this.POINT;

        /**
         * Lock all drag and drop functionality
         */
        this.lock = function() { this.locked = true; };

        /**
         * Unlock all drag and drop functionality
         */
        this.unlock = function() { this.locked = false; };

        /**
         * Is drag and drop locked?
         *
         * @type boolean
         */
        this.locked = false;

        /**
         * Is drag and drop locked?
         *
         * @return {boolean} True if drag and drop is locked, false otherwise.
         */
        this.isLocked = function() { return this.locked; };
        /**
         * Set useCache to false if you want to force object the lookup of each
         * drag and drop linked element constantly during a drag.
         * @type boolean
         */
        this.useCache = true;

        /**
         * The number of pixels that the mouse needs to move after the 
         * mousedown before the drag is initiated.  Default=3;
         * @type int
         */
        this.clickPixelThresh = 3;

        /**
         * The number of milliseconds after the mousedown event to initiate the
         * drag if we don't get a mouseup event. Default=1000
         * @type int
         */
        this.clickTimeThresh = 1000;
        
       /**
         * Each DragDrop instance must be registered with the DragDropMgr.  
         * This is executed in ygDragDrop.init()
         *
         * @param {DragDrop} oDD the DragDrop object to register
         * @param {String} sGroup the name of the group this element belongs to
         */
        this.regDragDrop = function(oDD, sGroup) {
            if (!this.initialized) { this.init(); }
            
            if (!this.ids[sGroup]) {
                this.ids[sGroup] = {};
            }
            this.ids[sGroup][oDD.id] = oDD;
        };

        

        /**
         * Each DragDrop handle element must be registered.  This is done
         * automatically when executing ygDragDrop.setHandleElId()
         *
         * @param {String} sDDId the DragDrop id this element is a handle for
         * @param {String} sHandleId the id of the element that is the drag 
         * handle
         */
        this.regHandle = function(sDDId, sHandleId) {
            if (!this.handleIds[sDDId]) {
                this.handleIds[sDDId] = {};
            }
            this.handleIds[sDDId][sHandleId] = sHandleId;
        };

        /**
         * Utility function to determine if a given element has been 
         * registered as a drag drop item.
         *
         * @param {String} id the element id to check
         * @return {boolean} true if this element is a DragDrop item, 
         * false otherwise
         */
        this.isDragDrop = function(id) {
            return ( this.getDDById(id) ) ? true : false;
        };

        /**
         * Returns the drag and drop instances that are in all groups the
         * passed in instance belongs to.
         *
         * @param {ygDragDrop} p_oDD the obj to get related data for
         * @param {boolean} bTargetsOnly if true, only return targetable objs
         * @return {ygDragDrop[]} the related instances
         */
        this.getRelated = function(p_oDD, bTargetsOnly) {
            var oDDs = [];
            for (var i in p_oDD.groups) {
                for (j in this.ids[i]) {
                    var dd = this.ids[i][j];
                    if (! this.isTypeOfDD(dd)) {
                        continue;
                    }
                    if (!bTargetsOnly || dd.isTarget) {
                        oDDs[oDDs.length] = dd;
                    }
                }
            }

            return oDDs;
        };

        /**
         * Returns true if the specified dd target is a legal target for 
         * the specifice drag obj
         *
         * @param {ygDragDrop} the drag obj
         * @param {ygDragDrop) the target
         * @return {boolean} true if the target is a legal target for the 
         * dd obj
         */
        this.isLegalTarget = function (oDD, oTargetDD) {
            var targets = this.getRelated(oDD);
            for (var i=0, len=targets.length;i<len;++i) {
                if (targets[i].id == oTargetDD.id) {
                    return true;
                }
            }

            return false;
        };

        /**
         * My goal is to be able to transparently determine if an object is
         * typeof ygDragDrop, and the exact subclass of ygDragDrop.  typeof 
         * returns "object", oDD.constructor.toString() always returns
         * "ygDragDrop" and not the name of the subclass.  So for now it just
         * evaluates a well-known variable in ygDragDrop.
         *
         * @param {Object} the object to evaluate
         * @return {boolean} true if typeof oDD = ygDragDrop
         */
        this.isTypeOfDD = function (oDD) {
            return (oDD && oDD.__ygDragDrop);
        };

        /**
         * Utility function to determine if a given element has been 
         * registered as a drag drop handle for the given Drag Drop object.
         *
         * @param {String} id the element id to check
         * @return {boolean} true if this element is a DragDrop handle, false 
         * otherwise
         */
        this.isHandle = function(sDDId, sHandleId) {
            return ( this.handleIds[sDDId] && 
                            this.handleIds[sDDId][sHandleId] );
        };

        /**
         * Returns the DragDrop instance for a given id
         *
         * @param {String} id the id of the DragDrop object
         * @return {DragDrop} the drag drop object, null if it is not found
         */
        this.getDDById = function(id) {
            for (var i in this.ids) {
                if (this.ids[i][id]) {
                    return this.ids[i][id];
                }
            }
            return null;
        };

        
        /**
         * Fired when either the drag pixel threshol or the mousedown hold 
         * time threshold has been met.
         * 
         * @param x {int} the X position of the original mousedown
         * @param y {int} the Y position of the original mousedown
         */
        this.startDrag = function(x, y) {
            this.logger.debug("firing drag start events");
            clearTimeout(this.clickTimeout);
            if (this.dragCurrent) {
                this.dragCurrent.b4StartDrag(x, y);
                this.dragCurrent.startDrag(x, y);
            }

            /**
             * @private
             */
            this.dragThreshMet = true;
        };

        

        /**
         * Utility to stop event propagation and event default, if these 
         * features are turned on.
         *
         * @param {Event} e the event as returned by this.getEvent()
         */
        this.stopEvent = function(e) {
            if (this.stopPropagation) {
                YAHOO.util.Event.stopPropagation(e);
            }

            if (this.preventDefault) {
                YAHOO.util.Event.preventDefault(e);
            }
        };

        


        /**
         * Helper function for getting the best match from the list of drag 
         * and drop objects returned by the drag and drop events when we are 
         * in INTERSECT mode.  It returns either the first object that the 
         * cursor is over, or the object that has the greatest overlap with 
         * the dragged element.
         *
         * @param  {ygDragDrop[]} dds The array of drag and drop objects 
         * targeted
         * @return {ygDragDrop}       The best single match
         */
        this.getBestMatch = function(dds) {
            var winner = null;
            // Return null if the input is not what we expect
            //if (!dds || !dds.length || dds.length == 0) {
               // winner = null;
            // If there is only one item, it wins
            //} else if (dds.length == 1) {

            var len = dds.length;

            if (len == 1) {
                winner = dds[0];
            } else {
                // Loop through the targeted items
                for (var i=0; i<len; ++i) {
                    var dd = dds[i];
                    // If the cursor is over the object, it wins.  If the 
                    // cursor is over multiple matches, the first one we come
                    // to wins.
                    if (dd.cursorIsOver) {
                        winner = dd;
                        break;
                    // Otherwise the object with the most overlap wins
                    } else {
                        if (!winner || 
                            winner.overlap.getArea() < dd.overlap.getArea()) {
                            winner = dd;
                        }
                    }
                }
            }

            return winner;
        };

        /**
         * Refreshes the cache of the top-left and bottom-right points of the 
         * drag and drop objects in the specified groups
         *
         * @param {Array} aGroups an associative array of groups to refresh
         */
        this.refreshCache = function(aGroups) {
            this.logger.debug("refreshing element location cache");
            for (sGroup in aGroups) {
                if ("string" != typeof sGroup) {
                    continue;
                }
                for (i in this.ids[sGroup]) {
                    var oDD = this.ids[sGroup][i];

                    if (this.isTypeOfDD(oDD)) {
                        var loc = this.getLocation(oDD);
                        if (loc) {
                            this.locationCache[oDD.id] = loc;
                        } else {
                            delete this.locationCache[oDD.id];
                            this.logger.debug("something is wrong with the element");
                            // this will unregister the drag and drop object if
                            // the element is not in a usable state
                            oDD.unreg();
                        }
                    }
                }
            }
        };

        /**
         * This checks to make sure an element exists and is in the DOM.  The
         * main purpose is to handle cases where innerHTML is used to remove
         * drag and drop objects from the DOM.  IE provides an 'unspecified
         * error' when trying to access the offsetParent of such an element
         * @param {HTMLElement} el the element to check
         * @return {boolean} true if the element looks usable
         */
        this.verifyEl = function(el) {
            try {
                if (el) {
                    var parent = el.offsetParent;
                    if (parent) {
                        return true;
                    }
                }
            } catch(e) {
                this.logger.debug("detected problem with an element");
            }

            return false;
        };
        
        /**
         * Returns the an array containing the drag and drop element's position
         * and size, including the ygDragDrop.padding configured for it
         *
         * @param {ygDragDrop} oDD the drag and drop object to get the 
         * location for
         * @return array containing the top left and bottom right points of the 
         * element 
         */
        this.getLocation = function(oDD) {
            if (! this.isTypeOfDD(oDD)) {
                this.logger.debug(oDD + " is not a DD obj");
                return null;
            }

            var el = oDD.getEl();

            if (!this.verifyEl(el)) {
                this.logger.debug(oDD + " element is not usable");
                return null;
            }

            // this.logger.debug(oDD.id + " padding: " + oDD.padding);

            // var aPos = ygPos.getPos(el);
            var aPos = YAHOO.util.Dom.getXY(el);

            x1 = aPos[0];
            x2 = x1 + el.offsetWidth;

            y1 = aPos[1];
            y2 = y1 + el.offsetHeight;

            var t = y1 - oDD.padding[0];
            var r = x2 + oDD.padding[1];
            var b = y2 + oDD.padding[2];
            var l = x1 - oDD.padding[3];

            return new YAHOO.util.Region( t, r, b, l );

        };

        

        /**
         * Returns the actual DOM element
         *
         * @param {String} id the id of the elment to get
         * @return {Object} The element
         */
        this.getElement = function(id) {
            // return this.getElWrapper(id).el;
            return document.getElementById(id);
        };
        
        /**
         * Returns the style property for the DOM element (i.e., 
         * document.getElById(id).style)
         *
         * @param {String} id the id of the elment to get
         * @return {Object} The style property of the element
         */
        this.getCss = function(id) {
            // return this.getElWrapper(id).css;
            var css = null;
            var el = document.getElementById(id);
            if (el) {
                css = el.style;
            }

            return css;
        };

        /**
         * Returns the X position of an html element
         * @param el the element for which to get the position
         * @return {int} the X coordinate
         */
        this.getPosX = function(el) {
            return YAHOO.util.Dom.getX(el);
        };

        /**
         * Returns the Y position of an html element
         * @param el the element for which to get the position
         * @return {int} the Y coordinate
         */
        this.getPosY = function(el) {
            return YAHOO.util.Dom.getY(el); 
        };

        /**
         * Swap two nodes.  In IE, we use the native method, for others we 
         * emulate the IE behavior
         *
         * @param n1 the first node to swap
         * @param n2 the other node to swap
         */
        this.swapNode = function(n1, n2) {
            if (n1.swapNode) {
                n1.swapNode(n2);
            } else {
                // the node reference order for the swap is a little tricky. 
                var p = n2.parentNode;
                var s = n2.nextSibling;
                n1.parentNode.replaceChild(n2, n1);
                p.insertBefore(n1,s);
            }
        };

        /**
         * Returns the specified element style property
         * @param {HTMLElement} el          the element
         * @param {string}      styleProp   the style property
         * @return {string}     The value of the style property
         * @deprecated, use YAHOO.util.Dom.getStyle
         */
        this.getStyle = function(el, styleProp) {
            return YAHOO.util.Dom.getStyle(el, styleProp);
        };

        /**
         * Gets the scrollTop
         *
         * @return {int} the document's scrollTop
         */
        this.getScrollTop = function () { return this.getScroll().top; };

        /**
         * Gets the scrollLeft
         *
         * @return {int} the document's scrollTop
         */
        this.getScrollLeft = function () { return this.getScroll().left; };

        this.moveToEl = function (moveEl, targetEl) {
            var aCoord = YAHOO.util.Dom.getXY(targetEl);
            this.logger.debug("moveToEl: " + aCoord);
            YAHOO.util.Dom.setXY(moveEl, aCoord);
        };

        /**
         * Gets the client height
         *
         * @return {int} client height in px
         */
        this.getClientHeight = function() {
            return (window.innerHeight) ? window.innerHeight : 
                (document.documentElement && document.documentElement.clientHeight) ?
                document.documentElement.clientHeight : document.body.offsetHeight;
        };

        /**
         * Gets the client width
         *
         * @return {int} client width in px
         */
        this.getClientWidth = function() {
            return (window.innerWidth) ? window.innerWidth : 
                (document.documentElement && document.documentElement.clientWidth) ?
                document.documentElement.clientWidth : document.body.offsetWidth;
        };

        /**
         * numeric array sort function
         */
        this.numericSort = function(a, b) { return (a - b); };

        
        /**
         * Recursively searches the immediate parent and all child nodes for 
         * the handle element in order to determine wheter or not it was 
         * clicked.
         *
         * @param node the html element to inspect
         */
        this.handleWasClicked = function(node, id) {
            if (this.isHandle(id, node.id)) {
                this.logger.debug("clicked node is a handle");
                return true;
            } else {
                // check to see if this is a text node child of the one we want
                var p = node.parentNode;
                // this.logger.debug("p: " + p);

                while (p) {
                    if (this.isHandle(id, p.id)) {
                        return true;
                    } else {
                        this.logger.debug(p.id + " is not a handle");
                        p = p.parentNode;
                    }
                }
            }

            return false;
        };

    } ();

    // shorter alias, save a few bytes
    YAHOO.util.DDM = YAHOO.util.DragDropMgr;
    YAHOO.util.DDM._addListeners();

}