# Yahoo! UI Library

### Container 2.6.0

Yahoo! UI Library > container > Panel.js (source view)

Show Private Show Protected Show Deprecated

    (function () {

        /**
        * Panel is an implementation of Overlay that behaves like an OS window,
        * with a draggable header and an optional close icon at the top right.
        * @namespace YAHOO.widget
        * @class Panel
        * @extends YAHOO.widget.Overlay
        * @constructor
        * @param {String} el The element ID representing the Panel <em>OR</em>
        * @param {HTMLElement} el The element representing the Panel
        * @param {Object} userConfig The configuration object literal containing
        * the configuration that should be set for this Panel. See configuration
        * documentation for more details.
        */
        YAHOO.widget.Panel = function (el, userConfig) {
            YAHOO.widget.Panel.superclass.constructor.call(this, el, userConfig);
        };

        var _currentModal = null;

        var Lang = YAHOO.lang,
            Util = YAHOO.util,
            Dom = Util.Dom,
            Event = Util.Event,
            CustomEvent = Util.CustomEvent,
            KeyListener = YAHOO.util.KeyListener,
            Config = Util.Config,
            Overlay = YAHOO.widget.Overlay,
            Panel = YAHOO.widget.Panel,
            UA = YAHOO.env.ua,

            bIEQuirks = (UA.ie == 6 || (UA.ie == 7 && document.compatMode == "BackCompat")),

            m_oMaskTemplate,
            m_oUnderlayTemplate,
            m_oCloseIconTemplate,

            /**
            * Constant representing the name of the Panel's events
            * @property EVENT_TYPES
            * @private
            * @final
            * @type Object
            */
            EVENT_TYPES = {
                "SHOW_MASK": "showMask",
                "HIDE_MASK": "hideMask",
                "DRAG": "drag"
            },

            /**
            * Constant representing the Panel's configuration properties
            * @property DEFAULT_CONFIG
            * @private
            * @final
            * @type Object
            */
            DEFAULT_CONFIG = {

                "CLOSE": {
                    key: "close",
                    value: true,
                    validator: Lang.isBoolean,
                    supercedes: ["visible"]
                },

                "DRAGGABLE": {
                    key: "draggable",
                    value: (Util.DD ? true : false),
                    validator: Lang.isBoolean,
                    supercedes: ["visible"]
                },

                "DRAG_ONLY" : {
                    key: "dragonly",
                    value: false,
                    validator: Lang.isBoolean,
                    supercedes: ["draggable"]
                },

                "UNDERLAY": {
                    key: "underlay",
                    value: "shadow",
                    supercedes: ["visible"]
                },

                "MODAL": {
                    key: "modal",
                    value: false,
                    validator: Lang.isBoolean,
                    supercedes: ["visible", "zindex"]
                },

                "KEY_LISTENERS": {
                    key: "keylisteners",
                    suppressEvent: true,
                    supercedes: ["visible"]
                },

                "STRINGS" : {
                    key: "strings",
                    supercedes: ["close"],
                    validator: Lang.isObject,
                    value: {
                        close: "Close"
                    }
                }
            };

        /**
        * Constant representing the default CSS class used for a Panel
        * @property YAHOO.widget.Panel.CSS_PANEL
        * @static
        * @final
        * @type String
        */
        Panel.CSS_PANEL = "yui-panel";

        /**
        * Constant representing the default CSS class used for a Panel's
        * wrapping container
        * @property YAHOO.widget.Panel.CSS_PANEL_CONTAINER
        * @static
        * @final
        * @type String
        */
        Panel.CSS_PANEL_CONTAINER = "yui-panel-container";

        /**
         * Constant representing the default set of focusable elements
         * on the pagewhich Modal Panels will prevent access to, when
         * the modal mask is displayed
         *
         * @property YAHOO.widget.Panel.FOCUSABLE
         * @static
         * @type Array
         */
        Panel.FOCUSABLE = [
            "a",
            "button",
            "select",
            "textarea",
            "input",
            "iframe"
        ];

        // Private CustomEvent listeners

        /*
            "beforeRender" event handler that creates an empty header for a Panel
            instance if its "draggable" configuration property is set to "true"
            and no header has been created.
        */

        function createHeader(p_sType, p_aArgs) {
            if (!this.header && this.cfg.getProperty("draggable")) {
                this.setHeader("&#160;");
            }
        }

        /*
            "hide" event handler that sets a Panel instance's "width"
            configuration property back to its original value before
            "setWidthToOffsetWidth" was called.
        */

        function restoreOriginalWidth(p_sType, p_aArgs, p_oObject) {

            var sOriginalWidth = p_oObject[0],
                sNewWidth = p_oObject[1],
                oConfig = this.cfg,
                sCurrentWidth = oConfig.getProperty("width");

            if (sCurrentWidth == sNewWidth) {
                oConfig.setProperty("width", sOriginalWidth);
            }

            this.unsubscribe("hide", restoreOriginalWidth, p_oObject);
        }

        /*
            "beforeShow" event handler that sets a Panel instance's "width"
            configuration property to the value of its root HTML
            elements's offsetWidth
        */

        function setWidthToOffsetWidth(p_sType, p_aArgs) {

            var nIE = YAHOO.env.ua.ie,
                oConfig,
                sOriginalWidth,
                sNewWidth;

            if (nIE == 6 || (nIE == 7 && document.compatMode == "BackCompat")) {

                oConfig = this.cfg;
                sOriginalWidth = oConfig.getProperty("width");

                if (!sOriginalWidth || sOriginalWidth == "auto") {

                    sNewWidth = (this.element.offsetWidth + "px");

                    oConfig.setProperty("width", sNewWidth);

                    this.subscribe("hide", restoreOriginalWidth,
                        [(sOriginalWidth || ""), sNewWidth]);

                }
            }
        }

        YAHOO.extend(Panel, Overlay, {

            /**
            * The Overlay initialization method, which is executed for Overlay and
            * all of its subclasses. This method is automatically called by the
            * constructor, and  sets up all DOM references for pre-existing markup,
            * and creates required markup if it is not already present.
            * @method init
            * @param {String} el The element ID representing the Overlay <em>OR</em>
            * @param {HTMLElement} el The element representing the Overlay
            * @param {Object} userConfig The configuration object literal
            * containing the configuration that should be set for this Overlay.
            * See configuration documentation for more details.
            */
            init: function (el, userConfig) {
                /*
                     Note that we don't pass the user config in here yet because
                     we only want it executed once, at the lowest subclass level
                */

                Panel.superclass.init.call(this, el/*, userConfig*/);

                this.beforeInitEvent.fire(Panel);

                Dom.addClass(this.element, Panel.CSS_PANEL);

                this.buildWrapper();

                if (userConfig) {
                    this.cfg.applyConfig(userConfig, true);
                }

                this.subscribe("showMask", this._addFocusHandlers);
                this.subscribe("hideMask", this._removeFocusHandlers);
                this.subscribe("beforeRender", createHeader);

                this.subscribe("render", function() {
                    this.setFirstLastFocusable();
                    this.subscribe("changeContent", this.setFirstLastFocusable);
                });

                this.subscribe("show", this.focusFirst);

                this.initEvent.fire(Panel);
            },

            /**
             * @method _onElementFocus
             * @private
             *
             * "focus" event handler for a focuable element. Used to automatically
             * blur the element when it receives focus to ensure that a Panel
             * instance's modality is not compromised.
             *
             * @param {Event} e The DOM event object
             */
            _onElementFocus : function(e){

                var target = Event.getTarget(e);

                if (target !== this.element && !Dom.isAncestor(this.element, target) && _currentModal == this) {
                    try {
                        if (this.firstElement) {
                            this.firstElement.focus();
                        } else {
                            if (this._modalFocus) {
                                this._modalFocus.focus();
                            } else {
                                this.innerElement.focus();
                            }
                        }
                    } catch(err){
                        // Just in case we fail to focus
                        try {
                            if (target !== document && target !== document.body && target !== window) {
                                target.blur();
                            }
                        } catch(err2) { }
                    }
                }
            },

            /**
             *  @method _addFocusHandlers
             *  @protected
             *
             *  "showMask" event handler that adds a "focus" event handler to all
             *  focusable elements in the document to enforce a Panel instance's
             *  modality from being compromised.
             *
             *  @param p_sType {String} Custom event type
             *  @param p_aArgs {Array} Custom event arguments
             */
            _addFocusHandlers: function(p_sType, p_aArgs) {
                if (!this.firstElement) {
                    if (UA.webkit || UA.opera) {
                        if (!this._modalFocus) {
                            this._createHiddenFocusElement();
                        }
                    } else {
                        this.innerElement.tabIndex = 0;
                    }
                }
                this.setTabLoop(this.firstElement, this.lastElement);
                Event.onFocus(document.documentElement, this._onElementFocus, this, true);
                _currentModal = this;
            },

            /**
             * Creates a hidden focusable element, used to focus on,
             * to enforce modality for browsers in which focus cannot
             * be applied to the container box.
             *
             * @method _createHiddenFocusElement
             * @private
             */
            _createHiddenFocusElement : function() {
                var e = document.createElement("button");
                e.style.height = "1px";
                e.style.width = "1px";
                e.style.position = "absolute";
                e.style.left = "-10000em";
                e.style.opacity = 0;
                e.tabIndex = "-1";
                this.innerElement.appendChild(e);
                this._modalFocus = e;
            },

            /**
             *  @method _removeFocusHandlers
             *  @protected
             *
             *  "hideMask" event handler that removes all "focus" event handlers added
             *  by the "addFocusEventHandlers" method.
             *
             *  @param p_sType {String} Event type
             *  @param p_aArgs {Array} Event Arguments
             */
            _removeFocusHandlers: function(p_sType, p_aArgs) {
                Event.removeFocusListener(document.documentElement, this._onElementFocus, this);

                if (_currentModal == this) {
                    _currentModal = null;
                }
            },

            /**
             * Sets focus to the first element in the Panel.
             *
             * @method focusFirst
             */
            focusFirst: function (type, args, obj) {
                var el = this.firstElement;

                if (args && args[1]) {
                    Event.stopEvent(args[1]);
                }

                if (el) {
                    try {
                        el.focus();
                    } catch(err) {
                        // Ignore
                    }
                }
            },

            /**
             * Sets focus to the last element in the Panel.
             *
             * @method focusLast
             */
            focusLast: function (type, args, obj) {
                var el = this.lastElement;

                if (args && args[1]) {
                    Event.stopEvent(args[1]);
                }

                if (el) {
                    try {
                        el.focus();
                    } catch(err) {
                        // Ignore
                    }
                }
            },

            /**
             * Sets up a tab, shift-tab loop between the first and last elements
             * provided. NOTE: Sets up the preventBackTab and preventTabOut KeyListener
             * instance properties, which are reset everytime this method is invoked.
             *
             * @method setTabLoop
             * @param {HTMLElement} firstElement
             * @param {HTMLElement} lastElement
             *
             */
            setTabLoop : function(firstElement, lastElement) {

                var backTab = this.preventBackTab, tab = this.preventTabOut,
                    showEvent = this.showEvent, hideEvent = this.hideEvent;

                if (backTab) {
                    backTab.disable();
                    showEvent.unsubscribe(backTab.enable, backTab);
                    hideEvent.unsubscribe(backTab.disable, backTab);
                    backTab = this.preventBackTab = null;
                }

                if (tab) {
                    tab.disable();
                    showEvent.unsubscribe(tab.enable, tab);
                    hideEvent.unsubscribe(tab.disable,tab);
                    tab = this.preventTabOut = null;
                }

                if (firstElement) {
                    this.preventBackTab = new KeyListener(firstElement,
                        {shift:true, keys:9},
                        {fn:this.focusLast, scope:this, correctScope:true}
                    );
                    backTab = this.preventBackTab;

                    showEvent.subscribe(backTab.enable, backTab, true);
                    hideEvent.subscribe(backTab.disable,backTab, true);
                }

                if (lastElement) {
                    this.preventTabOut = new KeyListener(lastElement,
                        {shift:false, keys:9},
                        {fn:this.focusFirst, scope:this, correctScope:true}
                    );
                    tab = this.preventTabOut;

                    showEvent.subscribe(tab.enable, tab, true);
                    hideEvent.subscribe(tab.disable,tab, true);
                }
            },

            /**
             * Returns an array of the currently focusable items which reside within
             * Panel. The set of focusable elements the method looks for are defined
             * in the Panel.FOCUSABLE static property
             *
             * @method getFocusableElements
             * @param {HTMLElement} root element to start from.
             */
            getFocusableElements : function(root) {

                root = root || this.innerElement;

                var focusable = {};
                for (var i = 0; i < Panel.FOCUSABLE.length; i++) {
                    focusable[Panel.FOCUSABLE[i]] = true;
                }

                function isFocusable(el) {
                    if (el.focus && el.type !== "hidden" && !el.disabled && focusable[el.tagName.toLowerCase()]) {
                        return true;
                    }
                    return false;
                }

                // Not looking by Tag, since we want elements in DOM order
                return Dom.getElementsBy(isFocusable, null, root);
            },

            /**
             * Sets the firstElement and lastElement instance properties
             * to the first and last focusable elements in the Panel.
             *
             * @method setFirstLastFocusable
             */
            setFirstLastFocusable : function() {

                this.firstElement = null;
                this.lastElement = null;

                var elements = this.getFocusableElements();
                this.focusableElements = elements;

                if (elements.length > 0) {
                    this.firstElement = elements[0];
                    this.lastElement = elements[elements.length - 1];
                }

                if (this.cfg.getProperty("modal")) {
                    this.setTabLoop(this.firstElement, this.lastElement);
                }
            },

            /**
             * Initializes the custom events for Module which are fired
             * automatically at appropriate times by the Module class.
             */
            initEvents: function () {
                Panel.superclass.initEvents.call(this);

                var SIGNATURE = CustomEvent.LIST;

                /**
                * CustomEvent fired after the modality mask is shown
                * @event showMaskEvent
                */
                this.showMaskEvent = this.createEvent(EVENT_TYPES.SHOW_MASK);
                this.showMaskEvent.signature = SIGNATURE;

                /**
                * CustomEvent fired after the modality mask is hidden
                * @event hideMaskEvent
                */
                this.hideMaskEvent = this.createEvent(EVENT_TYPES.HIDE_MASK);
                this.hideMaskEvent.signature = SIGNATURE;

                /**
                * CustomEvent when the Panel is dragged
                * @event dragEvent
                */
                this.dragEvent = this.createEvent(EVENT_TYPES.DRAG);
                this.dragEvent.signature = SIGNATURE;
            },

            /**
             * Initializes the class's configurable properties which can be changed
             * using the Panel's Config object (cfg).
             * @method initDefaultConfig
             */
            initDefaultConfig: function () {
                Panel.superclass.initDefaultConfig.call(this);

                // Add panel config properties //

                /**
                * True if the Panel should display a "close" button
                * @config close
                * @type Boolean
                * @default true
                */
                this.cfg.addProperty(DEFAULT_CONFIG.CLOSE.key, {
                    handler: this.configClose,
                    value: DEFAULT_CONFIG.CLOSE.value,
                    validator: DEFAULT_CONFIG.CLOSE.validator,
                    supercedes: DEFAULT_CONFIG.CLOSE.supercedes
                });

                /**
                * Boolean specifying if the Panel should be draggable.  The default
                * value is "true" if the Drag and Drop utility is included,
                * otherwise it is "false." <strong>PLEASE NOTE:</strong> There is a
                * known issue in IE 6 (Strict Mode and Quirks Mode) and IE 7
                * (Quirks Mode) where Panels that either don't have a value set for
                * their "width" configuration property, or their "width"
                * configuration property is set to "auto" will only be draggable by
                * placing the mouse on the text of the Panel's header element.
                * To fix this bug, draggable Panels missing a value for their
                * "width" configuration property, or whose "width" configuration
                * property is set to "auto" will have it set to the value of
                * their root HTML element's offsetWidth before they are made
                * visible.  The calculated width is then removed when the Panel is
                * hidden. <em>This fix is only applied to draggable Panels in IE 6
                * (Strict Mode and Quirks Mode) and IE 7 (Quirks Mode)</em>. For
                * more information on this issue see:
                * SourceForge bugs #1726972 and #1589210.
                * @config draggable
                * @type Boolean
                * @default true
                */
                this.cfg.addProperty(DEFAULT_CONFIG.DRAGGABLE.key, {
                    handler: this.configDraggable,
                    value: (Util.DD) ? true : false,
                    validator: DEFAULT_CONFIG.DRAGGABLE.validator,
                    supercedes: DEFAULT_CONFIG.DRAGGABLE.supercedes
                });

                /**
                * Boolean specifying if the draggable Panel should be drag only, not interacting with drop
                * targets on the page.
                * <p>
                * When set to true, draggable Panels will not check to see if they are over drop targets,
                * or fire the DragDrop events required to support drop target interaction (onDragEnter,
                * onDragOver, onDragOut, onDragDrop etc.).
                * If the Panel is not designed to be dropped on any target elements on the page, then this
                * flag can be set to true to improve performance.
                * </p>
                * <p>
                * When set to false, all drop target related events will be fired.
                * </p>
                * <p>
                * The property is set to false by default to maintain backwards compatibility but should be
                * set to true if drop target interaction is not required for the Panel, to improve performance.</p>
                *
                * @config dragOnly
                * @type Boolean
                * @default false
                */
                this.cfg.addProperty(DEFAULT_CONFIG.DRAG_ONLY.key, {
                    value: DEFAULT_CONFIG.DRAG_ONLY.value,
                    validator: DEFAULT_CONFIG.DRAG_ONLY.validator,
                    supercedes: DEFAULT_CONFIG.DRAG_ONLY.supercedes
                });

                /**
                * Sets the type of underlay to display for the Panel. Valid values
                * are "shadow," "matte," and "none".  <strong>PLEASE NOTE:</strong>
                * The creation of the underlay element is deferred until the Panel
                * is initially made visible.  For Gecko-based browsers on Mac
                * OS X the underlay elment is always created as it is used as a
                * shim to prevent Aqua scrollbars below a Panel instance from poking
                * through it (See SourceForge bug #836476).
                * @config underlay
                * @type String
                * @default shadow
                */
                this.cfg.addProperty(DEFAULT_CONFIG.UNDERLAY.key, {
                    handler: this.configUnderlay,
                    value: DEFAULT_CONFIG.UNDERLAY.value,
                    supercedes: DEFAULT_CONFIG.UNDERLAY.supercedes
                });

                /**
                * True if the Panel should be displayed in a modal fashion,
                * automatically creating a transparent mask over the document that
                * will not be removed until the Panel is dismissed.
                * @config modal
                * @type Boolean
                * @default false
                */
                this.cfg.addProperty(DEFAULT_CONFIG.MODAL.key, {
                    handler: this.configModal,
                    value: DEFAULT_CONFIG.MODAL.value,
                    validator: DEFAULT_CONFIG.MODAL.validator,
                    supercedes: DEFAULT_CONFIG.MODAL.supercedes
                });

                /**
                * A KeyListener (or array of KeyListeners) that will be enabled
                * when the Panel is shown, and disabled when the Panel is hidden.
                * @config keylisteners
                * @type YAHOO.util.KeyListener[]
                * @default null
                */
                this.cfg.addProperty(DEFAULT_CONFIG.KEY_LISTENERS.key, {
                    handler: this.configKeyListeners,
                    suppressEvent: DEFAULT_CONFIG.KEY_LISTENERS.suppressEvent,
                    supercedes: DEFAULT_CONFIG.KEY_LISTENERS.supercedes
                });

                /**
                * UI Strings used by the Panel
                *
                * @config strings
                * @type Object
                * @default An object literal with the properties shown below:
                *     <dl>
                *         <dt>close</dt><dd><em>String</em> : The string to use for the close icon. Defaults to "Close".</dd>
                *     </dl>
                */
                this.cfg.addProperty(DEFAULT_CONFIG.STRINGS.key, {
                    value:DEFAULT_CONFIG.STRINGS.value,
                    handler:this.configStrings,
                    validator:DEFAULT_CONFIG.STRINGS.validator,
                    supercedes:DEFAULT_CONFIG.STRINGS.supercedes
                });
            },

            // BEGIN BUILT-IN PROPERTY EVENT HANDLERS //

            /**
            * The default event handler fired when the "close" property is changed.
            * The method controls the appending or hiding of the close icon at the
            * top right of the Panel.
            * @method configClose
            * @param {String} type The CustomEvent type (usually the property name)
            * @param {Object[]} args The CustomEvent arguments. For configuration
            * handlers, args[0] will equal the newly applied value for the property.
            * @param {Object} obj The scope object. For configuration handlers,
            * this will usually equal the owner.
            */
            configClose: function (type, args, obj) {

                var val = args[0],
                    oClose = this.close,
                    strings = this.cfg.getProperty("strings");

                if (val) {
                    if (!oClose) {

                        if (!m_oCloseIconTemplate) {
                            m_oCloseIconTemplate = document.createElement("a");
                            m_oCloseIconTemplate.className = "container-close";
                            m_oCloseIconTemplate.href = "#";
                        }

                        oClose = m_oCloseIconTemplate.cloneNode(true);
                        this.innerElement.appendChild(oClose);

                        oClose.innerHTML = (strings && strings.close) ? strings.close : "&#160;";

                        Event.on(oClose, "click", this._doClose, this, true);

                        this.close = oClose;

                    } else {
                        oClose.style.display = "block";
                    }

                } else {
                    if (oClose) {
                        oClose.style.display = "none";
                    }
                }

            },

            /**
             * Event handler for the close icon
             *
             * @method _doClose
             * @protected
             *
             * @param {DOMEvent} e
             */
            _doClose : function (e) {
                Event.preventDefault(e);
                this.hide();
            },

            /**
            * The default event handler fired when the "draggable" property
            * is changed.
            * @method configDraggable
            * @param {String} type The CustomEvent type (usually the property name)
            * @param {Object[]} args The CustomEvent arguments. For configuration
            * handlers, args[0] will equal the newly applied value for the property.
            * @param {Object} obj The scope object. For configuration handlers,
            * this will usually equal the owner.
            */
            configDraggable: function (type, args, obj) {
                var val = args[0];

                if (val) {
                    if (!Util.DD) {
                        YAHOO.log("DD dependency not met.", "error");
                        this.cfg.setProperty("draggable", false);
                        return;
                    }

                    if (this.header) {
                        Dom.setStyle(this.header, "cursor", "move");
                        this.registerDragDrop();
                    }

                    this.subscribe("beforeShow", setWidthToOffsetWidth);

                } else {

                    if (this.dd) {
                        this.dd.unreg();
                    }

                    if (this.header) {
                        Dom.setStyle(this.header,"cursor","auto");
                    }

                    this.unsubscribe("beforeShow", setWidthToOffsetWidth);
                }
            },

            /**
            * The default event handler fired when the "underlay" property
            * is changed.
            * @method configUnderlay
            * @param {String} type The CustomEvent type (usually the property name)
            * @param {Object[]} args The CustomEvent arguments. For configuration
            * handlers, args[0] will equal the newly applied value for the property.
            * @param {Object} obj The scope object. For configuration handlers,
            * this will usually equal the owner.
            */
            configUnderlay: function (type, args, obj) {

                var bMacGecko = (this.platform == "mac" && UA.gecko),
                    sUnderlay = args[0].toLowerCase(),
                    oUnderlay = this.underlay,
                    oElement = this.element;

                function fixWebkitUnderlay() {
                    // Webkit 419.3 (Safari 2.x) does not update
                    // it's Render Tree for the Container when content changes.
                    // We need to force it to update using this contentChange
                    // listener

                    // Webkit 523.6 doesn't have this problem and doesn't
                    // need the fix
                    var u = this.underlay;
                    Dom.addClass(u, "yui-force-redraw");
                    window.setTimeout(function(){Dom.removeClass(u, "yui-force-redraw");}, 0);
                }

                function createUnderlay() {
                    var bNew = false;
                    if (!oUnderlay) { // create if not already in DOM

                        if (!m_oUnderlayTemplate) {
                            m_oUnderlayTemplate = document.createElement("div");
                            m_oUnderlayTemplate.className = "underlay";
                        }

                        oUnderlay = m_oUnderlayTemplate.cloneNode(false);
                        this.element.appendChild(oUnderlay);

                        this.underlay = oUnderlay;

                        if (bIEQuirks) {
                            this.sizeUnderlay();
                            this.cfg.subscribeToConfigEvent("width", this.sizeUnderlay);
                            this.cfg.subscribeToConfigEvent("height", this.sizeUnderlay);

                            this.changeContentEvent.subscribe(this.sizeUnderlay);
                            YAHOO.widget.Module.textResizeEvent.subscribe(this.sizeUnderlay, this, true);
                        }

                        if (UA.webkit && UA.webkit < 420) {
                            this.changeContentEvent.subscribe(fixWebkitUnderlay);
                        }
                        bNew = true;
                    }
                }

                function onBeforeShow() {
                    var bNew = createUnderlay.call(this);
                    if (!bNew && bIEQuirks) {
                        this.sizeUnderlay();
                    }
                    this._underlayDeferred = false;
                    this.beforeShowEvent.unsubscribe(onBeforeShow);
                }

                function destroyUnderlay() {
                    if (this._underlayDeferred) {
                        this.beforeShowEvent.unsubscribe(onBeforeShow);
                        this._underlayDeferred = false;
                    }

                    if (oUnderlay) {
                        this.cfg.unsubscribeFromConfigEvent("width", this.sizeUnderlay);
                        this.cfg.unsubscribeFromConfigEvent("height",this.sizeUnderlay);
                        this.changeContentEvent.unsubscribe(this.sizeUnderlay);
                        this.changeContentEvent.unsubscribe(fixWebkitUnderlay);
                        YAHOO.widget.Module.textResizeEvent.unsubscribe(this.sizeUnderlay, this, true);

                        this.element.removeChild(oUnderlay);

                        this.underlay = null;
                    }
                }

                switch (sUnderlay) {
                    case "shadow":
                        Dom.removeClass(oElement, "matte");
                        Dom.addClass(oElement, "shadow");
                        break;
                    case "matte":
                        if (!bMacGecko) {
                            destroyUnderlay.call(this);
                        }
                        Dom.removeClass(oElement, "shadow");
                        Dom.addClass(oElement, "matte");
                        break;
                    default:
                        if (!bMacGecko) {
                            destroyUnderlay.call(this);
                        }
                        Dom.removeClass(oElement, "shadow");
                        Dom.removeClass(oElement, "matte");
                        break;
                }

                if ((sUnderlay == "shadow") || (bMacGecko && !oUnderlay)) {
                    if (this.cfg.getProperty("visible")) {
                        var bNew = createUnderlay.call(this);
                        if (!bNew && bIEQuirks) {
                            this.sizeUnderlay();
                        }
                    } else {
                        if (!this._underlayDeferred) {
                            this.beforeShowEvent.subscribe(onBeforeShow);
                            this._underlayDeferred = true;
                        }
                    }
                }
            },

            /**
            * The default event handler fired when the "modal" property is
            * changed. This handler subscribes or unsubscribes to the show and hide
            * events to handle the display or hide of the modality mask.
            * @method configModal
            * @param {String} type The CustomEvent type (usually the property name)
            * @param {Object[]} args The CustomEvent arguments. For configuration
            * handlers, args[0] will equal the newly applied value for the property.
            * @param {Object} obj The scope object. For configuration handlers,
            * this will usually equal the owner.
            */
            configModal: function (type, args, obj) {

                var modal = args[0];
                if (modal) {
                    if (!this._hasModalityEventListeners) {

                        this.subscribe("beforeShow", this.buildMask);
                        this.subscribe("beforeShow", this.bringToTop);
                        this.subscribe("beforeShow", this.showMask);
                        this.subscribe("hide", this.hideMask);

                        Overlay.windowResizeEvent.subscribe(this.sizeMask,
                            this, true);

                        this._hasModalityEventListeners = true;
                    }
                } else {
                    if (this._hasModalityEventListeners) {

                        if (this.cfg.getProperty("visible")) {
                            this.hideMask();
                            this.removeMask();
                        }

                        this.unsubscribe("beforeShow", this.buildMask);
                        this.unsubscribe("beforeShow", this.bringToTop);
                        this.unsubscribe("beforeShow", this.showMask);
                        this.unsubscribe("hide", this.hideMask);

                        Overlay.windowResizeEvent.unsubscribe(this.sizeMask, this);

                        this._hasModalityEventListeners = false;
                    }
                }
            },

            /**
            * Removes the modality mask.
            * @method removeMask
            */
            removeMask: function () {

                var oMask = this.mask,
                    oParentNode;

                if (oMask) {
                    /*
                        Hide the mask before destroying it to ensure that DOM
                        event handlers on focusable elements get removed.
                    */
                    this.hideMask();

                    oParentNode = oMask.parentNode;
                    if (oParentNode) {
                        oParentNode.removeChild(oMask);
                    }

                    this.mask = null;
                }
            },

            /**
            * The default event handler fired when the "keylisteners" property
            * is changed.
            * @method configKeyListeners
            * @param {String} type The CustomEvent type (usually the property name)
            * @param {Object[]} args The CustomEvent arguments. For configuration
            * handlers, args[0] will equal the newly applied value for the property.
            * @param {Object} obj The scope object. For configuration handlers,
            * this will usually equal the owner.
            */
            configKeyListeners: function (type, args, obj) {

                var listeners = args[0],
                    listener,
                    nListeners,
                    i;

                if (listeners) {

                    if (listeners instanceof Array) {

                        nListeners = listeners.length;

                        for (i = 0; i < nListeners; i++) {

                            listener = listeners[i];

                            if (!Config.alreadySubscribed(this.showEvent,
                                listener.enable, listener)) {

                                this.showEvent.subscribe(listener.enable,
                                    listener, true);

                            }

                            if (!Config.alreadySubscribed(this.hideEvent,
                                listener.disable, listener)) {

                                this.hideEvent.subscribe(listener.disable,
                                    listener, true);

                                this.destroyEvent.subscribe(listener.disable,
                                    listener, true);
                            }
                        }

                    } else {

                        if (!Config.alreadySubscribed(this.showEvent,
                            listeners.enable, listeners)) {

                            this.showEvent.subscribe(listeners.enable,
                                listeners, true);
                        }

                        if (!Config.alreadySubscribed(this.hideEvent,
                            listeners.disable, listeners)) {

                            this.hideEvent.subscribe(listeners.disable,
                                listeners, true);

                            this.destroyEvent.subscribe(listeners.disable,
                                listeners, true);

                        }

                    }

                }

            },

            /**
            * The default handler for the "strings" property
            * @method configStrings
            */
            configStrings : function(type, args, obj) {
                var val = Lang.merge(DEFAULT_CONFIG.STRINGS.value, args[0]);
                this.cfg.setProperty(DEFAULT_CONFIG.STRINGS.key, val, true);
            },

            /**
            * The default event handler fired when the "height" property is changed.
            * @method configHeight
            * @param {String} type The CustomEvent type (usually the property name)
            * @param {Object[]} args The CustomEvent arguments. For configuration
            * handlers, args[0] will equal the newly applied value for the property.
            * @param {Object} obj The scope object. For configuration handlers,
            * this will usually equal the owner.
            */
            configHeight: function (type, args, obj) {
                var height = args[0],
                    el = this.innerElement;

                Dom.setStyle(el, "height", height);
                this.cfg.refireEvent("iframe");
            },

            /**
             * The default custom event handler executed when the Panel's height is changed,
             * if the autofillheight property has been set.
             *
             * @method _autoFillOnHeightChange
             * @protected
             * @param {String} type The event type
             * @param {Array} args The array of arguments passed to event subscribers
             * @param {HTMLElement} el The header, body or footer element which is to be resized to fill
             * out the containers height
             */
            _autoFillOnHeightChange : function(type, args, el) {
                Panel.superclass._autoFillOnHeightChange.apply(this, arguments);
                if (bIEQuirks) {
                    this.sizeUnderlay();
                }
            },

            /**
            * The default event handler fired when the "width" property is changed.
            * @method configWidth
            * @param {String} type The CustomEvent type (usually the property name)
            * @param {Object[]} args The CustomEvent arguments. For configuration
            * handlers, args[0] will equal the newly applied value for the property.
            * @param {Object} obj The scope object. For configuration handlers,
            * this will usually equal the owner.
            */
            configWidth: function (type, args, obj) {

                var width = args[0],
                    el = this.innerElement;

                Dom.setStyle(el, "width", width);
                this.cfg.refireEvent("iframe");

            },

            /**
            * The default event handler fired when the "zIndex" property is changed.
            * @method configzIndex
            * @param {String} type The CustomEvent type (usually the property name)
            * @param {Object[]} args The CustomEvent arguments. For configuration
            * handlers, args[0] will equal the newly applied value for the property.
            * @param {Object} obj The scope object. For configuration handlers,
            * this will usually equal the owner.
            */
            configzIndex: function (type, args, obj) {
                Panel.superclass.configzIndex.call(this, type, args, obj);

                if (this.mask || this.cfg.getProperty("modal") === true) {
                    var panelZ = Dom.getStyle(this.element, "zIndex");
                    if (!panelZ || isNaN(panelZ)) {
                        panelZ = 0;
                    }

                    if (panelZ === 0) {
                        // Recursive call to configzindex (which should be stopped
                        // from going further because panelZ should no longer === 0)
                        this.cfg.setProperty("zIndex", 1);
                    } else {
                        this.stackMask();
                    }
                }
            },

            // END BUILT-IN PROPERTY EVENT HANDLERS //
            /**
            * Builds the wrapping container around the Panel that is used for
            * positioning the shadow and matte underlays. The container element is
            * assigned to a  local instance variable called container, and the
            * element is reinserted inside of it.
            * @method buildWrapper
            */
            buildWrapper: function () {

                var elementParent = this.element.parentNode,
                    originalElement = this.element,
                    wrapper = document.createElement("div");

                wrapper.className = Panel.CSS_PANEL_CONTAINER;
                wrapper.id = originalElement.id + "_c";

                if (elementParent) {
                    elementParent.insertBefore(wrapper, originalElement);
                }

                wrapper.appendChild(originalElement);

                this.element = wrapper;
                this.innerElement = originalElement;

                Dom.setStyle(this.innerElement, "visibility", "inherit");
            },

            /**
            * Adjusts the size of the shadow based on the size of the element.
            * @method sizeUnderlay
            */
            sizeUnderlay: function () {
                var oUnderlay = this.underlay,
                    oElement;

                if (oUnderlay) {
                    oElement = this.element;
                    oUnderlay.style.width = oElement.offsetWidth + "px";
                    oUnderlay.style.height = oElement.offsetHeight + "px";
                }
            },

            /**
            * Registers the Panel's header for drag & drop capability.
            * @method registerDragDrop
            */
            registerDragDrop: function () {

                var me = this;

                if (this.header) {

                    if (!Util.DD) {
                        YAHOO.log("DD dependency not met.", "error");
                        return;
                    }

                    var bDragOnly = (this.cfg.getProperty("dragonly") === true);
                    this.dd = new Util.DD(this.element.id, this.id, {dragOnly: bDragOnly});

                    if (!this.header.id) {
                        this.header.id = this.id + "_h";
                    }

                    this.dd.startDrag = function () {

                        var offsetHeight,
                            offsetWidth,
                            viewPortWidth,
                            viewPortHeight,
                            scrollX,
                            scrollY;

                        if (YAHOO.env.ua.ie == 6) {
                            Dom.addClass(me.element,"drag");
                        }

                        if (me.cfg.getProperty("constraintoviewport")) {

                            var nViewportOffset = Overlay.VIEWPORT_OFFSET;

                            offsetHeight = me.element.offsetHeight;
                            offsetWidth = me.element.offsetWidth;

                            viewPortWidth = Dom.getViewportWidth();
                            viewPortHeight = Dom.getViewportHeight();

                            scrollX = Dom.getDocumentScrollLeft();
                            scrollY = Dom.getDocumentScrollTop();

                            if (offsetHeight + nViewportOffset < viewPortHeight) {
                                this.minY = scrollY + nViewportOffset;
                                this.maxY = scrollY + viewPortHeight - offsetHeight - nViewportOffset;
                            } else {
                                this.minY = scrollY + nViewportOffset;
                                this.maxY = scrollY + nViewportOffset;
                            }

                            if (offsetWidth + nViewportOffset < viewPortWidth) {
                                this.minX = scrollX + nViewportOffset;
                                this.maxX = scrollX + viewPortWidth - offsetWidth - nViewportOffset;
                            } else {
                                this.minX = scrollX + nViewportOffset;
                                this.maxX = scrollX + nViewportOffset;
                            }

                            this.constrainX = true;
                            this.constrainY = true;
                        } else {
                            this.constrainX = false;
                            this.constrainY = false;
                        }

                        me.dragEvent.fire("startDrag", arguments);
                    };

                    this.dd.onDrag = function () {
                        me.syncPosition();
                        me.cfg.refireEvent("iframe");
                        if (this.platform == "mac" && YAHOO.env.ua.gecko) {
                            this.showMacGeckoScrollbars();
                        }

                        me.dragEvent.fire("onDrag", arguments);
                    };

                    this.dd.endDrag = function () {

                        if (YAHOO.env.ua.ie == 6) {
                            Dom.removeClass(me.element,"drag");
                        }

                        me.dragEvent.fire("endDrag", arguments);
                        me.moveEvent.fire(me.cfg.getProperty("xy"));

                    };

                    this.dd.setHandleElId(this.header.id);
                    this.dd.addInvalidHandleType("INPUT");
                    this.dd.addInvalidHandleType("SELECT");
                    this.dd.addInvalidHandleType("TEXTAREA");
                }
            },

            /**
            * Builds the mask that is laid over the document when the Panel is
            * configured to be modal.
            * @method buildMask
            */
            buildMask: function () {
                var oMask = this.mask;
                if (!oMask) {
                    if (!m_oMaskTemplate) {
                        m_oMaskTemplate = document.createElement("div");
                        m_oMaskTemplate.className = "mask";
                        m_oMaskTemplate.innerHTML = "&#160;";
                    }
                    oMask = m_oMaskTemplate.cloneNode(true);
                    oMask.id = this.id + "_mask";

                    document.body.insertBefore(oMask, document.body.firstChild);

                    this.mask = oMask;

                    if (YAHOO.env.ua.gecko && this.platform == "mac") {
                        Dom.addClass(this.mask, "block-scrollbars");
                    }

                    // Stack mask based on the element zindex
                    this.stackMask();
                }
            },

            /**
            * Hides the modality mask.
            * @method hideMask
            */
            hideMask: function () {
                if (this.cfg.getProperty("modal") && this.mask) {
                    this.mask.style.display = "none";
                    Dom.removeClass(document.body, "masked");
                    this.hideMaskEvent.fire();
                }
            },

            /**
            * Shows the modality mask.
            * @method showMask
            */
            showMask: function () {
                if (this.cfg.getProperty("modal") && this.mask) {
                    Dom.addClass(document.body, "masked");
                    this.sizeMask();
                    this.mask.style.display = "block";
                    this.showMaskEvent.fire();
                }
            },

            /**
            * Sets the size of the modality mask to cover the entire scrollable
            * area of the document
            * @method sizeMask
            */
            sizeMask: function () {
                if (this.mask) {

                    // Shrink mask first, so it doesn't affect the document size.
                    var mask = this.mask,
                        viewWidth = Dom.getViewportWidth(),
                        viewHeight = Dom.getViewportHeight();

                    if (this.mask.offsetHeight > viewHeight) {
                        this.mask.style.height = viewHeight + "px";
                    }

                    if (this.mask.offsetWidth > viewWidth) {
                        this.mask.style.width = viewWidth + "px";
                    }

                    // Then size it to the document
                    this.mask.style.height = Dom.getDocumentHeight() + "px";
                    this.mask.style.width = Dom.getDocumentWidth() + "px";
                }
            },

            /**
             * Sets the zindex of the mask, if it exists, based on the zindex of
             * the Panel element. The zindex of the mask is set to be one less
             * than the Panel element's zindex.
             *
             * <p>NOTE: This method will not bump up the zindex of the Panel
             * to ensure that the mask has a non-negative zindex. If you require the
             * mask zindex to be 0 or higher, the zindex of the Panel
             * should be set to a value higher than 0, before this method is called.
             * </p>
             * @method stackMask
             */
            stackMask: function() {
                if (this.mask) {
                    var panelZ = Dom.getStyle(this.element, "zIndex");
                    if (!YAHOO.lang.isUndefined(panelZ) && !isNaN(panelZ)) {
                        Dom.setStyle(this.mask, "zIndex", panelZ - 1);
                    }
                }
            },

            /**
            * Renders the Panel by inserting the elements that are not already in
            * the main Panel into their correct places. Optionally appends the
            * Panel to the specified node prior to the render's execution. NOTE:
            * For Panels without existing markup, the appendToNode argument is
            * REQUIRED. If this argument is ommitted and the current element is
            * not present in the document, the function will return false,
            * indicating that the render was a failure.
            * @method render
            * @param {String} appendToNode The element id to which the Module
            * should be appended to prior to rendering <em>OR</em>
            * @param {HTMLElement} appendToNode The element to which the Module
            * should be appended to prior to rendering
            * @return {boolean} Success or failure of the render
            */
            render: function (appendToNode) {

                return Panel.superclass.render.call(this,
                    appendToNode, this.innerElement);

            },

            /**
            * Removes the Panel element from the DOM and sets all child elements
            * to null.
            * @method destroy
            */
            destroy: function () {
                Overlay.windowResizeEvent.unsubscribe(this.sizeMask, this);
                this.removeMask();
                if (this.close) {
                    Event.purgeElement(this.close);
                }
                Panel.superclass.destroy.call(this);
            },

            /**
            * Returns a String representation of the object.
            * @method toString
            * @return {String} The string representation of the Panel.
            */
            toString: function () {
                return "Panel " + this.id;
            }

        });

    }());

Copyright © 2008 Yahoo! Inc. All rights reserved.