Viewing File: /usr/local/cpanel/base/yui-gen/data/data_optimized-debug.js

(function(){
YAHOO.util.Event.throwErrors = true;
/*
Copyright (c) 2011, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 2.9.0
*/
/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * The LogMsg class defines a single log message.
 *
 * @class LogMsg
 * @constructor
 * @param oConfigs {Object} Object literal of configuration params.
 */
YAHOO.widget.LogMsg = function(oConfigs) {
    // Parse configs
    /**
     * Log message.
     *
     * @property msg
     * @type String
     */
    this.msg =
    /**
     * Log timestamp.
     *
     * @property time
     * @type Date
     */
    this.time =

    /**
     * Log category.
     *
     * @property category
     * @type String
     */
    this.category =

    /**
     * Log source. The first word passed in as the source argument.
     *
     * @property source
     * @type String
     */
    this.source =

    /**
     * Log source detail. The remainder of the string passed in as the source argument, not
     * including the first word (if any).
     *
     * @property sourceDetail
     * @type String
     */
    this.sourceDetail = null;

    if (oConfigs && (oConfigs.constructor == Object)) {
        for(var param in oConfigs) {
            if (oConfigs.hasOwnProperty(param)) {
                this[param] = oConfigs[param];
            }
        }
    }
};
/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * The LogWriter class provides a mechanism to log messages through
 * YAHOO.widget.Logger from a named source.
 *
 * @class LogWriter
 * @constructor
 * @param sSource {String} Source of LogWriter instance.
 */
YAHOO.widget.LogWriter = function(sSource) {
    if(!sSource) {
        YAHOO.log("Could not instantiate LogWriter due to invalid source.",
            "error", "LogWriter");
        return;
    }
    this._source = sSource;
 };

/////////////////////////////////////////////////////////////////////////////
//
// Public methods
//
/////////////////////////////////////////////////////////////////////////////

 /**
 * Public accessor to the unique name of the LogWriter instance.
 *
 * @method toString
 * @return {String} Unique name of the LogWriter instance.
 */
YAHOO.widget.LogWriter.prototype.toString = function() {
    return "LogWriter " + this._sSource;
};

/**
 * Logs a message attached to the source of the LogWriter.
 * Note: the LogReader adds the message and category to the DOM as HTML.
 *
 * @method log
 * @param sMsg {HTML} The log message.
 * @param sCategory {HTML} Category name.
 */
YAHOO.widget.LogWriter.prototype.log = function(sMsg, sCategory) {
    YAHOO.widget.Logger.log(sMsg, sCategory, this._source);
};

/**
 * Public accessor to get the source name.
 *
 * @method getSource
 * @return {String} The LogWriter source.
 */
YAHOO.widget.LogWriter.prototype.getSource = function() {
    return this._source;
};

/**
 * Public accessor to set the source name.
 *
 * @method setSource
 * @param sSource {String} Source of LogWriter instance.
 */
YAHOO.widget.LogWriter.prototype.setSource = function(sSource) {
    if(!sSource) {
        YAHOO.log("Could not set source due to invalid source.", "error", this.toString());
        return;
    }
    else {
        this._source = sSource;
    }
};

/////////////////////////////////////////////////////////////////////////////
//
// Private member variables
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Source of the LogWriter instance.
 *
 * @property _source
 * @type String
 * @private
 */
YAHOO.widget.LogWriter.prototype._source = null;



 /**
 * The Logger widget provides a simple way to read or write log messages in
 * JavaScript code. Integration with the YUI Library's debug builds allow
 * implementers to access under-the-hood events, errors, and debugging messages.
 * Output may be read through a LogReader console and/or output to a browser
 * console.
 *
 * @module logger
 * @requires yahoo, event, dom
 * @optional dragdrop
 * @namespace YAHOO.widget
 * @title Logger Widget
 */

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

// Define once
if(!YAHOO.widget.Logger) {
    /**
     * The singleton Logger class provides core log management functionality. Saves
     * logs written through the global YAHOO.log function or written by a LogWriter
     * instance. Provides access to logs for reading by a LogReader instance or
     * native browser console such as the Firebug extension to Firefox or Safari's
     * JavaScript console through integration with the console.log() method.
     *
     * @class Logger
     * @static
     */
    YAHOO.widget.Logger = {
        // Initialize properties
        loggerEnabled: true,
        _browserConsoleEnabled: false,
        categories: ["info","warn","error","time","window"],
        sources: ["global"],
        _stack: [], // holds all log msgs
        maxStackEntries: 2500,
        _startTime: new Date().getTime(), // static start timestamp
        _lastTime: null, // timestamp of last logged message
        _windowErrorsHandled: false,
        _origOnWindowError: null
    };

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public properties
    //
    /////////////////////////////////////////////////////////////////////////////
    /**
     * True if Logger is enabled, false otherwise.
     *
     * @property loggerEnabled
     * @type Boolean
     * @static
     * @default true
     */

    /**
     * Array of categories.
     *
     * @property categories
     * @type String[]
     * @static
     * @default ["info","warn","error","time","window"]
     */

    /**
     * Array of sources.
     *
     * @property sources
     * @type String[]
     * @static
     * @default ["global"]
     */

    /**
     * Upper limit on size of internal stack.
     *
     * @property maxStackEntries
     * @type Number
     * @static
     * @default 2500
     */

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private properties
    //
    /////////////////////////////////////////////////////////////////////////////
    /**
     * Internal property to track whether output to browser console is enabled.
     *
     * @property _browserConsoleEnabled
     * @type Boolean
     * @static
     * @default false
     * @private
     */

    /**
     * Array to hold all log messages.
     *
     * @property _stack
     * @type Array
     * @static
     * @private
     */
    /**
     * Static timestamp of Logger initialization.
     *
     * @property _startTime
     * @type Date
     * @static
     * @private
     */
    /**
     * Timestamp of last logged message.
     *
     * @property _lastTime
     * @type Date
     * @static
     * @private
     */
    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////
    /**
     * Saves a log message to the stack and fires newLogEvent. If the log message is
     * assigned to an unknown category, creates a new category. If the log message is
     * from an unknown source, creates a new source.  If browser console is enabled,
     * outputs the log message to browser console.
     * Note: the LogReader adds the message, category, and source to the DOM
     * as HTML.
     *
     * @method log
     * @param sMsg {HTML} The log message.
     * @param sCategory {HTML} Category of log message, or null.
     * @param sSource {HTML} Source of LogWriter, or null if global.
     */
    YAHOO.widget.Logger.log = function(sMsg, sCategory, sSource) {
        if(this.loggerEnabled) {
            if(!sCategory) {
                sCategory = "info"; // default category
            }
            else {
                sCategory = sCategory.toLocaleLowerCase();
                if(this._isNewCategory(sCategory)) {
                    this._createNewCategory(sCategory);
                }
            }
            var sClass = "global"; // default source
            var sDetail = null;
            if(sSource) {
                var spaceIndex = sSource.indexOf(" ");
                if(spaceIndex > 0) {
                    // Substring until first space
                    sClass = sSource.substring(0,spaceIndex);
                    // The rest of the source
                    sDetail = sSource.substring(spaceIndex,sSource.length);
                }
                else {
                    sClass = sSource;
                }
                if(this._isNewSource(sClass)) {
                    this._createNewSource(sClass);
                }
            }

            var timestamp = new Date();
            var logEntry = new YAHOO.widget.LogMsg({
                msg: sMsg,
                time: timestamp,
                category: sCategory,
                source: sClass,
                sourceDetail: sDetail
            });

            var stack = this._stack;
            var maxStackEntries = this.maxStackEntries;
            if(maxStackEntries && !isNaN(maxStackEntries) &&
                (stack.length >= maxStackEntries)) {
                stack.shift();
            }
            stack.push(logEntry);
            this.newLogEvent.fire(logEntry);

            if(this._browserConsoleEnabled) {
                this._printToBrowserConsole(logEntry);
            }
            return true;
        }
        else {
            return false;
        }
    };

    /**
     * Resets internal stack and startTime, enables Logger, and fires logResetEvent.
     *
     * @method reset
     */
    YAHOO.widget.Logger.reset = function() {
        this._stack = [];
        this._startTime = new Date().getTime();
        this.loggerEnabled = true;
        this.log("Logger reset");
        this.logResetEvent.fire();
    };

    /**
     * Public accessor to internal stack of log message objects.
     *
     * @method getStack
     * @return {Object[]} Array of log message objects.
     */
    YAHOO.widget.Logger.getStack = function() {
        return this._stack;
    };

    /**
     * Public accessor to internal start time.
     *
     * @method getStartTime
     * @return {Date} Internal date of when Logger singleton was initialized.
     */
    YAHOO.widget.Logger.getStartTime = function() {
        return this._startTime;
    };

    /**
     * Disables output to the browser's global console.log() function, which is used
     * by the Firebug extension to Firefox as well as Safari.
     *
     * @method disableBrowserConsole
     */
    YAHOO.widget.Logger.disableBrowserConsole = function() {
        YAHOO.log("Logger output to the function console.log() has been disabled.");
        this._browserConsoleEnabled = false;
    };

    /**
     * Enables output to the browser's global console.log() function, which is used
     * by the Firebug extension to Firefox as well as Safari.
     *
     * @method enableBrowserConsole
     */
    YAHOO.widget.Logger.enableBrowserConsole = function() {
        this._browserConsoleEnabled = true;
        YAHOO.log("Logger output to the function console.log() has been enabled.");
    };

    /**
     * Surpresses native JavaScript errors and outputs to console. By default,
     * Logger does not handle JavaScript window error events.
     * NB: Not all browsers support the window.onerror event.
     *
     * @method handleWindowErrors
     */
    YAHOO.widget.Logger.handleWindowErrors = function() {
        if(!YAHOO.widget.Logger._windowErrorsHandled) {
            // Save any previously defined handler to call
            if(window.error) {
                YAHOO.widget.Logger._origOnWindowError = window.onerror;
            }
            window.onerror = YAHOO.widget.Logger._onWindowError;
            YAHOO.widget.Logger._windowErrorsHandled = true;
            YAHOO.log("Logger handling of window.onerror has been enabled.");
        }
        else {
            YAHOO.log("Logger handling of window.onerror had already been enabled.");
        }
    };

    /**
     * Unsurpresses native JavaScript errors. By default,
     * Logger does not handle JavaScript window error events.
     * NB: Not all browsers support the window.onerror event.
     *
     * @method unhandleWindowErrors
     */
    YAHOO.widget.Logger.unhandleWindowErrors = function() {
        if(YAHOO.widget.Logger._windowErrorsHandled) {
            // Revert to any previously defined handler to call
            if(YAHOO.widget.Logger._origOnWindowError) {
                window.onerror = YAHOO.widget.Logger._origOnWindowError;
                YAHOO.widget.Logger._origOnWindowError = null;
            }
            else {
                window.onerror = null;
            }
            YAHOO.widget.Logger._windowErrorsHandled = false;
            YAHOO.log("Logger handling of window.onerror has been disabled.");
        }
        else {
            YAHOO.log("Logger handling of window.onerror had already been disabled.");
        }
    };
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Public events
    //
    /////////////////////////////////////////////////////////////////////////////

     /**
     * Fired when a new category has been created.
     *
     * @event categoryCreateEvent
     * @param sCategory {String} Category name.
     */
    YAHOO.widget.Logger.categoryCreateEvent =
        new YAHOO.util.CustomEvent("categoryCreate", this, true);

     /**
     * Fired when a new source has been named.
     *
     * @event sourceCreateEvent
     * @param sSource {String} Source name.
     */
    YAHOO.widget.Logger.sourceCreateEvent =
        new YAHOO.util.CustomEvent("sourceCreate", this, true);

     /**
     * Fired when a new log message has been created.
     *
     * @event newLogEvent
     * @param sMsg {String} Log message.
     */
    YAHOO.widget.Logger.newLogEvent = new YAHOO.util.CustomEvent("newLog", this, true);

    /**
     * Fired when the Logger has been reset has been created.
     *
     * @event logResetEvent
     */
    YAHOO.widget.Logger.logResetEvent = new YAHOO.util.CustomEvent("logReset", this, true);

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Creates a new category of log messages and fires categoryCreateEvent.
     *
     * @method _createNewCategory
     * @param sCategory {String} Category name.
     * @private
     */
    YAHOO.widget.Logger._createNewCategory = function(sCategory) {
        this.categories.push(sCategory);
        this.categoryCreateEvent.fire(sCategory);
    };

    /**
     * Checks to see if a category has already been created.
     *
     * @method _isNewCategory
     * @param sCategory {String} Category name.
     * @return {Boolean} Returns true if category is unknown, else returns false.
     * @private
     */
    YAHOO.widget.Logger._isNewCategory = function(sCategory) {
        for(var i=0; i < this.categories.length; i++) {
            if(sCategory == this.categories[i]) {
                return false;
            }
        }
        return true;
    };

    /**
     * Creates a new source of log messages and fires sourceCreateEvent.
     *
     * @method _createNewSource
     * @param sSource {String} Source name.
     * @private
     */
    YAHOO.widget.Logger._createNewSource = function(sSource) {
        this.sources.push(sSource);
        this.sourceCreateEvent.fire(sSource);
    };

    /**
     * Checks to see if a source already exists.
     *
     * @method _isNewSource
     * @param sSource {String} Source name.
     * @return {Boolean} Returns true if source is unknown, else returns false.
     * @private
     */
    YAHOO.widget.Logger._isNewSource = function(sSource) {
        if(sSource) {
            for(var i=0; i < this.sources.length; i++) {
                if(sSource == this.sources[i]) {
                    return false;
                }
            }
            return true;
        }
    };

    /**
     * Outputs a log message to global console.log() function.
     *
     * @method _printToBrowserConsole
     * @param oEntry {Object} Log entry object.
     * @private
     */
    YAHOO.widget.Logger._printToBrowserConsole = function(oEntry) {
        if ((window.console && console.log) ||
            (window.opera && opera.postError)) {
            var category = oEntry.category;
            var label = oEntry.category.substring(0,4).toUpperCase();

            var time = oEntry.time;
            var localTime;
            if (time.toLocaleTimeString) {
                localTime  = time.toLocaleTimeString();
            }
            else {
                localTime = time.toString();
            }

            var msecs = time.getTime();
            var elapsedTime = (YAHOO.widget.Logger._lastTime) ?
                (msecs - YAHOO.widget.Logger._lastTime) : 0;
            YAHOO.widget.Logger._lastTime = msecs;

            var output =
                localTime + " (" +
                elapsedTime + "ms): " +
                oEntry.source + ": ";

            if (window.console) {
                console.log(output, oEntry.msg);
            } else {
                opera.postError(output + oEntry.msg);
            }
        }
    };

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private event handlers
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Handles logging of messages due to window error events.
     *
     * @method _onWindowError
     * @param sMsg {String} The error message.
     * @param sUrl {String} URL of the error.
     * @param sLine {String} Line number of the error.
     * @private
     */
    YAHOO.widget.Logger._onWindowError = function(sMsg,sUrl,sLine) {
        // Logger is not in scope of this event handler
        try {
            YAHOO.widget.Logger.log(sMsg+' ('+sUrl+', line '+sLine+')', "window");
            if(YAHOO.widget.Logger._origOnWindowError) {
                YAHOO.widget.Logger._origOnWindowError();
            }
        }
        catch(e) {
            return false;
        }
    };

    /////////////////////////////////////////////////////////////////////////////
    //
    // First log
    //
    /////////////////////////////////////////////////////////////////////////////

    YAHOO.widget.Logger.log("Logger initialized");
}

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/
(function () {
var Logger = YAHOO.widget.Logger,
    u      = YAHOO.util,
    Dom    = u.Dom,
    Event  = u.Event,
    d      = document;

function make(el,props) {
    el = d.createElement(el);
    if (props) {
        for (var p in props) {
            if (props.hasOwnProperty(p)) {
                el[p] = props[p];
            }
        }
    }
    return el;
}

/**
 * The LogReader class provides UI to read messages logged to YAHOO.widget.Logger.
 *
 * @class LogReader
 * @constructor
 * @param elContainer {HTMLElement} (optional) DOM element reference of an existing DIV.
 * @param elContainer {String} (optional) String ID of an existing DIV.
 * @param oConfigs {Object} (optional) Object literal of configuration params.
 */
function LogReader(elContainer, oConfigs) {
    this._sName = LogReader._index;
    LogReader._index++;
    
    this._init.apply(this,arguments);

    /**
     * Render the LogReader immediately upon instantiation.  If set to false,
     * you must call myLogReader.render() to generate the UI.
     * 
     * @property autoRender
     * @type {Boolean}
     * @default true
     */
    if (this.autoRender !== false) {
        this.render();
    }
}

/////////////////////////////////////////////////////////////////////////////
//
// Static member variables
//
/////////////////////////////////////////////////////////////////////////////
YAHOO.lang.augmentObject(LogReader, {
    /**
     * Internal class member to index multiple LogReader instances.
     *
     * @property _memberName
     * @static
     * @type Number
     * @default 0
     * @private
     */
    _index : 0,

    /**
     * Node template for the log entries
     * @property ENTRY_TEMPLATE
     * @static
     * @type {HTMLElement}
     * @default <code>pre</code> element with class yui-log-entry
     */
    ENTRY_TEMPLATE : (function () {
        return make('pre',{ className: 'yui-log-entry' });
    })(),

    /**
     * Template used for innerHTML of verbose entry output.
     * @property VERBOSE_TEMPLATE
     * @static
     * @default "&lt;p>&lt;span class='{category}'>{label}&lt;/span>{totalTime}ms (+{elapsedTime}) {localTime}:&lt;/p>&lt;p>{sourceAndDetail}&lt;/p>&lt;p>{message}&lt;/p>"
     */
    VERBOSE_TEMPLATE : "<p><span class='{category}'>{label}</span> {totalTime}ms (+{elapsedTime}) {localTime}:</p><p>{sourceAndDetail}</p><p>{message}</p>",

    /**
     * Template used for innerHTML of compact entry output.
     * @property BASIC_TEMPLATE
     * @static
     * @default "&lt;p>&lt;span class='{category}'>{label}&lt;/span>{totalTime}ms (+{elapsedTime}) {localTime}: {sourceAndDetail}: {message}&lt;/p>"
     */
    BASIC_TEMPLATE : "<p><span class='{category}'>{label}</span> {totalTime}ms (+{elapsedTime}) {localTime}: {sourceAndDetail}: {message}</p>"
});

/////////////////////////////////////////////////////////////////////////////
//
// Public member variables
//
/////////////////////////////////////////////////////////////////////////////

LogReader.prototype = {
    /**
     * Whether or not LogReader is enabled to output log messages.
     *
     * @property logReaderEnabled
     * @type Boolean
     * @default true
     */
    logReaderEnabled : true,

    /**
     * Public member to access CSS width of the LogReader container.
     *
     * @property width
     * @type String
     */
    width : null,

    /**
     * Public member to access CSS height of the LogReader container.
     *
     * @property height
     * @type String
     */
    height : null,

    /**
     * Public member to access CSS top position of the LogReader container.
     *
     * @property top
     * @type String
     */
    top : null,

    /**
     * Public member to access CSS left position of the LogReader container.
     *
     * @property left
     * @type String
     */
    left : null,

    /**
     * Public member to access CSS right position of the LogReader container.
     *
     * @property right
     * @type String
     */
    right : null,

    /**
     * Public member to access CSS bottom position of the LogReader container.
     *
     * @property bottom
     * @type String
     */
    bottom : null,

    /**
     * Public member to access CSS font size of the LogReader container.
     *
     * @property fontSize
     * @type String
     */
    fontSize : null,

    /**
     * Whether or not the footer UI is enabled for the LogReader.
     *
     * @property footerEnabled
     * @type Boolean
     * @default true
     */
    footerEnabled : true,

    /**
     * Whether or not output is verbose (more readable). Setting to true will make
     * output more compact (less readable).
     *
     * @property verboseOutput
     * @type Boolean
     * @default true
     */
    verboseOutput : true,

    /**
     * Custom output format for log messages.  Defaults to null, which falls
     * back to verboseOutput param deciding between LogReader.VERBOSE_TEMPLATE
     * and LogReader.BASIC_TEMPLATE.  Use bracketed place holders to mark where
     * message info should go.  Available place holder names include:
     * <ul>
     *  <li>category</li>
     *  <li>label</li>
     *  <li>sourceAndDetail</li>
     *  <li>message</li>
     *  <li>localTime</li>
     *  <li>elapsedTime</li>
     *  <li>totalTime</li>
     * </ul>
     *
     * @property entryFormat
     * @type String
     * @default null
     */
    entryFormat : null,

    /**
     * Whether or not newest message is printed on top.
     *
     * @property newestOnTop
     * @type Boolean
     */
    newestOnTop : true,

    /**
     * Output timeout buffer in milliseconds.
     *
     * @property outputBuffer
     * @type Number
     * @default 100
     */
    outputBuffer : 100,

    /**
     * Maximum number of messages a LogReader console will display.
     *
     * @property thresholdMax
     * @type Number
     * @default 500
     */
    thresholdMax : 500,

    /**
     * When a LogReader console reaches its thresholdMax, it will clear out messages
     * and print out the latest thresholdMin number of messages.
     *
     * @property thresholdMin
     * @type Number
     * @default 100
     */
    thresholdMin : 100,

    /**
     * True when LogReader is in a collapsed state, false otherwise.
     *
     * @property isCollapsed
     * @type Boolean
     * @default false
     */
    isCollapsed : false,

    /**
     * True when LogReader is in a paused state, false otherwise.
     *
     * @property isPaused
     * @type Boolean
     * @default false
     */
    isPaused : false,

    /**
     * Enables draggable LogReader if DragDrop Utility is present.
     *
     * @property draggable
     * @type Boolean
     * @default true
     */
    draggable : true,

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

     /**
     * Public accessor to the unique name of the LogReader instance.
     *
     * @method toString
     * @return {String} Unique name of the LogReader instance.
     */
    toString : function() {
        return "LogReader instance" + this._sName;
    },
    /**
     * Pauses output of log messages. While paused, log messages are not lost, but
     * get saved to a buffer and then output upon resume of LogReader.
     *
     * @method pause
     */
    pause : function() {
        this.isPaused = true;
        this._timeout = null;
        this.logReaderEnabled = false;
        if (this._btnPause) {
            this._btnPause.value = "Resume";
        }
    },

    /**
     * Resumes output of log messages, including outputting any log messages that
     * have been saved to buffer while paused.
     *
     * @method resume
     */
    resume : function() {
        this.isPaused = false;
        this.logReaderEnabled = true;
        this._printBuffer();
        if (this._btnPause) {
            this._btnPause.value = "Pause";
        }
    },

    /**
     * Adds the UI to the DOM, attaches event listeners, and bootstraps initial
     * UI state.
     *
     * @method render
     */
    render : function () {
        if (this.rendered) {
            return;
        }

        this._initContainerEl();
        
        this._initHeaderEl();
        this._initConsoleEl();
        this._initFooterEl();

        this._initCategories();
        this._initSources();

        this._initDragDrop();

        // Subscribe to Logger custom events
        Logger.newLogEvent.subscribe(this._onNewLog, this);
        Logger.logResetEvent.subscribe(this._onReset, this);

        Logger.categoryCreateEvent.subscribe(this._onCategoryCreate, this);
        Logger.sourceCreateEvent.subscribe(this._onSourceCreate, this);

        this.rendered = true;

        this._filterLogs();
    },

    /**
     * Removes the UI from the DOM entirely and detaches all event listeners.
     * Implementers should note that Logger will still accumulate messages.
     *
     * @method destroy
     */
    destroy : function () {
        Event.purgeElement(this._elContainer,true);
        this._elContainer.innerHTML = '';
        this._elContainer.parentNode.removeChild(this._elContainer);

        this.rendered = false;
    },

    /**
     * Hides UI of LogReader. Logging functionality is not disrupted.
     *
     * @method hide
     */
    hide : function() {
        this._elContainer.style.display = "none";
    },

    /**
     * Shows UI of LogReader. Logging functionality is not disrupted.
     *
     * @method show
     */
    show : function() {
        this._elContainer.style.display = "block";
    },

    /**
     * Collapses UI of LogReader. Logging functionality is not disrupted.
     *
     * @method collapse
     */
    collapse : function() {
        this._elConsole.style.display = "none";
        if(this._elFt) {
            this._elFt.style.display = "none";
        }
        this._btnCollapse.value = "Expand";
        this.isCollapsed = true;
    },

    /**
     * Expands UI of LogReader. Logging functionality is not disrupted.
     *
     * @method expand
     */
    expand : function() {
        this._elConsole.style.display = "block";
        if(this._elFt) {
            this._elFt.style.display = "block";
        }
        this._btnCollapse.value = "Collapse";
        this.isCollapsed = false;
    },

    /**
     * Returns related checkbox element for given filter (i.e., category or source).
     *
     * @method getCheckbox
     * @param {String} Category or source name.
     * @return {Array} Array of all filter checkboxes.
     */
    getCheckbox : function(filter) {
        return this._filterCheckboxes[filter];
    },

    /**
     * Returns array of enabled categories.
     *
     * @method getCategories
     * @return {String[]} Array of enabled categories.
     */
    getCategories : function() {
        return this._categoryFilters;
    },

    /**
     * Shows log messages associated with given category.
     *
     * @method showCategory
     * @param {String} Category name.
     */
    showCategory : function(sCategory) {
        var filtersArray = this._categoryFilters;
        // Don't do anything if category is already enabled
        // Use Array.indexOf if available...
        if(filtersArray.indexOf) {
             if(filtersArray.indexOf(sCategory) >  -1) {
                return;
            }
        }
        // ...or do it the old-fashioned way
        else {
            for(var i=0; i<filtersArray.length; i++) {
               if(filtersArray[i] === sCategory){
                    return;
                }
            }
        }

        this._categoryFilters.push(sCategory);
        this._filterLogs();
        var elCheckbox = this.getCheckbox(sCategory);
        if(elCheckbox) {
            elCheckbox.checked = true;
        }
    },

    /**
     * Hides log messages associated with given category.
     *
     * @method hideCategory
     * @param {String} Category name.
     */
    hideCategory : function(sCategory) {
        var filtersArray = this._categoryFilters;
        for(var i=0; i<filtersArray.length; i++) {
            if(sCategory == filtersArray[i]) {
                filtersArray.splice(i, 1);
                break;
            }
        }
        this._filterLogs();
        var elCheckbox = this.getCheckbox(sCategory);
        if(elCheckbox) {
            elCheckbox.checked = false;
        }
    },

    /**
     * Returns array of enabled sources.
     *
     * @method getSources
     * @return {Array} Array of enabled sources.
     */
    getSources : function() {
        return this._sourceFilters;
    },

    /**
     * Shows log messages associated with given source.
     *
     * @method showSource
     * @param {String} Source name.
     */
    showSource : function(sSource) {
        var filtersArray = this._sourceFilters;
        // Don't do anything if category is already enabled
        // Use Array.indexOf if available...
        if(filtersArray.indexOf) {
             if(filtersArray.indexOf(sSource) >  -1) {
                return;
            }
        }
        // ...or do it the old-fashioned way
        else {
            for(var i=0; i<filtersArray.length; i++) {
               if(sSource == filtersArray[i]){
                    return;
                }
            }
        }
        filtersArray.push(sSource);
        this._filterLogs();
        var elCheckbox = this.getCheckbox(sSource);
        if(elCheckbox) {
            elCheckbox.checked = true;
        }
    },

    /**
     * Hides log messages associated with given source.
     *
     * @method hideSource
     * @param {String} Source name.
     */
    hideSource : function(sSource) {
        var filtersArray = this._sourceFilters;
        for(var i=0; i<filtersArray.length; i++) {
            if(sSource == filtersArray[i]) {
                filtersArray.splice(i, 1);
                break;
            }
        }
        this._filterLogs();
        var elCheckbox = this.getCheckbox(sSource);
        if(elCheckbox) {
            elCheckbox.checked = false;
        }
    },

    /**
     * Does not delete any log messages, but clears all printed log messages from
     * the console. Log messages will be printed out again if user re-filters. The
     * static method YAHOO.widget.Logger.reset() should be called in order to
     * actually delete log messages.
     *
     * @method clearConsole
     */
    clearConsole : function() {
        // Clear the buffer of any pending messages
        this._timeout = null;
        this._buffer = [];
        this._consoleMsgCount = 0;

        var elConsole = this._elConsole;
        elConsole.innerHTML = '';
    },

    /**
     * Updates title to given string.
     *
     * @method setTitle
     * @param sTitle {String} New title.
     */
    setTitle : function(sTitle) {
        this._title.innerHTML = this.html2Text(sTitle);
    },

    /**
     * Gets timestamp of the last log.
     *
     * @method getLastTime
     * @return {Date} Timestamp of the last log.
     */
    getLastTime : function() {
        return this._lastTime;
    },

    formatMsg : function (entry) {
        var entryFormat = this.entryFormat || (this.verboseOutput ?
                          LogReader.VERBOSE_TEMPLATE : LogReader.BASIC_TEMPLATE),
            info        = {
                category : entry.category,

                // Label for color-coded display
                label : entry.category.substring(0,4).toUpperCase(),

                sourceAndDetail : entry.sourceDetail ?
                                  entry.source + " " + entry.sourceDetail :
                                  entry.source,

                // Escape HTML entities in the log message itself for output
                // to console
                message : this.html2Text(entry.msg || entry.message || '')
            };

        // Add time info
        if (entry.time && entry.time.getTime) {
            info.localTime = entry.time.toLocaleTimeString ?
                             entry.time.toLocaleTimeString() :
                             entry.time.toString();

            // Calculate the elapsed time to be from the last item that
            // passed through the filter, not the absolute previous item
            // in the stack
            info.elapsedTime = entry.time.getTime() - this.getLastTime();

            info.totalTime = entry.time.getTime() - Logger.getStartTime();
        }

        var msg = LogReader.ENTRY_TEMPLATE.cloneNode(true);
        if (this.verboseOutput) {
            msg.className += ' yui-log-verbose';
        }

        // Bug 2061169: Workaround for YAHOO.lang.substitute()
        msg.innerHTML = entryFormat.replace(/\{(\w+)\}/g,
            function (x, placeholder) {
                return (placeholder in info) ? info[placeholder] : '';
            });

        return msg;
    },

    /**
     * Converts input chars "<", ">", and "&" to HTML entities.
     *
     * @method html2Text
     * @param sHtml {String} String to convert.
     * @private
     */
    html2Text : function(sHtml) {
        if(sHtml) {
            sHtml += "";
            return sHtml.replace(/&/g, "&#38;").
                         replace(/</g, "&#60;").
                         replace(/>/g, "&#62;");
        }
        return "";
    },

/////////////////////////////////////////////////////////////////////////////
//
// Private member variables
//
/////////////////////////////////////////////////////////////////////////////

    /**
     * Name of LogReader instance.
     *
     * @property _sName
     * @type String
     * @private
     */
    _sName : null,

    //TODO: remove
    /**
     * A class member shared by all LogReaders if a container needs to be
     * created during instantiation. Will be null if a container element never needs to
     * be created on the fly, such as when the implementer passes in their own element.
     *
     * @property _elDefaultContainer
     * @type HTMLElement
     * @private
     */
    //YAHOO.widget.LogReader._elDefaultContainer = null;

    /**
     * Buffer of log message objects for batch output.
     *
     * @property _buffer
     * @type Object[]
     * @private
     */
    _buffer : null,

    /**
     * Number of log messages output to console.
     *
     * @property _consoleMsgCount
     * @type Number
     * @default 0
     * @private
     */
    _consoleMsgCount : 0,

    /**
     * Date of last output log message.
     *
     * @property _lastTime
     * @type Date
     * @private
     */
    _lastTime : null,

    /**
     * Batched output timeout ID.
     *
     * @property _timeout
     * @type Number
     * @private
     */
    _timeout : null,

    /**
     * Hash of filters and their related checkbox elements.
     *
     * @property _filterCheckboxes
     * @type Object
     * @private
     */
    _filterCheckboxes : null,

    /**
     * Array of filters for log message categories.
     *
     * @property _categoryFilters
     * @type String[]
     * @private
     */
    _categoryFilters : null,

    /**
     * Array of filters for log message sources.
     *
     * @property _sourceFilters
     * @type String[]
     * @private
     */
    _sourceFilters : null,

    /**
     * LogReader container element.
     *
     * @property _elContainer
     * @type HTMLElement
     * @private
     */
    _elContainer : null,

    /**
     * LogReader header element.
     *
     * @property _elHd
     * @type HTMLElement
     * @private
     */
    _elHd : null,

    /**
     * LogReader collapse element.
     *
     * @property _elCollapse
     * @type HTMLElement
     * @private
     */
    _elCollapse : null,

    /**
     * LogReader collapse button element.
     *
     * @property _btnCollapse
     * @type HTMLElement
     * @private
     */
    _btnCollapse : null,

    /**
     * LogReader title header element.
     *
     * @property _title
     * @type HTMLElement
     * @private
     */
    _title : null,

    /**
     * LogReader console element.
     *
     * @property _elConsole
     * @type HTMLElement
     * @private
     */
    _elConsole : null,

    /**
     * LogReader footer element.
     *
     * @property _elFt
     * @type HTMLElement
     * @private
     */
    _elFt : null,

    /**
     * LogReader buttons container element.
     *
     * @property _elBtns
     * @type HTMLElement
     * @private
     */
    _elBtns : null,

    /**
     * Container element for LogReader category filter checkboxes.
     *
     * @property _elCategoryFilters
     * @type HTMLElement
     * @private
     */
    _elCategoryFilters : null,

    /**
     * Container element for LogReader source filter checkboxes.
     *
     * @property _elSourceFilters
     * @type HTMLElement
     * @private
     */
    _elSourceFilters : null,

    /**
     * LogReader pause button element.
     *
     * @property _btnPause
     * @type HTMLElement
     * @private
     */
    _btnPause : null,

    /**
     * Clear button element.
     *
     * @property _btnClear
     * @type HTMLElement
     * @private
     */
    _btnClear : null,

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Initializes the instance's message buffer, start time, etc
     *
     * @method _init
     * @param container {String|HTMLElement} (optional) the render target
     * @param config {Object} (optional) instance configuration
     * @protected
     */
    _init : function (container, config) {
        // Internal vars
        this._buffer = []; // output buffer
        this._filterCheckboxes = {}; // pointers to checkboxes
        this._lastTime = Logger.getStartTime(); // timestamp of last log message to console

        // Parse config vars here
        if (config && (config.constructor == Object)) {
            for(var param in config) {
                if (config.hasOwnProperty(param)) {
                    this[param] = config[param];
                }
            }
        }

        this._elContainer = Dom.get(container);

        YAHOO.log("LogReader initialized", null, this.toString());
    },

    /**
     * Initializes the primary container element.
     *
     * @method _initContainerEl
     * @private
     */
    _initContainerEl : function() {

        // Default the container if unset or not a div
        if(!this._elContainer || !/div$/i.test(this._elContainer.tagName)) {
            this._elContainer = d.body.insertBefore(make("div"),d.body.firstChild);
            // Only position absolutely if an in-DOM element is not supplied
            Dom.addClass(this._elContainer,"yui-log-container");
        }

        Dom.addClass(this._elContainer,"yui-log");

        // If implementer has provided container values, trust and set those
        var style = this._elContainer.style,
            styleProps = ['width','right','top','fontSize'],
            prop,i;

        for (i = styleProps.length - 1; i >= 0; --i) {
            prop = styleProps[i];
            if (this[prop]){ 
                style[prop] = this[prop];
            }
        }

        if(this.left) {
            style.left  = this.left;
            style.right = "auto";
        }
        if(this.bottom) {
            style.bottom = this.bottom;
            style.top    = "auto";
        }

        // Opera needs a little prodding to reflow sometimes
        if (YAHOO.env.ua.opera) {
            d.body.style += '';
        }

    },

    /**
     * Initializes the header element.
     *
     * @method _initHeaderEl
     * @private
     */
    _initHeaderEl : function() {
        // Destroy header if present
        if(this._elHd) {
            // Unhook DOM events
            Event.purgeElement(this._elHd, true);

            // Remove DOM elements
            this._elHd.innerHTML = "";
        }
        
        // Create header
        // TODO: refactor this into an innerHTML
        this._elHd = make("div",{
            className: "yui-log-hd"
        });
        Dom.generateId(this._elHd, 'yui-log-hd' + this._sName);

        this._elCollapse = make("div",{ className: 'yui-log-btns' });

        this._btnCollapse = make("input",{
            type: 'button',
            className: 'yui-log-button',
            value: 'Collapse'
        });
        Event.on(this._btnCollapse,'click',this._onClickCollapseBtn,this);


        this._title = make("h4",{ innerHTML : "Logger Console" });

        this._elCollapse.appendChild(this._btnCollapse);
        this._elHd.appendChild(this._elCollapse);
        this._elHd.appendChild(this._title);
        this._elContainer.appendChild(this._elHd);
    },

    /**
     * Initializes the console element.
     *
     * @method _initConsoleEl
     * @private
     */
    _initConsoleEl : function() {
        // Destroy console
        if(this._elConsole) {
            // Unhook DOM events
            Event.purgeElement(this._elConsole, true);

            // Remove DOM elements
            this._elConsole.innerHTML = "";
        }

        // Ceate console
        this._elConsole = make("div", { className: "yui-log-bd" });

        // If implementer has provided console, trust and set those
        if(this.height) {
            this._elConsole.style.height = this.height;
        }

        this._elContainer.appendChild(this._elConsole);
    },

    /**
     * Initializes the footer element.
     *
     * @method _initFooterEl
     * @private
     */
    _initFooterEl : function() {
        // Don't create footer elements if footer is disabled
        if(this.footerEnabled) {
            // Destroy console
            if(this._elFt) {
                // Unhook DOM events
                Event.purgeElement(this._elFt, true);

                // Remove DOM elements
                this._elFt.innerHTML = "";
            }

            // TODO: use innerHTML
            this._elFt = make("div",{ className: "yui-log-ft" });
            this._elBtns = make("div", { className: "yui-log-btns" });
            this._btnPause = make("input", {
                type: "button",
                className: "yui-log-button",
                value: "Pause"
            });

            Event.on(this._btnPause,'click',this._onClickPauseBtn,this);

            this._btnClear = make("input", {
                type: "button",
                className: "yui-log-button",
                value: "Clear"
            });

            Event.on(this._btnClear,'click',this._onClickClearBtn,this);

            this._elCategoryFilters = make("div", { className: "yui-log-categoryfilters" });
            this._elSourceFilters = make("div", { className: "yui-log-sourcefilters" });

            this._elBtns.appendChild(this._btnPause);
            this._elBtns.appendChild(this._btnClear);
            this._elFt.appendChild(this._elBtns);
            this._elFt.appendChild(this._elCategoryFilters);
            this._elFt.appendChild(this._elSourceFilters);
            this._elContainer.appendChild(this._elFt);
        }
    },

    /**
     * Initializes Drag and Drop on the header element.
     *
     * @method _initDragDrop
     * @private
     */
    _initDragDrop : function() {
        // If Drag and Drop utility is available...
        // ...and draggable is true...
        // ...then make the header draggable
        if(u.DD && this.draggable && this._elHd) {
            var ylog_dd = new u.DD(this._elContainer);
            ylog_dd.setHandleElId(this._elHd.id);
            //TODO: use class name
            this._elHd.style.cursor = "move";
        }
    },

    /**
     * Initializes category filters.
     *
     * @method _initCategories
     * @private
     */
    _initCategories : function() {
        // Initialize category filters
        this._categoryFilters = [];
        var aInitialCategories = Logger.categories;

        for(var j=0; j < aInitialCategories.length; j++) {
            var sCategory = aInitialCategories[j];

            // Add category to the internal array of filters
            this._categoryFilters.push(sCategory);

            // Add checkbox element if UI is enabled
            if(this._elCategoryFilters) {
                this._createCategoryCheckbox(sCategory);
            }
        }
    },

    /**
     * Initializes source filters.
     *
     * @method _initSources
     * @private
     */
    _initSources : function() {
        // Initialize source filters
        this._sourceFilters = [];
        var aInitialSources = Logger.sources;

        for(var j=0; j < aInitialSources.length; j++) {
            var sSource = aInitialSources[j];

            // Add source to the internal array of filters
            this._sourceFilters.push(sSource);

            // Add checkbox element if UI is enabled
            if(this._elSourceFilters) {
                this._createSourceCheckbox(sSource);
            }
        }
    },

    /**
     * Creates the UI for a category filter in the LogReader footer element.
     *
     * @method _createCategoryCheckbox
     * @param sCategory {String} Category name.
     * @private
     */
    _createCategoryCheckbox : function(sCategory) {
        if(this._elFt) {
            var filter = make("span",{ className: "yui-log-filtergrp" }),
                checkid = Dom.generateId(null, "yui-log-filter-" + sCategory + this._sName),
                check  = make("input", {
                    id: checkid,
                    className: "yui-log-filter-" + sCategory,
                    type: "checkbox",
                    category: sCategory
                }),
                label  = make("label", {
                    htmlFor: checkid,
                    className: sCategory,
                    innerHTML: sCategory
                });
            

            // Subscribe to the click event
            Event.on(check,'click',this._onCheckCategory,this);

            this._filterCheckboxes[sCategory] = check;

            // Append el at the end so IE 5.5 can set "type" attribute
            // and THEN set checked property
            filter.appendChild(check);
            filter.appendChild(label);
            this._elCategoryFilters.appendChild(filter);
            check.checked = true;
        }
    },

    /**
     * Creates a checkbox in the LogReader footer element to filter by source.
     *
     * @method _createSourceCheckbox
     * @param sSource {String} Source name.
     * @private
     */
    _createSourceCheckbox : function(sSource) {
        if(this._elFt) {
            var filter = make("span",{ className: "yui-log-filtergrp" }),
                checkid = Dom.generateId(null, "yui-log-filter-" + sSource + this._sName),
                check  = make("input", {
                    id: checkid,
                    className: "yui-log-filter-" + sSource,
                    type: "checkbox",
                    source: sSource
                }),
                label  = make("label", {
                    htmlFor: checkid,
                    className: sSource,
                    innerHTML: sSource
                });
            

            // Subscribe to the click event
            Event.on(check,'click',this._onCheckSource,this);

            this._filterCheckboxes[sSource] = check;

            // Append el at the end so IE 5.5 can set "type" attribute
            // and THEN set checked property
            filter.appendChild(check);
            filter.appendChild(label);
            this._elSourceFilters.appendChild(filter);
            check.checked = true;
        }
    },

    /**
     * Reprints all log messages in the stack through filters.
     *
     * @method _filterLogs
     * @private
     */
    _filterLogs : function() {
        // Reprint stack with new filters
        if (this._elConsole !== null) {
            this.clearConsole();
            this._printToConsole(Logger.getStack());
        }
    },

    /**
     * Sends buffer of log messages to output and clears buffer.
     *
     * @method _printBuffer
     * @private
     */
    _printBuffer : function() {
        this._timeout = null;

        if(this._elConsole !== null) {
            var thresholdMax = this.thresholdMax;
            thresholdMax = (thresholdMax && !isNaN(thresholdMax)) ? thresholdMax : 500;
            if(this._consoleMsgCount < thresholdMax) {
                var entries = [];
                for (var i=0; i<this._buffer.length; i++) {
                    entries[i] = this._buffer[i];
                }
                this._buffer = [];
                this._printToConsole(entries);
            }
            else {
                this._filterLogs();
            }
            
            if(!this.newestOnTop) {
                this._elConsole.scrollTop = this._elConsole.scrollHeight;
            }
        }
    },

    /**
     * Cycles through an array of log messages, and outputs each one to the console
     * if its category has not been filtered out.
     *
     * @method _printToConsole
     * @param aEntries {Object[]} Array of LogMsg objects to output to console.
     * @private
     */
    _printToConsole : function(aEntries) {
        // Manage the number of messages displayed in the console
        var entriesLen         = aEntries.length,
            df                 = d.createDocumentFragment(),
            msgHTML            = [],
            thresholdMin       = this.thresholdMin,
            sourceFiltersLen   = this._sourceFilters.length,
            categoryFiltersLen = this._categoryFilters.length,
            entriesStartIndex,
            i, j, msg, before;

        if(isNaN(thresholdMin) || (thresholdMin > this.thresholdMax)) {
            thresholdMin = 0;
        }
        entriesStartIndex = (entriesLen > thresholdMin) ? (entriesLen - thresholdMin) : 0;
        
        // Iterate through all log entries 
        for(i=entriesStartIndex; i<entriesLen; i++) {
            // Print only the ones that filter through
            var okToPrint = false,
                okToFilterCats = false,
                entry = aEntries[i],
                source = entry.source,
                category = entry.category;

            for(j=0; j<sourceFiltersLen; j++) {
                if(source == this._sourceFilters[j]) {
                    okToFilterCats = true;
                    break;
                }
            }
            if(okToFilterCats) {
                for(j=0; j<categoryFiltersLen; j++) {
                    if(category == this._categoryFilters[j]) {
                        okToPrint = true;
                        break;
                    }
                }
            }
            if(okToPrint) {
                // Start from 0ms elapsed time
                if (this._consoleMsgCount === 0) {
                    this._lastTime = entry.time.getTime();
                }

                msg = this.formatMsg(entry);
                if (typeof msg === 'string') {
                    msgHTML[msgHTML.length] = msg;
                } else {
                    df.insertBefore(msg, this.newestOnTop ?
                        df.firstChild || null : null);
                }
                this._consoleMsgCount++;
                this._lastTime = entry.time.getTime();
            }
        }

        if (msgHTML.length) {
            msgHTML.splice(0,0,this._elConsole.innerHTML);
            this._elConsole.innerHTML = this.newestOnTop ?
                                            msgHTML.reverse().join('') :
                                            msgHTML.join('');
        } else if (df.firstChild) {
            this._elConsole.insertBefore(df, this.newestOnTop ?
                        this._elConsole.firstChild || null : null);
        }
    },

/////////////////////////////////////////////////////////////////////////////
//
// Private event handlers
//
/////////////////////////////////////////////////////////////////////////////

    /**
     * Handles Logger's categoryCreateEvent.
     *
     * @method _onCategoryCreate
     * @param sType {String} The event.
     * @param aArgs {Object[]} Data passed from event firer.
     * @param oSelf {Object} The LogReader instance.
     * @private
     */
    _onCategoryCreate : function(sType, aArgs, oSelf) {
        var category = aArgs[0];
        
        // Add category to the internal array of filters
        oSelf._categoryFilters.push(category);

        if(oSelf._elFt) {
            oSelf._createCategoryCheckbox(category);
        }
    },

    /**
     * Handles Logger's sourceCreateEvent.
     *
     * @method _onSourceCreate
     * @param sType {String} The event.
     * @param aArgs {Object[]} Data passed from event firer.
     * @param oSelf {Object} The LogReader instance.
     * @private
     */
    _onSourceCreate : function(sType, aArgs, oSelf) {
        var source = aArgs[0];
        
        // Add source to the internal array of filters
        oSelf._sourceFilters.push(source);

        if(oSelf._elFt) {
            oSelf._createSourceCheckbox(source);
        }
    },

    /**
     * Handles check events on the category filter checkboxes.
     *
     * @method _onCheckCategory
     * @param v {HTMLEvent} The click event.
     * @param oSelf {Object} The LogReader instance.
     * @private
     */
    _onCheckCategory : function(v, oSelf) {
        var category = this.category;
        if(!this.checked) {
            oSelf.hideCategory(category);
        }
        else {
            oSelf.showCategory(category);
        }
    },

    /**
     * Handles check events on the category filter checkboxes.
     *
     * @method _onCheckSource
     * @param v {HTMLEvent} The click event.
     * @param oSelf {Object} The LogReader instance.
     * @private
     */
    _onCheckSource : function(v, oSelf) {
        var source = this.source;
        if(!this.checked) {
            oSelf.hideSource(source);
        }
        else {
            oSelf.showSource(source);
        }
    },

    /**
     * Handles click events on the collapse button.
     *
     * @method _onClickCollapseBtn
     * @param v {HTMLEvent} The click event.
     * @param oSelf {Object} The LogReader instance
     * @private
     */
    _onClickCollapseBtn : function(v, oSelf) {
        if(!oSelf.isCollapsed) {
            oSelf.collapse();
        }
        else {
            oSelf.expand();
        }
    },

    /**
     * Handles click events on the pause button.
     *
     * @method _onClickPauseBtn
     * @param v {HTMLEvent} The click event.
     * @param oSelf {Object} The LogReader instance.
     * @private
     */
    _onClickPauseBtn : function(v, oSelf) {
        if(!oSelf.isPaused) {
            oSelf.pause();
        }
        else {
            oSelf.resume();
        }
    },

    /**
     * Handles click events on the clear button.
     *
     * @method _onClickClearBtn
     * @param v {HTMLEvent} The click event.
     * @param oSelf {Object} The LogReader instance.
     * @private
     */
    _onClickClearBtn : function(v, oSelf) {
        oSelf.clearConsole();
    },

    /**
     * Handles Logger's newLogEvent.
     *
     * @method _onNewLog
     * @param sType {String} The event.
     * @param aArgs {Object[]} Data passed from event firer.
     * @param oSelf {Object} The LogReader instance.
     * @private
     */
    _onNewLog : function(sType, aArgs, oSelf) {
        var logEntry = aArgs[0];
        oSelf._buffer.push(logEntry);

        if (oSelf.logReaderEnabled === true && oSelf._timeout === null) {
            oSelf._timeout = setTimeout(function(){oSelf._printBuffer();}, oSelf.outputBuffer);
        }
    },

    /**
     * Handles Logger's resetEvent.
     *
     * @method _onReset
     * @param sType {String} The event.
     * @param aArgs {Object[]} Data passed from event firer.
     * @param oSelf {Object} The LogReader instance.
     * @private
     */
    _onReset : function(sType, aArgs, oSelf) {
        oSelf._filterLogs();
    }
};

YAHOO.widget.LogReader = LogReader;

})();
YAHOO.register("logger", YAHOO.widget.Logger, {version: "2.9.0", build: "2800"});
/*
Copyright (c) 2011, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 2.9.0
*/
(function () {

    /**
    * Config is a utility used within an Object to allow the implementer to
    * maintain a list of local configuration properties and listen for changes 
    * to those properties dynamically using CustomEvent. The initial values are 
    * also maintained so that the configuration can be reset at any given point 
    * to its initial state.
    * @namespace YAHOO.util
    * @class Config
    * @constructor
    * @param {Object} owner The owner Object to which this Config Object belongs
    */
    YAHOO.util.Config = function (owner) {

        if (owner) {
            this.init(owner);
        }

        if (!owner) {  YAHOO.log("No owner specified for Config object", "error", "Config"); }

    };


    var Lang = YAHOO.lang,
        CustomEvent = YAHOO.util.CustomEvent,
        Config = YAHOO.util.Config;


    /**
     * Constant representing the CustomEvent type for the config changed event.
     * @property YAHOO.util.Config.CONFIG_CHANGED_EVENT
     * @private
     * @static
     * @final
     */
    Config.CONFIG_CHANGED_EVENT = "configChanged";
    
    /**
     * Constant representing the boolean type string
     * @property YAHOO.util.Config.BOOLEAN_TYPE
     * @private
     * @static
     * @final
     */
    Config.BOOLEAN_TYPE = "boolean";
    
    Config.prototype = {
     
        /**
        * Object reference to the owner of this Config Object
        * @property owner
        * @type Object
        */
        owner: null,
        
        /**
        * Boolean flag that specifies whether a queue is currently 
        * being executed
        * @property queueInProgress
        * @type Boolean
        */
        queueInProgress: false,
        
        /**
        * Maintains the local collection of configuration property objects and 
        * their specified values
        * @property config
        * @private
        * @type Object
        */ 
        config: null,
        
        /**
        * Maintains the local collection of configuration property objects as 
        * they were initially applied.
        * This object is used when resetting a property.
        * @property initialConfig
        * @private
        * @type Object
        */ 
        initialConfig: null,
        
        /**
        * Maintains the local, normalized CustomEvent queue
        * @property eventQueue
        * @private
        * @type Object
        */ 
        eventQueue: null,
        
        /**
        * Custom Event, notifying subscribers when Config properties are set 
        * (setProperty is called without the silent flag
        * @event configChangedEvent
        */
        configChangedEvent: null,
    
        /**
        * Initializes the configuration Object and all of its local members.
        * @method init
        * @param {Object} owner The owner Object to which this Config 
        * Object belongs
        */
        init: function (owner) {
    
            this.owner = owner;
    
            this.configChangedEvent = 
                this.createEvent(Config.CONFIG_CHANGED_EVENT);
    
            this.configChangedEvent.signature = CustomEvent.LIST;
            this.queueInProgress = false;
            this.config = {};
            this.initialConfig = {};
            this.eventQueue = [];
        
        },
        
        /**
        * Validates that the value passed in is a Boolean.
        * @method checkBoolean
        * @param {Object} val The value to validate
        * @return {Boolean} true, if the value is valid
        */ 
        checkBoolean: function (val) {
            return (typeof val == Config.BOOLEAN_TYPE);
        },
        
        /**
        * Validates that the value passed in is a number.
        * @method checkNumber
        * @param {Object} val The value to validate
        * @return {Boolean} true, if the value is valid
        */
        checkNumber: function (val) {
            return (!isNaN(val));
        },
        
        /**
        * Fires a configuration property event using the specified value. 
        * @method fireEvent
        * @private
        * @param {String} key The configuration property's name
        * @param {value} Object The value of the correct type for the property
        */ 
        fireEvent: function ( key, value ) {
            YAHOO.log("Firing Config event: " + key + "=" + value, "info", "Config");
            var property = this.config[key];
        
            if (property && property.event) {
                property.event.fire(value);
            } 
        },
        
        /**
        * Adds a property to the Config Object's private config hash.
        * @method addProperty
        * @param {String} key The configuration property's name
        * @param {Object} propertyObject The Object containing all of this 
        * property's arguments
        */
        addProperty: function ( key, propertyObject ) {
            key = key.toLowerCase();
            YAHOO.log("Added property: " + key, "info", "Config");
        
            this.config[key] = propertyObject;
        
            propertyObject.event = this.createEvent(key, { scope: this.owner });
            propertyObject.event.signature = CustomEvent.LIST;
            
            
            propertyObject.key = key;
        
            if (propertyObject.handler) {
                propertyObject.event.subscribe(propertyObject.handler, 
                    this.owner);
            }
        
            this.setProperty(key, propertyObject.value, true);
            
            if (! propertyObject.suppressEvent) {
                this.queueProperty(key, propertyObject.value);
            }
            
        },
        
        /**
        * Returns a key-value configuration map of the values currently set in  
        * the Config Object.
        * @method getConfig
        * @return {Object} The current config, represented in a key-value map
        */
        getConfig: function () {
        
            var cfg = {},
                currCfg = this.config,
                prop,
                property;
                
            for (prop in currCfg) {
                if (Lang.hasOwnProperty(currCfg, prop)) {
                    property = currCfg[prop];
                    if (property && property.event) {
                        cfg[prop] = property.value;
                    }
                }
            }

            return cfg;
        },
        
        /**
        * Returns the value of specified property.
        * @method getProperty
        * @param {String} key The name of the property
        * @return {Object}  The value of the specified property
        */
        getProperty: function (key) {
            var property = this.config[key.toLowerCase()];
            if (property && property.event) {
                return property.value;
            } else {
                return undefined;
            }
        },
        
        /**
        * Resets the specified property's value to its initial value.
        * @method resetProperty
        * @param {String} key The name of the property
        * @return {Boolean} True is the property was reset, false if not
        */
        resetProperty: function (key) {
            key = key.toLowerCase();

            var property = this.config[key];

            if (property && property.event) {
                if (key in this.initialConfig) {
                    this.setProperty(key, this.initialConfig[key]);
                    return true;
                }
            } else {
                return false;
            }
        },
        
        /**
        * Sets the value of a property. If the silent property is passed as 
        * true, the property's event will not be fired.
        * @method setProperty
        * @param {String} key The name of the property
        * @param {String} value The value to set the property to
        * @param {Boolean} silent Whether the value should be set silently, 
        * without firing the property event.
        * @return {Boolean} True, if the set was successful, false if it failed.
        */
        setProperty: function (key, value, silent) {
        
            var property;
        
            key = key.toLowerCase();
            YAHOO.log("setProperty: " + key + "=" + value, "info", "Config");
        
            if (this.queueInProgress && ! silent) {
                // Currently running through a queue... 
                this.queueProperty(key,value);
                return true;
    
            } else {
                property = this.config[key];
                if (property && property.event) {
                    if (property.validator && !property.validator(value)) {
                        return false;
                    } else {
                        property.value = value;
                        if (! silent) {
                            this.fireEvent(key, value);
                            this.configChangedEvent.fire([key, value]);
                        }
                        return true;
                    }
                } else {
                    return false;
                }
            }
        },
        
        /**
        * Sets the value of a property and queues its event to execute. If the 
        * event is already scheduled to execute, it is
        * moved from its current position to the end of the queue.
        * @method queueProperty
        * @param {String} key The name of the property
        * @param {String} value The value to set the property to
        * @return {Boolean}  true, if the set was successful, false if 
        * it failed.
        */ 
        queueProperty: function (key, value) {
        
            key = key.toLowerCase();
            YAHOO.log("queueProperty: " + key + "=" + value, "info", "Config");
        
            var property = this.config[key],
                foundDuplicate = false,
                iLen,
                queueItem,
                queueItemKey,
                queueItemValue,
                sLen,
                supercedesCheck,
                qLen,
                queueItemCheck,
                queueItemCheckKey,
                queueItemCheckValue,
                i,
                s,
                q;
                                
            if (property && property.event) {
    
                if (!Lang.isUndefined(value) && property.validator && 
                    !property.validator(value)) { // validator
                    return false;
                } else {
        
                    if (!Lang.isUndefined(value)) {
                        property.value = value;
                    } else {
                        value = property.value;
                    }
        
                    foundDuplicate = false;
                    iLen = this.eventQueue.length;
        
                    for (i = 0; i < iLen; i++) {
                        queueItem = this.eventQueue[i];
        
                        if (queueItem) {
                            queueItemKey = queueItem[0];
                            queueItemValue = queueItem[1];

                            if (queueItemKey == key) {
    
                                /*
                                    found a dupe... push to end of queue, null 
                                    current item, and break
                                */
    
                                this.eventQueue[i] = null;
    
                                this.eventQueue.push(
                                    [key, (!Lang.isUndefined(value) ? 
                                    value : queueItemValue)]);
    
                                foundDuplicate = true;
                                break;
                            }
                        }
                    }
                    
                    // this is a refire, or a new property in the queue
    
                    if (! foundDuplicate && !Lang.isUndefined(value)) { 
                        this.eventQueue.push([key, value]);
                    }
                }
        
                if (property.supercedes) {

                    sLen = property.supercedes.length;

                    for (s = 0; s < sLen; s++) {

                        supercedesCheck = property.supercedes[s];
                        qLen = this.eventQueue.length;

                        for (q = 0; q < qLen; q++) {
                            queueItemCheck = this.eventQueue[q];

                            if (queueItemCheck) {
                                queueItemCheckKey = queueItemCheck[0];
                                queueItemCheckValue = queueItemCheck[1];

                                if (queueItemCheckKey == 
                                    supercedesCheck.toLowerCase() ) {

                                    this.eventQueue.push([queueItemCheckKey, 
                                        queueItemCheckValue]);

                                    this.eventQueue[q] = null;
                                    break;

                                }
                            }
                        }
                    }
                }

                YAHOO.log("Config event queue: " + this.outputEventQueue(), "info", "Config");

                return true;
            } else {
                return false;
            }
        },
        
        /**
        * Fires the event for a property using the property's current value.
        * @method refireEvent
        * @param {String} key The name of the property
        */
        refireEvent: function (key) {
    
            key = key.toLowerCase();
        
            var property = this.config[key];
    
            if (property && property.event && 
    
                !Lang.isUndefined(property.value)) {
    
                if (this.queueInProgress) {
    
                    this.queueProperty(key);
    
                } else {
    
                    this.fireEvent(key, property.value);
    
                }
    
            }
        },
        
        /**
        * Applies a key-value Object literal to the configuration, replacing  
        * any existing values, and queueing the property events.
        * Although the values will be set, fireQueue() must be called for their 
        * associated events to execute.
        * @method applyConfig
        * @param {Object} userConfig The configuration Object literal
        * @param {Boolean} init  When set to true, the initialConfig will 
        * be set to the userConfig passed in, so that calling a reset will 
        * reset the properties to the passed values.
        */
        applyConfig: function (userConfig, init) {
        
            var sKey,
                oConfig;

            if (init) {
                oConfig = {};
                for (sKey in userConfig) {
                    if (Lang.hasOwnProperty(userConfig, sKey)) {
                        oConfig[sKey.toLowerCase()] = userConfig[sKey];
                    }
                }
                this.initialConfig = oConfig;
            }

            for (sKey in userConfig) {
                if (Lang.hasOwnProperty(userConfig, sKey)) {
                    this.queueProperty(sKey, userConfig[sKey]);
                }
            }
        },
        
        /**
        * Refires the events for all configuration properties using their 
        * current values.
        * @method refresh
        */
        refresh: function () {

            var prop;

            for (prop in this.config) {
                if (Lang.hasOwnProperty(this.config, prop)) {
                    this.refireEvent(prop);
                }
            }
        },
        
        /**
        * Fires the normalized list of queued property change events
        * @method fireQueue
        */
        fireQueue: function () {
        
            var i, 
                queueItem,
                key,
                value,
                property;
        
            this.queueInProgress = true;
            for (i = 0;i < this.eventQueue.length; i++) {
                queueItem = this.eventQueue[i];
                if (queueItem) {
        
                    key = queueItem[0];
                    value = queueItem[1];
                    property = this.config[key];

                    property.value = value;

                    // Clear out queue entry, to avoid it being 
                    // re-added to the queue by any queueProperty/supercedes
                    // calls which are invoked during fireEvent
                    this.eventQueue[i] = null;

                    this.fireEvent(key,value);
                }
            }
            
            this.queueInProgress = false;
            this.eventQueue = [];
        },
        
        /**
        * Subscribes an external handler to the change event for any 
        * given property. 
        * @method subscribeToConfigEvent
        * @param {String} key The property name
        * @param {Function} handler The handler function to use subscribe to 
        * the property's event
        * @param {Object} obj The Object to use for scoping the event handler 
        * (see CustomEvent documentation)
        * @param {Boolean} overrideContext Optional. If true, will override
        * "this" within the handler to map to the scope Object passed into the
        * method.
        * @return {Boolean} True, if the subscription was successful, 
        * otherwise false.
        */ 
        subscribeToConfigEvent: function (key, handler, obj, overrideContext) {
    
            var property = this.config[key.toLowerCase()];
    
            if (property && property.event) {
                if (!Config.alreadySubscribed(property.event, handler, obj)) {
                    property.event.subscribe(handler, obj, overrideContext);
                }
                return true;
            } else {
                return false;
            }
    
        },
        
        /**
        * Unsubscribes an external handler from the change event for any 
        * given property. 
        * @method unsubscribeFromConfigEvent
        * @param {String} key The property name
        * @param {Function} handler The handler function to use subscribe to 
        * the property's event
        * @param {Object} obj The Object to use for scoping the event 
        * handler (see CustomEvent documentation)
        * @return {Boolean} True, if the unsubscription was successful, 
        * otherwise false.
        */
        unsubscribeFromConfigEvent: function (key, handler, obj) {
            var property = this.config[key.toLowerCase()];
            if (property && property.event) {
                return property.event.unsubscribe(handler, obj);
            } else {
                return false;
            }
        },
        
        /**
        * Returns a string representation of the Config object
        * @method toString
        * @return {String} The Config object in string format.
        */
        toString: function () {
            var output = "Config";
            if (this.owner) {
                output += " [" + this.owner.toString() + "]";
            }
            return output;
        },
        
        /**
        * Returns a string representation of the Config object's current 
        * CustomEvent queue
        * @method outputEventQueue
        * @return {String} The string list of CustomEvents currently queued 
        * for execution
        */
        outputEventQueue: function () {

            var output = "",
                queueItem,
                q,
                nQueue = this.eventQueue.length;
              
            for (q = 0; q < nQueue; q++) {
                queueItem = this.eventQueue[q];
                if (queueItem) {
                    output += queueItem[0] + "=" + queueItem[1] + ", ";
                }
            }
            return output;
        },

        /**
        * Sets all properties to null, unsubscribes all listeners from each 
        * property's change event and all listeners from the configChangedEvent.
        * @method destroy
        */
        destroy: function () {

            var oConfig = this.config,
                sProperty,
                oProperty;


            for (sProperty in oConfig) {
            
                if (Lang.hasOwnProperty(oConfig, sProperty)) {

                    oProperty = oConfig[sProperty];

                    oProperty.event.unsubscribeAll();
                    oProperty.event = null;

                }
            
            }
            
            this.configChangedEvent.unsubscribeAll();
            
            this.configChangedEvent = null;
            this.owner = null;
            this.config = null;
            this.initialConfig = null;
            this.eventQueue = null;
        
        }

    };
    
    
    
    /**
    * Checks to determine if a particular function/Object pair are already 
    * subscribed to the specified CustomEvent
    * @method YAHOO.util.Config.alreadySubscribed
    * @static
    * @param {YAHOO.util.CustomEvent} evt The CustomEvent for which to check 
    * the subscriptions
    * @param {Function} fn The function to look for in the subscribers list
    * @param {Object} obj The execution scope Object for the subscription
    * @return {Boolean} true, if the function/Object pair is already subscribed 
    * to the CustomEvent passed in
    */
    Config.alreadySubscribed = function (evt, fn, obj) {
    
        var nSubscribers = evt.subscribers.length,
            subsc,
            i;

        if (nSubscribers > 0) {
            i = nSubscribers - 1;
            do {
                subsc = evt.subscribers[i];
                if (subsc && subsc.obj == obj && subsc.fn == fn) {
                    return true;
                }
            }
            while (i--);
        }

        return false;

    };

    YAHOO.lang.augmentProto(Config, YAHOO.util.EventProvider);

}());
/**
* The datemath module provides utility methods for basic JavaScript Date object manipulation and 
* comparison. 
* 
* @module datemath
*/

/**
* YAHOO.widget.DateMath is used for simple date manipulation. The class is a static utility
* used for adding, subtracting, and comparing dates.
* @namespace YAHOO.widget
* @class DateMath
*/
YAHOO.widget.DateMath = {
    /**
    * Constant field representing Day
    * @property DAY
    * @static
    * @final
    * @type String
    */
    DAY : "D",

    /**
    * Constant field representing Week
    * @property WEEK
    * @static
    * @final
    * @type String
    */
    WEEK : "W",

    /**
    * Constant field representing Year
    * @property YEAR
    * @static
    * @final
    * @type String
    */
    YEAR : "Y",

    /**
    * Constant field representing Month
    * @property MONTH
    * @static
    * @final
    * @type String
    */
    MONTH : "M",

    /**
    * Constant field representing one day, in milliseconds
    * @property ONE_DAY_MS
    * @static
    * @final
    * @type Number
    */
    ONE_DAY_MS : 1000*60*60*24,
    
    /**
     * Constant field representing the date in first week of January
     * which identifies the first week of the year.
     * <p>
     * In the U.S, Jan 1st is normally used based on a Sunday start of week.
     * ISO 8601, used widely throughout Europe, uses Jan 4th, based on a Monday start of week.
     * </p>
     * @property WEEK_ONE_JAN_DATE
     * @static
     * @type Number
     */
    WEEK_ONE_JAN_DATE : 1,

    /**
    * Adds the specified amount of time to the this instance.
    * @method add
    * @param {Date} date The JavaScript Date object to perform addition on
    * @param {String} field The field constant to be used for performing addition.
    * @param {Number} amount The number of units (measured in the field constant) to add to the date.
    * @return {Date} The resulting Date object
    */
    add : function(date, field, amount) {
        var d = new Date(date.getTime());
        switch (field) {
            case this.MONTH:
                var newMonth = date.getMonth() + amount;
                var years = 0;

                if (newMonth < 0) {
                    while (newMonth < 0) {
                        newMonth += 12;
                        years -= 1;
                    }
                } else if (newMonth > 11) {
                    while (newMonth > 11) {
                        newMonth -= 12;
                        years += 1;
                    }
                }

                d.setMonth(newMonth);
                d.setFullYear(date.getFullYear() + years);
                break;
            case this.DAY:
                this._addDays(d, amount);
                // d.setDate(date.getDate() + amount);
                break;
            case this.YEAR:
                d.setFullYear(date.getFullYear() + amount);
                break;
            case this.WEEK:
                this._addDays(d, (amount * 7));
                // d.setDate(date.getDate() + (amount * 7));
                break;
        }
        return d;
    },

    /**
     * Private helper method to account for bug in Safari 2 (webkit < 420)
     * when Date.setDate(n) is called with n less than -128 or greater than 127.
     * <p>
     * Fix approach and original findings are available here:
     * http://brianary.blogspot.com/2006/03/safari-date-bug.html
     * </p>
     * @method _addDays
     * @param {Date} d JavaScript date object
     * @param {Number} nDays The number of days to add to the date object (can be negative)
     * @private
     */
    _addDays : function(d, nDays) {
        if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420) {
            if (nDays < 0) {
                // Ensure we don't go below -128 (getDate() is always 1 to 31, so we won't go above 127)
                for(var min = -128; nDays < min; nDays -= min) {
                    d.setDate(d.getDate() + min);
                }
            } else {
                // Ensure we don't go above 96 + 31 = 127
                for(var max = 96; nDays > max; nDays -= max) {
                    d.setDate(d.getDate() + max);
                }
            }
            // nDays should be remainder between -128 and 96
        }
        d.setDate(d.getDate() + nDays);
    },

    /**
    * Subtracts the specified amount of time from the this instance.
    * @method subtract
    * @param {Date} date The JavaScript Date object to perform subtraction on
    * @param {Number} field The this field constant to be used for performing subtraction.
    * @param {Number} amount The number of units (measured in the field constant) to subtract from the date.
    * @return {Date} The resulting Date object
    */
    subtract : function(date, field, amount) {
        return this.add(date, field, (amount*-1));
    },

    /**
    * Determines whether a given date is before another date on the calendar.
    * @method before
    * @param {Date} date  The Date object to compare with the compare argument
    * @param {Date} compareTo The Date object to use for the comparison
    * @return {Boolean} true if the date occurs before the compared date; false if not.
    */
    before : function(date, compareTo) {
        var ms = compareTo.getTime();
        if (date.getTime() < ms) {
            return true;
        } else {
            return false;
        }
    },

    /**
    * Determines whether a given date is after another date on the calendar.
    * @method after
    * @param {Date} date  The Date object to compare with the compare argument
    * @param {Date} compareTo The Date object to use for the comparison
    * @return {Boolean} true if the date occurs after the compared date; false if not.
    */
    after : function(date, compareTo) {
        var ms = compareTo.getTime();
        if (date.getTime() > ms) {
            return true;
        } else {
            return false;
        }
    },

    /**
    * Determines whether a given date is between two other dates on the calendar.
    * @method between
    * @param {Date} date  The date to check for
    * @param {Date} dateBegin The start of the range
    * @param {Date} dateEnd  The end of the range
    * @return {Boolean} true if the date occurs between the compared dates; false if not.
    */
    between : function(date, dateBegin, dateEnd) {
        if (this.after(date, dateBegin) && this.before(date, dateEnd)) {
            return true;
        } else {
            return false;
        }
    },
    
    /**
    * Retrieves a JavaScript Date object representing January 1 of any given year.
    * @method getJan1
    * @param {Number} calendarYear  The calendar year for which to retrieve January 1
    * @return {Date} January 1 of the calendar year specified.
    */
    getJan1 : function(calendarYear) {
        return this.getDate(calendarYear,0,1);
    },

    /**
    * Calculates the number of days the specified date is from January 1 of the specified calendar year.
    * Passing January 1 to this function would return an offset value of zero.
    * @method getDayOffset
    * @param {Date} date The JavaScript date for which to find the offset
    * @param {Number} calendarYear The calendar year to use for determining the offset
    * @return {Number} The number of days since January 1 of the given year
    */
    getDayOffset : function(date, calendarYear) {
        var beginYear = this.getJan1(calendarYear); // Find the start of the year. This will be in week 1.
        
        // Find the number of days the passed in date is away from the calendar year start
        var dayOffset = Math.ceil((date.getTime()-beginYear.getTime()) / this.ONE_DAY_MS);
        return dayOffset;
    },

    /**
    * Calculates the week number for the given date. Can currently support standard
    * U.S. week numbers, based on Jan 1st defining the 1st week of the year, and 
    * ISO8601 week numbers, based on Jan 4th defining the 1st week of the year.
    * 
    * @method getWeekNumber
    * @param {Date} date The JavaScript date for which to find the week number
    * @param {Number} firstDayOfWeek The index of the first day of the week (0 = Sun, 1 = Mon ... 6 = Sat).
    * Defaults to 0
    * @param {Number} janDate The date in the first week of January which defines week one for the year
    * Defaults to the value of YAHOO.widget.DateMath.WEEK_ONE_JAN_DATE, which is 1 (Jan 1st). 
    * For the U.S, this is normally Jan 1st. ISO8601 uses Jan 4th to define the first week of the year.
    * 
    * @return {Number} The number of the week containing the given date.
    */
    getWeekNumber : function(date, firstDayOfWeek, janDate) {

        // Setup Defaults
        firstDayOfWeek = firstDayOfWeek || 0;
        janDate = janDate || this.WEEK_ONE_JAN_DATE;

        var targetDate = this.clearTime(date),
            startOfWeek,
            endOfWeek;

        if (targetDate.getDay() === firstDayOfWeek) { 
            startOfWeek = targetDate;
        } else {
            startOfWeek = this.getFirstDayOfWeek(targetDate, firstDayOfWeek);
        }

        var startYear = startOfWeek.getFullYear();

        // DST shouldn't be a problem here, math is quicker than setDate();
        endOfWeek = new Date(startOfWeek.getTime() + 6*this.ONE_DAY_MS);

        var weekNum;
        if (startYear !== endOfWeek.getFullYear() && endOfWeek.getDate() >= janDate) {
            // If years don't match, endOfWeek is in Jan. and if the 
            // week has WEEK_ONE_JAN_DATE in it, it's week one by definition.
            weekNum = 1;
        } else {
            // Get the 1st day of the 1st week, and 
            // find how many days away we are from it.
            var weekOne = this.clearTime(this.getDate(startYear, 0, janDate)),
                weekOneDayOne = this.getFirstDayOfWeek(weekOne, firstDayOfWeek);

            // Round days to smoothen out 1 hr DST diff
            var daysDiff  = Math.round((targetDate.getTime() - weekOneDayOne.getTime())/this.ONE_DAY_MS);

            // Calc. Full Weeks
            var rem = daysDiff % 7;
            var weeksDiff = (daysDiff - rem)/7;
            weekNum = weeksDiff + 1;
        }
        return weekNum;
    },

    /**
     * Get the first day of the week, for the give date. 
     * @param {Date} dt The date in the week for which the first day is required.
     * @param {Number} startOfWeek The index for the first day of the week, 0 = Sun, 1 = Mon ... 6 = Sat (defaults to 0)
     * @return {Date} The first day of the week
     */
    getFirstDayOfWeek : function (dt, startOfWeek) {
        startOfWeek = startOfWeek || 0;
        var dayOfWeekIndex = dt.getDay(),
            dayOfWeek = (dayOfWeekIndex - startOfWeek + 7) % 7;

        return this.subtract(dt, this.DAY, dayOfWeek);
    },

    /**
    * Determines if a given week overlaps two different years.
    * @method isYearOverlapWeek
    * @param {Date} weekBeginDate The JavaScript Date representing the first day of the week.
    * @return {Boolean} true if the date overlaps two different years.
    */
    isYearOverlapWeek : function(weekBeginDate) {
        var overlaps = false;
        var nextWeek = this.add(weekBeginDate, this.DAY, 6);
        if (nextWeek.getFullYear() != weekBeginDate.getFullYear()) {
            overlaps = true;
        }
        return overlaps;
    },

    /**
    * Determines if a given week overlaps two different months.
    * @method isMonthOverlapWeek
    * @param {Date} weekBeginDate The JavaScript Date representing the first day of the week.
    * @return {Boolean} true if the date overlaps two different months.
    */
    isMonthOverlapWeek : function(weekBeginDate) {
        var overlaps = false;
        var nextWeek = this.add(weekBeginDate, this.DAY, 6);
        if (nextWeek.getMonth() != weekBeginDate.getMonth()) {
            overlaps = true;
        }
        return overlaps;
    },

    /**
    * Gets the first day of a month containing a given date.
    * @method findMonthStart
    * @param {Date} date The JavaScript Date used to calculate the month start
    * @return {Date}  The JavaScript Date representing the first day of the month
    */
    findMonthStart : function(date) {
        var start = this.getDate(date.getFullYear(), date.getMonth(), 1);
        return start;
    },

    /**
    * Gets the last day of a month containing a given date.
    * @method findMonthEnd
    * @param {Date} date The JavaScript Date used to calculate the month end
    * @return {Date}  The JavaScript Date representing the last day of the month
    */
    findMonthEnd : function(date) {
        var start = this.findMonthStart(date);
        var nextMonth = this.add(start, this.MONTH, 1);
        var end = this.subtract(nextMonth, this.DAY, 1);
        return end;
    },

    /**
    * Clears the time fields from a given date, effectively setting the time to 12 noon.
    * @method clearTime
    * @param {Date} date The JavaScript Date for which the time fields will be cleared
    * @return {Date}  The JavaScript Date cleared of all time fields
    */
    clearTime : function(date) {
        date.setHours(12,0,0,0);
        return date;
    },

    /**
     * Returns a new JavaScript Date object, representing the given year, month and date. Time fields (hr, min, sec, ms) on the new Date object
     * are set to 0. The method allows Date instances to be created with the a year less than 100. "new Date(year, month, date)" implementations 
     * set the year to 19xx if a year (xx) which is less than 100 is provided.
     * <p>
     * <em>NOTE:</em>Validation on argument values is not performed. It is the caller's responsibility to ensure
     * arguments are valid as per the ECMAScript-262 Date object specification for the new Date(year, month[, date]) constructor.
     * </p>
     * @method getDate
     * @param {Number} y Year.
     * @param {Number} m Month index from 0 (Jan) to 11 (Dec).
     * @param {Number} d (optional) Date from 1 to 31. If not provided, defaults to 1.
     * @return {Date} The JavaScript date object with year, month, date set as provided.
     */
    getDate : function(y, m, d) {
        var dt = null;
        if (YAHOO.lang.isUndefined(d)) {
            d = 1;
        }
        if (y >= 100) {
            dt = new Date(y, m, d);
        } else {
            dt = new Date();
            dt.setFullYear(y);
            dt.setMonth(m);
            dt.setDate(d);
            dt.setHours(0,0,0,0);
        }
        return dt;
    }
};
/**
* The Calendar component is a UI control that enables users to choose one or more dates from a graphical calendar presented in a one-month or
* multi-month interface. Calendars are generated entirely via script and can be navigated without any page refreshes.
* @module    calendar
* @title    Calendar
* @namespace  YAHOO.widget
* @requires  yahoo,dom,event
*/
(function(){

    var Dom = YAHOO.util.Dom,
        Event = YAHOO.util.Event,
        Lang = YAHOO.lang,
        DateMath = YAHOO.widget.DateMath;

/**
* Calendar is the base class for the Calendar widget. In its most basic
* implementation, it has the ability to render a calendar widget on the page
* that can be manipulated to select a single date, move back and forth between
* months and years.
* <p>To construct the placeholder for the calendar widget, the code is as
* follows:
*   <xmp>
*       <div id="calContainer"></div>
*   </xmp>
* </p>
* <p>
* <strong>NOTE: As of 2.4.0, the constructor's ID argument is optional.</strong>
* The Calendar can be constructed by simply providing a container ID string, 
* or a reference to a container DIV HTMLElement (the element needs to exist 
* in the document).
* 
* E.g.:
*   <xmp>
*       var c = new YAHOO.widget.Calendar("calContainer", configOptions);
*   </xmp>
* or:
*   <xmp>
*       var containerDiv = YAHOO.util.Dom.get("calContainer");
*       var c = new YAHOO.widget.Calendar(containerDiv, configOptions);
*   </xmp>
* </p>
* <p>
* If not provided, the ID will be generated from the container DIV ID by adding an "_t" suffix.
* For example if an ID is not provided, and the container's ID is "calContainer", the Calendar's ID will be set to "calContainer_t".
* </p>
* 
* @namespace YAHOO.widget
* @class Calendar
* @constructor
* @param {String} id optional The id of the table element that will represent the Calendar widget. As of 2.4.0, this argument is optional.
* @param {String | HTMLElement} container The id of the container div element that will wrap the Calendar table, or a reference to a DIV element which exists in the document.
* @param {Object} config optional The configuration object containing the initial configuration values for the Calendar.
*/
function Calendar(id, containerId, config) {
    this.init.apply(this, arguments);
}

/**
* The path to be used for images loaded for the Calendar
* @property YAHOO.widget.Calendar.IMG_ROOT
* @static
* @deprecated   You can now customize images by overriding the calclose, calnavleft and calnavright default CSS classes for the close icon, left arrow and right arrow respectively
* @type String
*/
Calendar.IMG_ROOT = null;

/**
* Type constant used for renderers to represent an individual date (M/D/Y)
* @property YAHOO.widget.Calendar.DATE
* @static
* @final
* @type String
*/
Calendar.DATE = "D";

/**
* Type constant used for renderers to represent an individual date across any year (M/D)
* @property YAHOO.widget.Calendar.MONTH_DAY
* @static
* @final
* @type String
*/
Calendar.MONTH_DAY = "MD";

/**
* Type constant used for renderers to represent a weekday
* @property YAHOO.widget.Calendar.WEEKDAY
* @static
* @final
* @type String
*/
Calendar.WEEKDAY = "WD";

/**
* Type constant used for renderers to represent a range of individual dates (M/D/Y-M/D/Y)
* @property YAHOO.widget.Calendar.RANGE
* @static
* @final
* @type String
*/
Calendar.RANGE = "R";

/**
* Type constant used for renderers to represent a month across any year
* @property YAHOO.widget.Calendar.MONTH
* @static
* @final
* @type String
*/
Calendar.MONTH = "M";

/**
* Constant that represents the total number of date cells that are displayed in a given month
* @property YAHOO.widget.Calendar.DISPLAY_DAYS
* @static
* @final
* @type Number
*/
Calendar.DISPLAY_DAYS = 42;

/**
* Constant used for halting the execution of the remainder of the render stack
* @property YAHOO.widget.Calendar.STOP_RENDER
* @static
* @final
* @type String
*/
Calendar.STOP_RENDER = "S";

/**
* Constant used to represent short date field string formats (e.g. Tu or Feb)
* @property YAHOO.widget.Calendar.SHORT
* @static
* @final
* @type String
*/
Calendar.SHORT = "short";

/**
* Constant used to represent long date field string formats (e.g. Monday or February)
* @property YAHOO.widget.Calendar.LONG
* @static
* @final
* @type String
*/
Calendar.LONG = "long";

/**
* Constant used to represent medium date field string formats (e.g. Mon)
* @property YAHOO.widget.Calendar.MEDIUM
* @static
* @final
* @type String
*/
Calendar.MEDIUM = "medium";

/**
* Constant used to represent single character date field string formats (e.g. M, T, W)
* @property YAHOO.widget.Calendar.ONE_CHAR
* @static
* @final
* @type String
*/
Calendar.ONE_CHAR = "1char";

/**
* The set of default Config property keys and values for the Calendar.
*
* <p>
* NOTE: This property is made public in order to allow users to change 
* the default values of configuration properties. Users should not 
* modify the key string, unless they are overriding the Calendar implementation
* </p>
*
* <p>
* The property is an object with key/value pairs, the key being the 
* uppercase configuration property name and the value being an object 
* literal with a key string property, and a value property, specifying the 
* default value of the property. To override a default value, you can set
* the value property, for example, <code>YAHOO.widget.Calendar.DEFAULT_CONFIG.MULTI_SELECT.value = true;</code>
* 
* @property YAHOO.widget.Calendar.DEFAULT_CONFIG
* @static
* @type Object
*/

Calendar.DEFAULT_CONFIG = {
    YEAR_OFFSET : {key:"year_offset", value:0, supercedes:["pagedate", "selected", "mindate","maxdate"]},
    TODAY : {key:"today", value:new Date(), supercedes:["pagedate"]}, 
    PAGEDATE : {key:"pagedate", value:null},
    SELECTED : {key:"selected", value:[]},
    TITLE : {key:"title", value:""},
    CLOSE : {key:"close", value:false},
    IFRAME : {key:"iframe", value:(YAHOO.env.ua.ie && YAHOO.env.ua.ie <= 6) ? true : false},
    MINDATE : {key:"mindate", value:null},
    MAXDATE : {key:"maxdate", value:null},
    MULTI_SELECT : {key:"multi_select", value:false},
    OOM_SELECT : {key:"oom_select", value:false},
    START_WEEKDAY : {key:"start_weekday", value:0},
    SHOW_WEEKDAYS : {key:"show_weekdays", value:true},
    SHOW_WEEK_HEADER : {key:"show_week_header", value:false},
    SHOW_WEEK_FOOTER : {key:"show_week_footer", value:false},
    HIDE_BLANK_WEEKS : {key:"hide_blank_weeks", value:false},
    NAV_ARROW_LEFT: {key:"nav_arrow_left", value:null} ,
    NAV_ARROW_RIGHT : {key:"nav_arrow_right", value:null} ,
    MONTHS_SHORT : {key:"months_short", value:["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]},
    MONTHS_LONG: {key:"months_long", value:["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]},
    WEEKDAYS_1CHAR: {key:"weekdays_1char", value:["S", "M", "T", "W", "T", "F", "S"]},
    WEEKDAYS_SHORT: {key:"weekdays_short", value:["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]},
    WEEKDAYS_MEDIUM: {key:"weekdays_medium", value:["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]},
    WEEKDAYS_LONG: {key:"weekdays_long", value:["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]},
    LOCALE_MONTHS:{key:"locale_months", value:"long"},
    LOCALE_WEEKDAYS:{key:"locale_weekdays", value:"short"},
    DATE_DELIMITER:{key:"date_delimiter", value:","},
    DATE_FIELD_DELIMITER:{key:"date_field_delimiter", value:"/"},
    DATE_RANGE_DELIMITER:{key:"date_range_delimiter", value:"-"},
    MY_MONTH_POSITION:{key:"my_month_position", value:1},
    MY_YEAR_POSITION:{key:"my_year_position", value:2},
    MD_MONTH_POSITION:{key:"md_month_position", value:1},
    MD_DAY_POSITION:{key:"md_day_position", value:2},
    MDY_MONTH_POSITION:{key:"mdy_month_position", value:1},
    MDY_DAY_POSITION:{key:"mdy_day_position", value:2},
    MDY_YEAR_POSITION:{key:"mdy_year_position", value:3},
    MY_LABEL_MONTH_POSITION:{key:"my_label_month_position", value:1},
    MY_LABEL_YEAR_POSITION:{key:"my_label_year_position", value:2},
    MY_LABEL_MONTH_SUFFIX:{key:"my_label_month_suffix", value:" "},
    MY_LABEL_YEAR_SUFFIX:{key:"my_label_year_suffix", value:""},
    NAV: {key:"navigator", value: null},
    STRINGS : { 
        key:"strings",
        value: {
            previousMonth : "Previous Month",
            nextMonth : "Next Month",
            close: "Close"
        },
        supercedes : ["close", "title"]
    }
};

/**
* The set of default Config property keys and values for the Calendar
* @property YAHOO.widget.Calendar._DEFAULT_CONFIG
* @deprecated Made public. See the public DEFAULT_CONFIG property for details
* @final
* @static
* @private
* @type Object
*/
Calendar._DEFAULT_CONFIG = Calendar.DEFAULT_CONFIG;

var DEF_CFG = Calendar.DEFAULT_CONFIG;

/**
* The set of Custom Event types supported by the Calendar
* @property YAHOO.widget.Calendar._EVENT_TYPES
* @final
* @static
* @private
* @type Object
*/
Calendar._EVENT_TYPES = {
    BEFORE_SELECT : "beforeSelect", 
    SELECT : "select",
    BEFORE_DESELECT : "beforeDeselect",
    DESELECT : "deselect",
    CHANGE_PAGE : "changePage",
    BEFORE_RENDER : "beforeRender",
    RENDER : "render",
    BEFORE_DESTROY : "beforeDestroy",
    DESTROY : "destroy",
    RESET : "reset",
    CLEAR : "clear",
    BEFORE_HIDE : "beforeHide",
    HIDE : "hide",
    BEFORE_SHOW : "beforeShow",
    SHOW : "show",
    BEFORE_HIDE_NAV : "beforeHideNav",
    HIDE_NAV : "hideNav",
    BEFORE_SHOW_NAV : "beforeShowNav",
    SHOW_NAV : "showNav",
    BEFORE_RENDER_NAV : "beforeRenderNav",
    RENDER_NAV : "renderNav"
};

/**
* The set of default style constants for the Calendar
* @property YAHOO.widget.Calendar.STYLES
* @static
* @type Object An object with name/value pairs for the class name identifier/value.
*/
Calendar.STYLES = {
    CSS_ROW_HEADER: "calrowhead",
    CSS_ROW_FOOTER: "calrowfoot",
    CSS_CELL : "calcell",
    CSS_CELL_SELECTOR : "selector",
    CSS_CELL_SELECTED : "selected",
    CSS_CELL_SELECTABLE : "selectable",
    CSS_CELL_RESTRICTED : "restricted",
    CSS_CELL_TODAY : "today",
    CSS_CELL_OOM : "oom",
    CSS_CELL_OOB : "previous",
    CSS_HEADER : "calheader",
    CSS_HEADER_TEXT : "calhead",
    CSS_BODY : "calbody",
    CSS_WEEKDAY_CELL : "calweekdaycell",
    CSS_WEEKDAY_ROW : "calweekdayrow",
    CSS_FOOTER : "calfoot",
    CSS_CALENDAR : "yui-calendar",
    CSS_SINGLE : "single",
    CSS_CONTAINER : "yui-calcontainer",
    CSS_NAV_LEFT : "calnavleft",
    CSS_NAV_RIGHT : "calnavright",
    CSS_NAV : "calnav",
    CSS_CLOSE : "calclose",
    CSS_CELL_TOP : "calcelltop",
    CSS_CELL_LEFT : "calcellleft",
    CSS_CELL_RIGHT : "calcellright",
    CSS_CELL_BOTTOM : "calcellbottom",
    CSS_CELL_HOVER : "calcellhover",
    CSS_CELL_HIGHLIGHT1 : "highlight1",
    CSS_CELL_HIGHLIGHT2 : "highlight2",
    CSS_CELL_HIGHLIGHT3 : "highlight3",
    CSS_CELL_HIGHLIGHT4 : "highlight4",
    CSS_WITH_TITLE: "withtitle",
    CSS_FIXED_SIZE: "fixedsize",
    CSS_LINK_CLOSE: "link-close"
};

/**
* The set of default style constants for the Calendar
* @property YAHOO.widget.Calendar._STYLES
* @deprecated Made public. See the public STYLES property for details
* @final
* @static
* @private
* @type Object
*/
Calendar._STYLES = Calendar.STYLES;

Calendar.prototype = {

    /**
    * The configuration object used to set up the calendars various locale and style options.
    * @property Config
    * @private
    * @deprecated Configuration properties should be set by calling Calendar.cfg.setProperty.
    * @type Object
    */
    Config : null,

    /**
    * The parent CalendarGroup, only to be set explicitly by the parent group
    * @property parent
    * @type CalendarGroup
    */ 
    parent : null,

    /**
    * The index of this item in the parent group
    * @property index
    * @type Number
    */
    index : -1,

    /**
    * The collection of calendar table cells
    * @property cells
    * @type HTMLTableCellElement[]
    */
    cells : null,

    /**
    * The collection of calendar cell dates that is parallel to the cells collection. The array contains dates field arrays in the format of [YYYY, M, D].
    * @property cellDates
    * @type Array[](Number[])
    */
    cellDates : null,

    /**
    * The id that uniquely identifies this Calendar.
    * @property id
    * @type String
    */
    id : null,

    /**
    * The unique id associated with the Calendar's container
    * @property containerId
    * @type String
    */
    containerId: null,

    /**
    * The DOM element reference that points to this calendar's container element. The calendar will be inserted into this element when the shell is rendered.
    * @property oDomContainer
    * @type HTMLElement
    */
    oDomContainer : null,

    /**
    * A Date object representing today's date.
    * @deprecated Use the "today" configuration property
    * @property today
    * @type Date
    */
    today : null,

    /**
    * The list of render functions, along with required parameters, used to render cells. 
    * @property renderStack
    * @type Array[]
    */
    renderStack : null,

    /**
    * A copy of the initial render functions created before rendering.
    * @property _renderStack
    * @private
    * @type Array
    */
    _renderStack : null,

    /**
    * A reference to the CalendarNavigator instance created for this Calendar.
    * Will be null if the "navigator" configuration property has not been set
    * @property oNavigator
    * @type CalendarNavigator
    */
    oNavigator : null,

    /**
    * The private list of initially selected dates.
    * @property _selectedDates
    * @private
    * @type Array
    */
    _selectedDates : null,

    /**
    * A map of DOM event handlers to attach to cells associated with specific CSS class names
    * @property domEventMap
    * @type Object
    */
    domEventMap : null,

    /**
     * Protected helper used to parse Calendar constructor/init arguments.
     *
     * As of 2.4.0, Calendar supports a simpler constructor 
     * signature. This method reconciles arguments
     * received in the pre 2.4.0 and 2.4.0 formats.
     * 
     * @protected
     * @method _parseArgs
     * @param {Array} Function "arguments" array
     * @return {Object} Object with id, container, config properties containing
     * the reconciled argument values.
     **/
    _parseArgs : function(args) {
        /*
           2.4.0 Constructors signatures

           new Calendar(String)
           new Calendar(HTMLElement)
           new Calendar(String, ConfigObject)
           new Calendar(HTMLElement, ConfigObject)

           Pre 2.4.0 Constructor signatures

           new Calendar(String, String)
           new Calendar(String, HTMLElement)
           new Calendar(String, String, ConfigObject)
           new Calendar(String, HTMLElement, ConfigObject)
         */
        var nArgs = {id:null, container:null, config:null};

        if (args && args.length && args.length > 0) {
            switch (args.length) {
                case 1:
                    nArgs.id = null;
                    nArgs.container = args[0];
                    nArgs.config = null;
                    break;
                case 2:
                    if (Lang.isObject(args[1]) && !args[1].tagName && !(args[1] instanceof String)) {
                        nArgs.id = null;
                        nArgs.container = args[0];
                        nArgs.config = args[1];
                    } else {
                        nArgs.id = args[0];
                        nArgs.container = args[1];
                        nArgs.config = null;
                    }
                    break;
                default: // 3+
                    nArgs.id = args[0];
                    nArgs.container = args[1];
                    nArgs.config = args[2];
                    break;
            }
        } else {
            this.logger.log("Invalid constructor/init arguments", "error");
        }
        return nArgs;
    },

    /**
    * Initializes the Calendar widget.
    * @method init
    *
    * @param {String} id optional The id of the table element that will represent the Calendar widget. As of 2.4.0, this argument is optional.
    * @param {String | HTMLElement} container The id of the container div element that will wrap the Calendar table, or a reference to a DIV element which exists in the document.
    * @param {Object} config optional The configuration object containing the initial configuration values for the Calendar.
    */
    init : function(id, container, config) {
        // Normalize 2.4.0, pre 2.4.0 args
        var nArgs = this._parseArgs(arguments);

        id = nArgs.id;
        container = nArgs.container;
        config = nArgs.config;

        this.oDomContainer = Dom.get(container);
        // Removing due to order of operations issue [ logger/id ]. 
        // The log is kind of pointless because it'll barf on the next statement anyway.
        // Any code related changes are beyond the scope of 2.9.0 at this point 
        // if (!this.oDomContainer) { this.logger.log("Container not found in document.", "error"); }

        this._oDoc = this.oDomContainer.ownerDocument;

        if (!this.oDomContainer.id) {
            this.oDomContainer.id = Dom.generateId();
        }

        if (!id) {
            id = this.oDomContainer.id + "_t";
        }

        this.id = id;
        this.containerId = this.oDomContainer.id;

        this.logger = new YAHOO.widget.LogWriter("Calendar " + this.id);
        this.initEvents();

        /**
        * The Config object used to hold the configuration variables for the Calendar
        * @property cfg
        * @type YAHOO.util.Config
        */
        this.cfg = new YAHOO.util.Config(this);

        /**
        * The local object which contains the Calendar's options
        * @property Options
        * @type Object
        */
        this.Options = {};

        /**
        * The local object which contains the Calendar's locale settings
        * @property Locale
        * @type Object
        */
        this.Locale = {};

        this.initStyles();

        Dom.addClass(this.oDomContainer, this.Style.CSS_CONTAINER);
        Dom.addClass(this.oDomContainer, this.Style.CSS_SINGLE);

        this.cellDates = [];
        this.cells = [];
        this.renderStack = [];
        this._renderStack = [];

        this.setupConfig();

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

        this.cfg.fireQueue();

        this.today = this.cfg.getProperty("today");
    },

    /**
    * Default Config listener for the iframe property. If the iframe config property is set to true, 
    * renders the built-in IFRAME shim if the container is relatively or absolutely positioned.
    * 
    * @method configIframe
    */
    configIframe : function(type, args, obj) {
        var useIframe = args[0];
    
        if (!this.parent) {
            if (Dom.inDocument(this.oDomContainer)) {
                if (useIframe) {
                    var pos = Dom.getStyle(this.oDomContainer, "position");
                    
                    if (pos == "absolute" || pos == "relative") {
                        
                        if (!Dom.inDocument(this.iframe)) {
                            this.iframe = document.createElement("iframe");
                            this.iframe.src = "javascript:false;";
    
                            Dom.setStyle(this.iframe, "opacity", "0");
    
                            if (YAHOO.env.ua.ie && YAHOO.env.ua.ie <= 6) {
                                Dom.addClass(this.iframe, this.Style.CSS_FIXED_SIZE);
                            }
    
                            this.oDomContainer.insertBefore(this.iframe, this.oDomContainer.firstChild);
                        }
                    }
                } else {
                    if (this.iframe) {
                        if (this.iframe.parentNode) {
                            this.iframe.parentNode.removeChild(this.iframe);
                        }
                        this.iframe = null;
                    }
                }
            }
        }
    },

    /**
    * Default handler for the "title" property
    * @method configTitle
    */
    configTitle : function(type, args, obj) {
        var title = args[0];

        // "" disables title bar
        if (title) {
            this.createTitleBar(title);
        } else {
            var close = this.cfg.getProperty(DEF_CFG.CLOSE.key);
            if (!close) {
                this.removeTitleBar();
            } else {
                this.createTitleBar("&#160;");
            }
        }
    },
    
    /**
    * Default handler for the "close" property
    * @method configClose
    */
    configClose : function(type, args, obj) {
        var close = args[0],
            title = this.cfg.getProperty(DEF_CFG.TITLE.key);
    
        if (close) {
            if (!title) {
                this.createTitleBar("&#160;");
            }
            this.createCloseButton();
        } else {
            this.removeCloseButton();
            if (!title) {
                this.removeTitleBar();
            }
        }
    },

    /**
    * Initializes Calendar's built-in CustomEvents
    * @method initEvents
    */
    initEvents : function() {

        var defEvents = Calendar._EVENT_TYPES,
            CE = YAHOO.util.CustomEvent,
            cal = this; // To help with minification

        /**
        * Fired before a date selection is made
        * @event beforeSelectEvent
        */
        cal.beforeSelectEvent = new CE(defEvents.BEFORE_SELECT); 

        /**
        * Fired when a date selection is made
        * @event selectEvent
        * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD].
        */
        cal.selectEvent = new CE(defEvents.SELECT);

        /**
        * Fired before a date or set of dates is deselected
        * @event beforeDeselectEvent
        */
        cal.beforeDeselectEvent = new CE(defEvents.BEFORE_DESELECT);

        /**
        * Fired when a date or set of dates is deselected
        * @event deselectEvent
        * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD].
        */
        cal.deselectEvent = new CE(defEvents.DESELECT);
    
        /**
        * Fired when the Calendar page is changed
        * @event changePageEvent
        * @param {Date} prevDate The date before the page was changed
        * @param {Date} newDate The date after the page was changed
        */
        cal.changePageEvent = new CE(defEvents.CHANGE_PAGE);
    
        /**
        * Fired before the Calendar is rendered
        * @event beforeRenderEvent
        */
        cal.beforeRenderEvent = new CE(defEvents.BEFORE_RENDER);
    
        /**
        * Fired when the Calendar is rendered
        * @event renderEvent
        */
        cal.renderEvent = new CE(defEvents.RENDER);

        /**
        * Fired just before the Calendar is to be destroyed
        * @event beforeDestroyEvent
        */
        cal.beforeDestroyEvent = new CE(defEvents.BEFORE_DESTROY);

        /**
        * Fired after the Calendar is destroyed. This event should be used
        * for notification only. When this event is fired, important Calendar instance
        * properties, dom references and event listeners have already been 
        * removed/dereferenced, and hence the Calendar instance is not in a usable 
        * state.
        *
        * @event destroyEvent
        */
        cal.destroyEvent = new CE(defEvents.DESTROY);

        /**
        * Fired when the Calendar is reset
        * @event resetEvent
        */
        cal.resetEvent = new CE(defEvents.RESET);

        /**
        * Fired when the Calendar is cleared
        * @event clearEvent
        */
        cal.clearEvent = new CE(defEvents.CLEAR);

        /**
        * Fired just before the Calendar is to be shown
        * @event beforeShowEvent
        */
        cal.beforeShowEvent = new CE(defEvents.BEFORE_SHOW);

        /**
        * Fired after the Calendar is shown
        * @event showEvent
        */
        cal.showEvent = new CE(defEvents.SHOW);

        /**
        * Fired just before the Calendar is to be hidden
        * @event beforeHideEvent
        */
        cal.beforeHideEvent = new CE(defEvents.BEFORE_HIDE);

        /**
        * Fired after the Calendar is hidden
        * @event hideEvent
        */
        cal.hideEvent = new CE(defEvents.HIDE);

        /**
        * Fired just before the CalendarNavigator is to be shown
        * @event beforeShowNavEvent
        */
        cal.beforeShowNavEvent = new CE(defEvents.BEFORE_SHOW_NAV);
    
        /**
        * Fired after the CalendarNavigator is shown
        * @event showNavEvent
        */
        cal.showNavEvent = new CE(defEvents.SHOW_NAV);
    
        /**
        * Fired just before the CalendarNavigator is to be hidden
        * @event beforeHideNavEvent
        */
        cal.beforeHideNavEvent = new CE(defEvents.BEFORE_HIDE_NAV);
    
        /**
        * Fired after the CalendarNavigator is hidden
        * @event hideNavEvent
        */
        cal.hideNavEvent = new CE(defEvents.HIDE_NAV);

        /**
        * Fired just before the CalendarNavigator is to be rendered
        * @event beforeRenderNavEvent
        */
        cal.beforeRenderNavEvent = new CE(defEvents.BEFORE_RENDER_NAV);

        /**
        * Fired after the CalendarNavigator is rendered
        * @event renderNavEvent
        */
        cal.renderNavEvent = new CE(defEvents.RENDER_NAV);

        cal.beforeSelectEvent.subscribe(cal.onBeforeSelect, this, true);
        cal.selectEvent.subscribe(cal.onSelect, this, true);
        cal.beforeDeselectEvent.subscribe(cal.onBeforeDeselect, this, true);
        cal.deselectEvent.subscribe(cal.onDeselect, this, true);
        cal.changePageEvent.subscribe(cal.onChangePage, this, true);
        cal.renderEvent.subscribe(cal.onRender, this, true);
        cal.resetEvent.subscribe(cal.onReset, this, true);
        cal.clearEvent.subscribe(cal.onClear, this, true);
    },

    /**
    * The default event handler for clicks on the "Previous Month" navigation UI
    *
    * @method doPreviousMonthNav
    * @param {DOMEvent} e The DOM event
    * @param {Calendar} cal A reference to the calendar
    */
    doPreviousMonthNav : function(e, cal) {
        Event.preventDefault(e);
        // previousMonth invoked in a timeout, to allow
        // event to bubble up, with correct target. Calling
        // previousMonth, will call render which will remove 
        // HTML which generated the event, resulting in an 
        // invalid event target in certain browsers.
        setTimeout(function() {
            cal.previousMonth();
            var navs = Dom.getElementsByClassName(cal.Style.CSS_NAV_LEFT, "a", cal.oDomContainer);
            if (navs && navs[0]) {
                try {
                    navs[0].focus();
                } catch (ex) {
                    // ignore
                }
            }
        }, 0);
    },

    /**
     * The default event handler for clicks on the "Next Month" navigation UI
     *
     * @method doNextMonthNav
     * @param {DOMEvent} e The DOM event
     * @param {Calendar} cal A reference to the calendar
     */
    doNextMonthNav : function(e, cal) {
        Event.preventDefault(e);
        setTimeout(function() {
            cal.nextMonth();
            var navs = Dom.getElementsByClassName(cal.Style.CSS_NAV_RIGHT, "a", cal.oDomContainer);
            if (navs && navs[0]) {
                try {
                    navs[0].focus();
                } catch (ex) {
                    // ignore
                }
            }
        }, 0);
    },

    /**
    * The default event handler for date cell selection. Currently attached to 
    * the Calendar's bounding box, referenced by it's <a href="#property_oDomContainer">oDomContainer</a> property.
    *
    * @method doSelectCell
    * @param {DOMEvent} e The DOM event
    * @param {Calendar} cal A reference to the calendar
    */
    doSelectCell : function(e, cal) {
        var cell, d, date, index;

        var target = Event.getTarget(e),
            tagName = target.tagName.toLowerCase(),
            defSelector = false;

        while (tagName != "td" && !Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) {

            if (!defSelector && tagName == "a" && Dom.hasClass(target, cal.Style.CSS_CELL_SELECTOR)) {
                defSelector = true;
            }

            target = target.parentNode;
            tagName = target.tagName.toLowerCase();

            if (target == this.oDomContainer || tagName == "html") {
                return;
            }
        }

        if (defSelector) {
            // Stop link href navigation for default renderer
            Event.preventDefault(e);
        }
    
        cell = target;

        if (Dom.hasClass(cell, cal.Style.CSS_CELL_SELECTABLE)) {
            index = cal.getIndexFromId(cell.id);
            if (index > -1) {
                d = cal.cellDates[index];
                if (d) {
                    date = DateMath.getDate(d[0],d[1]-1,d[2]);
                
                    var link;

                    cal.logger.log("Selecting cell " + index + " via click", "info");
                    if (cal.Options.MULTI_SELECT) {
                        link = cell.getElementsByTagName("a")[0];
                        if (link) {
                            link.blur();
                        }

                        var cellDate = cal.cellDates[index];
                        var cellDateIndex = cal._indexOfSelectedFieldArray(cellDate);

                        if (cellDateIndex > -1) { 
                            cal.deselectCell(index);
                        } else {
                            cal.selectCell(index);
                        }

                    } else {
                        link = cell.getElementsByTagName("a")[0];
                        if (link) {
                            link.blur();
                        }
                        cal.selectCell(index);
                    }
                }
            }
        }
    },

    /**
    * The event that is executed when the user hovers over a cell
    * @method doCellMouseOver
    * @param {DOMEvent} e The event
    * @param {Calendar} cal A reference to the calendar passed by the Event utility
    */
    doCellMouseOver : function(e, cal) {
        var target;
        if (e) {
            target = Event.getTarget(e);
        } else {
            target = this;
        }

        while (target.tagName && target.tagName.toLowerCase() != "td") {
            target = target.parentNode;
            if (!target.tagName || target.tagName.toLowerCase() == "html") {
                return;
            }
        }

        if (Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) {
            Dom.addClass(target, cal.Style.CSS_CELL_HOVER);
        }
    },

    /**
    * The event that is executed when the user moves the mouse out of a cell
    * @method doCellMouseOut
    * @param {DOMEvent} e The event
    * @param {Calendar} cal A reference to the calendar passed by the Event utility
    */
    doCellMouseOut : function(e, cal) {
        var target;
        if (e) {
            target = Event.getTarget(e);
        } else {
            target = this;
        }

        while (target.tagName && target.tagName.toLowerCase() != "td") {
            target = target.parentNode;
            if (!target.tagName || target.tagName.toLowerCase() == "html") {
                return;
            }
        }

        if (Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) {
            Dom.removeClass(target, cal.Style.CSS_CELL_HOVER);
        }
    },

    setupConfig : function() {

        var cfg = this.cfg;

        /**
        * The date to use to represent "Today".
        *
        * @config today
        * @type Date
        * @default The client side date (new Date()) when the Calendar is instantiated.
        */
        cfg.addProperty(DEF_CFG.TODAY.key, { value: new Date(DEF_CFG.TODAY.value.getTime()), supercedes:DEF_CFG.TODAY.supercedes, handler:this.configToday, suppressEvent:true } );

        /**
        * The month/year representing the current visible Calendar date (mm/yyyy)
        * @config pagedate
        * @type String | Date
        * @default Today's date
        */
        cfg.addProperty(DEF_CFG.PAGEDATE.key, { value: DEF_CFG.PAGEDATE.value || new Date(DEF_CFG.TODAY.value.getTime()), handler:this.configPageDate } );

        /**
        * The date or range of dates representing the current Calendar selection
        * @config selected
        * @type String
        * @default []
        */
        cfg.addProperty(DEF_CFG.SELECTED.key, { value:DEF_CFG.SELECTED.value.concat(), handler:this.configSelected } );

        /**
        * The title to display above the Calendar's month header. The title is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.   
        * @config title
        * @type HTML
        * @default ""
        */
        cfg.addProperty(DEF_CFG.TITLE.key, { value:DEF_CFG.TITLE.value, handler:this.configTitle } );

        /**
        * Whether or not a close button should be displayed for this Calendar
        * @config close
        * @type Boolean
        * @default false
        */
        cfg.addProperty(DEF_CFG.CLOSE.key, { value:DEF_CFG.CLOSE.value, handler:this.configClose } );

        /**
        * Whether or not an iframe shim should be placed under the Calendar to prevent select boxes from bleeding through in Internet Explorer 6 and below.
        * This property is enabled by default for IE6 and below. It is disabled by default for other browsers for performance reasons, but can be 
        * enabled if required.
        * 
        * @config iframe
        * @type Boolean
        * @default true for IE6 and below, false for all other browsers
        */
        cfg.addProperty(DEF_CFG.IFRAME.key, { value:DEF_CFG.IFRAME.value, handler:this.configIframe, validator:cfg.checkBoolean } );

        /**
        * The minimum selectable date in the current Calendar (mm/dd/yyyy)
        * @config mindate
        * @type String | Date
        * @default null
        */
        cfg.addProperty(DEF_CFG.MINDATE.key, { value:DEF_CFG.MINDATE.value, handler:this.configMinDate } );

        /**
        * The maximum selectable date in the current Calendar (mm/dd/yyyy)
        * @config maxdate
        * @type String | Date
        * @default null
        */
        cfg.addProperty(DEF_CFG.MAXDATE.key, { value:DEF_CFG.MAXDATE.value, handler:this.configMaxDate } );

        // Options properties
    
        /**
        * True if the Calendar should allow multiple selections. False by default.
        * @config MULTI_SELECT
        * @type Boolean
        * @default false
        */
        cfg.addProperty(DEF_CFG.MULTI_SELECT.key, { value:DEF_CFG.MULTI_SELECT.value, handler:this.configOptions, validator:cfg.checkBoolean } );

        /**
        * True if the Calendar should allow selection of out-of-month dates. False by default.
        * @config OOM_SELECT
        * @type Boolean
        * @default false
        */
        cfg.addProperty(DEF_CFG.OOM_SELECT.key, { value:DEF_CFG.OOM_SELECT.value, handler:this.configOptions, validator:cfg.checkBoolean } );

        /**
        * The weekday the week begins on. Default is 0 (Sunday = 0, Monday = 1 ... Saturday = 6).
        * @config START_WEEKDAY
        * @type number
        * @default 0
        */
        cfg.addProperty(DEF_CFG.START_WEEKDAY.key, { value:DEF_CFG.START_WEEKDAY.value, handler:this.configOptions, validator:cfg.checkNumber  } );
    
        /**
        * True if the Calendar should show weekday labels. True by default.
        * @config SHOW_WEEKDAYS
        * @type Boolean
        * @default true
        */
        cfg.addProperty(DEF_CFG.SHOW_WEEKDAYS.key, { value:DEF_CFG.SHOW_WEEKDAYS.value, handler:this.configOptions, validator:cfg.checkBoolean  } );
    
        /**
        * True if the Calendar should show week row headers. False by default.
        * @config SHOW_WEEK_HEADER
        * @type Boolean
        * @default false
        */
        cfg.addProperty(DEF_CFG.SHOW_WEEK_HEADER.key, { value:DEF_CFG.SHOW_WEEK_HEADER.value, handler:this.configOptions, validator:cfg.checkBoolean } );
    
        /**
        * True if the Calendar should show week row footers. False by default.
        * @config SHOW_WEEK_FOOTER
        * @type Boolean
        * @default false
        */ 
        cfg.addProperty(DEF_CFG.SHOW_WEEK_FOOTER.key,{ value:DEF_CFG.SHOW_WEEK_FOOTER.value, handler:this.configOptions, validator:cfg.checkBoolean } );
    
        /**
        * True if the Calendar should suppress weeks that are not a part of the current month. False by default.
        * @config HIDE_BLANK_WEEKS
        * @type Boolean
        * @default false
        */ 
        cfg.addProperty(DEF_CFG.HIDE_BLANK_WEEKS.key, { value:DEF_CFG.HIDE_BLANK_WEEKS.value, handler:this.configOptions, validator:cfg.checkBoolean } );
        
        /**
        * The image URL that should be used for the left navigation arrow. The image URL is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config NAV_ARROW_LEFT
        * @type String
        * @deprecated You can customize the image by overriding the default CSS class for the left arrow - "calnavleft"  
        * @default null
        */ 
        cfg.addProperty(DEF_CFG.NAV_ARROW_LEFT.key, { value:DEF_CFG.NAV_ARROW_LEFT.value, handler:this.configOptions } );
    
        /**
        * The image URL that should be used for the right navigation arrow. The image URL is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config NAV_ARROW_RIGHT
        * @type String
        * @deprecated You can customize the image by overriding the default CSS class for the right arrow - "calnavright"
        * @default null
        */ 
        cfg.addProperty(DEF_CFG.NAV_ARROW_RIGHT.key, { value:DEF_CFG.NAV_ARROW_RIGHT.value, handler:this.configOptions } );
    
        // Locale properties
    
        /**
        * The short month labels for the current locale. The month labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config MONTHS_SHORT
        * @type HTML[]
        * @default ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
        */
        cfg.addProperty(DEF_CFG.MONTHS_SHORT.key, { value:DEF_CFG.MONTHS_SHORT.value, handler:this.configLocale } );

        /**
        * The long month labels for the current locale. The month labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config MONTHS_LONG
        * @type HTML[]
        * @default ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
        */ 
        cfg.addProperty(DEF_CFG.MONTHS_LONG.key,  { value:DEF_CFG.MONTHS_LONG.value, handler:this.configLocale } );

        /**
        * The 1-character weekday labels for the current locale. The weekday labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config WEEKDAYS_1CHAR
        * @type HTML[]
        * @default ["S", "M", "T", "W", "T", "F", "S"]
        */ 
        cfg.addProperty(DEF_CFG.WEEKDAYS_1CHAR.key, { value:DEF_CFG.WEEKDAYS_1CHAR.value, handler:this.configLocale } );
        
        /**
        * The short weekday labels for the current locale. The weekday labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config WEEKDAYS_SHORT
        * @type HTML[]
        * @default ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
        */ 
        cfg.addProperty(DEF_CFG.WEEKDAYS_SHORT.key, { value:DEF_CFG.WEEKDAYS_SHORT.value, handler:this.configLocale } );
        
        /**
        * The medium weekday labels for the current locale. The weekday labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config WEEKDAYS_MEDIUM
        * @type HTML[]
        * @default ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
        */ 
        cfg.addProperty(DEF_CFG.WEEKDAYS_MEDIUM.key, { value:DEF_CFG.WEEKDAYS_MEDIUM.value, handler:this.configLocale } );
        
        /**
        * The long weekday labels for the current locale. The weekday labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config WEEKDAYS_LONG
        * @type HTML[]
        * @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
        */ 
        cfg.addProperty(DEF_CFG.WEEKDAYS_LONG.key, { value:DEF_CFG.WEEKDAYS_LONG.value, handler:this.configLocale } );

        /**
        * Refreshes the locale values used to build the Calendar.
        * @method refreshLocale
        * @private
        */
        var refreshLocale = function() {
            cfg.refireEvent(DEF_CFG.LOCALE_MONTHS.key);
            cfg.refireEvent(DEF_CFG.LOCALE_WEEKDAYS.key);
        };
    
        cfg.subscribeToConfigEvent(DEF_CFG.START_WEEKDAY.key, refreshLocale, this, true);
        cfg.subscribeToConfigEvent(DEF_CFG.MONTHS_SHORT.key, refreshLocale, this, true);
        cfg.subscribeToConfigEvent(DEF_CFG.MONTHS_LONG.key, refreshLocale, this, true);
        cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_1CHAR.key, refreshLocale, this, true);
        cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_SHORT.key, refreshLocale, this, true);
        cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_MEDIUM.key, refreshLocale, this, true);
        cfg.subscribeToConfigEvent(DEF_CFG.WEEKDAYS_LONG.key, refreshLocale, this, true);
       
        /**
        * The setting that determines which length of month labels should be used. Possible values are "short" and "long".
        * @config LOCALE_MONTHS
        * @type String
        * @default "long"
        */ 
        cfg.addProperty(DEF_CFG.LOCALE_MONTHS.key, { value:DEF_CFG.LOCALE_MONTHS.value, handler:this.configLocaleValues } );
        
        /**
        * The setting that determines which length of weekday labels should be used. Possible values are "1char", "short", "medium", and "long".
        * @config LOCALE_WEEKDAYS
        * @type String
        * @default "short"
        */ 
        cfg.addProperty(DEF_CFG.LOCALE_WEEKDAYS.key, { value:DEF_CFG.LOCALE_WEEKDAYS.value, handler:this.configLocaleValues } );

        /**
        * The positive or negative year offset from the Gregorian calendar year (assuming a January 1st rollover) to 
        * be used when displaying and parsing dates. NOTE: All JS Date objects returned by methods, or expected as input by
        * methods will always represent the Gregorian year, in order to maintain date/month/week values. 
        *
        * @config YEAR_OFFSET
        * @type Number
        * @default 0
        */
        cfg.addProperty(DEF_CFG.YEAR_OFFSET.key, { value:DEF_CFG.YEAR_OFFSET.value, supercedes:DEF_CFG.YEAR_OFFSET.supercedes, handler:this.configLocale  } );
    
        /**
        * The value used to delimit individual dates in a date string passed to various Calendar functions.
        * @config DATE_DELIMITER
        * @type String
        * @default ","
        */ 
        cfg.addProperty(DEF_CFG.DATE_DELIMITER.key,  { value:DEF_CFG.DATE_DELIMITER.value, handler:this.configLocale } );
    
        /**
        * The value used to delimit date fields in a date string passed to various Calendar functions.
        * @config DATE_FIELD_DELIMITER
        * @type String
        * @default "/"
        */ 
        cfg.addProperty(DEF_CFG.DATE_FIELD_DELIMITER.key, { value:DEF_CFG.DATE_FIELD_DELIMITER.value, handler:this.configLocale } );
    
        /**
        * The value used to delimit date ranges in a date string passed to various Calendar functions.
        * @config DATE_RANGE_DELIMITER
        * @type String
        * @default "-"
        */
        cfg.addProperty(DEF_CFG.DATE_RANGE_DELIMITER.key, { value:DEF_CFG.DATE_RANGE_DELIMITER.value, handler:this.configLocale } );
    
        /**
        * The position of the month in a month/year date string
        * @config MY_MONTH_POSITION
        * @type Number
        * @default 1
        */
        cfg.addProperty(DEF_CFG.MY_MONTH_POSITION.key, { value:DEF_CFG.MY_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
    
        /**
        * The position of the year in a month/year date string
        * @config MY_YEAR_POSITION
        * @type Number
        * @default 2
        */
        cfg.addProperty(DEF_CFG.MY_YEAR_POSITION.key, { value:DEF_CFG.MY_YEAR_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
    
        /**
        * The position of the month in a month/day date string
        * @config MD_MONTH_POSITION
        * @type Number
        * @default 1
        */
        cfg.addProperty(DEF_CFG.MD_MONTH_POSITION.key, { value:DEF_CFG.MD_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
    
        /**
        * The position of the day in a month/year date string
        * @config MD_DAY_POSITION
        * @type Number
        * @default 2
        */
        cfg.addProperty(DEF_CFG.MD_DAY_POSITION.key,  { value:DEF_CFG.MD_DAY_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
    
        /**
        * The position of the month in a month/day/year date string
        * @config MDY_MONTH_POSITION
        * @type Number
        * @default 1
        */
        cfg.addProperty(DEF_CFG.MDY_MONTH_POSITION.key, { value:DEF_CFG.MDY_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
    
        /**
        * The position of the day in a month/day/year date string
        * @config MDY_DAY_POSITION
        * @type Number
        * @default 2
        */
        cfg.addProperty(DEF_CFG.MDY_DAY_POSITION.key, { value:DEF_CFG.MDY_DAY_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
    
        /**
        * The position of the year in a month/day/year date string
        * @config MDY_YEAR_POSITION
        * @type Number
        * @default 3
        */
        cfg.addProperty(DEF_CFG.MDY_YEAR_POSITION.key, { value:DEF_CFG.MDY_YEAR_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
        
        /**
        * The position of the month in the month year label string used as the Calendar header
        * @config MY_LABEL_MONTH_POSITION
        * @type Number
        * @default 1
        */
        cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_POSITION.key, { value:DEF_CFG.MY_LABEL_MONTH_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
    
        /**
        * The position of the year in the month year label string used as the Calendar header
        * @config MY_LABEL_YEAR_POSITION
        * @type Number
        * @default 2
        */
        cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_POSITION.key, { value:DEF_CFG.MY_LABEL_YEAR_POSITION.value, handler:this.configLocale, validator:cfg.checkNumber } );
        
        /**
        * The suffix used after the month when rendering the Calendar header. The suffix is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config MY_LABEL_MONTH_SUFFIX
        * @type HTML
        * @default " "
        */
        cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_SUFFIX.key, { value:DEF_CFG.MY_LABEL_MONTH_SUFFIX.value, handler:this.configLocale } );
        
        /**
        * The suffix used after the year when rendering the Calendar header. The suffix is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config MY_LABEL_YEAR_SUFFIX
        * @type HTML
        * @default ""
        */
        cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_SUFFIX.key, { value:DEF_CFG.MY_LABEL_YEAR_SUFFIX.value, handler:this.configLocale } );

        /**
        * Configuration for the Month/Year CalendarNavigator UI which allows the user to jump directly to a 
        * specific Month/Year without having to scroll sequentially through months.
        * <p>
        * Setting this property to null (default value) or false, will disable the CalendarNavigator UI.
        * </p>
        * <p>
        * Setting this property to true will enable the CalendarNavigatior UI with the default CalendarNavigator configuration values.
        * </p>
        * <p>
        * This property can also be set to an object literal containing configuration properties for the CalendarNavigator UI.
        * The configuration object expects the the following case-sensitive properties, with the "strings" property being a nested object.
        * Any properties which are not provided will use the default values (defined in the CalendarNavigator class).
        * </p>
        * <dl>
        * <dt>strings</dt>
        * <dd><em>Object</em> :  An object with the properties shown below, defining the string labels to use in the Navigator's UI. The strings are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source. 
        *     <dl>
        *         <dt>month</dt><dd><em>HTML</em> : The markup to use for the month label. Defaults to "Month".</dd>
        *         <dt>year</dt><dd><em>HTML</em> : The markup to use for the year label. Defaults to "Year".</dd>
        *         <dt>submit</dt><dd><em>HTML</em> : The markup to use for the submit button label. Defaults to "Okay".</dd>
        *         <dt>cancel</dt><dd><em>HTML</em> : The markup to use for the cancel button label. Defaults to "Cancel".</dd>
        *         <dt>invalidYear</dt><dd><em>HTML</em> : The markup to use for invalid year values. Defaults to "Year needs to be a number".</dd>
        *     </dl>
        * </dd>
        * <dt>monthFormat</dt><dd><em>String</em> : The month format to use. Either YAHOO.widget.Calendar.LONG, or YAHOO.widget.Calendar.SHORT. Defaults to YAHOO.widget.Calendar.LONG</dd>
        * <dt>initialFocus</dt><dd><em>String</em> : Either "year" or "month" specifying which input control should get initial focus. Defaults to "year"</dd>
        * </dl>
        * <p>E.g.</p>
        * <pre>
        * var navConfig = {
        *   strings: {
        *    month:"Calendar Month",
        *    year:"Calendar Year",
        *    submit: "Submit",
        *    cancel: "Cancel",
        *    invalidYear: "Please enter a valid year"
        *   },
        *   monthFormat: YAHOO.widget.Calendar.SHORT,
        *   initialFocus: "month"
        * }
        * </pre>
        * @config navigator
        * @type {Object|Boolean}
        * @default null
        */
        cfg.addProperty(DEF_CFG.NAV.key, { value:DEF_CFG.NAV.value, handler:this.configNavigator } );

        /**
         * The map of UI strings which the Calendar UI uses.
         *
         * @config strings
         * @type {Object}
         * @default An object with the properties shown below:
         *     <dl>
         *         <dt>previousMonth</dt><dd><em>HTML</em> : The markup to use for the "Previous Month" navigation label. Defaults to "Previous Month". The string is added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.</dd>
         *         <dt>nextMonth</dt><dd><em>HTML</em> : The markup to use for the "Next Month" navigation UI. Defaults to "Next Month". The string is added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.</dd>
         *         <dt>close</dt><dd><em>HTML</em> : The markup to use for the close button label. Defaults to "Close". The string is added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.</dd>
         *     </dl>
         */
        cfg.addProperty(DEF_CFG.STRINGS.key, { 
            value:DEF_CFG.STRINGS.value,
            handler:this.configStrings,
            validator: function(val) {
                return Lang.isObject(val);
            },
            supercedes:DEF_CFG.STRINGS.supercedes
        });
    },

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

    /**
    * The default handler for the "pagedate" property
    * @method configPageDate
    */
    configPageDate : function(type, args, obj) {
        this.cfg.setProperty(DEF_CFG.PAGEDATE.key, this._parsePageDate(args[0]), true);
    },

    /**
    * The default handler for the "mindate" property
    * @method configMinDate
    */
    configMinDate : function(type, args, obj) {
        var val = args[0];
        if (Lang.isString(val)) {
            val = this._parseDate(val);
            this.cfg.setProperty(DEF_CFG.MINDATE.key, DateMath.getDate(val[0],(val[1]-1),val[2]));
        }
    },

    /**
    * The default handler for the "maxdate" property
    * @method configMaxDate
    */
    configMaxDate : function(type, args, obj) {
        var val = args[0];
        if (Lang.isString(val)) {
            val = this._parseDate(val);
            this.cfg.setProperty(DEF_CFG.MAXDATE.key, DateMath.getDate(val[0],(val[1]-1),val[2]));
        }
    },

    /**
    * The default handler for the "today" property
    * @method configToday
    */
    configToday : function(type, args, obj) {
        // Only do this for initial set. Changing the today property after the initial
        // set, doesn't affect pagedate
        var val = args[0];
        if (Lang.isString(val)) {
            val = this._parseDate(val);
        }
        var today = DateMath.clearTime(val);
        if (!this.cfg.initialConfig[DEF_CFG.PAGEDATE.key]) {
            this.cfg.setProperty(DEF_CFG.PAGEDATE.key, today);
        }
        this.today = today;
        this.cfg.setProperty(DEF_CFG.TODAY.key, today, true);
    },

    /**
    * The default handler for the "selected" property
    * @method configSelected
    */
    configSelected : function(type, args, obj) {
        var selected = args[0],
            cfgSelected = DEF_CFG.SELECTED.key;
        
        if (selected) {
            if (Lang.isString(selected)) {
                this.cfg.setProperty(cfgSelected, this._parseDates(selected), true);
            } 
        }
        if (! this._selectedDates) {
            this._selectedDates = this.cfg.getProperty(cfgSelected);
        }
    },
    
    /**
    * The default handler for all configuration options properties
    * @method configOptions
    */
    configOptions : function(type, args, obj) {
        this.Options[type.toUpperCase()] = args[0];
    },

    /**
    * The default handler for all configuration locale properties
    * @method configLocale
    */
    configLocale : function(type, args, obj) {
        this.Locale[type.toUpperCase()] = args[0];

        this.cfg.refireEvent(DEF_CFG.LOCALE_MONTHS.key);
        this.cfg.refireEvent(DEF_CFG.LOCALE_WEEKDAYS.key);
    },
    
    /**
    * The default handler for all configuration locale field length properties
    * @method configLocaleValues
    */
    configLocaleValues : function(type, args, obj) {

        type = type.toLowerCase();

        var val = args[0],
            cfg = this.cfg,
            Locale = this.Locale;

        switch (type) {
            case DEF_CFG.LOCALE_MONTHS.key:
                switch (val) {
                    case Calendar.SHORT:
                        Locale.LOCALE_MONTHS = cfg.getProperty(DEF_CFG.MONTHS_SHORT.key).concat();
                        break;
                    case Calendar.LONG:
                        Locale.LOCALE_MONTHS = cfg.getProperty(DEF_CFG.MONTHS_LONG.key).concat();
                        break;
                }
                break;
            case DEF_CFG.LOCALE_WEEKDAYS.key:
                switch (val) {
                    case Calendar.ONE_CHAR:
                        Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_1CHAR.key).concat();
                        break;
                    case Calendar.SHORT:
                        Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_SHORT.key).concat();
                        break;
                    case Calendar.MEDIUM:
                        Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_MEDIUM.key).concat();
                        break;
                    case Calendar.LONG:
                        Locale.LOCALE_WEEKDAYS = cfg.getProperty(DEF_CFG.WEEKDAYS_LONG.key).concat();
                        break;
                }
                
                var START_WEEKDAY = cfg.getProperty(DEF_CFG.START_WEEKDAY.key);
    
                if (START_WEEKDAY > 0) {
                    for (var w=0; w < START_WEEKDAY; ++w) {
                        Locale.LOCALE_WEEKDAYS.push(Locale.LOCALE_WEEKDAYS.shift());
                    }
                }
                break;
        }
    },

    /**
     * The default handler for the "navigator" property
     * @method configNavigator
     */
    configNavigator : function(type, args, obj) {
        var val = args[0];
        if (YAHOO.widget.CalendarNavigator && (val === true || Lang.isObject(val))) {
            if (!this.oNavigator) {
                this.oNavigator = new YAHOO.widget.CalendarNavigator(this);
                // Cleanup DOM Refs/Events before innerHTML is removed.
                this.beforeRenderEvent.subscribe(function () {
                    if (!this.pages) {
                        this.oNavigator.erase();
                    }
                }, this, true);
            }
        } else {
            if (this.oNavigator) {
                this.oNavigator.destroy();
                this.oNavigator = null;
            }
        }
    },

    /**
    * Defines the class names used by Calendar when rendering to DOM. NOTE: The class names are added to the DOM as HTML and should be escaped by the implementor if coming from an external source. 
    * @method initStyles
    */
    initStyles : function() {

        var defStyle = Calendar.STYLES;

        this.Style = {
            /**
            * @property Style.CSS_ROW_HEADER
            */
            CSS_ROW_HEADER: defStyle.CSS_ROW_HEADER,
            /**
            * @property Style.CSS_ROW_FOOTER
            */
            CSS_ROW_FOOTER: defStyle.CSS_ROW_FOOTER,
            /**
            * @property Style.CSS_CELL
            */
            CSS_CELL : defStyle.CSS_CELL,
            /**
            * @property Style.CSS_CELL_SELECTOR
            */
            CSS_CELL_SELECTOR : defStyle.CSS_CELL_SELECTOR,
            /**
            * @property Style.CSS_CELL_SELECTED
            */
            CSS_CELL_SELECTED : defStyle.CSS_CELL_SELECTED,
            /**
            * @property Style.CSS_CELL_SELECTABLE
            */
            CSS_CELL_SELECTABLE : defStyle.CSS_CELL_SELECTABLE,
            /**
            * @property Style.CSS_CELL_RESTRICTED
            */
            CSS_CELL_RESTRICTED : defStyle.CSS_CELL_RESTRICTED,
            /**
            * @property Style.CSS_CELL_TODAY
            */
            CSS_CELL_TODAY : defStyle.CSS_CELL_TODAY,
            /**
            * @property Style.CSS_CELL_OOM
            */
            CSS_CELL_OOM : defStyle.CSS_CELL_OOM,
            /**
            * @property Style.CSS_CELL_OOB
            */
            CSS_CELL_OOB : defStyle.CSS_CELL_OOB,
            /**
            * @property Style.CSS_HEADER
            */
            CSS_HEADER : defStyle.CSS_HEADER,
            /**
            * @property Style.CSS_HEADER_TEXT
            */
            CSS_HEADER_TEXT : defStyle.CSS_HEADER_TEXT,
            /**
            * @property Style.CSS_BODY
            */
            CSS_BODY : defStyle.CSS_BODY,
            /**
            * @property Style.CSS_WEEKDAY_CELL
            */
            CSS_WEEKDAY_CELL : defStyle.CSS_WEEKDAY_CELL,
            /**
            * @property Style.CSS_WEEKDAY_ROW
            */
            CSS_WEEKDAY_ROW : defStyle.CSS_WEEKDAY_ROW,
            /**
            * @property Style.CSS_FOOTER
            */
            CSS_FOOTER : defStyle.CSS_FOOTER,
            /**
            * @property Style.CSS_CALENDAR
            */
            CSS_CALENDAR : defStyle.CSS_CALENDAR,
            /**
            * @property Style.CSS_SINGLE
            */
            CSS_SINGLE : defStyle.CSS_SINGLE,
            /**
            * @property Style.CSS_CONTAINER
            */
            CSS_CONTAINER : defStyle.CSS_CONTAINER,
            /**
            * @property Style.CSS_NAV_LEFT
            */
            CSS_NAV_LEFT : defStyle.CSS_NAV_LEFT,
            /**
            * @property Style.CSS_NAV_RIGHT
            */
            CSS_NAV_RIGHT : defStyle.CSS_NAV_RIGHT,
            /**
            * @property Style.CSS_NAV
            */
            CSS_NAV : defStyle.CSS_NAV,
            /**
            * @property Style.CSS_CLOSE
            */
            CSS_CLOSE : defStyle.CSS_CLOSE,
            /**
            * @property Style.CSS_CELL_TOP
            */
            CSS_CELL_TOP : defStyle.CSS_CELL_TOP,
            /**
            * @property Style.CSS_CELL_LEFT
            */
            CSS_CELL_LEFT : defStyle.CSS_CELL_LEFT,
            /**
            * @property Style.CSS_CELL_RIGHT
            */
            CSS_CELL_RIGHT : defStyle.CSS_CELL_RIGHT,
            /**
            * @property Style.CSS_CELL_BOTTOM
            */
            CSS_CELL_BOTTOM : defStyle.CSS_CELL_BOTTOM,
            /**
            * @property Style.CSS_CELL_HOVER
            */
            CSS_CELL_HOVER : defStyle.CSS_CELL_HOVER,
            /**
            * @property Style.CSS_CELL_HIGHLIGHT1
            */
            CSS_CELL_HIGHLIGHT1 : defStyle.CSS_CELL_HIGHLIGHT1,
            /**
            * @property Style.CSS_CELL_HIGHLIGHT2
            */
            CSS_CELL_HIGHLIGHT2 : defStyle.CSS_CELL_HIGHLIGHT2,
            /**
            * @property Style.CSS_CELL_HIGHLIGHT3
            */
            CSS_CELL_HIGHLIGHT3 : defStyle.CSS_CELL_HIGHLIGHT3,
            /**
            * @property Style.CSS_CELL_HIGHLIGHT4
            */
            CSS_CELL_HIGHLIGHT4 : defStyle.CSS_CELL_HIGHLIGHT4,
            /**
             * @property Style.CSS_WITH_TITLE
             */
            CSS_WITH_TITLE : defStyle.CSS_WITH_TITLE,
             /**
             * @property Style.CSS_FIXED_SIZE
             */
            CSS_FIXED_SIZE : defStyle.CSS_FIXED_SIZE,
             /**
             * @property Style.CSS_LINK_CLOSE
             */
            CSS_LINK_CLOSE : defStyle.CSS_LINK_CLOSE
        };
    },

    /**
    * Builds the date label that will be displayed in the calendar header or
    * footer, depending on configuration.
    * @method buildMonthLabel
    * @return {HTML} The formatted calendar month label
    */
    buildMonthLabel : function() {
        return this._buildMonthLabel(this.cfg.getProperty(DEF_CFG.PAGEDATE.key));
    },

    /**
     * Helper method, to format a Month Year string, given a JavaScript Date, based on the 
     * Calendar localization settings
     * 
     * @method _buildMonthLabel
     * @private
     * @param {Date} date
     * @return {HTML} Formated month, year string
     */
    _buildMonthLabel : function(date) {
        var monthLabel  = this.Locale.LOCALE_MONTHS[date.getMonth()] + this.Locale.MY_LABEL_MONTH_SUFFIX,
            yearLabel = (date.getFullYear() + this.Locale.YEAR_OFFSET) + this.Locale.MY_LABEL_YEAR_SUFFIX;

        if (this.Locale.MY_LABEL_MONTH_POSITION == 2 || this.Locale.MY_LABEL_YEAR_POSITION == 1) {
            return yearLabel + monthLabel;
        } else {
            return monthLabel + yearLabel;
        }
    },

    /**
    * Builds the date digit that will be displayed in calendar cells
    * @method buildDayLabel
    * @param {Date} workingDate The current working date
    * @return {Number} The day
    */
    buildDayLabel : function(workingDate) {
        return workingDate.getDate();
    },

    /**
     * Creates the title bar element and adds it to Calendar container DIV. NOTE: The title parameter passed into this method is added to the DOM as HTML and should be escaped by the implementor if coming from an external source.  
     * 
     * @method createTitleBar
     * @param {HTML} strTitle The title to display in the title bar
     * @return The title bar element
     */
    createTitleBar : function(strTitle) {
        var tDiv = Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE, "div", this.oDomContainer)[0] || document.createElement("div");
        tDiv.className = YAHOO.widget.CalendarGroup.CSS_2UPTITLE;
        tDiv.innerHTML = strTitle;
        this.oDomContainer.insertBefore(tDiv, this.oDomContainer.firstChild);
    
        Dom.addClass(this.oDomContainer, this.Style.CSS_WITH_TITLE);
    
        return tDiv;
    },
    
    /**
     * Removes the title bar element from the DOM
     * 
     * @method removeTitleBar
     */
    removeTitleBar : function() {
        var tDiv = Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE, "div", this.oDomContainer)[0] || null;
        if (tDiv) {
            Event.purgeElement(tDiv);
            this.oDomContainer.removeChild(tDiv);
        }
        Dom.removeClass(this.oDomContainer, this.Style.CSS_WITH_TITLE);
    },

    /**
     * Creates the close button HTML element and adds it to Calendar container DIV
     * 
     * @method createCloseButton
     * @return {HTMLElement} The close HTML element created
     */
    createCloseButton : function() {
        var cssClose = YAHOO.widget.CalendarGroup.CSS_2UPCLOSE,
            cssLinkClose = this.Style.CSS_LINK_CLOSE,
            DEPR_CLOSE_PATH = "us/my/bn/x_d.gif",

            lnk = Dom.getElementsByClassName(cssLinkClose, "a", this.oDomContainer)[0],
            strings = this.cfg.getProperty(DEF_CFG.STRINGS.key),
            closeStr = (strings && strings.close) ? strings.close : "";

        if (!lnk) {
            lnk = document.createElement("a");
            Event.addListener(lnk, "click", function(e, cal) {
                cal.hide(); 
                Event.preventDefault(e);
            }, this);
        }

        lnk.href = "#";
        lnk.className = cssLinkClose;

        if (Calendar.IMG_ROOT !== null) {
            var img = Dom.getElementsByClassName(cssClose, "img", lnk)[0] || document.createElement("img");
            img.src = Calendar.IMG_ROOT + DEPR_CLOSE_PATH;
            img.className = cssClose;
            lnk.appendChild(img);
        } else {
            lnk.innerHTML = '<span class="' + cssClose + ' ' + this.Style.CSS_CLOSE + '">' + closeStr + '</span>';
        }
        this.oDomContainer.appendChild(lnk);

        return lnk;
    },
    
    /**
     * Removes the close button HTML element from the DOM
     * 
     * @method removeCloseButton
     */
    removeCloseButton : function() {
        var btn = Dom.getElementsByClassName(this.Style.CSS_LINK_CLOSE, "a", this.oDomContainer)[0] || null;
        if (btn) {
            Event.purgeElement(btn);
            this.oDomContainer.removeChild(btn);
        }
    },

    /**
    * Renders the calendar header. NOTE: The contents of the array passed into this method are added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.
    * @method renderHeader
    * @param {HTML[]} html The current working HTML array
    * @return {HTML[]} The current working HTML array
    */
    renderHeader : function(html) {

        this.logger.log("Rendering header", "render");

        var colSpan = 7,
            DEPR_NAV_LEFT = "us/tr/callt.gif",
            DEPR_NAV_RIGHT = "us/tr/calrt.gif",
            cfg = this.cfg,
            pageDate = cfg.getProperty(DEF_CFG.PAGEDATE.key),
            strings= cfg.getProperty(DEF_CFG.STRINGS.key),
            prevStr = (strings && strings.previousMonth) ?  strings.previousMonth : "",
            nextStr = (strings && strings.nextMonth) ? strings.nextMonth : "",
            monthLabel;

        if (cfg.getProperty(DEF_CFG.SHOW_WEEK_HEADER.key)) {
            colSpan += 1;
        }
    
        if (cfg.getProperty(DEF_CFG.SHOW_WEEK_FOOTER.key)) {
            colSpan += 1;
        }

        html[html.length] = "<thead>";
        html[html.length] =  "<tr>";
        html[html.length] =   '<th colspan="' + colSpan + '" class="' + this.Style.CSS_HEADER_TEXT + '">';
        html[html.length] =    '<div class="' + this.Style.CSS_HEADER + '">';

        var renderLeft, renderRight = false;

        if (this.parent) {
            if (this.index === 0) {
                renderLeft = true;
            }
            if (this.index == (this.parent.cfg.getProperty("pages") -1)) {
                renderRight = true;
            }
        } else {
            renderLeft = true;
            renderRight = true;
        }

        if (renderLeft) {
            monthLabel  = this._buildMonthLabel(DateMath.subtract(pageDate, DateMath.MONTH, 1));

            var leftArrow = cfg.getProperty(DEF_CFG.NAV_ARROW_LEFT.key);
            // Check for deprecated customization - If someone set IMG_ROOT, but didn't set NAV_ARROW_LEFT, then set NAV_ARROW_LEFT to the old deprecated value
            if (leftArrow === null && Calendar.IMG_ROOT !== null) {
                leftArrow = Calendar.IMG_ROOT + DEPR_NAV_LEFT;
            }
            var leftStyle = (leftArrow === null) ? "" : ' style="background-image:url(' + leftArrow + ')"';
            html[html.length] = '<a class="' + this.Style.CSS_NAV_LEFT + '"' + leftStyle + ' href="#">' + prevStr + ' (' + monthLabel + ')' + '</a>';
        }

        var lbl = this.buildMonthLabel();
        var cal = this.parent || this;
        if (cal.cfg.getProperty("navigator")) {
            lbl = "<a class=\"" + this.Style.CSS_NAV + "\" href=\"#\">" + lbl + "</a>";
        }
        html[html.length] = lbl;

        if (renderRight) {
            monthLabel  = this._buildMonthLabel(DateMath.add(pageDate, DateMath.MONTH, 1));

            var rightArrow = cfg.getProperty(DEF_CFG.NAV_ARROW_RIGHT.key);
            if (rightArrow === null && Calendar.IMG_ROOT !== null) {
                rightArrow = Calendar.IMG_ROOT + DEPR_NAV_RIGHT;
            }
            var rightStyle = (rightArrow === null) ? "" : ' style="background-image:url(' + rightArrow + ')"';
            html[html.length] = '<a class="' + this.Style.CSS_NAV_RIGHT + '"' + rightStyle + ' href="#">' + nextStr + ' (' + monthLabel + ')' + '</a>';
        }

        html[html.length] = '</div>\n</th>\n</tr>';

        if (cfg.getProperty(DEF_CFG.SHOW_WEEKDAYS.key)) {
            html = this.buildWeekdays(html);
        }
        
        html[html.length] = '</thead>';
    
        return html;
    },

    /**
    * Renders the Calendar's weekday headers. NOTE: The contents of the array passed into this method are added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.
    * @method buildWeekdays
    * @param {HTML[]} html The current working HTML array
    * @return {HTML[]} The current working HTML array
    */
    buildWeekdays : function(html) {

        html[html.length] = '<tr class="' + this.Style.CSS_WEEKDAY_ROW + '">';

        if (this.cfg.getProperty(DEF_CFG.SHOW_WEEK_HEADER.key)) {
            html[html.length] = '<th>&#160;</th>';
        }

        for(var i=0;i < this.Locale.LOCALE_WEEKDAYS.length; ++i) {
            html[html.length] = '<th class="' + this.Style.CSS_WEEKDAY_CELL + '">' + this.Locale.LOCALE_WEEKDAYS[i] + '</th>';
        }

        if (this.cfg.getProperty(DEF_CFG.SHOW_WEEK_FOOTER.key)) {
            html[html.length] = '<th>&#160;</th>';
        }

        html[html.length] = '</tr>';

        return html;
    },
    
    /**
    * Renders the calendar body. NOTE: The contents of the array passed into this method are added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.
    * @method renderBody
    * @param {Date} workingDate The current working Date being used for the render process
    * @param {HTML[]} html The current working HTML array
    * @return {HTML[]} The current working HTML array
    */
    renderBody : function(workingDate, html) {
        this.logger.log("Rendering body", "render");

        var startDay = this.cfg.getProperty(DEF_CFG.START_WEEKDAY.key);

        this.preMonthDays = workingDate.getDay();
        if (startDay > 0) {
            this.preMonthDays -= startDay;
        }
        if (this.preMonthDays < 0) {
            this.preMonthDays += 7;
        }

        this.monthDays = DateMath.findMonthEnd(workingDate).getDate();
        this.postMonthDays = Calendar.DISPLAY_DAYS-this.preMonthDays-this.monthDays;

        this.logger.log(this.preMonthDays + " preciding out-of-month days", "render");
        this.logger.log(this.monthDays + " month days", "render");
        this.logger.log(this.postMonthDays + " post-month days", "render");

        workingDate = DateMath.subtract(workingDate, DateMath.DAY, this.preMonthDays);
        this.logger.log("Calendar page starts on " + workingDate, "render");
    
        var weekNum,
            weekClass,
            weekPrefix = "w",
            cellPrefix = "_cell",
            workingDayPrefix = "wd",
            dayPrefix = "d",
            cellRenderers,
            renderer,
            t = this.today,
            cfg = this.cfg,
            oom,
            todayYear = t.getFullYear(),
            todayMonth = t.getMonth(),
            todayDate = t.getDate(),
            useDate = cfg.getProperty(DEF_CFG.PAGEDATE.key),
            hideBlankWeeks = cfg.getProperty(DEF_CFG.HIDE_BLANK_WEEKS.key),
            showWeekFooter = cfg.getProperty(DEF_CFG.SHOW_WEEK_FOOTER.key),
            showWeekHeader = cfg.getProperty(DEF_CFG.SHOW_WEEK_HEADER.key),
            oomSelect = cfg.getProperty(DEF_CFG.OOM_SELECT.key),
            mindate = cfg.getProperty(DEF_CFG.MINDATE.key),
            maxdate = cfg.getProperty(DEF_CFG.MAXDATE.key),
            yearOffset = this.Locale.YEAR_OFFSET;

        if (mindate) {
            mindate = DateMath.clearTime(mindate);
        }
        if (maxdate) {
            maxdate = DateMath.clearTime(maxdate);
        }

        html[html.length] = '<tbody class="m' + (useDate.getMonth()+1) + ' ' + this.Style.CSS_BODY + '">';

        var i = 0,
            tempDiv = document.createElement("div"),
            cell = document.createElement("td");

        tempDiv.appendChild(cell);

        var cal = this.parent || this;

        for (var r = 0; r < 6; r++) {
            weekNum = DateMath.getWeekNumber(workingDate, startDay);
            weekClass = weekPrefix + weekNum;

            // Local OOM check for performance, since we already have pagedate
            if (r !== 0 && hideBlankWeeks === true && workingDate.getMonth() != useDate.getMonth()) {
                break;
            } else {
                html[html.length] = '<tr class="' + weekClass + '">';

                if (showWeekHeader) { html = this.renderRowHeader(weekNum, html); }

                for (var d=0; d < 7; d++){ // Render actual days

                    cellRenderers = [];

                    this.clearElement(cell);
                    cell.className = this.Style.CSS_CELL;
                    cell.id = this.id + cellPrefix + i;
                    this.logger.log("Rendering cell " + cell.id + " (" + workingDate.getFullYear() + yearOffset + "-" + (workingDate.getMonth()+1) + "-" + workingDate.getDate() + ")", "cellrender");

                    if (workingDate.getDate()  == todayDate && 
                        workingDate.getMonth()  == todayMonth &&
                        workingDate.getFullYear() == todayYear) {
                        cellRenderers[cellRenderers.length]=cal.renderCellStyleToday;
                    }

                    var workingArray = [workingDate.getFullYear(),workingDate.getMonth()+1,workingDate.getDate()];
                    this.cellDates[this.cellDates.length] = workingArray; // Add this date to cellDates

                    // Local OOM check for performance, since we already have pagedate
                    oom = workingDate.getMonth() != useDate.getMonth(); 
                    if (oom && !oomSelect) {
                        cellRenderers[cellRenderers.length]=cal.renderCellNotThisMonth;
                    } else {
                        Dom.addClass(cell, workingDayPrefix + workingDate.getDay());
                        Dom.addClass(cell, dayPrefix + workingDate.getDate());

                        // Concat, so that we're not splicing from an array 
                        // which we're also iterating
                        var rs = this.renderStack.concat();

                        for (var s=0, l = rs.length; s < l; ++s) {

                            renderer = null;

                            var rArray = rs[s],
                                type = rArray[0],
                                month,
                                day,
                                year;

                            switch (type) {
                                case Calendar.DATE:
                                    month = rArray[1][1];
                                    day = rArray[1][2];
                                    year = rArray[1][0];

                                    if (workingDate.getMonth()+1 == month && workingDate.getDate() == day && workingDate.getFullYear() == year) {
                                        renderer = rArray[2];
                                        this.renderStack.splice(s,1);
                                    }

                                    break;
                                case Calendar.MONTH_DAY:
                                    month = rArray[1][0];
                                    day = rArray[1][1];

                                    if (workingDate.getMonth()+1 == month && workingDate.getDate() == day) {
                                        renderer = rArray[2];
                                        this.renderStack.splice(s,1);
                                    }
                                    break;
                                case Calendar.RANGE:
                                    var date1 = rArray[1][0],
                                        date2 = rArray[1][1],
                                        d1month = date1[1],
                                        d1day = date1[2],
                                        d1year = date1[0],
                                        d1 = DateMath.getDate(d1year, d1month-1, d1day),
                                        d2month = date2[1],
                                        d2day = date2[2],
                                        d2year = date2[0],
                                        d2 = DateMath.getDate(d2year, d2month-1, d2day);

                                    if (workingDate.getTime() >= d1.getTime() && workingDate.getTime() <= d2.getTime()) {
                                        renderer = rArray[2];

                                        if (workingDate.getTime()==d2.getTime()) { 
                                            this.renderStack.splice(s,1);
                                        }
                                    }
                                    break;
                                case Calendar.WEEKDAY:
                                    var weekday = rArray[1][0];
                                    if (workingDate.getDay()+1 == weekday) {
                                        renderer = rArray[2];
                                    }
                                    break;
                                case Calendar.MONTH:
                                    month = rArray[1][0];
                                    if (workingDate.getMonth()+1 == month) {
                                        renderer = rArray[2];
                                    }
                                    break;
                            }

                            if (renderer) {
                                cellRenderers[cellRenderers.length]=renderer;
                            }
                        }

                    }

                    if (this._indexOfSelectedFieldArray(workingArray) > -1) {
                        cellRenderers[cellRenderers.length]=cal.renderCellStyleSelected; 
                    }

                    if (oom) {
                        cellRenderers[cellRenderers.length] = cal.styleCellNotThisMonth; 
                    }

                    if ((mindate && (workingDate.getTime() < mindate.getTime())) || (maxdate && (workingDate.getTime() > maxdate.getTime()))) {
                        cellRenderers[cellRenderers.length] = cal.renderOutOfBoundsDate;
                    } else {
                        cellRenderers[cellRenderers.length] = cal.styleCellDefault;
                        cellRenderers[cellRenderers.length] = cal.renderCellDefault;
                    }

                    for (var x=0; x < cellRenderers.length; ++x) {
                        this.logger.log("renderer[" + x + "] for (" + workingDate.getFullYear() + yearOffset + "-" + (workingDate.getMonth()+1) + "-" + workingDate.getDate() + ")", "cellrender");
                        if (cellRenderers[x].call(cal, workingDate, cell) == Calendar.STOP_RENDER) {
                            break;
                        }
                    }

                    workingDate.setTime(workingDate.getTime() + DateMath.ONE_DAY_MS);
                    // Just in case we crossed DST/Summertime boundaries
                    workingDate = DateMath.clearTime(workingDate);

                    if (i >= 0 && i <= 6) {
                        Dom.addClass(cell, this.Style.CSS_CELL_TOP);
                    }
                    if ((i % 7) === 0) {
                        Dom.addClass(cell, this.Style.CSS_CELL_LEFT);
                    }
                    if (((i+1) % 7) === 0) {
                        Dom.addClass(cell, this.Style.CSS_CELL_RIGHT);
                    }

                    var postDays = this.postMonthDays; 
                    if (hideBlankWeeks && postDays >= 7) {
                        var blankWeeks = Math.floor(postDays/7);
                        for (var p=0;p<blankWeeks;++p) {
                            postDays -= 7;
                        }
                    }
                    
                    if (i >= ((this.preMonthDays+postDays+this.monthDays)-7)) {
                        Dom.addClass(cell, this.Style.CSS_CELL_BOTTOM);
                    }
    
                    html[html.length] = tempDiv.innerHTML;
                    i++;
                }
    
                if (showWeekFooter) { html = this.renderRowFooter(weekNum, html); }
    
                html[html.length] = '</tr>';
            }
        }
    
        html[html.length] = '</tbody>';
    
        return html;
    },
    
    /**
    * Renders the calendar footer. In the default implementation, there is no footer. NOTE: The contents of the array passed into this method are added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.
    * @method renderFooter
    * @param {HTML[]} html The current working HTML array
    * @return {HTML[]} The current working HTML array
    */
    renderFooter : function(html) { return html; },
    
    /**
    * Renders the calendar after it has been configured. The render() method has a specific call chain that will execute
    * when the method is called: renderHeader, renderBody, renderFooter.
    * Refer to the documentation for those methods for information on individual render tasks.
    * @method render
    */
    render : function() {
        this.beforeRenderEvent.fire();

        // Find starting day of the current month
        var workingDate = DateMath.findMonthStart(this.cfg.getProperty(DEF_CFG.PAGEDATE.key));

        this.resetRenderers();
        this.cellDates.length = 0;

        Event.purgeElement(this.oDomContainer, true);

        var html = [], 
            table;

        html[html.length] = '<table cellSpacing="0" class="' + this.Style.CSS_CALENDAR + ' y' + (workingDate.getFullYear() + this.Locale.YEAR_OFFSET) +'" id="' + this.id + '">';
        html = this.renderHeader(html);
        html = this.renderBody(workingDate, html);
        html = this.renderFooter(html);
        html[html.length] = '</table>';

        this.oDomContainer.innerHTML = html.join("\n");

        this.applyListeners();

        // Using oDomContainer.ownerDocument, to allow for cross-frame rendering
        table = ((this._oDoc) && this._oDoc.getElementById(this.id)) || (this.id);

        this.cells = Dom.getElementsByClassName(this.Style.CSS_CELL, "td", table);

        this.cfg.refireEvent(DEF_CFG.TITLE.key);
        this.cfg.refireEvent(DEF_CFG.CLOSE.key);
        this.cfg.refireEvent(DEF_CFG.IFRAME.key);

        this.renderEvent.fire();
    },

    /**
    * Applies the Calendar's DOM listeners to applicable elements.
    * @method applyListeners
    */
    applyListeners : function() {
        var root = this.oDomContainer,
            cal = this.parent || this,
            anchor = "a",
            click = "click";

        var linkLeft = Dom.getElementsByClassName(this.Style.CSS_NAV_LEFT, anchor, root),
            linkRight = Dom.getElementsByClassName(this.Style.CSS_NAV_RIGHT, anchor, root);

        if (linkLeft && linkLeft.length > 0) {
            this.linkLeft = linkLeft[0];
            Event.addListener(this.linkLeft, click, this.doPreviousMonthNav, cal, true);
        }

        if (linkRight && linkRight.length > 0) {
            this.linkRight = linkRight[0];
            Event.addListener(this.linkRight, click, this.doNextMonthNav, cal, true);
        }

        if (cal.cfg.getProperty("navigator") !== null) {
            this.applyNavListeners();
        }

        if (this.domEventMap) {
            var el,elements;
            for (var cls in this.domEventMap) { 
                if (Lang.hasOwnProperty(this.domEventMap, cls)) {
                    var items = this.domEventMap[cls];
    
                    if (! (items instanceof Array)) {
                        items = [items];
                    }
    
                    for (var i=0;i<items.length;i++) {
                        var item = items[i];
                        elements = Dom.getElementsByClassName(cls, item.tag, this.oDomContainer);
    
                        for (var c=0;c<elements.length;c++) {
                            el = elements[c];
                             Event.addListener(el, item.event, item.handler, item.scope, item.correct );
                        }
                    }
                }
            }
        }

        Event.addListener(this.oDomContainer, "click", this.doSelectCell, this);
        Event.addListener(this.oDomContainer, "mouseover", this.doCellMouseOver, this);
        Event.addListener(this.oDomContainer, "mouseout", this.doCellMouseOut, this);
    },

    /**
    * Applies the DOM listeners to activate the Calendar Navigator.
    * @method applyNavListeners
    */
    applyNavListeners : function() {
        var calParent = this.parent || this,
            cal = this,
            navBtns = Dom.getElementsByClassName(this.Style.CSS_NAV, "a", this.oDomContainer);

        if (navBtns.length > 0) {

            Event.addListener(navBtns, "click", function (e, obj) {
                var target = Event.getTarget(e);
                // this == navBtn
                if (this === target || Dom.isAncestor(this, target)) {
                    Event.preventDefault(e);
                }
                var navigator = calParent.oNavigator;
                if (navigator) {
                    var pgdate = cal.cfg.getProperty("pagedate");
                    navigator.setYear(pgdate.getFullYear() + cal.Locale.YEAR_OFFSET);
                    navigator.setMonth(pgdate.getMonth());
                    navigator.show();
                }
            });
        }
    },

    /**
    * Retrieves the Date object for the specified Calendar cell
    * @method getDateByCellId
    * @param {String} id The id of the cell
    * @return {Date} The Date object for the specified Calendar cell
    */
    getDateByCellId : function(id) {
        var date = this.getDateFieldsByCellId(id);
        return (date) ? DateMath.getDate(date[0],date[1]-1,date[2]) : null;
    },
    
    /**
    * Retrieves the Date object for the specified Calendar cell
    * @method getDateFieldsByCellId
    * @param {String} id The id of the cell
    * @return {Array} The array of Date fields for the specified Calendar cell
    */
    getDateFieldsByCellId : function(id) {
        id = this.getIndexFromId(id);
        return (id > -1) ? this.cellDates[id] : null;
    },

    /**
     * Find the Calendar's cell index for a given date.
     * If the date is not found, the method returns -1.
     * <p>
     * The returned index can be used to lookup the cell HTMLElement  
     * using the Calendar's cells array or passed to selectCell to select 
     * cells by index. 
     * </p>
     *
     * See <a href="#cells">cells</a>, <a href="#selectCell">selectCell</a>.
     *
     * @method getCellIndex
     * @param {Date} date JavaScript Date object, for which to find a cell index.
     * @return {Number} The index of the date in Calendars cellDates/cells arrays, or -1 if the date 
     * is not on the curently rendered Calendar page.
     */
    getCellIndex : function(date) {
        var idx = -1;
        if (date) {
            var m = date.getMonth(),
                y = date.getFullYear(),
                d = date.getDate(),
                dates = this.cellDates;

            for (var i = 0; i < dates.length; ++i) {
                var cellDate = dates[i];
                if (cellDate[0] === y && cellDate[1] === m+1 && cellDate[2] === d) {
                    idx = i;
                    break;
                }
            }
        }
        return idx;
    },

    /**
     * Given the id used to mark each Calendar cell, this method
     * extracts the index number from the id.
     * 
     * @param {String} strId The cell id
     * @return {Number} The index of the cell, or -1 if id does not contain an index number
     */
    getIndexFromId : function(strId) {
        var idx = -1,
            li = strId.lastIndexOf("_cell");

        if (li > -1) {
            idx = parseInt(strId.substring(li + 5), 10);
        }

        return idx;
    },
    
    // BEGIN BUILT-IN TABLE CELL RENDERERS
    
    /**
    * Renders a cell that falls before the minimum date or after the maximum date.
    * @method renderOutOfBoundsDate
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering
    *   should not be terminated
    */
    renderOutOfBoundsDate : function(workingDate, cell) {
        Dom.addClass(cell, this.Style.CSS_CELL_OOB);
        cell.innerHTML = workingDate.getDate();
        return Calendar.STOP_RENDER;
    },

    /**
    * Renders the row header HTML for a week.
    *
    * @method renderRowHeader
    * @param {Number} weekNum The week number of the current row
    * @param {HTML[]} cell The current working HTML array
    */
    renderRowHeader : function(weekNum, html) {
        html[html.length] = '<th class="' + this.Style.CSS_ROW_HEADER + '">' + weekNum + '</th>';
        return html;
    },

    /**
    * Renders the row footer HTML for a week.
    *
    * @method renderRowFooter
    * @param {Number} weekNum The week number of the current row
    * @param {HTML[]} cell The current working HTML array
    */
    renderRowFooter : function(weekNum, html) {
        html[html.length] = '<th class="' + this.Style.CSS_ROW_FOOTER + '">' + weekNum + '</th>';
        return html;
    },

    /**
    * Renders a single standard calendar cell in the calendar widget table.
    *
    * All logic for determining how a standard default cell will be rendered is 
    * encapsulated in this method, and must be accounted for when extending the
    * widget class.
    *
    * @method renderCellDefault
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    */
    renderCellDefault : function(workingDate, cell) {
        cell.innerHTML = '<a href="#" class="' + this.Style.CSS_CELL_SELECTOR + '">' + this.buildDayLabel(workingDate) + "</a>";
    },
    
    /**
    * Styles a selectable cell.
    * @method styleCellDefault
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    */
    styleCellDefault : function(workingDate, cell) {
        Dom.addClass(cell, this.Style.CSS_CELL_SELECTABLE);
    },
    
    
    /**
    * Renders a single standard calendar cell using the CSS hightlight1 style
    * @method renderCellStyleHighlight1
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    */
    renderCellStyleHighlight1 : function(workingDate, cell) {
        Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT1);
    },
    
    /**
    * Renders a single standard calendar cell using the CSS hightlight2 style
    * @method renderCellStyleHighlight2
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    */
    renderCellStyleHighlight2 : function(workingDate, cell) {
        Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT2);
    },
    
    /**
    * Renders a single standard calendar cell using the CSS hightlight3 style
    * @method renderCellStyleHighlight3
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    */
    renderCellStyleHighlight3 : function(workingDate, cell) {
        Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT3);
    },
    
    /**
    * Renders a single standard calendar cell using the CSS hightlight4 style
    * @method renderCellStyleHighlight4
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    */
    renderCellStyleHighlight4 : function(workingDate, cell) {
        Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT4);
    },
    
    /**
    * Applies the default style used for rendering today's date to the current calendar cell
    * @method renderCellStyleToday
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    */
    renderCellStyleToday : function(workingDate, cell) {
        Dom.addClass(cell, this.Style.CSS_CELL_TODAY);
    },

    /**
    * Applies the default style used for rendering selected dates to the current calendar cell
    * @method renderCellStyleSelected
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering
    *   should not be terminated
    */
    renderCellStyleSelected : function(workingDate, cell) {
        Dom.addClass(cell, this.Style.CSS_CELL_SELECTED);
    },

    /**
    * Applies the default style used for rendering dates that are not a part of the current
    * month (preceding or trailing the cells for the current month)
    *
    * @method renderCellNotThisMonth
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering
    *   should not be terminated
    */
    renderCellNotThisMonth : function(workingDate, cell) {
        this.styleCellNotThisMonth(workingDate, cell);
        cell.innerHTML=workingDate.getDate();
        return Calendar.STOP_RENDER;
    },

    /** Applies the style used for rendering out-of-month dates to the current calendar cell
    * @method styleCellNotThisMonth
    * @param {Date}                 workingDate     The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell            The current working cell in the calendar
    */
    styleCellNotThisMonth : function(workingDate, cell) {
        YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_OOM);
    },

    /**
    * Renders the current calendar cell as a non-selectable "black-out" date using the default
    * restricted style.
    * @method renderBodyCellRestricted
    * @param {Date}     workingDate  The current working Date object being used to generate the calendar
    * @param {HTMLTableCellElement} cell   The current working cell in the calendar
    * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering
    *   should not be terminated
    */
    renderBodyCellRestricted : function(workingDate, cell) {
        Dom.addClass(cell, this.Style.CSS_CELL);
        Dom.addClass(cell, this.Style.CSS_CELL_RESTRICTED);
        cell.innerHTML=workingDate.getDate();
        return Calendar.STOP_RENDER;
    },
    
    // END BUILT-IN TABLE CELL RENDERERS
    
    // BEGIN MONTH NAVIGATION METHODS

    /**
    * Adds the designated number of months to the current calendar month, and sets the current
    * calendar page date to the new month.
    * @method addMonths
    * @param {Number} count The number of months to add to the current calendar
    */
    addMonths : function(count) {
        var cfgPageDate = DEF_CFG.PAGEDATE.key,

        prevDate = this.cfg.getProperty(cfgPageDate),
        newDate = DateMath.add(prevDate, DateMath.MONTH, count);

        this.cfg.setProperty(cfgPageDate, newDate);
        this.resetRenderers();
        this.changePageEvent.fire(prevDate, newDate);
    },

    /**
    * Subtracts the designated number of months from the current calendar month, and sets the current
    * calendar page date to the new month.
    * @method subtractMonths
    * @param {Number} count The number of months to subtract from the current calendar
    */
    subtractMonths : function(count) {
        this.addMonths(-1*count);
    },

    /**
    * Adds the designated number of years to the current calendar, and sets the current
    * calendar page date to the new month.
    * @method addYears
    * @param {Number} count The number of years to add to the current calendar
    */
    addYears : function(count) {
        var cfgPageDate = DEF_CFG.PAGEDATE.key,

        prevDate = this.cfg.getProperty(cfgPageDate),
        newDate = DateMath.add(prevDate, DateMath.YEAR, count);

        this.cfg.setProperty(cfgPageDate, newDate);
        this.resetRenderers();
        this.changePageEvent.fire(prevDate, newDate);
    },

    /**
    * Subtcats the designated number of years from the current calendar, and sets the current
    * calendar page date to the new month.
    * @method subtractYears
    * @param {Number} count The number of years to subtract from the current calendar
    */
    subtractYears : function(count) {
        this.addYears(-1*count);
    },

    /**
    * Navigates to the next month page in the calendar widget.
    * @method nextMonth
    */
    nextMonth : function() {
        this.addMonths(1);
    },
    
    /**
    * Navigates to the previous month page in the calendar widget.
    * @method previousMonth
    */
    previousMonth : function() {
        this.addMonths(-1);
    },
    
    /**
    * Navigates to the next year in the currently selected month in the calendar widget.
    * @method nextYear
    */
    nextYear : function() {
        this.addYears(1);
    },
    
    /**
    * Navigates to the previous year in the currently selected month in the calendar widget.
    * @method previousYear
    */
    previousYear : function() {
        this.addYears(-1);
    },

    // END MONTH NAVIGATION METHODS
    
    // BEGIN SELECTION METHODS
    
    /**
    * Resets the calendar widget to the originally selected month and year, and 
    * sets the calendar to the initial selection(s).
    * @method reset
    */
    reset : function() {
        this.cfg.resetProperty(DEF_CFG.SELECTED.key);
        this.cfg.resetProperty(DEF_CFG.PAGEDATE.key);
        this.resetEvent.fire();
    },
    
    /**
    * Clears the selected dates in the current calendar widget and sets the calendar
    * to the current month and year.
    * @method clear
    */
    clear : function() {
        this.cfg.setProperty(DEF_CFG.SELECTED.key, []);
        this.cfg.setProperty(DEF_CFG.PAGEDATE.key, new Date(this.today.getTime()));
        this.clearEvent.fire();
    },
    
    /**
    * Selects a date or a collection of dates on the current calendar. This method, by default,
    * does not call the render method explicitly. Once selection has completed, render must be 
    * called for the changes to be reflected visually.
    *
    * Any dates which are OOB (out of bounds, not selectable) will not be selected and the array of 
    * selected dates passed to the selectEvent will not contain OOB dates.
    * 
    * If all dates are OOB, the no state change will occur; beforeSelect and select events will not be fired.
    *
    * @method select
    * @param {String/Date/Date[]} date The date string of dates to select in the current calendar. Valid formats are
    *        individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
    *        Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
    *        This method can also take a JavaScript Date object or an array of Date objects.
    * @return {Date[]}   Array of JavaScript Date objects representing all individual dates that are currently selected.
    */
    select : function(date) {
        this.logger.log("Select: " + date, "info");

        var aToBeSelected = this._toFieldArray(date),
            validDates = [],
            selected = [],
            cfgSelected = DEF_CFG.SELECTED.key;

        this.logger.log("Selection field array: " + aToBeSelected, "info");
        
        for (var a=0; a < aToBeSelected.length; ++a) {
            var toSelect = aToBeSelected[a];

            if (!this.isDateOOB(this._toDate(toSelect))) {

                if (validDates.length === 0) {
                    this.beforeSelectEvent.fire();
                    selected = this.cfg.getProperty(cfgSelected);
                }
                validDates.push(toSelect);

                if (this._indexOfSelectedFieldArray(toSelect) == -1) { 
                    selected[selected.length] = toSelect;
                }
            }
        }

        if (validDates.length === 0) { this.logger.log("All provided dates were OOB. beforeSelect and select events not fired", "info"); }

        if (validDates.length > 0) {
            if (this.parent) {
                this.parent.cfg.setProperty(cfgSelected, selected);
            } else {
                this.cfg.setProperty(cfgSelected, selected);
            }
            this.selectEvent.fire(validDates);
        }

        return this.getSelectedDates();
    },
    
    /**
    * Selects a date on the current calendar by referencing the index of the cell that should be selected.
    * This method is used to easily select a single cell (usually with a mouse click) without having to do
    * a full render. The selected style is applied to the cell directly.
    *
    * If the cell is not marked with the CSS_CELL_SELECTABLE class (as is the case by default for out of month 
    * or out of bounds cells), it will not be selected and in such a case beforeSelect and select events will not be fired.
    * 
    * @method selectCell
    * @param {Number} cellIndex The index of the cell to select in the current calendar. 
    * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected.
    */
    selectCell : function(cellIndex) {

        var cell = this.cells[cellIndex],
            cellDate = this.cellDates[cellIndex],
            dCellDate = this._toDate(cellDate),
            selectable = Dom.hasClass(cell, this.Style.CSS_CELL_SELECTABLE);

        this.logger.log("Select: " + dCellDate, "info");
        if (!selectable) {this.logger.log("The cell at cellIndex:" + cellIndex + " is not a selectable cell. beforeSelect, select events not fired", "info"); }

        if (selectable) {
    
            this.beforeSelectEvent.fire();
    
            var cfgSelected = DEF_CFG.SELECTED.key;
            var selected = this.cfg.getProperty(cfgSelected);
    
            var selectDate = cellDate.concat();
    
            if (this._indexOfSelectedFieldArray(selectDate) == -1) {
                selected[selected.length] = selectDate;
            }
            if (this.parent) {
                this.parent.cfg.setProperty(cfgSelected, selected);
            } else {
                this.cfg.setProperty(cfgSelected, selected);
            }
            this.renderCellStyleSelected(dCellDate,cell);
            this.selectEvent.fire([selectDate]);
    
            this.doCellMouseOut.call(cell, null, this);  
        }
    
        return this.getSelectedDates();
    },
    
    /**
    * Deselects a date or a collection of dates on the current calendar. This method, by default,
    * does not call the render method explicitly. Once deselection has completed, render must be 
    * called for the changes to be reflected visually.
    * 
    * The method will not attempt to deselect any dates which are OOB (out of bounds, and hence not selectable) 
    * and the array of deselected dates passed to the deselectEvent will not contain any OOB dates.
    * 
    * If all dates are OOB, beforeDeselect and deselect events will not be fired.
    * 
    * @method deselect
    * @param {String/Date/Date[]} date The date string of dates to deselect in the current calendar. Valid formats are
    *        individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
    *        Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
    *        This method can also take a JavaScript Date object or an array of Date objects. 
    * @return {Date[]}   Array of JavaScript Date objects representing all individual dates that are currently selected.
    */
    deselect : function(date) {
        this.logger.log("Deselect: " + date, "info");

        var aToBeDeselected = this._toFieldArray(date),
            validDates = [],
            selected = [],
            cfgSelected = DEF_CFG.SELECTED.key;

        this.logger.log("Deselection field array: " + aToBeDeselected, "info");

        for (var a=0; a < aToBeDeselected.length; ++a) {
            var toDeselect = aToBeDeselected[a];
    
            if (!this.isDateOOB(this._toDate(toDeselect))) {
    
                if (validDates.length === 0) {
                    this.beforeDeselectEvent.fire();
                    selected = this.cfg.getProperty(cfgSelected);
                }
    
                validDates.push(toDeselect);
    
                var index = this._indexOfSelectedFieldArray(toDeselect);
                if (index != -1) { 
                    selected.splice(index,1);
                }
            }
        }
    
        if (validDates.length === 0) { this.logger.log("All provided dates were OOB. beforeDeselect and deselect events not fired");}
    
        if (validDates.length > 0) {
            if (this.parent) {
                this.parent.cfg.setProperty(cfgSelected, selected);
            } else {
                this.cfg.setProperty(cfgSelected, selected);
            }
            this.deselectEvent.fire(validDates);
        }
    
        return this.getSelectedDates();
    },
    
    /**
    * Deselects a date on the current calendar by referencing the index of the cell that should be deselected.
    * This method is used to easily deselect a single cell (usually with a mouse click) without having to do
    * a full render. The selected style is removed from the cell directly.
    * 
    * If the cell is not marked with the CSS_CELL_SELECTABLE class (as is the case by default for out of month 
    * or out of bounds cells), the method will not attempt to deselect it and in such a case, beforeDeselect and 
    * deselect events will not be fired.
    * 
    * @method deselectCell
    * @param {Number} cellIndex The index of the cell to deselect in the current calendar. 
    * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected.
    */
    deselectCell : function(cellIndex) {
        var cell = this.cells[cellIndex],
            cellDate = this.cellDates[cellIndex],
            cellDateIndex = this._indexOfSelectedFieldArray(cellDate);

        var selectable = Dom.hasClass(cell, this.Style.CSS_CELL_SELECTABLE);
        if (!selectable) { this.logger.log("The cell at cellIndex:" + cellIndex + " is not a selectable/deselectable cell", "info"); }

        if (selectable) {

            this.beforeDeselectEvent.fire();

            var selected = this.cfg.getProperty(DEF_CFG.SELECTED.key),
                dCellDate = this._toDate(cellDate),
                selectDate = cellDate.concat();

            if (cellDateIndex > -1) {
                if ((this.cfg.getProperty(DEF_CFG.PAGEDATE.key).getMonth() == dCellDate.getMonth() &&
                    this.cfg.getProperty(DEF_CFG.PAGEDATE.key).getFullYear() == dCellDate.getFullYear()) || this.cfg.getProperty(DEF_CFG.OOM_SELECT.key)) {
                    Dom.removeClass(cell, this.Style.CSS_CELL_SELECTED);
                }
                selected.splice(cellDateIndex, 1);
            }

            if (this.parent) {
                this.parent.cfg.setProperty(DEF_CFG.SELECTED.key, selected);
            } else {
                this.cfg.setProperty(DEF_CFG.SELECTED.key, selected);
            }

            this.deselectEvent.fire([selectDate]);
        }

        return this.getSelectedDates();
    },

    /**
    * Deselects all dates on the current calendar.
    * @method deselectAll
    * @return {Date[]}  Array of JavaScript Date objects representing all individual dates that are currently selected.
    *      Assuming that this function executes properly, the return value should be an empty array.
    *      However, the empty array is returned for the sake of being able to check the selection status
    *      of the calendar.
    */
    deselectAll : function() {
        this.beforeDeselectEvent.fire();
        
        var cfgSelected = DEF_CFG.SELECTED.key,
            selected = this.cfg.getProperty(cfgSelected),
            count = selected.length,
            sel = selected.concat();

        if (this.parent) {
            this.parent.cfg.setProperty(cfgSelected, []);
        } else {
            this.cfg.setProperty(cfgSelected, []);
        }
        
        if (count > 0) {
            this.deselectEvent.fire(sel);
        }
    
        return this.getSelectedDates();
    },
    
    // END SELECTION METHODS
    
    // BEGIN TYPE CONVERSION METHODS
    
    /**
    * Converts a date (either a JavaScript Date object, or a date string) to the internal data structure
    * used to represent dates: [[yyyy,mm,dd],[yyyy,mm,dd]].
    * @method _toFieldArray
    * @private
    * @param {String/Date/Date[]} date The date string of dates to deselect in the current calendar. Valid formats are
    *        individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
    *        Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
    *        This method can also take a JavaScript Date object or an array of Date objects. 
    * @return {Array[](Number[])} Array of date field arrays
    */
    _toFieldArray : function(date) {
        var returnDate = [];
    
        if (date instanceof Date) {
            returnDate = [[date.getFullYear(), date.getMonth()+1, date.getDate()]];
        } else if (Lang.isString(date)) {
            returnDate = this._parseDates(date);
        } else if (Lang.isArray(date)) {
            for (var i=0;i<date.length;++i) {
                var d = date[i];
                returnDate[returnDate.length] = [d.getFullYear(),d.getMonth()+1,d.getDate()];
            }
        }
        
        return returnDate;
    },
    
    /**
    * Converts a date field array [yyyy,mm,dd] to a JavaScript Date object. The date field array
    * is the format in which dates are as provided as arguments to selectEvent and deselectEvent listeners.
    * 
    * @method toDate
    * @param {Number[]} dateFieldArray The date field array to convert to a JavaScript Date.
    * @return {Date} JavaScript Date object representing the date field array.
    */
    toDate : function(dateFieldArray) {
        return this._toDate(dateFieldArray);
    },
    
    /**
    * Converts a date field array [yyyy,mm,dd] to a JavaScript Date object.
    * @method _toDate
    * @private
    * @deprecated Made public, toDate 
    * @param {Number[]}  dateFieldArray The date field array to convert to a JavaScript Date.
    * @return {Date} JavaScript Date object representing the date field array
    */
    _toDate : function(dateFieldArray) {
        if (dateFieldArray instanceof Date) {
            return dateFieldArray;
        } else {
            return DateMath.getDate(dateFieldArray[0],dateFieldArray[1]-1,dateFieldArray[2]);
        }
    },
    
    // END TYPE CONVERSION METHODS 
    
    // BEGIN UTILITY METHODS
    
    /**
    * Determines if 2 field arrays are equal.
    * @method _fieldArraysAreEqual
    * @private
    * @param {Number[]} array1 The first date field array to compare
    * @param {Number[]} array2 The first date field array to compare
    * @return {Boolean} The boolean that represents the equality of the two arrays
    */
    _fieldArraysAreEqual : function(array1, array2) {
        var match = false;
    
        if (array1[0]==array2[0]&&array1[1]==array2[1]&&array1[2]==array2[2]) {
            match=true; 
        }
    
        return match;
    },
    
    /**
    * Gets the index of a date field array [yyyy,mm,dd] in the current list of selected dates.
    * @method _indexOfSelectedFieldArray
    * @private
    * @param {Number[]}  find The date field array to search for
    * @return {Number}   The index of the date field array within the collection of selected dates.
    *        -1 will be returned if the date is not found.
    */
    _indexOfSelectedFieldArray : function(find) {
        var selected = -1,
            seldates = this.cfg.getProperty(DEF_CFG.SELECTED.key);
    
        for (var s=0;s<seldates.length;++s) {
            var sArray = seldates[s];
            if (find[0]==sArray[0]&&find[1]==sArray[1]&&find[2]==sArray[2]) {
                selected = s;
                break;
            }
        }
    
        return selected;
    },
    
    /**
    * Determines whether a given date is OOM (out of month).
    * @method isDateOOM
    * @param {Date} date The JavaScript Date object for which to check the OOM status
    * @return {Boolean} true if the date is OOM
    */
    isDateOOM : function(date) {
        return (date.getMonth() != this.cfg.getProperty(DEF_CFG.PAGEDATE.key).getMonth());
    },
    
    /**
    * Determines whether a given date is OOB (out of bounds - less than the mindate or more than the maxdate).
    *
    * @method isDateOOB
    * @param {Date} date The JavaScript Date object for which to check the OOB status
    * @return {Boolean} true if the date is OOB
    */
    isDateOOB : function(date) {
        var minDate = this.cfg.getProperty(DEF_CFG.MINDATE.key),
            maxDate = this.cfg.getProperty(DEF_CFG.MAXDATE.key),
            dm = DateMath;
        
        if (minDate) {
            minDate = dm.clearTime(minDate);
        } 
        if (maxDate) {
            maxDate = dm.clearTime(maxDate);
        }
    
        var clearedDate = new Date(date.getTime());
        clearedDate = dm.clearTime(clearedDate);
    
        return ((minDate && clearedDate.getTime() < minDate.getTime()) || (maxDate && clearedDate.getTime() > maxDate.getTime()));
    },
    
    /**
     * Parses a pagedate configuration property value. The value can either be specified as a string of form "mm/yyyy" or a Date object 
     * and is parsed into a Date object normalized to the first day of the month. If no value is passed in, the month and year from today's date are used to create the Date object 
     * @method _parsePageDate
     * @private
     * @param {Date|String} date Pagedate value which needs to be parsed
     * @return {Date} The Date object representing the pagedate
     */
    _parsePageDate : function(date) {
        var parsedDate;

        if (date) {
            if (date instanceof Date) {
                parsedDate = DateMath.findMonthStart(date);
            } else {
                var month, year, aMonthYear;
                aMonthYear = date.split(this.cfg.getProperty(DEF_CFG.DATE_FIELD_DELIMITER.key));
                month = parseInt(aMonthYear[this.cfg.getProperty(DEF_CFG.MY_MONTH_POSITION.key)-1], 10)-1;
                year = parseInt(aMonthYear[this.cfg.getProperty(DEF_CFG.MY_YEAR_POSITION.key)-1], 10) - this.Locale.YEAR_OFFSET;

                parsedDate = DateMath.getDate(year, month, 1);
            }
        } else {
            parsedDate = DateMath.getDate(this.today.getFullYear(), this.today.getMonth(), 1);
        }
        return parsedDate;
    },
    
    // END UTILITY METHODS
    
    // BEGIN EVENT HANDLERS
    
    /**
    * Event executed before a date is selected in the calendar widget.
    * @deprecated Event handlers for this event should be susbcribed to beforeSelectEvent.
    */
    onBeforeSelect : function() {
        if (this.cfg.getProperty(DEF_CFG.MULTI_SELECT.key) === false) {
            if (this.parent) {
                this.parent.callChildFunction("clearAllBodyCellStyles", this.Style.CSS_CELL_SELECTED);
                this.parent.deselectAll();
            } else {
                this.clearAllBodyCellStyles(this.Style.CSS_CELL_SELECTED);
                this.deselectAll();
            }
        }
    },
    
    /**
    * Event executed when a date is selected in the calendar widget.
    * @param {Array} selected An array of date field arrays representing which date or dates were selected. Example: [ [2006,8,6],[2006,8,7],[2006,8,8] ]
    * @deprecated Event handlers for this event should be susbcribed to selectEvent.
    */
    onSelect : function(selected) { },
    
    /**
    * Event executed before a date is deselected in the calendar widget.
    * @deprecated Event handlers for this event should be susbcribed to beforeDeselectEvent.
    */
    onBeforeDeselect : function() { },
    
    /**
    * Event executed when a date is deselected in the calendar widget.
    * @param {Array} selected An array of date field arrays representing which date or dates were deselected. Example: [ [2006,8,6],[2006,8,7],[2006,8,8] ]
    * @deprecated Event handlers for this event should be susbcribed to deselectEvent.
    */
    onDeselect : function(deselected) { },
    
    /**
    * Event executed when the user navigates to a different calendar page.
    * @deprecated Event handlers for this event should be susbcribed to changePageEvent.
    */
    onChangePage : function() {
        this.render();
    },

    /**
    * Event executed when the calendar widget is rendered.
    * @deprecated Event handlers for this event should be susbcribed to renderEvent.
    */
    onRender : function() { },

    /**
    * Event executed when the calendar widget is reset to its original state.
    * @deprecated Event handlers for this event should be susbcribed to resetEvemt.
    */
    onReset : function() { this.render(); },

    /**
    * Event executed when the calendar widget is completely cleared to the current month with no selections.
    * @deprecated Event handlers for this event should be susbcribed to clearEvent.
    */
    onClear : function() { this.render(); },
    
    /**
    * Validates the calendar widget. This method has no default implementation
    * and must be extended by subclassing the widget.
    * @return Should return true if the widget validates, and false if
    * it doesn't.
    * @type Boolean
    */
    validate : function() { return true; },
    
    // END EVENT HANDLERS
    
    // BEGIN DATE PARSE METHODS
    
    /**
    * Converts a date string to a date field array
    * @private
    * @param {String} sDate   Date string. Valid formats are mm/dd and mm/dd/yyyy.
    * @return    A date field array representing the string passed to the method
    * @type Array[](Number[])
    */
    _parseDate : function(sDate) {
        var aDate = sDate.split(this.Locale.DATE_FIELD_DELIMITER),
            rArray;

        if (aDate.length == 2) {
            rArray = [aDate[this.Locale.MD_MONTH_POSITION-1],aDate[this.Locale.MD_DAY_POSITION-1]];
            rArray.type = Calendar.MONTH_DAY;
        } else {
            rArray = [aDate[this.Locale.MDY_YEAR_POSITION-1] - this.Locale.YEAR_OFFSET, aDate[this.Locale.MDY_MONTH_POSITION-1],aDate[this.Locale.MDY_DAY_POSITION-1]];
            rArray.type = Calendar.DATE;
        }

        for (var i=0;i<rArray.length;i++) {
            rArray[i] = parseInt(rArray[i], 10);
        }
    
        return rArray;
    },
    
    /**
    * Converts a multi or single-date string to an array of date field arrays
    * @private
    * @param {String} sDates  Date string with one or more comma-delimited dates. Valid formats are mm/dd, mm/dd/yyyy, mm/dd/yyyy-mm/dd/yyyy
    * @return       An array of date field arrays
    * @type Array[](Number[])
    */
    _parseDates : function(sDates) {
        var aReturn = [],
            aDates = sDates.split(this.Locale.DATE_DELIMITER);
        
        for (var d=0;d<aDates.length;++d) {
            var sDate = aDates[d];
    
            if (sDate.indexOf(this.Locale.DATE_RANGE_DELIMITER) != -1) {
                // This is a range
                var aRange = sDate.split(this.Locale.DATE_RANGE_DELIMITER),
                    dateStart = this._parseDate(aRange[0]),
                    dateEnd = this._parseDate(aRange[1]),
                    fullRange = this._parseRange(dateStart, dateEnd);

                aReturn = aReturn.concat(fullRange);
            } else {
                // This is not a range
                var aDate = this._parseDate(sDate);
                aReturn.push(aDate);
            }
        }
        return aReturn;
    },
    
    /**
    * Converts a date range to the full list of included dates
    * @private
    * @param {Number[]} startDate Date field array representing the first date in the range
    * @param {Number[]} endDate  Date field array representing the last date in the range
    * @return       An array of date field arrays
    * @type Array[](Number[])
    */
    _parseRange : function(startDate, endDate) {
        var dCurrent = DateMath.add(DateMath.getDate(startDate[0],startDate[1]-1,startDate[2]),DateMath.DAY,1),
            dEnd     = DateMath.getDate(endDate[0],  endDate[1]-1,  endDate[2]),
            results = [];

        results.push(startDate);
        while (dCurrent.getTime() <= dEnd.getTime()) {
            results.push([dCurrent.getFullYear(),dCurrent.getMonth()+1,dCurrent.getDate()]);
            dCurrent = DateMath.add(dCurrent,DateMath.DAY,1);
        }
        return results;
    },
    
    // END DATE PARSE METHODS
    
    // BEGIN RENDERER METHODS
    
    /**
    * Resets the render stack of the current calendar to its original pre-render value.
    */
    resetRenderers : function() {
        this.renderStack = this._renderStack.concat();
    },

    /**
     * Removes all custom renderers added to the Calendar through the addRenderer, addMonthRenderer and 
     * addWeekdayRenderer methods. Calendar's render method needs to be called after removing renderers 
     * to re-render the Calendar without custom renderers applied.
     */
    removeRenderers : function() {
        this._renderStack = [];
        this.renderStack = [];
    },

    /**
    * Clears the inner HTML, CSS class and style information from the specified cell.
    * @method clearElement
    * @param {HTMLTableCellElement} cell The cell to clear
    */ 
    clearElement : function(cell) {
        cell.innerHTML = "&#160;";
        cell.className="";
    },
    
    /**
    * Adds a renderer to the render stack. The function reference passed to this method will be executed
    * when a date cell matches the conditions specified in the date string for this renderer.
    * 
    * <p>NOTE: The contents of the cell set by the renderer will be added to the DOM as HTML. The custom renderer implementation should 
    * escape markup used to set the cell contents, if coming from an external source.<p>
    * @method addRenderer
    * @param {String} sDates  A date string to associate with the specified renderer. Valid formats
    *         include date (12/24/2005), month/day (12/24), and range (12/1/2004-1/1/2005)
    * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer.
    */
    addRenderer : function(sDates, fnRender) {
        var aDates = this._parseDates(sDates);
        for (var i=0;i<aDates.length;++i) {
            var aDate = aDates[i];
        
            if (aDate.length == 2) { // this is either a range or a month/day combo
                if (aDate[0] instanceof Array) { // this is a range
                    this._addRenderer(Calendar.RANGE,aDate,fnRender);
                } else { // this is a month/day combo
                    this._addRenderer(Calendar.MONTH_DAY,aDate,fnRender);
                }
            } else if (aDate.length == 3) {
                this._addRenderer(Calendar.DATE,aDate,fnRender);
            }
        }
    },
    
    /**
    * The private method used for adding cell renderers to the local render stack.
    * This method is called by other methods that set the renderer type prior to the method call.
    * @method _addRenderer
    * @private
    * @param {String} type  The type string that indicates the type of date renderer being added.
    *         Values are YAHOO.widget.Calendar.DATE, YAHOO.widget.Calendar.MONTH_DAY, YAHOO.widget.Calendar.WEEKDAY,
    *         YAHOO.widget.Calendar.RANGE, YAHOO.widget.Calendar.MONTH
    * @param {Array}  aDates  An array of dates used to construct the renderer. The format varies based
    *         on the renderer type
    * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer.
    */
    _addRenderer : function(type, aDates, fnRender) {
        var add = [type,aDates,fnRender];
        this.renderStack.unshift(add); 
        this._renderStack = this.renderStack.concat();
    },

    /**
    * Adds a month renderer to the render stack. The function reference passed to this method will be executed
    * when a date cell matches the month passed to this method
    * 
    * <p>NOTE: The contents of the cell set by the renderer will be added to the DOM as HTML. The custom renderer implementation should 
    * escape markup used to set the cell contents, if coming from an external source.<p>
    * @method addMonthRenderer
    * @param {Number} month  The month (1-12) to associate with this renderer
    * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer.
    */
    addMonthRenderer : function(month, fnRender) {
        this._addRenderer(Calendar.MONTH,[month],fnRender);
    },

    /**
    * Adds a weekday renderer to the render stack. The function reference passed to this method will be executed
    * when a date cell matches the weekday passed to this method.
    *
    * <p>NOTE: The contents of the cell set by the renderer will be added to the DOM as HTML. The custom renderer implementation should 
    * escape HTML used to set the cell contents, if coming from an external source.<p>
    *
    * @method addWeekdayRenderer
    * @param {Number} weekday  The weekday (Sunday = 1, Monday = 2 ... Saturday = 7) to associate with this renderer
    * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer.
    */
    addWeekdayRenderer : function(weekday, fnRender) {
        this._addRenderer(Calendar.WEEKDAY,[weekday],fnRender);
    },

    // END RENDERER METHODS
    
    // BEGIN CSS METHODS
    
    /**
    * Removes all styles from all body cells in the current calendar table.
    * @method clearAllBodyCellStyles
    * @param {style} style The CSS class name to remove from all calendar body cells
    */
    clearAllBodyCellStyles : function(style) {
        for (var c=0;c<this.cells.length;++c) {
            Dom.removeClass(this.cells[c],style);
        }
    },
    
    // END CSS METHODS
    
    // BEGIN GETTER/SETTER METHODS
    /**
    * Sets the calendar's month explicitly
    * @method setMonth
    * @param {Number} month  The numeric month, from 0 (January) to 11 (December)
    */
    setMonth : function(month) {
        var cfgPageDate = DEF_CFG.PAGEDATE.key,
            current = this.cfg.getProperty(cfgPageDate);
        current.setMonth(parseInt(month, 10));
        this.cfg.setProperty(cfgPageDate, current);
    },

    /**
    * Sets the calendar's year explicitly.
    * @method setYear
    * @param {Number} year  The numeric 4-digit year
    */
    setYear : function(year) {
        var cfgPageDate = DEF_CFG.PAGEDATE.key,
            current = this.cfg.getProperty(cfgPageDate);

        current.setFullYear(parseInt(year, 10) - this.Locale.YEAR_OFFSET);
        this.cfg.setProperty(cfgPageDate, current);
    },

    /**
    * Gets the list of currently selected dates from the calendar.
    * @method getSelectedDates
    * @return {Date[]} An array of currently selected JavaScript Date objects.
    */
    getSelectedDates : function() {
        var returnDates = [],
            selected = this.cfg.getProperty(DEF_CFG.SELECTED.key);

        for (var d=0;d<selected.length;++d) {
            var dateArray = selected[d];

            var date = DateMath.getDate(dateArray[0],dateArray[1]-1,dateArray[2]);
            returnDates.push(date);
        }

        returnDates.sort( function(a,b) { return a-b; } );
        return returnDates;
    },

    /// END GETTER/SETTER METHODS ///
    
    /**
    * Hides the Calendar's outer container from view.
    * @method hide
    */
    hide : function() {
        if (this.beforeHideEvent.fire()) {
            this.oDomContainer.style.display = "none";
            this.hideEvent.fire();
        }
    },

    /**
    * Shows the Calendar's outer container.
    * @method show
    */
    show : function() {
        if (this.beforeShowEvent.fire()) {
            this.oDomContainer.style.display = "block";
            this.showEvent.fire();
        }
    },

    /**
    * Returns a string representing the current browser.
    * @deprecated As of 2.3.0, environment information is available in YAHOO.env.ua
    * @see YAHOO.env.ua
    * @property browser
    * @type String
    */
    browser : (function() {
                var ua = navigator.userAgent.toLowerCase();
                      if (ua.indexOf('opera')!=-1) { // Opera (check first in case of spoof)
                         return 'opera';
                      } else if (ua.indexOf('msie 7')!=-1) { // IE7
                         return 'ie7';
                      } else if (ua.indexOf('msie') !=-1) { // IE
                         return 'ie';
                      } else if (ua.indexOf('safari')!=-1) { // Safari (check before Gecko because it includes "like Gecko")
                         return 'safari';
                      } else if (ua.indexOf('gecko') != -1) { // Gecko
                         return 'gecko';
                      } else {
                         return false;
                      }
                })(),
    /**
    * Returns a string representation of the object.
    * @method toString
    * @return {String} A string representation of the Calendar object.
    */
    toString : function() {
        return "Calendar " + this.id;
    },

    /**
     * Destroys the Calendar instance. The method will remove references
     * to HTML elements, remove any event listeners added by the Calendar,
     * and destroy the Config and CalendarNavigator instances it has created.
     *
     * @method destroy
     */
    destroy : function() {

        if (this.beforeDestroyEvent.fire()) {
            var cal = this;

            // Child objects
            if (cal.navigator) {
                cal.navigator.destroy();
            }

            if (cal.cfg) {
                cal.cfg.destroy();
            }

            // DOM event listeners
            Event.purgeElement(cal.oDomContainer, true);

            // Generated markup/DOM - Not removing the container DIV since we didn't create it.
            Dom.removeClass(cal.oDomContainer, cal.Style.CSS_WITH_TITLE);
            Dom.removeClass(cal.oDomContainer, cal.Style.CSS_CONTAINER);
            Dom.removeClass(cal.oDomContainer, cal.Style.CSS_SINGLE);
            cal.oDomContainer.innerHTML = "";

            // JS-to-DOM references
            cal.oDomContainer = null;
            cal.cells = null;

            this.destroyEvent.fire();
        }
    }
};

YAHOO.widget.Calendar = Calendar;

/**
* @namespace YAHOO.widget
* @class Calendar_Core
* @extends YAHOO.widget.Calendar
* @deprecated The old Calendar_Core class is no longer necessary.
*/
YAHOO.widget.Calendar_Core = YAHOO.widget.Calendar;

YAHOO.widget.Cal_Core = YAHOO.widget.Calendar;

})();
(function() {

    var Dom = YAHOO.util.Dom,
        DateMath = YAHOO.widget.DateMath,
        Event = YAHOO.util.Event,
        Lang = YAHOO.lang,
        Calendar = YAHOO.widget.Calendar;

/**
* YAHOO.widget.CalendarGroup is a special container class for YAHOO.widget.Calendar. This class facilitates
* the ability to have multi-page calendar views that share a single dataset and are
* dependent on each other.
*
* The calendar group instance will refer to each of its elements using a 0-based index.
* For example, to construct the placeholder for a calendar group widget with id "cal1" and
* containerId of "cal1Container", the markup would be as follows:
*   <xmp>
*       <div id="cal1Container_0"></div>
*       <div id="cal1Container_1"></div>
*   </xmp>
* The tables for the calendars ("cal1_0" and "cal1_1") will be inserted into those containers.
*
* <p>
* <strong>NOTE: As of 2.4.0, the constructor's ID argument is optional.</strong>
* The CalendarGroup can be constructed by simply providing a container ID string, 
* or a reference to a container DIV HTMLElement (the element needs to exist 
* in the document).
* 
* E.g.:
*   <xmp>
*       var c = new YAHOO.widget.CalendarGroup("calContainer", configOptions);
*   </xmp>
* or:
*   <xmp>
*       var containerDiv = YAHOO.util.Dom.get("calContainer");
*       var c = new YAHOO.widget.CalendarGroup(containerDiv, configOptions);
*   </xmp>
* </p>
* <p>
* If not provided, the ID will be generated from the container DIV ID by adding an "_t" suffix.
* For example if an ID is not provided, and the container's ID is "calContainer", the CalendarGroup's ID will be set to "calContainer_t".
* </p>
* 
* @namespace YAHOO.widget
* @class CalendarGroup
* @constructor
* @param {String} id optional The id of the table element that will represent the CalendarGroup widget. As of 2.4.0, this argument is optional.
* @param {String | HTMLElement} container The id of the container div element that will wrap the CalendarGroup table, or a reference to a DIV element which exists in the document.
* @param {Object} config optional The configuration object containing the initial configuration values for the CalendarGroup.
*/
function CalendarGroup(id, containerId, config) {
    if (arguments.length > 0) {
        this.init.apply(this, arguments);
    }
}

/**
* The set of default Config property keys and values for the CalendarGroup.
* 
* <p>
* NOTE: This property is made public in order to allow users to change 
* the default values of configuration properties. Users should not 
* modify the key string, unless they are overriding the Calendar implementation
* </p>
*
* @property YAHOO.widget.CalendarGroup.DEFAULT_CONFIG
* @static
* @type Object An object with key/value pairs, the key being the 
* uppercase configuration property name and the value being an objec 
* literal with a key string property, and a value property, specifying the 
* default value of the property 
*/

/**
* The set of default Config property keys and values for the CalendarGroup
* @property YAHOO.widget.CalendarGroup._DEFAULT_CONFIG
* @deprecated Made public. See the public DEFAULT_CONFIG property for details
* @private
* @static
* @type Object
*/
CalendarGroup.DEFAULT_CONFIG = CalendarGroup._DEFAULT_CONFIG = Calendar.DEFAULT_CONFIG;
CalendarGroup.DEFAULT_CONFIG.PAGES = {key:"pages", value:2};

var DEF_CFG = CalendarGroup.DEFAULT_CONFIG;

CalendarGroup.prototype = {

    /**
    * Initializes the calendar group. All subclasses must call this method in order for the
    * group to be initialized properly.
    * @method init
    * @param {String} id optional The id of the table element that will represent the CalendarGroup widget. As of 2.4.0, this argument is optional.
    * @param {String | HTMLElement} container The id of the container div element that will wrap the CalendarGroup table, or a reference to a DIV element which exists in the document.
    * @param {Object} config optional The configuration object containing the initial configuration values for the CalendarGroup.
    */
    init : function(id, container, config) {

        // Normalize 2.4.0, pre 2.4.0 args
        var nArgs = this._parseArgs(arguments);

        id = nArgs.id;
        container = nArgs.container;
        config = nArgs.config;

        this.oDomContainer = Dom.get(container);
        if (!this.oDomContainer) { this.logger.log("Container not found in document.", "error"); }

        if (!this.oDomContainer.id) {
            this.oDomContainer.id = Dom.generateId();
        }
        if (!id) {
            id = this.oDomContainer.id + "_t";
        }

        /**
        * The unique id associated with the CalendarGroup
        * @property id
        * @type String
        */
        this.id = id;

        /**
        * The unique id associated with the CalendarGroup container
        * @property containerId
        * @type String
        */
        this.containerId = this.oDomContainer.id;

        this.logger = new YAHOO.widget.LogWriter("CalendarGroup " + this.id);
        this.initEvents();
        this.initStyles();

        /**
        * The collection of Calendar pages contained within the CalendarGroup
        * @property pages
        * @type YAHOO.widget.Calendar[]
        */
        this.pages = [];

        Dom.addClass(this.oDomContainer, CalendarGroup.CSS_CONTAINER);
        Dom.addClass(this.oDomContainer, CalendarGroup.CSS_MULTI_UP);

        /**
        * The Config object used to hold the configuration variables for the CalendarGroup
        * @property cfg
        * @type YAHOO.util.Config
        */
        this.cfg = new YAHOO.util.Config(this);

        /**
        * The local object which contains the CalendarGroup's options
        * @property Options
        * @type Object
        */
        this.Options = {};

        /**
        * The local object which contains the CalendarGroup's locale settings
        * @property Locale
        * @type Object
        */
        this.Locale = {};

        this.setupConfig();

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

        this.cfg.fireQueue();

        this.logger.log("Initialized " + this.pages.length + "-page CalendarGroup", "info");
    },

    setupConfig : function() {

        var cfg = this.cfg;

        /**
        * The number of pages to include in the CalendarGroup. This value can only be set once, in the CalendarGroup's constructor arguments.
        * @config pages
        * @type Number
        * @default 2
        */
        cfg.addProperty(DEF_CFG.PAGES.key, { value:DEF_CFG.PAGES.value, validator:cfg.checkNumber, handler:this.configPages } );

        /**
        * The positive or negative year offset from the Gregorian calendar year (assuming a January 1st rollover) to 
        * be used when displaying or parsing dates.  NOTE: All JS Date objects returned by methods, or expected as input by
        * methods will always represent the Gregorian year, in order to maintain date/month/week values.
        *
        * @config year_offset
        * @type Number
        * @default 0
        */
        cfg.addProperty(DEF_CFG.YEAR_OFFSET.key, { value:DEF_CFG.YEAR_OFFSET.value, handler: this.delegateConfig, supercedes:DEF_CFG.YEAR_OFFSET.supercedes, suppressEvent:true } );

        /**
        * The date to use to represent "Today".
        *
        * @config today
        * @type Date
        * @default Today's date
        */
        cfg.addProperty(DEF_CFG.TODAY.key, { value: new Date(DEF_CFG.TODAY.value.getTime()), supercedes:DEF_CFG.TODAY.supercedes, handler: this.configToday, suppressEvent:false } );

        /**
        * The month/year representing the current visible Calendar date (mm/yyyy)
        * @config pagedate
        * @type String | Date
        * @default Today's date
        */
        cfg.addProperty(DEF_CFG.PAGEDATE.key, { value: DEF_CFG.PAGEDATE.value || new Date(DEF_CFG.TODAY.value.getTime()), handler:this.configPageDate } );

        /**
        * The date or range of dates representing the current Calendar selection
        *
        * @config selected
        * @type String
        * @default []
        */
        cfg.addProperty(DEF_CFG.SELECTED.key, { value:[], handler:this.configSelected } );

        /**
        * The title to display above the CalendarGroup's month header. The title is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config title
        * @type HTML
        * @default ""
        */
        cfg.addProperty(DEF_CFG.TITLE.key, { value:DEF_CFG.TITLE.value, handler:this.configTitle } );

        /**
        * Whether or not a close button should be displayed for this CalendarGroup
        * @config close
        * @type Boolean
        * @default false
        */
        cfg.addProperty(DEF_CFG.CLOSE.key, { value:DEF_CFG.CLOSE.value, handler:this.configClose } );

        /**
        * Whether or not an iframe shim should be placed under the Calendar to prevent select boxes from bleeding through in Internet Explorer 6 and below.
        * This property is enabled by default for IE6 and below. It is disabled by default for other browsers for performance reasons, but can be 
        * enabled if required.
        * 
        * @config iframe
        * @type Boolean
        * @default true for IE6 and below, false for all other browsers
        */
        cfg.addProperty(DEF_CFG.IFRAME.key, { value:DEF_CFG.IFRAME.value, handler:this.configIframe, validator:cfg.checkBoolean } );

        /**
        * The minimum selectable date in the current Calendar (mm/dd/yyyy)
        * @config mindate
        * @type String | Date
        * @default null
        */
        cfg.addProperty(DEF_CFG.MINDATE.key, { value:DEF_CFG.MINDATE.value, handler:this.delegateConfig } );

        /**
        * The maximum selectable date in the current Calendar (mm/dd/yyyy)
        * @config maxdate
        * @type String | Date
        * @default null
        */
        cfg.addProperty(DEF_CFG.MAXDATE.key, { value:DEF_CFG.MAXDATE.value, handler:this.delegateConfig  } );

        /**
        * True if the Calendar should allow multiple selections. False by default.
        * @config MULTI_SELECT
        * @type Boolean
        * @default false
        */
        cfg.addProperty(DEF_CFG.MULTI_SELECT.key, { value:DEF_CFG.MULTI_SELECT.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );

        /**
        * True if the Calendar should allow selection of out-of-month dates. False by default.
        * @config OOM_SELECT
        * @type Boolean
        * @default false
        */
        cfg.addProperty(DEF_CFG.OOM_SELECT.key, { value:DEF_CFG.OOM_SELECT.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );

        /**
        * The weekday the week begins on. Default is 0 (Sunday).
        * @config START_WEEKDAY
        * @type number
        * @default 0
        */ 
        cfg.addProperty(DEF_CFG.START_WEEKDAY.key, { value:DEF_CFG.START_WEEKDAY.value, handler:this.delegateConfig, validator:cfg.checkNumber  } );
        
        /**
        * True if the Calendar should show weekday labels. True by default.
        * @config SHOW_WEEKDAYS
        * @type Boolean
        * @default true
        */ 
        cfg.addProperty(DEF_CFG.SHOW_WEEKDAYS.key, { value:DEF_CFG.SHOW_WEEKDAYS.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );
        
        /**
        * True if the Calendar should show week row headers. False by default.
        * @config SHOW_WEEK_HEADER
        * @type Boolean
        * @default false
        */ 
        cfg.addProperty(DEF_CFG.SHOW_WEEK_HEADER.key,{ value:DEF_CFG.SHOW_WEEK_HEADER.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );
        
        /**
        * True if the Calendar should show week row footers. False by default.
        * @config SHOW_WEEK_FOOTER
        * @type Boolean
        * @default false
        */
        cfg.addProperty(DEF_CFG.SHOW_WEEK_FOOTER.key,{ value:DEF_CFG.SHOW_WEEK_FOOTER.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );
        
        /**
        * True if the Calendar should suppress weeks that are not a part of the current month. False by default.
        * @config HIDE_BLANK_WEEKS
        * @type Boolean
        * @default false
        */  
        cfg.addProperty(DEF_CFG.HIDE_BLANK_WEEKS.key,{ value:DEF_CFG.HIDE_BLANK_WEEKS.value, handler:this.delegateConfig, validator:cfg.checkBoolean } );

        /**
        * The image URL that should be used for the left navigation arrow. The image URL is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config NAV_ARROW_LEFT
        * @type String
        * @deprecated You can customize the image by overriding the default CSS class for the left arrow - "calnavleft"
        * @default null
        */  
        cfg.addProperty(DEF_CFG.NAV_ARROW_LEFT.key, { value:DEF_CFG.NAV_ARROW_LEFT.value, handler:this.delegateConfig } );

        /**
        * The image URL that should be used for the right navigation arrow. The image URL is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config NAV_ARROW_RIGHT
        * @type String
        * @deprecated You can customize the image by overriding the default CSS class for the right arrow - "calnavright"
        * @default null
        */  
        cfg.addProperty(DEF_CFG.NAV_ARROW_RIGHT.key, { value:DEF_CFG.NAV_ARROW_RIGHT.value, handler:this.delegateConfig } );
    
        // Locale properties
        
        /**
        * The short month labels for the current locale. The month labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config MONTHS_SHORT
        * @type HTML[]
        * @default ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
        */
        cfg.addProperty(DEF_CFG.MONTHS_SHORT.key, { value:DEF_CFG.MONTHS_SHORT.value, handler:this.delegateConfig } );
        
        /**
        * The long month labels for the current locale. The month labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config MONTHS_LONG
        * @type HTML[]
        * @default ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
        */ 
        cfg.addProperty(DEF_CFG.MONTHS_LONG.key,  { value:DEF_CFG.MONTHS_LONG.value, handler:this.delegateConfig } );
        
        /**
        * The 1-character weekday labels for the current locale. The weekday labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config WEEKDAYS_1CHAR
        * @type HTML[]
        * @default ["S", "M", "T", "W", "T", "F", "S"]
        */ 
        cfg.addProperty(DEF_CFG.WEEKDAYS_1CHAR.key, { value:DEF_CFG.WEEKDAYS_1CHAR.value, handler:this.delegateConfig } );
        
        /**
        * The short weekday labels for the current locale. The weekday labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config WEEKDAYS_SHORT
        * @type HTML[]
        * @default ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
        */ 
        cfg.addProperty(DEF_CFG.WEEKDAYS_SHORT.key, { value:DEF_CFG.WEEKDAYS_SHORT.value, handler:this.delegateConfig } );
        
        /**
        * The medium weekday labels for the current locale. The weekday labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config WEEKDAYS_MEDIUM
        * @type HTML[]
        * @default ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
        */ 
        cfg.addProperty(DEF_CFG.WEEKDAYS_MEDIUM.key, { value:DEF_CFG.WEEKDAYS_MEDIUM.value, handler:this.delegateConfig } );
        
        /**
        * The long weekday labels for the current locale. The weekday labels are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @config WEEKDAYS_LONG
        * @type HTML[]
        * @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
        */ 
        cfg.addProperty(DEF_CFG.WEEKDAYS_LONG.key, { value:DEF_CFG.WEEKDAYS_LONG.value, handler:this.delegateConfig } );
    
        /**
        * The setting that determines which length of month labels should be used. Possible values are "short" and "long".
        * @config LOCALE_MONTHS
        * @type String
        * @default "long"
        */
        cfg.addProperty(DEF_CFG.LOCALE_MONTHS.key, { value:DEF_CFG.LOCALE_MONTHS.value, handler:this.delegateConfig } );
    
        /**
        * The setting that determines which length of weekday labels should be used. Possible values are "1char", "short", "medium", and "long".
        * @config LOCALE_WEEKDAYS
        * @type String
        * @default "short"
        */ 
        cfg.addProperty(DEF_CFG.LOCALE_WEEKDAYS.key, { value:DEF_CFG.LOCALE_WEEKDAYS.value, handler:this.delegateConfig } );
    
        /**
        * The value used to delimit individual dates in a date string passed to various Calendar functions.
        * @config DATE_DELIMITER
        * @type String
        * @default ","
        */
        cfg.addProperty(DEF_CFG.DATE_DELIMITER.key,  { value:DEF_CFG.DATE_DELIMITER.value, handler:this.delegateConfig } );
    
        /**
        * The value used to delimit date fields in a date string passed to various Calendar functions.
        * @config DATE_FIELD_DELIMITER
        * @type String
        * @default "/"
        */ 
        cfg.addProperty(DEF_CFG.DATE_FIELD_DELIMITER.key,{ value:DEF_CFG.DATE_FIELD_DELIMITER.value, handler:this.delegateConfig } );
    
        /**
        * The value used to delimit date ranges in a date string passed to various Calendar functions.
        * @config DATE_RANGE_DELIMITER
        * @type String
        * @default "-"
        */
        cfg.addProperty(DEF_CFG.DATE_RANGE_DELIMITER.key,{ value:DEF_CFG.DATE_RANGE_DELIMITER.value, handler:this.delegateConfig } );
    
        /**
        * The position of the month in a month/year date string
        * @config MY_MONTH_POSITION
        * @type Number
        * @default 1
        */
        cfg.addProperty(DEF_CFG.MY_MONTH_POSITION.key, { value:DEF_CFG.MY_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
        
        /**
        * The position of the year in a month/year date string
        * @config MY_YEAR_POSITION
        * @type Number
        * @default 2
        */ 
        cfg.addProperty(DEF_CFG.MY_YEAR_POSITION.key, { value:DEF_CFG.MY_YEAR_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
        
        /**
        * The position of the month in a month/day date string
        * @config MD_MONTH_POSITION
        * @type Number
        * @default 1
        */ 
        cfg.addProperty(DEF_CFG.MD_MONTH_POSITION.key, { value:DEF_CFG.MD_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
        
        /**
        * The position of the day in a month/year date string
        * @config MD_DAY_POSITION
        * @type Number
        * @default 2
        */ 
        cfg.addProperty(DEF_CFG.MD_DAY_POSITION.key,  { value:DEF_CFG.MD_DAY_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
        
        /**
        * The position of the month in a month/day/year date string
        * @config MDY_MONTH_POSITION
        * @type Number
        * @default 1
        */ 
        cfg.addProperty(DEF_CFG.MDY_MONTH_POSITION.key, { value:DEF_CFG.MDY_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
        
        /**
        * The position of the day in a month/day/year date string
        * @config MDY_DAY_POSITION
        * @type Number
        * @default 2
        */ 
        cfg.addProperty(DEF_CFG.MDY_DAY_POSITION.key, { value:DEF_CFG.MDY_DAY_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
        
        /**
        * The position of the year in a month/day/year date string
        * @config MDY_YEAR_POSITION
        * @type Number
        * @default 3
        */ 
        cfg.addProperty(DEF_CFG.MDY_YEAR_POSITION.key, { value:DEF_CFG.MDY_YEAR_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
    
        /**
        * The position of the month in the month year label string used as the Calendar header
        * @config MY_LABEL_MONTH_POSITION
        * @type Number
        * @default 1
        */
        cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_POSITION.key, { value:DEF_CFG.MY_LABEL_MONTH_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );
    
        /**
        * The position of the year in the month year label string used as the Calendar header
        * @config MY_LABEL_YEAR_POSITION
        * @type Number
        * @default 2
        */
        cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_POSITION.key, { value:DEF_CFG.MY_LABEL_YEAR_POSITION.value, handler:this.delegateConfig, validator:cfg.checkNumber } );

        /**
        * The suffix used after the month when rendering the Calendar header
        * @config MY_LABEL_MONTH_SUFFIX
        * @type String
        * @default " "
        */
        cfg.addProperty(DEF_CFG.MY_LABEL_MONTH_SUFFIX.key, { value:DEF_CFG.MY_LABEL_MONTH_SUFFIX.value, handler:this.delegateConfig } );
        
        /**
        * The suffix used after the year when rendering the Calendar header
        * @config MY_LABEL_YEAR_SUFFIX
        * @type String
        * @default ""
        */
        cfg.addProperty(DEF_CFG.MY_LABEL_YEAR_SUFFIX.key, { value:DEF_CFG.MY_LABEL_YEAR_SUFFIX.value, handler:this.delegateConfig } );

        /**
        * Configuration for the Month/Year CalendarNavigator UI which allows the user to jump directly to a 
        * specific Month/Year without having to scroll sequentially through months.
        * <p>
        * Setting this property to null (default value) or false, will disable the CalendarNavigator UI.
        * </p>
        * <p>
        * Setting this property to true will enable the CalendarNavigatior UI with the default CalendarNavigator configuration values.
        * </p>
        * <p>
        * This property can also be set to an object literal containing configuration properties for the CalendarNavigator UI.
        * The configuration object expects the the following case-sensitive properties, with the "strings" property being a nested object.
        * Any properties which are not provided will use the default values (defined in the CalendarNavigator class).
        * </p>
        * <dl>
        * <dt>strings</dt>
        * <dd><em>Object</em> :  An object with the properties shown below, defining the string labels to use in the Navigator's UI. The strings are inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source. 
        *     <dl>
        *         <dt>month</dt><dd><em>HTML</em> : The markup to use for the month label. Defaults to "Month".</dd>
        *         <dt>year</dt><dd><em>HTML</em> : The markup to use for the year label. Defaults to "Year".</dd>
        *         <dt>submit</dt><dd><em>HTML</em> : The markup to use for the submit button label. Defaults to "Okay".</dd>
        *         <dt>cancel</dt><dd><em>HTML</em> : The markup to use for the cancel button label. Defaults to "Cancel".</dd>
        *         <dt>invalidYear</dt><dd><em>HTML</em> : The markup to use for invalid year values. Defaults to "Year needs to be a number".</dd>
        *     </dl>
        * </dd>
        * <dt>monthFormat</dt><dd><em>String</em> : The month format to use. Either YAHOO.widget.Calendar.LONG, or YAHOO.widget.Calendar.SHORT. Defaults to YAHOO.widget.Calendar.LONG</dd>
        * <dt>initialFocus</dt><dd><em>String</em> : Either "year" or "month" specifying which input control should get initial focus. Defaults to "year"</dd>
        * </dl>
        * <p>E.g.</p>
        * <pre>
        * var navConfig = {
        *   strings: {
        *    month:"Calendar Month",
        *    year:"Calendar Year",
        *    submit: "Submit",
        *    cancel: "Cancel",
        *    invalidYear: "Please enter a valid year"
        *   },
        *   monthFormat: YAHOO.widget.Calendar.SHORT,
        *   initialFocus: "month"
        * }
        * </pre>
        * @config navigator
        * @type {Object|Boolean}
        * @default null
        */
        cfg.addProperty(DEF_CFG.NAV.key, { value:DEF_CFG.NAV.value, handler:this.configNavigator } );

        /**
         * The map of UI strings which the CalendarGroup UI uses.
         *
         * @config strings
         * @type {Object}
         * @default An object with the properties shown below:
         *     <dl>
         *         <dt>previousMonth</dt><dd><em>HTML</em> : The markup to use for the "Previous Month" navigation label. Defaults to "Previous Month". The string is added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.</dd>
         *         <dt>nextMonth</dt><dd><em>HTML</em> : The markup to use for the "Next Month" navigation UI. Defaults to "Next Month". The string is added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.</dd>
         *         <dt>close</dt><dd><em>HTML</em> : The markup to use for the close button label. Defaults to "Close". The string is added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.</dd>
         *     </dl>
         */
        cfg.addProperty(DEF_CFG.STRINGS.key, { 
            value:DEF_CFG.STRINGS.value, 
            handler:this.configStrings, 
            validator: function(val) {
                return Lang.isObject(val);
            },
            supercedes: DEF_CFG.STRINGS.supercedes
        });
    },

    /**
    * Initializes CalendarGroup's built-in CustomEvents
    * @method initEvents
    */
    initEvents : function() {

        var me = this,
            strEvent = "Event",
            CE = YAHOO.util.CustomEvent;

        /**
        * Proxy subscriber to subscribe to the CalendarGroup's child Calendars' CustomEvents
        * @method sub
        * @private
        * @param {Function} fn The function to subscribe to this CustomEvent
        * @param {Object} obj The CustomEvent's scope object
        * @param {Boolean} bOverride Whether or not to apply scope correction
        */
        var sub = function(fn, obj, bOverride) {
            for (var p=0;p<me.pages.length;++p) {
                var cal = me.pages[p];
                cal[this.type + strEvent].subscribe(fn, obj, bOverride);
            }
        };

        /**
        * Proxy unsubscriber to unsubscribe from the CalendarGroup's child Calendars' CustomEvents
        * @method unsub
        * @private
        * @param {Function} fn The function to subscribe to this CustomEvent
        * @param {Object} obj The CustomEvent's scope object
        */
        var unsub = function(fn, obj) {
            for (var p=0;p<me.pages.length;++p) {
                var cal = me.pages[p];
                cal[this.type + strEvent].unsubscribe(fn, obj);
            }
        };

        var defEvents = Calendar._EVENT_TYPES;

        /**
        * Fired before a date selection is made
        * @event beforeSelectEvent
        */
        me.beforeSelectEvent = new CE(defEvents.BEFORE_SELECT);
        me.beforeSelectEvent.subscribe = sub; me.beforeSelectEvent.unsubscribe = unsub;

        /**
        * Fired when a date selection is made
        * @event selectEvent
        * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD].
        */
        me.selectEvent = new CE(defEvents.SELECT); 
        me.selectEvent.subscribe = sub; me.selectEvent.unsubscribe = unsub;

        /**
        * Fired before a date or set of dates is deselected
        * @event beforeDeselectEvent
        */
        me.beforeDeselectEvent = new CE(defEvents.BEFORE_DESELECT); 
        me.beforeDeselectEvent.subscribe = sub; me.beforeDeselectEvent.unsubscribe = unsub;

        /**
        * Fired when a date or set of dates has been deselected
        * @event deselectEvent
        * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD].
        */
        me.deselectEvent = new CE(defEvents.DESELECT); 
        me.deselectEvent.subscribe = sub; me.deselectEvent.unsubscribe = unsub;
        
        /**
        * Fired when the Calendar page is changed
        * @event changePageEvent
        */
        me.changePageEvent = new CE(defEvents.CHANGE_PAGE); 
        me.changePageEvent.subscribe = sub; me.changePageEvent.unsubscribe = unsub;

        /**
        * Fired before the Calendar is rendered
        * @event beforeRenderEvent
        */
        me.beforeRenderEvent = new CE(defEvents.BEFORE_RENDER);
        me.beforeRenderEvent.subscribe = sub; me.beforeRenderEvent.unsubscribe = unsub;
    
        /**
        * Fired when the Calendar is rendered
        * @event renderEvent
        */
        me.renderEvent = new CE(defEvents.RENDER);
        me.renderEvent.subscribe = sub; me.renderEvent.unsubscribe = unsub;
    
        /**
        * Fired when the Calendar is reset
        * @event resetEvent
        */
        me.resetEvent = new CE(defEvents.RESET); 
        me.resetEvent.subscribe = sub; me.resetEvent.unsubscribe = unsub;
    
        /**
        * Fired when the Calendar is cleared
        * @event clearEvent
        */
        me.clearEvent = new CE(defEvents.CLEAR);
        me.clearEvent.subscribe = sub; me.clearEvent.unsubscribe = unsub;

        /**
        * Fired just before the CalendarGroup is to be shown
        * @event beforeShowEvent
        */
        me.beforeShowEvent = new CE(defEvents.BEFORE_SHOW);
    
        /**
        * Fired after the CalendarGroup is shown
        * @event showEvent
        */
        me.showEvent = new CE(defEvents.SHOW);
    
        /**
        * Fired just before the CalendarGroup is to be hidden
        * @event beforeHideEvent
        */
        me.beforeHideEvent = new CE(defEvents.BEFORE_HIDE);
    
        /**
        * Fired after the CalendarGroup is hidden
        * @event hideEvent
        */
        me.hideEvent = new CE(defEvents.HIDE);

        /**
        * Fired just before the CalendarNavigator is to be shown
        * @event beforeShowNavEvent
        */
        me.beforeShowNavEvent = new CE(defEvents.BEFORE_SHOW_NAV);
    
        /**
        * Fired after the CalendarNavigator is shown
        * @event showNavEvent
        */
        me.showNavEvent = new CE(defEvents.SHOW_NAV);
    
        /**
        * Fired just before the CalendarNavigator is to be hidden
        * @event beforeHideNavEvent
        */
        me.beforeHideNavEvent = new CE(defEvents.BEFORE_HIDE_NAV);

        /**
        * Fired after the CalendarNavigator is hidden
        * @event hideNavEvent
        */
        me.hideNavEvent = new CE(defEvents.HIDE_NAV);

        /**
        * Fired just before the CalendarNavigator is to be rendered
        * @event beforeRenderNavEvent
        */
        me.beforeRenderNavEvent = new CE(defEvents.BEFORE_RENDER_NAV);

        /**
        * Fired after the CalendarNavigator is rendered
        * @event renderNavEvent
        */
        me.renderNavEvent = new CE(defEvents.RENDER_NAV);

        /**
        * Fired just before the CalendarGroup is to be destroyed
        * @event beforeDestroyEvent
        */
        me.beforeDestroyEvent = new CE(defEvents.BEFORE_DESTROY);

        /**
        * Fired after the CalendarGroup is destroyed. This event should be used
        * for notification only. When this event is fired, important CalendarGroup instance
        * properties, dom references and event listeners have already been 
        * removed/dereferenced, and hence the CalendarGroup instance is not in a usable 
        * state.
        *
        * @event destroyEvent
        */
        me.destroyEvent = new CE(defEvents.DESTROY);
    },
    
    /**
    * The default Config handler for the "pages" property
    * @method configPages
    * @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.
    */
    configPages : function(type, args, obj) {
        var pageCount = args[0],
            cfgPageDate = DEF_CFG.PAGEDATE.key,
            sep = "_",
            caldate,
            firstPageDate = null,
            groupCalClass = "groupcal",
            firstClass = "first-of-type",
            lastClass = "last-of-type";

        for (var p=0;p<pageCount;++p) {
            var calId = this.id + sep + p,
                calContainerId = this.containerId + sep + p,
                childConfig = this.cfg.getConfig();

            childConfig.close = false;
            childConfig.title = false;
            childConfig.navigator = null;

            if (p > 0) {
                caldate = new Date(firstPageDate);
                this._setMonthOnDate(caldate, caldate.getMonth() + p);
                childConfig.pageDate = caldate;
            }

            var cal = this.constructChild(calId, calContainerId, childConfig);

            Dom.removeClass(cal.oDomContainer, this.Style.CSS_SINGLE);
            Dom.addClass(cal.oDomContainer, groupCalClass);

            if (p===0) {
                firstPageDate = cal.cfg.getProperty(cfgPageDate);
                Dom.addClass(cal.oDomContainer, firstClass);
            }
    
            if (p==(pageCount-1)) {
                Dom.addClass(cal.oDomContainer, lastClass);
            }
    
            cal.parent = this;
            cal.index = p; 
    
            this.pages[this.pages.length] = cal;
        }
    },
    
    /**
    * The default Config handler for the "pagedate" property
    * @method configPageDate
    * @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.
    */
    configPageDate : function(type, args, obj) {
        var val = args[0],
            firstPageDate;

        var cfgPageDate = DEF_CFG.PAGEDATE.key;
        
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            if (p === 0) {
                firstPageDate = cal._parsePageDate(val);
                cal.cfg.setProperty(cfgPageDate, firstPageDate);
            } else {
                var pageDate = new Date(firstPageDate);
                this._setMonthOnDate(pageDate, pageDate.getMonth() + p);
                cal.cfg.setProperty(cfgPageDate, pageDate);
            }
        }
    },
    
    /**
    * The default Config handler for the CalendarGroup "selected" property
    * @method configSelected
    * @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.
    */
    configSelected : function(type, args, obj) {
        var cfgSelected = DEF_CFG.SELECTED.key;
        this.delegateConfig(type, args, obj);
        var selected = (this.pages.length > 0) ? this.pages[0].cfg.getProperty(cfgSelected) : []; 
        this.cfg.setProperty(cfgSelected, selected, true);
    },

    
    /**
    * Delegates a configuration property to the CustomEvents associated with the CalendarGroup's children
    * @method delegateConfig
    * @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.
    */
    delegateConfig : function(type, args, obj) {
        var val = args[0];
        var cal;
    
        for (var p=0;p<this.pages.length;p++) {
            cal = this.pages[p];
            cal.cfg.setProperty(type, val);
        }
    },

    /**
    * Adds a function to all child Calendars within this CalendarGroup.
    * @method setChildFunction
    * @param {String}  fnName  The name of the function
    * @param {Function}  fn   The function to apply to each Calendar page object
    */
    setChildFunction : function(fnName, fn) {
        var pageCount = this.cfg.getProperty(DEF_CFG.PAGES.key);
    
        for (var p=0;p<pageCount;++p) {
            this.pages[p][fnName] = fn;
        }
    },

    /**
    * Calls a function within all child Calendars within this CalendarGroup.
    * @method callChildFunction
    * @param {String}  fnName  The name of the function
    * @param {Array}  args  The arguments to pass to the function
    */
    callChildFunction : function(fnName, args) {
        var pageCount = this.cfg.getProperty(DEF_CFG.PAGES.key);

        for (var p=0;p<pageCount;++p) {
            var page = this.pages[p];
            if (page[fnName]) {
                var fn = page[fnName];
                fn.call(page, args);
            }
        } 
    },

    /**
    * Constructs a child calendar. This method can be overridden if a subclassed version of the default
    * calendar is to be used.
    * @method constructChild
    * @param {String} id   The id of the table element that will represent the calendar widget
    * @param {String} containerId The id of the container div element that will wrap the calendar table
    * @param {Object} config  The configuration object containing the Calendar's arguments
    * @return {YAHOO.widget.Calendar} The YAHOO.widget.Calendar instance that is constructed
    */
    constructChild : function(id,containerId,config) {
        var container = document.getElementById(containerId);
        if (! container) {
            container = document.createElement("div");
            container.id = containerId;
            this.oDomContainer.appendChild(container);
        }
        return new Calendar(id,containerId,config);
    },
    
    /**
    * Sets the calendar group's month explicitly. This month will be set into the first
    * page of the multi-page calendar, and all other months will be iterated appropriately.
    * @method setMonth
    * @param {Number} month  The numeric month, from 0 (January) to 11 (December)
    */
    setMonth : function(month) {
        month = parseInt(month, 10);
        var currYear;

        var cfgPageDate = DEF_CFG.PAGEDATE.key;

        for (var p=0; p<this.pages.length; ++p) {
            var cal = this.pages[p];
            var pageDate = cal.cfg.getProperty(cfgPageDate);
            if (p === 0) {
                currYear = pageDate.getFullYear();
            } else {
                pageDate.setFullYear(currYear);
            }
            this._setMonthOnDate(pageDate, month+p); 
            cal.cfg.setProperty(cfgPageDate, pageDate);
        }
    },

    /**
    * Sets the calendar group's year explicitly. This year will be set into the first
    * page of the multi-page calendar, and all other months will be iterated appropriately.
    * @method setYear
    * @param {Number} year  The numeric 4-digit year
    */
    setYear : function(year) {
    
        var cfgPageDate = DEF_CFG.PAGEDATE.key;
    
        year = parseInt(year, 10);
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            var pageDate = cal.cfg.getProperty(cfgPageDate);
    
            if ((pageDate.getMonth()+1) == 1 && p>0) {
                year+=1;
            }
            cal.setYear(year);
        }
    },

    /**
    * Calls the render function of all child calendars within the group.
    * @method render
    */
    render : function() {
        this.renderHeader();
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.render();
        }
        this.renderFooter();
    },

    /**
    * Selects a date or a collection of dates on the current calendar. This method, by default,
    * does not call the render method explicitly. Once selection has completed, render must be 
    * called for the changes to be reflected visually.
    * @method select
    * @param    {String/Date/Date[]}    date    The date string of dates to select in the current calendar. Valid formats are
    *                               individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
    *                               Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
    *                               This method can also take a JavaScript Date object or an array of Date objects.
    * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected.
    */
    select : function(date) {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.select(date);
        }
        return this.getSelectedDates();
    },

    /**
    * Selects dates in the CalendarGroup based on the cell index provided. This method is used to select cells without having to do a full render. The selected style is applied to the cells directly.
    * The value of the MULTI_SELECT Configuration attribute will determine the set of dates which get selected. 
    * <ul>
    *    <li>If MULTI_SELECT is false, selectCell will select the cell at the specified index for only the last displayed Calendar page.</li>
    *    <li>If MULTI_SELECT is true, selectCell will select the cell at the specified index, on each displayed Calendar page.</li>
    * </ul>
    * @method selectCell
    * @param {Number} cellIndex The index of the cell to be selected. 
    * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected.
    */
    selectCell : function(cellIndex) {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.selectCell(cellIndex);
        }
        return this.getSelectedDates();
    },
    
    /**
    * Deselects a date or a collection of dates on the current calendar. This method, by default,
    * does not call the render method explicitly. Once deselection has completed, render must be 
    * called for the changes to be reflected visually.
    * @method deselect
    * @param {String/Date/Date[]} date The date string of dates to deselect in the current calendar. Valid formats are
    *        individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006).
    *        Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005).
    *        This method can also take a JavaScript Date object or an array of Date objects. 
    * @return {Date[]}   Array of JavaScript Date objects representing all individual dates that are currently selected.
    */
    deselect : function(date) {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.deselect(date);
        }
        return this.getSelectedDates();
    },
    
    /**
    * Deselects all dates on the current calendar.
    * @method deselectAll
    * @return {Date[]}  Array of JavaScript Date objects representing all individual dates that are currently selected.
    *      Assuming that this function executes properly, the return value should be an empty array.
    *      However, the empty array is returned for the sake of being able to check the selection status
    *      of the calendar.
    */
    deselectAll : function() {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.deselectAll();
        }
        return this.getSelectedDates();
    },

    /**
    * Deselects dates in the CalendarGroup based on the cell index provided. This method is used to select cells without having to do a full render. The selected style is applied to the cells directly.
    * deselectCell will deselect the cell at the specified index on each displayed Calendar page.
    *
    * @method deselectCell
    * @param {Number} cellIndex The index of the cell to deselect. 
    * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected.
    */
    deselectCell : function(cellIndex) {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.deselectCell(cellIndex);
        }
        return this.getSelectedDates();
    },

    /**
    * Resets the calendar widget to the originally selected month and year, and 
    * sets the calendar to the initial selection(s).
    * @method reset
    */
    reset : function() {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.reset();
        }
    },

    /**
    * Clears the selected dates in the current calendar widget and sets the calendar
    * to the current month and year.
    * @method clear
    */
    clear : function() {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.clear();
        }

        this.cfg.setProperty(DEF_CFG.SELECTED.key, []);
        this.cfg.setProperty(DEF_CFG.PAGEDATE.key, new Date(this.pages[0].today.getTime()));
        this.render();
    },

    /**
    * Navigates to the next month page in the calendar widget.
    * @method nextMonth
    */
    nextMonth : function() {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.nextMonth();
        }
    },
    
    /**
    * Navigates to the previous month page in the calendar widget.
    * @method previousMonth
    */
    previousMonth : function() {
        for (var p=this.pages.length-1;p>=0;--p) {
            var cal = this.pages[p];
            cal.previousMonth();
        }
    },
    
    /**
    * Navigates to the next year in the currently selected month in the calendar widget.
    * @method nextYear
    */
    nextYear : function() {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.nextYear();
        }
    },

    /**
    * Navigates to the previous year in the currently selected month in the calendar widget.
    * @method previousYear
    */
    previousYear : function() {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.previousYear();
        }
    },

    /**
    * Gets the list of currently selected dates from the calendar.
    * @return   An array of currently selected JavaScript Date objects.
    * @type Date[]
    */
    getSelectedDates : function() { 
        var returnDates = [];
        var selected = this.cfg.getProperty(DEF_CFG.SELECTED.key);
        for (var d=0;d<selected.length;++d) {
            var dateArray = selected[d];

            var date = DateMath.getDate(dateArray[0],dateArray[1]-1,dateArray[2]);
            returnDates.push(date);
        }

        returnDates.sort( function(a,b) { return a-b; } );
        return returnDates;
    },

    /**
    * Adds a renderer to the render stack. The function reference passed to this method will be executed
    * when a date cell matches the conditions specified in the date string for this renderer.
    * 
    * <p>NOTE: The contents of the cell set by the renderer will be added to the DOM as HTML. The custom renderer implementation should 
    * escape markup used to set the cell contents, if coming from an external source.<p>
    * @method addRenderer
    * @param {String} sDates  A date string to associate with the specified renderer. Valid formats
    *         include date (12/24/2005), month/day (12/24), and range (12/1/2004-1/1/2005)
    * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer.
    */
    addRenderer : function(sDates, fnRender) {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.addRenderer(sDates, fnRender);
        }
    },

    /**
    * Adds a month renderer to the render stack. The function reference passed to this method will be executed
    * when a date cell matches the month passed to this method
    * 
    * <p>NOTE: The contents of the cell set by the renderer will be added to the DOM as HTML. The custom renderer implementation should 
    * escape markup used to set the cell contents, if coming from an external source.<p>
    * @method addMonthRenderer
    * @param {Number} month  The month (1-12) to associate with this renderer
    * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer.
    */
    addMonthRenderer : function(month, fnRender) {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.addMonthRenderer(month, fnRender);
        }
    },

    /**
    * Adds a weekday renderer to the render stack. The function reference passed to this method will be executed
    * when a date cell matches the weekday passed to this method.
    *
    * <p>NOTE: The contents of the cell set by the renderer will be added to the DOM as HTML. The custom renderer implementation should 
    * escape HTML used to set the cell contents, if coming from an external source.<p>
    *
    * @method addWeekdayRenderer
    * @param {Number} weekday  The weekday (Sunday = 1, Monday = 2 ... Saturday = 7) to associate with this renderer
    * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer.
    */
    addWeekdayRenderer : function(weekday, fnRender) {
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            cal.addWeekdayRenderer(weekday, fnRender);
        }
    },

    /**
     * Removes all custom renderers added to the CalendarGroup through the addRenderer, addMonthRenderer and 
     * addWeekRenderer methods. CalendarGroup's render method needs to be called to after removing renderers 
     * to see the changes applied.
     * 
     * @method removeRenderers
     */
    removeRenderers : function() {
        this.callChildFunction("removeRenderers");
    },

    /**
    * Renders the header for the CalendarGroup.
    * @method renderHeader
    */
    renderHeader : function() {
        // EMPTY DEFAULT IMPL
    },

    /**
    * Renders a footer for the 2-up calendar container. By default, this method is
    * unimplemented.
    * @method renderFooter
    */
    renderFooter : function() {
        // EMPTY DEFAULT IMPL
    },

    /**
    * Adds the designated number of months to the current calendar month, and sets the current
    * calendar page date to the new month.
    * @method addMonths
    * @param {Number} count The number of months to add to the current calendar
    */
    addMonths : function(count) {
        this.callChildFunction("addMonths", count);
    },
    
    /**
    * Subtracts the designated number of months from the current calendar month, and sets the current
    * calendar page date to the new month.
    * @method subtractMonths
    * @param {Number} count The number of months to subtract from the current calendar
    */
    subtractMonths : function(count) {
        this.callChildFunction("subtractMonths", count);
    },

    /**
    * Adds the designated number of years to the current calendar, and sets the current
    * calendar page date to the new month.
    * @method addYears
    * @param {Number} count The number of years to add to the current calendar
    */
    addYears : function(count) {
        this.callChildFunction("addYears", count);
    },

    /**
    * Subtcats the designated number of years from the current calendar, and sets the current
    * calendar page date to the new month.
    * @method subtractYears
    * @param {Number} count The number of years to subtract from the current calendar
    */
    subtractYears : function(count) {
        this.callChildFunction("subtractYears", count);
    },

    /**
     * Returns the Calendar page instance which has a pagedate (month/year) matching the given date. 
     * Returns null if no match is found.
     * 
     * @method getCalendarPage
     * @param {Date} date The JavaScript Date object for which a Calendar page is to be found.
     * @return {Calendar} The Calendar page instance representing the month to which the date 
     * belongs.
     */
    getCalendarPage : function(date) {
        var cal = null;
        if (date) {
            var y = date.getFullYear(),
                m = date.getMonth();

            var pages = this.pages;
            for (var i = 0; i < pages.length; ++i) {
                var pageDate = pages[i].cfg.getProperty("pagedate");
                if (pageDate.getFullYear() === y && pageDate.getMonth() === m) {
                    cal = pages[i];
                    break;
                }
            }
        }
        return cal;
    },

    /**
    * Sets the month on a Date object, taking into account year rollover if the month is less than 0 or greater than 11.
    * The Date object passed in is modified. It should be cloned before passing it into this method if the original value needs to be maintained
    * @method _setMonthOnDate
    * @private
    * @param {Date} date The Date object on which to set the month index
    * @param {Number} iMonth The month index to set
    */
    _setMonthOnDate : function(date, iMonth) {
        // Bug in Safari 1.3, 2.0 (WebKit build < 420), Date.setMonth does not work consistently if iMonth is not 0-11
        if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420 && (iMonth < 0 || iMonth > 11)) {
            var newDate = DateMath.add(date, DateMath.MONTH, iMonth-date.getMonth());
            date.setTime(newDate.getTime());
        } else {
            date.setMonth(iMonth);
        }
    },
    
    /**
     * Fixes the width of the CalendarGroup container element, to account for miswrapped floats
     * @method _fixWidth
     * @private
     */
    _fixWidth : function() {
        var w = 0;
        for (var p=0;p<this.pages.length;++p) {
            var cal = this.pages[p];
            w += cal.oDomContainer.offsetWidth;
        }
        if (w > 0) {
            this.oDomContainer.style.width = w + "px";
        }
    },
    
    /**
    * Returns a string representation of the object.
    * @method toString
    * @return {String} A string representation of the CalendarGroup object.
    */
    toString : function() {
        return "CalendarGroup " + this.id;
    },

    /**
     * Destroys the CalendarGroup instance. The method will remove references
     * to HTML elements, remove any event listeners added by the CalendarGroup.
     * 
     * It will also destroy the Config and CalendarNavigator instances created by the 
     * CalendarGroup and the individual Calendar instances created for each page.
     *
     * @method destroy
     */
    destroy : function() {

        if (this.beforeDestroyEvent.fire()) {

            var cal = this;
    
            // Child objects
            if (cal.navigator) {
                cal.navigator.destroy();
            }
    
            if (cal.cfg) {
                cal.cfg.destroy();
            }
    
            // DOM event listeners
            Event.purgeElement(cal.oDomContainer, true);
    
            // Generated markup/DOM - Not removing the container DIV since we didn't create it.
            Dom.removeClass(cal.oDomContainer, CalendarGroup.CSS_CONTAINER);
            Dom.removeClass(cal.oDomContainer, CalendarGroup.CSS_MULTI_UP);
            
            for (var i = 0, l = cal.pages.length; i < l; i++) {
                cal.pages[i].destroy();
                cal.pages[i] = null;
            }
    
            cal.oDomContainer.innerHTML = "";
    
            // JS-to-DOM references
            cal.oDomContainer = null;
    
            this.destroyEvent.fire();
        }
    }
};

/**
* CSS class representing the container for the calendar
* @property YAHOO.widget.CalendarGroup.CSS_CONTAINER
* @static
* @final
* @type String
*/
CalendarGroup.CSS_CONTAINER = "yui-calcontainer";

/**
* CSS class representing the container for the calendar
* @property YAHOO.widget.CalendarGroup.CSS_MULTI_UP
* @static
* @final
* @type String
*/
CalendarGroup.CSS_MULTI_UP = "multi";

/**
* CSS class representing the title for the 2-up calendar
* @property YAHOO.widget.CalendarGroup.CSS_2UPTITLE
* @static
* @final
* @type String
*/
CalendarGroup.CSS_2UPTITLE = "title";

/**
* CSS class representing the close icon for the 2-up calendar
* @property YAHOO.widget.CalendarGroup.CSS_2UPCLOSE
* @static
* @final
* @deprecated Along with Calendar.IMG_ROOT and NAV_ARROW_LEFT, NAV_ARROW_RIGHT configuration properties.
*     Calendar's <a href="YAHOO.widget.Calendar.html#Style.CSS_CLOSE">Style.CSS_CLOSE</a> property now represents the CSS class used to render the close icon
* @type String
*/
CalendarGroup.CSS_2UPCLOSE = "close-icon";

YAHOO.lang.augmentProto(CalendarGroup, Calendar, "buildDayLabel",
                                                 "buildMonthLabel",
                                                 "renderOutOfBoundsDate",
                                                 "renderRowHeader",
                                                 "renderRowFooter",
                                                 "renderCellDefault",
                                                 "styleCellDefault",
                                                 "renderCellStyleHighlight1",
                                                 "renderCellStyleHighlight2",
                                                 "renderCellStyleHighlight3",
                                                 "renderCellStyleHighlight4",
                                                 "renderCellStyleToday",
                                                 "renderCellStyleSelected",
                                                 "renderCellNotThisMonth",
                                                 "styleCellNotThisMonth",
                                                 "renderBodyCellRestricted",
                                                 "initStyles",
                                                 "configTitle",
                                                 "configClose",
                                                 "configIframe",
                                                 "configStrings",
                                                 "configToday",
                                                 "configNavigator",
                                                 "createTitleBar",
                                                 "createCloseButton",
                                                 "removeTitleBar",
                                                 "removeCloseButton",
                                                 "hide",
                                                 "show",
                                                 "toDate",
                                                 "_toDate",
                                                 "_parseArgs",
                                                 "browser");

YAHOO.widget.CalGrp = CalendarGroup;
YAHOO.widget.CalendarGroup = CalendarGroup;

/**
* @class YAHOO.widget.Calendar2up
* @extends YAHOO.widget.CalendarGroup
* @deprecated The old Calendar2up class is no longer necessary, since CalendarGroup renders in a 2up view by default.
*/
YAHOO.widget.Calendar2up = function(id, containerId, config) {
    this.init(id, containerId, config);
};

YAHOO.extend(YAHOO.widget.Calendar2up, CalendarGroup);

/**
* @deprecated The old Calendar2up class is no longer necessary, since CalendarGroup renders in a 2up view by default.
*/
YAHOO.widget.Cal2up = YAHOO.widget.Calendar2up;

})();
/**
 * The CalendarNavigator is used along with a Calendar/CalendarGroup to 
 * provide a Month/Year popup navigation control, allowing the user to navigate 
 * to a specific month/year in the Calendar/CalendarGroup without having to 
 * scroll through months sequentially
 *
 * @namespace YAHOO.widget
 * @class CalendarNavigator
 * @constructor
 * @param {Calendar|CalendarGroup} cal The instance of the Calendar or CalendarGroup to which this CalendarNavigator should be attached.
 */
YAHOO.widget.CalendarNavigator = function(cal) {
    this.init(cal);
};

(function() {
    // Setup static properties (inside anon fn, so that we can use shortcuts)
    var CN = YAHOO.widget.CalendarNavigator;

    /**
     * YAHOO.widget.CalendarNavigator.CLASSES contains constants
     * for the class values applied to the CalendarNaviatgator's 
     * DOM elements
     * @property YAHOO.widget.CalendarNavigator.CLASSES
     * @type Object
     * @static
     */
    CN.CLASSES = {
        /**
         * Class applied to the Calendar Navigator's bounding box
         * @property YAHOO.widget.CalendarNavigator.CLASSES.NAV
         * @type String
         * @static
         */
        NAV :"yui-cal-nav",
        /**
         * Class applied to the Calendar/CalendarGroup's bounding box to indicate
         * the Navigator is currently visible
         * @property YAHOO.widget.CalendarNavigator.CLASSES.NAV_VISIBLE
         * @type String
         * @static
         */
        NAV_VISIBLE: "yui-cal-nav-visible",
        /**
         * Class applied to the Navigator mask's bounding box
         * @property YAHOO.widget.CalendarNavigator.CLASSES.MASK
         * @type String
         * @static
         */
        MASK : "yui-cal-nav-mask",
        /**
         * Class applied to the year label/control bounding box
         * @property YAHOO.widget.CalendarNavigator.CLASSES.YEAR
         * @type String
         * @static
         */
        YEAR : "yui-cal-nav-y",
        /**
         * Class applied to the month label/control bounding box
         * @property YAHOO.widget.CalendarNavigator.CLASSES.MONTH
         * @type String
         * @static
         */
        MONTH : "yui-cal-nav-m",
        /**
         * Class applied to the submit/cancel button's bounding box
         * @property YAHOO.widget.CalendarNavigator.CLASSES.BUTTONS
         * @type String
         * @static
         */
        BUTTONS : "yui-cal-nav-b",
        /**
         * Class applied to buttons wrapping element
         * @property YAHOO.widget.CalendarNavigator.CLASSES.BUTTON
         * @type String
         * @static
         */
        BUTTON : "yui-cal-nav-btn",
        /**
         * Class applied to the validation error area's bounding box
         * @property YAHOO.widget.CalendarNavigator.CLASSES.ERROR
         * @type String
         * @static
         */
        ERROR : "yui-cal-nav-e",
        /**
         * Class applied to the year input control
         * @property YAHOO.widget.CalendarNavigator.CLASSES.YEAR_CTRL
         * @type String
         * @static
         */
        YEAR_CTRL : "yui-cal-nav-yc",
        /**
         * Class applied to the month input control
         * @property YAHOO.widget.CalendarNavigator.CLASSES.MONTH_CTRL
         * @type String
         * @static
         */
        MONTH_CTRL : "yui-cal-nav-mc",
        /**
         * Class applied to controls with invalid data (e.g. a year input field with invalid an year)
         * @property YAHOO.widget.CalendarNavigator.CLASSES.INVALID
         * @type String
         * @static
         */
        INVALID : "yui-invalid",
        /**
         * Class applied to default controls
         * @property YAHOO.widget.CalendarNavigator.CLASSES.DEFAULT
         * @type String
         * @static
         */
        DEFAULT : "yui-default"
    };

    /**
     * Object literal containing the default configuration values for the CalendarNavigator
     * The configuration object is expected to follow the format below, with the properties being
     * case sensitive.
     * <dl>
     * <dt>strings</dt>
     * <dd><em>Object</em> :  An object with the properties shown below, defining the string labels to use in the Navigator's UI
     *     <dl>
     *         <dt>month</dt><dd><em>HTML</em> : The markup to use for the month label. Defaults to "Month".</dd>
     *         <dt>year</dt><dd><em>HTML</em> : The markup to use for the year label. Defaults to "Year".</dd>
     *         <dt>submit</dt><dd><em>HTML</em> : The markup to use for the submit button label. Defaults to "Okay".</dd>
     *         <dt>cancel</dt><dd><em>HTML</em> : The markup to use for the cancel button label. Defaults to "Cancel".</dd>
     *         <dt>invalidYear</dt><dd><em>HTML</em> : The markup to use for invalid year values. Defaults to "Year needs to be a number".</dd>
     *     </dl>
     * </dd>
     * <dt>monthFormat</dt><dd><em>String</em> : The month format to use. Either YAHOO.widget.Calendar.LONG, or YAHOO.widget.Calendar.SHORT. Defaults to YAHOO.widget.Calendar.LONG</dd>
     * <dt>initialFocus</dt><dd><em>String</em> : Either "year" or "month" specifying which input control should get initial focus. Defaults to "year"</dd>
     * </dl>
     * @property DEFAULT_CONFIG
     * @type Object
     * @static
     */
    CN.DEFAULT_CONFIG = {
        strings : {
            month: "Month",
            year: "Year",
            submit: "Okay",
            cancel: "Cancel",
            invalidYear : "Year needs to be a number"
        },
        monthFormat: YAHOO.widget.Calendar.LONG,
        initialFocus: "year"
    };

    /**
     * Object literal containing the default configuration values for the CalendarNavigator
     * @property _DEFAULT_CFG
     * @protected
     * @deprecated Made public. See the public DEFAULT_CONFIG property
     * @type Object
     * @static
     */
    CN._DEFAULT_CFG = CN.DEFAULT_CONFIG;


    /**
     * The suffix added to the Calendar/CalendarGroup's ID, to generate
     * a unique ID for the Navigator and it's bounding box.
     * @property YAHOO.widget.CalendarNavigator.ID_SUFFIX
     * @static
     * @type String
     * @final
     */
    CN.ID_SUFFIX = "_nav";
    /**
     * The suffix added to the Navigator's ID, to generate
     * a unique ID for the month control.
     * @property YAHOO.widget.CalendarNavigator.MONTH_SUFFIX
     * @static
     * @type String 
     * @final
     */
    CN.MONTH_SUFFIX = "_month";
    /**
     * The suffix added to the Navigator's ID, to generate
     * a unique ID for the year control.
     * @property YAHOO.widget.CalendarNavigator.YEAR_SUFFIX
     * @static
     * @type String
     * @final
     */
    CN.YEAR_SUFFIX = "_year";
    /**
     * The suffix added to the Navigator's ID, to generate
     * a unique ID for the error bounding box.
     * @property YAHOO.widget.CalendarNavigator.ERROR_SUFFIX
     * @static
     * @type String
     * @final
     */
    CN.ERROR_SUFFIX = "_error";
    /**
     * The suffix added to the Navigator's ID, to generate
     * a unique ID for the "Cancel" button.
     * @property YAHOO.widget.CalendarNavigator.CANCEL_SUFFIX
     * @static
     * @type String
     * @final
     */
    CN.CANCEL_SUFFIX = "_cancel";
    /**
     * The suffix added to the Navigator's ID, to generate
     * a unique ID for the "Submit" button.
     * @property YAHOO.widget.CalendarNavigator.SUBMIT_SUFFIX
     * @static
     * @type String
     * @final
     */
    CN.SUBMIT_SUFFIX = "_submit";

    /**
     * The number of digits to which the year input control is to be limited.
     * @property YAHOO.widget.CalendarNavigator.YR_MAX_DIGITS
     * @static
     * @type Number
     */
    CN.YR_MAX_DIGITS = 4;

    /**
     * The amount by which to increment the current year value,
     * when the arrow up/down key is pressed on the year control
     * @property YAHOO.widget.CalendarNavigator.YR_MINOR_INC
     * @static
     * @type Number
     */
    CN.YR_MINOR_INC = 1;

    /**
     * The amount by which to increment the current year value,
     * when the page up/down key is pressed on the year control
     * @property YAHOO.widget.CalendarNavigator.YR_MAJOR_INC
     * @static
     * @type Number
     */
    CN.YR_MAJOR_INC = 10;

    /**
     * Artificial delay (in ms) between the time the Navigator is hidden
     * and the Calendar/CalendarGroup state is updated. Allows the user
     * the see the Calendar/CalendarGroup page changing. If set to 0
     * the Calendar/CalendarGroup page will be updated instantly
     * @property YAHOO.widget.CalendarNavigator.UPDATE_DELAY
     * @static
     * @type Number
     */
    CN.UPDATE_DELAY = 50;

    /**
     * Regular expression used to validate the year input
     * @property YAHOO.widget.CalendarNavigator.YR_PATTERN
     * @static
     * @type RegExp
     */
    CN.YR_PATTERN = /^\d+$/;
    /**
     * Regular expression used to trim strings
     * @property YAHOO.widget.CalendarNavigator.TRIM
     * @static
     * @type RegExp
     */
    CN.TRIM = /^\s*(.*?)\s*$/;
})();

YAHOO.widget.CalendarNavigator.prototype = {

    /**
     * The unique ID for this CalendarNavigator instance
     * @property id
     * @type String
     */
    id : null,

    /**
     * The Calendar/CalendarGroup instance to which the navigator belongs
     * @property cal
     * @type {Calendar|CalendarGroup}
     */
    cal : null,

    /**
     * Reference to the HTMLElement used to render the navigator's bounding box
     * @property navEl
     * @type HTMLElement
     */
    navEl : null,

    /**
     * Reference to the HTMLElement used to render the navigator's mask
     * @property maskEl
     * @type HTMLElement
     */
    maskEl : null,

    /**
     * Reference to the HTMLElement used to input the year
     * @property yearEl
     * @type HTMLElement
     */
    yearEl : null,

    /**
     * Reference to the HTMLElement used to input the month
     * @property monthEl
     * @type HTMLElement
     */
    monthEl : null,

    /**
     * Reference to the HTMLElement used to display validation errors
     * @property errorEl
     * @type HTMLElement
     */
    errorEl : null,

    /**
     * Reference to the HTMLElement used to update the Calendar/Calendar group
     * with the month/year values
     * @property submitEl
     * @type HTMLElement
     */
    submitEl : null,
    
    /**
     * Reference to the HTMLElement used to hide the navigator without updating the 
     * Calendar/Calendar group
     * @property cancelEl
     * @type HTMLElement
     */
    cancelEl : null,

    /** 
     * Reference to the first focusable control in the navigator (by default monthEl)
     * @property firstCtrl
     * @type HTMLElement
     */
    firstCtrl : null,
    
    /** 
     * Reference to the last focusable control in the navigator (by default cancelEl)
     * @property lastCtrl
     * @type HTMLElement
     */
    lastCtrl : null,

    /**
     * The document containing the Calendar/Calendar group instance
     * @protected
     * @property _doc
     * @type HTMLDocument
     */
    _doc : null,

    /**
     * Internal state property for the current year displayed in the navigator
     * @protected
     * @property _year
     * @type Number
     */
    _year: null,
    
    /**
     * Internal state property for the current month index displayed in the navigator
     * @protected
     * @property _month
     * @type Number
     */
    _month: 0,

    /**
     * Private internal state property which indicates whether or not the 
     * Navigator has been rendered.
     * @private
     * @property __rendered
     * @type Boolean
     */
    __rendered: false,

    /**
     * Init lifecycle method called as part of construction
     * 
     * @method init
     * @param {Calendar} cal The instance of the Calendar or CalendarGroup to which this CalendarNavigator should be attached
     */
    init : function(cal) {
        var calBox = cal.oDomContainer;

        this.cal = cal;
        this.id = calBox.id + YAHOO.widget.CalendarNavigator.ID_SUFFIX;
        this._doc = calBox.ownerDocument;

        /**
         * Private flag, to identify IE Quirks
         * @private
         * @property __isIEQuirks
         */
        var ie = YAHOO.env.ua.ie;
        this.__isIEQuirks = (ie && ((ie <= 6) || (this._doc.compatMode == "BackCompat")));
    },

    /**
     * Displays the navigator and mask, updating the input controls to reflect the 
     * currently set month and year. The show method will invoke the render method
     * if the navigator has not been renderered already, allowing for lazy rendering
     * of the control.
     * 
     * The show method will fire the Calendar/CalendarGroup's beforeShowNav and showNav events
     * 
     * @method show
     */
    show : function() {
        var CLASSES = YAHOO.widget.CalendarNavigator.CLASSES;

        if (this.cal.beforeShowNavEvent.fire()) {
            if (!this.__rendered) {
                this.render();
            }
            this.clearErrors();

            this._updateMonthUI();
            this._updateYearUI();
            this._show(this.navEl, true);

            this.setInitialFocus();
            this.showMask();

            YAHOO.util.Dom.addClass(this.cal.oDomContainer, CLASSES.NAV_VISIBLE);
            this.cal.showNavEvent.fire();
        }
    },

    /**
     * Hides the navigator and mask
     * 
     * The show method will fire the Calendar/CalendarGroup's beforeHideNav event and hideNav events
     * @method hide
     */
    hide : function() {
        var CLASSES = YAHOO.widget.CalendarNavigator.CLASSES;

        if (this.cal.beforeHideNavEvent.fire()) {
            this._show(this.navEl, false);
            this.hideMask();
            YAHOO.util.Dom.removeClass(this.cal.oDomContainer, CLASSES.NAV_VISIBLE);
            this.cal.hideNavEvent.fire();
        }
    },
    

    /**
     * Displays the navigator's mask element
     * 
     * @method showMask
     */
    showMask : function() {
        this._show(this.maskEl, true);
        if (this.__isIEQuirks) {
            this._syncMask();
        }
    },

    /**
     * Hides the navigator's mask element
     * 
     * @method hideMask
     */
    hideMask : function() {
        this._show(this.maskEl, false);
    },

    /**
     * Returns the current month set on the navigator
     * 
     * Note: This may not be the month set in the UI, if 
     * the UI contains an invalid value.
     * 
     * @method getMonth
     * @return {Number} The Navigator's current month index
     */
    getMonth: function() {
        return this._month;
    },

    /**
     * Returns the current year set on the navigator
     * 
     * Note: This may not be the year set in the UI, if 
     * the UI contains an invalid value.
     * 
     * @method getYear
     * @return {Number} The Navigator's current year value
     */
    getYear: function() {
        return this._year;
    },

    /**
     * Sets the current month on the Navigator, and updates the UI
     * 
     * @method setMonth
     * @param {Number} nMonth The month index, from 0 (Jan) through 11 (Dec).
     */
    setMonth : function(nMonth) {
        if (nMonth >= 0 && nMonth < 12) {
            this._month = nMonth;
        }
        this._updateMonthUI();
    },

    /**
     * Sets the current year on the Navigator, and updates the UI. If the 
     * provided year is invalid, it will not be set.
     * 
     * @method setYear
     * @param {Number} nYear The full year value to set the Navigator to.
     */
    setYear : function(nYear) {
        var yrPattern = YAHOO.widget.CalendarNavigator.YR_PATTERN;
        if (YAHOO.lang.isNumber(nYear) && yrPattern.test(nYear+"")) {
            this._year = nYear;
        }
        this._updateYearUI();
    },

    /**
     * Renders the HTML for the navigator, adding it to the 
     * document and attaches event listeners if it has not 
     * already been rendered.
     * 
     * @method render
     */
    render: function() {
        this.cal.beforeRenderNavEvent.fire();
        if (!this.__rendered) {
            this.createNav();
            this.createMask();
            this.applyListeners();
            this.__rendered = true;
        }
        this.cal.renderNavEvent.fire();
    },

    /**
     * Creates the navigator's containing HTMLElement, it's contents, and appends 
     * the containg element to the Calendar/CalendarGroup's container.
     * 
     * @method createNav
     */
    createNav : function() {
        var NAV = YAHOO.widget.CalendarNavigator;
        var doc = this._doc;

        var d = doc.createElement("div");
        d.className = NAV.CLASSES.NAV;

        var htmlBuf = this.renderNavContents([]);

        d.innerHTML = htmlBuf.join('');
        this.cal.oDomContainer.appendChild(d);

        this.navEl = d;

        this.yearEl = doc.getElementById(this.id + NAV.YEAR_SUFFIX);
        this.monthEl = doc.getElementById(this.id + NAV.MONTH_SUFFIX);
        this.errorEl = doc.getElementById(this.id + NAV.ERROR_SUFFIX);
        this.submitEl = doc.getElementById(this.id + NAV.SUBMIT_SUFFIX);
        this.cancelEl = doc.getElementById(this.id + NAV.CANCEL_SUFFIX);

        if (YAHOO.env.ua.gecko && this.yearEl && this.yearEl.type == "text") {
            // Avoid XUL error on focus, select [ https://bugzilla.mozilla.org/show_bug.cgi?id=236791, 
            // supposedly fixed in 1.8.1, but there are reports of it still being around for methods other than blur ]
            this.yearEl.setAttribute("autocomplete", "off");
        }

        this._setFirstLastElements();
    },

    /**
     * Creates the Mask HTMLElement and appends it to the Calendar/CalendarGroups
     * container.
     * 
     * @method createMask
     */
    createMask : function() {
        var C = YAHOO.widget.CalendarNavigator.CLASSES;

        var d = this._doc.createElement("div");
        d.className = C.MASK;

        this.cal.oDomContainer.appendChild(d);
        this.maskEl = d;
    },

    /**
     * Used to set the width/height of the mask in pixels to match the Calendar Container.
     * Currently only used for IE6 or IE in quirks mode. The other A-Grade browser are handled using CSS (width/height 100%).
     * <p>
     * The method is also registered as an HTMLElement resize listener on the Calendars container element.
     * </p>
     * @protected
     * @method _syncMask
     */
    _syncMask : function() {
        var c = this.cal.oDomContainer;
        if (c && this.maskEl) {
            var r = YAHOO.util.Dom.getRegion(c);
            YAHOO.util.Dom.setStyle(this.maskEl, "width", r.right - r.left + "px");
            YAHOO.util.Dom.setStyle(this.maskEl, "height", r.bottom - r.top + "px");
        }
    },

    /**
     * Renders the contents of the navigator. NOTE: The contents of the array passed into this method are added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.
     * 
     * @method renderNavContents
     * 
     * @param {HTML[]} html The HTML buffer to append the HTML to.
     * @return {HTML[]} A reference to the buffer passed in.
     */
    renderNavContents : function(html) {
        var NAV = YAHOO.widget.CalendarNavigator,
            C = NAV.CLASSES,
            h = html; // just to use a shorter name

        h[h.length] = '<div class="' + C.MONTH + '">';
        this.renderMonth(h);
        h[h.length] = '</div>';
        h[h.length] = '<div class="' + C.YEAR + '">';
        this.renderYear(h);
        h[h.length] = '</div>';
        h[h.length] = '<div class="' + C.BUTTONS + '">';
        this.renderButtons(h);
        h[h.length] = '</div>';
        h[h.length] = '<div class="' + C.ERROR + '" id="' + this.id + NAV.ERROR_SUFFIX + '"></div>';

        return h;
    },

    /**
     * Renders the month label and control for the navigator. NOTE: The contents of the array passed into this method are added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.
     * 
     * @method renderNavContents
     * @param {HTML[]} html The HTML buffer to append the HTML to.
     * @return {HTML[]} A reference to the buffer passed in.
     */
    renderMonth : function(html) {
        var NAV = YAHOO.widget.CalendarNavigator,
            C = NAV.CLASSES;

        var id = this.id + NAV.MONTH_SUFFIX,
            mf = this.__getCfg("monthFormat"),
            months = this.cal.cfg.getProperty((mf == YAHOO.widget.Calendar.SHORT) ? "MONTHS_SHORT" : "MONTHS_LONG"),
            h = html;

        if (months && months.length > 0) {
            h[h.length] = '<label for="' + id + '">';
            h[h.length] = this.__getCfg("month", true);
            h[h.length] = '</label>';
            h[h.length] = '<select name="' + id + '" id="' + id + '" class="' + C.MONTH_CTRL + '">';
            for (var i = 0; i < months.length; i++) {
                h[h.length] = '<option value="' + i + '">';
                h[h.length] = months[i];
                h[h.length] = '</option>';
            }
            h[h.length] = '</select>';
        }
        return h;
    },

    /**
     * Renders the year label and control for the navigator. NOTE: The contents of the array passed into this method are added to the DOM as HTML, and should be escaped by the implementor if coming from an external source. 
     * 
     * @method renderYear
     * @param {Array} html The HTML buffer to append the HTML to.
     * @return {Array} A reference to the buffer passed in.
     */
    renderYear : function(html) {
        var NAV = YAHOO.widget.CalendarNavigator,
            C = NAV.CLASSES;

        var id = this.id + NAV.YEAR_SUFFIX,
            size = NAV.YR_MAX_DIGITS,
            h = html;

        h[h.length] = '<label for="' + id + '">';
        h[h.length] = this.__getCfg("year", true);
        h[h.length] = '</label>';
        h[h.length] = '<input type="text" name="' + id + '" id="' + id + '" class="' + C.YEAR_CTRL + '" maxlength="' + size + '"/>';
        return h;
    },

    /**
     * Renders the submit/cancel buttons for the navigator. NOTE: The contents of the array passed into this method are added to the DOM as HTML, and should be escaped by the implementor if coming from an external source.
     * 
     * @method renderButtons
     * @param {Array} html The HTML buffer to append the HTML to.
     * @return {Array} A reference to the buffer passed in.
     */
    renderButtons : function(html) {
        var C = YAHOO.widget.CalendarNavigator.CLASSES;
        var h = html;

        h[h.length] = '<span class="' + C.BUTTON + ' ' + C.DEFAULT + '">';
        h[h.length] = '<button type="button" id="' + this.id + '_submit' + '">';
        h[h.length] = this.__getCfg("submit", true);
        h[h.length] = '</button>';
        h[h.length] = '</span>';
        h[h.length] = '<span class="' + C.BUTTON +'">';
        h[h.length] = '<button type="button" id="' + this.id + '_cancel' + '">';
        h[h.length] = this.__getCfg("cancel", true);
        h[h.length] = '</button>';
        h[h.length] = '</span>';

        return h;
    },

    /**
     * Attaches DOM event listeners to the rendered elements
     * <p>
     * The method will call applyKeyListeners, to setup keyboard specific 
     * listeners
     * </p>
     * @method applyListeners
     */
    applyListeners : function() {
        var E = YAHOO.util.Event;

        function yearUpdateHandler() {
            if (this.validate()) {
                this.setYear(this._getYearFromUI());
            }
        }

        function monthUpdateHandler() {
            this.setMonth(this._getMonthFromUI());
        }

        E.on(this.submitEl, "click", this.submit, this, true);
        E.on(this.cancelEl, "click", this.cancel, this, true);
        E.on(this.yearEl, "blur", yearUpdateHandler, this, true);
        E.on(this.monthEl, "change", monthUpdateHandler, this, true);

        if (this.__isIEQuirks) {
            YAHOO.util.Event.on(this.cal.oDomContainer, "resize", this._syncMask, this, true);
        }

        this.applyKeyListeners();
    },

    /**
     * Removes/purges DOM event listeners from the rendered elements
     * 
     * @method purgeListeners
     */
    purgeListeners : function() {
        var E = YAHOO.util.Event;
        E.removeListener(this.submitEl, "click", this.submit);
        E.removeListener(this.cancelEl, "click", this.cancel);
        E.removeListener(this.yearEl, "blur");
        E.removeListener(this.monthEl, "change");
        if (this.__isIEQuirks) {
            E.removeListener(this.cal.oDomContainer, "resize", this._syncMask);
        }

        this.purgeKeyListeners();
    },

    /**
     * Attaches DOM listeners for keyboard support. 
     * Tab/Shift-Tab looping, Enter Key Submit on Year element,
     * Up/Down/PgUp/PgDown year increment on Year element
     * <p>
     * NOTE: MacOSX Safari 2.x doesn't let you tab to buttons and 
     * MacOSX Gecko does not let you tab to buttons or select controls,
     * so for these browsers, Tab/Shift-Tab looping is limited to the 
     * elements which can be reached using the tab key.
     * </p>
     * @method applyKeyListeners
     */
    applyKeyListeners : function() {
        var E = YAHOO.util.Event,
            ua = YAHOO.env.ua;

        // IE/Safari 3.1 doesn't fire keypress for arrow/pg keys (non-char keys)
        var arrowEvt = (ua.ie || ua.webkit) ? "keydown" : "keypress";

        // - IE/Safari 3.1 doesn't fire keypress for non-char keys
        // - Opera doesn't allow us to cancel keydown or keypress for tab, but 
        //   changes focus successfully on keydown (keypress is too late to change focus - opera's already moved on).
        var tabEvt = (ua.ie || ua.opera || ua.webkit) ? "keydown" : "keypress";

        // Everyone likes keypress for Enter (char keys) - whoo hoo!
        E.on(this.yearEl, "keypress", this._handleEnterKey, this, true);

        E.on(this.yearEl, arrowEvt, this._handleDirectionKeys, this, true);
        E.on(this.lastCtrl, tabEvt, this._handleTabKey, this, true);
        E.on(this.firstCtrl, tabEvt, this._handleShiftTabKey, this, true);
    },

    /**
     * Removes/purges DOM listeners for keyboard support
     *
     * @method purgeKeyListeners
     */
    purgeKeyListeners : function() {
        var E = YAHOO.util.Event,
            ua = YAHOO.env.ua;

        var arrowEvt = (ua.ie || ua.webkit) ? "keydown" : "keypress";
        var tabEvt = (ua.ie || ua.opera || ua.webkit) ? "keydown" : "keypress";

        E.removeListener(this.yearEl, "keypress", this._handleEnterKey);
        E.removeListener(this.yearEl, arrowEvt, this._handleDirectionKeys);
        E.removeListener(this.lastCtrl, tabEvt, this._handleTabKey);
        E.removeListener(this.firstCtrl, tabEvt, this._handleShiftTabKey);
    },

    /**
     * Updates the Calendar/CalendarGroup's pagedate with the currently set month and year if valid.
     * <p>
     * If the currently set month/year is invalid, a validation error will be displayed and the 
     * Calendar/CalendarGroup's pagedate will not be updated.
     * </p>
     * @method submit
     */
    submit : function() {
        if (this.validate()) {
            this.hide();

            this.setMonth(this._getMonthFromUI());
            this.setYear(this._getYearFromUI());

            var cal = this.cal;

            // Artificial delay, just to help the user see something changed
            var delay = YAHOO.widget.CalendarNavigator.UPDATE_DELAY;
            if (delay > 0) {
                var nav = this;
                window.setTimeout(function(){ nav._update(cal); }, delay);
            } else {
                this._update(cal);
            }
        }
    },

    /**
     * Updates the Calendar rendered state, based on the state of the CalendarNavigator
     * @method _update
     * @param cal The Calendar instance to update
     * @protected
     */
    _update : function(cal) {
        var date = YAHOO.widget.DateMath.getDate(this.getYear() - cal.cfg.getProperty("YEAR_OFFSET"), this.getMonth(), 1);
        cal.cfg.setProperty("pagedate", date);
        cal.render();
    },

    /**
     * Hides the navigator and mask, without updating the Calendar/CalendarGroup's state
     * 
     * @method cancel
     */
    cancel : function() {
        this.hide();
    },

    /**
     * Validates the current state of the UI controls
     * 
     * @method validate
     * @return {Boolean} true, if the current UI state contains valid values, false if not
     */
    validate : function() {
        if (this._getYearFromUI() !== null) {
            this.clearErrors();
            return true;
        } else {
            this.setYearError();
            this.setError(this.__getCfg("invalidYear", true));
            return false;
        }
    },

    /**
     * Displays an error message in the Navigator's error panel.
     * 
     * @method setError
     * @param {HTML} msg The markup for the error message to display. NOTE: The msg passed into this method is added to the DOM as HTML, and should be escaped by the implementor if coming from an external source. 
     */
    setError : function(msg) {
        if (this.errorEl) {
            this.errorEl.innerHTML = msg;
            this._show(this.errorEl, true);
        }
    },

    /**
     * Clears the navigator's error message and hides the error panel
     * @method clearError 
     */
    clearError : function() {
        if (this.errorEl) {
            this.errorEl.innerHTML = "";
            this._show(this.errorEl, false);
        }
    },

    /**
     * Displays the validation error UI for the year control
     * @method setYearError
     */
    setYearError : function() {
        YAHOO.util.Dom.addClass(this.yearEl, YAHOO.widget.CalendarNavigator.CLASSES.INVALID);
    },

    /**
     * Removes the validation error UI for the year control
     * @method clearYearError
     */
    clearYearError : function() {
        YAHOO.util.Dom.removeClass(this.yearEl, YAHOO.widget.CalendarNavigator.CLASSES.INVALID);
    },

    /**
     * Clears all validation and error messages in the UI
     * @method clearErrors
     */
    clearErrors : function() {
        this.clearError();
        this.clearYearError();
    },

    /**
     * Sets the initial focus, based on the configured value
     * @method setInitialFocus
     */
    setInitialFocus : function() {
        var el = this.submitEl,
            f = this.__getCfg("initialFocus");

        if (f && f.toLowerCase) {
            f = f.toLowerCase();
            if (f == "year") {
                el = this.yearEl;
                try {
                    this.yearEl.select();
                } catch (selErr) {
                    // Ignore;
                }
            } else if (f == "month") {
                el = this.monthEl;
            }
        }

        if (el && YAHOO.lang.isFunction(el.focus)) {
            try {
                el.focus();
            } catch (focusErr) {
                // TODO: Fall back if focus fails?
            }
        }
    },

    /**
     * Removes all renderered HTML elements for the Navigator from
     * the DOM, purges event listeners and clears (nulls) any property
     * references to HTML references
     * @method erase
     */
    erase : function() {
        if (this.__rendered) {
            this.purgeListeners();

            // Clear out innerHTML references
            this.yearEl = null;
            this.monthEl = null;
            this.errorEl = null;
            this.submitEl = null;
            this.cancelEl = null;
            this.firstCtrl = null;
            this.lastCtrl = null;
            if (this.navEl) {
                this.navEl.innerHTML = "";
            }

            var p = this.navEl.parentNode;
            if (p) {
                p.removeChild(this.navEl);
            }
            this.navEl = null;

            var pm = this.maskEl.parentNode;
            if (pm) {
                pm.removeChild(this.maskEl);
            }
            this.maskEl = null;
            this.__rendered = false;
        }
    },

    /**
     * Destroys the Navigator object and any HTML references
     * @method destroy
     */
    destroy : function() {
        this.erase();
        this._doc = null;
        this.cal = null;
        this.id = null;
    },

    /**
     * Protected implementation to handle how UI elements are 
     * hidden/shown.
     *
     * @method _show
     * @protected
     */
    _show : function(el, bShow) {
        if (el) {
            YAHOO.util.Dom.setStyle(el, "display", (bShow) ? "block" : "none");
        }
    },

    /**
     * Returns the month value (index), from the month UI element
     * @protected
     * @method _getMonthFromUI
     * @return {Number} The month index, or 0 if a UI element for the month
     * is not found
     */
    _getMonthFromUI : function() {
        if (this.monthEl) {
            return this.monthEl.selectedIndex;
        } else {
            return 0; // Default to Jan
        }
    },

    /**
     * Returns the year value, from the Navitator's year UI element
     * @protected
     * @method _getYearFromUI
     * @return {Number} The year value set in the UI, if valid. null is returned if 
     * the UI does not contain a valid year value.
     */
    _getYearFromUI : function() {
        var NAV = YAHOO.widget.CalendarNavigator;

        var yr = null;
        if (this.yearEl) {
            var value = this.yearEl.value;
            value = value.replace(NAV.TRIM, "$1");

            if (NAV.YR_PATTERN.test(value)) {
                yr = parseInt(value, 10);
            }
        }
        return yr;
    },

    /**
     * Updates the Navigator's year UI, based on the year value set on the Navigator object
     * @protected
     * @method _updateYearUI
     */
    _updateYearUI : function() {
        if (this.yearEl && this._year !== null) {
            this.yearEl.value = this._year;
        }
    },

    /**
     * Updates the Navigator's month UI, based on the month value set on the Navigator object
     * @protected
     * @method _updateMonthUI
     */
    _updateMonthUI : function() {
        if (this.monthEl) {
            this.monthEl.selectedIndex = this._month;
        }
    },

    /**
     * Sets up references to the first and last focusable element in the Navigator's UI
     * in terms of tab order (Naviagator's firstEl and lastEl properties). The references
     * are used to control modality by looping around from the first to the last control
     * and visa versa for tab/shift-tab navigation.
     * <p>
     * See <a href="#applyKeyListeners">applyKeyListeners</a>
     * </p>
     * @protected
     * @method _setFirstLastElements
     */
    _setFirstLastElements : function() {
        this.firstCtrl = this.monthEl;
        this.lastCtrl = this.cancelEl;

        // Special handling for MacOSX.
        // - Safari 2.x can't focus on buttons
        // - Gecko can't focus on select boxes or buttons
        if (this.__isMac) {
            if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420){
                this.firstCtrl = this.monthEl;
                this.lastCtrl = this.yearEl;
            }
            if (YAHOO.env.ua.gecko) {
                this.firstCtrl = this.yearEl;
                this.lastCtrl = this.yearEl;
            }
        }
    },

    /**
     * Default Keyboard event handler to capture Enter 
     * on the Navigator's year control (yearEl)
     * 
     * @method _handleEnterKey
     * @protected
     * @param {Event} e The DOM event being handled
     */
    _handleEnterKey : function(e) {
        var KEYS = YAHOO.util.KeyListener.KEY;

        if (YAHOO.util.Event.getCharCode(e) == KEYS.ENTER) {
            YAHOO.util.Event.preventDefault(e);
            this.submit();
        }
    },

    /**
     * Default Keyboard event handler to capture up/down/pgup/pgdown
     * on the Navigator's year control (yearEl).
     * 
     * @method _handleDirectionKeys
     * @protected
     * @param {Event} e The DOM event being handled
     */
    _handleDirectionKeys : function(e) {
        var E = YAHOO.util.Event,
            KEYS = YAHOO.util.KeyListener.KEY,
            NAV = YAHOO.widget.CalendarNavigator;

        var value = (this.yearEl.value) ? parseInt(this.yearEl.value, 10) : null;
        if (isFinite(value)) {
            var dir = false;
            switch(E.getCharCode(e)) {
                case KEYS.UP:
                    this.yearEl.value = value + NAV.YR_MINOR_INC;
                    dir = true;
                    break;
                case KEYS.DOWN:
                    this.yearEl.value = Math.max(value - NAV.YR_MINOR_INC, 0);
                    dir = true;
                    break;
                case KEYS.PAGE_UP:
                    this.yearEl.value = value + NAV.YR_MAJOR_INC;
                    dir = true;
                    break;
                case KEYS.PAGE_DOWN:
                    this.yearEl.value = Math.max(value - NAV.YR_MAJOR_INC, 0);
                    dir = true;
                    break;
                default:
                    break;
            }
            if (dir) {
                E.preventDefault(e);
                try {
                    this.yearEl.select();
                } catch(err) {
                    // Ignore
                }
            }
        }
    },

    /**
     * Default Keyboard event handler to capture Tab 
     * on the last control (lastCtrl) in the Navigator.
     * 
     * @method _handleTabKey
     * @protected
     * @param {Event} e The DOM event being handled
     */
    _handleTabKey : function(e) {
        var E = YAHOO.util.Event,
            KEYS = YAHOO.util.KeyListener.KEY;

        if (E.getCharCode(e) == KEYS.TAB && !e.shiftKey) {
            try {
                E.preventDefault(e);
                this.firstCtrl.focus();
            } catch (err) {
                // Ignore - mainly for focus edge cases
            }
        }
    },

    /**
     * Default Keyboard event handler to capture Shift-Tab 
     * on the first control (firstCtrl) in the Navigator.
     * 
     * @method _handleShiftTabKey
     * @protected
     * @param {Event} e The DOM event being handled
     */
    _handleShiftTabKey : function(e) {
        var E = YAHOO.util.Event,
            KEYS = YAHOO.util.KeyListener.KEY;

        if (e.shiftKey && E.getCharCode(e) == KEYS.TAB) {
            try {
                E.preventDefault(e);
                this.lastCtrl.focus();
            } catch (err) {
                // Ignore - mainly for focus edge cases
            }
        }
    },

    /**
     * Retrieve Navigator configuration values from 
     * the parent Calendar/CalendarGroup's config value.
     * <p>
     * If it has not been set in the user provided configuration, the method will 
     * return the default value of the configuration property, as set in DEFAULT_CONFIG
     * </p>
     * @private
     * @method __getCfg
     * @param {String} Case sensitive property name.
     * @param {Boolean} true, if the property is a string property, false if not.
     * @return The value of the configuration property
     */
    __getCfg : function(prop, bIsStr) {
        var DEF_CFG = YAHOO.widget.CalendarNavigator.DEFAULT_CONFIG;
        var cfg = this.cal.cfg.getProperty("navigator");

        if (bIsStr) {
            return (cfg !== true && cfg.strings && cfg.strings[prop]) ? cfg.strings[prop] : DEF_CFG.strings[prop];
        } else {
            return (cfg !== true && cfg[prop]) ? cfg[prop] : DEF_CFG[prop];
        }
    },

    /**
     * Private flag, to identify MacOS
     * @private
     * @property __isMac
     */
    __isMac : (navigator.userAgent.toLowerCase().indexOf("macintosh") != -1)

};
YAHOO.register("calendar", YAHOO.widget.Calendar, {version: "2.9.0", build: "2800"});
/*
Copyright (c) 2011, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 2.9.0
*/
(function () {

var lang   = YAHOO.lang,
    util   = YAHOO.util,
    Ev     = util.Event;

/**
 * The DataSource utility provides a common configurable interface for widgets to
 * access a variety of data, from JavaScript arrays to online database servers.
 *
 * @module datasource
 * @requires yahoo, event
 * @optional json, get, connection 
 * @title DataSource Utility
 */

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * Base class for the YUI DataSource utility.
 *
 * @namespace YAHOO.util
 * @class YAHOO.util.DataSourceBase
 * @constructor
 * @param oLiveData {HTMLElement}  Pointer to live data.
 * @param oConfigs {object} (optional) Object literal of configuration values.
 */
util.DataSourceBase = function(oLiveData, oConfigs) {
    if(oLiveData === null || oLiveData === undefined) {
        YAHOO.log("Could not instantiate DataSource due to invalid live database",
                "error", this.toString());
        return;
    }
    
    this.liveData = oLiveData;
    this._oQueue = {interval:null, conn:null, requests:[]};
    this.responseSchema = {};   

    // Set any config params passed in to override defaults
    if(oConfigs && (oConfigs.constructor == Object)) {
        for(var sConfig in oConfigs) {
            if(sConfig) {
                this[sConfig] = oConfigs[sConfig];
            }
        }
    }
    
    // Validate and initialize public configs
    var maxCacheEntries = this.maxCacheEntries;
    if(!lang.isNumber(maxCacheEntries) || (maxCacheEntries < 0)) {
        maxCacheEntries = 0;
    }

    // Initialize interval tracker
    this._aIntervals = [];

    /////////////////////////////////////////////////////////////////////////////
    //
    // Custom Events
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Fired when a request is made to the local cache.
     *
     * @event cacheRequestEvent
     * @param oArgs.request {Object} The request object.
     * @param oArgs.callback {Object} The callback object.
     * @param oArgs.caller {Object} (deprecated) Use callback.scope.
     */
    this.createEvent("cacheRequestEvent");

    /**
     * Fired when data is retrieved from the local cache.
     *
     * @event cacheResponseEvent
     * @param oArgs.request {Object} The request object.
     * @param oArgs.response {Object} The response object.
     * @param oArgs.callback {Object} The callback object.
     * @param oArgs.caller {Object} (deprecated) Use callback.scope.
     */
    this.createEvent("cacheResponseEvent");

    /**
     * Fired when a request is sent to the live data source.
     *
     * @event requestEvent
     * @param oArgs.request {Object} The request object.
     * @param oArgs.callback {Object} The callback object.
     * @param oArgs.tId {Number} Transaction ID.     
     * @param oArgs.caller {Object} (deprecated) Use callback.scope.
     */
    this.createEvent("requestEvent");

    /**
     * Fired when live data source sends response.
     *
     * @event responseEvent
     * @param oArgs.request {Object} The request object.
     * @param oArgs.response {Object} The raw response object.
     * @param oArgs.callback {Object} The callback object.
     * @param oArgs.tId {Number} Transaction ID.     
     * @param oArgs.caller {Object} (deprecated) Use callback.scope.
     */
    this.createEvent("responseEvent");

    /**
     * Fired when response is parsed.
     *
     * @event responseParseEvent
     * @param oArgs.request {Object} The request object.
     * @param oArgs.response {Object} The parsed response object.
     * @param oArgs.callback {Object} The callback object.
     * @param oArgs.caller {Object} (deprecated) Use callback.scope.
     */
    this.createEvent("responseParseEvent");

    /**
     * Fired when response is cached.
     *
     * @event responseCacheEvent
     * @param oArgs.request {Object} The request object.
     * @param oArgs.response {Object} The parsed response object.
     * @param oArgs.callback {Object} The callback object.
     * @param oArgs.caller {Object} (deprecated) Use callback.scope.
     */
    this.createEvent("responseCacheEvent");
    /**
     * Fired when an error is encountered with the live data source.
     *
     * @event dataErrorEvent
     * @param oArgs.request {Object} The request object.
     * @param oArgs.response {String} The response object (if available).
     * @param oArgs.callback {Object} The callback object.
     * @param oArgs.caller {Object} (deprecated) Use callback.scope.
     * @param oArgs.message {String} The error message.
     */
    this.createEvent("dataErrorEvent");

    /**
     * Fired when the local cache is flushed.
     *
     * @event cacheFlushEvent
     */
    this.createEvent("cacheFlushEvent");

    var DS = util.DataSourceBase;
    this._sName = "DataSource instance" + DS._nIndex;
    DS._nIndex++;
    YAHOO.log("DataSource initialized", "info", this.toString());
};

var DS = util.DataSourceBase;

lang.augmentObject(DS, {

/////////////////////////////////////////////////////////////////////////////
//
// DataSourceBase public constants
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Type is unknown.
 *
 * @property TYPE_UNKNOWN
 * @type Number
 * @final
 * @default -1
 */
TYPE_UNKNOWN : -1,

/**
 * Type is a JavaScript Array.
 *
 * @property TYPE_JSARRAY
 * @type Number
 * @final
 * @default 0
 */
TYPE_JSARRAY : 0,

/**
 * Type is a JavaScript Function.
 *
 * @property TYPE_JSFUNCTION
 * @type Number
 * @final
 * @default 1
 */
TYPE_JSFUNCTION : 1,

/**
 * Type is hosted on a server via an XHR connection.
 *
 * @property TYPE_XHR
 * @type Number
 * @final
 * @default 2
 */
TYPE_XHR : 2,

/**
 * Type is JSON.
 *
 * @property TYPE_JSON
 * @type Number
 * @final
 * @default 3
 */
TYPE_JSON : 3,

/**
 * Type is XML.
 *
 * @property TYPE_XML
 * @type Number
 * @final
 * @default 4
 */
TYPE_XML : 4,

/**
 * Type is plain text.
 *
 * @property TYPE_TEXT
 * @type Number
 * @final
 * @default 5
 */
TYPE_TEXT : 5,

/**
 * Type is an HTML TABLE element. Data is parsed out of TR elements from all TBODY elements.
 *
 * @property TYPE_HTMLTABLE
 * @type Number
 * @final
 * @default 6
 */
TYPE_HTMLTABLE : 6,

/**
 * Type is hosted on a server via a dynamic script node.
 *
 * @property TYPE_SCRIPTNODE
 * @type Number
 * @final
 * @default 7
 */
TYPE_SCRIPTNODE : 7,

/**
 * Type is local.
 *
 * @property TYPE_LOCAL
 * @type Number
 * @final
 * @default 8
 */
TYPE_LOCAL : 8,

/**
 * Error message for invalid dataresponses.
 *
 * @property ERROR_DATAINVALID
 * @type String
 * @final
 * @default "Invalid data"
 */
ERROR_DATAINVALID : "Invalid data",

/**
 * Error message for null data responses.
 *
 * @property ERROR_DATANULL
 * @type String
 * @final
 * @default "Null data"
 */
ERROR_DATANULL : "Null data",

/////////////////////////////////////////////////////////////////////////////
//
// DataSourceBase private static properties
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Internal class variable to index multiple DataSource instances.
 *
 * @property DataSourceBase._nIndex
 * @type Number
 * @private
 * @static
 */
_nIndex : 0,

/**
 * Internal class variable to assign unique transaction IDs.
 *
 * @property DataSourceBase._nTransactionId
 * @type Number
 * @private
 * @static
 */
_nTransactionId : 0,

/////////////////////////////////////////////////////////////////////////////
//
// DataSourceBase private static methods
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Clones object literal or array of object literals.
 *
 * @method DataSourceBase._cloneObject
 * @param o {Object} Object.
 * @private
 * @static
 */
_cloneObject: function(o) {
    if(!lang.isValue(o)) {
        return o;
    }

    var copy = {};

    if(Object.prototype.toString.apply(o) === "[object RegExp]") {
        copy = o;
    }
    else if(lang.isFunction(o)) {
        copy = o;
    }
    else if(lang.isArray(o)) {
        var array = [];
        for(var i=0,len=o.length;i<len;i++) {
            array[i] = DS._cloneObject(o[i]);
        }
        copy = array;
    }
    else if(lang.isObject(o)) {
        for (var x in o){
            if(lang.hasOwnProperty(o, x)) {
                if(lang.isValue(o[x]) && lang.isObject(o[x]) || lang.isArray(o[x])) {
                    copy[x] = DS._cloneObject(o[x]);
                }
                else {
                    copy[x] = o[x];
                }
            }
        }
    }
    else {
        copy = o;
    }

    return copy;
},
    
/**
 * Get an XPath-specified value for a given field from an XML node or document.
 *
 * @method _getLocationValue
 * @param field {String | Object} Field definition.
 * @param context {Object} XML node or document to search within.
 * @return {Object} Data value or null.
 * @static
 * @private
 */
_getLocationValue: function(field, context) {
    var locator = field.locator || field.key || field,
        xmldoc = context.ownerDocument || context,
        result, res, value = null;

    try {
        // Standards mode
        if(!lang.isUndefined(xmldoc.evaluate)) {
            result = xmldoc.evaluate(locator, context, xmldoc.createNSResolver(!context.ownerDocument ? context.documentElement : context.ownerDocument.documentElement), 0, null);
            while(res = result.iterateNext()) {
                value = res.textContent;
            }
        }
        // IE mode
        else {
            xmldoc.setProperty("SelectionLanguage", "XPath");
            result = context.selectNodes(locator)[0];
            value = result.value || result.text || null;
        }
        return value;

    }
    catch(e) {
    }
},

/////////////////////////////////////////////////////////////////////////////
//
// DataSourceBase public static methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Executes a configured callback.  For object literal callbacks, the third
 * param determines whether to execute the success handler or failure handler.
 *  
 * @method issueCallback
 * @param callback {Function|Object} the callback to execute
 * @param params {Array} params to be passed to the callback method
 * @param error {Boolean} whether an error occurred
 * @param scope {Object} the scope from which to execute the callback
 * (deprecated - use an object literal callback)
 * @static     
 */
issueCallback : function (callback,params,error,scope) {
    if (lang.isFunction(callback)) {
        callback.apply(scope, params);
    } else if (lang.isObject(callback)) {
        scope = callback.scope || scope || window;
        var callbackFunc = callback.success;
        if (error) {
            callbackFunc = callback.failure;
        }
        if (callbackFunc) {
            callbackFunc.apply(scope, params.concat([callback.argument]));
        }
    }
},

/**
 * Converts data to type String.
 *
 * @method DataSourceBase.parseString
 * @param oData {String | Number | Boolean | Date | Array | Object} Data to parse.
 * The special values null and undefined will return null.
 * @return {String} A string, or null.
 * @static
 */
parseString : function(oData) {
    // Special case null and undefined
    if(!lang.isValue(oData)) {
        return null;
    }
    
    //Convert to string
    var string = oData + "";

    // Validate
    if(lang.isString(string)) {
        return string;
    }
    else {
        YAHOO.log("Could not convert data " + lang.dump(oData) + " to type String", "warn", this.toString());
        return null;
    }
},

/**
 * Converts data to type Number.
 *
 * @method DataSourceBase.parseNumber
 * @param oData {String | Number | Boolean} Data to convert. Note, the following
 * values return as null: null, undefined, NaN, "". 
 * @return {Number} A number, or null.
 * @static
 */
parseNumber : function(oData) {
    if(!lang.isValue(oData) || (oData === "")) {
        return null;
    }

    //Convert to number
    var number = oData * 1;
    
    // Validate
    if(lang.isNumber(number)) {
        return number;
    }
    else {
        YAHOO.log("Could not convert data " + lang.dump(oData) + " to type Number", "warn", this.toString());
        return null;
    }
},
// Backward compatibility
convertNumber : function(oData) {
    YAHOO.log("The method YAHOO.util.DataSourceBase.convertNumber() has been" +
    " deprecated in favor of YAHOO.util.DataSourceBase.parseNumber()", "warn",
    this.toString());
    return DS.parseNumber(oData);
},

/**
 * Converts data to type Date.
 *
 * @method DataSourceBase.parseDate
 * @param oData {Date | String | Number} Data to convert.
 * @return {Date} A Date instance.
 * @static
 */
parseDate : function(oData) {
    var date = null;
    
    //Convert to date
    if(lang.isValue(oData) && !(oData instanceof Date)) {
        date = new Date(oData);
    }
    else {
        return oData;
    }
    
    // Validate
    if(date instanceof Date) {
        return date;
    }
    else {
        YAHOO.log("Could not convert data " + lang.dump(oData) + " to type Date", "warn", this.toString());
        return null;
    }
},
// Backward compatibility
convertDate : function(oData) {
    YAHOO.log("The method YAHOO.util.DataSourceBase.convertDate() has been" +
    " deprecated in favor of YAHOO.util.DataSourceBase.parseDate()", "warn",
    this.toString());
    return DS.parseDate(oData);
}

});

// Done in separate step so referenced functions are defined.
/**
 * Data parsing functions.
 * @property DataSource.Parser
 * @type Object
 * @static
 */
DS.Parser = {
    string   : DS.parseString,
    number   : DS.parseNumber,
    date     : DS.parseDate
};

// Prototype properties and methods
DS.prototype = {

/////////////////////////////////////////////////////////////////////////////
//
// DataSourceBase private properties
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Name of DataSource instance.
 *
 * @property _sName
 * @type String
 * @private
 */
_sName : null,

/**
 * Local cache of data result object literals indexed chronologically.
 *
 * @property _aCache
 * @type Object[]
 * @private
 */
_aCache : null,

/**
 * Local queue of request connections, enabled if queue needs to be managed.
 *
 * @property _oQueue
 * @type Object
 * @private
 */
_oQueue : null,

/**
 * Array of polling interval IDs that have been enabled, needed to clear all intervals.
 *
 * @property _aIntervals
 * @type Array
 * @private
 */
_aIntervals : null,

/////////////////////////////////////////////////////////////////////////////
//
// DataSourceBase public properties
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Max size of the local cache.  Set to 0 to turn off caching.  Caching is
 * useful to reduce the number of server connections.  Recommended only for data
 * sources that return comprehensive results for queries or when stale data is
 * not an issue.
 *
 * @property maxCacheEntries
 * @type Number
 * @default 0
 */
maxCacheEntries : 0,

 /**
 * Pointer to live database.
 *
 * @property liveData
 * @type Object
 */
liveData : null,

/**
 * Where the live data is held:
 * 
 * <dl>  
 *    <dt>TYPE_UNKNOWN</dt>
 *    <dt>TYPE_LOCAL</dt>
 *    <dt>TYPE_XHR</dt>
 *    <dt>TYPE_SCRIPTNODE</dt>
 *    <dt>TYPE_JSFUNCTION</dt>
 * </dl> 
 *  
 * @property dataType
 * @type Number
 * @default YAHOO.util.DataSourceBase.TYPE_UNKNOWN
 *
 */
dataType : DS.TYPE_UNKNOWN,

/**
 * Format of response:
 *  
 * <dl>  
 *    <dt>TYPE_UNKNOWN</dt>
 *    <dt>TYPE_JSARRAY</dt>
 *    <dt>TYPE_JSON</dt>
 *    <dt>TYPE_XML</dt>
 *    <dt>TYPE_TEXT</dt>
 *    <dt>TYPE_HTMLTABLE</dt> 
 * </dl> 
 *
 * @property responseType
 * @type Number
 * @default YAHOO.util.DataSourceBase.TYPE_UNKNOWN
 */
responseType : DS.TYPE_UNKNOWN,

/**
 * Response schema object literal takes a combination of the following properties:
 *
 * <dl>
 * <dt>resultsList</dt> <dd>Pointer to array of tabular data</dd>
 * <dt>resultNode</dt> <dd>Pointer to node name of row data (XML data only)</dd>
 * <dt>recordDelim</dt> <dd>Record delimiter (text data only)</dd>
 * <dt>fieldDelim</dt> <dd>Field delimiter (text data only)</dd>
 * <dt>fields</dt> <dd>Array of field names (aka keys), or array of object literals
 * such as: {key:"fieldname",parser:YAHOO.util.DataSourceBase.parseDate}</dd>
 * <dt>metaFields</dt> <dd>Object literal of keys to include in the oParsedResponse.meta collection</dd>
 * <dt>metaNode</dt> <dd>Name of the node under which to search for meta information in XML response data</dd>
 * </dl>
 *
 * @property responseSchema
 * @type Object
 */
responseSchema : null,

/**
 * Additional arguments passed to the JSON parse routine.  The JSON string
 * is the assumed first argument (where applicable).  This property is not
 * set by default, but the parse methods will use it if present.
 *
 * @property parseJSONArgs
 * @type {MIXED|Array} If an Array, contents are used as individual arguments.
 *                     Otherwise, value is used as an additional argument.
 */
// property intentionally undefined
 
/**
 * When working with XML data, setting this property to true enables support for
 * XPath-syntaxed locators in schema definitions.
 *
 * @property useXPath
 * @type Boolean
 * @default false
 */
useXPath : false,

/**
 * Clones entries before adding to cache.
 *
 * @property cloneBeforeCaching
 * @type Boolean
 * @default false
 */
cloneBeforeCaching : false,

/////////////////////////////////////////////////////////////////////////////
//
// DataSourceBase public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Public accessor to the unique name of the DataSource instance.
 *
 * @method toString
 * @return {String} Unique name of the DataSource instance.
 */
toString : function() {
    return this._sName;
},

/**
 * Overridable method passes request to cache and returns cached response if any,
 * refreshing the hit in the cache as the newest item. Returns null if there is
 * no cache hit.
 *
 * @method getCachedResponse
 * @param oRequest {Object} Request object.
 * @param oCallback {Object} Callback object.
 * @param oCaller {Object} (deprecated) Use callback object.
 * @return {Object} Cached response object or null.
 */
getCachedResponse : function(oRequest, oCallback, oCaller) {
    var aCache = this._aCache;

    // If cache is enabled...
    if(this.maxCacheEntries > 0) {        
        // Initialize local cache
        if(!aCache) {
            this._aCache = [];
            YAHOO.log("Cache initialized", "info", this.toString());
        }
        // Look in local cache
        else {
            var nCacheLength = aCache.length;
            if(nCacheLength > 0) {
                var oResponse = null;
                this.fireEvent("cacheRequestEvent", {request:oRequest,callback:oCallback,caller:oCaller});
        
                // Loop through each cached element
                for(var i = nCacheLength-1; i >= 0; i--) {
                    var oCacheElem = aCache[i];
        
                    // Defer cache hit logic to a public overridable method
                    if(this.isCacheHit(oRequest,oCacheElem.request)) {
                        // The cache returned a hit!
                        // Grab the cached response
                        oResponse = oCacheElem.response;
                        this.fireEvent("cacheResponseEvent", {request:oRequest,response:oResponse,callback:oCallback,caller:oCaller});
                        
                        // Refresh the position of the cache hit
                        if(i < nCacheLength-1) {
                            // Remove element from its original location
                            aCache.splice(i,1);
                            // Add as newest
                            this.addToCache(oRequest, oResponse);
                            YAHOO.log("Refreshed cache position of the response for \"" +  oRequest + "\"", "info", this.toString());
                        }
                        
                        // Add a cache flag
                        oResponse.cached = true;
                        break;
                    }
                }
                YAHOO.log("The cached response for \"" + lang.dump(oRequest) +
                        "\" is " + lang.dump(oResponse), "info", this.toString());
                return oResponse;
            }
        }
    }
    else if(aCache) {
        this._aCache = null;
        YAHOO.log("Cache destroyed", "info", this.toString());
    }
    return null;
},

/**
 * Default overridable method matches given request to given cached request.
 * Returns true if is a hit, returns false otherwise.  Implementers should
 * override this method to customize the cache-matching algorithm.
 *
 * @method isCacheHit
 * @param oRequest {Object} Request object.
 * @param oCachedRequest {Object} Cached request object.
 * @return {Boolean} True if given request matches cached request, false otherwise.
 */
isCacheHit : function(oRequest, oCachedRequest) {
    return (oRequest === oCachedRequest);
},

/**
 * Adds a new item to the cache. If cache is full, evicts the stalest item
 * before adding the new item.
 *
 * @method addToCache
 * @param oRequest {Object} Request object.
 * @param oResponse {Object} Response object to cache.
 */
addToCache : function(oRequest, oResponse) {
    var aCache = this._aCache;
    if(!aCache) {
        return;
    }

    // If the cache is full, make room by removing stalest element (index=0)
    while(aCache.length >= this.maxCacheEntries) {
        aCache.shift();
    }

    // Add to cache in the newest position, at the end of the array
    oResponse = (this.cloneBeforeCaching) ? DS._cloneObject(oResponse) : oResponse;
    var oCacheElem = {request:oRequest,response:oResponse};
    aCache[aCache.length] = oCacheElem;
    this.fireEvent("responseCacheEvent", {request:oRequest,response:oResponse});
    YAHOO.log("Cached the response for \"" +  oRequest + "\"", "info", this.toString());
},

/**
 * Flushes cache.
 *
 * @method flushCache
 */
flushCache : function() {
    if(this._aCache) {
        this._aCache = [];
        this.fireEvent("cacheFlushEvent");
        YAHOO.log("Flushed the cache", "info", this.toString());
    }
},

/**
 * Sets up a polling mechanism to send requests at set intervals and forward
 * responses to given callback.
 *
 * @method setInterval
 * @param nMsec {Number} Length of interval in milliseconds.
 * @param oRequest {Object} Request object.
 * @param oCallback {Function} Handler function to receive the response.
 * @param oCaller {Object} (deprecated) Use oCallback.scope.
 * @return {Number} Interval ID.
 */
setInterval : function(nMsec, oRequest, oCallback, oCaller) {
    if(lang.isNumber(nMsec) && (nMsec >= 0)) {
        YAHOO.log("Enabling polling to live data for \"" + oRequest + "\" at interval " + nMsec, "info", this.toString());
        var oSelf = this;
        var nId = setInterval(function() {
            oSelf.makeConnection(oRequest, oCallback, oCaller);
        }, nMsec);
        this._aIntervals.push(nId);
        return nId;
    }
    else {
        YAHOO.log("Could not enable polling to live data for \"" + oRequest + "\" at interval " + nMsec, "info", this.toString());
    }
},

/**
 * Disables polling mechanism associated with the given interval ID. Does not
 * affect transactions that are in progress.
 *
 * @method clearInterval
 * @param nId {Number} Interval ID.
 */
clearInterval : function(nId) {
    // Remove from tracker if there
    var tracker = this._aIntervals || [];
    for(var i=tracker.length-1; i>-1; i--) {
        if(tracker[i] === nId) {
            tracker.splice(i,1);
            clearInterval(nId);
        }
    }
},

/**
 * Disables all known polling intervals. Does not affect transactions that are
 * in progress.
 *
 * @method clearAllIntervals
 */
clearAllIntervals : function() {
    var tracker = this._aIntervals || [];
    for(var i=tracker.length-1; i>-1; i--) {
        clearInterval(tracker[i]);
    }
    tracker = [];
},

/**
 * First looks for cached response, then sends request to live data. The
 * following arguments are passed to the callback function:
 *     <dl>
 *     <dt><code>oRequest</code></dt>
 *     <dd>The same value that was passed in as the first argument to sendRequest.</dd>
 *     <dt><code>oParsedResponse</code></dt>
 *     <dd>An object literal containing the following properties:
 *         <dl>
 *         <dt><code>tId</code></dt>
 *         <dd>Unique transaction ID number.</dd>
 *         <dt><code>results</code></dt>
 *         <dd>Schema-parsed data results.</dd>
 *         <dt><code>error</code></dt>
 *         <dd>True in cases of data error.</dd>
 *         <dt><code>cached</code></dt>
 *         <dd>True when response is returned from DataSource cache.</dd> 
 *         <dt><code>meta</code></dt>
 *         <dd>Schema-parsed meta data.</dd>
 *         </dl>
 *     <dt><code>oPayload</code></dt>
 *     <dd>The same value as was passed in as <code>argument</code> in the oCallback object literal.</dd>
 *     </dl> 
 *
 * @method sendRequest
 * @param oRequest {Object} Request object.
 * @param oCallback {Object} An object literal with the following properties:
 *     <dl>
 *     <dt><code>success</code></dt>
 *     <dd>The function to call when the data is ready.</dd>
 *     <dt><code>failure</code></dt>
 *     <dd>The function to call upon a response failure condition.</dd>
 *     <dt><code>scope</code></dt>
 *     <dd>The object to serve as the scope for the success and failure handlers.</dd>
 *     <dt><code>argument</code></dt>
 *     <dd>Arbitrary data that will be passed back to the success and failure handlers.</dd>
 *     </dl> 
 * @param oCaller {Object} (deprecated) Use oCallback.scope.
 * @return {Number} Transaction ID, or null if response found in cache.
 */
sendRequest : function(oRequest, oCallback, oCaller) {
    // First look in cache
    var oCachedResponse = this.getCachedResponse(oRequest, oCallback, oCaller);
    if(oCachedResponse) {
        DS.issueCallback(oCallback,[oRequest,oCachedResponse],false,oCaller);
        return null;
    }


    // Not in cache, so forward request to live data
    YAHOO.log("Making connection to live data for \"" + oRequest + "\"", "info", this.toString());
    return this.makeConnection(oRequest, oCallback, oCaller);
},

/**
 * Overridable default method generates a unique transaction ID and passes 
 * the live data reference directly to the  handleResponse function. This
 * method should be implemented by subclasses to achieve more complex behavior
 * or to access remote data.          
 *
 * @method makeConnection
 * @param oRequest {Object} Request object.
 * @param oCallback {Object} Callback object literal.
 * @param oCaller {Object} (deprecated) Use oCallback.scope.
 * @return {Number} Transaction ID.
 */
makeConnection : function(oRequest, oCallback, oCaller) {
    var tId = DS._nTransactionId++;
    this.fireEvent("requestEvent", {tId:tId, request:oRequest,callback:oCallback,caller:oCaller});

    /* accounts for the following cases:
    YAHOO.util.DataSourceBase.TYPE_UNKNOWN
    YAHOO.util.DataSourceBase.TYPE_JSARRAY
    YAHOO.util.DataSourceBase.TYPE_JSON
    YAHOO.util.DataSourceBase.TYPE_HTMLTABLE
    YAHOO.util.DataSourceBase.TYPE_XML
    YAHOO.util.DataSourceBase.TYPE_TEXT
    */
    var oRawResponse = this.liveData;
    
    this.handleResponse(oRequest, oRawResponse, oCallback, oCaller, tId);
    return tId;
},

/**
 * Receives raw data response and type converts to XML, JSON, etc as necessary.
 * Forwards oFullResponse to appropriate parsing function to get turned into
 * oParsedResponse. Calls doBeforeCallback() and adds oParsedResponse to 
 * the cache when appropriate before calling issueCallback().
 * 
 * The oParsedResponse object literal has the following properties:
 * <dl>
 *     <dd><dt>tId {Number}</dt> Unique transaction ID</dd>
 *     <dd><dt>results {Array}</dt> Array of parsed data results</dd>
 *     <dd><dt>meta {Object}</dt> Object literal of meta values</dd> 
 *     <dd><dt>error {Boolean}</dt> (optional) True if there was an error</dd>
 *     <dd><dt>cached {Boolean}</dt> (optional) True if response was cached</dd>
 * </dl>
 *
 * @method handleResponse
 * @param oRequest {Object} Request object
 * @param oRawResponse {Object} The raw response from the live database.
 * @param oCallback {Object} Callback object literal.
 * @param oCaller {Object} (deprecated) Use oCallback.scope.
 * @param tId {Number} Transaction ID.
 */
handleResponse : function(oRequest, oRawResponse, oCallback, oCaller, tId) {
    this.fireEvent("responseEvent", {tId:tId, request:oRequest, response:oRawResponse,
            callback:oCallback, caller:oCaller});
    YAHOO.log("Received live data response for \"" + oRequest + "\"", "info", this.toString());
    var xhr = (this.dataType == DS.TYPE_XHR) ? true : false;
    var oParsedResponse = null;
    var oFullResponse = oRawResponse;
    
    // Try to sniff data type if it has not been defined
    if(this.responseType === DS.TYPE_UNKNOWN) {
        var ctype = (oRawResponse && oRawResponse.getResponseHeader) ? oRawResponse.getResponseHeader["Content-Type"] : null;
        if(ctype) {
             // xml
            if(ctype.indexOf("text/xml") > -1) {
                this.responseType = DS.TYPE_XML;
            }
            else if(ctype.indexOf("application/json") > -1) { // json
                this.responseType = DS.TYPE_JSON;
            }
            else if(ctype.indexOf("text/plain") > -1) { // text
                this.responseType = DS.TYPE_TEXT;
            }
        }
        else {
            if(YAHOO.lang.isArray(oRawResponse)) { // array
                this.responseType = DS.TYPE_JSARRAY;
            }
             // xml
            else if(oRawResponse && oRawResponse.nodeType && (oRawResponse.nodeType === 9 || oRawResponse.nodeType === 1 || oRawResponse.nodeType === 11)) {
                this.responseType = DS.TYPE_XML;
            }
            else if(oRawResponse && oRawResponse.nodeName && (oRawResponse.nodeName.toLowerCase() == "table")) { // table
                this.responseType = DS.TYPE_HTMLTABLE;
            }    
            else if(YAHOO.lang.isObject(oRawResponse)) { // json
                this.responseType = DS.TYPE_JSON;
            }
            else if(YAHOO.lang.isString(oRawResponse)) { // text
                this.responseType = DS.TYPE_TEXT;
            }
        }
    }

    switch(this.responseType) {
        case DS.TYPE_JSARRAY:
            if(xhr && oRawResponse && oRawResponse.responseText) {
                oFullResponse = oRawResponse.responseText; 
            }
            try {
                // Convert to JS array if it's a string
                if(lang.isString(oFullResponse)) {
                    var parseArgs = [oFullResponse].concat(this.parseJSONArgs);
                    // Check for YUI JSON Util
                    if(lang.JSON) {
                        oFullResponse = lang.JSON.parse.apply(lang.JSON,parseArgs);
                    }
                    // Look for JSON parsers using an API similar to json2.js
                    else if(window.JSON && JSON.parse) {
                        oFullResponse = JSON.parse.apply(JSON,parseArgs);
                    }
                    // Look for JSON parsers using an API similar to json.js
                    else if(oFullResponse.parseJSON) {
                        oFullResponse = oFullResponse.parseJSON.apply(oFullResponse,parseArgs.slice(1));
                    }
                    // No JSON lib found so parse the string
                    else {
                        // Trim leading spaces
                        while (oFullResponse.length > 0 &&
                                (oFullResponse.charAt(0) != "{") &&
                                (oFullResponse.charAt(0) != "[")) {
                            oFullResponse = oFullResponse.substring(1, oFullResponse.length);
                        }

                        if(oFullResponse.length > 0) {
                            // Strip extraneous stuff at the end
                            var arrayEnd =
Math.max(oFullResponse.lastIndexOf("]"),oFullResponse.lastIndexOf("}"));
                            oFullResponse = oFullResponse.substring(0,arrayEnd+1);

                            // Turn the string into an object literal...
                            // ...eval is necessary here
                            oFullResponse = eval("(" + oFullResponse + ")");

                        }
                    }
                }
            }
            catch(e1) {
            }
            oFullResponse = this.doBeforeParseData(oRequest, oFullResponse, oCallback);
            oParsedResponse = this.parseArrayData(oRequest, oFullResponse);
            break;
        case DS.TYPE_JSON:
            if(xhr && oRawResponse && oRawResponse.responseText) {
                oFullResponse = oRawResponse.responseText;
            }
            try {
                // Convert to JSON object if it's a string
                if(lang.isString(oFullResponse)) {
                    var parseArgs = [oFullResponse].concat(this.parseJSONArgs);
                    // Check for YUI JSON Util
                    if(lang.JSON) {
                        oFullResponse = lang.JSON.parse.apply(lang.JSON,parseArgs);
                    }
                    // Look for JSON parsers using an API similar to json2.js
                    else if(window.JSON && JSON.parse) {
                        oFullResponse = JSON.parse.apply(JSON,parseArgs);
                    }
                    // Look for JSON parsers using an API similar to json.js
                    else if(oFullResponse.parseJSON) {
                        oFullResponse = oFullResponse.parseJSON.apply(oFullResponse,parseArgs.slice(1));
                    }
                    // No JSON lib found so parse the string
                    else {
                        // Trim leading spaces
                        while (oFullResponse.length > 0 &&
                                (oFullResponse.charAt(0) != "{") &&
                                (oFullResponse.charAt(0) != "[")) {
                            oFullResponse = oFullResponse.substring(1, oFullResponse.length);
                        }
    
                        if(oFullResponse.length > 0) {
                            // Strip extraneous stuff at the end
                            var objEnd = Math.max(oFullResponse.lastIndexOf("]"),oFullResponse.lastIndexOf("}"));
                            oFullResponse = oFullResponse.substring(0,objEnd+1);
    
                            // Turn the string into an object literal...
                            // ...eval is necessary here
                            oFullResponse = eval("(" + oFullResponse + ")");
    
                        }
                    }
                }
            }
            catch(e) {
            }

            oFullResponse = this.doBeforeParseData(oRequest, oFullResponse, oCallback);
            oParsedResponse = this.parseJSONData(oRequest, oFullResponse);
            break;
        case DS.TYPE_HTMLTABLE:
            if(xhr && oRawResponse.responseText) {
                var el = document.createElement('div');
                el.innerHTML = oRawResponse.responseText;
                oFullResponse = el.getElementsByTagName('table')[0];
            }
            oFullResponse = this.doBeforeParseData(oRequest, oFullResponse, oCallback);
            oParsedResponse = this.parseHTMLTableData(oRequest, oFullResponse);
            break;
        case DS.TYPE_XML:
            if(xhr && oRawResponse.responseXML) {
                oFullResponse = oRawResponse.responseXML;
            }
            oFullResponse = this.doBeforeParseData(oRequest, oFullResponse, oCallback);
            oParsedResponse = this.parseXMLData(oRequest, oFullResponse);
            break;
        case DS.TYPE_TEXT:
            if(xhr && lang.isString(oRawResponse.responseText)) {
                oFullResponse = oRawResponse.responseText;
            }
            oFullResponse = this.doBeforeParseData(oRequest, oFullResponse, oCallback);
            oParsedResponse = this.parseTextData(oRequest, oFullResponse);
            break;
        default:
            oFullResponse = this.doBeforeParseData(oRequest, oFullResponse, oCallback);
            oParsedResponse = this.parseData(oRequest, oFullResponse);
            break;
    }


    // Clean up for consistent signature
    oParsedResponse = oParsedResponse || {};
    if(!oParsedResponse.results) {
        oParsedResponse.results = [];
    }
    if(!oParsedResponse.meta) {
        oParsedResponse.meta = {};
    }

    // Success
    if(!oParsedResponse.error) {
        // Last chance to touch the raw response or the parsed response
        oParsedResponse = this.doBeforeCallback(oRequest, oFullResponse, oParsedResponse, oCallback);
        this.fireEvent("responseParseEvent", {request:oRequest,
                response:oParsedResponse, callback:oCallback, caller:oCaller});
        // Cache the response
        this.addToCache(oRequest, oParsedResponse);
    }
    // Error
    else {
        // Be sure the error flag is on
        oParsedResponse.error = true;
        this.fireEvent("dataErrorEvent", {request:oRequest, response: oRawResponse, callback:oCallback, 
                caller:oCaller, message:DS.ERROR_DATANULL});
        YAHOO.log(DS.ERROR_DATANULL, "error", this.toString());
    }

    // Send the response back to the caller
    oParsedResponse.tId = tId;
    DS.issueCallback(oCallback,[oRequest,oParsedResponse],oParsedResponse.error,oCaller);
},

/**
 * Overridable method gives implementers access to the original full response
 * before the data gets parsed. Implementers should take care not to return an
 * unparsable or otherwise invalid response.
 *
 * @method doBeforeParseData
 * @param oRequest {Object} Request object.
 * @param oFullResponse {Object} The full response from the live database.
 * @param oCallback {Object} The callback object.  
 * @return {Object} Full response for parsing.
  
 */
doBeforeParseData : function(oRequest, oFullResponse, oCallback) {
    return oFullResponse;
},

/**
 * Overridable method gives implementers access to the original full response and
 * the parsed response (parsed against the given schema) before the data
 * is added to the cache (if applicable) and then sent back to callback function.
 * This is your chance to access the raw response and/or populate the parsed
 * response with any custom data.
 *
 * @method doBeforeCallback
 * @param oRequest {Object} Request object.
 * @param oFullResponse {Object} The full response from the live database.
 * @param oParsedResponse {Object} The parsed response to return to calling object.
 * @param oCallback {Object} The callback object. 
 * @return {Object} Parsed response object.
 */
doBeforeCallback : function(oRequest, oFullResponse, oParsedResponse, oCallback) {
    return oParsedResponse;
},

/**
 * Overridable method parses data of generic RESPONSE_TYPE into a response object.
 *
 * @method parseData
 * @param oRequest {Object} Request object.
 * @param oFullResponse {Object} The full Array from the live database.
 * @return {Object} Parsed response object with the following properties:<br>
 *     - results {Array} Array of parsed data results<br>
 *     - meta {Object} Object literal of meta values<br>
 *     - error {Boolean} (optional) True if there was an error<br>
 */
parseData : function(oRequest, oFullResponse) {
    if(lang.isValue(oFullResponse)) {
        var oParsedResponse = {results:oFullResponse,meta:{}};
        YAHOO.log("Parsed generic data is " +
                lang.dump(oParsedResponse), "info", this.toString());
        return oParsedResponse;

    }
    YAHOO.log("Generic data could not be parsed: " + lang.dump(oFullResponse), 
            "error", this.toString());
    return null;
},

/**
 * Overridable method parses Array data into a response object.
 *
 * @method parseArrayData
 * @param oRequest {Object} Request object.
 * @param oFullResponse {Object} The full Array from the live database.
 * @return {Object} Parsed response object with the following properties:<br>
 *     - results (Array) Array of parsed data results<br>
 *     - error (Boolean) True if there was an error
 */
parseArrayData : function(oRequest, oFullResponse) {
    if(lang.isArray(oFullResponse)) {
        var results = [],
            i, j,
            rec, field, data;
        
        // Parse for fields
        if(lang.isArray(this.responseSchema.fields)) {
            var fields = this.responseSchema.fields;
            for (i = fields.length - 1; i >= 0; --i) {
                if (typeof fields[i] !== 'object') {
                    fields[i] = { key : fields[i] };
                }
            }

            var parsers = {}, p;
            for (i = fields.length - 1; i >= 0; --i) {
                p = (typeof fields[i].parser === 'function' ?
                          fields[i].parser :
                          DS.Parser[fields[i].parser+'']) || fields[i].converter;
                if (p) {
                    parsers[fields[i].key] = p;
                }
            }

            var arrType = lang.isArray(oFullResponse[0]);
            for(i=oFullResponse.length-1; i>-1; i--) {
                var oResult = {};
                rec = oFullResponse[i];
                if (typeof rec === 'object') {
                    for(j=fields.length-1; j>-1; j--) {
                        field = fields[j];
                        data = arrType ? rec[j] : rec[field.key];

                        if (parsers[field.key]) {
                            data = parsers[field.key].call(this,data);
                        }

                        // Safety measure
                        if(data === undefined) {
                            data = null;
                        }

                        oResult[field.key] = data;
                    }
                }
                else if (lang.isString(rec)) {
                    for(j=fields.length-1; j>-1; j--) {
                        field = fields[j];
                        data = rec;

                        if (parsers[field.key]) {
                            data = parsers[field.key].call(this,data);
                        }

                        // Safety measure
                        if(data === undefined) {
                            data = null;
                        }

                        oResult[field.key] = data;
                    }                
                }
                results[i] = oResult;
            }    
        }
        // Return entire data set
        else {
            results = oFullResponse;
        }
        var oParsedResponse = {results:results};
        YAHOO.log("Parsed array data is " +
                lang.dump(oParsedResponse), "info", this.toString());
        return oParsedResponse;

    }
    YAHOO.log("Array data could not be parsed: " + lang.dump(oFullResponse), 
            "error", this.toString());
    return null;
},

/**
 * Overridable method parses plain text data into a response object.
 *
 * @method parseTextData
 * @param oRequest {Object} Request object.
 * @param oFullResponse {Object} The full text response from the live database.
 * @return {Object} Parsed response object with the following properties:<br>
 *     - results (Array) Array of parsed data results<br>
 *     - error (Boolean) True if there was an error
 */
parseTextData : function(oRequest, oFullResponse) {
    if(lang.isString(oFullResponse)) {
        if(lang.isString(this.responseSchema.recordDelim) &&
                lang.isString(this.responseSchema.fieldDelim)) {
            var oParsedResponse = {results:[]};
            var recDelim = this.responseSchema.recordDelim;
            var fieldDelim = this.responseSchema.fieldDelim;
            if(oFullResponse.length > 0) {
                // Delete the last line delimiter at the end of the data if it exists
                var newLength = oFullResponse.length-recDelim.length;
                if(oFullResponse.substr(newLength) == recDelim) {
                    oFullResponse = oFullResponse.substr(0, newLength);
                }
                if(oFullResponse.length > 0) {
                    // Split along record delimiter to get an array of strings
                    var recordsarray = oFullResponse.split(recDelim);
                    // Cycle through each record
                    for(var i = 0, len = recordsarray.length, recIdx = 0; i < len; ++i) {
                        var bError = false,
                            sRecord = recordsarray[i];
                        if (lang.isString(sRecord) && (sRecord.length > 0)) {
                            // Split each record along field delimiter to get data
                            var fielddataarray = recordsarray[i].split(fieldDelim);
                            var oResult = {};
                            
                            // Filter for fields data
                            if(lang.isArray(this.responseSchema.fields)) {
                                var fields = this.responseSchema.fields;
                                for(var j=fields.length-1; j>-1; j--) {
                                    try {
                                        // Remove quotation marks from edges, if applicable
                                        var data = fielddataarray[j];
                                        if (lang.isString(data)) {
                                            if(data.charAt(0) == "\"") {
                                                data = data.substr(1);
                                            }
                                            if(data.charAt(data.length-1) == "\"") {
                                                data = data.substr(0,data.length-1);
                                            }
                                            var field = fields[j];
                                            var key = (lang.isValue(field.key)) ? field.key : field;
                                            // Backward compatibility
                                            if(!field.parser && field.converter) {
                                                field.parser = field.converter;
                                                YAHOO.log("The field property converter has been deprecated" +
                                                        " in favor of parser", "warn", this.toString());
                                            }
                                            var parser = (typeof field.parser === 'function') ?
                                                field.parser :
                                                DS.Parser[field.parser+''];
                                            if(parser) {
                                                data = parser.call(this, data);
                                            }
                                            // Safety measure
                                            if(data === undefined) {
                                                data = null;
                                            }
                                            oResult[key] = data;
                                        }
                                        else {
                                            bError = true;
                                        }
                                    }
                                    catch(e) {
                                        bError = true;
                                    }
                                }
                            }            
                            // No fields defined so pass along all data as an array
                            else {
                                oResult = fielddataarray;
                            }
                            if(!bError) {
                                oParsedResponse.results[recIdx++] = oResult;
                            }
                        }
                    }
                }
            }
            YAHOO.log("Parsed text data is " +
                    lang.dump(oParsedResponse), "info", this.toString());
            return oParsedResponse;
        }
    }
    YAHOO.log("Text data could not be parsed: " + lang.dump(oFullResponse), 
            "error", this.toString());
    return null;
            
},

/**
 * Overridable method parses XML data for one result into an object literal.
 *
 * @method parseXMLResult
 * @param result {XML} XML for one result.
 * @return {Object} Object literal of data for one result.
 */
parseXMLResult : function(result) {
    var oResult = {},
        schema = this.responseSchema;
        
    try {
        // Loop through each data field in each result using the schema
        for(var m = schema.fields.length-1; m >= 0 ; m--) {
            var field = schema.fields[m];
            var key = (lang.isValue(field.key)) ? field.key : field;
            var data = null;

            if(this.useXPath) {
                data = YAHOO.util.DataSource._getLocationValue(field, result);
            }
            else {
                // Values may be held in an attribute...
                var xmlAttr = result.attributes.getNamedItem(key);
                if(xmlAttr) {
                    data = xmlAttr.value;
                }
                // ...or in a node
                else {
                    var xmlNode = result.getElementsByTagName(key);
                    if(xmlNode && xmlNode.item(0)) {
                        var item = xmlNode.item(0);
                        // For IE, then DOM...
                        data = (item) ? ((item.text) ? item.text : (item.textContent) ? item.textContent : null) : null;
                        // ...then fallback, but check for multiple child nodes
                        if(!data) {
                            var datapieces = [];
                            for(var j=0, len=item.childNodes.length; j<len; j++) {
                                if(item.childNodes[j].nodeValue) {
                                    datapieces[datapieces.length] = item.childNodes[j].nodeValue;
                                }
                            }
                            if(datapieces.length > 0) {
                                data = datapieces.join("");
                            }
                        }
                    }
                }
            }
            
            
            // Safety net
            if(data === null) {
                   data = "";
            }
            // Backward compatibility
            if(!field.parser && field.converter) {
                field.parser = field.converter;
                YAHOO.log("The field property converter has been deprecated" +
                        " in favor of parser", "warn", this.toString());
            }
            var parser = (typeof field.parser === 'function') ?
                field.parser :
                DS.Parser[field.parser+''];
            if(parser) {
                data = parser.call(this, data);
            }
            // Safety measure
            if(data === undefined) {
                data = null;
            }
            oResult[key] = data;
        }
    }
    catch(e) {
        YAHOO.log("Error while parsing XML result: " + e.message);
    }

    return oResult;
},



/**
 * Overridable method parses XML data into a response object.
 *
 * @method parseXMLData
 * @param oRequest {Object} Request object.
 * @param oFullResponse {Object} The full XML response from the live database.
 * @return {Object} Parsed response object with the following properties<br>
 *     - results (Array) Array of parsed data results<br>
 *     - error (Boolean) True if there was an error
 */
parseXMLData : function(oRequest, oFullResponse) {
    var bError = false,
        schema = this.responseSchema,
        oParsedResponse = {meta:{}},
        xmlList = null,
        metaNode      = schema.metaNode,
        metaLocators  = schema.metaFields || {},
        i,k,loc,v;

    // In case oFullResponse is something funky
    try {
        // Pull any meta identified
        if(this.useXPath) {
            for (k in metaLocators) {
                oParsedResponse.meta[k] = YAHOO.util.DataSource._getLocationValue(metaLocators[k], oFullResponse);
            }
        }
        else {
            metaNode = metaNode ? oFullResponse.getElementsByTagName(metaNode)[0] :
                       oFullResponse;

            if (metaNode) {
                for (k in metaLocators) {
                    if (lang.hasOwnProperty(metaLocators, k)) {
                        loc = metaLocators[k];
                        // Look for a node
                        v = metaNode.getElementsByTagName(loc)[0];

                        if (v) {
                            v = v.firstChild.nodeValue;
                        } else {
                            // Look for an attribute
                            v = metaNode.attributes.getNamedItem(loc);
                            if (v) {
                                v = v.value;
                            }
                        }

                        if (lang.isValue(v)) {
                            oParsedResponse.meta[k] = v;
                        }
                    }
                }
            }
        }
        
        // For result data
        xmlList = (schema.resultNode) ?
            oFullResponse.getElementsByTagName(schema.resultNode) :
            null;
    }
    catch(e) {
        YAHOO.log("Error while parsing XML data: " + e.message);
    }
    if(!xmlList || !lang.isArray(schema.fields)) {
        bError = true;
    }
    // Loop through each result
    else {
        oParsedResponse.results = [];
        for(i = xmlList.length-1; i >= 0 ; --i) {
            var oResult = this.parseXMLResult(xmlList.item(i));
            // Capture each array of values into an array of results
            oParsedResponse.results[i] = oResult;
        }
    }
    if(bError) {
        YAHOO.log("XML data could not be parsed: " +
                lang.dump(oFullResponse), "error", this.toString());
        oParsedResponse.error = true;
    }
    else {
        YAHOO.log("Parsed XML data is " +
                lang.dump(oParsedResponse), "info", this.toString());
    }
    return oParsedResponse;
},

/**
 * Overridable method parses JSON data into a response object.
 *
 * @method parseJSONData
 * @param oRequest {Object} Request object.
 * @param oFullResponse {Object} The full JSON from the live database.
 * @return {Object} Parsed response object with the following properties<br>
 *     - results (Array) Array of parsed data results<br>
 *     - error (Boolean) True if there was an error
 */
parseJSONData : function(oRequest, oFullResponse) {
    var oParsedResponse = {results:[],meta:{}};
    
    if(lang.isObject(oFullResponse) && this.responseSchema.resultsList) {
        var schema = this.responseSchema,
            fields          = schema.fields,
            resultsList     = oFullResponse,
            results         = [],
            metaFields      = schema.metaFields || {},
            fieldParsers    = [],
            fieldPaths      = [],
            simpleFields    = [],
            bError          = false,
            i,len,j,v,key,parser,path;

        // Function to convert the schema's fields into walk paths
        var buildPath = function (needle) {
            var path = null, keys = [], i = 0;
            if (needle) {
                // Strip the ["string keys"] and [1] array indexes
                needle = needle.
                    replace(/\[(['"])(.*?)\1\]/g,
                    function (x,$1,$2) {keys[i]=$2;return '.@'+(i++);}).
                    replace(/\[(\d+)\]/g,
                    function (x,$1) {keys[i]=parseInt($1,10)|0;return '.@'+(i++);}).
                    replace(/^\./,''); // remove leading dot

                // If the cleaned needle contains invalid characters, the
                // path is invalid
                if (!/[^\w\.\$@]/.test(needle)) {
                    path = needle.split('.');
                    for (i=path.length-1; i >= 0; --i) {
                        if (path[i].charAt(0) === '@') {
                            path[i] = keys[parseInt(path[i].substr(1),10)];
                        }
                    }
                }
                else {
                    YAHOO.log("Invalid locator: " + needle, "error", this.toString());
                }
            }
            return path;
        };


        // Function to walk a path and return the pot of gold
        var walkPath = function (path, origin) {
            var v=origin,i=0,len=path.length;
            for (;i<len && v;++i) {
                v = v[path[i]];
            }
            return v;
        };

        // Parse the response
        // Step 1. Pull the resultsList from oFullResponse (default assumes
        // oFullResponse IS the resultsList)
        path = buildPath(schema.resultsList);
        if (path) {
            resultsList = walkPath(path, oFullResponse);
            if (resultsList === undefined) {
                bError = true;
            }
        } else {
            bError = true;
        }
        
        if (!resultsList) {
            resultsList = [];
        }

        if (!lang.isArray(resultsList)) {
            resultsList = [resultsList];
        }

        if (!bError) {
            // Step 2. Parse out field data if identified
            if(schema.fields) {
                var field;
                // Build the field parser map and location paths
                for (i=0, len=fields.length; i<len; i++) {
                    field = fields[i];
                    key    = field.key || field;
                    parser = ((typeof field.parser === 'function') ?
                        field.parser :
                        DS.Parser[field.parser+'']) || field.converter;
                    path   = buildPath(key);
    
                    if (parser) {
                        fieldParsers[fieldParsers.length] = {key:key,parser:parser};
                    }
    
                    if (path) {
                        if (path.length > 1) {
                            fieldPaths[fieldPaths.length] = {key:key,path:path};
                        } else {
                            simpleFields[simpleFields.length] = {key:key,path:path[0]};
                        }
                    } else {
                        YAHOO.log("Invalid key syntax: " + key,"warn",this.toString());
                    }
                }

                // Process the results, flattening the records and/or applying parsers if needed
                for (i = resultsList.length - 1; i >= 0; --i) {
                    var r = resultsList[i], rec = {};
                    if(r) {
                        for (j = simpleFields.length - 1; j >= 0; --j) {
                            // Bug 1777850: data might be held in an array
                            rec[simpleFields[j].key] =
                                    (r[simpleFields[j].path] !== undefined) ?
                                    r[simpleFields[j].path] : r[j];
                        }

                        for (j = fieldPaths.length - 1; j >= 0; --j) {
                            rec[fieldPaths[j].key] = walkPath(fieldPaths[j].path,r);
                        }

                        for (j = fieldParsers.length - 1; j >= 0; --j) {
                            var p = fieldParsers[j].key;
                            rec[p] = fieldParsers[j].parser.call(this, rec[p]);
                            if (rec[p] === undefined) {
                                rec[p] = null;
                            }
                        }
                    }
                    results[i] = rec;
                }
            }
            else {
                results = resultsList;
            }

            for (key in metaFields) {
                if (lang.hasOwnProperty(metaFields,key)) {
                    path = buildPath(metaFields[key]);
                    if (path) {
                        v = walkPath(path, oFullResponse);
                        oParsedResponse.meta[key] = v;
                    }
                }
            }

        } else {
            YAHOO.log("JSON data could not be parsed due to invalid responseSchema.resultsList or invalid response: " +
                    lang.dump(oFullResponse), "error", this.toString());

            oParsedResponse.error = true;
        }

        oParsedResponse.results = results;
    }
    else {
        YAHOO.log("JSON data could not be parsed: " +
                lang.dump(oFullResponse), "error", this.toString());
        oParsedResponse.error = true;
    }

    return oParsedResponse;
},

/**
 * Overridable method parses an HTML TABLE element reference into a response object.
 * Data is parsed out of TR elements from all TBODY elements. 
 *
 * @method parseHTMLTableData
 * @param oRequest {Object} Request object.
 * @param oFullResponse {Object} The full HTML element reference from the live database.
 * @return {Object} Parsed response object with the following properties<br>
 *     - results (Array) Array of parsed data results<br>
 *     - error (Boolean) True if there was an error
 */
parseHTMLTableData : function(oRequest, oFullResponse) {
    var bError = false;
    var elTable = oFullResponse;
    var fields = this.responseSchema.fields;
    var oParsedResponse = {results:[]};

    if(lang.isArray(fields)) {
        // Iterate through each TBODY
        for(var i=0; i<elTable.tBodies.length; i++) {
            var elTbody = elTable.tBodies[i];
    
            // Iterate through each TR
            for(var j=elTbody.rows.length-1; j>-1; j--) {
                var elRow = elTbody.rows[j];
                var oResult = {};
                
                for(var k=fields.length-1; k>-1; k--) {
                    var field = fields[k];
                    var key = (lang.isValue(field.key)) ? field.key : field;
                    var data = elRow.cells[k].innerHTML;
    
                    // Backward compatibility
                    if(!field.parser && field.converter) {
                        field.parser = field.converter;
                        YAHOO.log("The field property converter has been deprecated" +
                                " in favor of parser", "warn", this.toString());
                    }
                    var parser = (typeof field.parser === 'function') ?
                        field.parser :
                        DS.Parser[field.parser+''];
                    if(parser) {
                        data = parser.call(this, data);
                    }
                    // Safety measure
                    if(data === undefined) {
                        data = null;
                    }
                    oResult[key] = data;
                }
                oParsedResponse.results[j] = oResult;
            }
        }
    }
    else {
        bError = true;
        YAHOO.log("Invalid responseSchema.fields", "error", this.toString());
    }

    if(bError) {
        YAHOO.log("HTML TABLE data could not be parsed: " +
                lang.dump(oFullResponse), "error", this.toString());
        oParsedResponse.error = true;
    }
    else {
        YAHOO.log("Parsed HTML TABLE data is " +
                lang.dump(oParsedResponse), "info", this.toString());
    }
    return oParsedResponse;
}

};

// DataSourceBase uses EventProvider
lang.augmentProto(DS, util.EventProvider);



/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * LocalDataSource class for in-memory data structs including JavaScript arrays,
 * JavaScript object literals (JSON), XML documents, and HTML tables.
 *
 * @namespace YAHOO.util
 * @class YAHOO.util.LocalDataSource
 * @extends YAHOO.util.DataSourceBase 
 * @constructor
 * @param oLiveData {HTMLElement}  Pointer to live data.
 * @param oConfigs {object} (optional) Object literal of configuration values.
 */
util.LocalDataSource = function(oLiveData, oConfigs) {
    this.dataType = DS.TYPE_LOCAL;
    
    if(oLiveData) {
        if(YAHOO.lang.isArray(oLiveData)) { // array
            this.responseType = DS.TYPE_JSARRAY;
        }
         // xml
        else if(oLiveData.nodeType && oLiveData.nodeType == 9) {
            this.responseType = DS.TYPE_XML;
        }
        else if(oLiveData.nodeName && (oLiveData.nodeName.toLowerCase() == "table")) { // table
            this.responseType = DS.TYPE_HTMLTABLE;
            oLiveData = oLiveData.cloneNode(true);
        }    
        else if(YAHOO.lang.isString(oLiveData)) { // text
            this.responseType = DS.TYPE_TEXT;
        }
        else if(YAHOO.lang.isObject(oLiveData)) { // json
            this.responseType = DS.TYPE_JSON;
        }
    }
    else {
        oLiveData = [];
        this.responseType = DS.TYPE_JSARRAY;
    }
    
    util.LocalDataSource.superclass.constructor.call(this, oLiveData, oConfigs); 
};

// LocalDataSource extends DataSourceBase
lang.extend(util.LocalDataSource, DS);

// Copy static members to LocalDataSource class
lang.augmentObject(util.LocalDataSource, DS);













/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * FunctionDataSource class for JavaScript functions.
 *
 * @namespace YAHOO.util
 * @class YAHOO.util.FunctionDataSource
 * @extends YAHOO.util.DataSourceBase  
 * @constructor
 * @param oLiveData {HTMLElement}  Pointer to live data.
 * @param oConfigs {object} (optional) Object literal of configuration values.
 */
util.FunctionDataSource = function(oLiveData, oConfigs) {
    this.dataType = DS.TYPE_JSFUNCTION;
    oLiveData = oLiveData || function() {};
    
    util.FunctionDataSource.superclass.constructor.call(this, oLiveData, oConfigs); 
};

// FunctionDataSource extends DataSourceBase
lang.extend(util.FunctionDataSource, DS, {

/////////////////////////////////////////////////////////////////////////////
//
// FunctionDataSource public properties
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Context in which to execute the function. By default, is the DataSource
 * instance itself. If set, the function will receive the DataSource instance
 * as an additional argument. 
 *
 * @property scope
 * @type Object
 * @default null
 */
scope : null,


/////////////////////////////////////////////////////////////////////////////
//
// FunctionDataSource public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Overriding method passes query to a function. The returned response is then
 * forwarded to the handleResponse function.
 *
 * @method makeConnection
 * @param oRequest {Object} Request object.
 * @param oCallback {Object} Callback object literal.
 * @param oCaller {Object} (deprecated) Use oCallback.scope.
 * @return {Number} Transaction ID.
 */
makeConnection : function(oRequest, oCallback, oCaller) {
    var tId = DS._nTransactionId++;
    this.fireEvent("requestEvent", {tId:tId,request:oRequest,callback:oCallback,caller:oCaller});

    // Pass the request in as a parameter and
    // forward the return value to the handler
    
    
    var oRawResponse = (this.scope) ? this.liveData.call(this.scope, oRequest, this, oCallback) : this.liveData(oRequest, oCallback);
    
    // Try to sniff data type if it has not been defined
    if(this.responseType === DS.TYPE_UNKNOWN) {
        if(YAHOO.lang.isArray(oRawResponse)) { // array
            this.responseType = DS.TYPE_JSARRAY;
        }
         // xml
        else if(oRawResponse && oRawResponse.nodeType && oRawResponse.nodeType == 9) {
            this.responseType = DS.TYPE_XML;
        }
        else if(oRawResponse && oRawResponse.nodeName && (oRawResponse.nodeName.toLowerCase() == "table")) { // table
            this.responseType = DS.TYPE_HTMLTABLE;
        }    
        else if(YAHOO.lang.isObject(oRawResponse)) { // json
            this.responseType = DS.TYPE_JSON;
        }
        else if(YAHOO.lang.isString(oRawResponse)) { // text
            this.responseType = DS.TYPE_TEXT;
        }
    }

    this.handleResponse(oRequest, oRawResponse, oCallback, oCaller, tId);
    return tId;
}

});

// Copy static members to FunctionDataSource class
lang.augmentObject(util.FunctionDataSource, DS);













/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * ScriptNodeDataSource class for accessing remote data via the YUI Get Utility. 
 *
 * @namespace YAHOO.util
 * @class YAHOO.util.ScriptNodeDataSource
 * @extends YAHOO.util.DataSourceBase  
 * @constructor
 * @param oLiveData {HTMLElement}  Pointer to live data.
 * @param oConfigs {object} (optional) Object literal of configuration values.
 */
util.ScriptNodeDataSource = function(oLiveData, oConfigs) {
    this.dataType = DS.TYPE_SCRIPTNODE;
    oLiveData = oLiveData || "";
    
    util.ScriptNodeDataSource.superclass.constructor.call(this, oLiveData, oConfigs); 
};

// ScriptNodeDataSource extends DataSourceBase
lang.extend(util.ScriptNodeDataSource, DS, {

/////////////////////////////////////////////////////////////////////////////
//
// ScriptNodeDataSource public properties
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Alias to YUI Get Utility, to allow implementers to use a custom class.
 *
 * @property getUtility
 * @type Object
 * @default YAHOO.util.Get
 */
getUtility : util.Get,

/**
 * Defines request/response management in the following manner:
 * <dl>
 *     <!--<dt>queueRequests</dt>
 *     <dd>If a request is already in progress, wait until response is returned before sending the next request.</dd>
 *     <dt>cancelStaleRequests</dt>
 *     <dd>If a request is already in progress, cancel it before sending the next request.</dd>-->
 *     <dt>ignoreStaleResponses</dt>
 *     <dd>Send all requests, but handle only the response for the most recently sent request.</dd>
 *     <dt>allowAll</dt>
 *     <dd>Send all requests and handle all responses.</dd>
 * </dl>
 *
 * @property asyncMode
 * @type String
 * @default "allowAll"
 */
asyncMode : "allowAll",

/**
 * Callback string parameter name sent to the remote script. By default,
 * requests are sent to
 * &#60;URI&#62;?&#60;scriptCallbackParam&#62;=callback
 *
 * @property scriptCallbackParam
 * @type String
 * @default "callback"
 */
scriptCallbackParam : "callback",


/////////////////////////////////////////////////////////////////////////////
//
// ScriptNodeDataSource public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Creates a request callback that gets appended to the script URI. Implementers
 * can customize this string to match their server's query syntax.
 *
 * @method generateRequestCallback
 * @return {String} String fragment that gets appended to script URI that 
 * specifies the callback function 
 */
generateRequestCallback : function(id) {
    return "&" + this.scriptCallbackParam + "=YAHOO.util.ScriptNodeDataSource.callbacks["+id+"]" ;
},

/**
 * Overridable method gives implementers access to modify the URI before the dynamic
 * script node gets inserted. Implementers should take care not to return an
 * invalid URI.
 *
 * @method doBeforeGetScriptNode
 * @param {String} URI to the script 
 * @return {String} URI to the script
 */
doBeforeGetScriptNode : function(sUri) {
    return sUri;
},

/**
 * Overriding method passes query to Get Utility. The returned
 * response is then forwarded to the handleResponse function.
 *
 * @method makeConnection
 * @param oRequest {Object} Request object.
 * @param oCallback {Object} Callback object literal.
 * @param oCaller {Object} (deprecated) Use oCallback.scope.
 * @return {Number} Transaction ID.
 */
makeConnection : function(oRequest, oCallback, oCaller) {
    var tId = DS._nTransactionId++;
    this.fireEvent("requestEvent", {tId:tId,request:oRequest,callback:oCallback,caller:oCaller});
    
    // If there are no global pending requests, it is safe to purge global callback stack and global counter
    if(util.ScriptNodeDataSource._nPending === 0) {
        util.ScriptNodeDataSource.callbacks = [];
        util.ScriptNodeDataSource._nId = 0;
    }
    
    // ID for this request
    var id = util.ScriptNodeDataSource._nId;
    util.ScriptNodeDataSource._nId++;
    
    // Dynamically add handler function with a closure to the callback stack
    var oSelf = this;
    util.ScriptNodeDataSource.callbacks[id] = function(oRawResponse) {
        if((oSelf.asyncMode !== "ignoreStaleResponses")||
                (id === util.ScriptNodeDataSource.callbacks.length-1)) { // Must ignore stale responses
                
            // Try to sniff data type if it has not been defined
            if(oSelf.responseType === DS.TYPE_UNKNOWN) {
                if(YAHOO.lang.isArray(oRawResponse)) { // array
                    oSelf.responseType = DS.TYPE_JSARRAY;
                }
                 // xml
                else if(oRawResponse.nodeType && oRawResponse.nodeType == 9) {
                    oSelf.responseType = DS.TYPE_XML;
                }
                else if(oRawResponse.nodeName && (oRawResponse.nodeName.toLowerCase() == "table")) { // table
                    oSelf.responseType = DS.TYPE_HTMLTABLE;
                }    
                else if(YAHOO.lang.isObject(oRawResponse)) { // json
                    oSelf.responseType = DS.TYPE_JSON;
                }
                else if(YAHOO.lang.isString(oRawResponse)) { // text
                    oSelf.responseType = DS.TYPE_TEXT;
                }
            }

            oSelf.handleResponse(oRequest, oRawResponse, oCallback, oCaller, tId);
        }
        else {
            YAHOO.log("DataSource ignored stale response for tId " + tId + "(" + oRequest + ")", "info", oSelf.toString());
        }
    
        delete util.ScriptNodeDataSource.callbacks[id];
    };
    
    // We are now creating a request
    util.ScriptNodeDataSource._nPending++;
    var sUri = this.liveData + oRequest + this.generateRequestCallback(id);
    sUri = this.doBeforeGetScriptNode(sUri);
    YAHOO.log("DataSource is querying URL " + sUri, "info", this.toString());
    this.getUtility.script(sUri,
            {autopurge: true,
            onsuccess: util.ScriptNodeDataSource._bumpPendingDown,
            onfail: util.ScriptNodeDataSource._bumpPendingDown});

    return tId;
}

});

// Copy static members to ScriptNodeDataSource class
lang.augmentObject(util.ScriptNodeDataSource, DS);

// Copy static members to ScriptNodeDataSource class
lang.augmentObject(util.ScriptNodeDataSource,  {

/////////////////////////////////////////////////////////////////////////////
//
// ScriptNodeDataSource private static properties
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Unique ID to track requests.
 *
 * @property _nId
 * @type Number
 * @private
 * @static
 */
_nId : 0,

/**
 * Counter for pending requests. When this is 0, it is safe to purge callbacks
 * array.
 *
 * @property _nPending
 * @type Number
 * @private
 * @static
 */
_nPending : 0,

/**
 * Global array of callback functions, one for each request sent.
 *
 * @property callbacks
 * @type Function[]
 * @static
 */
callbacks : []

});














/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * XHRDataSource class for accessing remote data via the YUI Connection Manager
 * Utility
 *
 * @namespace YAHOO.util
 * @class YAHOO.util.XHRDataSource
 * @extends YAHOO.util.DataSourceBase  
 * @constructor
 * @param oLiveData {HTMLElement}  Pointer to live data.
 * @param oConfigs {object} (optional) Object literal of configuration values.
 */
util.XHRDataSource = function(oLiveData, oConfigs) {
    this.dataType = DS.TYPE_XHR;
    this.connMgr = this.connMgr || util.Connect;
    oLiveData = oLiveData || "";
    
    util.XHRDataSource.superclass.constructor.call(this, oLiveData, oConfigs); 
};

// XHRDataSource extends DataSourceBase
lang.extend(util.XHRDataSource, DS, {

/////////////////////////////////////////////////////////////////////////////
//
// XHRDataSource public properties
//
/////////////////////////////////////////////////////////////////////////////

 /**
 * Alias to YUI Connection Manager, to allow implementers to use a custom class.
 *
 * @property connMgr
 * @type Object
 * @default YAHOO.util.Connect
 */
connMgr: null,

 /**
 * Defines request/response management in the following manner:
 * <dl>
 *     <dt>queueRequests</dt>
 *     <dd>If a request is already in progress, wait until response is returned
 *     before sending the next request.</dd>
 *
 *     <dt>cancelStaleRequests</dt>
 *     <dd>If a request is already in progress, cancel it before sending the next
 *     request.</dd>
 *
 *     <dt>ignoreStaleResponses</dt>
 *     <dd>Send all requests, but handle only the response for the most recently
 *     sent request.</dd>
 *
 *     <dt>allowAll</dt>
 *     <dd>Send all requests and handle all responses.</dd>
 *
 * </dl>
 *
 * @property connXhrMode
 * @type String
 * @default "allowAll"
 */
connXhrMode: "allowAll",

 /**
 * True if data is to be sent via POST. By default, data will be sent via GET.
 *
 * @property connMethodPost
 * @type Boolean
 * @default false
 */
connMethodPost: false,

 /**
 * The connection timeout defines how many  milliseconds the XHR connection will
 * wait for a server response. Any non-zero value will enable the Connection Manager's
 * Auto-Abort feature.
 *
 * @property connTimeout
 * @type Number
 * @default 0
 */
connTimeout: 0,

/////////////////////////////////////////////////////////////////////////////
//
// XHRDataSource public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Overriding method passes query to Connection Manager. The returned
 * response is then forwarded to the handleResponse function.
 *
 * @method makeConnection
 * @param oRequest {Object} Request object.
 * @param oCallback {Object} Callback object literal.
 * @param oCaller {Object} (deprecated) Use oCallback.scope.
 * @return {Number} Transaction ID.
 */
makeConnection : function(oRequest, oCallback, oCaller) {

    var oRawResponse = null;
    var tId = DS._nTransactionId++;
    this.fireEvent("requestEvent", {tId:tId,request:oRequest,callback:oCallback,caller:oCaller});

    // Set up the callback object and
    // pass the request in as a URL query and
    // forward the response to the handler
    var oSelf = this;
    var oConnMgr = this.connMgr;
    var oQueue = this._oQueue;

    /**
     * Define Connection Manager success handler
     *
     * @method _xhrSuccess
     * @param oResponse {Object} HTTPXMLRequest object
     * @private
     */
    var _xhrSuccess = function(oResponse) {
        // If response ID does not match last made request ID,
        // silently fail and wait for the next response
        if(oResponse && (this.connXhrMode == "ignoreStaleResponses") &&
                (oResponse.tId != oQueue.conn.tId)) {
            YAHOO.log("Ignored stale response", "warn", this.toString());
            return null;
        }
        // Error if no response
        else if(!oResponse) {
            this.fireEvent("dataErrorEvent", {request:oRequest, response:null,
                    callback:oCallback, caller:oCaller,
                    message:DS.ERROR_DATANULL});
            YAHOO.log(DS.ERROR_DATANULL, "error", this.toString());

            // Send error response back to the caller with the error flag on
            DS.issueCallback(oCallback,[oRequest, {error:true}], true, oCaller);

            return null;
        }
        // Forward to handler
        else {
            // Try to sniff data type if it has not been defined
            if(this.responseType === DS.TYPE_UNKNOWN) {
                var ctype = (oResponse.getResponseHeader) ? oResponse.getResponseHeader["Content-Type"] : null;
                if(ctype) {
                    // xml
                    if(ctype.indexOf("text/xml") > -1) {
                        this.responseType = DS.TYPE_XML;
                    }
                    else if(ctype.indexOf("application/json") > -1) { // json
                        this.responseType = DS.TYPE_JSON;
                    }
                    else if(ctype.indexOf("text/plain") > -1) { // text
                        this.responseType = DS.TYPE_TEXT;
                    }
                }
            }
            this.handleResponse(oRequest, oResponse, oCallback, oCaller, tId);
        }
    };

    /**
     * Define Connection Manager failure handler
     *
     * @method _xhrFailure
     * @param oResponse {Object} HTTPXMLRequest object
     * @private
     */
    var _xhrFailure = function(oResponse) {
        this.fireEvent("dataErrorEvent", {request:oRequest, response: oResponse,
                callback:oCallback, caller:oCaller,
                message:DS.ERROR_DATAINVALID});
        YAHOO.log(DS.ERROR_DATAINVALID + ": " +
                oResponse.statusText, "error", this.toString());

        // Backward compatibility
        if(lang.isString(this.liveData) && lang.isString(oRequest) &&
            (this.liveData.lastIndexOf("?") !== this.liveData.length-1) &&
            (oRequest.indexOf("?") !== 0)){
                YAHOO.log("DataSources using XHR no longer automatically supply " + 
                "a \"?\" between the host and query parameters" +
                " -- please check that the request URL is correct", "warn", this.toString());
        }

        // Send failure response back to the caller with the error flag on
        oResponse = oResponse || {};
        oResponse.error = true;
        DS.issueCallback(oCallback,[oRequest,oResponse],true, oCaller);

        return null;
    };

    /**
     * Define Connection Manager callback object
     *
     * @property _xhrCallback
     * @param oResponse {Object} HTTPXMLRequest object
     * @private
     */
     var _xhrCallback = {
        success:_xhrSuccess,
        failure:_xhrFailure,
        scope: this
    };

    // Apply Connection Manager timeout
    if(lang.isNumber(this.connTimeout)) {
        _xhrCallback.timeout = this.connTimeout;
    }

    // Cancel stale requests
    if(this.connXhrMode == "cancelStaleRequests") {
            // Look in queue for stale requests
            if(oQueue.conn) {
                if(oConnMgr.abort) {
                    oConnMgr.abort(oQueue.conn);
                    oQueue.conn = null;
                    YAHOO.log("Canceled stale request", "warn", this.toString());
                }
                else {
                    YAHOO.log("Could not find Connection Manager abort() function", "error", this.toString());
                }
            }
    }

    // Get ready to send the request URL
    if(oConnMgr && oConnMgr.asyncRequest) {
        var sLiveData = this.liveData;
        var isPost = this.connMethodPost;
        var sMethod = (isPost) ? "POST" : "GET";
        // Validate request
        var sUri = (isPost || !lang.isValue(oRequest)) ? sLiveData : sLiveData+oRequest;
        var sRequest = (isPost) ? oRequest : null;

        // Send the request right away
        if(this.connXhrMode != "queueRequests") {
            oQueue.conn = oConnMgr.asyncRequest(sMethod, sUri, _xhrCallback, sRequest);
        }
        // Queue up then send the request
        else {
            // Found a request already in progress
            if(oQueue.conn) {
                var allRequests = oQueue.requests;
                // Add request to queue
                allRequests.push({request:oRequest, callback:_xhrCallback});

                // Interval needs to be started
                if(!oQueue.interval) {
                    oQueue.interval = setInterval(function() {
                        // Connection is in progress
                        if(oConnMgr.isCallInProgress(oQueue.conn)) {
                            return;
                        }
                        else {
                            // Send next request
                            if(allRequests.length > 0) {
                                // Validate request
                                sUri = (isPost || !lang.isValue(allRequests[0].request)) ? sLiveData : sLiveData+allRequests[0].request;
                                sRequest = (isPost) ? allRequests[0].request : null;
                                oQueue.conn = oConnMgr.asyncRequest(sMethod, sUri, allRequests[0].callback, sRequest);

                                // Remove request from queue
                                allRequests.shift();
                            }
                            // No more requests
                            else {
                                clearInterval(oQueue.interval);
                                oQueue.interval = null;
                            }
                        }
                    }, 50);
                }
            }
            // Nothing is in progress
            else {
                oQueue.conn = oConnMgr.asyncRequest(sMethod, sUri, _xhrCallback, sRequest);
            }
        }
    }
    else {
        YAHOO.log("Could not find Connection Manager asyncRequest() function", "error", this.toString());
        // Send null response back to the caller with the error flag on
        DS.issueCallback(oCallback,[oRequest,{error:true}],true,oCaller);
    }

    return tId;
}

});

// Copy static members to XHRDataSource class
lang.augmentObject(util.XHRDataSource, DS);













/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * Factory class for creating a BaseDataSource subclass instance. The sublcass is
 * determined by oLiveData's type, unless the dataType config is explicitly passed in.  
 *
 * @namespace YAHOO.util
 * @class YAHOO.util.DataSource
 * @constructor
 * @param oLiveData {HTMLElement}  Pointer to live data.
 * @param oConfigs {object} (optional) Object literal of configuration values.
 */
util.DataSource = function(oLiveData, oConfigs) {
    oConfigs = oConfigs || {};
    
    // Point to one of the subclasses, first by dataType if given, then by sniffing oLiveData type.
    var dataType = oConfigs.dataType;
    if(dataType) {
        if(dataType == DS.TYPE_LOCAL) {
            return new util.LocalDataSource(oLiveData, oConfigs);
        }
        else if(dataType == DS.TYPE_XHR) {
            return new util.XHRDataSource(oLiveData, oConfigs);            
        }
        else if(dataType == DS.TYPE_SCRIPTNODE) {
            return new util.ScriptNodeDataSource(oLiveData, oConfigs);            
        }
        else if(dataType == DS.TYPE_JSFUNCTION) {
            return new util.FunctionDataSource(oLiveData, oConfigs);            
        }
    }
    
    if(YAHOO.lang.isString(oLiveData)) { // strings default to xhr
        return new util.XHRDataSource(oLiveData, oConfigs);
    }
    else if(YAHOO.lang.isFunction(oLiveData)) {
        return new util.FunctionDataSource(oLiveData, oConfigs);
    }
    else { // ultimate default is local
        return new util.LocalDataSource(oLiveData, oConfigs);
    }
};

// Copy static members to DataSource class
lang.augmentObject(util.DataSource, DS);

})();

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * The static Number class provides helper functions to deal with data of type
 * Number.
 *
 * @namespace YAHOO.util
 * @requires yahoo
 * @class Number
 * @static
 */
 YAHOO.util.Number = {
 
     /**
     * Takes a native JavaScript Number and formats to a string for display.
     *
     * @method format
     * @param nData {Number} Number.
     * @param oConfig {Object} (Optional) Optional configuration values:
     *  <dl>
     *   <dt>format</dt>
     *   <dd>String used as a template for formatting positive numbers.
     *   {placeholders} in the string are applied from the values in this
     *   config object. {number} is used to indicate where the numeric portion
     *   of the output goes.  For example &quot;{prefix}{number} per item&quot;
     *   might yield &quot;$5.25 per item&quot;.  The only required
     *   {placeholder} is {number}.</dd>
     *
     *   <dt>negativeFormat</dt>
     *   <dd>Like format, but applied to negative numbers.  If set to null,
     *   defaults from the configured format, prefixed with -.  This is
     *   separate from format to support formats like &quot;($12,345.67)&quot;.
     *
     *   <dt>prefix {String} (deprecated, use format/negativeFormat)</dt>
     *   <dd>String prepended before each number, like a currency designator "$"</dd>
     *   <dt>decimalPlaces {Number}</dt>
     *   <dd>Number of decimal places to round.</dd>
     *
     *   <dt>decimalSeparator {String}</dt>
     *   <dd>Decimal separator</dd>
     *
     *   <dt>thousandsSeparator {String}</dt>
     *   <dd>Thousands separator</dd>
     *
     *   <dt>suffix {String} (deprecated, use format/negativeFormat)</dt>
     *   <dd>String appended after each number, like " items" (note the space)</dd>
     *  </dl>
     * @return {String} Formatted number for display. Note, the following values
     * return as "": null, undefined, NaN, "".
     */
    format : function(n, cfg) {
        if (n === '' || n === null || !isFinite(n)) {
            return '';
        }

        n   = +n;
        cfg = YAHOO.lang.merge(YAHOO.util.Number.format.defaults, (cfg || {}));

        var stringN = n+'',
            absN   = Math.abs(n),
            places = cfg.decimalPlaces || 0,
            sep    = cfg.thousandsSeparator,
            negFmt = cfg.negativeFormat || ('-' + cfg.format),
            s, bits, i, precision;

        if (negFmt.indexOf('#') > -1) {
            // for backward compatibility of negativeFormat supporting '-#'
            negFmt = negFmt.replace(/#/, cfg.format);
        }

        if (places < 0) {
            // Get rid of the decimal info
            s = absN - (absN % 1) + '';
            i = s.length + places;

            // avoid 123 vs decimalPlaces -4 (should return "0")
            if (i > 0) {
                // leverage toFixed by making 123 => 0.123 for the rounding
                // operation, then add the appropriate number of zeros back on
                s = Number('.' + s).toFixed(i).slice(2) +
                    new Array(s.length - i + 1).join('0');
            } else {
                s = "0";
            }
        } else {
            // Avoid toFixed on floats:
            // Bug 2528976
            // Bug 2528977
            var unfloatedN = absN+'';
            if(places > 0 || unfloatedN.indexOf('.') > 0) {
                var power = Math.pow(10, places);
                s = Math.round(absN * power) / power + '';
                var dot = s.indexOf('.'),
                    padding, zeroes;
                
                // Add padding
                if(dot < 0) {
                    padding = places;
                    zeroes = (Math.pow(10, padding) + '').substring(1);
                    if(places > 0) {
                        s = s + '.' + zeroes;
                    }
                }
                else {
                    padding = places - (s.length - dot - 1);
                    zeroes = (Math.pow(10, padding) + '').substring(1);
                    s = s + zeroes;
                }
            }
            else {
                s = absN.toFixed(places)+'';
            }
        }

        bits  = s.split(/\D/);

        if (absN >= 1000) {
            i  = bits[0].length % 3 || 3;

            bits[0] = bits[0].slice(0,i) +
                      bits[0].slice(i).replace(/(\d{3})/g, sep + '$1');

        }

        return YAHOO.util.Number.format._applyFormat(
            (n < 0 ? negFmt : cfg.format),
            bits.join(cfg.decimalSeparator),
            cfg);
    }
};

/**
 * <p>Default values for Number.format behavior.  Override properties of this
 * object if you want every call to Number.format in your system to use
 * specific presets.</p>
 *
 * <p>Available keys include:</p>
 * <ul>
 *   <li>format</li>
 *   <li>negativeFormat</li>
 *   <li>decimalSeparator</li>
 *   <li>decimalPlaces</li>
 *   <li>thousandsSeparator</li>
 *   <li>prefix/suffix or any other token you want to use in the format templates</li>
 * </ul>
 *
 * @property Number.format.defaults
 * @type {Object}
 * @static
 */
YAHOO.util.Number.format.defaults = {
    format : '{prefix}{number}{suffix}',
    negativeFormat : null, // defaults to -(format)
    decimalSeparator : '.',
    decimalPlaces    : null,
    thousandsSeparator : ''
};

/**
 * Apply any special formatting to the "d,ddd.dd" string.  Takes either the
 * cfg.format or cfg.negativeFormat template and replaces any {placeholders}
 * with either the number or a value from a so-named property of the config
 * object.
 *
 * @method Number.format._applyFormat
 * @static
 * @param tmpl {String} the cfg.format or cfg.numberFormat template string
 * @param num {String} the number with separators and decimalPlaces applied
 * @param data {Object} the config object, used here to populate {placeholder}s
 * @return {String} the number with any decorators added
 */
YAHOO.util.Number.format._applyFormat = function (tmpl, num, data) {
    return tmpl.replace(/\{(\w+)\}/g, function (_, token) {
        return token === 'number' ? num :
               token in data ? data[token] : '';
    });
};


/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

(function () {

var xPad=function (x, pad, r)
{
    if(typeof r === 'undefined')
    {
        r=10;
    }
    for( ; parseInt(x, 10)<r && r>1; r/=10) {
        x = pad.toString() + x;
    }
    return x.toString();
};


/**
 * The static Date class provides helper functions to deal with data of type Date.
 *
 * @namespace YAHOO.util
 * @requires yahoo
 * @class Date
 * @static
 */
 var Dt = {
    formats: {
        a: function (d, l) { return l.a[d.getDay()]; },
        A: function (d, l) { return l.A[d.getDay()]; },
        b: function (d, l) { return l.b[d.getMonth()]; },
        B: function (d, l) { return l.B[d.getMonth()]; },
        C: function (d) { return xPad(parseInt(d.getFullYear()/100, 10), 0); },
        d: ['getDate', '0'],
        e: ['getDate', ' '],
        g: function (d) { return xPad(parseInt(Dt.formats.G(d)%100, 10), 0); },
        G: function (d) {
                var y = d.getFullYear();
                var V = parseInt(Dt.formats.V(d), 10);
                var W = parseInt(Dt.formats.W(d), 10);
    
                if(W > V) {
                    y++;
                } else if(W===0 && V>=52) {
                    y--;
                }
    
                return y;
            },
        H: ['getHours', '0'],
        I: function (d) { var I=d.getHours()%12; return xPad(I===0?12:I, 0); },
        j: function (d) {
                var gmd_1 = new Date('' + d.getFullYear() + '/1/1 GMT');
                var gmdate = new Date('' + d.getFullYear() + '/' + (d.getMonth()+1) + '/' + d.getDate() + ' GMT');
                var ms = gmdate - gmd_1;
                var doy = parseInt(ms/60000/60/24, 10)+1;
                return xPad(doy, 0, 100);
            },
        k: ['getHours', ' '],
        l: function (d) { var I=d.getHours()%12; return xPad(I===0?12:I, ' '); },
        m: function (d) { return xPad(d.getMonth()+1, 0); },
        M: ['getMinutes', '0'],
        p: function (d, l) { return l.p[d.getHours() >= 12 ? 1 : 0 ]; },
        P: function (d, l) { return l.P[d.getHours() >= 12 ? 1 : 0 ]; },
        s: function (d, l) { return parseInt(d.getTime()/1000, 10); },
        S: ['getSeconds', '0'],
        u: function (d) { var dow = d.getDay(); return dow===0?7:dow; },
        U: function (d) {
                var doy = parseInt(Dt.formats.j(d), 10);
                var rdow = 6-d.getDay();
                var woy = parseInt((doy+rdow)/7, 10);
                return xPad(woy, 0);
            },
        V: function (d) {
                var woy = parseInt(Dt.formats.W(d), 10);
                var dow1_1 = (new Date('' + d.getFullYear() + '/1/1')).getDay();
                // First week is 01 and not 00 as in the case of %U and %W,
                // so we add 1 to the final result except if day 1 of the year
                // is a Monday (then %W returns 01).
                // We also need to subtract 1 if the day 1 of the year is 
                // Friday-Sunday, so the resulting equation becomes:
                var idow = woy + (dow1_1 > 4 || dow1_1 <= 1 ? 0 : 1);
                if(idow === 53 && (new Date('' + d.getFullYear() + '/12/31')).getDay() < 4)
                {
                    idow = 1;
                }
                else if(idow === 0)
                {
                    idow = Dt.formats.V(new Date('' + (d.getFullYear()-1) + '/12/31'));
                }
    
                return xPad(idow, 0);
            },
        w: 'getDay',
        W: function (d) {
                var doy = parseInt(Dt.formats.j(d), 10);
                var rdow = 7-Dt.formats.u(d);
                var woy = parseInt((doy+rdow)/7, 10);
                return xPad(woy, 0, 10);
            },
        y: function (d) { return xPad(d.getFullYear()%100, 0); },
        Y: 'getFullYear',
        z: function (d) {
                var o = d.getTimezoneOffset();
                var H = xPad(parseInt(Math.abs(o/60), 10), 0);
                var M = xPad(Math.abs(o%60), 0);
                return (o>0?'-':'+') + H + M;
            },
        Z: function (d) {
		var tz = d.toString().replace(/^.*:\d\d( GMT[+-]\d+)? \(?([A-Za-z ]+)\)?\d*$/, '$2').replace(/[a-z ]/g, '');
		if(tz.length > 4) {
			tz = Dt.formats.z(d);
		}
		return tz;
	},
        '%': function (d) { return '%'; }
    },

    aggregates: {
        c: 'locale',
        D: '%m/%d/%y',
        F: '%Y-%m-%d',
        h: '%b',
        n: '\n',
        r: 'locale',
        R: '%H:%M',
        t: '\t',
        T: '%H:%M:%S',
        x: 'locale',
        X: 'locale'
        //'+': '%a %b %e %T %Z %Y'
    },

     /**
     * Takes a native JavaScript Date and formats to string for display to user.
     *
     * @method format
     * @param oDate {Date} Date.
     * @param oConfig {Object} (Optional) Object literal of configuration values:
     *  <dl>
     *   <dt>format &lt;String&gt;</dt>
     *   <dd>
     *   <p>
     *   Any strftime string is supported, such as "%I:%M:%S %p". strftime has several format specifiers defined by the Open group at 
     *   <a href="http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html">http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html</a>
     *   </p>
     *   <p>   
     *   PHP added a few of its own, defined at <a href="http://www.php.net/strftime">http://www.php.net/strftime</a>
     *   </p>
     *   <p>
     *   This javascript implementation supports all the PHP specifiers and a few more.  The full list is below:
     *   </p>
     *   <dl>
     *    <dt>%a</dt> <dd>abbreviated weekday name according to the current locale</dd>
     *    <dt>%A</dt> <dd>full weekday name according to the current locale</dd>
     *    <dt>%b</dt> <dd>abbreviated month name according to the current locale</dd>
     *    <dt>%B</dt> <dd>full month name according to the current locale</dd>
     *    <dt>%c</dt> <dd>preferred date and time representation for the current locale</dd>
     *    <dt>%C</dt> <dd>century number (the year divided by 100 and truncated to an integer, range 00 to 99)</dd>
     *    <dt>%d</dt> <dd>day of the month as a decimal number (range 01 to 31)</dd>
     *    <dt>%D</dt> <dd>same as %m/%d/%y</dd>
     *    <dt>%e</dt> <dd>day of the month as a decimal number, a single digit is preceded by a space (range ' 1' to '31')</dd>
     *    <dt>%F</dt> <dd>same as %Y-%m-%d (ISO 8601 date format)</dd>
     *    <dt>%g</dt> <dd>like %G, but without the century</dd>
     *    <dt>%G</dt> <dd>The 4-digit year corresponding to the ISO week number</dd>
     *    <dt>%h</dt> <dd>same as %b</dd>
     *    <dt>%H</dt> <dd>hour as a decimal number using a 24-hour clock (range 00 to 23)</dd>
     *    <dt>%I</dt> <dd>hour as a decimal number using a 12-hour clock (range 01 to 12)</dd>
     *    <dt>%j</dt> <dd>day of the year as a decimal number (range 001 to 366)</dd>
     *    <dt>%k</dt> <dd>hour as a decimal number using a 24-hour clock (range 0 to 23); single digits are preceded by a blank. (See also %H.)</dd>
     *    <dt>%l</dt> <dd>hour as a decimal number using a 12-hour clock (range 1 to 12); single digits are preceded by a blank. (See also %I.) </dd>
     *    <dt>%m</dt> <dd>month as a decimal number (range 01 to 12)</dd>
     *    <dt>%M</dt> <dd>minute as a decimal number</dd>
     *    <dt>%n</dt> <dd>newline character</dd>
     *    <dt>%p</dt> <dd>either `AM' or `PM' according to the given time value, or the corresponding strings for the current locale</dd>
     *    <dt>%P</dt> <dd>like %p, but lower case</dd>
     *    <dt>%r</dt> <dd>time in a.m. and p.m. notation equal to %I:%M:%S %p</dd>
     *    <dt>%R</dt> <dd>time in 24 hour notation equal to %H:%M</dd>
     *    <dt>%s</dt> <dd>number of seconds since the Epoch, ie, since 1970-01-01 00:00:00 UTC</dd>
     *    <dt>%S</dt> <dd>second as a decimal number</dd>
     *    <dt>%t</dt> <dd>tab character</dd>
     *    <dt>%T</dt> <dd>current time, equal to %H:%M:%S</dd>
     *    <dt>%u</dt> <dd>weekday as a decimal number [1,7], with 1 representing Monday</dd>
     *    <dt>%U</dt> <dd>week number of the current year as a decimal number, starting with the
     *            first Sunday as the first day of the first week</dd>
     *    <dt>%V</dt> <dd>The ISO 8601:1988 week number of the current year as a decimal number,
     *            range 01 to 53, where week 1 is the first week that has at least 4 days
     *            in the current year, and with Monday as the first day of the week.</dd>
     *    <dt>%w</dt> <dd>day of the week as a decimal, Sunday being 0</dd>
     *    <dt>%W</dt> <dd>week number of the current year as a decimal number, starting with the
     *            first Monday as the first day of the first week</dd>
     *    <dt>%x</dt> <dd>preferred date representation for the current locale without the time</dd>
     *    <dt>%X</dt> <dd>preferred time representation for the current locale without the date</dd>
     *    <dt>%y</dt> <dd>year as a decimal number without a century (range 00 to 99)</dd>
     *    <dt>%Y</dt> <dd>year as a decimal number including the century</dd>
     *    <dt>%z</dt> <dd>numerical time zone representation</dd>
     *    <dt>%Z</dt> <dd>time zone name or abbreviation</dd>
     *    <dt>%%</dt> <dd>a literal `%' character</dd>
     *   </dl>
     *  </dd>
     * </dl>
     * @param sLocale {String} (Optional) The locale to use when displaying days of week,
     *  months of the year, and other locale specific strings.  The following locales are
     *  built in:
     *  <dl>
     *   <dt>en</dt>
     *   <dd>English</dd>
     *   <dt>en-US</dt>
     *   <dd>US English</dd>
     *   <dt>en-GB</dt>
     *   <dd>British English</dd>
     *   <dt>en-AU</dt>
     *   <dd>Australian English (identical to British English)</dd>
     *  </dl>
     *  More locales may be added by subclassing of YAHOO.util.DateLocale.
     *  See YAHOO.util.DateLocale for more information.
     * @return {HTML} Formatted date for display. Non-date values are passed
     * through as-is.
     * @sa YAHOO.util.DateLocale
     */
    format : function (oDate, oConfig, sLocale) {
        oConfig = oConfig || {};
        
        if(!(oDate instanceof Date)) {
            return YAHOO.lang.isValue(oDate) ? oDate : "";
        }

        var format = oConfig.format || "%m/%d/%Y";

        // Be backwards compatible, support strings that are
        // exactly equal to YYYY/MM/DD, DD/MM/YYYY and MM/DD/YYYY
        if(format === 'YYYY/MM/DD') {
            format = '%Y/%m/%d';
        } else if(format === 'DD/MM/YYYY') {
            format = '%d/%m/%Y';
        } else if(format === 'MM/DD/YYYY') {
            format = '%m/%d/%Y';
        }
        // end backwards compatibility block
 
        sLocale = sLocale || "en";

        // Make sure we have a definition for the requested locale, or default to en.
        if(!(sLocale in YAHOO.util.DateLocale)) {
            if(sLocale.replace(/-[a-zA-Z]+$/, '') in YAHOO.util.DateLocale) {
                sLocale = sLocale.replace(/-[a-zA-Z]+$/, '');
            } else {
                sLocale = "en";
            }
        }

        var aLocale = YAHOO.util.DateLocale[sLocale];

        var replace_aggs = function (m0, m1) {
            var f = Dt.aggregates[m1];
            return (f === 'locale' ? aLocale[m1] : f);
        };

        var replace_formats = function (m0, m1) {
            var f = Dt.formats[m1];
            if(typeof f === 'string') {             // string => built in date function
                return oDate[f]();
            } else if(typeof f === 'function') {    // function => our own function
                return f.call(oDate, oDate, aLocale);
            } else if(typeof f === 'object' && typeof f[0] === 'string') {  // built in function with padding
                return xPad(oDate[f[0]](), f[1]);
            } else {
                return m1;
            }
        };

        // First replace aggregates (run in a loop because an agg may be made up of other aggs)
        while(format.match(/%[cDFhnrRtTxX]/)) {
            format = format.replace(/%([cDFhnrRtTxX])/g, replace_aggs);
        }

        // Now replace formats (do not run in a loop otherwise %%a will be replace with the value of %a)
        var str = format.replace(/%([aAbBCdegGHIjklmMpPsSuUVwWyYzZ%])/g, replace_formats);

        replace_aggs = replace_formats = undefined;

        return str;
    }
 };
 
 YAHOO.namespace("YAHOO.util");
 YAHOO.util.Date = Dt;

/**
 * The DateLocale class is a container and base class for all
 * localised date strings used by YAHOO.util.Date. It is used
 * internally, but may be extended to provide new date localisations.
 *
 * To create your own DateLocale, follow these steps:
 * <ol>
 *  <li>Find an existing locale that matches closely with your needs</li>
 *  <li>Use this as your base class.  Use YAHOO.util.DateLocale if nothing
 *   matches.</li>
 *  <li>Create your own class as an extension of the base class using
 *   YAHOO.lang.merge, and add your own localisations where needed.</li>
 * </ol>
 * See the YAHOO.util.DateLocale['en-US'] and YAHOO.util.DateLocale['en-GB']
 * classes which extend YAHOO.util.DateLocale['en'].
 *
 * For example, to implement locales for French french and Canadian french,
 * we would do the following:
 * <ol>
 *  <li>For French french, we have no existing similar locale, so use
 *   YAHOO.util.DateLocale as the base, and extend it:
 *   <pre>
 *      YAHOO.util.DateLocale['fr'] = YAHOO.lang.merge(YAHOO.util.DateLocale, {
 *          a: ['dim', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam'],
 *          A: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
 *          b: ['jan', 'f&eacute;v', 'mar', 'avr', 'mai', 'jun', 'jui', 'ao&ucirc;', 'sep', 'oct', 'nov', 'd&eacute;c'],
 *          B: ['janvier', 'f&eacute;vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao&ucirc;t', 'septembre', 'octobre', 'novembre', 'd&eacute;cembre'],
 *          c: '%a %d %b %Y %T %Z',
 *          p: ['', ''],
 *          P: ['', ''],
 *          x: '%d.%m.%Y',
 *          X: '%T'
 *      });
 *   </pre>
 *  </li>
 *  <li>For Canadian french, we start with French french and change the meaning of \%x:
 *   <pre>
 *      YAHOO.util.DateLocale['fr-CA'] = YAHOO.lang.merge(YAHOO.util.DateLocale['fr'], {
 *          x: '%Y-%m-%d'
 *      });
 *   </pre>
 *  </li>
 * </ol>
 *
 * With that, you can use your new locales:
 * <pre>
 *    var d = new Date("2008/04/22");
 *    YAHOO.util.Date.format(d, {format: "%A, %d %B == %x"}, "fr");
 * </pre>
 * will return:
 * <pre>
 *    mardi, 22 avril == 22.04.2008
 * </pre>
 * And
 * <pre>
 *    YAHOO.util.Date.format(d, {format: "%A, %d %B == %x"}, "fr-CA");
 * </pre>
 * Will return:
 * <pre>
 *   mardi, 22 avril == 2008-04-22
 * </pre>
 * @namespace YAHOO.util
 * @requires yahoo
 * @class DateLocale
 */
 YAHOO.util.DateLocale = {
        a: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
        A: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
        b: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
        B: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
        c: '%a %d %b %Y %T %Z',
        p: ['AM', 'PM'],
        P: ['am', 'pm'],
        r: '%I:%M:%S %p',
        x: '%d/%m/%y',
        X: '%T'
 };

 YAHOO.util.DateLocale['en'] = YAHOO.lang.merge(YAHOO.util.DateLocale, {});

 YAHOO.util.DateLocale['en-US'] = YAHOO.lang.merge(YAHOO.util.DateLocale['en'], {
        c: '%a %d %b %Y %I:%M:%S %p %Z',
        x: '%m/%d/%Y',
        X: '%I:%M:%S %p'
 });

 YAHOO.util.DateLocale['en-GB'] = YAHOO.lang.merge(YAHOO.util.DateLocale['en'], {
        r: '%l:%M:%S %P %Z'
 });
 YAHOO.util.DateLocale['en-AU'] = YAHOO.lang.merge(YAHOO.util.DateLocale['en']);

})();

YAHOO.register("datasource", YAHOO.util.DataSource, {version: "2.9.0", build: "2800"});
/*
Copyright (c) 2011, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 2.9.0
*/
/**
 * Mechanism to execute a series of callbacks in a non-blocking queue.  Each callback is executed via setTimout unless configured with a negative timeout, in which case it is run in blocking mode in the same execution thread as the previous callback.  Callbacks can be function references or object literals with the following keys:
 * <ul>
 *    <li><code>method</code> - {Function} REQUIRED the callback function.</li>
 *    <li><code>scope</code> - {Object} the scope from which to execute the callback.  Default is the global window scope.</li>
 *    <li><code>argument</code> - {Array} parameters to be passed to method as individual arguments.</li>
 *    <li><code>timeout</code> - {number} millisecond delay to wait after previous callback completion before executing this callback.  Negative values cause immediate blocking execution.  Default 0.</li>
 *    <li><code>until</code> - {Function} boolean function executed before each iteration.  Return true to indicate completion and proceed to the next callback.</li>
 *    <li><code>iterations</code> - {Number} number of times to execute the callback before proceeding to the next callback in the chain. Incompatible with <code>until</code>.</li>
 * </ul>
 *
 * @namespace YAHOO.util
 * @class Chain
 * @constructor
 * @param callback* {Function|Object} Any number of callbacks to initialize the queue
*/
YAHOO.util.Chain = function () {
    /**
     * The callback queue
     * @property q
     * @type {Array}
     * @private
     */
    this.q = [].slice.call(arguments);

    /**
     * Event fired when the callback queue is emptied via execution (not via
     * a call to chain.stop().
     * @event end
     */
    this.createEvent('end');
};

YAHOO.util.Chain.prototype = {
    /**
     * Timeout id used to pause or stop execution and indicate the execution state of the Chain.  0 indicates paused or stopped, -1 indicates blocking execution, and any positive number indicates non-blocking execution.
     * @property id
     * @type {number}
     * @private
     */
    id   : 0,

    /**
     * Begin executing the chain, or resume execution from the last paused position.
     * @method run
     * @return {Chain} the Chain instance
     */
    run : function () {
        // Grab the first callback in the queue
        var c  = this.q[0],
            fn;

        // If there is no callback in the queue or the Chain is currently
        // in an execution mode, return
        if (!c) {
            this.fireEvent('end');
            return this;
        } else if (this.id) {
            return this;
        }

        fn = c.method || c;

        if (typeof fn === 'function') {
            var o    = c.scope || {},
                args = c.argument || [],
                ms   = c.timeout || 0,
                me   = this;
                
            if (!(args instanceof Array)) {
                args = [args];
            }

            // Execute immediately if the callback timeout is negative.
            if (ms < 0) {
                this.id = ms;
                if (c.until) {
                    for (;!c.until();) {
                        // Execute the callback from scope, with argument
                        fn.apply(o,args);
                    }
                } else if (c.iterations) {
                    for (;c.iterations-- > 0;) {
                        fn.apply(o,args);
                    }
                } else {
                    fn.apply(o,args);
                }
                this.q.shift();
                this.id = 0;
                return this.run();
            } else {
                // If the until condition is set, check if we're done
                if (c.until) {
                    if (c.until()) {
                        // Shift this callback from the queue and execute the next
                        // callback
                        this.q.shift();
                        return this.run();
                    }
                // Otherwise if either iterations is not set or we're
                // executing the last iteration, shift callback from the queue
                } else if (!c.iterations || !--c.iterations) {
                    this.q.shift();
                }

                // Otherwise set to execute after the configured timeout
                this.id = setTimeout(function () {
                    // Execute the callback from scope, with argument
                    fn.apply(o,args);
                    // Check if the Chain was not paused from inside the callback
                    if (me.id) {
                        // Indicate ready to run state
                        me.id = 0;
                        // Start the fun all over again
                        me.run();
                    }
                },ms);
            }
        }

        return this;
    },
    
    /**
     * Add a callback to the end of the queue
     * @method add
     * @param c {Function|Object} the callback function ref or object literal
     * @return {Chain} the Chain instance
     */
    add  : function (c) {
        this.q.push(c);
        return this;
    },

    /**
     * Pause the execution of the Chain after the current execution of the
     * current callback completes.  If called interstitially, clears the
     * timeout for the pending callback. Paused Chains can be restarted with
     * chain.run()
     * @method pause
     * @return {Chain} the Chain instance
     */
    pause: function () {
        // Conditional added for Caja compatibility
        if (this.id > 0) {
            clearTimeout(this.id);
        }
        this.id = 0;
        return this;
    },

    /**
     * Stop and clear the Chain's queue after the current execution of the
     * current callback completes.
     * @method stop
     * @return {Chain} the Chain instance
     */
    stop : function () { 
        this.pause();
        this.q = [];
        return this;
    }
};
YAHOO.lang.augmentProto(YAHOO.util.Chain,YAHOO.util.EventProvider);

/**
 * Augments the Event Utility with a <code>delegate</code> method that 
 * facilitates easy creation of delegated event listeners.  (Note: Using CSS 
 * selectors as the filtering criteria for delegated event listeners requires 
 * inclusion of the Selector Utility.)
 *
 * @module event-delegate
 * @title Event Utility Event Delegation Module
 * @namespace YAHOO.util
 * @requires event
 */

(function () {

    var Event = YAHOO.util.Event,
        Lang = YAHOO.lang,
        delegates = [],


        getMatch = function(el, selector, container) {
        
            var returnVal;
        
            if (!el || el === container) {
                returnVal = false;
            }
            else {
                returnVal = YAHOO.util.Selector.test(el, selector) ? el: getMatch(el.parentNode, selector, container);
            }
        
            return returnVal;
        
        };


    Lang.augmentObject(Event, {

        /**
         * Creates a delegate function used to call event listeners specified 
         * via the <code>YAHOO.util.Event.delegate</code> method.
         *
         * @method _createDelegate
         *
         * @param {Function} fn        The method (event listener) to call.
         * @param {Function|string} filter Function or CSS selector used to 
         * determine for what element(s) the event listener should be called.        
         * @param {Object}   obj    An arbitrary object that will be 
         *                             passed as a parameter to the listener.
         * @param {Boolean|object}  overrideContext  If true, the value of the 
         *                             obj parameter becomes the execution context
         *                          of the listener. If an object, this object
         *                          becomes the execution context.
         * @return {Function} Function that will call the event listener 
         * specified by the <code>YAHOO.util.Event.delegate</code> method.
         * @private
         * @for Event
         * @static
         */
        _createDelegate: function (fn, filter, obj, overrideContext) {

            return function (event) {

                var container = this,
                    target = Event.getTarget(event),
                    selector = filter,

                    //    The user might have specified the document object 
                    //    as the delegation container, in which case it is not 
                    //    nessary to scope the provided CSS selector(s) to the 
                    //    delegation container
                    bDocument = (container.nodeType === 9),

                    matchedEl,
                    context,
                    sID,
                    sIDSelector;


                if (Lang.isFunction(filter)) {
                    matchedEl = filter(target);
                }
                else if (Lang.isString(filter)) {

                    if (!bDocument) {

                        sID = container.id;

                        if (!sID) {
                            sID = Event.generateId(container);
                        }                        

                        //    Scope all selectors to the container
                        sIDSelector = ("#" + sID + " ");
                        selector = (sIDSelector + filter).replace(/,/gi, ("," + sIDSelector));

                    }


                    if (YAHOO.util.Selector.test(target, selector)) {
                        matchedEl = target;
                    }
                    else if (YAHOO.util.Selector.test(target, ((selector.replace(/,/gi, " *,")) + " *"))) {

                        //    The target is a descendant of an element matching 
                        //    the selector, so crawl up to find the ancestor that 
                        //    matches the selector

                        matchedEl = getMatch(target, selector, container);

                    }

                }


                if (matchedEl) {

                    //    The default context for delegated listeners is the 
                    //    element that matched the filter.

                    context = matchedEl;

                    if (overrideContext) {
                        if (overrideContext === true) {
                            context = obj;
                        } else {
                            context = overrideContext;
                        }
                    }

                    //    Call the listener passing in the container and the 
                    //    element that matched the filter in case the user 
                    //    needs those.

                    return fn.call(context, event, matchedEl, container, obj);

                }

            };

        },


        /**
         * Appends a delegated event listener.  Delegated event listeners 
         * receive three arguments by default: the DOM event, the element  
         * specified by the filtering function or CSS selector, and the 
         * container element (the element to which the event listener is 
         * bound).  (Note: Using the delegate method requires the event-delegate 
         * module.  Using CSS selectors as the filtering criteria for delegated 
         * event listeners requires inclusion of the Selector Utility.)
         *
         * @method delegate
         *
         * @param {String|HTMLElement|Array|NodeList} container An id, an element 
         *  reference, or a collection of ids and/or elements to assign the 
         *  listener to.
         * @param {String}   type     The type of event listener to append
         * @param {Function} fn        The method the event invokes
         * @param {Function|string} filter Function or CSS selector used to 
         * determine for what element(s) the event listener should be called. 
         * When a function is specified, the function should return an 
         * HTML element.  Using a CSS Selector requires the inclusion of the 
         * CSS Selector Utility.
         * @param {Object}   obj    An arbitrary object that will be 
         *                             passed as a parameter to the listener
         * @param {Boolean|object}  overrideContext  If true, the value of the obj parameter becomes
         *                             the execution context of the listener. If an
         *                             object, this object becomes the execution
         *                             context.
         * @return {Boolean} Returns true if the action was successful or defered,
         *                   false if one or more of the elements 
         *                   could not have the listener attached,
         *                   or if the operation throws an exception.
         * @static
         * @for Event
         */
        delegate: function (container, type, fn, filter, obj, overrideContext) {

            var sType = type,
                fnMouseDelegate,
                fnDelegate;


            if (Lang.isString(filter) && !YAHOO.util.Selector) {
                YAHOO.log("Using a CSS selector to define the filtering criteria for a delegated listener requires the Selector Utility.", "error", "Event");
                return false;
            }


            if (type == "mouseenter" || type == "mouseleave") {

                if (!Event._createMouseDelegate) {
                    YAHOO.log("Delegating a " + type + " event requires the event-mouseenter module.", "error", "Event");
                    return false;
                }

                //    Look up the real event--either mouseover or mouseout
                sType = Event._getType(type);

                fnMouseDelegate = Event._createMouseDelegate(fn, obj, overrideContext);

                fnDelegate = Event._createDelegate(function (event, matchedEl, container) {

                    return fnMouseDelegate.call(matchedEl, event, container);

                }, filter, obj, overrideContext);

            }
            else {

                fnDelegate = Event._createDelegate(fn, filter, obj, overrideContext);

            }

            delegates.push([container, sType, fn, fnDelegate]);
            
            return Event.on(container, sType, fnDelegate);

        },


        /**
         * Removes a delegated event listener.
         *
         * @method removeDelegate
         *
         * @param {String|HTMLElement|Array|NodeList} container An id, an element 
         *  reference, or a collection of ids and/or elements to remove
         *  the listener from.
         * @param {String} type The type of event to remove.
         * @param {Function} fn The method the event invokes.  If fn is
         *  undefined, then all event listeners for the type of event are 
         *  removed.
         * @return {boolean} Returns true if the unbind was successful, false 
         *  otherwise.
         * @static
         * @for Event
         */
        removeDelegate: function (container, type, fn) {

            var sType = type,
                returnVal = false,
                index,
                cacheItem;

            //    Look up the real event--either mouseover or mouseout
            if (type == "mouseenter" || type == "mouseleave") {
                sType = Event._getType(type);
            }

            index = Event._getCacheIndex(delegates, container, sType, fn);

            if (index >= 0) {
                cacheItem = delegates[index];
            }


            if (container && cacheItem) {

                returnVal = Event.removeListener(cacheItem[0], cacheItem[1], cacheItem[3]);

                if (returnVal) {
                    delete delegates[index][2];
                    delete delegates[index][3];
                    delegates.splice(index, 1);
                }        
        
            }

            return returnVal;

        }
        
    });

}());


/**
 * Augments the Event Utility with support for the mouseenter and mouseleave
 * events:  A mouseenter event fires the first time the mouse enters an
 * element; a mouseleave event first the first time the mouse leaves an
 * element.
 *
 * @module event-mouseenter
 * @title Event Utility mouseenter and mouseout Module
 * @namespace YAHOO.util
 * @requires event
 */

(function () {

    var Event = YAHOO.util.Event,
        Lang = YAHOO.lang,

        addListener = Event.addListener,
        removeListener = Event.removeListener,
        getListeners = Event.getListeners,

        delegates = [],

        specialTypes = {
            mouseenter: "mouseover",
            mouseleave: "mouseout"
        },

        remove = function(el, type, fn) {

            var index = Event._getCacheIndex(delegates, el, type, fn),
                cacheItem,
                returnVal;

            if (index >= 0) {
                cacheItem = delegates[index];
            }

            if (el && cacheItem) {

                //    removeListener will translate the value of type
                returnVal = removeListener.call(Event, cacheItem[0], type, cacheItem[3]);

                if (returnVal) {
                    delete delegates[index][2];
                    delete delegates[index][3];
                    delegates.splice(index, 1);
                }

            }

            return returnVal;

        };


    Lang.augmentObject(Event._specialTypes, specialTypes);

    Lang.augmentObject(Event, {

        /**
         * Creates a delegate function used to call mouseover and mouseleave
         * event listeners specified via the
         * <code>YAHOO.util.Event.addListener</code>
         * or <code>YAHOO.util.Event.on</code> method.
         *
         * @method _createMouseDelegate
         *
         * @param {Function} fn        The method (event listener) to call
         * @param {Object}   obj    An arbitrary object that will be
         *                             passed as a parameter to the listener
         * @param {Boolean|object}  overrideContext  If true, the value of the
         *                             obj parameter becomes the execution context
         *                          of the listener. If an object, this object
         *                          becomes the execution context.
         * @return {Function} Function that will call the event listener
         * specified by either the <code>YAHOO.util.Event.addListener</code>
         * or <code>YAHOO.util.Event.on</code> method.
         * @private
         * @static
         * @for Event
         */
        _createMouseDelegate: function (fn, obj, overrideContext) {

            return function (event, container) {

                var el = this,
                    relatedTarget = Event.getRelatedTarget(event),
                    context,
                    args;

                if (el != relatedTarget && !YAHOO.util.Dom.isAncestor(el, relatedTarget)) {

                    context = el;

                    if (overrideContext) {
                        if (overrideContext === true) {
                            context = obj;
                        } else {
                            context = overrideContext;
                        }
                    }

                    // The default args passed back to a mouseenter or
                    // mouseleave listener are: the event, and any object
                    // the user passed when subscribing

                    args = [event, obj];

                    // Add the element and delegation container as arguments
                    // when delegating mouseenter and mouseleave

                    if (container) {
                        args.splice(1, 0, el, container);
                    }

                    return fn.apply(context, args);

                }

            };

        },

        addListener: function (el, type, fn, obj, overrideContext) {

            var fnDelegate,
                returnVal;

            if (specialTypes[type]) {

                fnDelegate = Event._createMouseDelegate(fn, obj, overrideContext);

                fnDelegate.mouseDelegate = true;

                delegates.push([el, type, fn, fnDelegate]);

                //    addListener will translate the value of type
                returnVal = addListener.call(Event, el, type, fnDelegate);

            }
            else {
                returnVal = addListener.apply(Event, arguments);
            }

            return returnVal;

        },

        removeListener: function (el, type, fn) {

            var returnVal;

            if (specialTypes[type]) {
                returnVal = remove.apply(Event, arguments);
            }
            else {
                returnVal = removeListener.apply(Event, arguments);
            }

            return returnVal;

        },

        getListeners: function (el, type) {

            //    If the user specified the type as mouseover or mouseout,
            //    need to filter out those used by mouseenter and mouseleave.
            //    If the user specified the type as mouseenter or mouseleave,
            //    need to filter out the true mouseover and mouseout listeners.

            var listeners = [],
                elListeners,
                bMouseOverOrOut = (type === "mouseover" || type === "mouseout"),
                bMouseDelegate,
                i,
                l;

            if (type && (bMouseOverOrOut || specialTypes[type])) {

                elListeners = getListeners.call(Event, el, this._getType(type));

                if (elListeners) {

                    for (i=elListeners.length-1; i>-1; i--) {

                        l = elListeners[i];
                        bMouseDelegate = l.fn.mouseDelegate;

                        if ((specialTypes[type] && bMouseDelegate) || (bMouseOverOrOut && !bMouseDelegate)) {
                            listeners.push(l);
                        }

                    }

                }

            }
            else {
                listeners = getListeners.apply(Event, arguments);
            }

            return (listeners && listeners.length) ? listeners : null;

        }

    }, true);

    Event.on = Event.addListener;

}());
YAHOO.register("event-mouseenter", YAHOO.util.Event, {version: "2.9.0", build: "2800"});

var Y = YAHOO,
    Y_DOM = YAHOO.util.Dom,
    EMPTY_ARRAY = [],
    Y_UA = Y.env.ua,
    Y_Lang = Y.lang,
    Y_DOC = document,
    Y_DOCUMENT_ELEMENT = Y_DOC.documentElement,

    Y_DOM_inDoc = Y_DOM.inDocument,
    Y_mix = Y_Lang.augmentObject,
    Y_guid = Y_DOM.generateId,

    Y_getDoc = function(element) {
        var doc = Y_DOC;
        if (element) {
            doc = (element.nodeType === 9) ? element : // element === document
                element.ownerDocument || // element === DOM node
                element.document || // element === window
                Y_DOC; // default
        }

        return doc;
    },

    Y_Array = function(o, startIdx) {
        var l, a, start = startIdx || 0;

        // IE errors when trying to slice HTMLElement collections
        try {
            return Array.prototype.slice.call(o, start);
        } catch (e) {
            a = [];
            l = o.length;
            for (; start < l; start++) {
                a.push(o[start]);
            }
            return a;
        }
    },

    Y_DOM_allById = function(id, root) {
        root = root || Y_DOC;
        var nodes = [],
            ret = [],
            i,
            node;

        if (root.querySelectorAll) {
            ret = root.querySelectorAll('[id="' + id + '"]');
        } else if (root.all) {
            nodes = root.all(id);

            if (nodes) {
                // root.all may return HTMLElement or HTMLCollection.
                // some elements are also HTMLCollection (FORM, SELECT).
                if (nodes.nodeName) {
                    if (nodes.id === id) { // avoid false positive on name
                        ret.push(nodes);
                        nodes = EMPTY_ARRAY; // done, no need to filter
                    } else { //  prep for filtering
                        nodes = [nodes];
                    }
                }

                if (nodes.length) {
                    // filter out matches on node.name
                    // and element.id as reference to element with id === 'id'
                    for (i = 0; node = nodes[i++];) {
                        if (node.id === id  ||
                                (node.attributes && node.attributes.id &&
                                node.attributes.id.value === id)) {
                            ret.push(node);
                        }
                    }
                }
            }
        } else {
            ret = [Y_getDoc(root).getElementById(id)];
        }

        return ret;
    };

/**
 * The selector-native module provides support for native querySelector
 * @module dom
 * @submodule selector-native
 * @for Selector
 */

/**
 * Provides support for using CSS selectors to query the DOM
 * @class Selector
 * @static
 * @for Selector
 */

var COMPARE_DOCUMENT_POSITION = 'compareDocumentPosition',
    OWNER_DOCUMENT = 'ownerDocument',

Selector = {
    _foundCache: [],

    useNative: true,

    _compare: ('sourceIndex' in Y_DOCUMENT_ELEMENT) ?
        function(nodeA, nodeB) {
            var a = nodeA.sourceIndex,
                b = nodeB.sourceIndex;

            if (a === b) {
                return 0;
            } else if (a > b) {
                return 1;
            }

            return -1;

        } : (Y_DOCUMENT_ELEMENT[COMPARE_DOCUMENT_POSITION] ?
        function(nodeA, nodeB) {
            if (nodeA[COMPARE_DOCUMENT_POSITION](nodeB) & 4) {
                return -1;
            } else {
                return 1;
            }
        } :
        function(nodeA, nodeB) {
            var rangeA, rangeB, compare;
            if (nodeA && nodeB) {
                rangeA = nodeA[OWNER_DOCUMENT].createRange();
                rangeA.setStart(nodeA, 0);
                rangeB = nodeB[OWNER_DOCUMENT].createRange();
                rangeB.setStart(nodeB, 0);
                compare = rangeA.compareBoundaryPoints(1, rangeB); // 1 === Range.START_TO_END
            }

            return compare;

    }),

    _sort: function(nodes) {
        if (nodes) {
            nodes = Y_Array(nodes, 0, true);
            if (nodes.sort) {
                nodes.sort(Selector._compare);
            }
        }

        return nodes;
    },

    _deDupe: function(nodes) {
        var ret = [],
            i, node;

        for (i = 0; (node = nodes[i++]);) {
            if (!node._found) {
                ret[ret.length] = node;
                node._found = true;
            }
        }

        for (i = 0; (node = ret[i++]);) {
            node._found = null;
            node.removeAttribute('_found');
        }

        return ret;
    },

    /**
     * Retrieves a set of nodes based on a given CSS selector.
     * @method query
     *
     * @param {string} selector The CSS Selector to test the node against.
     * @param {HTMLElement} root optional An HTMLElement to start the query from. Defaults to Y.config.doc
     * @param {Boolean} firstOnly optional Whether or not to return only the first match.
     * @return {Array} An array of nodes that match the given selector.
     * @static
     */
    query: function(selector, root, firstOnly, skipNative) {
        if (typeof root == 'string') {
            root = Y_DOM.get(root);
            if (!root) {
                return (firstOnly) ? null : [];
            }
        } else {
            root = root || Y_DOC;
        }

        var ret = [],
            useNative = (Selector.useNative && Y_DOC.querySelector && !skipNative),
            queries = [[selector, root]],
            query,
            result,
            i,
            fn = (useNative) ? Selector._nativeQuery : Selector._bruteQuery;

        if (selector && fn) {
            // split group into seperate queries
            if (!skipNative && // already done if skipping
                    (!useNative || root.tagName)) { // split native when element scoping is needed
                queries = Selector._splitQueries(selector, root);
            }

            for (i = 0; (query = queries[i++]);) {
                result = fn(query[0], query[1], firstOnly);
                if (!firstOnly) { // coerce DOM Collection to Array
                    result = Y_Array(result, 0, true);
                }
                if (result) {
                    ret = ret.concat(result);
                }
            }

            if (queries.length > 1) { // remove dupes and sort by doc order
                ret = Selector._sort(Selector._deDupe(ret));
            }
        }

        Y.log('query: ' + selector + ' returning: ' + ret.length, 'info', 'Selector');
        return (firstOnly) ? (ret[0] || null) : ret;

    },

    // allows element scoped queries to begin with combinator
    // e.g. query('> p', document.body) === query('body > p')
    _splitQueries: function(selector, node) {
        var groups = selector.split(','),
            queries = [],
            prefix = '',
            i, len;

        if (node) {
            // enforce for element scoping
            if (node.tagName) {
                node.id = node.id || Y_guid();
                prefix = '[id="' + node.id + '"] ';
            }

            for (i = 0, len = groups.length; i < len; ++i) {
                selector =  prefix + groups[i];
                queries.push([selector, node]);
            }
        }

        return queries;
    },

    _nativeQuery: function(selector, root, one) {
        if (Y_UA.webkit && selector.indexOf(':checked') > -1 &&
                (Selector.pseudos && Selector.pseudos.checked)) { // webkit (chrome, safari) fails to find "selected"
            return Selector.query(selector, root, one, true); // redo with skipNative true to try brute query
        }
        try {
            //Y.log('trying native query with: ' + selector, 'info', 'selector-native');
            return root['querySelector' + (one ? '' : 'All')](selector);
        } catch(e) { // fallback to brute if available
            //Y.log('native query error; reverting to brute query with: ' + selector, 'info', 'selector-native');
            return Selector.query(selector, root, one, true); // redo with skipNative true
        }
    },

    filter: function(nodes, selector) {
        var ret = [],
            i, node;

        if (nodes && selector) {
            for (i = 0; (node = nodes[i++]);) {
                if (Selector.test(node, selector)) {
                    ret[ret.length] = node;
                }
            }
        } else {
            Y.log('invalid filter input (nodes: ' + nodes +
                    ', selector: ' + selector + ')', 'warn', 'Selector');
        }

        return ret;
    },

    test: function(node, selector, root) {
        var ret = false,
            groups = selector.split(','),
            useFrag = false,
            parent,
            item,
            items,
            frag,
            i, j, group;

        if (node && node.tagName) { // only test HTMLElements

            // we need a root if off-doc
            if (!root && !Y_DOM_inDoc(node)) {
                parent = node.parentNode;
                if (parent) {
                    root = parent;
                } else { // only use frag when no parent to query
                    frag = node[OWNER_DOCUMENT].createDocumentFragment();
                    frag.appendChild(node);
                    root = frag;
                    useFrag = true;
                }
            }
            root = root || node[OWNER_DOCUMENT];

            if (!node.id) {
                node.id = Y_guid();
            }
            for (i = 0; (group = groups[i++]);) { // TODO: off-dom test
                group += '[id="' + node.id + '"]';
                items = Selector.query(group, root);

                for (j = 0; item = items[j++];) {
                    if (item === node) {
                        ret = true;
                        break;
                    }
                }
                if (ret) {
                    break;
                }
            }

            if (useFrag) { // cleanup
                frag.removeChild(node);
            }
        }

        return ret;
    }

};

YAHOO.util.Selector = Selector;
/**
 * The selector module provides helper methods allowing CSS2 Selectors to be used with DOM elements.
 * @module dom
 * @submodule selector-css2
 * @for Selector
 */

/**
 * Provides helper methods for collecting and filtering DOM elements.
 */

var PARENT_NODE = 'parentNode',
    TAG_NAME = 'tagName',
    ATTRIBUTES = 'attributes',
    COMBINATOR = 'combinator',
    PSEUDOS = 'pseudos',

    SelectorCSS2 = {
        _reRegExpTokens: /([\^\$\?\[\]\*\+\-\.\(\)\|\\])/, // TODO: move?
        SORT_RESULTS: true,
        _children: function(node, tag) {
            var ret = node.children,
                i,
                children = [],
                childNodes,
                child;

            if (node.children && tag && node.children.tags) {
                children = node.children.tags(tag);
            } else if ((!ret && node[TAG_NAME]) || (ret && tag)) { // only HTMLElements have children
                childNodes = ret || node.childNodes;
                ret = [];
                for (i = 0; (child = childNodes[i++]);) {
                    if (child.tagName) {
                        if (!tag || tag === child.tagName) {
                            ret.push(child);
                        }
                    }
                }
            }

            return ret || [];
        },

        _re: {
            //attr: /(\[.*\])/g,
            attr: /(\[[^\]]*\])/g,
            //esc: /\\[:\[][\w\d\]]*/gi,
            esc: /\\[:\[\]\(\)#\.\'\>+~"]/gi,
            //pseudos: /:([\-\w]+(?:\(?:['"]?(.+)['"]?\))*)/i
            pseudos: /(\([^\)]*\))/g
        },

        /**
         * Mapping of shorthand tokens to corresponding attribute selector
         * @property shorthand
         * @type object
         */
        shorthand: {
            //'\\#([^\\s\\\\(\\[:]*)': '[id=$1]',
            '\\#(-?[_a-z]+[-\\w\\uE000]*)': '[id=$1]',
            //'\\#([^\\s\\\.:\\[\\]]*)': '[id=$1]',
            //'\\.([^\\s\\\\(\\[:]*)': '[className=$1]'
            '\\.(-?[_a-z]+[-\\w\\uE000]*)': '[className~=$1]'
        },

        /**
         * List of operators and corresponding boolean functions.
         * These functions are passed the attribute and the current node's value of the attribute.
         * @property operators
         * @type object
         */
        operators: {
            '': function(node, attr) { return !!node.getAttribute(attr); }, // Just test for existence of attribute
            //'': '.+',
            //'=': '^{val}$', // equality
            '~=': '(?:^|\\s+){val}(?:\\s+|$)', // space-delimited
            '|=': '^{val}(?:-|$)' // optional hyphen-delimited
        },

        pseudos: {
           'first-child': function(node) {
                return Selector._children(node[PARENT_NODE])[0] === node;
            }
        },

        _bruteQuery: function(selector, root, firstOnly) {
            var ret = [],
                nodes = [],
                tokens = Selector._tokenize(selector),
                token = tokens[tokens.length - 1],
                rootDoc = Y_getDoc(root),
                child,
                id,
                className,
                tagName;


            // if we have an initial ID, set to root when in document
            /*
            if (tokens[0] && rootDoc === root &&
                    (id = tokens[0].id) &&
                    rootDoc.getElementById(id)) {
                root = rootDoc.getElementById(id);
            }
            */

            if (token) {
                // prefilter nodes
                id = token.id;
                className = token.className;
                tagName = token.tagName || '*';

                if (root.getElementsByTagName) { // non-IE lacks DOM api on doc frags
                    // try ID first, unless no root.all && root not in document
                    // (root.all works off document, but not getElementById)
                    // TODO: move to allById?
                    if (id && (root.all || (root.nodeType === 9 || Y_DOM_inDoc(root)))) {
                        nodes = Y_DOM_allById(id, root);
                    // try className
                    } else if (className) {
                        nodes = root.getElementsByClassName(className);
                    } else { // default to tagName
                        nodes = root.getElementsByTagName(tagName);
                    }

                } else { // brute getElementsByTagName('*')
                    child = root.firstChild;
                    while (child) {
                        if (child.tagName) { // only collect HTMLElements
                            nodes.push(child);
                        }
                        child = child.nextSilbing || child.firstChild;
                    }
                }
                if (nodes.length) {
                    ret = Selector._filterNodes(nodes, tokens, firstOnly);
                }
            }

            return ret;
        },

        _filterNodes: function(nodes, tokens, firstOnly) {
            var i = 0,
                j,
                len = tokens.length,
                n = len - 1,
                result = [],
                node = nodes[0],
                tmpNode = node,
                getters = Selector.getters,
                operator,
                combinator,
                token,
                path,
                pass,
                //FUNCTION = 'function',
                value,
                tests,
                test;

            //do {
            for (i = 0; (tmpNode = node = nodes[i++]);) {
                n = len - 1;
                path = null;

                testLoop:
                while (tmpNode && tmpNode.tagName) {
                    token = tokens[n];
                    tests = token.tests;
                    j = tests.length;
                    if (j && !pass) {
                        while ((test = tests[--j])) {
                            operator = test[1];
                            if (getters[test[0]]) {
                                value = getters[test[0]](tmpNode, test[0]);
                            } else {
                                value = tmpNode[test[0]];
                                // use getAttribute for non-standard attributes
                                if (value === undefined && tmpNode.getAttribute) {
                                    value = tmpNode.getAttribute(test[0]);
                                }
                            }

                            if ((operator === '=' && value !== test[2]) ||  // fast path for equality
                                (typeof operator !== 'string' && // protect against String.test monkey-patch (Moo)
                                operator.test && !operator.test(value)) ||  // regex test
                                (!operator.test && // protect against RegExp as function (webkit)
                                        typeof operator === 'function' && !operator(tmpNode, test[0], test[2]))) { // function test

                                // skip non element nodes or non-matching tags
                                if ((tmpNode = tmpNode[path])) {
                                    while (tmpNode &&
                                        (!tmpNode.tagName ||
                                            (token.tagName && token.tagName !== tmpNode.tagName))
                                    ) {
                                        tmpNode = tmpNode[path];
                                    }
                                }
                                continue testLoop;
                            }
                        }
                    }

                    n--; // move to next token
                    // now that we've passed the test, move up the tree by combinator
                    if (!pass && (combinator = token.combinator)) {
                        path = combinator.axis;
                        tmpNode = tmpNode[path];

                        // skip non element nodes
                        while (tmpNode && !tmpNode.tagName) {
                            tmpNode = tmpNode[path];
                        }

                        if (combinator.direct) { // one pass only
                            path = null;
                        }

                    } else { // success if we made it this far
                        result.push(node);
                        if (firstOnly) {
                            return result;
                        }
                        break;
                    }
                }
            }// while (tmpNode = node = nodes[++i]);
            node = tmpNode = null;
            return result;
        },

        combinators: {
            ' ': {
                axis: 'parentNode'
            },

            '>': {
                axis: 'parentNode',
                direct: true
            },


            '+': {
                axis: 'previousSibling',
                direct: true
            }
        },

        _parsers: [
            {
                name: ATTRIBUTES,
                //re: /^\[(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\]]*?)['"]?\]/i,
                re: /^\uE003(-?[a-z]+[\w\-]*)+([~\|\^\$\*!=]=?)?['"]?([^\uE004'"]*)['"]?\uE004/i,
                fn: function(match, token) {
                    var operator = match[2] || '',
                        operators = Selector.operators,
                        escVal = (match[3]) ? match[3].replace(/\\/g, '') : '',
                        test;

                    // add prefiltering for ID and CLASS
                    if ((match[1] === 'id' && operator === '=') ||
                            (match[1] === 'className' &&
                            Y_DOCUMENT_ELEMENT.getElementsByClassName &&
                            (operator === '~=' || operator === '='))) {
                        token.prefilter = match[1];


                        match[3] = escVal;

                        // escape all but ID for prefilter, which may run through QSA (via Dom.allById)
                        token[match[1]] = (match[1] === 'id') ? match[3] : escVal;

                    }

                    // add tests
                    if (operator in operators) {
                        test = operators[operator];
                        if (typeof test === 'string') {
                            match[3] = escVal.replace(Selector._reRegExpTokens, '\\$1');
                            test = new RegExp(test.replace('{val}', match[3]));
                        }
                        match[2] = test;
                    }
                    if (!token.last || token.prefilter !== match[1]) {
                        return match.slice(1);
                    }
                }

            },
            {
                name: TAG_NAME,
                re: /^((?:-?[_a-z]+[\w-]*)|\*)/i,
                fn: function(match, token) {
                    var tag = match[1].toUpperCase();
                    token.tagName = tag;

                    if (tag !== '*' && (!token.last || token.prefilter)) {
                        return [TAG_NAME, '=', tag];
                    }
                    if (!token.prefilter) {
                        token.prefilter = 'tagName';
                    }
                }
            },
            {
                name: COMBINATOR,
                re: /^\s*([>+~]|\s)\s*/,
                fn: function(match, token) {
                }
            },
            {
                name: PSEUDOS,
                re: /^:([\-\w]+)(?:\uE005['"]?([^\uE005]*)['"]?\uE006)*/i,
                fn: function(match, token) {
                    var test = Selector[PSEUDOS][match[1]];
                    if (test) { // reorder match array and unescape special chars for tests
                        if (match[2]) {
                            match[2] = match[2].replace(/\\/g, '');
                        }
                        return [match[2], test];
                    } else { // selector token not supported (possibly missing CSS3 module)
                        return false;
                    }
                }
            }
            ],

        _getToken: function(token) {
            return {
                tagName: null,
                id: null,
                className: null,
                attributes: {},
                combinator: null,
                tests: []
            };
        },

        /**
            Break selector into token units per simple selector.
            Combinator is attached to the previous token.
         */
        _tokenize: function(selector) {
            selector = selector || '';
            selector = Selector._replaceShorthand(Y_Lang.trim(selector));
            var token = Selector._getToken(),     // one token per simple selector (left selector holds combinator)
                query = selector, // original query for debug report
                tokens = [],    // array of tokens
                found = false,  // whether or not any matches were found this pass
                match,         // the regex match
                test,
                i, parser;

            /*
                Search for selector patterns, store, and strip them from the selector string
                until no patterns match (invalid selector) or we run out of chars.

                Multiple attributes and pseudos are allowed, in any order.
                for example:
                    'form:first-child[type=button]:not(button)[lang|=en]'
            */

            outer:
            do {
                found = false; // reset after full pass

                for (i = 0; (parser = Selector._parsers[i++]);) {
                    if ( (match = parser.re.exec(selector)) ) { // note assignment
                        if (parser.name !== COMBINATOR ) {
                            token.selector = selector;
                        }
                        selector = selector.replace(match[0], ''); // strip current match from selector
                        if (!selector.length) {
                            token.last = true;
                        }

                        if (Selector._attrFilters[match[1]]) { // convert class to className, etc.
                            match[1] = Selector._attrFilters[match[1]];
                        }

                        test = parser.fn(match, token);
                        if (test === false) { // selector not supported
                            found = false;
                            break outer;
                        } else if (test) {
                            token.tests.push(test);
                        }

                        if (!selector.length || parser.name === COMBINATOR) {
                            tokens.push(token);
                            token = Selector._getToken(token);
                            if (parser.name === COMBINATOR) {
                                token.combinator = Selector.combinators[match[1]];
                            }
                        }
                        found = true;


                    }
                }
            } while (found && selector.length);

            if (!found || selector.length) { // not fully parsed
                Y.log('query: ' + query + ' contains unsupported token in: ' + selector, 'warn', 'Selector');
                tokens = [];
            }
            return tokens;
        },

        _replaceShorthand: function(selector) {
            var shorthand = Selector.shorthand,
                esc = selector.match(Selector._re.esc), // pull escaped colon, brackets, etc.
                attrs,
                pseudos,
                re, i, len;

            if (esc) {
                selector = selector.replace(Selector._re.esc, '\uE000');
            }

            attrs = selector.match(Selector._re.attr);
            pseudos = selector.match(Selector._re.pseudos);

            if (attrs) {
                selector = selector.replace(Selector._re.attr, '\uE001');
            }

            if (pseudos) {
                selector = selector.replace(Selector._re.pseudos, '\uE002');
            }


            for (re in shorthand) {
                if (shorthand.hasOwnProperty(re)) {
                    selector = selector.replace(new RegExp(re, 'gi'), shorthand[re]);
                }
            }

            if (attrs) {
                for (i = 0, len = attrs.length; i < len; ++i) {
                    selector = selector.replace(/\uE001/, attrs[i]);
                }
            }

            if (pseudos) {
                for (i = 0, len = pseudos.length; i < len; ++i) {
                    selector = selector.replace(/\uE002/, pseudos[i]);
                }
            }

            selector = selector.replace(/\[/g, '\uE003');
            selector = selector.replace(/\]/g, '\uE004');

            selector = selector.replace(/\(/g, '\uE005');
            selector = selector.replace(/\)/g, '\uE006');

            if (esc) {
                for (i = 0, len = esc.length; i < len; ++i) {
                    selector = selector.replace('\uE000', esc[i]);
                }
            }

            return selector;
        },

        _attrFilters: {
            'class': 'className',
            'for': 'htmlFor'
        },

        getters: {
            href: function(node, attr) {
                return Y_DOM.getAttribute(node, attr);
            }
        }
    };

Y_mix(Selector, SelectorCSS2, true);
Selector.getters.src = Selector.getters.rel = Selector.getters.href;

// IE wants class with native queries
if (Selector.useNative && Y_DOC.querySelector) {
    Selector.shorthand['\\.([^\\s\\\\(\\[:]*)'] = '[class~=$1]';
}

/**
 * The selector css3 module provides support for css3 selectors.
 * @module dom
 * @submodule selector-css3
 * @for Selector
 */

/*
    an+b = get every _a_th node starting at the _b_th
    0n+b = no repeat ("0" and "n" may both be omitted (together) , e.g. "0n+1" or "1", not "0+1"), return only the _b_th element
    1n+b =  get every element starting from b ("1" may may be omitted, e.g. "1n+0" or "n+0" or "n")
    an+0 = get every _a_th element, "0" may be omitted
*/

Selector._reNth = /^(?:([\-]?\d*)(n){1}|(odd|even)$)*([\-+]?\d*)$/;

Selector._getNth = function(node, expr, tag, reverse) {
    Selector._reNth.test(expr);
    var a = parseInt(RegExp.$1, 10), // include every _a_ elements (zero means no repeat, just first _a_)
        n = RegExp.$2, // "n"
        oddeven = RegExp.$3, // "odd" or "even"
        b = parseInt(RegExp.$4, 10) || 0, // start scan from element _b_
        result = [],
        siblings = Selector._children(node.parentNode, tag),
        op;

    if (oddeven) {
        a = 2; // always every other
        op = '+';
        n = 'n';
        b = (oddeven === 'odd') ? 1 : 0;
    } else if ( isNaN(a) ) {
        a = (n) ? 1 : 0; // start from the first or no repeat
    }

    if (a === 0) { // just the first
        if (reverse) {
            b = siblings.length - b + 1;
        }

        if (siblings[b - 1] === node) {
            return true;
        } else {
            return false;
        }

    } else if (a < 0) {
        reverse = !!reverse;
        a = Math.abs(a);
    }

    if (!reverse) {
        for (var i = b - 1, len = siblings.length; i < len; i += a) {
            if ( i >= 0 && siblings[i] === node ) {
                return true;
            }
        }
    } else {
        for (var i = siblings.length - b, len = siblings.length; i >= 0; i -= a) {
            if ( i < len && siblings[i] === node ) {
                return true;
            }
        }
    }
    return false;
};

Y_mix(Selector.pseudos, {
    'root': function(node) {
        return node === node.ownerDocument.documentElement;
    },

    'nth-child': function(node, expr) {
        return Selector._getNth(node, expr);
    },

    'nth-last-child': function(node, expr) {
        return Selector._getNth(node, expr, null, true);
    },

    'nth-of-type': function(node, expr) {
        return Selector._getNth(node, expr, node.tagName);
    },

    'nth-last-of-type': function(node, expr) {
        return Selector._getNth(node, expr, node.tagName, true);
    },

    'last-child': function(node) {
        var children = Selector._children(node.parentNode);
        return children[children.length - 1] === node;
    },

    'first-of-type': function(node) {
        return Selector._children(node.parentNode, node.tagName)[0] === node;
    },

    'last-of-type': function(node) {
        var children = Selector._children(node.parentNode, node.tagName);
        return children[children.length - 1] === node;
    },

    'only-child': function(node) {
        var children = Selector._children(node.parentNode);
        return children.length === 1 && children[0] === node;
    },

    'only-of-type': function(node) {
        var children = Selector._children(node.parentNode, node.tagName);
        return children.length === 1 && children[0] === node;
    },

    'empty': function(node) {
        return node.childNodes.length === 0;
    },

    'not': function(node, expr) {
        return !Selector.test(node, expr);
    },

    'contains': function(node, expr) {
        var text = node.innerText || node.textContent || '';
        return text.indexOf(expr) > -1;
    },

    'checked': function(node) {
        return (node.checked === true || node.selected === true);
    },

    enabled: function(node) {
        return (node.disabled !== undefined && !node.disabled);
    },

    disabled: function(node) {
        return (node.disabled);
    }
});

Y_mix(Selector.operators, {
    '^=': '^{val}', // Match starts with value
    '!=': function(node, attr, val) { return node[attr] !== val; }, // Match starts with value
    '$=': '{val}$', // Match ends with value
    '*=': '{val}' // Match contains value as substring
});

Selector.combinators['~'] = {
    axis: 'previousSibling'
};
YAHOO.register("selector", YAHOO.util.Selector, {version: "2.9.0", build: "2800"});



/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

var Dom = YAHOO.util.Dom;

/**
 * The ColumnSet class defines and manages a DataTable's Columns,
 * including nested hierarchies and access to individual Column instances.
 *
 * @namespace YAHOO.widget
 * @class ColumnSet
 * @uses YAHOO.util.EventProvider
 * @constructor
 * @param aDefinitions {Object[]} Array of object literals that define cells in
 * the THEAD.
 */
YAHOO.widget.ColumnSet = function(aDefinitions) {
    this._sId = Dom.generateId(null, "yui-cs"); // "yui-cs" + YAHOO.widget.ColumnSet._nCount;

    // First clone the defs
    aDefinitions = YAHOO.widget.DataTable._cloneObject(aDefinitions);
    this._init(aDefinitions);

    YAHOO.widget.ColumnSet._nCount++;
    YAHOO.log("ColumnSet initialized", "info", this.toString());
};

/////////////////////////////////////////////////////////////////////////////
//
// Private member variables
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Internal class variable to index multiple ColumnSet instances.
 *
 * @property ColumnSet._nCount
 * @type Number
 * @private
 * @static
 */
YAHOO.widget.ColumnSet._nCount = 0;

YAHOO.widget.ColumnSet.prototype = {
    /**
     * Unique instance name.
     *
     * @property _sId
     * @type String
     * @private
     */
    _sId : null,

    /**
     * Array of object literal Column definitions passed to the constructor.
     *
     * @property _aDefinitions
     * @type Object[]
     * @private
     */
    _aDefinitions : null,

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public member variables
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Top-down tree representation of Column hierarchy.
     *
     * @property tree
     * @type YAHOO.widget.Column[]
     */
    tree : null,

    /**
     * Flattened representation of all Columns.
     *
     * @property flat
     * @type YAHOO.widget.Column[]
     * @default []
     */
    flat : null,

    /**
     * Array of Columns that map one-to-one to a table column.
     *
     * @property keys
     * @type YAHOO.widget.Column[]
     * @default []
     */
    keys : null,

    /**
     * ID index of nested parent hierarchies for HEADERS accessibility attribute.
     *
     * @property headers
     * @type String[]
     * @default []
     */
    headers : null,

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Initializes ColumnSet instance with data from Column definitions.
     *
     * @method _init
     * @param aDefinitions {Object[]} Array of object literals that define cells in
     * the THEAD .
     * @private
     */

    _init : function(aDefinitions) {        
        // DOM tree representation of all Columns
        var tree = [];
        // Flat representation of all Columns
        var flat = [];
        // Flat representation of only Columns that are meant to display data
        var keys = [];
        // Array of HEADERS attribute values for all keys in the "keys" array
        var headers = [];

        // Tracks current node list depth being tracked
        var nodeDepth = -1;

        // Internal recursive function to define Column instances
        var parseColumns = function(nodeList, parent) {
            // One level down
            nodeDepth++;

            // Create corresponding tree node if not already there for this depth
            if(!tree[nodeDepth]) {
                tree[nodeDepth] = [];
            }


            // Parse each node at this depth for attributes and any children
            for(var j=0; j<nodeList.length; j++) {
                var currentNode = nodeList[j];

                // Instantiate a new Column for each node
                var oColumn = new YAHOO.widget.Column(currentNode);
                
                // Cross-reference Column ID back to the original object literal definition
                currentNode.yuiColumnId = oColumn._sId;
                
                // Add the new Column to the flat list
                flat.push(oColumn);

                // Assign its parent as an attribute, if applicable
                if(parent) {
                    oColumn._oParent = parent;
                }

                // The Column has descendants
                if(YAHOO.lang.isArray(currentNode.children)) {
                    oColumn.children = currentNode.children;

                    // Determine COLSPAN value for this Column
                    var terminalChildNodes = 0;
                    var countTerminalChildNodes = function(ancestor) {
                        var descendants = ancestor.children;
                        // Drill down each branch and count terminal nodes
                        for(var k=0; k<descendants.length; k++) {
                            // Keep drilling down
                            if(YAHOO.lang.isArray(descendants[k].children)) {
                                countTerminalChildNodes(descendants[k]);
                            }
                            // Reached branch terminus
                            else {
                                terminalChildNodes++;
                            }
                        }
                    };
                    countTerminalChildNodes(currentNode);
                    oColumn._nColspan = terminalChildNodes;

                    // Cascade certain properties to children if not defined on their own
                    var currentChildren = currentNode.children;
                    for(var k=0; k<currentChildren.length; k++) {
                        var child = currentChildren[k];
                        if(oColumn.className && (child.className === undefined)) {
                            child.className = oColumn.className;
                        }
                        if(oColumn.editor && (child.editor === undefined)) {
                            child.editor = oColumn.editor;
                        }
                        //TODO: Deprecated
                        if(oColumn.editorOptions && (child.editorOptions === undefined)) {
                            child.editorOptions = oColumn.editorOptions;
                        }
                        if(oColumn.formatter && (child.formatter === undefined)) {
                            child.formatter = oColumn.formatter;
                        }
                        if(oColumn.resizeable && (child.resizeable === undefined)) {
                            child.resizeable = oColumn.resizeable;
                        }
                        if(oColumn.sortable && (child.sortable === undefined)) {
                            child.sortable = oColumn.sortable;
                        }
                        if(oColumn.hidden) {
                            child.hidden = true;
                        }
                        if(oColumn.width && (child.width === undefined)) {
                            child.width = oColumn.width;
                        }
                        if(oColumn.minWidth && (child.minWidth === undefined)) {
                            child.minWidth = oColumn.minWidth;
                        }
                        if(oColumn.maxAutoWidth && (child.maxAutoWidth === undefined)) {
                            child.maxAutoWidth = oColumn.maxAutoWidth;
                        }
                        // Backward compatibility
                        if(oColumn.type && (child.type === undefined)) {
                            child.type = oColumn.type;
                        }
                        if(oColumn.type && !oColumn.formatter) {
                            YAHOO.log("The property type has been" +
                            " deprecated in favor of formatter", "warn", oColumn.toString());
                            oColumn.formatter = oColumn.type;
                        }
                        if(oColumn.text && !YAHOO.lang.isValue(oColumn.label)) {
                            YAHOO.log("The property text has been" +
                            " deprecated in favor of label", "warn", oColumn.toString());
                            oColumn.label = oColumn.text;
                        }
                        if(oColumn.parser) {
                            YAHOO.log("The property parser is no longer supported",
                            "warn", this.toString());
                        }
                        if(oColumn.sortOptions && ((oColumn.sortOptions.ascFunction) ||
                                (oColumn.sortOptions.descFunction))) {
                            YAHOO.log("The properties sortOptions.ascFunction and " +
                            " sortOptions.descFunction have been deprecated in favor " +
                            " of sortOptions.sortFunction", "warn", oColumn.toString());
                        }
                    }

                    // The children themselves must also be parsed for Column instances
                    if(!tree[nodeDepth+1]) {
                        tree[nodeDepth+1] = [];
                    }
                    parseColumns(currentChildren, oColumn);
                }
                // This Column does not have any children
                else {
                    oColumn._nKeyIndex = keys.length;
                    oColumn._nColspan = 1;
                    keys.push(oColumn);
                }

                // Add the Column to the top-down tree
                tree[nodeDepth].push(oColumn);
            }
            nodeDepth--;
        };

        // Parse out Column instances from the array of object literals
        if(YAHOO.lang.isArray(aDefinitions)) {
            parseColumns(aDefinitions);

            // Store the array
            this._aDefinitions = aDefinitions;
        }
        else {
            YAHOO.log("Could not initialize ColumnSet due to invalid definitions","error");
            return null;
        }

        var i;

        // Determine ROWSPAN value for each Column in the tree
        var parseTreeForRowspan = function(tree) {
            var maxRowDepth = 1;
            var currentRow;
            var currentColumn;

            // Calculate the max depth of descendants for this row
            var countMaxRowDepth = function(row, tmpRowDepth) {
                tmpRowDepth = tmpRowDepth || 1;

                for(var n=0; n<row.length; n++) {
                    var col = row[n];
                    // Column has children, so keep counting
                    if(YAHOO.lang.isArray(col.children)) {
                        tmpRowDepth++;
                        countMaxRowDepth(col.children, tmpRowDepth);
                        tmpRowDepth--;
                    }
                    // No children, is it the max depth?
                    else {
                        if(tmpRowDepth > maxRowDepth) {
                            maxRowDepth = tmpRowDepth;
                        }
                    }

                }
            };

            // Count max row depth for each row
            for(var m=0; m<tree.length; m++) {
                currentRow = tree[m];
                countMaxRowDepth(currentRow);

                // Assign the right ROWSPAN values to each Column in the row
                for(var p=0; p<currentRow.length; p++) {
                    currentColumn = currentRow[p];
                    if(!YAHOO.lang.isArray(currentColumn.children)) {
                        currentColumn._nRowspan = maxRowDepth;
                    }
                    else {
                        currentColumn._nRowspan = 1;
                    }
                }

                // Reset counter for next row
                maxRowDepth = 1;
            }
        };
        parseTreeForRowspan(tree);

        // Store tree index values
        for(i=0; i<tree[0].length; i++) {
            tree[0][i]._nTreeIndex = i;
        }

        // Store header relationships in an array for HEADERS attribute
        var recurseAncestorsForHeaders = function(i, oColumn) {
            headers[i].push(oColumn.getSanitizedKey());
            if(oColumn._oParent) {
                recurseAncestorsForHeaders(i, oColumn._oParent);
            }
        };
        for(i=0; i<keys.length; i++) {
            headers[i] = [];
            recurseAncestorsForHeaders(i, keys[i]);
            headers[i] = headers[i].reverse();
        }

        // Save to the ColumnSet instance
        this.tree = tree;
        this.flat = flat;
        this.keys = keys;
        this.headers = headers;
    },

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Returns unique name of the ColumnSet instance.
     *
     * @method getId
     * @return {String} Unique name of the ColumnSet instance.
     */

    getId : function() {
        return this._sId;
    },

    /**
     * ColumnSet instance name, for logging.
     *
     * @method toString
     * @return {String} Unique name of the ColumnSet instance.
     */

    toString : function() {
        return "ColumnSet instance " + this._sId;
    },

    /**
     * Public accessor to the definitions array.
     *
     * @method getDefinitions
     * @return {Object[]} Array of object literal Column definitions.
     */

    getDefinitions : function() {
        var aDefinitions = this._aDefinitions;
        
        // Internal recursive function to define Column instances
        var parseColumns = function(nodeList, oSelf) {
            // Parse each node at this depth for attributes and any children
            for(var j=0; j<nodeList.length; j++) {
                var currentNode = nodeList[j];
                
                // Get the Column for each node
                var oColumn = oSelf.getColumnById(currentNode.yuiColumnId);
                
                if(oColumn) {    
                    // Update the current values
                    var oDefinition = oColumn.getDefinition();
                    for(var name in oDefinition) {
                        if(YAHOO.lang.hasOwnProperty(oDefinition, name)) {
                            currentNode[name] = oDefinition[name];
                        }
                    }
                }
                            
                // The Column has descendants
                if(YAHOO.lang.isArray(currentNode.children)) {
                    // The children themselves must also be parsed for Column instances
                    parseColumns(currentNode.children, oSelf);
                }
            }
        };

        parseColumns(aDefinitions, this);
        this._aDefinitions = aDefinitions;
        return aDefinitions;
    },

    /**
     * Returns Column instance with given ID.
     *
     * @method getColumnById
     * @param column {String} Column ID.
     * @return {YAHOO.widget.Column} Column instance.
     */

    getColumnById : function(column) {
        if(YAHOO.lang.isString(column)) {
            var allColumns = this.flat;
            for(var i=allColumns.length-1; i>-1; i--) {
                if(allColumns[i]._sId === column) {
                    return allColumns[i];
                }
            }
        }
        return null;
    },

    /**
     * Returns Column instance with given key or ColumnSet key index.
     *
     * @method getColumn
     * @param column {String | Number} Column key or ColumnSet key index.
     * @return {YAHOO.widget.Column} Column instance.
     */

    getColumn : function(column) {
        if(YAHOO.lang.isNumber(column) && this.keys[column]) {
            return this.keys[column];
        }
        else if(YAHOO.lang.isString(column)) {
            var allColumns = this.flat;
            var aColumns = [];
            for(var i=0; i<allColumns.length; i++) {
                if(allColumns[i].key === column) {
                    aColumns.push(allColumns[i]);
                }
            }
            if(aColumns.length === 1) {
                return aColumns[0];
            }
            else if(aColumns.length > 1) {
                return aColumns;
            }
        }
        return null;
    },

    /**
     * Public accessor returns array of given Column's desendants (if any), including itself.
     *
     * @method getDescendants
     * @parem {YAHOO.widget.Column} Column instance.
     * @return {Array} Array including the Column itself and all descendants (if any).
     */
    getDescendants : function(oColumn) {
        var oSelf = this;
        var allDescendants = [];
        var i;

        // Recursive function to loop thru all children
        var parse = function(oParent) {
            allDescendants.push(oParent);
            // This Column has children
            if(oParent.children) {
                for(i=0; i<oParent.children.length; i++) {
                    parse(oSelf.getColumn(oParent.children[i].key));
                }
            }
        };
        parse(oColumn);

        return allDescendants;
    }
};

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * The Column class defines and manages attributes of DataTable Columns
 *
 * @namespace YAHOO.widget
 * @class Column
 * @constructor
 * @param oConfigs {Object} Object literal of definitions.
 */
YAHOO.widget.Column = function(oConfigs) {
    this._sId = Dom.generateId(null, "yui-col"); // "yui-col" + YAHOO.widget.Column._nCount;
    
    // Object literal defines Column attributes
    if(oConfigs && YAHOO.lang.isObject(oConfigs)) {
        for(var sConfig in oConfigs) {
            if(sConfig) {
                this[sConfig] = oConfigs[sConfig];
            }
        }
    }

    // Assign a key if not found
    if(!YAHOO.lang.isValue(this.key)) {
        this.key = Dom.generateId(null, "yui-dt-col"); //"yui-dt-col" + YAHOO.widget.Column._nCount;
    }
    
    // Assign a field if not found, defaults to key
    if(!YAHOO.lang.isValue(this.field)) {
        this.field = this.key;
    }

    // Increment counter
    YAHOO.widget.Column._nCount++;

    // Backward compatibility
    if(this.width && !YAHOO.lang.isNumber(this.width)) {
        this.width = null;
        YAHOO.log("The Column property width must be a number", "warn", this.toString());
    }
    if(this.editor && YAHOO.lang.isString(this.editor)) {
        this.editor = new YAHOO.widget.CellEditor(this.editor, this.editorOptions);
        YAHOO.log("The Column property editor must be an instance of YAHOO.widget.CellEditor", "warn", this.toString());
    }
};

/////////////////////////////////////////////////////////////////////////////
//
// Private member variables
//
/////////////////////////////////////////////////////////////////////////////

YAHOO.lang.augmentObject(YAHOO.widget.Column, {
    /**
     * Internal class variable to index multiple Column instances.
     *
     * @property Column._nCount
     * @type Number
     * @private
     * @static
     */
    _nCount : 0,

    formatCheckbox : function(elCell, oRecord, oColumn, oData) {
        YAHOO.log("The method YAHOO.widget.Column.formatCheckbox() has been" +
        " deprecated in favor of YAHOO.widget.DataTable.formatCheckbox()", "warn",
        "YAHOO.widget.Column.formatCheckbox");
        YAHOO.widget.DataTable.formatCheckbox(elCell, oRecord, oColumn, oData);
    },

    formatCurrency : function(elCell, oRecord, oColumn, oData) {
        YAHOO.log("The method YAHOO.widget.Column.formatCurrency() has been" +
        " deprecated in favor of YAHOO.widget.DataTable.formatCurrency()", "warn",
        "YAHOO.widget.Column.formatCurrency");
        YAHOO.widget.DataTable.formatCurrency(elCell, oRecord, oColumn, oData);
    },

    formatDate : function(elCell, oRecord, oColumn, oData) {
        YAHOO.log("The method YAHOO.widget.Column.formatDate() has been" +
        " deprecated in favor of YAHOO.widget.DataTable.formatDate()", "warn",
        "YAHOO.widget.Column.formatDate");
        YAHOO.widget.DataTable.formatDate(elCell, oRecord, oColumn, oData);
    },

    formatEmail : function(elCell, oRecord, oColumn, oData) {
        YAHOO.log("The method YAHOO.widget.Column.formatEmail() has been" +
        " deprecated in favor of YAHOO.widget.DataTable.formatEmail()", "warn",
        "YAHOO.widget.Column.formatEmail");
        YAHOO.widget.DataTable.formatEmail(elCell, oRecord, oColumn, oData);
    },

    formatLink : function(elCell, oRecord, oColumn, oData) {
        YAHOO.log("The method YAHOO.widget.Column.formatLink() has been" +
        " deprecated in favor of YAHOO.widget.DataTable.formatLink()", "warn",
        "YAHOO.widget.Column.formatLink");
        YAHOO.widget.DataTable.formatLink(elCell, oRecord, oColumn, oData);
    },

    formatNumber : function(elCell, oRecord, oColumn, oData) {
        YAHOO.log("The method YAHOO.widget.Column.formatNumber() has been" +
        " deprecated in favor of YAHOO.widget.DataTable.formatNumber()", "warn",
        "YAHOO.widget.Column.formatNumber");
        YAHOO.widget.DataTable.formatNumber(elCell, oRecord, oColumn, oData);
    },

    formatSelect : function(elCell, oRecord, oColumn, oData) {
        YAHOO.log("The method YAHOO.widget.Column.formatSelect() has been" +
        " deprecated in favor of YAHOO.widget.DataTable.formatDropdown()", "warn",
        "YAHOO.widget.Column.formatSelect");
        YAHOO.widget.DataTable.formatDropdown(elCell, oRecord, oColumn, oData);
    }
});

YAHOO.widget.Column.prototype = {
    /**
     * Unique String identifier assigned at instantiation.
     *
     * @property _sId
     * @type String
     * @private
     */
    _sId : null,

    /**
     * Reference to Column's current position index within its ColumnSet's keys
     * array, if applicable. This property only applies to non-nested and bottom-
     * level child Columns.
     *
     * @property _nKeyIndex
     * @type Number
     * @private
     */
    _nKeyIndex : null,

    /**
     * Reference to Column's current position index within its ColumnSet's tree
     * array, if applicable. This property only applies to non-nested and top-
     * level parent Columns.
     *
     * @property _nTreeIndex
     * @type Number
     * @private
     */
    _nTreeIndex : null,

    /**
     * Number of table cells the Column spans.
     *
     * @property _nColspan
     * @type Number
     * @private
     */
    _nColspan : 1,

    /**
     * Number of table rows the Column spans.
     *
     * @property _nRowspan
     * @type Number
     * @private
     */
    _nRowspan : 1,

    /**
     * Column's parent Column instance, or null.
     *
     * @property _oParent
     * @type YAHOO.widget.Column
     * @private
     */
    _oParent : null,

    /**
     * The DOM reference to the associated TH element.
     *
     * @property _elTh
     * @type HTMLElement
     * @private
     */
    _elTh : null,

    /**
     * The DOM reference to the associated TH element's liner DIV element.
     *
     * @property _elThLiner
     * @type HTMLElement
     * @private
     */
    _elThLiner : null,

    /**
     * The DOM reference to the associated TH element's label SPAN element.
     *
     * @property _elThLabel
     * @type HTMLElement
     * @private
     */
    _elThLabel : null,

    /**
     * The DOM reference to the associated resizerelement (if any).
     *
     * @property _elResizer
     * @type HTMLElement
     * @private
     */
    _elResizer : null,

    /**
     * Internal width tracker.
     *
     * @property _nWidth
     * @type Number
     * @private
     */
    _nWidth : null,

    /**
     * For unreg() purposes, a reference to the Column's DragDrop instance.
     *
     * @property _dd
     * @type YAHOO.util.DragDrop
     * @private
     */
    _dd : null,

    /**
     * For unreg() purposes, a reference to the Column resizer's DragDrop instance.
     *
     * @property _ddResizer
     * @type YAHOO.util.DragDrop
     * @private
     */
    _ddResizer : null,

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public member variables
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Unique name, required. If "label" property is not provided, the "key"
     * value will be treated as markup and inserted into the DOM as innerHTML.
     *
     * @property key
     * @type String|HTML
     */
    key : null,

    /**
     * Associated database field, or null.
     *
     * @property field
     * @type String
     */
    field : null,

    /**
     * Value displayed as Column header in the TH element. String value is
     * treated as markup and inserted into the DOM as innerHTML.
     *
     * @property label
     * @type HTML
     */
    label : null,

    /**
     * Column head cell ABBR for accessibility.
     *
     * @property abbr
     * @type String
     */
    abbr : null,

    /**
     * Array of object literals that define children (nested headers) of a Column.
     *
     * @property children
     * @type Object[]
     */
    children : null,

    /**
     * Column width (in pixels).
     *
     * @property width
     * @type Number
     */
    width : null,

    /**
     * Minimum Column width (in pixels).
     *
     * @property minWidth
     * @type Number
     * @default null
     */
    minWidth : null,

    /**
     * When a width is not defined for a Column, maxAutoWidth defines an upper
     * limit that the Column should be auto-sized to. If resizeable is enabled, 
     * users may still resize to a greater width. Most useful for Columns intended
     * to hold long unbroken, unwrapped Strings, such as URLs, to prevent very
     * wide Columns from disrupting visual readability by inducing truncation.
     *
     * @property maxAutoWidth
     * @type Number
     * @default null
     */
    maxAutoWidth : null,

    /**
     * True if Column is in hidden state.
     *
     * @property hidden
     * @type Boolean
     * @default false     
     */
    hidden : false,

    /**
     * True if Column is in selected state.
     *
     * @property selected
     * @type Boolean
     * @default false     
     */
    selected : false,

    /**
     * Custom CSS class or array of classes to be applied to every cell in the Column.
     *
     * @property className
     * @type String || String[]
     */
    className : null,

    /**
     * Cell formatter function, or a shortcut pointer to a function in the
     * DataTable.Formatter object. The function, called from the DataTable's
     * formatCell method, renders markup into the cell liner
     * element and accepts the following arguments:
     * <dl>
     *    <dt>elLiner</dt>
     *    <dd>The element to write innerHTML to.</dd>
     *    <dt>oRecord</dt>
     *    <dd>The associated Record for the row.</dd>
     *    <dt>oColumn</dt>
     *    <dd>The Column instance for the cell.</dd>
     *    <dt>oData</dt>
     *    <dd>The data value for the cell.</dd>
     * </dl>
     *
     * @property formatter
     * @type String || HTMLFunction
     */
    formatter : null,
    
    /**
     * Config passed to YAHOO.util.Number.format() by the 'currency' Column formatter.
     *
     * @property currencyOptions
     * @type Object
     * @default null
     */
    currencyOptions : null,

    /**
     * Config passed to YAHOO.util.Date.format() by the 'date' Column formatter.
     *
     * @property dateOptions
     * @type Object
     * @default null
     */
    dateOptions : null,

    /**
     * Array of dropdown values for formatter:"dropdown" cases. Can either be a
     * simple array (e.g., ["Alabama","Alaska","Arizona","Arkansas"]) or a an
     * array of objects (e.g., [{label:"Alabama", value:"AL"},
     * {label:"Alaska", value:"AK"}, {label:"Arizona", value:"AZ"},
     * {label:"Arkansas", value:"AR"}]). String values are treated as markup and
     * inserted into the DOM as innerHTML.
     *
     * @property dropdownOptions
     * @type HTML[] | Object[]
     */
    dropdownOptions : null,
     
    /**
     * A CellEditor instance, otherwise Column is not editable.     
     *
     * @property editor
     * @type YAHOO.widget.CellEditor
     */
    editor : null,

    /**
     * True if Column is resizeable, false otherwise. The Drag & Drop Utility is
     * required to enable this feature. Only bottom-level and non-nested Columns are
     * resizeble. 
     *
     * @property resizeable
     * @type Boolean
     * @default false
     */
    resizeable : false,

    /**
     * True if Column is sortable, false otherwise.
     *
     * @property sortable
     * @type Boolean
     * @default false
     */
    sortable : false,

    /**
     * @property sortOptions.defaultOrder
     * @deprecated Use sortOptions.defaultDir.
     */
    /**
     * Default sort direction for Column: YAHOO.widget.DataTable.CLASS_ASC or YAHOO.widget.DataTable.CLASS_DESC.
     *
     * @property sortOptions.defaultDir
     * @type String
     * @default null
     */
    /**
     * Custom field to sort on.
     *
     * @property sortOptions.field
     * @type String
     * @default null
     */
    /**
     * Custom sort handler. Signature: sortFunction(a, b, desc, field) where field is the sortOptions.field value
     *
     * @property sortOptions.sortFunction
     * @type Function
     * @default null
     */
    sortOptions : null,















    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Returns unique ID string.
     *
     * @method getId
     * @return {String} Unique ID string.
     */
    getId : function() {
        return this._sId;
    },

    /**
     * Column instance name, for logging.
     *
     * @method toString
     * @return {String} Column's unique name.
     */
    toString : function() {
        return "Column instance " + this._sId;
    },

    /**
     * Returns object literal definition.
     *
     * @method getDefinition
     * @return {Object} Object literal definition.
     */
    getDefinition : function() {
        var oDefinition = {};
        
        // Update the definition
        oDefinition.abbr = this.abbr;
        oDefinition.className = this.className;
        oDefinition.editor = this.editor;
        oDefinition.editorOptions = this.editorOptions; //TODO: deprecated
        oDefinition.field = this.field;
        oDefinition.formatter = this.formatter;
        oDefinition.hidden = this.hidden;
        oDefinition.key = this.key;
        oDefinition.label = this.label;
        oDefinition.minWidth = this.minWidth;
        oDefinition.maxAutoWidth = this.maxAutoWidth;
        oDefinition.resizeable = this.resizeable;
        oDefinition.selected = this.selected;
        oDefinition.sortable = this.sortable;
        oDefinition.sortOptions = this.sortOptions;
        oDefinition.width = this.width;
        
        // Bug 2529147
        oDefinition._calculatedWidth = this._calculatedWidth;

        return oDefinition;
    },

    /**
     * Returns unique Column key.
     *
     * @method getKey
     * @return {String} Column key.
     */
    getKey : function() {
        return this.key;
    },
    
    /**
     * Returns field.
     *
     * @method getField
     * @return {String} Column field.
     */
    getField : function() {
        return this.field;
    },
    
    /**
     * Returns Column key which has been sanitized for DOM (class and ID) usage
     * starts with letter, contains only letters, numbers, hyphen, or period.
     *
     * @method getSanitizedKey
     * @return {String} Sanitized Column key.
     */
    getSanitizedKey : function() {
        return this.getKey().replace(/[^\w\-]/g,"");
    },

    /**
     * Public accessor returns Column's current position index within its
     * ColumnSet's keys array, if applicable. Only non-nested and bottom-level
     * child Columns will return a value.
     *
     * @method getKeyIndex
     * @return {Number} Position index, or null.
     */
    getKeyIndex : function() {
        return this._nKeyIndex;
    },

    /**
     * Public accessor returns Column's current position index within its
     * ColumnSet's tree array, if applicable. Only non-nested and top-level parent
     * Columns will return a value;
     *
     * @method getTreeIndex
     * @return {Number} Position index, or null.
     */
    getTreeIndex : function() {
        return this._nTreeIndex;
    },

    /**
     * Public accessor returns Column's parent instance if any, or null otherwise.
     *
     * @method getParent
     * @return {YAHOO.widget.Column} Column's parent instance.
     */
    getParent : function() {
        return this._oParent;
    },

    /**
     * Public accessor returns Column's calculated COLSPAN value.
     *
     * @method getColspan
     * @return {Number} Column's COLSPAN value.
     */
    getColspan : function() {
        return this._nColspan;
    },
    // Backward compatibility
    getColSpan : function() {
        YAHOO.log("The method getColSpan() has been" +
        " deprecated in favor of getColspan()", "warn", this.toString());
        return this.getColspan();
    },

    /**
     * Public accessor returns Column's calculated ROWSPAN value.
     *
     * @method getRowspan
     * @return {Number} Column's ROWSPAN value.
     */
    getRowspan : function() {
        return this._nRowspan;
    },

    /**
     * Returns DOM reference to the key TH element.
     *
     * @method getThEl
     * @return {HTMLElement} TH element.
     */
    getThEl : function() {
        return this._elTh;
    },

    /**
     * Returns DOM reference to the TH's liner DIV element. Introduced since
     * resizeable Columns may have an extra resizer liner, making the DIV liner
     * not reliably the TH element's first child.               
     *
     * @method getThLInerEl
     * @return {HTMLElement} TH element.
     */
    getThLinerEl : function() {
        return this._elThLiner;
    },
    
    /**
     * Returns DOM reference to the resizer element, or null.
     *
     * @method getResizerEl
     * @return {HTMLElement} DIV element.
     */
    getResizerEl : function() {
        return this._elResizer;
    },

    // Backward compatibility
    /**
     * @method getColEl
     * @deprecated Use getThEl
     */
    getColEl : function() {
        YAHOO.log("The method getColEl() has been" +
        " deprecated in favor of getThEl()", "warn",
        this.toString());
        return this.getThEl();
    },
    getIndex : function() {
        YAHOO.log("The method getIndex() has been" +
        " deprecated in favor of getKeyIndex()", "warn",
        this.toString());
        return this.getKeyIndex();
    },
    format : function() {
        YAHOO.log("The method format() has been deprecated in favor of the " +
        "DataTable method formatCell()", "error", this.toString());
    }
};

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * Sort static utility to support Column sorting.
 *
 * @namespace YAHOO.util
 * @class Sort
 * @static
 */
YAHOO.util.Sort = {
    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Comparator function for simple case-insensitive string sorting.
     *
     * @method compare
     * @param a {Object} First sort argument.
     * @param b {Object} Second sort argument.
     * @param desc {Boolean} True if sort direction is descending, false if
     * sort direction is ascending.
     * @return {Boolean} Return -1 when a < b. Return 0 when a = b.
     * Return 1 when a > b.
     */
    compare: function(a, b, desc) {
        if((a === null) || (typeof a == "undefined")) {
            if((b === null) || (typeof b == "undefined")) {
                return 0;
            }
            else {
                return 1;
            }
        }
        else if((b === null) || (typeof b == "undefined")) {
            return -1;
        }

        if(a.constructor == String) {
            a = a.toLowerCase();
        }
        if(b.constructor == String) {
            b = b.toLowerCase();
        }
        if(a < b) {
            return (desc) ? 1 : -1;
        }
        else if (a > b) {
            return (desc) ? -1 : 1;
        }
        else {
            return 0;
        }
    }
};

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * ColumnDD subclasses DragDrop to support rearrangeable Columns.
 *
 * @namespace YAHOO.util
 * @class ColumnDD
 * @extends YAHOO.util.DDProxy
 * @constructor
 * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param elTh {HTMLElement} TH element reference.
 * @param elTarget {HTMLElement} Drag target element.
 */
YAHOO.widget.ColumnDD = function(oDataTable, oColumn, elTh, elTarget) {
    if(oDataTable && oColumn && elTh && elTarget) {
        this.datatable = oDataTable;
        this.table = oDataTable.getTableEl();
        this.column = oColumn;
        this.headCell = elTh;
        this.pointer = elTarget;
        this.newIndex = null;
        this.init(elTh);
        this.initFrame(); // Needed for DDProxy
        this.invalidHandleTypes = {};

        // Set top/bottom padding to account for children of nested columns
        this.setPadding(10, 0, (this.datatable.getTheadEl().offsetHeight + 10) , 0);

        YAHOO.util.Event.on(window, 'resize', function() {
            this.initConstraints();
        }, this, true);
    }
    else {
        YAHOO.log("Column dragdrop could not be created","warn",oDataTable.toString());
    }
};

if(YAHOO.util.DDProxy) {
    YAHOO.extend(YAHOO.widget.ColumnDD, YAHOO.util.DDProxy, {
        initConstraints: function() {
            //Get the top, right, bottom and left positions
            var region = YAHOO.util.Dom.getRegion(this.table),
                //Get the element we are working on
                el = this.getEl(),
                //Get the xy position of it
                xy = YAHOO.util.Dom.getXY(el),
                //Get the width and height
                width = parseInt(YAHOO.util.Dom.getStyle(el, 'width'), 10),
                height = parseInt(YAHOO.util.Dom.getStyle(el, 'height'), 10),
                //Set left to x minus left
                left = ((xy[0] - region.left) + 15), //Buffer of 15px
                //Set right to right minus x minus width
                right = ((region.right - xy[0] - width) + 15);
    
            //Set the constraints based on the above calculations
            this.setXConstraint(left, right);
            this.setYConstraint(10, 10);            
        },
        _resizeProxy: function() {
            YAHOO.widget.ColumnDD.superclass._resizeProxy.apply(this, arguments);
            var dragEl = this.getDragEl(),
                el = this.getEl();

            YAHOO.util.Dom.setStyle(this.pointer, 'height', (this.table.parentNode.offsetHeight + 10) + 'px');
            YAHOO.util.Dom.setStyle(this.pointer, 'display', 'block');
            var xy = YAHOO.util.Dom.getXY(el);
            YAHOO.util.Dom.setXY(this.pointer, [xy[0], (xy[1] - 5)]);
            
            YAHOO.util.Dom.setStyle(dragEl, 'height', this.datatable.getContainerEl().offsetHeight + "px");
            YAHOO.util.Dom.setStyle(dragEl, 'width', (parseInt(YAHOO.util.Dom.getStyle(dragEl, 'width'),10) + 4) + 'px');
            YAHOO.util.Dom.setXY(this.dragEl, xy);
        },
        onMouseDown: function() {
                this.initConstraints();
                this.resetConstraints();
        },
        clickValidator: function(e) {
            if(!this.column.hidden) {
                var target = YAHOO.util.Event.getTarget(e);
                return ( this.isValidHandleChild(target) &&
                            (this.id == this.handleElId ||
                                this.DDM.handleWasClicked(target, this.id)) );
            }
        },
        onDragOver: function(ev, id) {
            // Validate target as a Column
            var target = this.datatable.getColumn(id);
            if(target) {                
                // Validate target as a top-level parent
                var targetIndex = target.getTreeIndex();
                while((targetIndex === null) && target.getParent()) {
                    target = target.getParent();
                    targetIndex = target.getTreeIndex();
                }
                if(targetIndex !== null) {
                    // Are we placing to left or right of target?
                    var elTarget = target.getThEl();
                    var newIndex = targetIndex;
                    var mouseX = YAHOO.util.Event.getPageX(ev),
                        targetX = YAHOO.util.Dom.getX(elTarget),
                        midX = targetX + ((YAHOO.util.Dom.get(elTarget).offsetWidth)/2),
                        currentIndex =  this.column.getTreeIndex();
                    
                    if (mouseX < midX) {
                       YAHOO.util.Dom.setX(this.pointer, targetX);
                    } else {
                        var targetWidth = parseInt(elTarget.offsetWidth, 10);
                        YAHOO.util.Dom.setX(this.pointer, (targetX + targetWidth));
                        newIndex++;
                    }
                    if (targetIndex > currentIndex) {
                        newIndex--;
                    }
                    if(newIndex < 0) {
                        newIndex = 0;
                    }
                    else if(newIndex > this.datatable.getColumnSet().tree[0].length) {
                        newIndex = this.datatable.getColumnSet().tree[0].length;
                    }
                    this.newIndex = newIndex;
                }
            }
        },
        onDragDrop: function() {
            this.datatable.reorderColumn(this.column, this.newIndex);
        },
        endDrag: function() {
            this.newIndex = null;
            YAHOO.util.Dom.setStyle(this.pointer, 'display', 'none');
        }
    });
}

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * ColumnResizer subclasses DragDrop to support resizeable Columns.
 *
 * @namespace YAHOO.util
 * @class ColumnResizer
 * @extends YAHOO.util.DDProxy
 * @constructor
 * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param elTh {HTMLElement} TH element reference.
 * @param sHandleElId {String} DOM ID of the handle element that causes the resize.
 * @param elProxy {HTMLElement} Resizer proxy element.
 */
YAHOO.util.ColumnResizer = function(oDataTable, oColumn, elTh, sHandleId, elProxy) {
    if(oDataTable && oColumn && elTh && sHandleId) {
        this.datatable = oDataTable;
        this.column = oColumn;
        this.headCell = elTh;
        this.headCellLiner = oColumn.getThLinerEl();
        this.resizerLiner = elTh.firstChild;
        this.init(sHandleId, sHandleId, {dragOnly:true, dragElId: elProxy.id});
        this.initFrame(); // Needed for proxy
        this.resetResizerEl(); // Needed when rowspan > 0

        // Set right padding for bug 1858462
        this.setPadding(0, 1, 0, 0);
    }
    else {
        YAHOO.log("Column resizer could not be created","warn",oDataTable.toString());
    }
};

if(YAHOO.util.DD) {
    YAHOO.extend(YAHOO.util.ColumnResizer, YAHOO.util.DDProxy, {
        /////////////////////////////////////////////////////////////////////////////
        //
        // Public methods
        //
        /////////////////////////////////////////////////////////////////////////////
        /**
         * Resets resizer element.
         *
         * @method resetResizerEl
         */
        resetResizerEl : function() {
            var resizerStyle = YAHOO.util.Dom.get(this.handleElId).style;
            resizerStyle.left = "auto";
            resizerStyle.right = 0;
            resizerStyle.top = "auto";
            resizerStyle.bottom = 0;
            resizerStyle.height = this.headCell.offsetHeight+"px";
        },
    
        /////////////////////////////////////////////////////////////////////////////
        //
        // Public DOM event handlers
        //
        /////////////////////////////////////////////////////////////////////////////
    
        /**
         * Handles mouseup events on the Column resizer.
         *
         * @method onMouseUp
         * @param e {string} The mouseup event
         */
        onMouseUp : function(e) {
            // Reset height of all resizer els in case TH's have changed height
            var allKeys = this.datatable.getColumnSet().keys,
                col;
            for(var i=0, len=allKeys.length; i<len; i++) {
                col = allKeys[i];
                if(col._ddResizer) {
                    col._ddResizer.resetResizerEl();
                }
            }
            this.resetResizerEl();
            
            var el = this.headCellLiner;
            var newWidth = el.offsetWidth -
                (parseInt(YAHOO.util.Dom.getStyle(el,"paddingLeft"),10)|0) -
                (parseInt(YAHOO.util.Dom.getStyle(el,"paddingRight"),10)|0);

            this.datatable.fireEvent("columnResizeEvent", {column:this.column,target:this.headCell,width:newWidth});
        },
    
        /**
         * Handles mousedown events on the Column resizer.
         *
         * @method onMouseDown
         * @param e {string} The mousedown event
         */
        onMouseDown : function(e) {
            this.startWidth = this.headCellLiner.offsetWidth;
            this.startX = YAHOO.util.Event.getXY(e)[0];
            this.nLinerPadding = (parseInt(YAHOO.util.Dom.getStyle(this.headCellLiner,"paddingLeft"),10)|0) +
                    (parseInt(YAHOO.util.Dom.getStyle(this.headCellLiner,"paddingRight"),10)|0);
        },
    
        /**
         * Custom clickValidator to ensure Column is not in hidden state.
         *
         * @method clickValidator
         * @param {Event} e
         * @private
         */
        clickValidator : function(e) {
            if(!this.column.hidden) {
                var target = YAHOO.util.Event.getTarget(e);
                return ( this.isValidHandleChild(target) &&
                            (this.id == this.handleElId ||
                                this.DDM.handleWasClicked(target, this.id)) );
            }
        },
    
        /**
         * Handles start drag on the Column resizer.
         *
         * @method startDrag
         * @param e {string} The drag event
         */
        startDrag : function() {
            // Shrinks height of all resizer els to not hold open TH els
            var allKeys = this.datatable.getColumnSet().keys,
                thisKey = this.column.getKeyIndex(),
                col;
            for(var i=0, len=allKeys.length; i<len; i++) {
                col = allKeys[i];
                if(col._ddResizer) {
                    YAHOO.util.Dom.get(col._ddResizer.handleElId).style.height = "1em";
                }
            }
        },

        /**
         * Handles drag events on the Column resizer.
         *
         * @method onDrag
         * @param e {string} The drag event
         */
        onDrag : function(e) {
            var newX = YAHOO.util.Event.getXY(e)[0];
            if(newX > YAHOO.util.Dom.getX(this.headCellLiner)) {
                var offsetX = newX - this.startX;
                var newWidth = this.startWidth + offsetX - this.nLinerPadding;
                if(newWidth > 0) {
                    this.datatable.setColumnWidth(this.column, newWidth);
                }
            }
        }
    });
}

/////////////////////////////////////////////////////////////////////////////
//
// Deprecated
//
/////////////////////////////////////////////////////////////////////////////

/**
 * @property editorOptions
 * @deprecated Pass configs directly to CellEditor constructor. 
 */


(function () {

var lang   = YAHOO.lang,
    util   = YAHOO.util,
    widget = YAHOO.widget,
    
    Dom    = util.Dom,
    Ev     = util.Event,
    DT     = widget.DataTable;

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * A RecordSet defines and manages a set of Records.
 *
 * @namespace YAHOO.widget
 * @class RecordSet
 * @param data {Object || Object[]} An object literal or an array of data.
 * @constructor
 */
YAHOO.widget.RecordSet = function(data) {
    this._init(data);
};

var RS = widget.RecordSet;

/**
 * Internal class variable to name multiple Recordset instances.
 *
 * @property RecordSet._nCount
 * @type Number
 * @private
 * @static
 */
RS._nCount = 0;

RS.prototype = {

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private member variables
    //
    /////////////////////////////////////////////////////////////////////////////
    /**
     * Unique String identifier assigned at instantiation.
     *
     * @property _sId
     * @type String
     * @private
     */
    _sId : null,

    /**
     * Internal counter of how many Records are in the RecordSet.
     *
     * @property _length
     * @type Number
     * @private
     * @deprecated No longer used
     */
    //_length : null,

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private methods
    //
    /////////////////////////////////////////////////////////////////////////////
    
    /**
     * Initializer.
     *
     * @method _init
     * @param data {Object || Object[]} An object literal or an array of data.
     * @private
     */
    _init : function(data) {
        // Internal variables
        this._sId = Dom.generateId(null, "yui-rs");// "yui-rs" + widget.RecordSet._nCount;
        widget.RecordSet._nCount++;
        this._records = [];
        //this._length = 0;

        this._initEvents();

        if(data) {
            if(lang.isArray(data)) {
                this.addRecords(data);
            }
            else if(lang.isObject(data)) {
                this.addRecord(data);
            }
        }

        YAHOO.log("RecordSet initialized", "info", this.toString());
    },
    
    /**
     * Initializes custom events.
     *
     * @method _initEvents
     * @private
     */
    _initEvents : function() {
        this.createEvent("recordAddEvent");
        this.createEvent("recordsAddEvent");
        this.createEvent("recordSetEvent");
        this.createEvent("recordsSetEvent");
        this.createEvent("recordUpdateEvent");
        this.createEvent("recordDeleteEvent");
        this.createEvent("recordsDeleteEvent");
        this.createEvent("resetEvent");
        this.createEvent("recordValueUpdateEvent");
    },

    /**
     * Adds one Record to the RecordSet at the given index. If index is null,
     * then adds the Record to the end of the RecordSet.
     *
     * @method _addRecord
     * @param oData {Object} An object literal of data.
     * @param index {Number} (optional) Position index.
     * @return {YAHOO.widget.Record} A Record instance.
     * @private
     */
    _addRecord : function(oData, index) {
        var oRecord = new YAHOO.widget.Record(oData);
        
        if(YAHOO.lang.isNumber(index) && (index > -1)) {
            this._records.splice(index,0,oRecord);
        }
        else {
            //index = this.getLength();
            //this._records[index] = oRecord;
            this._records[this._records.length] = oRecord;
        }
        //this._length++;
        return oRecord;
    },

    /**
     * Sets/replaces one Record to the RecordSet at the given index.  Existing
     * Records with higher indexes are not shifted.  If no index specified, the
     * Record is added to the end of the RecordSet.
     *
     * @method _setRecord
     * @param oData {Object} An object literal of data.
     * @param index {Number} (optional) Position index.
     * @return {YAHOO.widget.Record} A Record instance.
     * @private
     */
    _setRecord : function(oData, index) {
        if (!lang.isNumber(index) || index < 0) {
            index = this._records.length;
        }
        return (this._records[index] = new widget.Record(oData));
        /*
        if(lang.isNumber(index) && (index > -1)) {
            this._records[index] = oRecord;
            if((index+1) > this.getLength()) {
                this._length = index+1;
            }
        }
        else {
            this._records[this.getLength()] = oRecord;
            this._length++;
        }
        return oRecord;
        */
    },

    /**
     * Deletes Records from the RecordSet at the given index. If range is null,
     * then only one Record is deleted.
     *
     * @method _deleteRecord
     * @param index {Number} Position index.
     * @param range {Number} (optional) How many Records to delete
     * @private
     */
    _deleteRecord : function(index, range) {
        if(!lang.isNumber(range) || (range < 0)) {
            range = 1;
        }
        this._records.splice(index, range);
        //this._length = this._length - range;
    },

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Returns unique name of the RecordSet instance.
     *
     * @method getId
     * @return {String} Unique name of the RecordSet instance.
     */
    getId : function() {
        return this._sId;
    },

    /**
     * Public accessor to the unique name of the RecordSet instance.
     *
     * @method toString
     * @return {String} Unique name of the RecordSet instance.
     */
    toString : function() {
        return "RecordSet instance " + this._sId;
    },

    /**
     * Returns the number of Records held in the RecordSet.
     *
     * @method getLength
     * @return {Number} Number of records in the RecordSet.
     */
    getLength : function() {
            //return this._length;
            return this._records.length;
    },

    /**
     * Returns Record by ID or RecordSet position index.
     *
     * @method getRecord
     * @param record {YAHOO.widget.Record | Number | String} Record instance,
     * RecordSet position index, or Record ID.
     * @return {YAHOO.widget.Record} Record object.
     */
    getRecord : function(record) {
        var i;
        if(record instanceof widget.Record) {
            for(i=0; i<this._records.length; i++) {
                if(this._records[i] && (this._records[i]._sId === record._sId)) {
                    return record;
                }
            }
        }
        else if(lang.isNumber(record)) {
            if((record > -1) && (record < this.getLength())) {
                return this._records[record];
            }
        }
        else if(lang.isString(record)) {
            for(i=0; i<this._records.length; i++) {
                if(this._records[i] && (this._records[i]._sId === record)) {
                    return this._records[i];
                }
            }
        }
        // Not a valid Record for this RecordSet
        return null;

    },

    /**
     * Returns an array of Records from the RecordSet.
     *
     * @method getRecords
     * @param index {Number} (optional) Recordset position index of which Record to
     * start at.
     * @param range {Number} (optional) Number of Records to get.
     * @return {YAHOO.widget.Record[]} Array of Records starting at given index and
     * length equal to given range. If index is not given, all Records are returned.
     */
    getRecords : function(index, range) {
        if(!lang.isNumber(index)) {
            return this._records;
        }
        if(!lang.isNumber(range)) {
            return this._records.slice(index);
        }
        return this._records.slice(index, index+range);
    },

    /**
     * Returns a boolean indicating whether Records exist in the RecordSet at the
     * specified index range.  Returns true if and only if a Record exists at each
     * index in the range.
     * @method hasRecords
     * @param index
     * @param range
     * @return {Boolean} true if all indices are populated in the RecordSet
     */
    hasRecords : function (index, range) {
        var recs = this.getRecords(index,range);
        for (var i = 0; i < range; ++i) {
            if (typeof recs[i] === 'undefined') {
                return false;
            }
        }
        return true;
    },

    /**
     * Returns current position index for the given Record.
     *
     * @method getRecordIndex
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @return {Number} Record's RecordSet position index.
     */

    getRecordIndex : function(oRecord) {
        if(oRecord) {
            for(var i=this._records.length-1; i>-1; i--) {
                if(this._records[i] && oRecord.getId() === this._records[i].getId()) {
                    return i;
                }
            }
        }
        return null;

    },

    /**
     * Adds one Record to the RecordSet at the given index. If index is null,
     * then adds the Record to the end of the RecordSet.
     *
     * @method addRecord
     * @param oData {Object} An object literal of data.
     * @param index {Number} (optional) Position index.
     * @return {YAHOO.widget.Record} A Record instance.
     */
    addRecord : function(oData, index) {
        if(lang.isObject(oData)) {
            var oRecord = this._addRecord(oData, index);
            this.fireEvent("recordAddEvent",{record:oRecord,data:oData});
            YAHOO.log("Added Record at index " + index +
                    " with data " + lang.dump(oData), "info", this.toString());
            return oRecord;
        }
        else {
            YAHOO.log("Could not add Record with data" +
                    lang.dump(oData), "info", this.toString());
            return null;
        }
    },

    /**
     * Adds multiple Records at once to the RecordSet at the given index with the
     * given object literal data. If index is null, then the new Records are
     * added to the end of the RecordSet.
     *
     * @method addRecords
     * @param aData {Object[]} An object literal data or an array of data object literals.
     * @param index {Number} (optional) Position index.
     * @return {YAHOO.widget.Record[]} An array of Record instances.
     */
    addRecords : function(aData, index) {
        if(lang.isArray(aData)) {
            var newRecords = [],
                idx,i,len;

            index = lang.isNumber(index) ? index : this._records.length;
            idx = index;

            // Can't go backwards bc we need to preserve order
            for(i=0,len=aData.length; i<len; ++i) {
                if(lang.isObject(aData[i])) {
                    var record = this._addRecord(aData[i], idx++);
                    newRecords.push(record);
                }
           }
            this.fireEvent("recordsAddEvent",{records:newRecords,data:aData});
            YAHOO.log("Added " + newRecords.length + " Record(s) at index " + index +
                    " with data " + lang.dump(aData), "info", this.toString());
           return newRecords;
        }
        else if(lang.isObject(aData)) {
            var oRecord = this._addRecord(aData);
            this.fireEvent("recordsAddEvent",{records:[oRecord],data:aData});
            YAHOO.log("Added 1 Record at index " + index +
                    " with data " + lang.dump(aData), "info", this.toString());
            return oRecord;
        }
        else {
            YAHOO.log("Could not add Records with data " +
                    lang.dump(aData), "info", this.toString());
            return null;
        }
    },

    /**
     * Sets or replaces one Record to the RecordSet at the given index. Unlike
     * addRecord, an existing Record at that index is not shifted to preserve it.
     * If no index is specified, it adds the Record to the end of the RecordSet.
     *
     * @method setRecord
     * @param oData {Object} An object literal of data.
     * @param index {Number} (optional) Position index.
     * @return {YAHOO.widget.Record} A Record instance.
     */
    setRecord : function(oData, index) {
        if(lang.isObject(oData)) {
            var oRecord = this._setRecord(oData, index);
            this.fireEvent("recordSetEvent",{record:oRecord,data:oData});
            YAHOO.log("Set Record at index " + index +
                    " with data " + lang.dump(oData), "info", this.toString());
            return oRecord;
        }
        else {
            YAHOO.log("Could not set Record with data" +
                    lang.dump(oData), "info", this.toString());
            return null;
        }
    },

    /**
     * Sets or replaces multiple Records at once to the RecordSet with the given
     * data, starting at the given index. If index is not specified, then the new
     * Records are added to the end of the RecordSet.
     *
     * @method setRecords
     * @param aData {Object[]} An array of object literal data.
     * @param index {Number} (optional) Position index.
     * @return {YAHOO.widget.Record[]} An array of Record instances.
     */
    setRecords : function(aData, index) {
        var Rec   = widget.Record,
            a     = lang.isArray(aData) ? aData : [aData],
            added = [],
            i = 0, l = a.length, j = 0;

        index = parseInt(index,10)|0;

        for(; i < l; ++i) {
            if (typeof a[i] === 'object' && a[i]) {
                added[j++] = this._records[index + i] = new Rec(a[i]);
            }
        }

        this.fireEvent("recordsSetEvent",{records:added,data:aData});
        // Backward compatibility for bug 1918245
        this.fireEvent("recordsSet",{records:added,data:aData});
        YAHOO.log("Set "+j+" Record(s) at index "+index, "info",
                  this.toString());

        if (a.length && !added.length) {
            YAHOO.log("Could not set Records with data " +
                    lang.dump(aData), "info", this.toString());
        }

        return added;
    },

    /**
     * Updates given Record with given data.
     *
     * @method updateRecord
     * @param record {YAHOO.widget.Record | Number | String} A Record instance,
     * a RecordSet position index, or a Record ID.
     * @param oData {Object} Object literal of new data.
     * @return {YAHOO.widget.Record} Updated Record, or null.
     */
    updateRecord : function(record, oData) {
        var oRecord = this.getRecord(record);
        if(oRecord && lang.isObject(oData)) {
            // Copy data from the Record for the event that gets fired later
            var oldData = {};
            for(var key in oRecord._oData) {
                if(lang.hasOwnProperty(oRecord._oData, key)) {
                    oldData[key] = oRecord._oData[key];
                }
            }
            oRecord._oData = oData;
            this.fireEvent("recordUpdateEvent",{record:oRecord,newData:oData,oldData:oldData});
            YAHOO.log("Record at index " + this.getRecordIndex(oRecord) +
                    " updated with data " + lang.dump(oData), "info", this.toString());
            return oRecord;
        }
        else {
            YAHOO.log("Could not update Record " + record, "error", this.toString());
            return null;
        }
    },

    /**
     * @method updateKey
     * @deprecated Use updateRecordValue
     */
    updateKey : function(record, sKey, oData) {
        this.updateRecordValue(record, sKey, oData);
    },
    /**
     * Sets given Record at given key to given data.
     *
     * @method updateRecordValue
     * @param record {YAHOO.widget.Record | Number | String} A Record instance,
     * a RecordSet position index, or a Record ID.
     * @param sKey {String} Key name.
     * @param oData {Object} New data.
     */
    updateRecordValue : function(record, sKey, oData) {
        var oRecord = this.getRecord(record);
        if(oRecord) {
            var oldData = null;
            var keyValue = oRecord._oData[sKey];
            // Copy data from the Record for the event that gets fired later
            if(keyValue && lang.isObject(keyValue)) {
                oldData = {};
                for(var key in keyValue)  {
                    if(lang.hasOwnProperty(keyValue, key)) {
                        oldData[key] = keyValue[key];
                    }
                }
            }
            // Copy by value
            else {
                oldData = keyValue;
            }

            oRecord._oData[sKey] = oData;
            this.fireEvent("keyUpdateEvent",{record:oRecord,key:sKey,newData:oData,oldData:oldData});
            this.fireEvent("recordValueUpdateEvent",{record:oRecord,key:sKey,newData:oData,oldData:oldData});
            YAHOO.log("Key \"" + sKey +
                    "\" for Record at index " + this.getRecordIndex(oRecord) +
                    " updated to \"" + lang.dump(oData) + "\"", "info", this.toString());
        }
        else {
            YAHOO.log("Could not update key " + sKey + " for Record " + record, "error", this.toString());
        }
    },

    /**
     * Replaces all Records in RecordSet with new object literal data.
     *
     * @method replaceRecords
     * @param data {Object || Object[]} An object literal of data or an array of
     * data object literals.
     * @return {YAHOO.widget.Record || YAHOO.widget.Record[]} A Record instance or
     * an array of Records.
     */
    replaceRecords : function(data) {
        this.reset();
        return this.addRecords(data);
    },

    /**
     * Sorts all Records by given function. Records keep their unique IDs but will
     * have new RecordSet position indexes.
     *
     * @method sortRecords
     * @param fnSort {Function} Reference to a sort function.
     * @param desc {Boolean} True if sort direction is descending, false if sort
     * direction is ascending.
     * @param field {String} The field to sort by, from sortOptions.field
     * @return {YAHOO.widget.Record[]} Sorted array of Records.
     */
    sortRecords : function(fnSort, desc, field) {
        return this._records.sort(function(a, b) {return fnSort(a, b, desc, field);});
    },

    /**
     * Reverses all Records, so ["one", "two", "three"] becomes ["three", "two", "one"].
     *
     * @method reverseRecords
     * @return {YAHOO.widget.Record[]} Reverse-sorted array of Records.
     */
    reverseRecords : function() {
        return this._records.reverse();
    },

    /**
     * Removes the Record at the given position index from the RecordSet. If a range
     * is also provided, removes that many Records, starting from the index. Length
     * of RecordSet is correspondingly shortened.
     *
     * @method deleteRecord
     * @param index {Number} Record's RecordSet position index.
     * @return {Object} A copy of the data held by the deleted Record.
     */
    deleteRecord : function(index) {
        if(lang.isNumber(index) && (index > -1) && (index < this.getLength())) {
            var oData = this.getRecord(index).getData();
            
            this._deleteRecord(index);
            this.fireEvent("recordDeleteEvent",{data:oData,index:index});
            YAHOO.log("Record deleted at index " + index +
                    " and containing data " + lang.dump(oData), "info", this.toString());
            return oData;
        }
        else {
            YAHOO.log("Could not delete Record at index " + index, "error", this.toString());
            return null;
        }
    },

    /**
     * Removes the Record at the given position index from the RecordSet. If a range
     * is also provided, removes that many Records, starting from the index. Length
     * of RecordSet is correspondingly shortened.
     *
     * @method deleteRecords
     * @param index {Number} Record's RecordSet position index.
     * @param range {Number} (optional) How many Records to delete.
     * @return {Object[]} An array of copies of the data held by the deleted Records.     
     */
    deleteRecords : function(index, range) {
        if(!lang.isNumber(range)) {
            range = 1;
        }
        if(lang.isNumber(index) && (index > -1) && (index < this.getLength())) {
            var recordsToDelete = this.getRecords(index, range);
            var deletedData = [], // this mistakenly held Records, not data
                deletedObjects = []; // this passes data only
            
            for(var i=0; i<recordsToDelete.length; i++) {
                deletedData[deletedData.length] = recordsToDelete[i]; // backward compatibility
                deletedObjects[deletedObjects.length] = recordsToDelete[i].getData();
            }
            this._deleteRecord(index, range);

            this.fireEvent("recordsDeleteEvent",{data:deletedData,deletedData:deletedObjects,index:index});
            YAHOO.log(range + "Record(s) deleted at index " + index +
                    " and containing data " + lang.dump(deletedObjects), "info", this.toString());

            return deletedData;
        }
        else {
            YAHOO.log("Could not delete Records at index " + index, "error", this.toString());
            return null;
        }
    },

    /**
     * Deletes all Records from the RecordSet.
     *
     * @method reset
     */
    reset : function() {
        this._records = [];
        //this._length = 0;
        this.fireEvent("resetEvent");
        YAHOO.log("All Records deleted from RecordSet", "info", this.toString());
    }
};

/////////////////////////////////////////////////////////////////////////////
//
// Custom Events
//
/////////////////////////////////////////////////////////////////////////////

// RecordSet uses EventProvider
lang.augmentProto(RS, util.EventProvider);

/**
 * Fired when a new Record is added to the RecordSet.
 *
 * @event recordAddEvent
 * @param oArgs.record {YAHOO.widget.Record} The Record instance.
 * @param oArgs.data {Object} Data added.
 */

/**
 * Fired when multiple Records are added to the RecordSet at once.
 *
 * @event recordsAddEvent
 * @param oArgs.records {YAHOO.widget.Record[]} An array of Record instances.
 * @param oArgs.data {Object[]} Data added.
 */

/**
 * Fired when a Record is set in the RecordSet.
 *
 * @event recordSetEvent
 * @param oArgs.record {YAHOO.widget.Record} The Record instance.
 * @param oArgs.data {Object} Data added.
 */

/**
 * Fired when multiple Records are set in the RecordSet at once.
 *
 * @event recordsSetEvent
 * @param oArgs.records {YAHOO.widget.Record[]} An array of Record instances.
 * @param oArgs.data {Object[]} Data added.
 */

/**
 * Fired when a Record is updated with new data.
 *
 * @event recordUpdateEvent
 * @param oArgs.record {YAHOO.widget.Record} The Record instance.
 * @param oArgs.newData {Object} New data.
 * @param oArgs.oldData {Object} Old data.
 */

/**
 * Fired when a Record is deleted from the RecordSet.
 *
 * @event recordDeleteEvent
 * @param oArgs.data {Object} The data held by the deleted Record,
 * or an array of data object literals if multiple Records were deleted at once.
 * @param oArgs.index {Object} Index of the deleted Record.
 */

/**
 * Fired when multiple Records are deleted from the RecordSet at once.
 *
 * @event recordsDeleteEvent
 * @param oArgs.data {Object[]} An array of deleted Records.
 * @param oArgs.deletedData {Object[]} An array of deleted data.
 * @param oArgs.index {Object} Index of the first deleted Record.
 */

/**
 * Fired when all Records are deleted from the RecordSet at once.
 *
 * @event resetEvent
 */

/**
 * @event keyUpdateEvent    
 * @deprecated Use recordValueUpdateEvent     
 */

/**
 * Fired when a Record value is updated with new data.
 *
 * @event recordValueUpdateEvent
 * @param oArgs.record {YAHOO.widget.Record} The Record instance.
 * @param oArgs.key {String} The updated key.
 * @param oArgs.newData {Object} New data.
 * @param oArgs.oldData {Object} Old data.
 *
 */


/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * The Record class defines a DataTable record.
 *
 * @namespace YAHOO.widget
 * @class Record
 * @constructor
 * @param oConfigs {Object} (optional) Object literal of key/value pairs.
 */
YAHOO.widget.Record = function(oLiteral) {
    this._nCount = widget.Record._nCount;
    this._sId = Dom.generateId(null, "yui-rec");//"yui-rec" + this._nCount;
    widget.Record._nCount++;
    this._oData = {};
    if(lang.isObject(oLiteral)) {
        for(var sKey in oLiteral) {
            if(lang.hasOwnProperty(oLiteral, sKey)) {
                this._oData[sKey] = oLiteral[sKey];
            }
        }
    }
};

/////////////////////////////////////////////////////////////////////////////
//
// Private member variables
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Internal class variable to give unique IDs to Record instances.
 *
 * @property Record._nCount
 * @type Number
 * @private
 */
YAHOO.widget.Record._nCount = 0;

YAHOO.widget.Record.prototype = {
    /**
     * Immutable unique count assigned at instantiation. Remains constant while a
     * Record's position index can change from sorting.
     *
     * @property _nCount
     * @type Number
     * @private
     */
    _nCount : null,

    /**
     * Immutable unique ID assigned at instantiation. Remains constant while a
     * Record's position index can change from sorting.
     *
     * @property _sId
     * @type String
     * @private
     */
    _sId : null,

    /**
     * Holds data for the Record in an object literal.
     *
     * @property _oData
     * @type Object
     * @private
     */
    _oData : null,

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public member variables
    //
    /////////////////////////////////////////////////////////////////////////////

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Returns unique count assigned at instantiation.
     *
     * @method getCount
     * @return Number
     */
    getCount : function() {
        return this._nCount;
    },

    /**
     * Returns unique ID assigned at instantiation.
     *
     * @method getId
     * @return String
     */
    getId : function() {
        return this._sId;
    },

    /**
     * Returns data for the Record for a field if given, or the entire object
     * literal otherwise.
     *
     * @method getData
     * @param sField {String} (Optional) The field from which to retrieve data value.
     * @return Object
     */
    getData : function(sField) {
        if(lang.isString(sField)) {
            return this._oData[sField];
        }
        else {
            return this._oData;
        }
    },

    /**
     * Sets given data at the given key. Use the RecordSet method updateRecordValue to trigger
     * events. 
     *
     * @method setData
     * @param sKey {String} The key of the new value.
     * @param oData {MIXED} The new value.
     */
    setData : function(sKey, oData) {
        this._oData[sKey] = oData;
    }
};

})();

(function () {

var lang   = YAHOO.lang,
    util   = YAHOO.util,
    widget = YAHOO.widget,
    ua     = YAHOO.env.ua,
    
    Dom    = util.Dom,
    Ev     = util.Event,
    DS     = util.DataSourceBase;

/**
 * The DataTable widget provides a progressively enhanced DHTML control for
 * displaying tabular data across A-grade browsers.
 *
 * @module datatable
 * @requires yahoo, dom, event, element, datasource
 * @optional dragdrop, dragdrop
 * @title DataTable Widget
 */

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/

/**
 * DataTable class for the YUI DataTable widget.
 *
 * @namespace YAHOO.widget
 * @class DataTable
 * @extends YAHOO.util.Element
 * @constructor
 * @param elContainer {HTMLElement} Container element for the TABLE.
 * @param aColumnDefs {Object[]} Array of object literal Column definitions.
 * @param oDataSource {YAHOO.util.DataSource} DataSource instance.
 * @param oConfigs {object} (optional) Object literal of configuration values.
 */
YAHOO.widget.DataTable = function(elContainer,aColumnDefs,oDataSource,oConfigs) {
    var DT = widget.DataTable;
    
    ////////////////////////////////////////////////////////////////////////////
    // Backward compatibility for SDT, but prevent infinite loops
    
    if(oConfigs && oConfigs.scrollable) {
        return new YAHOO.widget.ScrollingDataTable(elContainer,aColumnDefs,oDataSource,oConfigs);
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Initialization

    // Internal vars
    this._nIndex = DT._nCount;
    this._sId = Dom.generateId(null, "yui-dt");// "yui-dt"+this._nIndex;
    this._oChainRender = new YAHOO.util.Chain();
    this._oChainRender.subscribe("end",this._onRenderChainEnd, this, true);

    // Initialize configs
    this._initConfigs(oConfigs);

    // Initialize DataSource
    this._initDataSource(oDataSource);
    if(!this._oDataSource) {
        YAHOO.log("Could not instantiate DataTable due to an invalid DataSource", "error", this.toString());
        return;
    }

    // Initialize ColumnSet
    this._initColumnSet(aColumnDefs);
    if(!this._oColumnSet) {
        YAHOO.log("Could not instantiate DataTable due to an invalid ColumnSet", "error", this.toString());
        return;
    }

    // Initialize RecordSet
    this._initRecordSet();
    if(!this._oRecordSet) {
    }

    // Initialize Attributes
    DT.superclass.constructor.call(this, elContainer, this.configs);

    // Initialize DOM elements
    var okDom = this._initDomElements(elContainer);
    if(!okDom) {
        YAHOO.log("Could not instantiate DataTable due to an invalid DOM element", "error", this.toString());
        return;
    }
            
    // Show message as soon as config is available
    this.showTableMessage(this.get("MSG_LOADING"), DT.CLASS_LOADING);
    
    ////////////////////////////////////////////////////////////////////////////
    // Once per instance
    this._initEvents();

    DT._nCount++;
    DT._nCurrentCount++;
    
    ////////////////////////////////////////////////////////////////////////////
    // Data integration

    // Send a simple initial request
    var oCallback = {
        success : this.onDataReturnSetRows,
        failure : this.onDataReturnSetRows,
        scope   : this,
        argument: this.getState()
    };
    
    var initialLoad = this.get("initialLoad");
    if(initialLoad === true) {
        this._oDataSource.sendRequest(this.get("initialRequest"), oCallback);
    }
    // Do not send an initial request at all
    else if(initialLoad === false) {
        this.showTableMessage(this.get("MSG_EMPTY"), DT.CLASS_EMPTY);
    }
    // Send an initial request with a custom payload
    else {
        var oCustom = initialLoad || {};
        oCallback.argument = oCustom.argument || {};
        this._oDataSource.sendRequest(oCustom.request, oCallback);
    }
};

var DT = widget.DataTable;

/////////////////////////////////////////////////////////////////////////////
//
// Public constants
//
/////////////////////////////////////////////////////////////////////////////

lang.augmentObject(DT, {

    /**
     * Class name assigned to outer DataTable container.
     *
     * @property DataTable.CLASS_DATATABLE
     * @type String
     * @static
     * @final
     * @default "yui-dt"
     */
    CLASS_DATATABLE : "yui-dt",

    /**
     * Class name assigned to liner DIV elements.
     *
     * @property DataTable.CLASS_LINER
     * @type String
     * @static
     * @final
     * @default "yui-dt-liner"
     */
    CLASS_LINER : "yui-dt-liner",

    /**
     * Class name assigned to display label elements.
     *
     * @property DataTable.CLASS_LABEL
     * @type String
     * @static
     * @final
     * @default "yui-dt-label"
     */
    CLASS_LABEL : "yui-dt-label",

    /**
     * Class name assigned to messaging elements.
     *
     * @property DataTable.CLASS_MESSAGE
     * @type String
     * @static
     * @final
     * @default "yui-dt-message"
     */
    CLASS_MESSAGE : "yui-dt-message",

    /**
     * Class name assigned to mask element when DataTable is disabled.
     *
     * @property DataTable.CLASS_MASK
     * @type String
     * @static
     * @final
     * @default "yui-dt-mask"
     */
    CLASS_MASK : "yui-dt-mask",

    /**
     * Class name assigned to data elements.
     *
     * @property DataTable.CLASS_DATA
     * @type String
     * @static
     * @final
     * @default "yui-dt-data"
     */
    CLASS_DATA : "yui-dt-data",

    /**
     * Class name assigned to Column drag target.
     *
     * @property DataTable.CLASS_COLTARGET
     * @type String
     * @static
     * @final
     * @default "yui-dt-coltarget"
     */
    CLASS_COLTARGET : "yui-dt-coltarget",

    /**
     * Class name assigned to resizer handle elements.
     *
     * @property DataTable.CLASS_RESIZER
     * @type String
     * @static
     * @final
     * @default "yui-dt-resizer"
     */
    CLASS_RESIZER : "yui-dt-resizer",

    /**
     * Class name assigned to resizer liner elements.
     *
     * @property DataTable.CLASS_RESIZERLINER
     * @type String
     * @static
     * @final
     * @default "yui-dt-resizerliner"
     */
    CLASS_RESIZERLINER : "yui-dt-resizerliner",

    /**
     * Class name assigned to resizer proxy elements.
     *
     * @property DataTable.CLASS_RESIZERPROXY
     * @type String
     * @static
     * @final
     * @default "yui-dt-resizerproxy"
     */
    CLASS_RESIZERPROXY : "yui-dt-resizerproxy",

    /**
     * Class name assigned to CellEditor container elements.
     *
     * @property DataTable.CLASS_EDITOR
     * @type String
     * @static
     * @final
     * @default "yui-dt-editor"
     */
    CLASS_EDITOR : "yui-dt-editor",

    /**
     * Class name assigned to CellEditor container shim.
     *
     * @property DataTable.CLASS_EDITOR_SHIM
     * @type String
     * @static
     * @final
     * @default "yui-dt-editor-shim"
     */
    CLASS_EDITOR_SHIM : "yui-dt-editor-shim",

    /**
     * Class name assigned to paginator container elements.
     *
     * @property DataTable.CLASS_PAGINATOR
     * @type String
     * @static
     * @final
     * @default "yui-dt-paginator"
     */
    CLASS_PAGINATOR : "yui-dt-paginator",

    /**
     * Class name assigned to page number indicators.
     *
     * @property DataTable.CLASS_PAGE
     * @type String
     * @static
     * @final
     * @default "yui-dt-page"
     */
    CLASS_PAGE : "yui-dt-page",

    /**
     * Class name assigned to default indicators.
     *
     * @property DataTable.CLASS_DEFAULT
     * @type String
     * @static
     * @final
     * @default "yui-dt-default"
     */
    CLASS_DEFAULT : "yui-dt-default",

    /**
     * Class name assigned to previous indicators.
     *
     * @property DataTable.CLASS_PREVIOUS
     * @type String
     * @static
     * @final
     * @default "yui-dt-previous"
     */
    CLASS_PREVIOUS : "yui-dt-previous",

    /**
     * Class name assigned next indicators.
     *
     * @property DataTable.CLASS_NEXT
     * @type String
     * @static
     * @final
     * @default "yui-dt-next"
     */
    CLASS_NEXT : "yui-dt-next",

    /**
     * Class name assigned to first elements.
     *
     * @property DataTable.CLASS_FIRST
     * @type String
     * @static
     * @final
     * @default "yui-dt-first"
     */
    CLASS_FIRST : "yui-dt-first",

    /**
     * Class name assigned to last elements.
     *
     * @property DataTable.CLASS_LAST
     * @type String
     * @static
     * @final
     * @default "yui-dt-last"
     */
    CLASS_LAST : "yui-dt-last",

    /**
     * Class name assigned to Record elements.
     *
     * @property DataTable.CLASS_REC
     * @type String
     * @static
     * @final
     * @default "yui-dt-rec"
     */
    CLASS_REC : "yui-dt-rec",

    /**
     * Class name assigned to even elements.
     *
     * @property DataTable.CLASS_EVEN
     * @type String
     * @static
     * @final
     * @default "yui-dt-even"
     */
    CLASS_EVEN : "yui-dt-even",

    /**
     * Class name assigned to odd elements.
     *
     * @property DataTable.CLASS_ODD
     * @type String
     * @static
     * @final
     * @default "yui-dt-odd"
     */
    CLASS_ODD : "yui-dt-odd",

    /**
     * Class name assigned to selected elements.
     *
     * @property DataTable.CLASS_SELECTED
     * @type String
     * @static
     * @final
     * @default "yui-dt-selected"
     */
    CLASS_SELECTED : "yui-dt-selected",

    /**
     * Class name assigned to highlighted elements.
     *
     * @property DataTable.CLASS_HIGHLIGHTED
     * @type String
     * @static
     * @final
     * @default "yui-dt-highlighted"
     */
    CLASS_HIGHLIGHTED : "yui-dt-highlighted",

    /**
     * Class name assigned to hidden elements.
     *
     * @property DataTable.CLASS_HIDDEN
     * @type String
     * @static
     * @final
     * @default "yui-dt-hidden"
     */
    CLASS_HIDDEN : "yui-dt-hidden",

    /**
     * Class name assigned to disabled elements.
     *
     * @property DataTable.CLASS_DISABLED
     * @type String
     * @static
     * @final
     * @default "yui-dt-disabled"
     */
    CLASS_DISABLED : "yui-dt-disabled",

    /**
     * Class name assigned to empty indicators.
     *
     * @property DataTable.CLASS_EMPTY
     * @type String
     * @static
     * @final
     * @default "yui-dt-empty"
     */
    CLASS_EMPTY : "yui-dt-empty",

    /**
     * Class name assigned to loading indicatorx.
     *
     * @property DataTable.CLASS_LOADING
     * @type String
     * @static
     * @final
     * @default "yui-dt-loading"
     */
    CLASS_LOADING : "yui-dt-loading",

    /**
     * Class name assigned to error indicators.
     *
     * @property DataTable.CLASS_ERROR
     * @type String
     * @static
     * @final
     * @default "yui-dt-error"
     */
    CLASS_ERROR : "yui-dt-error",

    /**
     * Class name assigned to editable elements.
     *
     * @property DataTable.CLASS_EDITABLE
     * @type String
     * @static
     * @final
     * @default "yui-dt-editable"
     */
    CLASS_EDITABLE : "yui-dt-editable",

    /**
     * Class name assigned to draggable elements.
     *
     * @property DataTable.CLASS_DRAGGABLE
     * @type String
     * @static
     * @final
     * @default "yui-dt-draggable"
     */
    CLASS_DRAGGABLE : "yui-dt-draggable",

    /**
     * Class name assigned to resizeable elements.
     *
     * @property DataTable.CLASS_RESIZEABLE
     * @type String
     * @static
     * @final
     * @default "yui-dt-resizeable"
     */
    CLASS_RESIZEABLE : "yui-dt-resizeable",

    /**
     * Class name assigned to scrollable elements.
     *
     * @property DataTable.CLASS_SCROLLABLE
     * @type String
     * @static
     * @final
     * @default "yui-dt-scrollable"
     */
    CLASS_SCROLLABLE : "yui-dt-scrollable",

    /**
     * Class name assigned to sortable elements.
     *
     * @property DataTable.CLASS_SORTABLE
     * @type String
     * @static
     * @final
     * @default "yui-dt-sortable"
     */
    CLASS_SORTABLE : "yui-dt-sortable",

    /**
     * Class name assigned to ascending elements.
     *
     * @property DataTable.CLASS_ASC
     * @type String
     * @static
     * @final
     * @default "yui-dt-asc"
     */
    CLASS_ASC : "yui-dt-asc",

    /**
     * Class name assigned to descending elements.
     *
     * @property DataTable.CLASS_DESC
     * @type String
     * @static
     * @final
     * @default "yui-dt-desc"
     */
    CLASS_DESC : "yui-dt-desc",

    /**
     * Class name assigned to BUTTON elements and/or container elements.
     *
     * @property DataTable.CLASS_BUTTON
     * @type String
     * @static
     * @final
     * @default "yui-dt-button"
     */
    CLASS_BUTTON : "yui-dt-button",

    /**
     * Class name assigned to INPUT TYPE=CHECKBOX elements and/or container elements.
     *
     * @property DataTable.CLASS_CHECKBOX
     * @type String
     * @static
     * @final
     * @default "yui-dt-checkbox"
     */
    CLASS_CHECKBOX : "yui-dt-checkbox",

    /**
     * Class name assigned to SELECT elements and/or container elements.
     *
     * @property DataTable.CLASS_DROPDOWN
     * @type String
     * @static
     * @final
     * @default "yui-dt-dropdown"
     */
    CLASS_DROPDOWN : "yui-dt-dropdown",

    /**
     * Class name assigned to INPUT TYPE=RADIO elements and/or container elements.
     *
     * @property DataTable.CLASS_RADIO
     * @type String
     * @static
     * @final
     * @default "yui-dt-radio"
     */
    CLASS_RADIO : "yui-dt-radio",

    /////////////////////////////////////////////////////////////////////////
    //
    // Private static properties
    //
    /////////////////////////////////////////////////////////////////////////

    /**
     * Internal class variable for indexing multiple DataTable instances.
     *
     * @property DataTable._nCount
     * @type Number
     * @private
     * @static
     */
    _nCount : 0,

    /**
     * Internal class variable tracking current number of DataTable instances,
     * so that certain class values can be reset when all instances are destroyed.          
     *
     * @property DataTable._nCurrentCount
     * @type Number
     * @private
     * @static
     */
    _nCurrentCount : 0,

    /**
     * Reference to the STYLE node that is dynamically created and updated
     * in order to manage Column widths.
     *
     * @property DataTable._elDynStyleNode
     * @type HTMLElement
     * @private
     * @static     
     */
    _elDynStyleNode : null,

    /**
     * Set to true if _elDynStyleNode cannot be populated due to browser incompatibility.
     *
     * @property DataTable._bDynStylesFallback
     * @type boolean
     * @private
     * @static     
     */
    _bDynStylesFallback : (ua.ie) ? true : false,

    /**
     * Object literal hash of Columns and their dynamically create style rules.
     *
     * @property DataTable._oDynStyles
     * @type Object
     * @private
     * @static     
     */
    _oDynStyles : {},

    /////////////////////////////////////////////////////////////////////////
    //
    // Private static methods
    //
    /////////////////////////////////////////////////////////////////////////

    /**
     * Clones object literal or array of object literals.
     *
     * @method DataTable._cloneObject
     * @param o {Object} Object.
     * @private
     * @static     
     */
    _cloneObject: function(o) {
        if(!lang.isValue(o)) {
            return o;
        }

        var copy = {};

        if(o instanceof YAHOO.widget.BaseCellEditor) {
            copy = o;
        }
        else if(Object.prototype.toString.apply(o) === "[object RegExp]") {
            copy = o;
        }
        else if(lang.isFunction(o)) {
            copy = o;
        }
        else if(lang.isArray(o)) {
            var array = [];
            for(var i=0,len=o.length;i<len;i++) {
                array[i] = DT._cloneObject(o[i]);
            }
            copy = array;
        }
        else if(lang.isObject(o)) {
            for (var x in o){
                if(lang.hasOwnProperty(o, x)) {
                    if(lang.isValue(o[x]) && lang.isObject(o[x]) || lang.isArray(o[x])) {
                        copy[x] = DT._cloneObject(o[x]);
                    }
                    else {
                        copy[x] = o[x];
                    }
                }
            }
        }
        else {
            copy = o;
        }

        return copy;
    },

    /**
     * Formats a BUTTON element.
     *
     * @method DataTable.formatButton
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {HTML} Data value for the cell. By default, the value
     * is what gets written to the BUTTON. String values are treated as markup
     * and inserted into the DOM with innerHTML.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatButton : function(el, oRecord, oColumn, oData, oDataTable) {
        var sValue = lang.isValue(oData) ? oData : "Click";
        //TODO: support YAHOO.widget.Button
        //if(YAHOO.widget.Button) {

        //}
        //else {
            el.innerHTML = "<button type=\"button\" class=\""+
                    DT.CLASS_BUTTON + "\">" + sValue + "</button>";
        //}
    },

    /**
     * Formats a CHECKBOX element.
     *
     * @method DataTable.formatCheckbox
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {Object | Boolean | HTML} Data value for the cell. Can be a simple
     * Boolean to indicate whether checkbox is checked or not. Can be object literal
     * {checked:bBoolean, label:sLabel}. String values are treated as markup
     * and inserted into the DOM with innerHTML.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatCheckbox : function(el, oRecord, oColumn, oData, oDataTable) {
        var bChecked = oData;
        bChecked = (bChecked) ? " checked=\"checked\"" : "";
        el.innerHTML = "<input type=\"checkbox\"" + bChecked +
                " class=\"" + DT.CLASS_CHECKBOX + "\" />";
    },

    /**
     * Formats currency. Default unit is USD.
     *
     * @method DataTable.formatCurrency
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {Number} Data value for the cell.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatCurrency : function(el, oRecord, oColumn, oData, oDataTable) {
        var oDT = oDataTable || this;
        el.innerHTML = util.Number.format(oData, oColumn.currencyOptions || oDT.get("currencyOptions"));
    },

    /**
     * Formats JavaScript Dates.
     *
     * @method DataTable.formatDate
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {Object} Data value for the cell, or null. String values are
     * treated as markup and inserted into the DOM with innerHTML.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatDate : function(el, oRecord, oColumn, oData, oDataTable) {
        var oDT = oDataTable || this,
            oConfig = oColumn.dateOptions || oDT.get("dateOptions");
        el.innerHTML = util.Date.format(oData, oConfig, oConfig.locale);
    },

    /**
     * Formats SELECT elements.
     *
     * @method DataTable.formatDropdown
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {Object} Data value for the cell, or null. String values may
     * be treated as markup and inserted into the DOM with innerHTML as element
     * label.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatDropdown : function(el, oRecord, oColumn, oData, oDataTable) {
        var oDT = oDataTable || this,
            selectedValue = (lang.isValue(oData)) ? oData : oRecord.getData(oColumn.field),
            options = (lang.isArray(oColumn.dropdownOptions)) ?
                oColumn.dropdownOptions : null,

            selectEl,
            collection = el.getElementsByTagName("select");

        // Create the form element only once, so we can attach the onChange listener
        if(collection.length === 0) {
            // Create SELECT element
            selectEl = document.createElement("select");
            selectEl.className = DT.CLASS_DROPDOWN;
            selectEl = el.appendChild(selectEl);

            // Add event listener
            Ev.addListener(selectEl,"change",oDT._onDropdownChange,oDT);
        }

        selectEl = collection[0];

        // Update the form element
        if(selectEl) {
            // Clear out previous options
            selectEl.innerHTML = "";

            // We have options to populate
            if(options) {
                // Create OPTION elements
                for(var i=0; i<options.length; i++) {
                    var option = options[i];
                    var optionEl = document.createElement("option");
                    optionEl.value = (lang.isValue(option.value)) ?
                            option.value : option;
                    // Bug 2334323: Support legacy text, support label for consistency with DropdownCellEditor
                    optionEl.innerHTML = (lang.isValue(option.text)) ?
                            option.text : (lang.isValue(option.label)) ? option.label : option;
                    optionEl = selectEl.appendChild(optionEl);
                    if (optionEl.value == selectedValue) {
                        optionEl.selected = true;
                    }
                }
            }
            // Selected value is our only option
            else {
                selectEl.innerHTML = "<option selected value=\"" + selectedValue + "\">" + selectedValue + "</option>";
            }
        }
        else {
            el.innerHTML = lang.isValue(oData) ? oData : "";
        }
    },

    /**
     * Formats emails.
     *
     * @method DataTable.formatEmail
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {String} Data value for the cell, or null. Values are
     * HTML-escaped.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatEmail : function(el, oRecord, oColumn, oData, oDataTable) {
        if(lang.isString(oData)) {
            oData = lang.escapeHTML(oData);
            el.innerHTML = "<a href=\"mailto:" + oData + "\">" + oData + "</a>";
        }
        else {
            el.innerHTML = lang.isValue(oData) ? lang.escapeHTML(oData.toString()) : "";
        }
    },

    /**
     * Formats links.
     *
     * @method DataTable.formatLink
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {String} Data value for the cell, or null. Values are
     * HTML-escaped
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatLink : function(el, oRecord, oColumn, oData, oDataTable) {
        if(lang.isString(oData)) {
            oData = lang.escapeHTML(oData);
            el.innerHTML = "<a href=\"" + oData + "\">" + oData + "</a>";
        }
        else {
            el.innerHTML = lang.isValue(oData) ? lang.escapeHTML(oData.toString()) : "";
        }
    },

    /**
     * Formats numbers.
     *
     * @method DataTable.formatNumber
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {Object} Data value for the cell, or null.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatNumber : function(el, oRecord, oColumn, oData, oDataTable) {
        var oDT = oDataTable || this;
        el.innerHTML = util.Number.format(oData, oColumn.numberOptions || oDT.get("numberOptions"));
    },

    /**
     * Formats INPUT TYPE=RADIO elements.
     *
     * @method DataTable.formatRadio
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {Object} (Optional) Data value for the cell.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatRadio : function(el, oRecord, oColumn, oData, oDataTable) {
        var oDT = oDataTable || this,
            bChecked = oData;
        bChecked = (bChecked) ? " checked=\"checked\"" : "";
        el.innerHTML = "<input type=\"radio\"" + bChecked +
                " name=\""+oDT.getId()+"-col-" + oColumn.getSanitizedKey() + "\"" +
                " class=\"" + DT.CLASS_RADIO+ "\" />";
    },

    /**
     * Formats text strings.
     *
     * @method DataTable.formatText
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {String} (Optional) Data value for the cell. Values are
     * HTML-escaped.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatText : function(el, oRecord, oColumn, oData, oDataTable) {
        var value = (lang.isValue(oData)) ? oData : "";
        el.innerHTML = lang.escapeHTML(value.toString());
    },

    /**
     * Formats TEXTAREA elements.
     *
     * @method DataTable.formatTextarea
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {Object} (Optional) Data value for the cell. Values are
     * HTML-escaped.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatTextarea : function(el, oRecord, oColumn, oData, oDataTable) {
        var value = (lang.isValue(oData)) ? lang.escapeHTML(oData.toString()) : "",
            markup = "<textarea>" + value + "</textarea>";
        el.innerHTML = markup;
    },

    /**
     * Formats INPUT TYPE=TEXT elements.
     *
     * @method DataTable.formatTextbox
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {Object} (Optional) Data value for the cell. Values are
     * HTML-escaped.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatTextbox : function(el, oRecord, oColumn, oData, oDataTable) {
        var value = (lang.isValue(oData)) ? lang.escapeHTML(oData.toString()) : "",
            markup = "<input type=\"text\" value=\"" + value + "\" />";
        el.innerHTML = markup;
    },

    /**
     * Default cell formatter
     *
     * @method DataTable.formatDefault
     * @param el {HTMLElement} The element to format with markup.
     * @param oRecord {YAHOO.widget.Record} Record instance.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oData {HTML} (Optional) Data value for the cell. String values are
     * treated as markup and inserted into the DOM with innerHTML.
     * @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
     * @static
     */
    formatDefault : function(el, oRecord, oColumn, oData, oDataTable) {
        el.innerHTML = (lang.isValue(oData) && oData !== "") ? oData.toString() : "&#160;";
    },

    /**
     * Validates data value to type Number, doing type conversion as
     * necessary. A valid Number value is return, else null is returned
     * if input value does not validate.
     *
     *
     * @method DataTable.validateNumber
     * @param oData {Object} Data to validate.
     * @static
    */
    validateNumber : function(oData) {
        //Convert to number
        var number = oData * 1;

        // Validate
        if(lang.isNumber(number)) {
            return number;
        }
        else {
            YAHOO.log("Could not validate data " + lang.dump(oData) + " to type Number", "warn", this.toString());
            return undefined;
        }
    }
});

// Done in separate step so referenced functions are defined.
/**
 * Registry of cell formatting functions, enables shortcut pointers in Column
 * definition formatter value (i.e., {key:"myColumn", formatter:"date"}).
 * @property DataTable.Formatter
 * @type Object
 * @static
 */
DT.Formatter = {
    button   : DT.formatButton,
    checkbox : DT.formatCheckbox,
    currency : DT.formatCurrency,
    "date"   : DT.formatDate,
    dropdown : DT.formatDropdown,
    email    : DT.formatEmail,
    link     : DT.formatLink,
    "number" : DT.formatNumber,
    radio    : DT.formatRadio,
    text     : DT.formatText,
    textarea : DT.formatTextarea,
    textbox  : DT.formatTextbox,

    defaultFormatter : DT.formatDefault
};

lang.extend(DT, util.Element, {

/////////////////////////////////////////////////////////////////////////////
//
// Superclass methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Implementation of Element's abstract method. Sets up config values.
 *
 * @method initAttributes
 * @param oConfigs {Object} (Optional) Object literal definition of configuration values.
 * @private
 */

initAttributes : function(oConfigs) {
    oConfigs = oConfigs || {};
    DT.superclass.initAttributes.call(this, oConfigs);

    /**
    * @attribute summary
    * @description String value for the SUMMARY attribute.
    * @type String
    * @default ""    
    */
    this.setAttributeConfig("summary", {
        value: "",
        validator: lang.isString,
        method: function(sSummary) {
            if(this._elTable) {
                this._elTable.summary = sSummary;
            }
        }
    });

    /**
    * @attribute selectionMode
    * @description Specifies row or cell selection mode. Accepts the following strings:
    *    <dl>
    *      <dt>"standard"</dt>
    *      <dd>Standard row selection with support for modifier keys to enable
    *      multiple selections.</dd>
    *
    *      <dt>"single"</dt>
    *      <dd>Row selection with modifier keys disabled to not allow
    *      multiple selections.</dd>
    *
    *      <dt>"singlecell"</dt>
    *      <dd>Cell selection with modifier keys disabled to not allow
    *      multiple selections.</dd>
    *
    *      <dt>"cellblock"</dt>
    *      <dd>Cell selection with support for modifier keys to enable multiple
    *      selections in a block-fashion, like a spreadsheet.</dd>
    *
    *      <dt>"cellrange"</dt>
    *      <dd>Cell selection with support for modifier keys to enable multiple
    *      selections in a range-fashion, like a calendar.</dd>
    *    </dl>
    *
    * @default "standard"
    * @type String
    */
    this.setAttributeConfig("selectionMode", {
        value: "standard",
        validator: lang.isString
    });

    /**
    * @attribute sortedBy
    * @description Object literal provides metadata for initial sort values if
    * data will arrive pre-sorted:
    * <dl>
    *     <dt>sortedBy.key</dt>
    *     <dd>{String} Key of sorted Column</dd>
    *     <dt>sortedBy.dir</dt>
    *     <dd>{String} Initial sort direction, either YAHOO.widget.DataTable.CLASS_ASC or YAHOO.widget.DataTable.CLASS_DESC</dd>
    * </dl>
    * @type Object | null
    */
    this.setAttributeConfig("sortedBy", {
        value: null,
        // TODO: accepted array for nested sorts
        validator: function(oNewSortedBy) {
            if(oNewSortedBy) {
                return (lang.isObject(oNewSortedBy) && oNewSortedBy.key);
            }
            else {
                return (oNewSortedBy === null);
            }
        },
        method: function(oNewSortedBy) {
            // Stash the previous value
            var oOldSortedBy = this.get("sortedBy");
            
            // Workaround for bug 1827195
            this._configs.sortedBy.value = oNewSortedBy;

            // Remove ASC/DESC from TH
            var oOldColumn,
                nOldColumnKeyIndex,
                oNewColumn,
                nNewColumnKeyIndex;
                
            if(this._elThead) {
                if(oOldSortedBy && oOldSortedBy.key && oOldSortedBy.dir) {
                    oOldColumn = this._oColumnSet.getColumn(oOldSortedBy.key);
                    nOldColumnKeyIndex = oOldColumn.getKeyIndex();
                    
                    // Remove previous UI from THEAD
                    var elOldTh = oOldColumn.getThEl();
                    Dom.removeClass(elOldTh, oOldSortedBy.dir);
                    this.formatTheadCell(oOldColumn.getThLinerEl().firstChild, oOldColumn, oNewSortedBy);
                }
                if(oNewSortedBy) {
                    oNewColumn = (oNewSortedBy.column) ? oNewSortedBy.column : this._oColumnSet.getColumn(oNewSortedBy.key);
                    nNewColumnKeyIndex = oNewColumn.getKeyIndex();
    
                    // Update THEAD with new UI
                    var elNewTh = oNewColumn.getThEl();
                    // Backward compatibility
                    if(oNewSortedBy.dir && ((oNewSortedBy.dir == "asc") ||  (oNewSortedBy.dir == "desc"))) {
                        var newClass = (oNewSortedBy.dir == "desc") ?
                                DT.CLASS_DESC :
                                DT.CLASS_ASC;
                        Dom.addClass(elNewTh, newClass);
                    }
                    else {
                         var sortClass = oNewSortedBy.dir || DT.CLASS_ASC;
                         Dom.addClass(elNewTh, sortClass);
                    }
                    this.formatTheadCell(oNewColumn.getThLinerEl().firstChild, oNewColumn, oNewSortedBy);
                }
            }
          
            if(this._elTbody) {
                // Update TBODY UI
                this._elTbody.style.display = "none";
                var allRows = this._elTbody.rows,
                    allCells;
                for(var i=allRows.length-1; i>-1; i--) {
                    allCells = allRows[i].childNodes;
                    if(allCells[nOldColumnKeyIndex]) {
                        Dom.removeClass(allCells[nOldColumnKeyIndex], oOldSortedBy.dir);
                    }
                    if(allCells[nNewColumnKeyIndex]) {
                        Dom.addClass(allCells[nNewColumnKeyIndex], oNewSortedBy.dir);
                    }
                }
                this._elTbody.style.display = "";
            }
                
            this._clearTrTemplateEl();
        }
    });
    
    /**
    * @attribute paginator
    * @description An instance of YAHOO.widget.Paginator.
    * @default null
    * @type {Object|YAHOO.widget.Paginator}
    */
    this.setAttributeConfig("paginator", {
        value : null,
        validator : function (val) {
            return val === null || val instanceof widget.Paginator;
        },
        method : function () { this._updatePaginator.apply(this,arguments); }
    });

    /**
    * @attribute caption
    * @description Value for the CAPTION element. String values are treated as
    * markup and inserted into the DOM with innerHTML. NB: Not supported in
    * ScrollingDataTable.    
    * @type HTML
    */
    this.setAttributeConfig("caption", {
        value: null,
        validator: lang.isString,
        method: function(sCaption) {
            this._initCaptionEl(sCaption);
        }
    });

    /**
    * @attribute draggableColumns
    * @description True if Columns are draggable to reorder, false otherwise.
    * The Drag & Drop Utility is required to enable this feature. Only top-level
    * and non-nested Columns are draggable. Write once.
    * @default false
    * @type Boolean
    */
    this.setAttributeConfig("draggableColumns", {
        value: false,
        validator: lang.isBoolean,
        method: function(oParam) {
            if(this._elThead) {
                if(oParam) {
                    this._initDraggableColumns();
                }
                else {
                    this._destroyDraggableColumns();
                }
            }
        }
    });

    /**
    * @attribute renderLoopSize      
    * @description A value greater than 0 enables DOM rendering of rows to be
    * executed from a non-blocking timeout queue and sets how many rows to be
    * rendered per timeout. Recommended for very large data sets.     
    * @type Number      
    * @default 0      
    */      
     this.setAttributeConfig("renderLoopSize", {
         value: 0,
         validator: lang.isNumber
     });

    /**
    * @attribute sortFunction
    * @description Default Column sort function, receives the following args:
    *    <dl>
    *      <dt>a {Object}</dt>
    *      <dd>First sort argument.</dd>
    *      <dt>b {Object}</dt>
    *      <dd>Second sort argument.</dd>

    *      <dt>desc {Boolean}</dt>
    *      <dd>True if sort direction is descending, false if
    * sort direction is ascending.</dd>
    *      <dt>field {String}</dt>
    *      <dd>The field to sort by, from sortOptions.field</dd>
    *   </dl>
    * @type function
    */
    this.setAttributeConfig("sortFunction", {
        value: function(a, b, desc, field) {
            var compare = YAHOO.util.Sort.compare,
                sorted = compare(a.getData(field),b.getData(field), desc);
            if(sorted === 0) {
                return compare(a.getCount(),b.getCount(), desc); // Bug 1932978
            }
            else {
                return sorted;
            }
        }
    });

    /**
    * @attribute formatRow
    * @description A function that accepts a TR element and its associated Record
    * for custom formatting. The function must return TRUE in order to automatically
    * continue formatting of child TD elements, else TD elements will not be
    * automatically formatted.
    * @type function
    * @default null
    */
    this.setAttributeConfig("formatRow", {
        value: null,
        validator: lang.isFunction
    });

    /**
    * @attribute generateRequest
    * @description A function that converts an object literal of desired DataTable
    * states into a request value which is then passed to the DataSource's
    * sendRequest method in order to retrieve data for those states. This
    * function is passed an object literal of state data and a reference to the
    * DataTable instance:
    *     
    * <dl>
    *   <dt>pagination<dt>
    *   <dd>        
    *         <dt>offsetRecord</dt>
    *         <dd>{Number} Index of the first Record of the desired page</dd>
    *         <dt>rowsPerPage</dt>
    *         <dd>{Number} Number of rows per page</dd>
    *   </dd>
    *   <dt>sortedBy</dt>
    *   <dd>                
    *         <dt>key</dt>
    *         <dd>{String} Key of sorted Column</dd>
    *         <dt>dir</dt>
    *         <dd>{String} Sort direction, either YAHOO.widget.DataTable.CLASS_ASC or YAHOO.widget.DataTable.CLASS_DESC</dd>
    *   </dd>
    *   <dt>self</dt>
    *   <dd>The DataTable instance</dd>
    * </dl>
    * 
    * and by default returns a String of syntax:
    * "sort={sortColumn}&dir={sortDir}&startIndex={pageStartIndex}&results={rowsPerPage}"
    * @type function
    * @default HTMLFunction
    */
    this.setAttributeConfig("generateRequest", {
        value: function(oState, oSelf) {
            // Set defaults
            oState = oState || {pagination:null, sortedBy:null};
            var sort = encodeURIComponent((oState.sortedBy) ? oState.sortedBy.key : oSelf.getColumnSet().keys[0].getKey());
            var dir = (oState.sortedBy && oState.sortedBy.dir === YAHOO.widget.DataTable.CLASS_DESC) ? "desc" : "asc";
            var startIndex = (oState.pagination) ? oState.pagination.recordOffset : 0;
            var results = (oState.pagination) ? oState.pagination.rowsPerPage : null;
            
            // Build the request
            return  "sort=" + sort +
                    "&dir=" + dir +
                    "&startIndex=" + startIndex +
                    ((results !== null) ? "&results=" + results : "");
        },
        validator: lang.isFunction
    });

    /**
    * @attribute initialRequest
    * @description Defines the initial request that gets sent to the DataSource
    * during initialization. Value is ignored if initialLoad is set to any value
    * other than true.    
    * @type MIXED
    * @default null
    */
    this.setAttributeConfig("initialRequest", {
        value: null
    });

    /**
    * @attribute initialLoad
    * @description Determines whether or not to load data at instantiation. By
    * default, will trigger a sendRequest() to the DataSource and pass in the
    * request defined by initialRequest. If set to false, data will not load
    * at instantiation. Alternatively, implementers who wish to work with a 
    * custom payload may pass in an object literal with the following values:
    *     
    *    <dl>
    *      <dt>request (MIXED)</dt>
    *      <dd>Request value.</dd>
    *
    *      <dt>argument (MIXED)</dt>
    *      <dd>Custom data that will be passed through to the callback function.</dd>
    *    </dl>
    *
    *                    
    * @type Boolean | Object
    * @default true
    */
    this.setAttributeConfig("initialLoad", {
        value: true
    });
    
    /**
    * @attribute dynamicData
    * @description If true, sorting and pagination are relegated to the DataSource
    * for handling, using the request returned by the "generateRequest" function.
    * Each new DataSource response blows away all previous Records. False by default, so 
    * sorting and pagination will be handled directly on the client side, without
    * causing any new requests for data from the DataSource.
    * @type Boolean
    * @default false
    */
    this.setAttributeConfig("dynamicData", {
        value: false,
        validator: lang.isBoolean
    });

    /**
     * @attribute MSG_EMPTY
     * @description Message to display if DataTable has no data. String
     * values are treated as markup and inserted into the DOM with innerHTML.
     * @type HTML
     * @default "No records found."
     */
     this.setAttributeConfig("MSG_EMPTY", {
         value: "No records found.",
         validator: lang.isString
     });      

    /**
     * @attribute MSG_LOADING
     * @description Message to display while DataTable is loading data. String
     * values are treated as markup and inserted into the DOM with innerHTML.
     * @type HTML
     * @default "Loading..."
     */      
     this.setAttributeConfig("MSG_LOADING", {
         value: "Loading...",
         validator: lang.isString
     });      

    /**
     * @attribute MSG_ERROR
     * @description Message to display while DataTable has data error. String
     * values are treated as markup and inserted into the DOM with innerHTML.
     * @type HTML
     * @default "Data error."
     */      
     this.setAttributeConfig("MSG_ERROR", {
         value: "Data error.",
         validator: lang.isString
     });

    /**
     * @attribute MSG_SORTASC
     * @description Message to display in tooltip to sort Column in ascending
     * order. String values are treated as markup and inserted into the DOM as
     * innerHTML.
     * @type HTML
     * @default "Click to sort ascending"
     */      
     this.setAttributeConfig("MSG_SORTASC", {      
         value: "Click to sort ascending",      
         validator: lang.isString,
         method: function(sParam) {
            if(this._elThead) {
                for(var i=0, allKeys=this.getColumnSet().keys, len=allKeys.length; i<len; i++) {
                    if(allKeys[i].sortable && this.getColumnSortDir(allKeys[i]) === DT.CLASS_ASC) {
                        allKeys[i]._elThLabel.firstChild.title = sParam;
                    }
                }
            }      
         }
     });

    /**
     * @attribute MSG_SORTDESC
     * @description Message to display in tooltip to sort Column in descending
     * order. String values are treated as markup and inserted into the DOM as
     * innerHTML.
     * @type HTML
     * @default "Click to sort descending"
     */      
     this.setAttributeConfig("MSG_SORTDESC", {      
         value: "Click to sort descending",      
         validator: lang.isString,
         method: function(sParam) {
            if(this._elThead) {
                for(var i=0, allKeys=this.getColumnSet().keys, len=allKeys.length; i<len; i++) {
                    if(allKeys[i].sortable && this.getColumnSortDir(allKeys[i]) === DT.CLASS_DESC) {
                        allKeys[i]._elThLabel.firstChild.title = sParam;
                    }
                }
            }               
         }
     });
     
    /**
     * @attribute currencySymbol
     * @deprecated Use currencyOptions.
     */
    this.setAttributeConfig("currencySymbol", {
        value: "$",
        validator: lang.isString
    });
    
    /**
     * Default config passed to YAHOO.util.Number.format() by the 'currency' Column formatter.
     * @attribute currencyOptions
     * @type Object
     * @default {prefix: $, decimalPlaces:2, decimalSeparator:".", thousandsSeparator:","}
     */
    this.setAttributeConfig("currencyOptions", {
        value: {
            prefix: this.get("currencySymbol"), // TODO: deprecate currencySymbol
            decimalPlaces:2,
            decimalSeparator:".",
            thousandsSeparator:","
        }
    });
    
    /**
     * Default config passed to YAHOO.util.Date.format() by the 'date' Column formatter.
     * @attribute dateOptions
     * @type Object
     * @default {format:"%m/%d/%Y", locale:"en"}
     */
    this.setAttributeConfig("dateOptions", {
        value: {format:"%m/%d/%Y", locale:"en"}
    });
    
    /**
     * Default config passed to YAHOO.util.Number.format() by the 'number' Column formatter.
     * @attribute numberOptions
     * @type Object
     * @default {decimalPlaces:0, thousandsSeparator:","}
     */
    this.setAttributeConfig("numberOptions", {
        value: {
            decimalPlaces:0,
            thousandsSeparator:","
        }
    });

},

/////////////////////////////////////////////////////////////////////////////
//
// Private member variables
//
/////////////////////////////////////////////////////////////////////////////

/**
 * True if instance is initialized, so as to fire the initEvent after render.
 *
 * @property _bInit
 * @type Boolean
 * @default true
 * @private
 */
_bInit : true,

/**
 * Index assigned to instance.
 *
 * @property _nIndex
 * @type Number
 * @private
 */
_nIndex : null,

/**
 * Counter for IDs assigned to TR elements.
 *
 * @property _nTrCount
 * @type Number
 * @private
 */
_nTrCount : 0,

/**
 * Counter for IDs assigned to TD elements.
 *
 * @property _nTdCount
 * @type Number
 * @private
 */
_nTdCount : 0,

/**
 * Unique id assigned to instance "yui-dtN", useful prefix for generating unique
 * DOM ID strings and log messages.
 *
 * @property _sId
 * @type String
 * @private
 */
_sId : null,

/**
 * Render chain.
 *
 * @property _oChainRender
 * @type YAHOO.util.Chain
 * @private
 */
_oChainRender : null,

/**
 * DOM reference to the container element for the DataTable instance into which
 * all other elements get created.
 *
 * @property _elContainer
 * @type HTMLElement
 * @private
 */
_elContainer : null,

/**
 * DOM reference to the mask element for the DataTable instance which disables it.
 *
 * @property _elMask
 * @type HTMLElement
 * @private
 */
_elMask : null,

/**
 * DOM reference to the TABLE element for the DataTable instance.
 *
 * @property _elTable
 * @type HTMLElement
 * @private
 */
_elTable : null,

/**
 * DOM reference to the CAPTION element for the DataTable instance.
 *
 * @property _elCaption
 * @type HTMLElement
 * @private
 */
_elCaption : null,

/**
 * DOM reference to the COLGROUP element for the DataTable instance.
 *
 * @property _elColgroup
 * @type HTMLElement
 * @private
 */
_elColgroup : null,

/**
 * DOM reference to the THEAD element for the DataTable instance.
 *
 * @property _elThead
 * @type HTMLElement
 * @private
 */
_elThead : null,

/**
 * DOM reference to the primary TBODY element for the DataTable instance.
 *
 * @property _elTbody
 * @type HTMLElement
 * @private
 */
_elTbody : null,

/**
 * DOM reference to the secondary TBODY element used to display DataTable messages.
 *
 * @property _elMsgTbody
 * @type HTMLElement
 * @private
 */
_elMsgTbody : null,

/**
 * DOM reference to the secondary TBODY element's single TR element used to display DataTable messages.
 *
 * @property _elMsgTr
 * @type HTMLElement
 * @private
 */
_elMsgTr : null,

/**
 * DOM reference to the secondary TBODY element's single TD element used to display DataTable messages.
 *
 * @property _elMsgTd
 * @type HTMLElement
 * @private
 */
_elMsgTd : null,

/**
 * Element reference to shared Column drag target.
 *
 * @property _elColumnDragTarget
 * @type HTMLElement
 * @private
 */
_elColumnDragTarget : null,

/**
 * Element reference to shared Column resizer proxy.
 *
 * @property _elColumnResizerProxy
 * @type HTMLElement
 * @private
 */
_elColumnResizerProxy : null,

/**
 * DataSource instance for the DataTable instance.
 *
 * @property _oDataSource
 * @type YAHOO.util.DataSource
 * @private
 */
_oDataSource : null,

/**
 * ColumnSet instance for the DataTable instance.
 *
 * @property _oColumnSet
 * @type YAHOO.widget.ColumnSet
 * @private
 */
_oColumnSet : null,

/**
 * RecordSet instance for the DataTable instance.
 *
 * @property _oRecordSet
 * @type YAHOO.widget.RecordSet
 * @private
 */
_oRecordSet : null,

/**
 * The active CellEditor instance for the DataTable instance.
 *
 * @property _oCellEditor
 * @type YAHOO.widget.CellEditor
 * @private
 */
_oCellEditor : null,

/**
 * ID string of first TR element of the current DataTable page.
 *
 * @property _sFirstTrId
 * @type String
 * @private
 */
_sFirstTrId : null,

/**
 * ID string of the last TR element of the current DataTable page.
 *
 * @property _sLastTrId
 * @type String
 * @private
 */
_sLastTrId : null,

/**
 * Template row to create all new rows from.
 * @property _elTrTemplate
 * @type {HTMLElement}
 * @private 
 */
_elTrTemplate : null,

/**
 * Sparse array of custom functions to set column widths for browsers that don't
 * support dynamic CSS rules.  Functions are added at the index representing
 * the number of rows they update.
 *
 * @property _aDynFunctions
 * @type Array
 * @private
 */
_aDynFunctions : [],

/**
 * Disabled state.
 *
 * @property _disabled
 * @type Boolean
 * @private
 */
_disabled : false,




























/////////////////////////////////////////////////////////////////////////////
//
// Private methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Clears browser text selection. Useful to call on rowSelectEvent or
 * cellSelectEvent to prevent clicks or dblclicks from selecting text in the
 * browser.
 *
 * @method clearTextSelection
 */
clearTextSelection : function() {
    var sel;
    if(window.getSelection) {
        sel = window.getSelection();
    }
    else if(document.getSelection) {
        sel = document.getSelection();
    }
    else if(document.selection) {
        sel = document.selection;
    }
    if(sel) {
        if(sel.empty) {
            sel.empty();
        }
        else if (sel.removeAllRanges) {
            sel.removeAllRanges();
        }
        else if(sel.collapse) {
            sel.collapse();
        }
    }
},

/**
 * Sets focus on the given element.
 *
 * @method _focusEl
 * @param el {HTMLElement} Element.
 * @private
 */
_focusEl : function(el) {
    el = el || this._elTbody;
    // http://developer.mozilla.org/en/docs/index.php?title=Key-navigable_custom_DHTML_widgets
    // The timeout is necessary in both IE and Firefox 1.5, to prevent scripts from doing
    // strange unexpected things as the user clicks on buttons and other controls.
    setTimeout(function() {
        try {
            el.focus();
        }
        catch(e) {
        }
    },0);
},

/**
 * Forces Gecko repaint.
 *
 * @method _repaintGecko
 * @el {HTMLElement} (Optional) Element to repaint, otherwise entire document body.
 * @private
 */
_repaintGecko : (ua.gecko) ? 
    function(el) {
        el = el || this._elContainer;
        var parent = el.parentNode;
        var nextSibling = el.nextSibling;
        parent.insertBefore(parent.removeChild(el), nextSibling);
    } : function() {},

/**
 * Forces Opera repaint.
 *
 * @method _repaintOpera
 * @private 
 */
_repaintOpera : (ua.opera) ? 
    function() {
        if(ua.opera) {
            document.documentElement.className += " ";
            document.documentElement.className = YAHOO.lang.trim(document.documentElement.className);
        }
    } : function() {} ,

/**
 * Forces Webkit repaint.
 *
 * @method _repaintWebkit
 * @el {HTMLElement} (Optional) Element to repaint, otherwise entire document body.
 * @private
 */
_repaintWebkit : (ua.webkit) ? 
    function(el) {
        el = el || this._elContainer;
        var parent = el.parentNode;
        var nextSibling = el.nextSibling;
        parent.insertBefore(parent.removeChild(el), nextSibling);
    } : function() {},






















// INIT FUNCTIONS

/**
 * Initializes object literal of config values.
 *
 * @method _initConfigs
 * @param oConfig {Object} Object literal of config values.
 * @private
 */
_initConfigs : function(oConfigs) {
    if(!oConfigs || !lang.isObject(oConfigs)) {
        oConfigs = {};
    }
    this.configs = oConfigs;
},

/**
 * Initializes ColumnSet.
 *
 * @method _initColumnSet
 * @param aColumnDefs {Object[]} Array of object literal Column definitions.
 * @private
 */
_initColumnSet : function(aColumnDefs) {
    var oColumn, i, len;
    
    if(this._oColumnSet) {
        // First clear _oDynStyles for existing ColumnSet and
        // uregister CellEditor Custom Events
        for(i=0, len=this._oColumnSet.keys.length; i<len; i++) {
            oColumn = this._oColumnSet.keys[i];
            DT._oDynStyles["."+this.getId()+"-col-"+oColumn.getSanitizedKey()+" ."+DT.CLASS_LINER] = undefined;
            if(oColumn.editor && oColumn.editor.unsubscribeAll) { // Backward compatibility
                oColumn.editor.unsubscribeAll();
            }
        }
        
        this._oColumnSet = null;
        this._clearTrTemplateEl();
    }
    
    if(lang.isArray(aColumnDefs)) {
        this._oColumnSet =  new YAHOO.widget.ColumnSet(aColumnDefs);
    }
    // Backward compatibility
    else if(aColumnDefs instanceof YAHOO.widget.ColumnSet) {
        this._oColumnSet =  aColumnDefs;
        YAHOO.log("DataTable's constructor now requires an array" +
        " of object literal Column definitions instead of a ColumnSet instance",
        "warn", this.toString());
    }

    // Register CellEditor Custom Events
    var allKeys = this._oColumnSet.keys;
    for(i=0, len=allKeys.length; i<len; i++) {
        oColumn = allKeys[i];
        if(oColumn.editor && oColumn.editor.subscribe) { // Backward incompatibility
            oColumn.editor.subscribe("showEvent", this._onEditorShowEvent, this, true);
            oColumn.editor.subscribe("keydownEvent", this._onEditorKeydownEvent, this, true);
            oColumn.editor.subscribe("revertEvent", this._onEditorRevertEvent, this, true);
            oColumn.editor.subscribe("saveEvent", this._onEditorSaveEvent, this, true);
            oColumn.editor.subscribe("cancelEvent", this._onEditorCancelEvent, this, true);
            oColumn.editor.subscribe("blurEvent", this._onEditorBlurEvent, this, true);
            oColumn.editor.subscribe("blockEvent", this._onEditorBlockEvent, this, true);
            oColumn.editor.subscribe("unblockEvent", this._onEditorUnblockEvent, this, true);
        }
    }
},

/**
 * Initializes DataSource.
 *
 * @method _initDataSource
 * @param oDataSource {YAHOO.util.DataSource} DataSource instance.
 * @private
 */
_initDataSource : function(oDataSource) {
    this._oDataSource = null;
    if(oDataSource && (lang.isFunction(oDataSource.sendRequest))) {
        this._oDataSource = oDataSource;
    }
    // Backward compatibility
    else {
        var tmpTable = null;
        var tmpContainer = this._elContainer;
        var i=0;
        //TODO: this will break if re-initing DS at runtime for SDT
        // Peek in container child nodes to see if TABLE already exists
        if(tmpContainer.hasChildNodes()) {
            var tmpChildren = tmpContainer.childNodes;
            for(i=0; i<tmpChildren.length; i++) {
                if(tmpChildren[i].nodeName && tmpChildren[i].nodeName.toLowerCase() == "table") {
                    tmpTable = tmpChildren[i];
                    break;
                }
            }
            if(tmpTable) {
                var tmpFieldsArray = [];
                for(; i<this._oColumnSet.keys.length; i++) {
                    tmpFieldsArray.push({key:this._oColumnSet.keys[i].key});
                }

                this._oDataSource = new DS(tmpTable);
                this._oDataSource.responseType = DS.TYPE_HTMLTABLE;
                this._oDataSource.responseSchema = {fields: tmpFieldsArray};
                YAHOO.log("Null DataSource for progressive enhancement from" +
                " markup has been deprecated", "warn", this.toString());
            }
        }
    }
},

/**
 * Initializes RecordSet.
 *
 * @method _initRecordSet
 * @private
 */
_initRecordSet : function() {
    if(this._oRecordSet) {
        this._oRecordSet.reset();
    }
    else {
        this._oRecordSet = new YAHOO.widget.RecordSet();
    }
},

/**
 * Initializes DOM elements.
 *
 * @method _initDomElements
 * @param elContainer {HTMLElement | String} HTML DIV element by reference or ID. 
 * return {Boolean} False in case of error, otherwise true 
 * @private
 */
_initDomElements : function(elContainer) {
    // Outer container
    this._initContainerEl(elContainer);
    // TABLE
    this._initTableEl(this._elContainer);
    // COLGROUP
    this._initColgroupEl(this._elTable);
    // THEAD
    this._initTheadEl(this._elTable);
    
    // Message TBODY
    this._initMsgTbodyEl(this._elTable);  

    // Primary TBODY
    this._initTbodyEl(this._elTable);

    if(!this._elContainer || !this._elTable || !this._elColgroup ||  !this._elThead || !this._elTbody || !this._elMsgTbody) {
        return false;
    }
    else {
        return true;
    }
},

/**
 * Destroy's the DataTable outer container element, if available.
 *
 * @method _destroyContainerEl
 * @param elContainer {HTMLElement} Reference to the container element. 
 * @private
 */
_destroyContainerEl : function(elContainer) {
        var columns = this._oColumnSet.keys,
        elements, i;

        Dom.removeClass(elContainer, DT.CLASS_DATATABLE);

    // Bug 2528783
    Ev.purgeElement( elContainer );
    Ev.purgeElement( this._elThead, true ); // recursive to get resize handles
    Ev.purgeElement( this._elTbody );
    Ev.purgeElement( this._elMsgTbody );

    // because change doesn't bubble, each select (via formatDropdown) gets
    // its own subscription
    elements = elContainer.getElementsByTagName( 'select' );

    if ( elements.length ) {
        Ev.detachListener( elements, 'change' );
    }

    for ( i = columns.length - 1; i >= 0; --i ) {
        if ( columns[i].editor ) {
            Ev.purgeElement( columns[i].editor._elContainer );
        }
    }

    elContainer.innerHTML = "";
    
    this._elContainer = null;
    this._elColgroup = null;
    this._elThead = null;
    this._elTbody = null;
},

/**
 * Initializes the DataTable outer container element, including a mask.
 *
 * @method _initContainerEl
 * @param elContainer {HTMLElement | String} HTML DIV element by reference or ID.
 * @private
 */
_initContainerEl : function(elContainer) {
    // Validate container
    elContainer = Dom.get(elContainer);
    
    if(elContainer && elContainer.nodeName && (elContainer.nodeName.toLowerCase() == "div")) {
        // Destroy previous
        this._destroyContainerEl(elContainer);

        Dom.addClass(elContainer, DT.CLASS_DATATABLE);
        Ev.addListener(elContainer, "focus", this._onTableFocus, this);
        Ev.addListener(elContainer, "dblclick", this._onTableDblclick, this);
        this._elContainer = elContainer;
        
        var elMask = document.createElement("div");
        elMask.className = DT.CLASS_MASK;
        elMask.style.display = "none";
        this._elMask = elContainer.appendChild(elMask);
    }
},

/**
 * Destroy's the DataTable TABLE element, if available.
 *
 * @method _destroyTableEl
 * @private
 */
_destroyTableEl : function() {
    var elTable = this._elTable;
    if(elTable) {
        Ev.purgeElement(elTable, true);
        elTable.parentNode.removeChild(elTable);
        this._elCaption = null;
        this._elColgroup = null;
        this._elThead = null;
        this._elTbody = null;
    }
},

/**
 * Creates HTML markup CAPTION element.
 *
 * @method _initCaptionEl
 * @param sCaption {HTML} Caption value. String values are treated as markup and
 * inserted into the DOM with innerHTML.
 * @private
 */
_initCaptionEl : function(sCaption) {
    if(this._elTable && sCaption) {
        // Create CAPTION element
        if(!this._elCaption) { 
            this._elCaption = this._elTable.createCaption();
        }
        // Set CAPTION value
        this._elCaption.innerHTML = sCaption;
    }
    else if(this._elCaption) {
        this._elCaption.parentNode.removeChild(this._elCaption);
    }
},

/**
 * Creates HTML markup for TABLE, COLGROUP, THEAD and TBODY elements in outer
 * container element.
 *
 * @method _initTableEl
 * @param elContainer {HTMLElement} Container element into which to create TABLE.
 * @private
 */
_initTableEl : function(elContainer) {
    if(elContainer) {
        // Destroy previous
        this._destroyTableEl();
    
        // Create TABLE
        this._elTable = elContainer.appendChild(document.createElement("table"));  
         
        // Set SUMMARY attribute
        this._elTable.summary = this.get("summary");
        
        // Create CAPTION element
        if(this.get("caption")) {
            this._initCaptionEl(this.get("caption"));
        }

        // Set up mouseover/mouseout events via mouseenter/mouseleave delegation
        Ev.delegate(this._elTable, "mouseenter", this._onTableMouseover, "thead ."+DT.CLASS_LABEL, this);
        Ev.delegate(this._elTable, "mouseleave", this._onTableMouseout, "thead ."+DT.CLASS_LABEL, this);
        Ev.delegate(this._elTable, "mouseenter", this._onTableMouseover, "tbody.yui-dt-data>tr>td", this);
        Ev.delegate(this._elTable, "mouseleave", this._onTableMouseout, "tbody.yui-dt-data>tr>td", this);
        Ev.delegate(this._elTable, "mouseenter", this._onTableMouseover, "tbody.yui-dt-message>tr>td", this);
        Ev.delegate(this._elTable, "mouseleave", this._onTableMouseout, "tbody.yui-dt-message>tr>td", this);
    }
},

/**
 * Destroy's the DataTable COLGROUP element, if available.
 *
 * @method _destroyColgroupEl
 * @private
 */
_destroyColgroupEl : function() {
    var elColgroup = this._elColgroup;
    if(elColgroup) {
        var elTable = elColgroup.parentNode;
        Ev.purgeElement(elColgroup, true);
        elTable.removeChild(elColgroup);
        this._elColgroup = null;
    }
},

/**
 * Initializes COLGROUP and COL elements for managing minWidth.
 *
 * @method _initColgroupEl
 * @param elTable {HTMLElement} TABLE element into which to create COLGROUP.
 * @private
 */
_initColgroupEl : function(elTable) {
    if(elTable) {
        // Destroy previous
        this._destroyColgroupEl();

        // Add COLs to DOCUMENT FRAGMENT
        var allCols = this._aColIds || [],
            allKeys = this._oColumnSet.keys,
            i = 0, len = allCols.length,
            elCol, oColumn,
            elFragment = document.createDocumentFragment(),
            elColTemplate = document.createElement("col");
    
        for(i=0,len=allKeys.length; i<len; i++) {
            oColumn = allKeys[i];
            elCol = elFragment.appendChild(elColTemplate.cloneNode(false));
        }
    
        // Create COLGROUP
        var elColgroup = elTable.insertBefore(document.createElement("colgroup"), elTable.firstChild);
        elColgroup.appendChild(elFragment);
        this._elColgroup = elColgroup;
    }
},

/**
 * Adds a COL element to COLGROUP at given index.
 *
 * @method _insertColgroupColEl
 * @param index {Number} Index of new COL element.
 * @private
 */
_insertColgroupColEl : function(index) {
    if(lang.isNumber(index)&& this._elColgroup) {
        var nextSibling = this._elColgroup.childNodes[index] || null;
        this._elColgroup.insertBefore(document.createElement("col"), nextSibling);
    }
},

/**
 * Removes a COL element to COLGROUP at given index.
 *
 * @method _removeColgroupColEl
 * @param index {Number} Index of removed COL element.
 * @private
 */
_removeColgroupColEl : function(index) {
    if(lang.isNumber(index) && this._elColgroup && this._elColgroup.childNodes[index]) {
        this._elColgroup.removeChild(this._elColgroup.childNodes[index]);
    }
},

/**
 * Reorders a COL element from old index(es) to new index.
 *
 * @method _reorderColgroupColEl
 * @param aKeyIndexes {Number[]} Array of indexes of removed COL element.
 * @param newIndex {Number} New index. 
 * @private
 */
_reorderColgroupColEl : function(aKeyIndexes, newIndex) {
    if(lang.isArray(aKeyIndexes) && lang.isNumber(newIndex) && this._elColgroup && (this._elColgroup.childNodes.length > aKeyIndexes[aKeyIndexes.length-1])) {
        var i,
            tmpCols = [];
        // Remove COL
        for(i=aKeyIndexes.length-1; i>-1; i--) {
            tmpCols.push(this._elColgroup.removeChild(this._elColgroup.childNodes[aKeyIndexes[i]]));
        }
        // Insert COL
        var nextSibling = this._elColgroup.childNodes[newIndex] || null;
        for(i=tmpCols.length-1; i>-1; i--) {
            this._elColgroup.insertBefore(tmpCols[i], nextSibling);
        }
    }
},

/**
 * Destroy's the DataTable THEAD element, if available.
 *
 * @method _destroyTheadEl
 * @private
 */
_destroyTheadEl : function() {
    var elThead = this._elThead;
    if(elThead) {
        var elTable = elThead.parentNode;
        Ev.purgeElement(elThead, true);
        this._destroyColumnHelpers();
        elTable.removeChild(elThead);
        this._elThead = null;
    }
},

/**
 * Initializes THEAD element.
 *
 * @method _initTheadEl
 * @param elTable {HTMLElement} TABLE element into which to create COLGROUP.
 * @param {HTMLElement} Initialized THEAD element. 
 * @private
 */
_initTheadEl : function(elTable) {
    elTable = elTable || this._elTable;
    
    if(elTable) {
        // Destroy previous
        this._destroyTheadEl();
    
        //TODO: append to DOM later for performance
        var elThead = (this._elColgroup) ?
            elTable.insertBefore(document.createElement("thead"), this._elColgroup.nextSibling) :
            elTable.appendChild(document.createElement("thead"));
    
        // Set up DOM events for THEAD
        Ev.addListener(elThead, "focus", this._onTheadFocus, this);
        Ev.addListener(elThead, "keydown", this._onTheadKeydown, this);
        Ev.addListener(elThead, "mousedown", this._onTableMousedown, this);
        Ev.addListener(elThead, "mouseup", this._onTableMouseup, this);
        Ev.addListener(elThead, "click", this._onTheadClick, this);
        
        // Bug 2528073: mouseover/mouseout handled via mouseenter/mouseleave
        // delegation at the TABLE level

        // Since we can't listen for click and dblclick on the same element...
        // Attach separately to THEAD and TBODY
        ///Ev.addListener(elThead, "dblclick", this._onTableDblclick, this);
        
       var oColumnSet = this._oColumnSet,
            oColumn, i,j, l;
        
        // Add TRs to the THEAD
        var colTree = oColumnSet.tree;
        var elTh;
        for(i=0; i<colTree.length; i++) {
            var elTheadTr = elThead.appendChild(document.createElement("tr"));
    
            // ...and create TH cells
            for(j=0; j<colTree[i].length; j++) {
                oColumn = colTree[i][j];
                elTh = elTheadTr.appendChild(document.createElement("th"));
                this._initThEl(elTh,oColumn);
            }
    
                // Set FIRST/LAST on THEAD rows
                if(i === 0) {
                    Dom.addClass(elTheadTr, DT.CLASS_FIRST);
                }
                if(i === (colTree.length-1)) {
                    Dom.addClass(elTheadTr, DT.CLASS_LAST);
                }

        }

        // Set FIRST/LAST on edge TH elements using the values in ColumnSet headers array
        var aFirstHeaders = oColumnSet.headers[0] || [];
        for(i=0; i<aFirstHeaders.length; i++) {
            Dom.addClass(Dom.get(this.getId() +"-th-"+aFirstHeaders[i]), DT.CLASS_FIRST);
        }
        var aLastHeaders = oColumnSet.headers[oColumnSet.headers.length-1] || [];
        for(i=0; i<aLastHeaders.length; i++) {
            Dom.addClass(Dom.get(this.getId() +"-th-"+aLastHeaders[i]), DT.CLASS_LAST);
        }
        
        YAHOO.log("TH cells for " + this._oColumnSet.keys.length + " keys created","info",this.toString());

        ///TODO: try _repaintGecko(this._elContainer) instead
        // Bug 1806891
        if(ua.webkit && ua.webkit < 420) {
            var oSelf = this;
            setTimeout(function() {
                elThead.style.display = "";
            },0);
            elThead.style.display = 'none';
        }
        
        this._elThead = elThead;
        
        // Column helpers needs _elThead to exist
        this._initColumnHelpers();  
    }
},

/**
 * Populates TH element as defined by Column.
 *
 * @method _initThEl
 * @param elTh {HTMLElement} TH element reference.
 * @param oColumn {YAHOO.widget.Column} Column object.
 * @private
 */
_initThEl : function(elTh, oColumn) {
    elTh.id = this.getId() + "-th-" + oColumn.getSanitizedKey(); // Needed for accessibility, getColumn by TH, and ColumnDD
    elTh.innerHTML = "";
    elTh.rowSpan = oColumn.getRowspan();
    elTh.colSpan = oColumn.getColspan();
    oColumn._elTh = elTh;
    
    var elThLiner = elTh.appendChild(document.createElement("div"));
    elThLiner.id = elTh.id + "-liner"; // Needed for resizer
    elThLiner.className = DT.CLASS_LINER;
    oColumn._elThLiner = elThLiner;
    
    var elThLabel = elThLiner.appendChild(document.createElement("span"));
    elThLabel.className = DT.CLASS_LABEL;    

    // Assign abbr attribute
    if(oColumn.abbr) {
        elTh.abbr = oColumn.abbr;
    }
    // Clear minWidth on hidden Columns
    if(oColumn.hidden) {
        this._clearMinWidth(oColumn);
    }
        
    elTh.className = this._getColumnClassNames(oColumn);
            
    // Set Column width...
    if(oColumn.width) {
        // Validate minWidth
        var nWidth = (oColumn.minWidth && (oColumn.width < oColumn.minWidth)) ?
                oColumn.minWidth : oColumn.width;
        // ...for fallback cases
        if(DT._bDynStylesFallback) {
            elTh.firstChild.style.overflow = 'hidden';
            elTh.firstChild.style.width = nWidth + 'px';        
        }
        // ...for non fallback cases
        else {
            this._setColumnWidthDynStyles(oColumn, nWidth + 'px', 'hidden');
        }
    }

    this.formatTheadCell(elThLabel, oColumn, this.get("sortedBy"));
    oColumn._elThLabel = elThLabel;
},

/**
 * Outputs markup into the given TH based on given Column.
 *
 * @method formatTheadCell
 * @param elCellLabel {HTMLElement} The label SPAN element within the TH liner,
 * not the liner DIV element.     
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param oSortedBy {Object} Sort state object literal.
*/
formatTheadCell : function(elCellLabel, oColumn, oSortedBy) {
    var sKey = oColumn.getKey();
    var sLabel = lang.isValue(oColumn.label) ? oColumn.label : sKey;

    // Add accessibility link for sortable Columns
    if(oColumn.sortable) {
        // Calculate the direction
        var sSortClass = this.getColumnSortDir(oColumn, oSortedBy);
        var bDesc = (sSortClass === DT.CLASS_DESC);

        // This is the sorted Column
        if(oSortedBy && (oColumn.key === oSortedBy.key)) {
            bDesc = !(oSortedBy.dir === DT.CLASS_DESC);
        }

        // Generate a unique HREF for visited status
        var sHref = this.getId() + "-href-" + oColumn.getSanitizedKey();
        
        // Generate a dynamic TITLE for sort status
        var sTitle = (bDesc) ? this.get("MSG_SORTDESC") : this.get("MSG_SORTASC");
        
        // Format the element
        elCellLabel.innerHTML = "<a href=\"" + sHref + "\" title=\"" + sTitle + "\" class=\"" + DT.CLASS_SORTABLE + "\">" + sLabel + "</a>";
    }
    // Just display the label for non-sortable Columns
    else {
        elCellLabel.innerHTML = sLabel;
    }
},

/**
 * Disables DD from top-level Column TH elements.
 *
 * @method _destroyDraggableColumns
 * @private
 */
_destroyDraggableColumns : function() {
    var oColumn, elTh;
    for(var i=0, len=this._oColumnSet.tree[0].length; i<len; i++) {
        oColumn = this._oColumnSet.tree[0][i];
        if(oColumn._dd) {
            oColumn._dd = oColumn._dd.unreg();
            Dom.removeClass(oColumn.getThEl(), DT.CLASS_DRAGGABLE);       
        }
    }
    
    // Destroy column drag proxy
    this._destroyColumnDragTargetEl();
},

/**
 * Initializes top-level Column TH elements into DD instances.
 *
 * @method _initDraggableColumns
 * @private
 */
_initDraggableColumns : function() {
    this._destroyDraggableColumns();
    if(util.DD) {
        var oColumn, elTh, elDragTarget;
        for(var i=0, len=this._oColumnSet.tree[0].length; i<len; i++) {
            oColumn = this._oColumnSet.tree[0][i];
            elTh = oColumn.getThEl();
            Dom.addClass(elTh, DT.CLASS_DRAGGABLE);
            elDragTarget = this._initColumnDragTargetEl();
            oColumn._dd = new YAHOO.widget.ColumnDD(this, oColumn, elTh, elDragTarget);
        }
    }
    else {
        YAHOO.log("Could not find DragDrop for draggable Columns", "warn", this.toString());
    }
},

/**
 * Destroys shared Column drag target.
 *
 * @method _destroyColumnDragTargetEl
 * @private
 */
_destroyColumnDragTargetEl : function() {
    if(this._elColumnDragTarget) {
        var el = this._elColumnDragTarget;
        YAHOO.util.Event.purgeElement(el);
        el.parentNode.removeChild(el);
        this._elColumnDragTarget = null;
    }
},

/**
 * Creates HTML markup for shared Column drag target.
 *
 * @method _initColumnDragTargetEl
 * @return {HTMLElement} Reference to Column drag target.
 * @private
 */
_initColumnDragTargetEl : function() {
    if(!this._elColumnDragTarget) {
        // Attach Column drag target element as first child of body
        var elColumnDragTarget = document.createElement('div');
        elColumnDragTarget.id = this.getId() + "-coltarget";
        elColumnDragTarget.className = DT.CLASS_COLTARGET;
        elColumnDragTarget.style.display = "none";
        document.body.insertBefore(elColumnDragTarget, document.body.firstChild);

        // Internal tracker of Column drag target
        this._elColumnDragTarget = elColumnDragTarget;

    }
    return this._elColumnDragTarget;
},

/**
 * Disables resizeability on key Column TH elements.
 *
 * @method _destroyResizeableColumns
 * @private
 */
_destroyResizeableColumns : function() {
    var aKeys = this._oColumnSet.keys;
    for(var i=0, len=aKeys.length; i<len; i++) {
        if(aKeys[i]._ddResizer) {
            aKeys[i]._ddResizer = aKeys[i]._ddResizer.unreg();
            Dom.removeClass(aKeys[i].getThEl(), DT.CLASS_RESIZEABLE);
        }
    }

    // Destroy resizer proxy
    this._destroyColumnResizerProxyEl();
},

/**
 * Initializes resizeability on key Column TH elements.
 *
 * @method _initResizeableColumns
 * @private
 */
_initResizeableColumns : function() {
    this._destroyResizeableColumns();
    if(util.DD) {
        var oColumn, elTh, elThLiner, elThResizerLiner, elThResizer, elResizerProxy, cancelClick;
        for(var i=0, len=this._oColumnSet.keys.length; i<len; i++) {
            oColumn = this._oColumnSet.keys[i];
            if(oColumn.resizeable) {
                elTh = oColumn.getThEl();
                Dom.addClass(elTh, DT.CLASS_RESIZEABLE);
                elThLiner = oColumn.getThLinerEl();
                
                // Bug 1915349: So resizer is as tall as TH when rowspan > 1
                // Create a separate resizer liner with position:relative
                elThResizerLiner = elTh.appendChild(document.createElement("div"));
                elThResizerLiner.className = DT.CLASS_RESIZERLINER;
                
                // Move TH contents into the new resizer liner
                elThResizerLiner.appendChild(elThLiner);
                
                // Create the resizer
                elThResizer = elThResizerLiner.appendChild(document.createElement("div"));
                elThResizer.id = elTh.id + "-resizer"; // Needed for ColumnResizer
                elThResizer.className = DT.CLASS_RESIZER;
                oColumn._elResizer = elThResizer;

                // Create the resizer proxy, once per instance
                elResizerProxy = this._initColumnResizerProxyEl();
                oColumn._ddResizer = new YAHOO.util.ColumnResizer(
                        this, oColumn, elTh, elThResizer, elResizerProxy);
                cancelClick = function(e) {
                    Ev.stopPropagation(e);
                };
                Ev.addListener(elThResizer,"click",cancelClick);
            }
        }
    }
    else {
        YAHOO.log("Could not find DragDrop for resizeable Columns", "warn", this.toString());
    }
},

/**
 * Destroys shared Column resizer proxy.
 *
 * @method _destroyColumnResizerProxyEl
 * @return {HTMLElement} Reference to Column resizer proxy.
 * @private
 */
_destroyColumnResizerProxyEl : function() {
    if(this._elColumnResizerProxy) {
        var el = this._elColumnResizerProxy;
        YAHOO.util.Event.purgeElement(el);
        el.parentNode.removeChild(el);
        this._elColumnResizerProxy = null;
    }
},

/**
 * Creates HTML markup for shared Column resizer proxy.
 *
 * @method _initColumnResizerProxyEl
 * @return {HTMLElement} Reference to Column resizer proxy.
 * @private
 */
_initColumnResizerProxyEl : function() {
    if(!this._elColumnResizerProxy) {
        // Attach Column resizer element as first child of body
        var elColumnResizerProxy = document.createElement("div");
        elColumnResizerProxy.id = this.getId() + "-colresizerproxy"; // Needed for ColumnResizer
        elColumnResizerProxy.className = DT.CLASS_RESIZERPROXY;
        document.body.insertBefore(elColumnResizerProxy, document.body.firstChild);

        // Internal tracker of Column resizer proxy
        this._elColumnResizerProxy = elColumnResizerProxy;
    }
    return this._elColumnResizerProxy;
},

/**
 * Destroys elements associated with Column functionality: ColumnDD and ColumnResizers.
 *
 * @method _destroyColumnHelpers
 * @private
 */
_destroyColumnHelpers : function() {
    this._destroyDraggableColumns();
    this._destroyResizeableColumns();
},

/**
 * Initializes elements associated with Column functionality: ColumnDD and ColumnResizers.
 *
 * @method _initColumnHelpers
 * @private
 */
_initColumnHelpers : function() {
    if(this.get("draggableColumns")) {
        this._initDraggableColumns();
    }
    this._initResizeableColumns();
},

/**
 * Destroy's the DataTable TBODY element, if available.
 *
 * @method _destroyTbodyEl
 * @private
 */
_destroyTbodyEl : function() {
    var elTbody = this._elTbody;
    if(elTbody) {
        var elTable = elTbody.parentNode;
        Ev.purgeElement(elTbody, true);
        elTable.removeChild(elTbody);
        this._elTbody = null;
    }
},

/**
 * Initializes TBODY element for data.
 *
 * @method _initTbodyEl
 * @param elTable {HTMLElement} TABLE element into which to create TBODY .
 * @private
 */
_initTbodyEl : function(elTable) {
    if(elTable) {
        // Destroy previous
        this._destroyTbodyEl();
        
        // Create TBODY
        var elTbody = elTable.appendChild(document.createElement("tbody"));
        elTbody.tabIndex = 0;
        elTbody.className = DT.CLASS_DATA;
    
        // Set up DOM events for TBODY
        Ev.addListener(elTbody, "focus", this._onTbodyFocus, this);
        Ev.addListener(elTbody, "mousedown", this._onTableMousedown, this);
        Ev.addListener(elTbody, "mouseup", this._onTableMouseup, this);
        Ev.addListener(elTbody, "keydown", this._onTbodyKeydown, this);
        Ev.addListener(elTbody, "click", this._onTbodyClick, this);

        // Bug 2528073: mouseover/mouseout handled via mouseenter/mouseleave
        // delegation at the TABLE level

        // Since we can't listen for click and dblclick on the same element...
        // Attach separately to THEAD and TBODY
        ///Ev.addListener(elTbody, "dblclick", this._onTableDblclick, this);
        
    
        // IE puts focus outline in the wrong place
        if(ua.ie) {
            elTbody.hideFocus=true;
        }

        this._elTbody = elTbody;
    }
},

/**
 * Destroy's the DataTable message TBODY element, if available.
 *
 * @method _destroyMsgTbodyEl
 * @private
 */
_destroyMsgTbodyEl : function() {
    var elMsgTbody = this._elMsgTbody;
    if(elMsgTbody) {
        var elTable = elMsgTbody.parentNode;
        Ev.purgeElement(elMsgTbody, true);
        elTable.removeChild(elMsgTbody);
        this._elTbody = null;
    }
},

/**
 * Initializes TBODY element for messaging.
 *
 * @method _initMsgTbodyEl
 * @param elTable {HTMLElement} TABLE element into which to create TBODY 
 * @private
 */
_initMsgTbodyEl : function(elTable) {
    if(elTable) {
        var elMsgTbody = document.createElement("tbody");
        elMsgTbody.className = DT.CLASS_MESSAGE;
        var elMsgTr = elMsgTbody.appendChild(document.createElement("tr"));
        elMsgTr.className = DT.CLASS_FIRST + " " + DT.CLASS_LAST;
        this._elMsgTr = elMsgTr;
        var elMsgTd = elMsgTr.appendChild(document.createElement("td"));
        elMsgTd.colSpan = this._oColumnSet.keys.length || 1;
        elMsgTd.className = DT.CLASS_FIRST + " " + DT.CLASS_LAST;
        this._elMsgTd = elMsgTd;
        elMsgTbody = elTable.insertBefore(elMsgTbody, this._elTbody);
        var elMsgLiner = elMsgTd.appendChild(document.createElement("div"));
        elMsgLiner.className = DT.CLASS_LINER;
        this._elMsgTbody = elMsgTbody;

        // Set up DOM events for TBODY
        Ev.addListener(elMsgTbody, "focus", this._onTbodyFocus, this);
        Ev.addListener(elMsgTbody, "mousedown", this._onTableMousedown, this);
        Ev.addListener(elMsgTbody, "mouseup", this._onTableMouseup, this);
        Ev.addListener(elMsgTbody, "keydown", this._onTbodyKeydown, this);
        Ev.addListener(elMsgTbody, "click", this._onTbodyClick, this);

        // Bug 2528073: mouseover/mouseout handled via mouseenter/mouseleave
        // delegation at the TABLE level
    }
},

/**
 * Initialize internal event listeners
 *
 * @method _initEvents
 * @private
 */
_initEvents : function () {
    // Initialize Column sort
    this._initColumnSort();
        
    // Add the document level click listener
    YAHOO.util.Event.addListener(document, "click", this._onDocumentClick, this);

    // Paginator integration
    this.subscribe("paginatorChange",function () {
        this._handlePaginatorChange.apply(this,arguments);
    });

    this.subscribe("initEvent",function () {
        this.renderPaginator();
    });

    // Initialize CellEditor integration
    this._initCellEditing();
},

/**      
  * Initializes Column sorting.      
  *      
  * @method _initColumnSort      
  * @private      
  */      
_initColumnSort : function() {
    this.subscribe("theadCellClickEvent", this.onEventSortColumn);      

    // Backward compatibility
    var oSortedBy = this.get("sortedBy");
    if(oSortedBy) {
        if(oSortedBy.dir == "desc") {
            this._configs.sortedBy.value.dir = DT.CLASS_DESC;
        }
        else if(oSortedBy.dir == "asc") {
            this._configs.sortedBy.value.dir = DT.CLASS_ASC;
        }
    }
},

/**      
  * Initializes CellEditor integration.      
  *      
  * @method _initCellEditing      
  * @private      
  */      
_initCellEditing : function() {
    this.subscribe("editorBlurEvent",function () {
        this.onEditorBlurEvent.apply(this,arguments);
    });
    this.subscribe("editorBlockEvent",function () {
        this.onEditorBlockEvent.apply(this,arguments);
    });
    this.subscribe("editorUnblockEvent",function () {
        this.onEditorUnblockEvent.apply(this,arguments);
    });
},

































// DOM MUTATION FUNCTIONS

/**
 * Retruns classnames to represent current Column states.
 * @method _getColumnClassnames 
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param aAddClasses {String[]} An array of additional classnames to add to the
 * return value.  
 * @return {String} A String of classnames to be assigned to TH or TD elements
 * for given Column.  
 * @private 
 */
_getColumnClassNames : function (oColumn, aAddClasses) {
    var allClasses;
    
    // Add CSS classes
    if(lang.isString(oColumn.className)) {
        // Single custom class
        allClasses = [oColumn.className];
    }
    else if(lang.isArray(oColumn.className)) {
        // Array of custom classes
        allClasses = oColumn.className;
    }
    else {
        // no custom classes
        allClasses = [];
    }
    
    // Hook for setting width with via dynamic style uses key since ID is too disposable
    allClasses[allClasses.length] = this.getId() + "-col-" +oColumn.getSanitizedKey();

    // Column key - minus any chars other than "A-Z", "a-z", "0-9", "_", "-", ".", or ":"
    allClasses[allClasses.length] = "yui-dt-col-" +oColumn.getSanitizedKey();

    var isSortedBy = this.get("sortedBy") || {};
    // Sorted
    if(oColumn.key === isSortedBy.key) {
        allClasses[allClasses.length] = isSortedBy.dir || '';
    }
    // Hidden
    if(oColumn.hidden) {
        allClasses[allClasses.length] = DT.CLASS_HIDDEN;
    }
    // Selected
    if(oColumn.selected) {
        allClasses[allClasses.length] = DT.CLASS_SELECTED;
    }
    // Sortable
    if(oColumn.sortable) {
        allClasses[allClasses.length] = DT.CLASS_SORTABLE;
    }
    // Resizeable
    if(oColumn.resizeable) {
        allClasses[allClasses.length] = DT.CLASS_RESIZEABLE;
    }
    // Editable
    if(oColumn.editor) {
        allClasses[allClasses.length] = DT.CLASS_EDITABLE;
    }
    
    // Addtnl classes, including First/Last
    if(aAddClasses) {
        allClasses = allClasses.concat(aAddClasses);
    }
    
    return allClasses.join(' ');  
},

/**
 * Clears TR element template in response to any Column state change.
 * @method _clearTrTemplateEl
 * @private 
 */
_clearTrTemplateEl : function () {
    this._elTrTemplate = null;
},

/**
 * Returns a new TR element template with TD elements classed with current
 * Column states.
 * @method _getTrTemplateEl 
 * @return {HTMLElement} A TR element to be cloned and added to the DOM.
 * @private 
 */
_getTrTemplateEl : function (oRecord, index) {
    // Template is already available
    if(this._elTrTemplate) {
        return this._elTrTemplate;
    }
    // Template needs to be created
    else {
        var d   = document,
            tr  = d.createElement('tr'),
            td  = d.createElement('td'),
            div = d.createElement('div');
    
        // Append the liner element
        td.appendChild(div);

        // Create TD elements into DOCUMENT FRAGMENT
        var df = document.createDocumentFragment(),
            allKeys = this._oColumnSet.keys,
            elTd;

        // Set state for each TD;
        var aAddClasses;
        for(var i=0, keysLen=allKeys.length; i<keysLen; i++) {
            // Clone the TD template
            elTd = td.cloneNode(true);

            // Format the base TD
            elTd = this._formatTdEl(allKeys[i], elTd, i, (i===keysLen-1));
                        
            df.appendChild(elTd);
        }
        tr.appendChild(df);
        tr.className = DT.CLASS_REC;
        this._elTrTemplate = tr;
        return tr;
    }   
},

/**
 * Formats a basic TD element.
 * @method _formatTdEl 
 * @param oColumn {YAHOO.widget.Column} Associated Column instance. 
 * @param elTd {HTMLElement} An unformatted TD element.
 * @param index {Number} Column key index. 
 * @param isLast {Boolean} True if Column is last key of the ColumnSet.
 * @return {HTMLElement} A formatted TD element.
 * @private 
 */
_formatTdEl : function (oColumn, elTd, index, isLast) {
    var oColumnSet = this._oColumnSet;
    
    // Set the TD's accessibility headers
    var allHeaders = oColumnSet.headers,
        allColHeaders = allHeaders[index],
        sTdHeaders = "",
        sHeader;
    for(var j=0, headersLen=allColHeaders.length; j < headersLen; j++) {
        sHeader = this._sId + "-th-" + allColHeaders[j] + ' ';
        sTdHeaders += sHeader;
    }
    elTd.headers = sTdHeaders;
    
    // Class the TD element
    var aAddClasses = [];
    if(index === 0) {
        aAddClasses[aAddClasses.length] = DT.CLASS_FIRST;
    }
    if(isLast) {
        aAddClasses[aAddClasses.length] = DT.CLASS_LAST;
    }
    elTd.className = this._getColumnClassNames(oColumn, aAddClasses);

    // Class the liner element
    elTd.firstChild.className = DT.CLASS_LINER;

    // Set Column width for fallback cases
    if(oColumn.width && DT._bDynStylesFallback) {
        // Validate minWidth
        var nWidth = (oColumn.minWidth && (oColumn.width < oColumn.minWidth)) ?
                oColumn.minWidth : oColumn.width;
        elTd.firstChild.style.overflow = 'hidden';
        elTd.firstChild.style.width = nWidth + 'px';
    }
    
    return elTd;
},


/**
 * Create a new TR element for a given Record and appends it with the correct
 * number of Column-state-classed TD elements. Striping is the responsibility of
 * the calling function, which may decide to stripe the single row, a subset of
 * rows, or all the rows.
 * @method _createTrEl
 * @param oRecord {YAHOO.widget.Record} Record instance
 * @return {HTMLElement} The new TR element.  This must be added to the DOM.
 * @private 
 */
_addTrEl : function (oRecord) {
    var elTrTemplate = this._getTrTemplateEl();
    
    // Clone the TR template.
    var elTr = elTrTemplate.cloneNode(true);
    
    // Populate content
    return this._updateTrEl(elTr,oRecord);
},

/**
 * Formats the contents of the given TR's TD elements with data from the given
 * Record. Only innerHTML should change, nothing structural.
 *
 * @method _updateTrEl
 * @param elTr {HTMLElement} The TR element to update.
 * @param oRecord {YAHOO.widget.Record} The associated Record instance.
 * @return {HTMLElement} DOM reference to the new TR element.
 * @private
 */
_updateTrEl : function(elTr, oRecord) {
    var ok = this.get("formatRow") ? this.get("formatRow").call(this, elTr, oRecord) : true;
    if(ok) {
        // Hide the row to prevent constant reflows
        elTr.style.display = 'none';
        
        // Update TD elements with new data
        var allTds = elTr.childNodes,
            elTd;
        for(var i=0,len=allTds.length; i<len; ++i) {
            elTd = allTds[i];
            
            // Set the cell content
            this.formatCell(allTds[i].firstChild, oRecord, this._oColumnSet.keys[i]);
        }
        
        // Redisplay the row for reflow
        elTr.style.display = '';
    }
    
     // Record-to-TR association and tracking of FIRST/LAST
    var oldId = elTr.id,
        newId = oRecord.getId();
    if(this._sFirstTrId === oldId) {
        this._sFirstTrId = newId;
    }
    if(this._sLastTrId === oldId) {
        this._sLastTrId = newId;
    }
    elTr.id = newId;
    return elTr;
},


/**
 * Deletes TR element by DOM reference or by DataTable page row index.
 *
 * @method _deleteTrEl
 * @param row {HTMLElement | Number} TR element reference or Datatable page row index.
 * @return {Boolean} Returns true if successful, else returns false.
 * @private
 */
_deleteTrEl : function(row) {
    var rowIndex;

    // Get page row index for the element
    if(!lang.isNumber(row)) {
        rowIndex = Dom.get(row).sectionRowIndex;
    }
    else {
        rowIndex = row;
    }
    if(lang.isNumber(rowIndex) && (rowIndex > -2) && (rowIndex < this._elTbody.rows.length)) {
        // Cannot use tbody.deleteRow due to IE6 instability
        //return this._elTbody.deleteRow(rowIndex);
        return this._elTbody.removeChild(this._elTbody.rows[row]);
    }
    else {
        return null;
    }
},



























// CSS/STATE FUNCTIONS




/**
 * Removes the class YAHOO.widget.DataTable.CLASS_FIRST from the first TR element
 * of the DataTable page and updates internal tracker.
 *
 * @method _unsetFirstRow
 * @private
 */
_unsetFirstRow : function() {
    // Remove FIRST
    if(this._sFirstTrId) {
        Dom.removeClass(this._sFirstTrId, DT.CLASS_FIRST);
        this._sFirstTrId = null;
    }
},

/**
 * Assigns the class YAHOO.widget.DataTable.CLASS_FIRST to the first TR element
 * of the DataTable page and updates internal tracker.
 *
 * @method _setFirstRow
 * @private
 */
_setFirstRow : function() {
    this._unsetFirstRow();
    var elTr = this.getFirstTrEl();
    if(elTr) {
        // Set FIRST
        Dom.addClass(elTr, DT.CLASS_FIRST);
        this._sFirstTrId = elTr.id;
    }
},

/**
 * Removes the class YAHOO.widget.DataTable.CLASS_LAST from the last TR element
 * of the DataTable page and updates internal tracker.
 *
 * @method _unsetLastRow
 * @private
 */
_unsetLastRow : function() {
    // Unassign previous class
    if(this._sLastTrId) {
        Dom.removeClass(this._sLastTrId, DT.CLASS_LAST);
        this._sLastTrId = null;
    }   
},

/**
 * Assigns the class YAHOO.widget.DataTable.CLASS_LAST to the last TR element
 * of the DataTable page and updates internal tracker.
 *
 * @method _setLastRow
 * @private
 */
_setLastRow : function() {
    this._unsetLastRow();
    var elTr = this.getLastTrEl();
    if(elTr) {
        // Assign class
        Dom.addClass(elTr, DT.CLASS_LAST);
        this._sLastTrId = elTr.id;
    }
},

/**
 * Assigns the classes DT.CLASS_EVEN and DT.CLASS_ODD to one, many, or all TR elements.
 *
 * @method _setRowStripes
 * @param row {HTMLElement | String | Number} (optional) HTML TR element reference
 * or string ID, or page row index of where to start striping.
 * @param range {Number} (optional) If given, how many rows to stripe, otherwise
 * stripe all the rows until the end.
 * @private
 */
_setRowStripes : function(row, range) {
    // Default values stripe all rows
    var allRows = this._elTbody.rows,
        nStartIndex = 0,
        nEndIndex = allRows.length,
        aOdds = [], nOddIdx = 0,
        aEvens = [], nEvenIdx = 0;

    // Stripe a subset
    if((row !== null) && (row !== undefined)) {
        // Validate given start row
        var elStartRow = this.getTrEl(row);
        if(elStartRow) {
            nStartIndex = elStartRow.sectionRowIndex;

            // Validate given range
            if(lang.isNumber(range) && (range > 1)) {
                nEndIndex = nStartIndex + range;
            }
        }
    }

    for(var i=nStartIndex; i<nEndIndex; i++) {
        if(i%2) {
            aOdds[nOddIdx++] = allRows[i];
        } else {
            aEvens[nEvenIdx++] = allRows[i];
        }
    }

    if (aOdds.length) {
        Dom.replaceClass(aOdds, DT.CLASS_EVEN, DT.CLASS_ODD);
    }

    if (aEvens.length) {
        Dom.replaceClass(aEvens, DT.CLASS_ODD, DT.CLASS_EVEN);
    }
},

/**
 * Assigns the class DT.CLASS_SELECTED to TR and TD elements.
 *
 * @method _setSelections
 * @private
 */
_setSelections : function() {
    // Keep track of selected rows
    var allSelectedRows = this.getSelectedRows();
    // Keep track of selected cells
    var allSelectedCells = this.getSelectedCells();
    // Anything to select?
    if((allSelectedRows.length>0) || (allSelectedCells.length > 0)) {
        var oColumnSet = this._oColumnSet,
            el;
        // Loop over each row
        for(var i=0; i<allSelectedRows.length; i++) {
            el = Dom.get(allSelectedRows[i]);
            if(el) {
                Dom.addClass(el, DT.CLASS_SELECTED);
            }
        }
        // Loop over each cell
        for(i=0; i<allSelectedCells.length; i++) {
            el = Dom.get(allSelectedCells[i].recordId);
            if(el) {
                Dom.addClass(el.childNodes[oColumnSet.getColumn(allSelectedCells[i].columnKey).getKeyIndex()], DT.CLASS_SELECTED);
            }
        }
    }       
},











































/////////////////////////////////////////////////////////////////////////////
//
// Private DOM Event Handlers
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Validates minWidths whenever the render chain ends.
 *
 * @method _onRenderChainEnd
 * @private
 */
_onRenderChainEnd : function() {
    // Hide loading message
    this.hideTableMessage();
    
    // Show empty message
    if(this._elTbody.rows.length === 0) {
        this.showTableMessage(this.get("MSG_EMPTY"), DT.CLASS_EMPTY);        
    }

    // Execute in timeout thread to give implementers a chance
    // to subscribe after the constructor
    var oSelf = this;
    setTimeout(function() {
        if((oSelf instanceof DT) && oSelf._sId) {        
            // Init event
            if(oSelf._bInit) {
                oSelf._bInit = false;
                oSelf.fireEvent("initEvent");
            }
    
            // Render event
            oSelf.fireEvent("renderEvent");
            // Backward compatibility
            oSelf.fireEvent("refreshEvent");
            YAHOO.log("DataTable rendered", "info", oSelf.toString());
    
            // Post-render routine
            oSelf.validateColumnWidths();
    
            // Post-render event
            oSelf.fireEvent("postRenderEvent");
            
            /*if(YAHOO.example.Performance.trialStart) {
                YAHOO.log((new Date()).getTime() - YAHOO.example.Performance.trialStart.getTime() + " ms", "time");
                YAHOO.example.Performance.trialStart = null;
            }*/
            
            YAHOO.log("Post-render routine executed", "info", oSelf.toString());
        }
    }, 0);
},

/**
 * Handles click events on the DOCUMENT.
 *
 * @method _onDocumentClick
 * @param e {HTMLEvent} The click event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onDocumentClick : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName.toLowerCase();

    if(!Dom.isAncestor(oSelf._elContainer, elTarget)) {
        oSelf.fireEvent("tableBlurEvent");

        // Fires editorBlurEvent when click is not within the TABLE.
        // For cases when click is within the TABLE, due to timing issues,
        // the editorBlurEvent needs to get fired by the lower-level DOM click
        // handlers below rather than by the TABLE click handler directly.
        if(oSelf._oCellEditor) {
            if(oSelf._oCellEditor.getContainerEl) {
                var elContainer = oSelf._oCellEditor.getContainerEl();
                // Only if the click was not within the CellEditor container
                if(!Dom.isAncestor(elContainer, elTarget) &&
                        (elContainer.id !== elTarget.id)) {
                    oSelf._oCellEditor.fireEvent("blurEvent", {editor: oSelf._oCellEditor});
                }
            }
            // Backward Compatibility
            else if(oSelf._oCellEditor.isActive) {
                // Only if the click was not within the Cell Editor container
                if(!Dom.isAncestor(oSelf._oCellEditor.container, elTarget) &&
                        (oSelf._oCellEditor.container.id !== elTarget.id)) {
                    oSelf.fireEvent("editorBlurEvent", {editor:oSelf._oCellEditor});
                }
            }
        }
    }
},

/**
 * Handles focus events on the DataTable instance.
 *
 * @method _onTableFocus
 * @param e {HTMLEvent} The focus event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTableFocus : function(e, oSelf) {
    oSelf.fireEvent("tableFocusEvent");
},

/**
 * Handles focus events on the THEAD element.
 *
 * @method _onTheadFocus
 * @param e {HTMLEvent} The focus event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTheadFocus : function(e, oSelf) {
    oSelf.fireEvent("theadFocusEvent");
    oSelf.fireEvent("tableFocusEvent");
},

/**
 * Handles focus events on the TBODY element.
 *
 * @method _onTbodyFocus
 * @param e {HTMLEvent} The focus event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTbodyFocus : function(e, oSelf) {
    oSelf.fireEvent("tbodyFocusEvent");
    oSelf.fireEvent("tableFocusEvent");
},

/**
 * Handles mouseover events on the DataTable instance.
 *
 * @method _onTableMouseover
 * @param e {HTMLEvent} The mouseover event.
 * @param origTarget {HTMLElement} The mouseenter delegated element.
 * @param container {HTMLElement} The mouseenter delegation container.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTableMouseover : function(e, origTarget, container, oSelf) {
    var elTarget = origTarget;
    var elTag = elTarget.nodeName && elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                 return;
            case "a":
                break;
            case "td":
                bKeepBubbling = oSelf.fireEvent("cellMouseoverEvent",{target:elTarget,event:e});
                break;
            case "span":
                if(Dom.hasClass(elTarget, DT.CLASS_LABEL)) {
                    bKeepBubbling = oSelf.fireEvent("theadLabelMouseoverEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerLabelMouseoverEvent",{target:elTarget,event:e});
                }
                break;
            case "th":
                bKeepBubbling = oSelf.fireEvent("theadCellMouseoverEvent",{target:elTarget,event:e});
                // Backward compatibility
                bKeepBubbling = oSelf.fireEvent("headerCellMouseoverEvent",{target:elTarget,event:e});
                break;
            case "tr":
                if(elTarget.parentNode.nodeName.toLowerCase() == "thead") {
                    bKeepBubbling = oSelf.fireEvent("theadRowMouseoverEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerRowMouseoverEvent",{target:elTarget,event:e});
                }
                else {
                    bKeepBubbling = oSelf.fireEvent("rowMouseoverEvent",{target:elTarget,event:e});
                }
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableMouseoverEvent",{target:(elTarget || oSelf._elContainer),event:e});
},

/**
 * Handles mouseout events on the DataTable instance.
 *
 * @method _onTableMouseout
 * @param e {HTMLEvent} The mouseout event.
 * @param origTarget {HTMLElement} The mouseleave delegated element.
 * @param container {HTMLElement} The mouseleave delegation container.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTableMouseout : function(e, origTarget, container, oSelf) {
    var elTarget = origTarget;
    var elTag = elTarget.nodeName && elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "a":
                break;
            case "td":
                bKeepBubbling = oSelf.fireEvent("cellMouseoutEvent",{target:elTarget,event:e});
                break;
            case "span":
                if(Dom.hasClass(elTarget, DT.CLASS_LABEL)) {
                    bKeepBubbling = oSelf.fireEvent("theadLabelMouseoutEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerLabelMouseoutEvent",{target:elTarget,event:e});
                }
                break;
            case "th":
                bKeepBubbling = oSelf.fireEvent("theadCellMouseoutEvent",{target:elTarget,event:e});
                // Backward compatibility
                bKeepBubbling = oSelf.fireEvent("headerCellMouseoutEvent",{target:elTarget,event:e});
                break;
            case "tr":
                if(elTarget.parentNode.nodeName.toLowerCase() == "thead") {
                    bKeepBubbling = oSelf.fireEvent("theadRowMouseoutEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerRowMouseoutEvent",{target:elTarget,event:e});
                }
                else {
                    bKeepBubbling = oSelf.fireEvent("rowMouseoutEvent",{target:elTarget,event:e});
                }
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableMouseoutEvent",{target:(elTarget || oSelf._elContainer),event:e});
},

/**
 * Handles mousedown events on the DataTable instance.
 *
 * @method _onTableMousedown
 * @param e {HTMLEvent} The mousedown event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTableMousedown : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName && elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "a":
                break;
            case "td":
                bKeepBubbling = oSelf.fireEvent("cellMousedownEvent",{target:elTarget,event:e});
                break;
            case "span":
                if(Dom.hasClass(elTarget, DT.CLASS_LABEL)) {
                    bKeepBubbling = oSelf.fireEvent("theadLabelMousedownEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerLabelMousedownEvent",{target:elTarget,event:e});
                }
                break;
            case "th":
                bKeepBubbling = oSelf.fireEvent("theadCellMousedownEvent",{target:elTarget,event:e});
                // Backward compatibility
                bKeepBubbling = oSelf.fireEvent("headerCellMousedownEvent",{target:elTarget,event:e});
                break;
            case "tr":
                if(elTarget.parentNode.nodeName.toLowerCase() == "thead") {
                    bKeepBubbling = oSelf.fireEvent("theadRowMousedownEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerRowMousedownEvent",{target:elTarget,event:e});
                }
                else {
                    bKeepBubbling = oSelf.fireEvent("rowMousedownEvent",{target:elTarget,event:e});
                }
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableMousedownEvent",{target:(elTarget || oSelf._elContainer),event:e});
},

/**
 * Handles mouseup events on the DataTable instance.
 *
 * @method _onTableMouseup
 * @param e {HTMLEvent} The mouseup event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTableMouseup : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName && elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "a":
                break;
            case "td":
                bKeepBubbling = oSelf.fireEvent("cellMouseupEvent",{target:elTarget,event:e});
                break;
            case "span":
                if(Dom.hasClass(elTarget, DT.CLASS_LABEL)) {
                    bKeepBubbling = oSelf.fireEvent("theadLabelMouseupEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerLabelMouseupEvent",{target:elTarget,event:e});
                }
                break;
            case "th":
                bKeepBubbling = oSelf.fireEvent("theadCellMouseupEvent",{target:elTarget,event:e});
                // Backward compatibility
                bKeepBubbling = oSelf.fireEvent("headerCellMouseupEvent",{target:elTarget,event:e});
                break;
            case "tr":
                if(elTarget.parentNode.nodeName.toLowerCase() == "thead") {
                    bKeepBubbling = oSelf.fireEvent("theadRowMouseupEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerRowMouseupEvent",{target:elTarget,event:e});
                }
                else {
                    bKeepBubbling = oSelf.fireEvent("rowMouseupEvent",{target:elTarget,event:e});
                }
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableMouseupEvent",{target:(elTarget || oSelf._elContainer),event:e});
},

/**
 * Handles dblclick events on the DataTable instance.
 *
 * @method _onTableDblclick
 * @param e {HTMLEvent} The dblclick event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTableDblclick : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName && elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "td":
                bKeepBubbling = oSelf.fireEvent("cellDblclickEvent",{target:elTarget,event:e});
                break;
            case "span":
                if(Dom.hasClass(elTarget, DT.CLASS_LABEL)) {
                    bKeepBubbling = oSelf.fireEvent("theadLabelDblclickEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerLabelDblclickEvent",{target:elTarget,event:e});
                }
                break;
            case "th":
                bKeepBubbling = oSelf.fireEvent("theadCellDblclickEvent",{target:elTarget,event:e});
                // Backward compatibility
                bKeepBubbling = oSelf.fireEvent("headerCellDblclickEvent",{target:elTarget,event:e});
                break;
            case "tr":
                if(elTarget.parentNode.nodeName.toLowerCase() == "thead") {
                    bKeepBubbling = oSelf.fireEvent("theadRowDblclickEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerRowDblclickEvent",{target:elTarget,event:e});
                }
                else {
                    bKeepBubbling = oSelf.fireEvent("rowDblclickEvent",{target:elTarget,event:e});
                }
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableDblclickEvent",{target:(elTarget || oSelf._elContainer),event:e});
},
/**
 * Handles keydown events on the THEAD element.
 *
 * @method _onTheadKeydown
 * @param e {HTMLEvent} The key event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTheadKeydown : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName && elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "input":
            case "textarea":
                // TODO: implement textareaKeyEvent
                break;
            case "thead":
                bKeepBubbling = oSelf.fireEvent("theadKeyEvent",{target:elTarget,event:e});
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableKeyEvent",{target:(elTarget || oSelf._elContainer),event:e});
},

/**
 * Handles keydown events on the TBODY element. Handles selection behavior,
 * provides hooks for ENTER to edit functionality.
 *
 * @method _onTbodyKeydown
 * @param e {HTMLEvent} The key event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTbodyKeydown : function(e, oSelf) {
    var sMode = oSelf.get("selectionMode");

    if(sMode == "standard") {
        oSelf._handleStandardSelectionByKey(e);
    }
    else if(sMode == "single") {
        oSelf._handleSingleSelectionByKey(e);
    }
    else if(sMode == "cellblock") {
        oSelf._handleCellBlockSelectionByKey(e);
    }
    else if(sMode == "cellrange") {
        oSelf._handleCellRangeSelectionByKey(e);
    }
    else if(sMode == "singlecell") {
        oSelf._handleSingleCellSelectionByKey(e);
    }
    
    if(oSelf._oCellEditor) {
        if(oSelf._oCellEditor.fireEvent) {
            oSelf._oCellEditor.fireEvent("blurEvent", {editor: oSelf._oCellEditor});
        }
        else if(oSelf._oCellEditor.isActive) {
            oSelf.fireEvent("editorBlurEvent", {editor:oSelf._oCellEditor});
        }
    }

    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName && elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "tbody":
                bKeepBubbling = oSelf.fireEvent("tbodyKeyEvent",{target:elTarget,event:e});
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableKeyEvent",{target:(elTarget || oSelf._elContainer),event:e});
},

/**
 * Handles click events on the THEAD element.
 *
 * @method _onTheadClick
 * @param e {HTMLEvent} The click event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTheadClick : function(e, oSelf) {
    // This blurs the CellEditor
    if(oSelf._oCellEditor) {
        if(oSelf._oCellEditor.fireEvent) {
            oSelf._oCellEditor.fireEvent("blurEvent", {editor: oSelf._oCellEditor});
        }
        // Backward compatibility
        else if(oSelf._oCellEditor.isActive) {
            oSelf.fireEvent("editorBlurEvent", {editor:oSelf._oCellEditor});
        }
    }

    var elTarget = Ev.getTarget(e),
        elTag = elTarget.nodeName && elTarget.nodeName.toLowerCase(),
        bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "input":
                var sType = elTarget.type.toLowerCase();
                if(sType == "checkbox") {
                    bKeepBubbling = oSelf.fireEvent("theadCheckboxClickEvent",{target:elTarget,event:e});
                }
                else if(sType == "radio") {
                    bKeepBubbling = oSelf.fireEvent("theadRadioClickEvent",{target:elTarget,event:e});
                }
                else if((sType == "button") || (sType == "image") || (sType == "submit") || (sType == "reset")) {
                    if(!elTarget.disabled) {
                        bKeepBubbling = oSelf.fireEvent("theadButtonClickEvent",{target:elTarget,event:e});
                    }
                    else {
                        bKeepBubbling = false;
                    }
                }
                else if (elTarget.disabled){
                    bKeepBubbling = false;
                }
                break;
            case "a":
                bKeepBubbling = oSelf.fireEvent("theadLinkClickEvent",{target:elTarget,event:e});
                break;
            case "button":
                if(!elTarget.disabled) {
                    bKeepBubbling = oSelf.fireEvent("theadButtonClickEvent",{target:elTarget,event:e});
                }
                else {
                    bKeepBubbling = false;
                }
                break;
            case "span":
                if(Dom.hasClass(elTarget, DT.CLASS_LABEL)) {
                    bKeepBubbling = oSelf.fireEvent("theadLabelClickEvent",{target:elTarget,event:e});
                    // Backward compatibility
                    bKeepBubbling = oSelf.fireEvent("headerLabelClickEvent",{target:elTarget,event:e});
                }
                break;
            case "th":
                bKeepBubbling = oSelf.fireEvent("theadCellClickEvent",{target:elTarget,event:e});
                // Backward compatibility
                bKeepBubbling = oSelf.fireEvent("headerCellClickEvent",{target:elTarget,event:e});
                break;
            case "tr":
                bKeepBubbling = oSelf.fireEvent("theadRowClickEvent",{target:elTarget,event:e});
                // Backward compatibility
                bKeepBubbling = oSelf.fireEvent("headerRowClickEvent",{target:elTarget,event:e});
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableClickEvent",{target:(elTarget || oSelf._elContainer),event:e});
},

/**
 * Handles click events on the primary TBODY element.
 *
 * @method _onTbodyClick
 * @param e {HTMLEvent} The click event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onTbodyClick : function(e, oSelf) {
    // This blurs the CellEditor
    if(oSelf._oCellEditor) {
        if(oSelf._oCellEditor.fireEvent) {
            oSelf._oCellEditor.fireEvent("blurEvent", {editor: oSelf._oCellEditor});
        }
        else if(oSelf._oCellEditor.isActive) {
            oSelf.fireEvent("editorBlurEvent", {editor:oSelf._oCellEditor});
        }
    }

    // Fire Custom Events
    var elTarget = Ev.getTarget(e),
        elTag = elTarget.nodeName && elTarget.nodeName.toLowerCase(),
        bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "input":
                var sType = elTarget.type.toLowerCase();
                if(sType == "checkbox") {
                    bKeepBubbling = oSelf.fireEvent("checkboxClickEvent",{target:elTarget,event:e});
                }
                else if(sType == "radio") {
                    bKeepBubbling = oSelf.fireEvent("radioClickEvent",{target:elTarget,event:e});
                }
                else if((sType == "button") || (sType == "image") || (sType == "submit") || (sType == "reset")) {
                    if(!elTarget.disabled) {
                        bKeepBubbling = oSelf.fireEvent("buttonClickEvent",{target:elTarget,event:e});
                    }
                    else {
                        bKeepBubbling = false;
                    }
                }
                else if (elTarget.disabled){
                    bKeepBubbling = false;
                }
                break;
            case "a":
                bKeepBubbling = oSelf.fireEvent("linkClickEvent",{target:elTarget,event:e});
                break;
            case "button":
                if(!elTarget.disabled) {
                    bKeepBubbling = oSelf.fireEvent("buttonClickEvent",{target:elTarget,event:e});
                }
                else {
                    bKeepBubbling = false;
                }
                break;
            case "td":
                bKeepBubbling = oSelf.fireEvent("cellClickEvent",{target:elTarget,event:e});
                break;
            case "tr":
                bKeepBubbling = oSelf.fireEvent("rowClickEvent",{target:elTarget,event:e});
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableClickEvent",{target:(elTarget || oSelf._elContainer),event:e});
},

/**
 * Handles change events on SELECT elements within DataTable.
 *
 * @method _onDropdownChange
 * @param e {HTMLEvent} The change event.
 * @param oSelf {YAHOO.wiget.DataTable} DataTable instance.
 * @private
 */
_onDropdownChange : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    oSelf.fireEvent("dropdownChangeEvent", {event:e, target:elTarget});
},
































/////////////////////////////////////////////////////////////////////////////
//
// Public member variables
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Returns object literal of initial configs.
 *
 * @property configs
 * @type Object
 * @default {} 
 */
configs: null,


/////////////////////////////////////////////////////////////////////////////
//
// Public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Returns unique id assigned to instance, which is a useful prefix for
 * generating unique DOM ID strings.
 *
 * @method getId
 * @return {String} Unique ID of the DataSource instance.
 */
getId : function() {
    return this._sId;
},

/**
 * DataSource instance name, for logging.
 *
 * @method toString
 * @return {String} Unique name of the DataSource instance.
 */

toString : function() {
    return "DataTable instance " + this._sId;
},

/**
 * Returns the DataTable instance's DataSource instance.
 *
 * @method getDataSource
 * @return {YAHOO.util.DataSource} DataSource instance.
 */
getDataSource : function() {
    return this._oDataSource;
},

/**
 * Returns the DataTable instance's ColumnSet instance.
 *
 * @method getColumnSet
 * @return {YAHOO.widget.ColumnSet} ColumnSet instance.
 */
getColumnSet : function() {
    return this._oColumnSet;
},

/**
 * Returns the DataTable instance's RecordSet instance.
 *
 * @method getRecordSet
 * @return {YAHOO.widget.RecordSet} RecordSet instance.
 */
getRecordSet : function() {
    return this._oRecordSet;
},

/**
 * Returns on object literal representing the DataTable instance's current
 * state with the following properties:
 * <dl>
 * <dt>pagination</dt>
 * <dd>Instance of YAHOO.widget.Paginator</dd>
 *
 * <dt>sortedBy</dt>
 * <dd>
 *     <dl>
 *         <dt>sortedBy.key</dt>
 *         <dd>{String} Key of sorted Column</dd>
 *         <dt>sortedBy.dir</dt>
 *         <dd>{String} Initial sort direction, either YAHOO.widget.DataTable.CLASS_ASC or YAHOO.widget.DataTable.CLASS_DESC</dd>
 *     </dl>
 * </dd>
 *
 * <dt>selectedRows</dt>
 * <dd>Array of selected rows by Record ID.</dd>
 *
 * <dt>selectedCells</dt>
 * <dd>Selected cells as an array of object literals:
 *     {recordId:sRecordId, columnKey:sColumnKey}</dd>
 * </dl>
 *  
 * @method getState
 * @return {Object} DataTable instance state object literal values.
 */
getState : function() {
    return {
        totalRecords: this.get('paginator') ? this.get('paginator').get("totalRecords") : this._oRecordSet.getLength(),
        pagination: this.get("paginator") ? this.get("paginator").getState() : null,
        sortedBy: this.get("sortedBy"),
        selectedRows: this.getSelectedRows(),
        selectedCells: this.getSelectedCells()
    };
},











































// DOM ACCESSORS

/**
 * Returns DOM reference to the DataTable's container element.
 *
 * @method getContainerEl
 * @return {HTMLElement} Reference to DIV element.
 */
getContainerEl : function() {
    return this._elContainer;
},

/**
 * Returns DOM reference to the DataTable's TABLE element.
 *
 * @method getTableEl
 * @return {HTMLElement} Reference to TABLE element.
 */
getTableEl : function() {
    return this._elTable;
},

/**
 * Returns DOM reference to the DataTable's THEAD element.
 *
 * @method getTheadEl
 * @return {HTMLElement} Reference to THEAD element.
 */
getTheadEl : function() {
    return this._elThead;
},

/**
 * Returns DOM reference to the DataTable's primary TBODY element.
 *
 * @method getTbodyEl
 * @return {HTMLElement} Reference to TBODY element.
 */
getTbodyEl : function() {
    return this._elTbody;
},

/**
 * Returns DOM reference to the DataTable's secondary TBODY element that is
 * used to display messages.
 *
 * @method getMsgTbodyEl
 * @return {HTMLElement} Reference to TBODY element.
 */
getMsgTbodyEl : function() {
    return this._elMsgTbody;
},

/**
 * Returns DOM reference to the TD element within the secondary TBODY that is
 * used to display messages.
 *
 * @method getMsgTdEl
 * @return {HTMLElement} Reference to TD element.
 */
getMsgTdEl : function() {
    return this._elMsgTd;
},

/**
 * Returns the corresponding TR reference for a given DOM element, ID string or
 * page row index. If the given identifier is a child of a TR element,
 * then DOM tree is traversed until a parent TR element is returned, otherwise
 * null. Returns null if the row is not considered a primary row (i.e., row
 * extensions).
 *
 * @method getTrEl
 * @param row {HTMLElement | String | Number | YAHOO.widget.Record} Which row to
 * get: by element reference, ID string, page row index, or Record.
 * @return {HTMLElement} Reference to TR element, or null.
 */
getTrEl : function(row) {
    // By Record
    if(row instanceof YAHOO.widget.Record) {
        return document.getElementById(row.getId());
    }
    // By page row index
    else if(lang.isNumber(row)) {
        var dataRows = Dom.getElementsByClassName(DT.CLASS_REC, "tr", this._elTbody);
        return dataRows && dataRows[row] ? dataRows[row] : null;
    }
    // By ID string or element reference
    else if(row) {
        var elRow = (lang.isString(row)) ? document.getElementById(row) : row;

        // Validate HTML element
        if(elRow && elRow.ownerDocument == document) {
            // Validate TR element
            if(elRow.nodeName.toLowerCase() != "tr") {
                // Traverse up the DOM to find the corresponding TR element
                elRow = Dom.getAncestorByTagName(elRow,"tr");
            }

            return elRow;
        }
    }

    return null;
},

/**
 * Returns DOM reference to the first primary TR element in the DataTable page, or null.
 *
 * @method getFirstTrEl
 * @return {HTMLElement} Reference to TR element.
 */
getFirstTrEl : function() {
    var allRows = this._elTbody.rows,
        i=0;
    while(allRows[i]) {
        if(this.getRecord(allRows[i])) {
            return allRows[i];
        }
        i++;
    }
    return null;

},

/**
 * Returns DOM reference to the last primary TR element in the DataTable page, or null.
 *
 * @method getLastTrEl
 * @return {HTMLElement} Reference to last TR element.
 */
getLastTrEl : function() {
    var allRows = this._elTbody.rows,
        i=allRows.length-1;
    while(i>-1) {
        if(this.getRecord(allRows[i])) {
            return allRows[i];
        }
        i--;
    }
    return null;
},

/**
 * Returns DOM reference to the next TR element from the given primary TR element, or null.
 *
 * @method getNextTrEl
 * @param row {HTMLElement | String | Number | YAHOO.widget.Record} Element
 * reference, ID string, page row index, or Record from which to get next TR element.
 * @param forcePrimary {Boolean} (optional) If true, will only return TR elements
 * that correspond to Records. Non-primary rows (such as row expansions)
 * will be skipped.
 * @return {HTMLElement} Reference to next TR element.
 */
getNextTrEl : function(row, forcePrimary) {
    var nThisTrIndex = this.getTrIndex(row);
    if(nThisTrIndex !== null) {
        var allRows = this._elTbody.rows;
        if(forcePrimary) {
            while(nThisTrIndex < allRows.length-1) {
                row = allRows[nThisTrIndex+1];
                if(this.getRecord(row)) {
                    return row;
                }
                nThisTrIndex++;
            }
        }
        else {
            if(nThisTrIndex < allRows.length-1) {
                return allRows[nThisTrIndex+1];
            }
        }
    }

    YAHOO.log("Could not get next TR element for row " + row, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to the previous TR element from the given primary TR element, or null.
 *
 * @method getPreviousTrEl
 * @param row {HTMLElement | String | Number | YAHOO.widget.Record} Element
 * reference, ID string, page row index, or Record from which to get previous TR element.
 * @param forcePrimary {Boolean} (optional) If true, will only return TR elements
 * from rothat correspond to Records. Non-primary rows (such as row expansions)
 * will be skipped.
 * @return {HTMLElement} Reference to previous TR element.
 */
getPreviousTrEl : function(row, forcePrimary) {
    var nThisTrIndex = this.getTrIndex(row);
    if(nThisTrIndex !== null) {
        var allRows = this._elTbody.rows;

        if(forcePrimary) {
            while(nThisTrIndex > 0) {
                row = allRows[nThisTrIndex-1];
                if(this.getRecord(row)) {
                    return row;
                }
                nThisTrIndex--;
            }
        }
        else {
            if(nThisTrIndex > 0) {
                return allRows[nThisTrIndex-1];
            }
        }
    }

    YAHOO.log("Could not get previous TR element for row " + row, "info", this.toString());
    return null;
},


/**
 * Workaround for IE bug where hidden or not-in-dom elements cause cellIndex
 * value to be incorrect.
 *
 * @method getCellIndex
 * @param cell {HTMLElement | Object} TD element or child of a TD element, or
 * object literal of syntax {record:oRecord, column:oColumn}.
 * @return {Number} TD.cellIndex value.
 */
getCellIndex : function(cell) {
    cell = this.getTdEl(cell);
    if(cell) {
        if(ua.ie > 0) {
            var i=0,
                tr = cell.parentNode,
                allCells = tr.childNodes,
                len = allCells.length;
            for(; i<len; i++) {
                if(allCells[i] == cell) {
                    return i;
                }
            }
        }
        else {
            return cell.cellIndex;
        }
    }
},

/**
 * Returns DOM reference to a TD liner element.
 *
 * @method getTdLinerEl
 * @param cell {HTMLElement | Object} TD element or child of a TD element, or
 * object literal of syntax {record:oRecord, column:oColumn}.
 * @return {HTMLElement} Reference to TD liner element.
 */
getTdLinerEl : function(cell) {
    var elCell = this.getTdEl(cell);
    return elCell.firstChild || null;
},

/**
 * Returns DOM reference to a TD element. Returns null if the row is not
 * considered a primary row (i.e., row extensions).
 *
 * @method getTdEl
 * @param cell {HTMLElement | String | Object} TD element or child of a TD element, or
 * object literal of syntax {record:oRecord, column:oColumn}.
 * @return {HTMLElement} Reference to TD element.
 */
getTdEl : function(cell) {
    var elCell;
    var el = Dom.get(cell);

    // Validate HTML element
    if(el && (el.ownerDocument == document)) {
        // Validate TD element
        if(el.nodeName.toLowerCase() != "td") {
            // Traverse up the DOM to find the corresponding TR element
            elCell = Dom.getAncestorByTagName(el, "td");
        }
        else {
            elCell = el;
        }
        
        // Make sure the TD is in this TBODY or is not in DOM
        // Bug 2527707 and bug 2263558
        if(elCell && ((elCell.parentNode.parentNode == this._elTbody) ||
            (elCell.parentNode.parentNode === null) ||
            (elCell.parentNode.parentNode.nodeType === 11))) {
            // Now we can return the TD element
            return elCell;
        }
    }
    else if(cell) {
        var oRecord, nColKeyIndex;

        if(lang.isString(cell.columnKey) && lang.isString(cell.recordId)) {
            oRecord = this.getRecord(cell.recordId);
            var oColumn = this.getColumn(cell.columnKey);
            if(oColumn) {
                nColKeyIndex = oColumn.getKeyIndex();
            }

        }
        if(cell.record && cell.column && cell.column.getKeyIndex) {
            oRecord = cell.record;
            nColKeyIndex = cell.column.getKeyIndex();
        }
        var elRow = this.getTrEl(oRecord);
        if((nColKeyIndex !== null) && elRow && elRow.cells && elRow.cells.length > 0) {
            return elRow.cells[nColKeyIndex] || null;
        }
    }

    return null;
},

/**
 * Returns DOM reference to the first primary TD element in the DataTable page (by default),
 * the first TD element of the optionally given row, or null.
 *
 * @method getFirstTdEl
 * @param row {HTMLElement} (optional) row from which to get first TD
 * @return {HTMLElement} Reference to TD element.
 */
getFirstTdEl : function(row) {
    var elRow = lang.isValue(row) ? this.getTrEl(row) : this.getFirstTrEl();
    if(elRow) {
        if(elRow.cells && elRow.cells.length > 0) {
            return elRow.cells[0];
        }
        else if(elRow.childNodes && elRow.childNodes.length > 0) {
            return elRow.childNodes[0];
        }
    }
    YAHOO.log("Could not get first TD element for row " + elRow, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to the last primary TD element in the DataTable page (by default),
 * the first TD element of the optionally given row, or null.
 *
 * @method getLastTdEl
 * @param row {HTMLElement} (optional) row from which to get first TD
 * @return {HTMLElement} Reference to last TD element.
 */
getLastTdEl : function(row) {
    var elRow = lang.isValue(row) ? this.getTrEl(row) : this.getLastTrEl();
    if(elRow) {
        if(elRow.cells && elRow.cells.length > 0) {
            return elRow.cells[elRow.cells.length-1];
        }
        else if(elRow.childNodes && elRow.childNodes.length > 0) {
            return elRow.childNodes[elRow.childNodes.length-1];
        }
    }
    YAHOO.log("Could not get last TD element for row " + elRow, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to the next TD element from the given cell, or null.
 *
 * @method getNextTdEl
 * @param cell {HTMLElement | String | Object} DOM element reference or string ID, or
 * object literal of syntax {record:oRecord, column:oColumn} from which to get next TD element.
 * @return {HTMLElement} Reference to next TD element, or null.
 */
getNextTdEl : function(cell) {
    var elCell = this.getTdEl(cell);
    if(elCell) {
        var nThisTdIndex = this.getCellIndex(elCell);
        var elRow = this.getTrEl(elCell);
        if(elRow.cells && (elRow.cells.length) > 0 && (nThisTdIndex < elRow.cells.length-1)) {
            return elRow.cells[nThisTdIndex+1];
        }
        else if(elRow.childNodes && (elRow.childNodes.length) > 0 && (nThisTdIndex < elRow.childNodes.length-1)) {
            return elRow.childNodes[nThisTdIndex+1];
        }
        else {
            var elNextRow = this.getNextTrEl(elRow);
            if(elNextRow) {
                return elNextRow.cells[0];
            }
        }
    }
    YAHOO.log("Could not get next TD element for cell " + cell, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to the previous TD element from the given cell, or null.
 *
 * @method getPreviousTdEl
 * @param cell {HTMLElement | String | Object} DOM element reference or string ID, or
 * object literal of syntax {record:oRecord, column:oColumn} from which to get previous TD element.
 * @return {HTMLElement} Reference to previous TD element, or null.
 */
getPreviousTdEl : function(cell) {
    var elCell = this.getTdEl(cell);
    if(elCell) {
        var nThisTdIndex = this.getCellIndex(elCell);
        var elRow = this.getTrEl(elCell);
        if(nThisTdIndex > 0) {
            if(elRow.cells && elRow.cells.length > 0) {
                return elRow.cells[nThisTdIndex-1];
            }
            else if(elRow.childNodes && elRow.childNodes.length > 0) {
                return elRow.childNodes[nThisTdIndex-1];
            }
        }
        else {
            var elPreviousRow = this.getPreviousTrEl(elRow);
            if(elPreviousRow) {
                return this.getLastTdEl(elPreviousRow);
            }
        }
    }
    YAHOO.log("Could not get next TD element for cell " + cell, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to the above TD element from the given cell, or null.
 *
 * @method getAboveTdEl
 * @param cell {HTMLElement | String | Object} DOM element reference or string ID, or
 * object literal of syntax {record:oRecord, column:oColumn} from which to get next TD element.
 * @param forcePrimary {Boolean} (optional) If true, will only return TD elements
 * from rows that correspond to Records. Non-primary rows (such as row expansions)
 * will be skipped.
 * @return {HTMLElement} Reference to above TD element, or null.
 */
getAboveTdEl : function(cell, forcePrimary) {
    var elCell = this.getTdEl(cell);
    if(elCell) {
        var elPreviousRow = this.getPreviousTrEl(elCell, forcePrimary);
        if(elPreviousRow ) {
            var cellIndex = this.getCellIndex(elCell);
            if(elPreviousRow.cells && elPreviousRow.cells.length > 0) {
                return elPreviousRow.cells[cellIndex] ? elPreviousRow.cells[cellIndex] : null;
            }
            else if(elPreviousRow.childNodes && elPreviousRow.childNodes.length > 0) {
                return elPreviousRow.childNodes[cellIndex] ? elPreviousRow.childNodes[cellIndex] : null;
            }
        }
    }
    YAHOO.log("Could not get above TD element for cell " + cell, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to the below TD element from the given cell, or null.
 *
 * @method getBelowTdEl
 * @param cell {HTMLElement | String | Object} DOM element reference or string ID, or
 * object literal of syntax {record:oRecord, column:oColumn} from which to get previous TD element.
 * @param forcePrimary {Boolean} (optional) If true, will only return TD elements
 * from rows that correspond to Records. Non-primary rows (such as row expansions)
 * will be skipped.
 * @return {HTMLElement} Reference to below TD element, or null.
 */
getBelowTdEl : function(cell, forcePrimary) {
    var elCell = this.getTdEl(cell);
    if(elCell) {
        var elNextRow = this.getNextTrEl(elCell, forcePrimary);
        if(elNextRow) {
            var cellIndex = this.getCellIndex(elCell);
            if(elNextRow.cells && elNextRow.cells.length > 0) {
                return elNextRow.cells[cellIndex] ? elNextRow.cells[cellIndex] : null;
            }
            else if(elNextRow.childNodes && elNextRow.childNodes.length > 0) {
                return elNextRow.childNodes[cellIndex] ? elNextRow.childNodes[cellIndex] : null;
            }
        }
    }
    YAHOO.log("Could not get below TD element for cell " + cell, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to a TH liner element. Needed to normalize for resizeable 
 * Columns, which have an additional resizer liner DIV element between the TH
 * element and the liner DIV element. 
 *
 * @method getThLinerEl
 * @param theadCell {YAHOO.widget.Column | HTMLElement | String} Column instance,
 * DOM element reference, or string ID.
 * @return {HTMLElement} Reference to TH liner element.
 */
getThLinerEl : function(theadCell) {
    var oColumn = this.getColumn(theadCell);
    return (oColumn) ? oColumn.getThLinerEl() : null;
},

/**
 * Returns DOM reference to a TH element.
 *
 * @method getThEl
 * @param theadCell {YAHOO.widget.Column | HTMLElement | String} Column instance,
 * DOM element reference, or string ID.
 * @return {HTMLElement} Reference to TH element.
 */
getThEl : function(theadCell) {
    var elTh;

    // Validate Column instance
    if(theadCell instanceof YAHOO.widget.Column) {
        var oColumn = theadCell;
        elTh = oColumn.getThEl();
        if(elTh) {
            return elTh;
        }
    }
    // Validate HTML element
    else {
        var el = Dom.get(theadCell);

        if(el && (el.ownerDocument == document)) {
            // Validate TH element
            if(el.nodeName.toLowerCase() != "th") {
                // Traverse up the DOM to find the corresponding TR element
                elTh = Dom.getAncestorByTagName(el,"th");
            }
            else {
                elTh = el;
            }

            return elTh;
        }
    }

    return null;
},

/**
 * Returns the page row index of given primary row. Returns null if the row is not on the
 * current DataTable page, or if row is not considered a primary row (i.e., row
 * extensions).
 *
 * @method getTrIndex
 * @param row {HTMLElement | String | YAHOO.widget.Record | Number} DOM or ID
 * string reference to an element within the DataTable page, a Record instance,
 * or a Record's RecordSet index.
 * @return {Number} Page row index, or null if data row does not exist or is not on current page.
 */
getTrIndex : function(row) {
    var record = this.getRecord(row),
        index = this.getRecordIndex(record),
        tr;
    if(record) {
        tr = this.getTrEl(record);
        if(tr) {
            return tr.sectionRowIndex;
        }
        else {
            var oPaginator = this.get("paginator");
            if(oPaginator) {
                return oPaginator.get('recordOffset') + index;
            }
            else {
                return index;
            }
        }
    }
    YAHOO.log("Could not get page row index for row " + row, "info", this.toString());
    return null;
},














































// TABLE FUNCTIONS

/**
 * Loads new data. Convenience method that calls DataSource's sendRequest()
 * method under the hood.
 *
 * @method load
 * @param oConfig {object} Optional configuration parameters:
 *
 * <dl>
 * <dt>request</dt><dd>Pass in a new request, or initialRequest is used.</dd>
 * <dt>callback</dt><dd>Pass in DataSource sendRequest() callback object, or the following is used:
 *    <dl>
 *      <dt>success</dt><dd>datatable.onDataReturnInitializeTable</dd>
 *      <dt>failure</dt><dd>datatable.onDataReturnInitializeTable</dd>
 *      <dt>scope</dt><dd>datatable</dd>
 *      <dt>argument</dt><dd>datatable.getState()</dd>
 *    </dl>
 * </dd>
 * <dt>datasource</dt><dd>Pass in a new DataSource instance to override the current DataSource for this transaction.</dd>
 * </dl>
 */
load : function(oConfig) {
    oConfig = oConfig || {};

    (oConfig.datasource || this._oDataSource).sendRequest(oConfig.request || this.get("initialRequest"), oConfig.callback || {
        success: this.onDataReturnInitializeTable,
        failure: this.onDataReturnInitializeTable,
        scope: this,
        argument: this.getState()
    });
},

/**
 * Resets a RecordSet with the given data and populates the page view
 * with the new data. Any previous data, and selection and sort states are
 * cleared. New data should be added as a separate step. 
 *
 * @method initializeTable
 */
initializeTable : function() {
    // Reset init flag
    this._bInit = true;
    
    // Clear the RecordSet
    this._oRecordSet.reset();

    // Clear the Paginator's totalRecords if paginating
    var pag = this.get('paginator');
    if (pag) {
        pag.set('totalRecords',0);
    }

    // Clear selections
    this._unselectAllTrEls();
    this._unselectAllTdEls();
    this._aSelections = null;
    this._oAnchorRecord = null;
    this._oAnchorCell = null;
    
    // Clear sort
    this.set("sortedBy", null);
},

/**
 * Internal wrapper calls run() on render Chain instance.
 *
 * @method _runRenderChain
 * @private 
 */
_runRenderChain : function() {
    this._oChainRender.run();
},

/**
 * Returns array of Records for current view. For example, if paginated, it
 * returns the subset of Records for current page.
 *
 * @method _getViewRecords
 * @protected
 * @return {Array} Array of Records to display in current view.
 */
_getViewRecords : function() {
    // Paginator is enabled, show a subset of Records
    var oPaginator = this.get('paginator');
    if(oPaginator) {
        return this._oRecordSet.getRecords(
                        oPaginator.getStartIndex(),
                        oPaginator.getRowsPerPage());
    }
    // Not paginated, show all records
    else {
        return this._oRecordSet.getRecords();
    }

},

/**
 * Renders the view with existing Records from the RecordSet while
 * maintaining sort, pagination, and selection states. For performance, reuses
 * existing DOM elements when possible while deleting extraneous elements.
 *
 * @method render
 */
render : function() {
//YAHOO.example.Performance.trialStart = new Date();

    this._oChainRender.stop();

    this.fireEvent("beforeRenderEvent");
    YAHOO.log("DataTable rendering...", "info", this.toString());

    var i, j, k, len,
        allRecords = this._getViewRecords();


    // From the top, update in-place existing rows, so as to reuse DOM elements
    var elTbody = this._elTbody,
        loopN = this.get("renderLoopSize"),
        nRecordsLength = allRecords.length;
    
    // Table has rows
    if(nRecordsLength > 0) {                
        elTbody.style.display = "none";
        while(elTbody.lastChild) {
            elTbody.removeChild(elTbody.lastChild);
        }
        elTbody.style.display = "";

        // Set up the loop Chain to render rows
        this._oChainRender.add({
            method: function(oArg) {
                if((this instanceof DT) && this._sId) {
                    var i = oArg.nCurrentRecord,
                        endRecordIndex = ((oArg.nCurrentRecord+oArg.nLoopLength) > nRecordsLength) ?
                                nRecordsLength : (oArg.nCurrentRecord+oArg.nLoopLength),
                        elRow, nextSibling;

                    elTbody.style.display = "none";
                    
                    for(; i<endRecordIndex; i++) {
                        elRow = Dom.get(allRecords[i].getId());
                        elRow = elRow || this._addTrEl(allRecords[i]);
                        nextSibling = elTbody.childNodes[i] || null;
                        elTbody.insertBefore(elRow, nextSibling);
                    }
                    elTbody.style.display = "";
                    
                    // Set up for the next loop
                    oArg.nCurrentRecord = i;
                }
            },
            scope: this,
            iterations: (loopN > 0) ? Math.ceil(nRecordsLength/loopN) : 1,
            argument: {
                nCurrentRecord: 0,//nRecordsLength-1,  // Start at first Record
                nLoopLength: (loopN > 0) ? loopN : nRecordsLength
            },
            timeout: (loopN > 0) ? 0 : -1
        });
        
        // Post-render tasks
        this._oChainRender.add({
            method: function(oArg) {
                if((this instanceof DT) && this._sId) {
                    while(elTbody.rows.length > nRecordsLength) {
                        elTbody.removeChild(elTbody.lastChild);
                    }
                    this._setFirstRow();
                    this._setLastRow();
                    this._setRowStripes();
                    this._setSelections();
                }
            },
            scope: this,
            timeout: (loopN > 0) ? 0 : -1
        });
     
    }
    // Table has no rows
    else {
        // Set up the loop Chain to delete rows
        var nTotal = elTbody.rows.length;
        if(nTotal > 0) {
            this._oChainRender.add({
                method: function(oArg) {
                    if((this instanceof DT) && this._sId) {
                        var i = oArg.nCurrent,
                            loopN = oArg.nLoopLength,
                            nIterEnd = (i - loopN < 0) ? 0 : i - loopN;
    
                        elTbody.style.display = "none";
                        
                        for(; i>nIterEnd; i--) {
                            elTbody.deleteRow(-1);
                        }
                        elTbody.style.display = "";
                        
                        // Set up for the next loop
                        oArg.nCurrent = i;
                    }
                },
                scope: this,
                iterations: (loopN > 0) ? Math.ceil(nTotal/loopN) : 1,
                argument: {
                    nCurrent: nTotal, 
                    nLoopLength: (loopN > 0) ? loopN : nTotal
                },
                timeout: (loopN > 0) ? 0 : -1
            });
        }
    }
    this._runRenderChain();
},

/**
 * Disables DataTable UI.
 *
 * @method disable
 */
disable : function() {
    this._disabled = true;
    var elTable = this._elTable;
    var elMask = this._elMask;
    elMask.style.width = elTable.offsetWidth + "px";
    elMask.style.height = elTable.offsetHeight + "px";
    elMask.style.left = elTable.offsetLeft + "px";
    elMask.style.display = "";
    this.fireEvent("disableEvent");
},

/**
 * Undisables DataTable UI.
 *
 * @method undisable
 */
undisable : function() {
    this._disabled = false;
    this._elMask.style.display = "none";
    this.fireEvent("undisableEvent");
},

 /**
 * Returns disabled state.
 *
 * @method isDisabled
 * @return {Boolean} True if UI is disabled, otherwise false
 */
isDisabled : function() {
    return this._disabled;
},

/**
 * Nulls out the entire DataTable instance and related objects, removes attached
 * event listeners, and clears out DOM elements inside the container. After
 * calling this method, the instance reference should be expliclitly nulled by
 * implementer, as in myDataTable = null. Use with caution!
 *
 * @method destroy
 */
destroy : function() {
    // Store for later
    var instanceName = this.toString();

    this._oChainRender.stop();
    
    // Destroy ColumnDD and ColumnResizers
    this._destroyColumnHelpers();
    
    // Destroy all CellEditors
    var oCellEditor;
    for(var i=0, len=this._oColumnSet.flat.length; i<len; i++) {
        oCellEditor = this._oColumnSet.flat[i].editor;
        if(oCellEditor && oCellEditor.destroy) {
            oCellEditor.destroy();
            this._oColumnSet.flat[i].editor = null;
        }
    }

    // Destroy Paginator
    this._destroyPaginator();

    // Unhook custom events
    this._oRecordSet.unsubscribeAll();
    this.unsubscribeAll();

    // Unhook DOM events
    Ev.removeListener(document, "click", this._onDocumentClick);
    
    // Clear out the container
    this._destroyContainerEl(this._elContainer);

    // Null out objects
    for(var param in this) {
        if(lang.hasOwnProperty(this, param)) {
            this[param] = null;
        }
    }
    
    // Clean up static values
    DT._nCurrentCount--;
    
    if(DT._nCurrentCount < 1) {
        if(DT._elDynStyleNode) {
            document.getElementsByTagName('head')[0].removeChild(DT._elDynStyleNode);
            DT._elDynStyleNode = null;
        }
    }

    YAHOO.log("DataTable instance destroyed: " + instanceName);
},

/**
 * Displays message within secondary TBODY.
 *
 * @method showTableMessage
 * @param sHTML {HTML} (optional) Value for innerHTML.
 * @param sClassName {String} (optional) Classname.
 */
showTableMessage : function(sHTML, sClassName) {
    var elCell = this._elMsgTd;
    if(lang.isString(sHTML)) {
        elCell.firstChild.innerHTML = sHTML;
    }
    if(lang.isString(sClassName)) {
        elCell.className = sClassName;
    }

    this._elMsgTbody.style.display = "";

    this.fireEvent("tableMsgShowEvent", {html:sHTML, className:sClassName});
    YAHOO.log("DataTable showing message: " + sHTML, "info", this.toString());
},

/**
 * Hides secondary TBODY.
 *
 * @method hideTableMessage
 */
hideTableMessage : function() {
    if(this._elMsgTbody.style.display != "none") {
        this._elMsgTbody.style.display = "none";
        this._elMsgTbody.parentNode.style.width = "";
        this.fireEvent("tableMsgHideEvent");
        YAHOO.log("DataTable message hidden", "info", this.toString());
    }
},

/**
 * Brings focus to the TBODY element. Alias to focusTbodyEl.
 *
 * @method focus
 */
focus : function() {
    this.focusTbodyEl();
},

/**
 * Brings focus to the THEAD element.
 *
 * @method focusTheadEl
 */
focusTheadEl : function() {
    this._focusEl(this._elThead);
},

/**
 * Brings focus to the TBODY element.
 *
 * @method focusTbodyEl
 */
focusTbodyEl : function() {
    this._focusEl(this._elTbody);
},

/**
 * Setting display:none on DataTable or any parent may impact width validations.
 * After setting display back to "", implementers should call this method to 
 * manually perform those validations.
 *
 * @method onShow
 */
onShow : function() {
    this.validateColumnWidths();
    
    for(var allKeys = this._oColumnSet.keys, i=0, len=allKeys.length, col; i<len; i++) {
        col = allKeys[i];
        if(col._ddResizer) {
            col._ddResizer.resetResizerEl();
        }
    }
},



































































// RECORDSET FUNCTIONS

/**
 * Returns Record index for given TR element or page row index.
 *
 * @method getRecordIndex
 * @param row {YAHOO.widget.Record | HTMLElement | Number} Record instance, TR
 * element reference or page row index.
 * @return {Number} Record's RecordSet index, or null.
 */
getRecordIndex : function(row) {
    var nTrIndex;

    if(!lang.isNumber(row)) {
        // By Record
        if(row instanceof YAHOO.widget.Record) {
            return this._oRecordSet.getRecordIndex(row);
        }
        // By element reference
        else {
            // Find the TR element
            var el = this.getTrEl(row);
            if(el) {
                nTrIndex = el.sectionRowIndex;
            }
        }
    }
    // By page row index
    else {
        nTrIndex = row;
    }

    if(lang.isNumber(nTrIndex)) {
        var oPaginator = this.get("paginator");
        if(oPaginator) {
            return oPaginator.get('recordOffset') + nTrIndex;
        }
        else {
            return nTrIndex;
        }
    }

    YAHOO.log("Could not get Record index for row " + row, "info", this.toString());
    return null;
},

/**
 * For the given identifier, returns the associated Record instance.
 *
 * @method getRecord
 * @param row {HTMLElement | Number | String} DOM reference to a TR element (or
 * child of a TR element), RecordSet position index, or Record ID.
 * @return {YAHOO.widget.Record} Record instance.
 */
getRecord : function(row) {
    var oRecord = this._oRecordSet.getRecord(row);

    if(!oRecord) {
        // Validate TR element
        var elRow = this.getTrEl(row);
        if(elRow) {
            oRecord = this._oRecordSet.getRecord(elRow.id);
        }
    }

    if(oRecord instanceof YAHOO.widget.Record) {
        return this._oRecordSet.getRecord(oRecord);
    }
    else {
        YAHOO.log("Could not get Record for row at " + row, "info", this.toString());
        return null;
    }
},














































// COLUMN FUNCTIONS

/**
 * For the given identifier, returns the associated Column instance. Note: For
 * getting Columns by Column ID string, please use the method getColumnById().
 *
 * @method getColumn
 * @param column {HTMLElement | String | Number} TH/TD element (or child of a
 * TH/TD element), a Column key, or a ColumnSet key index.
 * @return {YAHOO.widget.Column} Column instance.
 */
getColumn : function(column) {
    var oColumn = this._oColumnSet.getColumn(column);

    if(!oColumn) {
        // Validate TD element
        var elCell = this.getTdEl(column);
        if(elCell) {
            oColumn = this._oColumnSet.getColumn(this.getCellIndex(elCell));
        }
        // Validate TH element
        else {
            elCell = this.getThEl(column);
            if(elCell) {
                // Find by TH el ID
                var allColumns = this._oColumnSet.flat;
                for(var i=0, len=allColumns.length; i<len; i++) {
                    if(allColumns[i].getThEl().id === elCell.id) {
                        oColumn = allColumns[i];
                    } 
                }
            }
        }
    }
    if(!oColumn) {
        YAHOO.log("Could not get Column for column at " + column, "info", this.toString());
    }
    return oColumn;
},

/**
 * For the given Column ID, returns the associated Column instance. Note: For
 * getting Columns by key, please use the method getColumn().
 *
 * @method getColumnById
 * @param column {String} Column ID string.
 * @return {YAHOO.widget.Column} Column instance.
 */
getColumnById : function(column) {
    return this._oColumnSet.getColumnById(column);
},

/**
 * For the given Column instance, returns next direction to sort.
 *
 * @method getColumnSortDir
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param oSortedBy {Object} (optional) Specify the state, or use current state. 
 * @return {String} YAHOO.widget.DataTable.CLASS_ASC or YAHOO.widget.DataTableCLASS_DESC.
 */
getColumnSortDir : function(oColumn, oSortedBy) {
    // Backward compatibility
    if(oColumn.sortOptions && oColumn.sortOptions.defaultDir) {
        if(oColumn.sortOptions.defaultDir == "asc") {
            oColumn.sortOptions.defaultDir = DT.CLASS_ASC;
        }
        else if (oColumn.sortOptions.defaultDir == "desc") {
            oColumn.sortOptions.defaultDir = DT.CLASS_DESC;
        }
    }
    
    // What is the Column's default sort direction?
    var sortDir = (oColumn.sortOptions && oColumn.sortOptions.defaultDir) ? oColumn.sortOptions.defaultDir : DT.CLASS_ASC;

    // Is the Column currently sorted?
    var bSorted = false;
    oSortedBy = oSortedBy || this.get("sortedBy");
    if(oSortedBy && (oSortedBy.key === oColumn.key)) {
        bSorted = true;
        if(oSortedBy.dir) {
            sortDir = (oSortedBy.dir === DT.CLASS_ASC) ? DT.CLASS_DESC : DT.CLASS_ASC;
        }
        else {
            sortDir = (sortDir === DT.CLASS_ASC) ? DT.CLASS_DESC : DT.CLASS_ASC;
        }
    }
    return sortDir;
},

/**
 * Overridable method gives implementers a hook to show loading message before
 * sorting Column.
 *
 * @method doBeforeSortColumn
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param sSortDir {String} YAHOO.widget.DataTable.CLASS_ASC or
 * YAHOO.widget.DataTable.CLASS_DESC.
 * @return {Boolean} Return true to continue sorting Column.
 */
doBeforeSortColumn : function(oColumn, sSortDir) {
    this.showTableMessage(this.get("MSG_LOADING"), DT.CLASS_LOADING);
    return true;
},

/**
 * Sorts given Column. If "dynamicData" is true, current selections are purged before
 * a request is sent to the DataSource for data for the new state (using the
 * request returned by "generateRequest()").
 *
 * @method sortColumn
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param sDir {String} (Optional) YAHOO.widget.DataTable.CLASS_ASC or
 * YAHOO.widget.DataTable.CLASS_DESC
 */
sortColumn : function(oColumn, sDir) {
    if(oColumn && (oColumn instanceof YAHOO.widget.Column)) {
        if(!oColumn.sortable) {
            Dom.addClass(this.getThEl(oColumn), DT.CLASS_SORTABLE);
        }
        
        // Validate given direction
        if(sDir && (sDir !== DT.CLASS_ASC) && (sDir !== DT.CLASS_DESC)) {
            sDir = null;
        }
        
        // Get the sort dir
        var sSortDir = sDir || this.getColumnSortDir(oColumn);

        // Is the Column currently sorted?
        var oSortedBy = this.get("sortedBy") || {};
        var bSorted = (oSortedBy.key === oColumn.key) ? true : false;

        var ok = this.doBeforeSortColumn(oColumn, sSortDir);
        if(ok) {
            // Server-side sort
            if(this.get("dynamicData")) {
                // Get current state
                var oState = this.getState();
                
                // Reset record offset, if paginated
                if(oState.pagination) {
                    oState.pagination.recordOffset = 0;
                }
                
                // Update sortedBy to new values
                oState.sortedBy = {
                    key: oColumn.key,
                    dir: sSortDir
                };
                
                // Get the request for the new state
                var request = this.get("generateRequest")(oState, this);

                // Purge selections
                this.unselectAllRows();
                this.unselectAllCells();

                // Send request for new data
                var callback = {
                    success : this.onDataReturnSetRows,
                    failure : this.onDataReturnSetRows,
                    argument : oState, // Pass along the new state to the callback
                    scope : this
                };
                this._oDataSource.sendRequest(request, callback);            
            }
            // Client-side sort
            else {
                // Is there a custom sort handler function defined?
                var sortFnc = (oColumn.sortOptions && lang.isFunction(oColumn.sortOptions.sortFunction)) ?
                        // Custom sort function
                        oColumn.sortOptions.sortFunction : null;
                   
                // Sort the Records
                if(!bSorted || sDir || sortFnc) {
                    // Default sort function if necessary
                    sortFnc = sortFnc || this.get("sortFunction");
                    // Get the field to sort
                    var sField = (oColumn.sortOptions && oColumn.sortOptions.field) ? oColumn.sortOptions.field : oColumn.field;

                    // Sort the Records        
                    this._oRecordSet.sortRecords(sortFnc, ((sSortDir == DT.CLASS_DESC) ? true : false), sField);
                }
                // Just reverse the Records
                else {
                    this._oRecordSet.reverseRecords();
                }
        
                // Reset to first page if paginated
                var oPaginator = this.get('paginator');
                if (oPaginator) {
                    // Set page silently, so as not to fire change event.
                    oPaginator.setPage(1,true);
                }
        
                // Update UI via sortedBy
                this.render();
                this.set("sortedBy", {key:oColumn.key, dir:sSortDir, column:oColumn}); 
            }       
            
            this.fireEvent("columnSortEvent",{column:oColumn,dir:sSortDir});
            YAHOO.log("Column \"" + oColumn.key + "\" sorted \"" + sSortDir + "\"", "info", this.toString());
            return;
        }
    }
    YAHOO.log("Could not sort Column \"" + oColumn.key + "\"", "warn", this.toString());
},

/**
 * Sets given Column to given pixel width. If new width is less than minimum
 * width, sets to minimum width. Updates oColumn.width value.
 *
 * @method setColumnWidth
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param nWidth {Number} New width in pixels. A null value auto-sizes Column,
 * subject to minWidth and maxAutoWidth validations. 
 */
setColumnWidth : function(oColumn, nWidth) {
    if(!(oColumn instanceof YAHOO.widget.Column)) {
        oColumn = this.getColumn(oColumn);
    }
    if(oColumn) {
        // Validate new width against minimum width
        if(lang.isNumber(nWidth)) {
            // This is why we must require a Number... :-|
            nWidth = (nWidth > oColumn.minWidth) ? nWidth : oColumn.minWidth;

            // Save state
            oColumn.width = nWidth;
            
            // Resize the DOM elements
            this._setColumnWidth(oColumn, nWidth+"px");
            
            this.fireEvent("columnSetWidthEvent",{column:oColumn,width:nWidth});
            YAHOO.log("Set width of Column " + oColumn + " to " + nWidth + "px", "info", this.toString());
        }
        // Unsets a width to auto-size
        else if(nWidth === null) {
            // Save state
            oColumn.width = nWidth;
            
            // Resize the DOM elements
            this._setColumnWidth(oColumn, "auto");
            this.validateColumnWidths(oColumn);
            this.fireEvent("columnUnsetWidthEvent",{column:oColumn});
            YAHOO.log("Column " + oColumn + " width unset", "info", this.toString());
        }
                
        // Bug 2339454: resize then sort misaligment
        this._clearTrTemplateEl();
    }
    else {
        YAHOO.log("Could not set width of Column " + oColumn + " to " + nWidth + "px", "warn", this.toString());
    }
},

/**
 * Sets liner DIV elements of given Column to given width. When value should be
 * auto-calculated to fit content overflow is set to visible, otherwise overflow
 * is set to hidden. No validations against minimum width and no updating
 * Column.width value.
 *
 * @method _setColumnWidth
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param sWidth {String} New width value.
 * @param sOverflow {String} Should be "hidden" when Column width is explicitly
 * being set to a value, but should be "visible" when Column is meant to auto-fit content.  
 * @private
 */
_setColumnWidth : function(oColumn, sWidth, sOverflow) {
    if(oColumn && (oColumn.getKeyIndex() !== null)) {
        sOverflow = sOverflow || (((sWidth === '') || (sWidth === 'auto')) ? 'visible' : 'hidden');
    
        // Dynamic style algorithm
        if(!DT._bDynStylesFallback) {
            this._setColumnWidthDynStyles(oColumn, sWidth, sOverflow);
        }
        // Dynamic function algorithm
        else {
            this._setColumnWidthDynFunction(oColumn, sWidth, sOverflow);
        }
    }
    else {
        YAHOO.log("Could not set width of unknown Column " + oColumn + " to " + sWidth, "warn", this.toString());
    }
},

/**
 * Updates width of a Column's liner DIV elements by dynamically creating a
 * STYLE node and writing and updating CSS style rules to it. If this fails during
 * runtime, the fallback method _setColumnWidthDynFunction() will be called.
 * Notes: This technique is not performant in IE6. IE7 crashes if DataTable is
 * nested within another TABLE element. For these cases, it is recommended to
 * use the method _setColumnWidthDynFunction by setting _bDynStylesFallback to TRUE.
 *
 * @method _setColumnWidthDynStyles
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param sWidth {String} New width value.
 * @private
 */
_setColumnWidthDynStyles : function(oColumn, sWidth, sOverflow) {
    var s = DT._elDynStyleNode,
        rule;
    
    // Create a new STYLE node
    if(!s) {
        s = document.createElement('style');
        s.type = 'text/css';
        s = document.getElementsByTagName('head').item(0).appendChild(s);
        DT._elDynStyleNode = s;
    }
    
    // We have a STYLE node to update
    if(s) {
        // Use unique classname for this Column instance as a hook for resizing
        var sClassname = "." + this.getId() + "-col-" + oColumn.getSanitizedKey() + " ." + DT.CLASS_LINER;
        
        // Hide for performance
        if(this._elTbody) {
            this._elTbody.style.display = 'none';
        }
        
        rule = DT._oDynStyles[sClassname];

        // The Column does not yet have a rule
        if(!rule) {
            if(s.styleSheet && s.styleSheet.addRule) {
                s.styleSheet.addRule(sClassname,"overflow:"+sOverflow);
                s.styleSheet.addRule(sClassname,'width:'+sWidth);
                rule = s.styleSheet.rules[s.styleSheet.rules.length-1];
                DT._oDynStyles[sClassname] = rule;
            }
            else if(s.sheet && s.sheet.insertRule) {
                s.sheet.insertRule(sClassname+" {overflow:"+sOverflow+";width:"+sWidth+";}",s.sheet.cssRules.length);
                rule = s.sheet.cssRules[s.sheet.cssRules.length-1];
                DT._oDynStyles[sClassname] = rule;
            }
        }
        // We have a rule to update
        else {
            rule.style.overflow = sOverflow;
            rule.style.width = sWidth;
        } 
        
        // Unhide
        if(this._elTbody) {
            this._elTbody.style.display = '';
        }
    }
    
    // That was not a success, we must call the fallback routine
    if(!rule) {
        DT._bDynStylesFallback = true;
        this._setColumnWidthDynFunction(oColumn, sWidth);
    }
},

/**
 * Updates width of a Column's liner DIV elements by dynamically creating a
 * function to update all element style properties in one pass. Note: This
 * technique is not supported in sandboxed environments that prohibit EVALs.    
 *
 * @method _setColumnWidthDynFunction
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param sWidth {String} New width value.
 * @private
 */
_setColumnWidthDynFunction : function(oColumn, sWidth, sOverflow) {
    // TODO: why is this here?
    if(sWidth == 'auto') {
        sWidth = ''; 
    }
    
    // Create one function for each value of rows.length
    var rowslen = this._elTbody ? this._elTbody.rows.length : 0;
    
    // Dynamically create the function
    if (!this._aDynFunctions[rowslen]) {
        
        //Compile a custom function to do all the liner div width
        //assignments at the same time.  A unique function is required
        //for each unique number of rows in _elTbody.  This will
        //result in a function declaration like:
        //function (oColumn,sWidth,sOverflow) {
        //    var colIdx = oColumn.getKeyIndex();
        //    oColumn.getThLinerEl().style.overflow =
        //    this._elTbody.rows[0].cells[colIdx].firstChild.style.overflow =
        //    this._elTbody.rows[1].cells[colIdx].firstChild.style.overflow =
        //    ... (for all row indices in this._elTbody.rows.length - 1)
        //    this._elTbody.rows[99].cells[colIdx].firstChild.style.overflow =
        //    sOverflow;
        //    oColumn.getThLinerEl().style.width =
        //    this._elTbody.rows[0].cells[colIdx].firstChild.style.width =
        //    this._elTbody.rows[1].cells[colIdx].firstChild.style.width =
        //    ... (for all row indices in this._elTbody.rows.length - 1)
        //    this._elTbody.rows[99].cells[colIdx].firstChild.style.width =
        //    sWidth;
        //}
        
        var i,j,k;
        var resizerDef = [
            'var colIdx=oColumn.getKeyIndex();',
            'oColumn.getThLinerEl().style.overflow='
        ];
        for (i=rowslen-1, j=2; i >= 0; --i) {
            resizerDef[j++] = 'this._elTbody.rows[';
            resizerDef[j++] = i;
            resizerDef[j++] = '].cells[colIdx].firstChild.style.overflow=';
        }
        resizerDef[j] = 'sOverflow;';
        resizerDef[j+1] = 'oColumn.getThLinerEl().style.width=';
        for (i=rowslen-1, k=j+2; i >= 0; --i) {
            resizerDef[k++] = 'this._elTbody.rows[';
            resizerDef[k++] = i;
            resizerDef[k++] = '].cells[colIdx].firstChild.style.width=';
        }
        resizerDef[k] = 'sWidth;';
        this._aDynFunctions[rowslen] =
            new Function('oColumn','sWidth','sOverflow',resizerDef.join(''));
    }
    
    // Get the function to execute
    var resizerFn = this._aDynFunctions[rowslen];

    // TODO: Hide TBODY for performance in _setColumnWidthDynFunction?
    if (resizerFn) {
        resizerFn.call(this,oColumn,sWidth,sOverflow);
    }
},

/**
 * For one or all Columns, when Column is not hidden, width is not set, and minWidth
 * and/or maxAutoWidth is set, validates auto-width against minWidth and maxAutoWidth.
 *
 * @method validateColumnWidths
 * @param oArg.column {YAHOO.widget.Column} (optional) One Column to validate. If null, all Columns' widths are validated.
 */
validateColumnWidths : function(oColumn) {
    var elColgroup = this._elColgroup;
    var elColgroupClone = elColgroup.cloneNode(true);
    var bNeedsValidation = false;
    var allKeys = this._oColumnSet.keys;
    var elThLiner;
    // Validate just one Column's minWidth and/or maxAutoWidth
    if(oColumn && !oColumn.hidden && !oColumn.width && (oColumn.getKeyIndex() !== null)) {
            elThLiner = oColumn.getThLinerEl();
            if((oColumn.minWidth > 0) && (elThLiner.offsetWidth < oColumn.minWidth)) {
                elColgroupClone.childNodes[oColumn.getKeyIndex()].style.width = 
                        oColumn.minWidth + 
                        (parseInt(Dom.getStyle(elThLiner,"paddingLeft"),10)|0) +
                        (parseInt(Dom.getStyle(elThLiner,"paddingRight"),10)|0) + "px";
                bNeedsValidation = true;
            }
            else if((oColumn.maxAutoWidth > 0) && (elThLiner.offsetWidth > oColumn.maxAutoWidth)) {
                this._setColumnWidth(oColumn, oColumn.maxAutoWidth+"px", "hidden");
            }
    }
    // Validate all Columns
    else {
        for(var i=0, len=allKeys.length; i<len; i++) {
            oColumn = allKeys[i];
            if(!oColumn.hidden && !oColumn.width) {
                elThLiner = oColumn.getThLinerEl();
                if((oColumn.minWidth > 0) && (elThLiner.offsetWidth < oColumn.minWidth)) {
                    elColgroupClone.childNodes[i].style.width = 
                            oColumn.minWidth + 
                            (parseInt(Dom.getStyle(elThLiner,"paddingLeft"),10)|0) +
                            (parseInt(Dom.getStyle(elThLiner,"paddingRight"),10)|0) + "px";
                    bNeedsValidation = true;
                }
                else if((oColumn.maxAutoWidth > 0) && (elThLiner.offsetWidth > oColumn.maxAutoWidth)) {
                    this._setColumnWidth(oColumn, oColumn.maxAutoWidth+"px", "hidden");
                }
            }
        }
    }
    if(bNeedsValidation) {
        elColgroup.parentNode.replaceChild(elColgroupClone, elColgroup);
        this._elColgroup = elColgroupClone;
    }
},

/**
 * Clears minWidth.
 *
 * @method _clearMinWidth
 * @param oColumn {YAHOO.widget.Column} Which Column.
 * @private
 */
_clearMinWidth : function(oColumn) {
    if(oColumn.getKeyIndex() !== null) {
        this._elColgroup.childNodes[oColumn.getKeyIndex()].style.width = '';
    }
},

/**
 * Restores minWidth.
 *
 * @method _restoreMinWidth
 * @param oColumn {YAHOO.widget.Column} Which Column.
 * @private
 */
_restoreMinWidth : function(oColumn) {
    if(oColumn.minWidth && (oColumn.getKeyIndex() !== null)) {
        this._elColgroup.childNodes[oColumn.getKeyIndex()].style.width = oColumn.minWidth + 'px';
    }
},

/**
 * Hides given Column. NOTE: You cannot hide/show nested Columns. You can only
 * hide/show non-nested Columns, and top-level parent Columns (which will
 * hide/show all children Columns).
 *
 * @method hideColumn
 * @param oColumn {YAHOO.widget.Column | HTMLElement | String | Number} Column
 * instance, TH/TD element (or child of a TH/TD element), a Column key, or a
 * ColumnSet key index.
 */
hideColumn : function(oColumn) {
    if(!(oColumn instanceof YAHOO.widget.Column)) {
        oColumn = this.getColumn(oColumn);
    }
    // Only top-level Columns can get hidden due to issues in FF2 and SF3
    if(oColumn && !oColumn.hidden && oColumn.getTreeIndex() !== null) {
        
        var allrows = this.getTbodyEl().rows;
        var l = allrows.length;
        var allDescendants = this._oColumnSet.getDescendants(oColumn);
        
        // Hide each nested Column
        for(var i=0, len=allDescendants.length; i<len; i++) {
            var thisColumn = allDescendants[i];
            thisColumn.hidden = true;

            // Style the head cell
            Dom.addClass(thisColumn.getThEl(), DT.CLASS_HIDDEN);
            
            // Does this Column have body cells?
            var thisKeyIndex = thisColumn.getKeyIndex();
            if(thisKeyIndex !== null) {                    
                // Clear minWidth
                this._clearMinWidth(oColumn);
                
                // Style the body cells
                for(var j=0;j<l;j++) {
                    Dom.addClass(allrows[j].cells[thisKeyIndex],DT.CLASS_HIDDEN);
                }
            }
            
            this.fireEvent("columnHideEvent",{column:thisColumn});
            YAHOO.log("Column \"" + oColumn.key + "\" hidden", "info", this.toString());
        }
      
        this._repaintOpera();
        this._clearTrTemplateEl();
    }
    else {
        YAHOO.log("Could not hide Column \"" + lang.dump(oColumn) + "\". Only non-nested Columns can be hidden", "warn", this.toString());
    }
},

/**
 * Shows given Column. NOTE: You cannot hide/show nested Columns. You can only
 * hide/show non-nested Columns, and top-level parent Columns (which will
 * hide/show all children Columns).
 *
 * @method showColumn
 * @param oColumn {YAHOO.widget.Column | HTMLElement | String | Number} Column
 * instance, TH/TD element (or child of a TH/TD element), a Column key, or a
 * ColumnSet key index.
 */
showColumn : function(oColumn) {
    if(!(oColumn instanceof YAHOO.widget.Column)) {
        oColumn = this.getColumn(oColumn);
    }
    // Only top-level Columns can get hidden
    if(oColumn && oColumn.hidden && (oColumn.getTreeIndex() !== null)) {
        var allrows = this.getTbodyEl().rows;
        var l = allrows.length;
        var allDescendants = this._oColumnSet.getDescendants(oColumn);
        
        // Show each nested Column
        for(var i=0, len=allDescendants.length; i<len; i++) {
            var thisColumn = allDescendants[i];
            thisColumn.hidden = false;
            
            // Unstyle the head cell
            Dom.removeClass(thisColumn.getThEl(), DT.CLASS_HIDDEN);

            // Does this Column have body cells?
            var thisKeyIndex = thisColumn.getKeyIndex();
            if(thisKeyIndex !== null) {
                // Restore minWidth
                this._restoreMinWidth(oColumn);
                
            
                // Unstyle the body cells
                for(var j=0;j<l;j++) {
                    Dom.removeClass(allrows[j].cells[thisKeyIndex],DT.CLASS_HIDDEN);
                }
            }

            this.fireEvent("columnShowEvent",{column:thisColumn});
            YAHOO.log("Column \"" + oColumn.key + "\" shown", "info", this.toString());
        }
        this._clearTrTemplateEl();
    }
    else {
        YAHOO.log("Could not show Column \"" + lang.dump(oColumn) + "\". Only non-nested Columns can be shown", "warn", this.toString());
    }
},

/**
 * Removes given Column. NOTE: You cannot remove nested Columns. You can only remove
 * non-nested Columns, and top-level parent Columns (which will remove all
 * children Columns).
 *
 * @method removeColumn
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @return oColumn {YAHOO.widget.Column} Removed Column instance.
 */
removeColumn : function(oColumn) {
    // Validate Column
    if(!(oColumn instanceof YAHOO.widget.Column)) {
        oColumn = this.getColumn(oColumn);
    }
    if(oColumn) {
        var nColTreeIndex = oColumn.getTreeIndex();
        if(nColTreeIndex !== null) {
            // Which key index(es)
            var i, len,
                aKeyIndexes = oColumn.getKeyIndex();
            // Must be a parent Column
            if(aKeyIndexes === null) {
                var descKeyIndexes = [];
                var allDescendants = this._oColumnSet.getDescendants(oColumn);
                for(i=0, len=allDescendants.length; i<len; i++) {
                    // Is this descendant a key Column?
                    var thisKey = allDescendants[i].getKeyIndex();
                    if(thisKey !== null) {
                        descKeyIndexes[descKeyIndexes.length] = thisKey;
                    }
                }
                if(descKeyIndexes.length > 0) {
                    aKeyIndexes = descKeyIndexes;
                }
            }
            // Must be a key Column
            else {
                aKeyIndexes = [aKeyIndexes];
            }
            
            if(aKeyIndexes !== null) {
                // Sort the indexes so we can remove from the right
                aKeyIndexes.sort(function(a, b) {return YAHOO.util.Sort.compare(a, b);});
                
                // Destroy previous THEAD
                this._destroyTheadEl();
    
                // Create new THEAD
                var aOrigColumnDefs = this._oColumnSet.getDefinitions();
                oColumn = aOrigColumnDefs.splice(nColTreeIndex,1)[0];
                this._initColumnSet(aOrigColumnDefs);
                this._initTheadEl();
                
                // Remove COL
                for(i=aKeyIndexes.length-1; i>-1; i--) {
                    this._removeColgroupColEl(aKeyIndexes[i]);
                }
                
                // Remove TD
                var allRows = this._elTbody.rows;
                if(allRows.length > 0) {
                    var loopN = this.get("renderLoopSize"),
                        loopEnd = allRows.length;
                    this._oChainRender.add({
                        method: function(oArg) {
                            if((this instanceof DT) && this._sId) {
                                var i = oArg.nCurrentRow,
                                    len = loopN > 0 ? Math.min(i + loopN,allRows.length) : allRows.length,
                                    aIndexes = oArg.aIndexes,
                                    j;
                                for(; i < len; ++i) {
                                    for(j = aIndexes.length-1; j>-1; j--) {
                                        allRows[i].removeChild(allRows[i].childNodes[aIndexes[j]]);
                                    }
                                }
                                oArg.nCurrentRow = i;
                            }
                        },
                        iterations: (loopN > 0) ? Math.ceil(loopEnd/loopN) : 1,
                        argument: {nCurrentRow:0, aIndexes:aKeyIndexes},
                        scope: this,
                        timeout: (loopN > 0) ? 0 : -1
                    });
                    this._runRenderChain();
                }
        
                this.fireEvent("columnRemoveEvent",{column:oColumn});
                YAHOO.log("Column \"" + oColumn.key + "\" removed", "info", this.toString());
                return oColumn;
            }
        }
    }
    YAHOO.log("Could not remove Column \"" + oColumn.key + "\". Only non-nested Columns can be removed", "warn", this.toString());
},

/**
 * Inserts given Column at the index if given, otherwise at the end. NOTE: You
 * can only add non-nested Columns and top-level parent Columns. You cannot add
 * a nested Column to an existing parent.
 *
 * @method insertColumn
 * @param oColumn {Object | YAHOO.widget.Column} Object literal Column
 * definition or a Column instance.
 * @param index {Number} (optional) New tree index.
 * @return oColumn {YAHOO.widget.Column} Inserted Column instance. 
 */
insertColumn : function(oColumn, index) {
    // Validate Column
    if(oColumn instanceof YAHOO.widget.Column) {
        oColumn = oColumn.getDefinition();
    }
    else if(oColumn.constructor !== Object) {
        YAHOO.log("Could not insert Column \"" + oColumn + "\" due to invalid argument", "warn", this.toString());
        return;
    }
    
    // Validate index or append new Column to the end of the ColumnSet
    var oColumnSet = this._oColumnSet;
    if(!lang.isValue(index) || !lang.isNumber(index)) {
        index = oColumnSet.tree[0].length;
    }
    
    // Destroy previous THEAD
    this._destroyTheadEl();
    
    // Create new THEAD
    var aNewColumnDefs = this._oColumnSet.getDefinitions();
    aNewColumnDefs.splice(index, 0, oColumn);
    this._initColumnSet(aNewColumnDefs);
    this._initTheadEl();
    
    // Need to refresh the reference
    oColumnSet = this._oColumnSet;
    var oNewColumn = oColumnSet.tree[0][index];
    
    // Get key index(es) for new Column
    var i, len,
        descKeyIndexes = [];
    var allDescendants = oColumnSet.getDescendants(oNewColumn);
    for(i=0, len=allDescendants.length; i<len; i++) {
        // Is this descendant a key Column?
        var thisKey = allDescendants[i].getKeyIndex();
        if(thisKey !== null) {
            descKeyIndexes[descKeyIndexes.length] = thisKey;
        }
    }
    
    if(descKeyIndexes.length > 0) {  
        // Sort the indexes
        var newIndex = descKeyIndexes.sort(function(a, b) {return YAHOO.util.Sort.compare(a, b);})[0];
        
        // Add COL
        for(i=descKeyIndexes.length-1; i>-1; i--) {
            this._insertColgroupColEl(descKeyIndexes[i]);
        }
            
        // Add TD
        var allRows = this._elTbody.rows;
        if(allRows.length > 0) {
            var loopN = this.get("renderLoopSize"),
                loopEnd = allRows.length;
            
            // Get templates for each new TD
            var aTdTemplates = [],
                elTdTemplate;
            for(i=0, len=descKeyIndexes.length; i<len; i++) {
                var thisKeyIndex = descKeyIndexes[i];
                elTdTemplate = this._getTrTemplateEl().childNodes[i].cloneNode(true);
                elTdTemplate = this._formatTdEl(this._oColumnSet.keys[thisKeyIndex], elTdTemplate, thisKeyIndex, (thisKeyIndex===this._oColumnSet.keys.length-1));
                aTdTemplates[thisKeyIndex] = elTdTemplate;
            }
            
            this._oChainRender.add({
                method: function(oArg) {
                    if((this instanceof DT) && this._sId) {
                        var i = oArg.nCurrentRow, j,
                            descKeyIndexes = oArg.descKeyIndexes,
                            len = loopN > 0 ? Math.min(i + loopN,allRows.length) : allRows.length,
                            nextSibling;
                        for(; i < len; ++i) {
                            nextSibling = allRows[i].childNodes[newIndex] || null;
                            for(j=descKeyIndexes.length-1; j>-1; j--) {
                                allRows[i].insertBefore(oArg.aTdTemplates[descKeyIndexes[j]].cloneNode(true), nextSibling);
                            }
                        }
                        oArg.nCurrentRow = i;
                    }
                },
                iterations: (loopN > 0) ? Math.ceil(loopEnd/loopN) : 1,
                argument: {nCurrentRow:0,aTdTemplates:aTdTemplates,descKeyIndexes:descKeyIndexes},
                scope: this,
                timeout: (loopN > 0) ? 0 : -1
            });
            this._runRenderChain(); 
        }

        this.fireEvent("columnInsertEvent",{column:oColumn,index:index});
        YAHOO.log("Column \"" + oColumn.key + "\" inserted into index " + index, "info", this.toString());
        return oNewColumn;
    }
},

/**
 * Removes given Column and inserts into given tree index. NOTE: You
 * can only reorder non-nested Columns and top-level parent Columns. You cannot
 * reorder a nested Column to an existing parent.
 *
 * @method reorderColumn
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param index {Number} New tree index.
 * @return oColumn {YAHOO.widget.Column} Reordered Column instance. 
 */
reorderColumn : function(oColumn, index) {
    // Validate Column and new index
    if(!(oColumn instanceof YAHOO.widget.Column)) {
        oColumn = this.getColumn(oColumn);
    }
    if(oColumn && YAHOO.lang.isNumber(index)) {
        var nOrigTreeIndex = oColumn.getTreeIndex();
        if((nOrigTreeIndex !== null) && (nOrigTreeIndex !== index)) {
            // Which key index(es)
            var i, len,
                aOrigKeyIndexes = oColumn.getKeyIndex(),
                allDescendants,
                descKeyIndexes = [],
                thisKey;
            // Must be a parent Column...
            if(aOrigKeyIndexes === null) {
                allDescendants = this._oColumnSet.getDescendants(oColumn);
                for(i=0, len=allDescendants.length; i<len; i++) {
                    // Is this descendant a key Column?
                    thisKey = allDescendants[i].getKeyIndex();
                    if(thisKey !== null) {
                        descKeyIndexes[descKeyIndexes.length] = thisKey;
                    }
                }
                if(descKeyIndexes.length > 0) {
                    aOrigKeyIndexes = descKeyIndexes;
                }
            }
            // ...or else must be a key Column
            else {
                aOrigKeyIndexes = [aOrigKeyIndexes];
            }
            
            if(aOrigKeyIndexes !== null) {                   
                // Sort the indexes
                aOrigKeyIndexes.sort(function(a, b) {return YAHOO.util.Sort.compare(a, b);});
                
                // Destroy previous THEAD
                this._destroyTheadEl();
    
                // Create new THEAD
                var aColumnDefs = this._oColumnSet.getDefinitions();
                var oColumnDef = aColumnDefs.splice(nOrigTreeIndex,1)[0];
                aColumnDefs.splice(index, 0, oColumnDef);
                this._initColumnSet(aColumnDefs);
                this._initTheadEl();
                
                // Need to refresh the reference
                var oNewColumn = this._oColumnSet.tree[0][index];

                // What are new key index(es)
                var aNewKeyIndexes = oNewColumn.getKeyIndex();
                // Must be a parent Column
                if(aNewKeyIndexes === null) {
                    descKeyIndexes = [];
                    allDescendants = this._oColumnSet.getDescendants(oNewColumn);
                    for(i=0, len=allDescendants.length; i<len; i++) {
                        // Is this descendant a key Column?
                        thisKey = allDescendants[i].getKeyIndex();
                        if(thisKey !== null) {
                            descKeyIndexes[descKeyIndexes.length] = thisKey;
                        }
                    }
                    if(descKeyIndexes.length > 0) {
                        aNewKeyIndexes = descKeyIndexes;
                    }
                }
                // Must be a key Column
                else {
                    aNewKeyIndexes = [aNewKeyIndexes];
                }
                
                // Sort the new indexes and grab the first one for the new location
                var newIndex = aNewKeyIndexes.sort(function(a, b) {return YAHOO.util.Sort.compare(a, b);})[0];

                // Reorder COL
                this._reorderColgroupColEl(aOrigKeyIndexes, newIndex);
                
                // Reorder TD
                var allRows = this._elTbody.rows;
                if(allRows.length > 0) {
                    var loopN = this.get("renderLoopSize"),
                        loopEnd = allRows.length;
                    this._oChainRender.add({
                        method: function(oArg) {
                            if((this instanceof DT) && this._sId) {
                                var i = oArg.nCurrentRow, j, tmpTds, nextSibling,
                                    len = loopN > 0 ? Math.min(i + loopN,allRows.length) : allRows.length,
                                    aIndexes = oArg.aIndexes, thisTr;
                                // For each row
                                for(; i < len; ++i) {
                                    tmpTds = [];
                                    thisTr = allRows[i];
                                    
                                    // Remove each TD
                                    for(j=aIndexes.length-1; j>-1; j--) {
                                        tmpTds.push(thisTr.removeChild(thisTr.childNodes[aIndexes[j]]));
                                    }
                                    
                                    // Insert each TD
                                    nextSibling = thisTr.childNodes[newIndex] || null;
                                    for(j=tmpTds.length-1; j>-1; j--) {
                                        thisTr.insertBefore(tmpTds[j], nextSibling);
                                    }                                    
                                }
                                oArg.nCurrentRow = i;
                            }
                        },
                        iterations: (loopN > 0) ? Math.ceil(loopEnd/loopN) : 1,
                        argument: {nCurrentRow:0, aIndexes:aOrigKeyIndexes},
                        scope: this,
                        timeout: (loopN > 0) ? 0 : -1
                    });
                    this._runRenderChain();
                }
        
                this.fireEvent("columnReorderEvent",{column:oNewColumn, oldIndex:nOrigTreeIndex});
                YAHOO.log("Column \"" + oNewColumn.key + "\" reordered", "info", this.toString());
                return oNewColumn;
            }
        }
    }
    YAHOO.log("Could not reorder Column \"" + oColumn.key + "\". Only non-nested Columns can be reordered", "warn", this.toString());
},

/**
 * Selects given Column. NOTE: You cannot select/unselect nested Columns. You can only
 * select/unselect non-nested Columns, and bottom-level key Columns.
 *
 * @method selectColumn
 * @param column {HTMLElement | String | Number} DOM reference or ID string to a
 * TH/TD element (or child of a TH/TD element), a Column key, or a ColumnSet key index.
 */
selectColumn : function(oColumn) {
    oColumn = this.getColumn(oColumn);
    if(oColumn && !oColumn.selected) {
        // Only bottom-level Columns can get hidden
        if(oColumn.getKeyIndex() !== null) {
            oColumn.selected = true;
            
            // Update head cell
            var elTh = oColumn.getThEl();
            Dom.addClass(elTh,DT.CLASS_SELECTED);

            // Update body cells
            var allRows = this.getTbodyEl().rows;
            var oChainRender = this._oChainRender;
            oChainRender.add({
                method: function(oArg) {
                    if((this instanceof DT) && this._sId && allRows[oArg.rowIndex] && allRows[oArg.rowIndex].cells[oArg.cellIndex]) {
                        Dom.addClass(allRows[oArg.rowIndex].cells[oArg.cellIndex],DT.CLASS_SELECTED);                    
                    }
                    oArg.rowIndex++;
                },
                scope: this,
                iterations: allRows.length,
                argument: {rowIndex:0,cellIndex:oColumn.getKeyIndex()}
            });

            this._clearTrTemplateEl();
            
            this._elTbody.style.display = "none";
            this._runRenderChain();
            this._elTbody.style.display = "";      
            
            this.fireEvent("columnSelectEvent",{column:oColumn});
            YAHOO.log("Column \"" + oColumn.key + "\" selected", "info", this.toString());
        }
        else {
            YAHOO.log("Could not select Column \"" + oColumn.key + "\". Only non-nested Columns can be selected", "warn", this.toString());
        }
    }
},

/**
 * Unselects given Column. NOTE: You cannot select/unselect nested Columns. You can only
 * select/unselect non-nested Columns, and bottom-level key Columns.
 *
 * @method unselectColumn
 * @param column {HTMLElement | String | Number} DOM reference or ID string to a
 * TH/TD element (or child of a TH/TD element), a Column key, or a ColumnSet key index.
 */
unselectColumn : function(oColumn) {
    oColumn = this.getColumn(oColumn);
    if(oColumn && oColumn.selected) {
        // Only bottom-level Columns can get hidden
        if(oColumn.getKeyIndex() !== null) {
            oColumn.selected = false;
            
            // Update head cell
            var elTh = oColumn.getThEl();
            Dom.removeClass(elTh,DT.CLASS_SELECTED);

            // Update body cells
            var allRows = this.getTbodyEl().rows;
            var oChainRender = this._oChainRender;
            oChainRender.add({
                method: function(oArg) {
                    if((this instanceof DT) && this._sId && allRows[oArg.rowIndex] && allRows[oArg.rowIndex].cells[oArg.cellIndex]) {
                        Dom.removeClass(allRows[oArg.rowIndex].cells[oArg.cellIndex],DT.CLASS_SELECTED); 
                    }                   
                    oArg.rowIndex++;
                },
                scope: this,
                iterations:allRows.length,
                argument: {rowIndex:0,cellIndex:oColumn.getKeyIndex()}
            });
            
            this._clearTrTemplateEl();

            this._elTbody.style.display = "none";
            this._runRenderChain();
            this._elTbody.style.display = "";      
            
            this.fireEvent("columnUnselectEvent",{column:oColumn});
            YAHOO.log("Column \"" + oColumn.key + "\" unselected", "info", this.toString());
        }
        else {
            YAHOO.log("Could not unselect Column \"" + oColumn.key + "\". Only non-nested Columns can be unselected", "warn", this.toString());
        }
    }
},

/**
 * Returns an array selected Column instances.
 *
 * @method getSelectedColumns
 * @return {YAHOO.widget.Column[]} Array of Column instances.
 */
getSelectedColumns : function(oColumn) {
    var selectedColumns = [];
    var aKeys = this._oColumnSet.keys;
    for(var i=0,len=aKeys.length; i<len; i++) {
        if(aKeys[i].selected) {
            selectedColumns[selectedColumns.length] = aKeys[i];
        }
    }
    return selectedColumns;
},

/**
 * Assigns the class YAHOO.widget.DataTable.CLASS_HIGHLIGHTED to cells of the given Column.
 * NOTE: You cannot highlight/unhighlight nested Columns. You can only
 * highlight/unhighlight non-nested Columns, and bottom-level key Columns.
 *
 * @method highlightColumn
 * @param column {HTMLElement | String | Number} DOM reference or ID string to a
 * TH/TD element (or child of a TH/TD element), a Column key, or a ColumnSet key index.
 */
highlightColumn : function(column) {
    var oColumn = this.getColumn(column);
    // Only bottom-level Columns can get highlighted
    if(oColumn && (oColumn.getKeyIndex() !== null)) {            
        // Update head cell
        var elTh = oColumn.getThEl();
        Dom.addClass(elTh,DT.CLASS_HIGHLIGHTED);

        // Update body cells
        var allRows = this.getTbodyEl().rows;
        var oChainRender = this._oChainRender;
        oChainRender.add({
            method: function(oArg) {
                if((this instanceof DT) && this._sId && allRows[oArg.rowIndex] && allRows[oArg.rowIndex].cells[oArg.cellIndex]) {
                    Dom.addClass(allRows[oArg.rowIndex].cells[oArg.cellIndex],DT.CLASS_HIGHLIGHTED);   
                }                 
                oArg.rowIndex++;
            },
            scope: this,
            iterations:allRows.length,
            argument: {rowIndex:0,cellIndex:oColumn.getKeyIndex()},
            timeout: -1
        });
        this._elTbody.style.display = "none";
        this._runRenderChain();
        this._elTbody.style.display = "";      
            
        this.fireEvent("columnHighlightEvent",{column:oColumn});
        YAHOO.log("Column \"" + oColumn.key + "\" highlighed", "info", this.toString());
    }
    else {
        YAHOO.log("Could not highlight Column \"" + oColumn.key + "\". Only non-nested Columns can be highlighted", "warn", this.toString());
    }
},

/**
 * Removes the class YAHOO.widget.DataTable.CLASS_HIGHLIGHTED to cells of the given Column.
 * NOTE: You cannot highlight/unhighlight nested Columns. You can only
 * highlight/unhighlight non-nested Columns, and bottom-level key Columns.
 *
 * @method unhighlightColumn
 * @param column {HTMLElement | String | Number} DOM reference or ID string to a
 * TH/TD element (or child of a TH/TD element), a Column key, or a ColumnSet key index.
 */
unhighlightColumn : function(column) {
    var oColumn = this.getColumn(column);
    // Only bottom-level Columns can get highlighted
    if(oColumn && (oColumn.getKeyIndex() !== null)) {
        // Update head cell
        var elTh = oColumn.getThEl();
        Dom.removeClass(elTh,DT.CLASS_HIGHLIGHTED);

        // Update body cells
        var allRows = this.getTbodyEl().rows;
        var oChainRender = this._oChainRender;
        oChainRender.add({
            method: function(oArg) {
                if((this instanceof DT) && this._sId && allRows[oArg.rowIndex] && allRows[oArg.rowIndex].cells[oArg.cellIndex]) {
                    Dom.removeClass(allRows[oArg.rowIndex].cells[oArg.cellIndex],DT.CLASS_HIGHLIGHTED);
                }                 
                oArg.rowIndex++;
            },
            scope: this,
            iterations:allRows.length,
            argument: {rowIndex:0,cellIndex:oColumn.getKeyIndex()},
            timeout: -1
        });
        this._elTbody.style.display = "none";
        this._runRenderChain();
        this._elTbody.style.display = "";     
            
        this.fireEvent("columnUnhighlightEvent",{column:oColumn});
        YAHOO.log("Column \"" + oColumn.key + "\" unhighlighted", "info", this.toString());
    }
    else {
        YAHOO.log("Could not unhighlight Column \"" + oColumn.key + "\". Only non-nested Columns can be unhighlighted", "warn", this.toString());
    }
},












































// ROW FUNCTIONS

/**
 * Adds one new Record of data into the RecordSet at the index if given,
 * otherwise at the end. If the new Record is in page view, the
 * corresponding DOM elements are also updated.
 *
 * @method addRow
 * @param oData {Object} Object literal of data for the row.
 * @param index {Number} (optional) RecordSet position index at which to add data.
 */
addRow : function(oData, index) {
    if(lang.isNumber(index) && (index < 0 || index > this._oRecordSet.getLength())) {
        YAHOO.log("Could not add row at index " + index + " with " + lang.dump(oData), "warn", this.toString());
        return;
    }

    if(oData && lang.isObject(oData)) {
        var oRecord = this._oRecordSet.addRecord(oData, index);
        if(oRecord) {
            var recIndex;
            var oPaginator = this.get('paginator');

            // Paginated
            if (oPaginator) {     
                // Update the paginator's totalRecords
                var totalRecords = oPaginator.get('totalRecords');
                if (totalRecords !== widget.Paginator.VALUE_UNLIMITED) {
                    oPaginator.set('totalRecords',totalRecords + 1);
                }

                recIndex = this.getRecordIndex(oRecord);
                var endRecIndex = (oPaginator.getPageRecords())[1];

                // New record affects the view
                if (recIndex <= endRecIndex) {
                    // Defer UI updates to the render method
                    this.render();
                }
                
                this.fireEvent("rowAddEvent", {record:oRecord});
                YAHOO.log("Added a row for Record " + YAHOO.lang.dump(oRecord) + " at RecordSet index " + recIndex, "info", this.toString()); 
                return;
            }
            // Not paginated
            else {
                recIndex = this.getRecordIndex(oRecord);
                if(lang.isNumber(recIndex)) {
                    // Add the TR element
                    this._oChainRender.add({
                        method: function(oArg) {
                            if((this instanceof DT) && this._sId) {
                                var oRecord = oArg.record;
                                var recIndex = oArg.recIndex;
                                var elNewTr = this._addTrEl(oRecord);
                                if(elNewTr) {
                                    var elNext = (this._elTbody.rows[recIndex]) ? this._elTbody.rows[recIndex] : null;
                                    this._elTbody.insertBefore(elNewTr, elNext);

                                    // Set FIRST/LAST
                                    if(recIndex === 0) {
                                        this._setFirstRow();
                                    }
                                    if(elNext === null) {
                                        this._setLastRow();
                                    }
                                    // Set EVEN/ODD
                                    this._setRowStripes();                           
                                    
                                    this.hideTableMessage();
            
                                    this.fireEvent("rowAddEvent", {record:oRecord});
                                    YAHOO.log("Added a row for Record " + YAHOO.lang.dump(oRecord) + " at RecordSet index " + recIndex, "info", this.toString());
                                }
                            }
                        },
                        argument: {record: oRecord, recIndex: recIndex},
                        scope: this,
                        timeout: (this.get("renderLoopSize") > 0) ? 0 : -1
                    });
                    this._runRenderChain();
                    return;
                }
            }            
        }
    }
    YAHOO.log("Could not add row at index " + index + " with " + lang.dump(oData), "warn", this.toString());
},

/**
 * Convenience method to add multiple rows.
 *
 * @method addRows
 * @param aData {Object[]} Array of object literal data for the rows.
 * @param index {Number} (optional) RecordSet position index at which to add data.
 */
addRows : function(aData, index) {
    if(lang.isNumber(index) && (index < 0 || index > this._oRecordSet.getLength())) {
        YAHOO.log("Could not add rows at index " + index + " with " + lang.dump(aData), "warn", this.toString());    
        return;
    }

    if(lang.isArray(aData)) {
        var aRecords = this._oRecordSet.addRecords(aData, index);
        if(aRecords) {
            var recIndex = this.getRecordIndex(aRecords[0]);
            
            // Paginated
            var oPaginator = this.get('paginator');
            if (oPaginator) {
                // Update the paginator's totalRecords
                var totalRecords = oPaginator.get('totalRecords');
                if (totalRecords !== widget.Paginator.VALUE_UNLIMITED) {
                    oPaginator.set('totalRecords',totalRecords + aRecords.length);
                }
    
                var endRecIndex = (oPaginator.getPageRecords())[1];

                // At least one of the new records affects the view
                if (recIndex <= endRecIndex) {
                    this.render();
                }
                
                this.fireEvent("rowsAddEvent", {records:aRecords});
                YAHOO.log("Added " + aRecords.length + 
                        " rows at index " + this._oRecordSet.getRecordIndex(aRecords[0]) +
                        " with data " + lang.dump(aData), "info", this.toString());
                return;
            }
            // Not paginated
            else {
                // Add the TR elements
                var loopN = this.get("renderLoopSize");
                var loopEnd = recIndex + aData.length;
                var nRowsNeeded = (loopEnd - recIndex); // how many needed
                var isLast = (recIndex >= this._elTbody.rows.length);
                this._oChainRender.add({
                    method: function(oArg) {
                        if((this instanceof DT) && this._sId) {
                            var aRecords = oArg.aRecords,
                                i = oArg.nCurrentRow,
                                j = oArg.nCurrentRecord,
                                len = loopN > 0 ? Math.min(i + loopN,loopEnd) : loopEnd,
                                df = document.createDocumentFragment(),
                                elNext = (this._elTbody.rows[i]) ? this._elTbody.rows[i] : null;
                            for(; i < len; i++, j++) {
                                df.appendChild(this._addTrEl(aRecords[j]));
                            }
                            this._elTbody.insertBefore(df, elNext);
                            oArg.nCurrentRow = i;
                            oArg.nCurrentRecord = j;
                        }
                    },
                    iterations: (loopN > 0) ? Math.ceil(loopEnd/loopN) : 1,
                    argument: {nCurrentRow:recIndex,nCurrentRecord:0,aRecords:aRecords},
                    scope: this,
                    timeout: (loopN > 0) ? 0 : -1
                });
                this._oChainRender.add({
                    method: function(oArg) {
                        var recIndex = oArg.recIndex;
                        // Set FIRST/LAST
                        if(recIndex === 0) {
                            this._setFirstRow();
                        }
                        if(oArg.isLast) {
                            this._setLastRow();
                        }
                        // Set EVEN/ODD
                        this._setRowStripes();                           

                        this.fireEvent("rowsAddEvent", {records:aRecords});
                        YAHOO.log("Added " + aRecords.length + 
                                " rows at index " + this._oRecordSet.getRecordIndex(aRecords[0]) +
                                " with data " + lang.dump(aData), "info", this.toString());
                    },
                    argument: {recIndex: recIndex, isLast: isLast},
                    scope: this,
                    timeout: -1 // Needs to run immediately after the DOM insertions above
                });
                this._runRenderChain();
                this.hideTableMessage();                
                return;
            }            
        }
    }
    YAHOO.log("Could not add rows at index " + index + " with " + lang.dump(aData), "warn", this.toString());    
},

/**
 * For the given row, updates the associated Record with the given data. If the
 * row is on current page, the corresponding DOM elements are also updated.
 *
 * @method updateRow
 * @param row {YAHOO.widget.Record | Number | HTMLElement | String}
 * Which row to update: By Record instance, by Record's RecordSet
 * position index, by HTMLElement reference to the TR element, or by ID string
 * of the TR element.
 * @param oData {Object} Object literal of data for the row.
 */
updateRow : function(row, oData) {
    var index = row;
    if (!lang.isNumber(index)) {
        index = this.getRecordIndex(row);
    }

    // Update the Record
    if(lang.isNumber(index) && (index >= 0)) {
        var oRecordSet = this._oRecordSet,
            oldRecord = oRecordSet.getRecord(index);

        if(oldRecord) {
            var updatedRecord = this._oRecordSet.setRecord(oData, index),
                elRow = this.getTrEl(oldRecord),
                // Copy data from the Record for the event that gets fired later
                oldData = oldRecord ? oldRecord.getData() : null;

            if(updatedRecord) {
                // Update selected rows as necessary
                var tracker = this._aSelections || [],
                i=0,
                oldId = oldRecord.getId(),
                newId = updatedRecord.getId();
                for(; i<tracker.length; i++) {
                    if((tracker[i] === oldId)) {
                        tracker[i] = newId;
                    }
                    else if(tracker[i].recordId === oldId) {
                        tracker[i].recordId = newId;
                    }
                }

                // Update anchors as necessary
                if(this._oAnchorRecord && this._oAnchorRecord.getId() === oldId) {
                    this._oAnchorRecord = updatedRecord;
                }
                if(this._oAnchorCell && this._oAnchorCell.record.getId() === oldId) {
                    this._oAnchorCell.record = updatedRecord;
                }

                // Update the TR only if row is on current page
                this._oChainRender.add({
                    method: function() {
                        if((this instanceof DT) && this._sId) {
                            // Paginated
                            var oPaginator = this.get('paginator');
                            if (oPaginator) {
                                var pageStartIndex = (oPaginator.getPageRecords())[0],
                                    pageLastIndex = (oPaginator.getPageRecords())[1];

                                // At least one of the new records affects the view
                                if ((index >= pageStartIndex) || (index <= pageLastIndex)) {
                                    this.render();
                                }
                            }
                            else {
                                if(elRow) {
                                    this._updateTrEl(elRow, updatedRecord);
                                }
                                else {
                                    this.getTbodyEl().appendChild(this._addTrEl(updatedRecord));
                                }
                            }
                            this.fireEvent("rowUpdateEvent", {record:updatedRecord, oldData:oldData});
                            YAHOO.log("DataTable row updated: Record ID = " + updatedRecord.getId() +
                                    ", Record index = " + this.getRecordIndex(updatedRecord) +
                                    ", page row index = " + this.getTrIndex(updatedRecord), "info", this.toString());
                        }
                    },
                    scope: this,
                    timeout: (this.get("renderLoopSize") > 0) ? 0 : -1
                });
                this._runRenderChain();
                return;
            }
        }
    }
    YAHOO.log("Could not update row " + row + " with the data : " + lang.dump(oData), "warn", this.toString());
    return;
},

/**
 * Starting with the given row, updates associated Records with the given data.
 * The number of rows to update are determined by the array of data provided.
 * Undefined data (i.e., not an object literal) causes a row to be skipped. If
 * any of the rows are on current page, the corresponding DOM elements are also
 * updated.
 *
 * @method updateRows
 * @param startrow {YAHOO.widget.Record | Number | HTMLElement | String}
 * Starting row to update: By Record instance, by Record's RecordSet
 * position index, by HTMLElement reference to the TR element, or by ID string
 * of the TR element.
 * @param aData {Object[]} Array of object literal of data for the rows.
 */
updateRows : function(startrow, aData) {
    if(lang.isArray(aData)) {
        var startIndex = startrow,
            oRecordSet = this._oRecordSet,
            lastRowIndex = oRecordSet.getLength();

        if (!lang.isNumber(startrow)) {
            startIndex = this.getRecordIndex(startrow);
        }
            
        if(lang.isNumber(startIndex) && (startIndex >= 0) && (startIndex < oRecordSet.getLength())) {
            var lastIndex = startIndex + aData.length,
                aOldRecords = oRecordSet.getRecords(startIndex, aData.length),
                aNewRecords = oRecordSet.setRecords(aData, startIndex);
            if(aNewRecords) {
                var tracker = this._aSelections || [],
                    i=0, j, newRecord, newId, oldId,
                    anchorRecord = this._oAnchorRecord ? this._oAnchorRecord.getId() : null,
                    anchorCell = this._oAnchorCell ? this._oAnchorCell.record.getId() : null;
                for(; i<aOldRecords.length; i++) {
                    oldId = aOldRecords[i].getId();
                    newRecord = aNewRecords[i];
                    newId = newRecord.getId();

                    // Update selected rows as necessary
                    for(j=0; j<tracker.length; j++) {
                        if((tracker[j] === oldId)) {
                            tracker[j] = newId;
                        }
                        else if(tracker[j].recordId === oldId) {
                            tracker[j].recordId = newId;
                        }
                    }

                    // Update anchors as necessary
                    if(anchorRecord && anchorRecord === oldId) {
                        this._oAnchorRecord = newRecord;
                    }
                    if(anchorCell && anchorCell === oldId) {
                        this._oAnchorCell.record = newRecord;
                    }
               }

                // Paginated
                var oPaginator = this.get('paginator');
                if (oPaginator) {
                    var pageStartIndex = (oPaginator.getPageRecords())[0],
                        pageLastIndex = (oPaginator.getPageRecords())[1];
    
                    // At least one of the new records affects the view
                    if ((startIndex >= pageStartIndex) || (lastIndex <= pageLastIndex)) {
                        this.render();
                    }

                    this.fireEvent("rowsAddEvent", {newRecords:aNewRecords, oldRecords:aOldRecords});
                    YAHOO.log("Added " + aNewRecords.length + 
                            " rows starting at index " + startIndex +
                            " with data " + lang.dump(aData), "info", this.toString());
                    return;
                }
                // Not paginated
                else {
                    // Update the TR elements
                    var loopN = this.get("renderLoopSize"),
                        rowCount = aData.length, // how many needed
                        isLast = (lastIndex >= lastRowIndex),
                        isAdding = (lastIndex > lastRowIndex);
                                           
                    this._oChainRender.add({
                        method: function(oArg) {
                            if((this instanceof DT) && this._sId) {
                                var aRecords = oArg.aRecords,
                                    i = oArg.nCurrentRow,
                                    j = oArg.nDataPointer,
                                    len = loopN > 0 ? Math.min(i+loopN, startIndex+aRecords.length) : startIndex+aRecords.length;
                                    
                                for(; i < len; i++,j++) {
                                    if(isAdding && (i>=lastRowIndex)) {
                                        this._elTbody.appendChild(this._addTrEl(aRecords[j]));
                                    }
                                    else {
                                        this._updateTrEl(this._elTbody.rows[i], aRecords[j]);
                                    }
                                }
                                oArg.nCurrentRow = i;
                                oArg.nDataPointer = j;
                            }
                        },
                        iterations: (loopN > 0) ? Math.ceil(rowCount/loopN) : 1,
                        argument: {nCurrentRow:startIndex,aRecords:aNewRecords,nDataPointer:0,isAdding:isAdding},
                        scope: this,
                        timeout: (loopN > 0) ? 0 : -1
                    });
                    this._oChainRender.add({
                        method: function(oArg) {
                            var recIndex = oArg.recIndex;
                            // Set FIRST/LAST
                            if(recIndex === 0) {
                                this._setFirstRow();
                            }
                            if(oArg.isLast) {
                                this._setLastRow();
                            }
                            // Set EVEN/ODD
                            this._setRowStripes();                           
    
                            this.fireEvent("rowsAddEvent", {newRecords:aNewRecords, oldRecords:aOldRecords});
                            YAHOO.log("Added " + aNewRecords.length + 
                                    " rows starting at index " + startIndex +
                                    " with data " + lang.dump(aData), "info", this.toString());
                        },
                        argument: {recIndex: startIndex, isLast: isLast},
                        scope: this,
                        timeout: -1 // Needs to run immediately after the DOM insertions above
                    });
                    this._runRenderChain();
                    this.hideTableMessage();                
                    return;
                }            
            }
        }
    }
    YAHOO.log("Could not update rows at " + startrow + " with " + lang.dump(aData), "warn", this.toString());
},

/**
 * Deletes the given row's Record from the RecordSet. If the row is on current page,
 * the corresponding DOM elements are also deleted.
 *
 * @method deleteRow
 * @param row {HTMLElement | String | Number} DOM element reference or ID string
 * to DataTable page element or RecordSet index.
 */
deleteRow : function(row) {
    var nRecordIndex = (lang.isNumber(row)) ? row : this.getRecordIndex(row);
    if(lang.isNumber(nRecordIndex)) {
        var oRecord = this.getRecord(nRecordIndex);
        if(oRecord) {
            var nTrIndex = this.getTrIndex(nRecordIndex);
            
            // Remove from selection tracker if there
            var sRecordId = oRecord.getId();
            var tracker = this._aSelections || [];
            for(var j=tracker.length-1; j>-1; j--) {
                if((lang.isString(tracker[j]) && (tracker[j] === sRecordId)) ||
                        (lang.isObject(tracker[j]) && (tracker[j].recordId === sRecordId))) {
                    tracker.splice(j,1);
                }
            }
    
            // Delete Record from RecordSet
            var oData = this._oRecordSet.deleteRecord(nRecordIndex);
    
            // Update the UI
            if(oData) {
                // If paginated and the deleted row was on this or a prior page, just
                // re-render
                var oPaginator = this.get('paginator');
                if (oPaginator) {
                    // Update the paginator's totalRecords
                    var totalRecords = oPaginator.get('totalRecords'),
                        // must capture before the totalRecords change because
                        // Paginator shifts to previous page automatically
                        rng = oPaginator.getPageRecords();

                    if (totalRecords !== widget.Paginator.VALUE_UNLIMITED) {
                        oPaginator.set('totalRecords',totalRecords - 1);
                    }
    
                    // The deleted record was on this or a prior page, re-render
                    if (!rng || nRecordIndex <= rng[1]) {
                        this.render();
                    }

                    this._oChainRender.add({
                        method: function() {
                            if((this instanceof DT) && this._sId) {
                                this.fireEvent("rowDeleteEvent", {recordIndex:nRecordIndex, oldData:oData, trElIndex:nTrIndex});
                                YAHOO.log("Deleted row with data " + YAHOO.lang.dump(oData) + " at RecordSet index " + nRecordIndex + " and page row index " + nTrIndex, "info", this.toString());     
                            }
                        },
                        scope: this,
                        timeout: (this.get("renderLoopSize") > 0) ? 0 : -1
                    });
                    this._runRenderChain();
                }
                // Not paginated
                else {
                    if(lang.isNumber(nTrIndex)) {
                        this._oChainRender.add({
                            method: function() {
                                if((this instanceof DT) && this._sId) {
                                    var isLast = (nRecordIndex === this._oRecordSet.getLength());//(nTrIndex == this.getLastTrEl().sectionRowIndex);
                                    this._deleteTrEl(nTrIndex);
                    
                                    // Post-delete tasks
                                    if(this._elTbody.rows.length > 0) {
                                        // Set FIRST/LAST
                                        if(nTrIndex === 0) {
                                            this._setFirstRow();
                                        }
                                        if(isLast) {
                                            this._setLastRow();
                                        }
                                        // Set EVEN/ODD
                                        if(nTrIndex != this._elTbody.rows.length) {
                                            this._setRowStripes(nTrIndex);
                                        }                                
                                    }
                    
                                    this.fireEvent("rowDeleteEvent", {recordIndex:nRecordIndex,oldData:oData, trElIndex:nTrIndex});
                                    YAHOO.log("Deleted row with data " + YAHOO.lang.dump(oData) + " at RecordSet index " + nRecordIndex + " and page row index " + nTrIndex, "info", this.toString());     
                                }
                            },
                            scope: this,
                            timeout: (this.get("renderLoopSize") > 0) ? 0 : -1
                        });
                        this._runRenderChain();
                        return;
                    }
                }
            }
        }
    }
    YAHOO.log("Could not delete row: " + row, "warn", this.toString());
    return null;
},

/**
 * Convenience method to delete multiple rows.
 *
 * @method deleteRows
 * @param row {HTMLElement | String | Number} DOM element reference or ID string
 * to DataTable page element or RecordSet index.
 * @param count {Number} (optional) How many rows to delete. A negative value
 * will delete towards the beginning.
 */
deleteRows : function(row, count) {
    var nRecordIndex = (lang.isNumber(row)) ? row : this.getRecordIndex(row);
    if(lang.isNumber(nRecordIndex)) {
        var oRecord = this.getRecord(nRecordIndex);
        if(oRecord) {
            var nTrIndex = this.getTrIndex(nRecordIndex);
            
            // Remove from selection tracker if there
            var sRecordId = oRecord.getId();
            var tracker = this._aSelections || [];
            for(var j=tracker.length-1; j>-1; j--) {
                if((lang.isString(tracker[j]) && (tracker[j] === sRecordId)) ||
                        (lang.isObject(tracker[j]) && (tracker[j].recordId === sRecordId))) {
                    tracker.splice(j,1);
                }
            }
    
            // Delete Record from RecordSet
            var highIndex = nRecordIndex;
            var lowIndex = nRecordIndex;
        
            // Validate count and account for negative value
            if(count && lang.isNumber(count)) {
                highIndex = (count > 0) ? nRecordIndex + count -1 : nRecordIndex;
                lowIndex = (count > 0) ? nRecordIndex : nRecordIndex + count + 1;
                count = (count > 0) ? count : count*-1;
                if(lowIndex < 0) {
                    lowIndex = 0;
                    count = highIndex - lowIndex + 1;
                }
            }
            else {
                count = 1;
            }
            
            var aData = this._oRecordSet.deleteRecords(lowIndex, count);
    
            // Update the UI
            if(aData) {
                var oPaginator = this.get('paginator'),
                    loopN = this.get("renderLoopSize");
                // If paginated and the deleted row was on this or a prior page, just
                // re-render
                if (oPaginator) {
                    // Update the paginator's totalRecords
                    var totalRecords = oPaginator.get('totalRecords'),
                        // must capture before the totalRecords change because
                        // Paginator shifts to previous page automatically
                        rng = oPaginator.getPageRecords();

                    if (totalRecords !== widget.Paginator.VALUE_UNLIMITED) {
                        oPaginator.set('totalRecords',totalRecords - aData.length);
                    }
    
                    // The records were on this or a prior page, re-render
                    if (!rng || lowIndex <= rng[1]) {
                        this.render();
                    }

                    this._oChainRender.add({
                        method: function(oArg) {
                            if((this instanceof DT) && this._sId) {
                                this.fireEvent("rowsDeleteEvent", {recordIndex:lowIndex, oldData:aData, count:count});
                                YAHOO.log("DataTable " + count + " rows deleted starting at index " + lowIndex, "info", this.toString());
                            }
                        },
                        scope: this,
                        timeout: (loopN > 0) ? 0 : -1
                    });
                    this._runRenderChain();
                    return;
                }
                // Not paginated
                else {
                    if(lang.isNumber(nTrIndex)) {
                        // Delete the TR elements starting with highest index
                        var loopEnd = lowIndex;
                        var nRowsNeeded = count; // how many needed
                        this._oChainRender.add({
                            method: function(oArg) {
                                if((this instanceof DT) && this._sId) {
                                    var i = oArg.nCurrentRow,
                                        len = (loopN > 0) ? (Math.max(i - loopN,loopEnd)-1) : loopEnd-1;
                                    for(; i>len; --i) {
                                        this._deleteTrEl(i);
                                    }
                                    oArg.nCurrentRow = i;
                                }
                            },
                            iterations: (loopN > 0) ? Math.ceil(count/loopN) : 1,
                            argument: {nCurrentRow:highIndex},
                            scope: this,
                            timeout: (loopN > 0) ? 0 : -1
                        });
                        this._oChainRender.add({
                            method: function() {    
                                // Post-delete tasks
                                if(this._elTbody.rows.length > 0) {
                                    this._setFirstRow();
                                    this._setLastRow();
                                    this._setRowStripes();
                                }
                                
                                this.fireEvent("rowsDeleteEvent", {recordIndex:lowIndex, oldData:aData, count:count});
                                YAHOO.log("DataTable " + count + " rows deleted starting at index " + lowIndex, "info", this.toString());
                            },
                            scope: this,
                            timeout: -1 // Needs to run immediately after the DOM deletions above
                        });
                        this._runRenderChain();
                        return;
                    }
                }
            }
        }
    }
    YAHOO.log("Could not delete " + count + " rows at row " + row, "warn", this.toString());
    return null;
},














































// CELL FUNCTIONS

/**
 * Outputs markup into the given TD based on given Record.
 *
 * @method formatCell
 * @param elLiner {HTMLElement} The liner DIV element within the TD.
 * @param oRecord {YAHOO.widget.Record} (Optional) Record instance.
 * @param oColumn {YAHOO.widget.Column} (Optional) Column instance.
 */
formatCell : function(elLiner, oRecord, oColumn) {
    if(!oRecord) {
        oRecord = this.getRecord(elLiner);
    }
    if(!oColumn) {
        oColumn = this.getColumn(this.getCellIndex(elLiner.parentNode));
    }

    if(oRecord && oColumn) {
        var sField = oColumn.field;
        var oData = oRecord.getData(sField);

        var fnFormatter = typeof oColumn.formatter === 'function' ?
                          oColumn.formatter :
                          DT.Formatter[oColumn.formatter+''] ||
                          DT.Formatter.defaultFormatter;

        // Apply special formatter
        if(fnFormatter) {
            fnFormatter.call(this, elLiner, oRecord, oColumn, oData);
        }
        else {
            elLiner.innerHTML = oData;
        }

        this.fireEvent("cellFormatEvent", {record:oRecord, column:oColumn, key:oColumn.key, el:elLiner});
    }
    else {
        YAHOO.log("Could not format cell " + elLiner, "error", this.toString());
    }
},

/**
 * For the given row and column, updates the Record with the given data. If the
 * cell is on current page, the corresponding DOM elements are also updated.
 *
 * @method updateCell
 * @param oRecord {YAHOO.widget.Record} Record instance.
 * @param oColumn {YAHOO.widget.Column | String | Number} A Column key, or a ColumnSet key index.
 * @param oData {Object} New data value for the cell.
 * @param skipRender {Boolean} Skips render step. Editors that update multiple
 * cells in ScrollingDataTable should render only on the last call to updateCell().
 */
updateCell : function(oRecord, oColumn, oData, skipRender) {
    // Validate Column and Record
    oColumn = (oColumn instanceof YAHOO.widget.Column) ? oColumn : this.getColumn(oColumn);
    if(oColumn && oColumn.getField() && (oRecord instanceof YAHOO.widget.Record)) {
        var sKey = oColumn.getField(),
        
        // Copy data from the Record for the event that gets fired later
        //var oldData = YAHOO.widget.DataTable._cloneObject(oRecord.getData());
            oldData = oRecord.getData(sKey);

        // Update Record with new data
        this._oRecordSet.updateRecordValue(oRecord, sKey, oData);
    
        // Update the TD only if row is on current page
        var elTd = this.getTdEl({record: oRecord, column: oColumn});
        if(elTd) {
            this._oChainRender.add({
                method: function() {
                    if((this instanceof DT) && this._sId) {
                        this.formatCell(elTd.firstChild, oRecord, oColumn);
                        this.fireEvent("cellUpdateEvent", {record:oRecord, column: oColumn, oldData:oldData});
                        YAHOO.log("DataTable cell updated: Record ID = " + oRecord.getId() +
                                ", Record index = " + this.getRecordIndex(oRecord) +
                                ", page row index = " + this.getTrIndex(oRecord) +
                                ", Column key = " + oColumn.getKey(), "info", this.toString());
                    }
                },
                scope: this,
                timeout: (this.get("renderLoopSize") > 0) ? 0 : -1
            });
            // Bug 2529024
            if(!skipRender) {
                this._runRenderChain();
            }
        }
        else {
            this.fireEvent("cellUpdateEvent", {record:oRecord, column: oColumn, oldData:oldData});
            YAHOO.log("DataTable cell updated: Record ID = " + oRecord.getId() +
                    ", Record index = " + this.getRecordIndex(oRecord) +
                    ", page row index = " + this.getTrIndex(oRecord) +
                    ", Column key = " + oColumn.getKey(), "info", this.toString());   
        }
    }
},



















































// PAGINATION
/**
 * Method executed during set() operation for the "paginator" attribute.
 * Adds and/or severs event listeners between DataTable and Paginator
 *
 * @method _updatePaginator
 * @param newPag {Paginator} Paginator instance (or null) for DataTable to use
 * @private
 */
_updatePaginator : function (newPag) {
    var oldPag = this.get('paginator');
    if (oldPag && newPag !== oldPag) {
        oldPag.unsubscribe('changeRequest', this.onPaginatorChangeRequest, this, true);
    }
    if (newPag) {
        newPag.subscribe('changeRequest', this.onPaginatorChangeRequest, this, true);
    }
},

/**
 * Update the UI infrastructure in response to a "paginator" attribute change.
 *
 * @method _handlePaginatorChange
 * @param e {Object} Change event object containing keys 'type','newValue',
 *                   and 'prevValue'
 * @private
 */
_handlePaginatorChange : function (e) {
    if (e.prevValue === e.newValue) { return; }

    var newPag     = e.newValue,
        oldPag     = e.prevValue,
        containers = this._defaultPaginatorContainers();

    if (oldPag) {
        if (oldPag.getContainerNodes()[0] == containers[0]) {
            oldPag.set('containers',[]);
        }
        oldPag.destroy();

        // Convenience: share the default containers if possible.
        // Otherwise, remove the default containers from the DOM.
        if (containers[0]) {
            if (newPag && !newPag.getContainerNodes().length) {
                newPag.set('containers',containers);
            } else {
                // No new Paginator to use existing containers, OR new
                // Paginator has configured containers.
                for (var i = containers.length - 1; i >= 0; --i) {
                    if (containers[i]) {
                        containers[i].parentNode.removeChild(containers[i]);
                    }
                }
            }
        }
    }

    if (!this._bInit) {
        this.render();

    }

    if (newPag) {
        this.renderPaginator();
    }

},

/**
 * Returns the default containers used for Paginators.  If create param is
 * passed, the containers will be created and added to the DataTable container.
 *
 * @method _defaultPaginatorContainers
 * @param create {boolean} Create the default containers if not found
 * @private
 */
_defaultPaginatorContainers : function (create) {
    var above_id = this._sId + '-paginator0',
        below_id = this._sId + '-paginator1',
        above    = Dom.get(above_id),
        below    = Dom.get(below_id);

    if (create && (!above || !below)) {
        // One above and one below the table
        if (!above) {
            above    = document.createElement('div');
            above.id = above_id;
            Dom.addClass(above, DT.CLASS_PAGINATOR);

            this._elContainer.insertBefore(above,this._elContainer.firstChild);
        }

        if (!below) {
            below    = document.createElement('div');
            below.id = below_id;
            Dom.addClass(below, DT.CLASS_PAGINATOR);

            this._elContainer.appendChild(below);
        }
    }

    return [above,below];
},

/**
 * Calls Paginator's destroy() method
 *
 * @method _destroyPaginator
 * @private
 */
_destroyPaginator : function () {
    var oldPag = this.get('paginator');
    if (oldPag) {
        oldPag.destroy();
    }
},

/**
 * Renders the Paginator to the DataTable UI
 *
 * @method renderPaginator
 */
renderPaginator : function () {
    var pag = this.get("paginator");
    if (!pag) { return; }

    // Add the containers if the Paginator is not configured with containers
    if (!pag.getContainerNodes().length) {
        pag.set('containers',this._defaultPaginatorContainers(true));
    }

    pag.render();
},

/**
 * Overridable method gives implementers a hook to show loading message before
 * changing Paginator value.
 *
 * @method doBeforePaginatorChange
 * @param oPaginatorState {Object} An object literal describing the proposed pagination state.
 * @return {Boolean} Return true to continue changing Paginator value.
 */
doBeforePaginatorChange : function(oPaginatorState) {
    this.showTableMessage(this.get("MSG_LOADING"), DT.CLASS_LOADING);
    return true;
},

/**
 * Responds to new Pagination states. By default, updates the UI to reflect the
 * new state. If "dynamicData" is true, current selections are purged before
 * a request is sent to the DataSource for data for the new state (using the
 * request returned by "generateRequest()").
 *  
 * @method onPaginatorChangeRequest
 * @param oPaginatorState {Object} An object literal describing the proposed pagination state.
 */
onPaginatorChangeRequest : function (oPaginatorState) {
    var ok = this.doBeforePaginatorChange(oPaginatorState);
    if(ok) {
        // Server-side pagination
        if(this.get("dynamicData")) {
            // Get the current state
            var oState = this.getState();
            
            // Update pagination values
            oState.pagination = oPaginatorState;
    
            // Get the request for the new state
            var request = this.get("generateRequest")(oState, this);
            
            // Purge selections
            this.unselectAllRows();
            this.unselectAllCells();
            
            // Get the new data from the server
            var callback = {
                success : this.onDataReturnSetRows,
                failure : this.onDataReturnSetRows,
                argument : oState, // Pass along the new state to the callback
                scope : this
            };
            this._oDataSource.sendRequest(request, callback);
        }
        // Client-side pagination
        else {
            // Set the core pagination values silently (the second param)
            // to avoid looping back through the changeRequest mechanism
            oPaginatorState.paginator.setStartIndex(oPaginatorState.recordOffset,true);
            oPaginatorState.paginator.setRowsPerPage(oPaginatorState.rowsPerPage,true);
    
            // Update the UI
            this.render();
        }
    }
    else {
        YAHOO.log("Could not change Paginator value \"" + oPaginatorState + "\"", "warn", this.toString());
    }
},


















































// SELECTION/HIGHLIGHTING

/*
 * Reference to last highlighted cell element
 *
 * @property _elLastHighlightedTd
 * @type HTMLElement
 * @private
 */
_elLastHighlightedTd : null,

/*
 * ID string of last highlighted row element
 *
 * @property _sLastHighlightedTrElId
 * @type String
 * @private
 */
//_sLastHighlightedTrElId : null,

/**
 * Array to track row selections (by sRecordId) and/or cell selections
 * (by {recordId:sRecordId, columnKey:sColumnKey})
 *
 * @property _aSelections
 * @type Object[]
 * @private
 */
_aSelections : null,

/**
 * Record instance of the row selection anchor.
 *
 * @property _oAnchorRecord
 * @type YAHOO.widget.Record
 * @private
 */
_oAnchorRecord : null,

/**
 * Object literal representing cell selection anchor:
 * {recordId:sRecordId, columnKey:sColumnKey}.
 *
 * @property _oAnchorCell
 * @type Object
 * @private
 */
_oAnchorCell : null,

/**
 * Convenience method to remove the class YAHOO.widget.DataTable.CLASS_SELECTED
 * from all TR elements on the page.
 *
 * @method _unselectAllTrEls
 * @private
 */
_unselectAllTrEls : function() {
    var selectedRows = Dom.getElementsByClassName(DT.CLASS_SELECTED,"tr",this._elTbody);
    Dom.removeClass(selectedRows, DT.CLASS_SELECTED);
},

/**
 * Returns object literal of values that represent the selection trigger. Used
 * to determine selection behavior resulting from a key event.
 *
 * @method _getSelectionTrigger
 * @private
 */
_getSelectionTrigger : function() {
    var sMode = this.get("selectionMode");
    var oTrigger = {};
    var oTriggerCell, oTriggerRecord, nTriggerRecordIndex, elTriggerRow, nTriggerTrIndex;

    // Cell mode
    if((sMode == "cellblock") || (sMode == "cellrange") || (sMode == "singlecell")) {
        oTriggerCell = this.getLastSelectedCell();
        // No selected cells found
        if(!oTriggerCell) {
            return null;
        }
        else {
            oTriggerRecord = this.getRecord(oTriggerCell.recordId);
            nTriggerRecordIndex = this.getRecordIndex(oTriggerRecord);
            elTriggerRow = this.getTrEl(oTriggerRecord);
            nTriggerTrIndex = this.getTrIndex(elTriggerRow);

            // Selected cell not found on this page
            if(nTriggerTrIndex === null) {
                return null;
            }
            else {
                oTrigger.record = oTriggerRecord;
                oTrigger.recordIndex = nTriggerRecordIndex;
                oTrigger.el = this.getTdEl(oTriggerCell);
                oTrigger.trIndex = nTriggerTrIndex;
                oTrigger.column = this.getColumn(oTriggerCell.columnKey);
                oTrigger.colKeyIndex = oTrigger.column.getKeyIndex();
                oTrigger.cell = oTriggerCell;
                return oTrigger;
            }
        }
    }
    // Row mode
    else {
        oTriggerRecord = this.getLastSelectedRecord();
        // No selected rows found
        if(!oTriggerRecord) {
                return null;
        }
        else {
            // Selected row found, but is it on current page?
            oTriggerRecord = this.getRecord(oTriggerRecord);
            nTriggerRecordIndex = this.getRecordIndex(oTriggerRecord);
            elTriggerRow = this.getTrEl(oTriggerRecord);
            nTriggerTrIndex = this.getTrIndex(elTriggerRow);

            // Selected row not found on this page
            if(nTriggerTrIndex === null) {
                return null;
            }
            else {
                oTrigger.record = oTriggerRecord;
                oTrigger.recordIndex = nTriggerRecordIndex;
                oTrigger.el = elTriggerRow;
                oTrigger.trIndex = nTriggerTrIndex;
                return oTrigger;
            }
        }
    }
},

/**
 * Returns object literal of values that represent the selection anchor. Used
 * to determine selection behavior resulting from a user event.
 *
 * @method _getSelectionAnchor
 * @param oTrigger {Object} (Optional) Object literal of selection trigger values
 * (for key events).
 * @private
 */
_getSelectionAnchor : function(oTrigger) {
    var sMode = this.get("selectionMode");
    var oAnchor = {};
    var oAnchorRecord, nAnchorRecordIndex, nAnchorTrIndex;

    // Cell mode
    if((sMode == "cellblock") || (sMode == "cellrange") || (sMode == "singlecell")) {
        // Validate anchor cell
        var oAnchorCell = this._oAnchorCell;
        if(!oAnchorCell) {
            if(oTrigger) {
                oAnchorCell = this._oAnchorCell = oTrigger.cell;
            }
            else {
                return null;
            }
        }
        oAnchorRecord = this._oAnchorCell.record;
        nAnchorRecordIndex = this._oRecordSet.getRecordIndex(oAnchorRecord);
        nAnchorTrIndex = this.getTrIndex(oAnchorRecord);
        // If anchor cell is not on this page...
        if(nAnchorTrIndex === null) {
            // ...set TR index equal to top TR
            if(nAnchorRecordIndex < this.getRecordIndex(this.getFirstTrEl())) {
                nAnchorTrIndex = 0;
            }
            // ...set TR index equal to bottom TR
            else {
                nAnchorTrIndex = this.getRecordIndex(this.getLastTrEl());
            }
        }

        oAnchor.record = oAnchorRecord;
        oAnchor.recordIndex = nAnchorRecordIndex;
        oAnchor.trIndex = nAnchorTrIndex;
        oAnchor.column = this._oAnchorCell.column;
        oAnchor.colKeyIndex = oAnchor.column.getKeyIndex();
        oAnchor.cell = oAnchorCell;
        return oAnchor;
    }
    // Row mode
    else {
        oAnchorRecord = this._oAnchorRecord;
        if(!oAnchorRecord) {
            if(oTrigger) {
                oAnchorRecord = this._oAnchorRecord = oTrigger.record;
            }
            else {
                return null;
            }
        }

        nAnchorRecordIndex = this.getRecordIndex(oAnchorRecord);
        nAnchorTrIndex = this.getTrIndex(oAnchorRecord);
        // If anchor row is not on this page...
        if(nAnchorTrIndex === null) {
            // ...set TR index equal to top TR
            if(nAnchorRecordIndex < this.getRecordIndex(this.getFirstTrEl())) {
                nAnchorTrIndex = 0;
            }
            // ...set TR index equal to bottom TR
            else {
                nAnchorTrIndex = this.getRecordIndex(this.getLastTrEl());
            }
        }

        oAnchor.record = oAnchorRecord;
        oAnchor.recordIndex = nAnchorRecordIndex;
        oAnchor.trIndex = nAnchorTrIndex;
        return oAnchor;
    }
},

/**
 * Determines selection behavior resulting from a mouse event when selection mode
 * is set to "standard".
 *
 * @method _handleStandardSelectionByMouse
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 * @private
 */
_handleStandardSelectionByMouse : function(oArgs) {
    var elTarget = oArgs.target;

    // Validate target row
    var elTargetRow = this.getTrEl(elTarget);
    if(elTargetRow) {
        var e = oArgs.event;
        var bSHIFT = e.shiftKey;
        var bCTRL = e.ctrlKey || ((navigator.userAgent.toLowerCase().indexOf("mac") != -1) && e.metaKey);

        var oTargetRecord = this.getRecord(elTargetRow);
        var nTargetRecordIndex = this._oRecordSet.getRecordIndex(oTargetRecord);

        var oAnchor = this._getSelectionAnchor();

        var i;

        // Both SHIFT and CTRL
        if(bSHIFT && bCTRL) {
            // Validate anchor
            if(oAnchor) {
                if(this.isSelected(oAnchor.record)) {
                    // Select all rows between anchor row and target row, including target row
                    if(oAnchor.recordIndex < nTargetRecordIndex) {
                        for(i=oAnchor.recordIndex+1; i<=nTargetRecordIndex; i++) {
                            if(!this.isSelected(i)) {
                                this.selectRow(i);
                            }
                        }
                    }
                    // Select all rows between target row and anchor row, including target row
                    else {
                        for(i=oAnchor.recordIndex-1; i>=nTargetRecordIndex; i--) {
                            if(!this.isSelected(i)) {
                                this.selectRow(i);
                            }
                        }
                    }
                }
                else {
                    // Unselect all rows between anchor row and target row
                    if(oAnchor.recordIndex < nTargetRecordIndex) {
                        for(i=oAnchor.recordIndex+1; i<=nTargetRecordIndex-1; i++) {
                            if(this.isSelected(i)) {
                                this.unselectRow(i);
                            }
                        }
                    }
                    // Unselect all rows between target row and anchor row
                    else {
                        for(i=nTargetRecordIndex+1; i<=oAnchor.recordIndex-1; i++) {
                            if(this.isSelected(i)) {
                                this.unselectRow(i);
                            }
                        }
                    }
                    // Select the target row
                    this.selectRow(oTargetRecord);
                }
            }
            // Invalid anchor
            else {
                // Set anchor
                this._oAnchorRecord = oTargetRecord;

                // Toggle selection of target
                if(this.isSelected(oTargetRecord)) {
                    this.unselectRow(oTargetRecord);
                }
                else {
                    this.selectRow(oTargetRecord);
                }
            }
        }
         // Only SHIFT
        else if(bSHIFT) {
            this.unselectAllRows();

            // Validate anchor
            if(oAnchor) {
                // Select all rows between anchor row and target row,
                // including the anchor row and target row
                if(oAnchor.recordIndex < nTargetRecordIndex) {
                    for(i=oAnchor.recordIndex; i<=nTargetRecordIndex; i++) {
                        this.selectRow(i);
                    }
                }
                // Select all rows between target row and anchor row,
                // including the target row and anchor row
                else {
                    for(i=oAnchor.recordIndex; i>=nTargetRecordIndex; i--) {
                        this.selectRow(i);
                    }
                }
            }
            // Invalid anchor
            else {
                // Set anchor
                this._oAnchorRecord = oTargetRecord;

                // Select target row only
                this.selectRow(oTargetRecord);
            }
        }
        // Only CTRL
        else if(bCTRL) {
            // Set anchor
            this._oAnchorRecord = oTargetRecord;

            // Toggle selection of target
            if(this.isSelected(oTargetRecord)) {
                this.unselectRow(oTargetRecord);
            }
            else {
                this.selectRow(oTargetRecord);
            }
        }
        // Neither SHIFT nor CTRL
        else {
            this._handleSingleSelectionByMouse(oArgs);
            return;
        }
    }
},

/**
 * Determines selection behavior resulting from a key event when selection mode
 * is set to "standard".
 *
 * @method _handleStandardSelectionByKey
 * @param e {HTMLEvent} Event object.
 * @private
 */
_handleStandardSelectionByKey : function(e) {
    var nKey = Ev.getCharCode(e);

    if((nKey == 38) || (nKey == 40)) {
        var bSHIFT = e.shiftKey;

        // Validate trigger
        var oTrigger = this._getSelectionTrigger();
        // Arrow selection only works if last selected row is on current page
        if(!oTrigger) {
            return null;
        }

        Ev.stopEvent(e);

        // Validate anchor
        var oAnchor = this._getSelectionAnchor(oTrigger);

        // Determine which direction we're going to
        if(bSHIFT) {
            // Selecting down away from anchor row
            if((nKey == 40) && (oAnchor.recordIndex <= oTrigger.trIndex)) {
                this.selectRow(this.getNextTrEl(oTrigger.el));
            }
            // Selecting up away from anchor row
            else if((nKey == 38) && (oAnchor.recordIndex >= oTrigger.trIndex)) {
                this.selectRow(this.getPreviousTrEl(oTrigger.el));
            }
            // Unselect trigger
            else {
                this.unselectRow(oTrigger.el);
            }
        }
        else {
            this._handleSingleSelectionByKey(e);
        }
    }
},

/**
 * Determines selection behavior resulting from a mouse event when selection mode
 * is set to "single".
 *
 * @method _handleSingleSelectionByMouse
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 * @private
 */
_handleSingleSelectionByMouse : function(oArgs) {
    var elTarget = oArgs.target;

    // Validate target row
    var elTargetRow = this.getTrEl(elTarget);
    if(elTargetRow) {
        var oTargetRecord = this.getRecord(elTargetRow);

        // Set anchor
        this._oAnchorRecord = oTargetRecord;

        // Select only target
        this.unselectAllRows();
        this.selectRow(oTargetRecord);
    }
},

/**
 * Determines selection behavior resulting from a key event when selection mode
 * is set to "single".
 *
 * @method _handleSingleSelectionByKey
 * @param e {HTMLEvent} Event object.
 * @private
 */
_handleSingleSelectionByKey : function(e) {
    var nKey = Ev.getCharCode(e);

    if((nKey == 38) || (nKey == 40)) {
        // Validate trigger
        var oTrigger = this._getSelectionTrigger();
        // Arrow selection only works if last selected row is on current page
        if(!oTrigger) {
            return null;
        }

        Ev.stopEvent(e);

        // Determine the new row to select
        var elNew;
        if(nKey == 38) { // arrow up
            elNew = this.getPreviousTrEl(oTrigger.el);

            // Validate new row
            if(elNew === null) {
                //TODO: wrap around to last tr on current page
                //elNew = this.getLastTrEl();

                //TODO: wrap back to last tr of previous page

                // Top row selection is sticky
                elNew = this.getFirstTrEl();
            }
        }
        else if(nKey == 40) { // arrow down
            elNew = this.getNextTrEl(oTrigger.el);

            // Validate new row
            if(elNew === null) {
                //TODO: wrap around to first tr on current page
                //elNew = this.getFirstTrEl();

                //TODO: wrap forward to first tr of previous page

                // Bottom row selection is sticky
                elNew = this.getLastTrEl();
            }
        }

        // Unselect all rows
        this.unselectAllRows();

        // Select the new row
        this.selectRow(elNew);

        // Set new anchor
        this._oAnchorRecord = this.getRecord(elNew);
    }
},

/**
 * Determines selection behavior resulting from a mouse event when selection mode
 * is set to "cellblock".
 *
 * @method _handleCellBlockSelectionByMouse
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 * @private
 */
_handleCellBlockSelectionByMouse : function(oArgs) {
    var elTarget = oArgs.target;

    // Validate target cell
    var elTargetCell = this.getTdEl(elTarget);
    if(elTargetCell) {
        var e = oArgs.event;
        var bSHIFT = e.shiftKey;
        var bCTRL = e.ctrlKey || ((navigator.userAgent.toLowerCase().indexOf("mac") != -1) && e.metaKey);

        var elTargetRow = this.getTrEl(elTargetCell);
        var nTargetTrIndex = this.getTrIndex(elTargetRow);
        var oTargetColumn = this.getColumn(elTargetCell);
        var nTargetColKeyIndex = oTargetColumn.getKeyIndex();
        var oTargetRecord = this.getRecord(elTargetRow);
        var nTargetRecordIndex = this._oRecordSet.getRecordIndex(oTargetRecord);
        var oTargetCell = {record:oTargetRecord, column:oTargetColumn};

        var oAnchor = this._getSelectionAnchor();

        var allRows = this.getTbodyEl().rows;
        var startIndex, endIndex, currentRow, i, j;

        // Both SHIFT and CTRL
        if(bSHIFT && bCTRL) {

            // Validate anchor
            if(oAnchor) {
                // Anchor is selected
                if(this.isSelected(oAnchor.cell)) {
                    // All cells are on the same row
                    if(oAnchor.recordIndex === nTargetRecordIndex) {
                        // Select all cells between anchor cell and target cell, including target cell
                        if(oAnchor.colKeyIndex < nTargetColKeyIndex) {
                            for(i=oAnchor.colKeyIndex+1; i<=nTargetColKeyIndex; i++) {
                                this.selectCell(elTargetRow.cells[i]);
                            }
                        }
                        // Select all cells between target cell and anchor cell, including target cell
                        else if(nTargetColKeyIndex < oAnchor.colKeyIndex) {
                            for(i=nTargetColKeyIndex; i<oAnchor.colKeyIndex; i++) {
                                this.selectCell(elTargetRow.cells[i]);
                            }
                        }
                    }
                    // Anchor row is above target row
                    else if(oAnchor.recordIndex < nTargetRecordIndex) {
                        startIndex = Math.min(oAnchor.colKeyIndex, nTargetColKeyIndex);
                        endIndex = Math.max(oAnchor.colKeyIndex, nTargetColKeyIndex);

                        // Select all cells from startIndex to endIndex on rows between anchor row and target row
                        for(i=oAnchor.trIndex; i<=nTargetTrIndex; i++) {
                            for(j=startIndex; j<=endIndex; j++) {
                                this.selectCell(allRows[i].cells[j]);
                            }
                        }
                    }
                    // Anchor row is below target row
                    else {
                        startIndex = Math.min(oAnchor.trIndex, nTargetColKeyIndex);
                        endIndex = Math.max(oAnchor.trIndex, nTargetColKeyIndex);

                        // Select all cells from startIndex to endIndex on rows between target row and anchor row
                        for(i=oAnchor.trIndex; i>=nTargetTrIndex; i--) {
                            for(j=endIndex; j>=startIndex; j--) {
                                this.selectCell(allRows[i].cells[j]);
                            }
                        }
                    }
                }
                // Anchor cell is unselected
                else {
                    // All cells are on the same row
                    if(oAnchor.recordIndex === nTargetRecordIndex) {
                        // Unselect all cells between anchor cell and target cell
                        if(oAnchor.colKeyIndex < nTargetColKeyIndex) {
                            for(i=oAnchor.colKeyIndex+1; i<nTargetColKeyIndex; i++) {
                                this.unselectCell(elTargetRow.cells[i]);
                            }
                        }
                        // Select all cells between target cell and anchor cell
                        else if(nTargetColKeyIndex < oAnchor.colKeyIndex) {
                            for(i=nTargetColKeyIndex+1; i<oAnchor.colKeyIndex; i++) {
                                this.unselectCell(elTargetRow.cells[i]);
                            }
                        }
                    }
                    // Anchor row is above target row
                    if(oAnchor.recordIndex < nTargetRecordIndex) {
                        // Unselect all cells from anchor cell to target cell
                        for(i=oAnchor.trIndex; i<=nTargetTrIndex; i++) {
                            currentRow = allRows[i];
                            for(j=0; j<currentRow.cells.length; j++) {
                                // This is the anchor row, only unselect cells after the anchor cell
                                if(currentRow.sectionRowIndex === oAnchor.trIndex) {
                                    if(j>oAnchor.colKeyIndex) {
                                        this.unselectCell(currentRow.cells[j]);
                                    }
                                }
                                // This is the target row, only unelect cells before the target cell
                                else if(currentRow.sectionRowIndex === nTargetTrIndex) {
                                    if(j<nTargetColKeyIndex) {
                                        this.unselectCell(currentRow.cells[j]);
                                    }
                                }
                                // Unselect all cells on this row
                                else {
                                    this.unselectCell(currentRow.cells[j]);
                                }
                            }
                        }
                    }
                    // Anchor row is below target row
                    else {
                        // Unselect all cells from target cell to anchor cell
                        for(i=nTargetTrIndex; i<=oAnchor.trIndex; i++) {
                            currentRow = allRows[i];
                            for(j=0; j<currentRow.cells.length; j++) {
                                // This is the target row, only unselect cells after the target cell
                                if(currentRow.sectionRowIndex == nTargetTrIndex) {
                                    if(j>nTargetColKeyIndex) {
                                        this.unselectCell(currentRow.cells[j]);
                                    }
                                }
                                // This is the anchor row, only unselect cells before the anchor cell
                                else if(currentRow.sectionRowIndex == oAnchor.trIndex) {
                                    if(j<oAnchor.colKeyIndex) {
                                        this.unselectCell(currentRow.cells[j]);
                                    }
                                }
                                // Unselect all cells on this row
                                else {
                                    this.unselectCell(currentRow.cells[j]);
                                }
                            }
                        }
                    }

                    // Select the target cell
                    this.selectCell(elTargetCell);
                }
            }
            // Invalid anchor
            else {
                // Set anchor
                this._oAnchorCell = oTargetCell;

                // Toggle selection of target
                if(this.isSelected(oTargetCell)) {
                    this.unselectCell(oTargetCell);
                }
                else {
                    this.selectCell(oTargetCell);
                }
            }

        }
         // Only SHIFT
        else if(bSHIFT) {
            this.unselectAllCells();

            // Validate anchor
            if(oAnchor) {
                // All cells are on the same row
                if(oAnchor.recordIndex === nTargetRecordIndex) {
                    // Select all cells between anchor cell and target cell,
                    // including the anchor cell and target cell
                    if(oAnchor.colKeyIndex < nTargetColKeyIndex) {
                        for(i=oAnchor.colKeyIndex; i<=nTargetColKeyIndex; i++) {
                            this.selectCell(elTargetRow.cells[i]);
                        }
                    }
                    // Select all cells between target cell and anchor cell
                    // including the target cell and anchor cell
                    else if(nTargetColKeyIndex < oAnchor.colKeyIndex) {
                        for(i=nTargetColKeyIndex; i<=oAnchor.colKeyIndex; i++) {
                            this.selectCell(elTargetRow.cells[i]);
                        }
                    }
                }
                // Anchor row is above target row
                else if(oAnchor.recordIndex < nTargetRecordIndex) {
                    // Select the cellblock from anchor cell to target cell
                    // including the anchor cell and the target cell
                    startIndex = Math.min(oAnchor.colKeyIndex, nTargetColKeyIndex);
                    endIndex = Math.max(oAnchor.colKeyIndex, nTargetColKeyIndex);

                    for(i=oAnchor.trIndex; i<=nTargetTrIndex; i++) {
                        for(j=startIndex; j<=endIndex; j++) {
                            this.selectCell(allRows[i].cells[j]);
                        }
                    }
                }
                // Anchor row is below target row
                else {
                    // Select the cellblock from target cell to anchor cell
                    // including the target cell and the anchor cell
                    startIndex = Math.min(oAnchor.colKeyIndex, nTargetColKeyIndex);
                    endIndex = Math.max(oAnchor.colKeyIndex, nTargetColKeyIndex);

                    for(i=nTargetTrIndex; i<=oAnchor.trIndex; i++) {
                        for(j=startIndex; j<=endIndex; j++) {
                            this.selectCell(allRows[i].cells[j]);
                        }
                    }
                }
            }
            // Invalid anchor
            else {
                // Set anchor
                this._oAnchorCell = oTargetCell;

                // Select target only
                this.selectCell(oTargetCell);
            }
        }
        // Only CTRL
        else if(bCTRL) {

            // Set anchor
            this._oAnchorCell = oTargetCell;

            // Toggle selection of target
            if(this.isSelected(oTargetCell)) {
                this.unselectCell(oTargetCell);
            }
            else {
                this.selectCell(oTargetCell);
            }

        }
        // Neither SHIFT nor CTRL
        else {
            this._handleSingleCellSelectionByMouse(oArgs);
        }
    }
},

/**
 * Determines selection behavior resulting from a key event when selection mode
 * is set to "cellblock".
 *
 * @method _handleCellBlockSelectionByKey
 * @param e {HTMLEvent} Event object.
 * @private
 */
_handleCellBlockSelectionByKey : function(e) {
    var nKey = Ev.getCharCode(e);
    var bSHIFT = e.shiftKey;
    if((nKey == 9) || !bSHIFT) {
        this._handleSingleCellSelectionByKey(e);
        return;
    }

    if((nKey > 36) && (nKey < 41)) {
        // Validate trigger
        var oTrigger = this._getSelectionTrigger();
        // Arrow selection only works if last selected row is on current page
        if(!oTrigger) {
            return null;
        }

        Ev.stopEvent(e);

        // Validate anchor
        var oAnchor = this._getSelectionAnchor(oTrigger);

        var i, startIndex, endIndex, elNew, elNewRow;
        var allRows = this.getTbodyEl().rows;
        var elThisRow = oTrigger.el.parentNode;

        // Determine which direction we're going to

        if(nKey == 40) { // arrow down
            // Selecting away from anchor cell
            if(oAnchor.recordIndex <= oTrigger.recordIndex) {
                // Select the horiz block on the next row...
                // ...making sure there is room below the trigger row
                elNewRow = this.getNextTrEl(oTrigger.el);
                if(elNewRow) {
                    startIndex = oAnchor.colKeyIndex;
                    endIndex = oTrigger.colKeyIndex;
                    // ...going left
                    if(startIndex > endIndex) {
                        for(i=startIndex; i>=endIndex; i--) {
                            elNew = elNewRow.cells[i];
                            this.selectCell(elNew);
                        }
                    }
                    // ... going right
                    else {
                        for(i=startIndex; i<=endIndex; i++) {
                            elNew = elNewRow.cells[i];
                            this.selectCell(elNew);
                        }
                    }
                }
            }
            // Unselecting towards anchor cell
            else {
                startIndex = Math.min(oAnchor.colKeyIndex, oTrigger.colKeyIndex);
                endIndex = Math.max(oAnchor.colKeyIndex, oTrigger.colKeyIndex);
                // Unselect the horiz block on this row towards the next row
                for(i=startIndex; i<=endIndex; i++) {
                    this.unselectCell(elThisRow.cells[i]);
                }
            }
        }
        // Arrow up
        else if(nKey == 38) {
            // Selecting away from anchor cell
            if(oAnchor.recordIndex >= oTrigger.recordIndex) {
                // Select the horiz block on the previous row...
                // ...making sure there is room
                elNewRow = this.getPreviousTrEl(oTrigger.el);
                if(elNewRow) {
                    // Select in order from anchor to trigger...
                    startIndex = oAnchor.colKeyIndex;
                    endIndex = oTrigger.colKeyIndex;
                    // ...going left
                    if(startIndex > endIndex) {
                        for(i=startIndex; i>=endIndex; i--) {
                            elNew = elNewRow.cells[i];
                            this.selectCell(elNew);
                        }
                    }
                    // ... going right
                    else {
                        for(i=startIndex; i<=endIndex; i++) {
                            elNew = elNewRow.cells[i];
                            this.selectCell(elNew);
                        }
                    }
                }
            }
            // Unselecting towards anchor cell
            else {
                startIndex = Math.min(oAnchor.colKeyIndex, oTrigger.colKeyIndex);
                endIndex = Math.max(oAnchor.colKeyIndex, oTrigger.colKeyIndex);
                // Unselect the horiz block on this row towards the previous row
                for(i=startIndex; i<=endIndex; i++) {
                    this.unselectCell(elThisRow.cells[i]);
                }
            }
        }
        // Arrow right
        else if(nKey == 39) {
            // Selecting away from anchor cell
            if(oAnchor.colKeyIndex <= oTrigger.colKeyIndex) {
                // Select the next vert block to the right...
                // ...making sure there is room
                if(oTrigger.colKeyIndex < elThisRow.cells.length-1) {
                    // Select in order from anchor to trigger...
                    startIndex = oAnchor.trIndex;
                    endIndex = oTrigger.trIndex;
                    // ...going up
                    if(startIndex > endIndex) {
                        for(i=startIndex; i>=endIndex; i--) {
                            elNew = allRows[i].cells[oTrigger.colKeyIndex+1];
                            this.selectCell(elNew);
                        }
                    }
                    // ... going down
                    else {
                        for(i=startIndex; i<=endIndex; i++) {
                            elNew = allRows[i].cells[oTrigger.colKeyIndex+1];
                            this.selectCell(elNew);
                        }
                    }
                }
            }
            // Unselecting towards anchor cell
            else {
                // Unselect the vert block on this column towards the right
                startIndex = Math.min(oAnchor.trIndex, oTrigger.trIndex);
                endIndex = Math.max(oAnchor.trIndex, oTrigger.trIndex);
                for(i=startIndex; i<=endIndex; i++) {
                    this.unselectCell(allRows[i].cells[oTrigger.colKeyIndex]);
                }
            }
        }
        // Arrow left
        else if(nKey == 37) {
            // Selecting away from anchor cell
            if(oAnchor.colKeyIndex >= oTrigger.colKeyIndex) {
                //Select the previous vert block to the left
                if(oTrigger.colKeyIndex > 0) {
                    // Select in order from anchor to trigger...
                    startIndex = oAnchor.trIndex;
                    endIndex = oTrigger.trIndex;
                    // ...going up
                    if(startIndex > endIndex) {
                        for(i=startIndex; i>=endIndex; i--) {
                            elNew = allRows[i].cells[oTrigger.colKeyIndex-1];
                            this.selectCell(elNew);
                        }
                    }
                    // ... going down
                    else {
                        for(i=startIndex; i<=endIndex; i++) {
                            elNew = allRows[i].cells[oTrigger.colKeyIndex-1];
                            this.selectCell(elNew);
                        }
                    }
                }
            }
            // Unselecting towards anchor cell
            else {
                // Unselect the vert block on this column towards the left
                startIndex = Math.min(oAnchor.trIndex, oTrigger.trIndex);
                endIndex = Math.max(oAnchor.trIndex, oTrigger.trIndex);
                for(i=startIndex; i<=endIndex; i++) {
                    this.unselectCell(allRows[i].cells[oTrigger.colKeyIndex]);
                }
            }
        }
    }
},

/**
 * Determines selection behavior resulting from a mouse event when selection mode
 * is set to "cellrange".
 *
 * @method _handleCellRangeSelectionByMouse
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 * @private
 */
_handleCellRangeSelectionByMouse : function(oArgs) {
    var elTarget = oArgs.target;

    // Validate target cell
    var elTargetCell = this.getTdEl(elTarget);
    if(elTargetCell) {
        var e = oArgs.event;
        var bSHIFT = e.shiftKey;
        var bCTRL = e.ctrlKey || ((navigator.userAgent.toLowerCase().indexOf("mac") != -1) && e.metaKey);

        var elTargetRow = this.getTrEl(elTargetCell);
        var nTargetTrIndex = this.getTrIndex(elTargetRow);
        var oTargetColumn = this.getColumn(elTargetCell);
        var nTargetColKeyIndex = oTargetColumn.getKeyIndex();
        var oTargetRecord = this.getRecord(elTargetRow);
        var nTargetRecordIndex = this._oRecordSet.getRecordIndex(oTargetRecord);
        var oTargetCell = {record:oTargetRecord, column:oTargetColumn};

        var oAnchor = this._getSelectionAnchor();

        var allRows = this.getTbodyEl().rows;
        var currentRow, i, j;

        // Both SHIFT and CTRL
        if(bSHIFT && bCTRL) {

            // Validate anchor
            if(oAnchor) {
                // Anchor is selected
                if(this.isSelected(oAnchor.cell)) {
                    // All cells are on the same row
                    if(oAnchor.recordIndex === nTargetRecordIndex) {
                        // Select all cells between anchor cell and target cell, including target cell
                        if(oAnchor.colKeyIndex < nTargetColKeyIndex) {
                            for(i=oAnchor.colKeyIndex+1; i<=nTargetColKeyIndex; i++) {
                                this.selectCell(elTargetRow.cells[i]);
                            }
                        }
                        // Select all cells between target cell and anchor cell, including target cell
                        else if(nTargetColKeyIndex < oAnchor.colKeyIndex) {
                            for(i=nTargetColKeyIndex; i<oAnchor.colKeyIndex; i++) {
                                this.selectCell(elTargetRow.cells[i]);
                            }
                        }
                    }
                    // Anchor row is above target row
                    else if(oAnchor.recordIndex < nTargetRecordIndex) {
                        // Select all cells on anchor row from anchor cell to the end of the row
                        for(i=oAnchor.colKeyIndex+1; i<elTargetRow.cells.length; i++) {
                            this.selectCell(elTargetRow.cells[i]);
                        }

                        // Select all cells on all rows between anchor row and target row
                        for(i=oAnchor.trIndex+1; i<nTargetTrIndex; i++) {
                            for(j=0; j<allRows[i].cells.length; j++){
                                this.selectCell(allRows[i].cells[j]);
                            }
                        }

                        // Select all cells on target row from first cell to the target cell
                        for(i=0; i<=nTargetColKeyIndex; i++) {
                            this.selectCell(elTargetRow.cells[i]);
                        }
                    }
                    // Anchor row is below target row
                    else {
                        // Select all cells on target row from target cell to the end of the row
                        for(i=nTargetColKeyIndex; i<elTargetRow.cells.length; i++) {
                            this.selectCell(elTargetRow.cells[i]);
                        }

                        // Select all cells on all rows between target row and anchor row
                        for(i=nTargetTrIndex+1; i<oAnchor.trIndex; i++) {
                            for(j=0; j<allRows[i].cells.length; j++){
                                this.selectCell(allRows[i].cells[j]);
                            }
                        }

                        // Select all cells on anchor row from first cell to the anchor cell
                        for(i=0; i<oAnchor.colKeyIndex; i++) {
                            this.selectCell(elTargetRow.cells[i]);
                        }
                    }
                }
                // Anchor cell is unselected
                else {
                    // All cells are on the same row
                    if(oAnchor.recordIndex === nTargetRecordIndex) {
                        // Unselect all cells between anchor cell and target cell
                        if(oAnchor.colKeyIndex < nTargetColKeyIndex) {
                            for(i=oAnchor.colKeyIndex+1; i<nTargetColKeyIndex; i++) {
                                this.unselectCell(elTargetRow.cells[i]);
                            }
                        }
                        // Select all cells between target cell and anchor cell
                        else if(nTargetColKeyIndex < oAnchor.colKeyIndex) {
                            for(i=nTargetColKeyIndex+1; i<oAnchor.colKeyIndex; i++) {
                                this.unselectCell(elTargetRow.cells[i]);
                            }
                        }
                    }
                    // Anchor row is above target row
                    if(oAnchor.recordIndex < nTargetRecordIndex) {
                        // Unselect all cells from anchor cell to target cell
                        for(i=oAnchor.trIndex; i<=nTargetTrIndex; i++) {
                            currentRow = allRows[i];
                            for(j=0; j<currentRow.cells.length; j++) {
                                // This is the anchor row, only unselect cells after the anchor cell
                                if(currentRow.sectionRowIndex === oAnchor.trIndex) {
                                    if(j>oAnchor.colKeyIndex) {
                                        this.unselectCell(currentRow.cells[j]);
                                    }
                                }
                                // This is the target row, only unelect cells before the target cell
                                else if(currentRow.sectionRowIndex === nTargetTrIndex) {
                                    if(j<nTargetColKeyIndex) {
                                        this.unselectCell(currentRow.cells[j]);
                                    }
                                }
                                // Unselect all cells on this row
                                else {
                                    this.unselectCell(currentRow.cells[j]);
                                }
                            }
                        }
                    }
                    // Anchor row is below target row
                    else {
                        // Unselect all cells from target cell to anchor cell
                        for(i=nTargetTrIndex; i<=oAnchor.trIndex; i++) {
                            currentRow = allRows[i];
                            for(j=0; j<currentRow.cells.length; j++) {
                                // This is the target row, only unselect cells after the target cell
                                if(currentRow.sectionRowIndex == nTargetTrIndex) {
                                    if(j>nTargetColKeyIndex) {
                                        this.unselectCell(currentRow.cells[j]);
                                    }
                                }
                                // This is the anchor row, only unselect cells before the anchor cell
                                else if(currentRow.sectionRowIndex == oAnchor.trIndex) {
                                    if(j<oAnchor.colKeyIndex) {
                                        this.unselectCell(currentRow.cells[j]);
                                    }
                                }
                                // Unselect all cells on this row
                                else {
                                    this.unselectCell(currentRow.cells[j]);
                                }
                            }
                        }
                    }

                    // Select the target cell
                    this.selectCell(elTargetCell);
                }
            }
            // Invalid anchor
            else {
                // Set anchor
                this._oAnchorCell = oTargetCell;

                // Toggle selection of target
                if(this.isSelected(oTargetCell)) {
                    this.unselectCell(oTargetCell);
                }
                else {
                    this.selectCell(oTargetCell);
                }
            }
        }
         // Only SHIFT
        else if(bSHIFT) {

            this.unselectAllCells();

            // Validate anchor
            if(oAnchor) {
                // All cells are on the same row
                if(oAnchor.recordIndex === nTargetRecordIndex) {
                    // Select all cells between anchor cell and target cell,
                    // including the anchor cell and target cell
                    if(oAnchor.colKeyIndex < nTargetColKeyIndex) {
                        for(i=oAnchor.colKeyIndex; i<=nTargetColKeyIndex; i++) {
                            this.selectCell(elTargetRow.cells[i]);
                        }
                    }
                    // Select all cells between target cell and anchor cell
                    // including the target cell and anchor cell
                    else if(nTargetColKeyIndex < oAnchor.colKeyIndex) {
                        for(i=nTargetColKeyIndex; i<=oAnchor.colKeyIndex; i++) {
                            this.selectCell(elTargetRow.cells[i]);
                        }
                    }
                }
                // Anchor row is above target row
                else if(oAnchor.recordIndex < nTargetRecordIndex) {
                    // Select all cells from anchor cell to target cell
                    // including the anchor cell and target cell
                    for(i=oAnchor.trIndex; i<=nTargetTrIndex; i++) {
                        currentRow = allRows[i];
                        for(j=0; j<currentRow.cells.length; j++) {
                            // This is the anchor row, only select the anchor cell and after
                            if(currentRow.sectionRowIndex == oAnchor.trIndex) {
                                if(j>=oAnchor.colKeyIndex) {
                                    this.selectCell(currentRow.cells[j]);
                                }
                            }
                            // This is the target row, only select the target cell and before
                            else if(currentRow.sectionRowIndex == nTargetTrIndex) {
                                if(j<=nTargetColKeyIndex) {
                                    this.selectCell(currentRow.cells[j]);
                                }
                            }
                            // Select all cells on this row
                            else {
                                this.selectCell(currentRow.cells[j]);
                            }
                        }
                    }
                }
                // Anchor row is below target row
                else {
                    // Select all cells from target cell to anchor cell,
                    // including the target cell and anchor cell
                    for(i=nTargetTrIndex; i<=oAnchor.trIndex; i++) {
                        currentRow = allRows[i];
                        for(j=0; j<currentRow.cells.length; j++) {
                            // This is the target row, only select the target cell and after
                            if(currentRow.sectionRowIndex == nTargetTrIndex) {
                                if(j>=nTargetColKeyIndex) {
                                    this.selectCell(currentRow.cells[j]);
                                }
                            }
                            // This is the anchor row, only select the anchor cell and before
                            else if(currentRow.sectionRowIndex == oAnchor.trIndex) {
                                if(j<=oAnchor.colKeyIndex) {
                                    this.selectCell(currentRow.cells[j]);
                                }
                            }
                            // Select all cells on this row
                            else {
                                this.selectCell(currentRow.cells[j]);
                            }
                        }
                    }
                }
            }
            // Invalid anchor
            else {
                // Set anchor
                this._oAnchorCell = oTargetCell;

                // Select target only
                this.selectCell(oTargetCell);
            }


        }
        // Only CTRL
        else if(bCTRL) {

            // Set anchor
            this._oAnchorCell = oTargetCell;

            // Toggle selection of target
            if(this.isSelected(oTargetCell)) {
                this.unselectCell(oTargetCell);
            }
            else {
                this.selectCell(oTargetCell);
            }

        }
        // Neither SHIFT nor CTRL
        else {
            this._handleSingleCellSelectionByMouse(oArgs);
        }
    }
},

/**
 * Determines selection behavior resulting from a key event when selection mode
 * is set to "cellrange".
 *
 * @method _handleCellRangeSelectionByKey
 * @param e {HTMLEvent} Event object.
 * @private
 */
_handleCellRangeSelectionByKey : function(e) {
    var nKey = Ev.getCharCode(e);
    var bSHIFT = e.shiftKey;
    if((nKey == 9) || !bSHIFT) {
        this._handleSingleCellSelectionByKey(e);
        return;
    }

    if((nKey > 36) && (nKey < 41)) {
        // Validate trigger
        var oTrigger = this._getSelectionTrigger();
        // Arrow selection only works if last selected row is on current page
        if(!oTrigger) {
            return null;
        }

        Ev.stopEvent(e);

        // Validate anchor
        var oAnchor = this._getSelectionAnchor(oTrigger);

        var i, elNewRow, elNew;
        var allRows = this.getTbodyEl().rows;
        var elThisRow = oTrigger.el.parentNode;

        // Arrow down
        if(nKey == 40) {
            elNewRow = this.getNextTrEl(oTrigger.el);

            // Selecting away from anchor cell
            if(oAnchor.recordIndex <= oTrigger.recordIndex) {
                // Select all cells to the end of this row
                for(i=oTrigger.colKeyIndex+1; i<elThisRow.cells.length; i++){
                    elNew = elThisRow.cells[i];
                    this.selectCell(elNew);
                }

                // Select some of the cells on the next row down
                if(elNewRow) {
                    for(i=0; i<=oTrigger.colKeyIndex; i++){
                        elNew = elNewRow.cells[i];
                        this.selectCell(elNew);
                    }
                }
            }
            // Unselecting towards anchor cell
            else {
                // Unselect all cells to the end of this row
                for(i=oTrigger.colKeyIndex; i<elThisRow.cells.length; i++){
                    this.unselectCell(elThisRow.cells[i]);
                }

                // Unselect some of the cells on the next row down
                if(elNewRow) {
                    for(i=0; i<oTrigger.colKeyIndex; i++){
                        this.unselectCell(elNewRow.cells[i]);
                    }
                }
            }
        }
        // Arrow up
        else if(nKey == 38) {
            elNewRow = this.getPreviousTrEl(oTrigger.el);

            // Selecting away from anchor cell
            if(oAnchor.recordIndex >= oTrigger.recordIndex) {
                // Select all the cells to the beginning of this row
                for(i=oTrigger.colKeyIndex-1; i>-1; i--){
                    elNew = elThisRow.cells[i];
                    this.selectCell(elNew);
                }

                // Select some of the cells from the end of the previous row
                if(elNewRow) {
                    for(i=elThisRow.cells.length-1; i>=oTrigger.colKeyIndex; i--){
                        elNew = elNewRow.cells[i];
                        this.selectCell(elNew);
                    }
                }
            }
            // Unselecting towards anchor cell
            else {
                // Unselect all the cells to the beginning of this row
                for(i=oTrigger.colKeyIndex; i>-1; i--){
                    this.unselectCell(elThisRow.cells[i]);
                }

                // Unselect some of the cells from the end of the previous row
                if(elNewRow) {
                    for(i=elThisRow.cells.length-1; i>oTrigger.colKeyIndex; i--){
                        this.unselectCell(elNewRow.cells[i]);
                    }
                }
            }
        }
        // Arrow right
        else if(nKey == 39) {
            elNewRow = this.getNextTrEl(oTrigger.el);

            // Selecting away from anchor cell
            if(oAnchor.recordIndex < oTrigger.recordIndex) {
                // Select the next cell to the right
                if(oTrigger.colKeyIndex < elThisRow.cells.length-1) {
                    elNew = elThisRow.cells[oTrigger.colKeyIndex+1];
                    this.selectCell(elNew);
                }
                // Select the first cell of the next row
                else if(elNewRow) {
                    elNew = elNewRow.cells[0];
                    this.selectCell(elNew);
                }
            }
            // Unselecting towards anchor cell
            else if(oAnchor.recordIndex > oTrigger.recordIndex) {
                this.unselectCell(elThisRow.cells[oTrigger.colKeyIndex]);

                // Unselect this cell towards the right
                if(oTrigger.colKeyIndex < elThisRow.cells.length-1) {
                }
                // Unselect this cells towards the first cell of the next row
                else {
                }
            }
            // Anchor is on this row
            else {
                // Selecting away from anchor
                if(oAnchor.colKeyIndex <= oTrigger.colKeyIndex) {
                    // Select the next cell to the right
                    if(oTrigger.colKeyIndex < elThisRow.cells.length-1) {
                        elNew = elThisRow.cells[oTrigger.colKeyIndex+1];
                        this.selectCell(elNew);
                    }
                    // Select the first cell on the next row
                    else if(oTrigger.trIndex < allRows.length-1){
                        elNew = elNewRow.cells[0];
                        this.selectCell(elNew);
                    }
                }
                // Unselecting towards anchor
                else {
                    // Unselect this cell towards the right
                    this.unselectCell(elThisRow.cells[oTrigger.colKeyIndex]);
                }
            }
        }
        // Arrow left
        else if(nKey == 37) {
            elNewRow = this.getPreviousTrEl(oTrigger.el);

            // Unselecting towards the anchor
            if(oAnchor.recordIndex < oTrigger.recordIndex) {
                this.unselectCell(elThisRow.cells[oTrigger.colKeyIndex]);

                // Unselect this cell towards the left
                if(oTrigger.colKeyIndex > 0) {
                }
                // Unselect this cell towards the last cell of the previous row
                else {
                }
            }
            // Selecting towards the anchor
            else if(oAnchor.recordIndex > oTrigger.recordIndex) {
                // Select the next cell to the left
                if(oTrigger.colKeyIndex > 0) {
                    elNew = elThisRow.cells[oTrigger.colKeyIndex-1];
                    this.selectCell(elNew);
                }
                // Select the last cell of the previous row
                else if(oTrigger.trIndex > 0){
                    elNew = elNewRow.cells[elNewRow.cells.length-1];
                    this.selectCell(elNew);
                }
            }
            // Anchor is on this row
            else {
                // Selecting away from anchor cell
                if(oAnchor.colKeyIndex >= oTrigger.colKeyIndex) {
                    // Select the next cell to the left
                    if(oTrigger.colKeyIndex > 0) {
                        elNew = elThisRow.cells[oTrigger.colKeyIndex-1];
                        this.selectCell(elNew);
                    }
                    // Select the last cell of the previous row
                    else if(oTrigger.trIndex > 0){
                        elNew = elNewRow.cells[elNewRow.cells.length-1];
                        this.selectCell(elNew);
                    }
                }
                // Unselecting towards anchor cell
                else {
                    this.unselectCell(elThisRow.cells[oTrigger.colKeyIndex]);

                    // Unselect this cell towards the left
                    if(oTrigger.colKeyIndex > 0) {
                    }
                    // Unselect this cell towards the last cell of the previous row
                    else {
                    }
                }
            }
        }
    }
},

/**
 * Determines selection behavior resulting from a mouse event when selection mode
 * is set to "singlecell".
 *
 * @method _handleSingleCellSelectionByMouse
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 * @private
 */
_handleSingleCellSelectionByMouse : function(oArgs) {
    var elTarget = oArgs.target;

    // Validate target cell
    var elTargetCell = this.getTdEl(elTarget);
    if(elTargetCell) {
        var elTargetRow = this.getTrEl(elTargetCell);
        var oTargetRecord = this.getRecord(elTargetRow);
        var oTargetColumn = this.getColumn(elTargetCell);
        var oTargetCell = {record:oTargetRecord, column:oTargetColumn};

        // Set anchor
        this._oAnchorCell = oTargetCell;

        // Select only target
        this.unselectAllCells();
        this.selectCell(oTargetCell);
    }
},

/**
 * Determines selection behavior resulting from a key event when selection mode
 * is set to "singlecell".
 *
 * @method _handleSingleCellSelectionByKey
 * @param e {HTMLEvent} Event object.
 * @private
 */
_handleSingleCellSelectionByKey : function(e) {
    var nKey = Ev.getCharCode(e);
    if((nKey == 9) || ((nKey > 36) && (nKey < 41))) {
        var bSHIFT = e.shiftKey;

        // Validate trigger
        var oTrigger = this._getSelectionTrigger();
        // Arrow selection only works if last selected row is on current page
        if(!oTrigger) {
            return null;
        }

        // Determine the new cell to select
        var elNew;
        if(nKey == 40) { // Arrow down
            elNew = this.getBelowTdEl(oTrigger.el);

            // Validate new cell
            if(elNew === null) {
                //TODO: wrap around to first tr on current page

                //TODO: wrap forward to first tr of next page

                // Bottom selection is sticky
                elNew = oTrigger.el;
            }
        }
        else if(nKey == 38) { // Arrow up
            elNew = this.getAboveTdEl(oTrigger.el);

            // Validate new cell
            if(elNew === null) {
                //TODO: wrap around to last tr on current page

                //TODO: wrap back to last tr of previous page

                // Top selection is sticky
                elNew = oTrigger.el;
            }
        }
        else if((nKey == 39) || (!bSHIFT && (nKey == 9))) { // Arrow right or tab
            elNew = this.getNextTdEl(oTrigger.el);

            // Validate new cell
            if(elNew === null) {
                //TODO: wrap around to first td on current page

                //TODO: wrap forward to first td of next page

                // Top-left selection is sticky, and release TAB focus
                //elNew = oTrigger.el;
                return;
            }
        }
        else if((nKey == 37) || (bSHIFT && (nKey == 9))) { // Arrow left or shift-tab
            elNew = this.getPreviousTdEl(oTrigger.el);

            // Validate new cell
            if(elNew === null) {
                //TODO: wrap around to last td on current page

                //TODO: wrap back to last td of previous page

                // Bottom-right selection is sticky, and release TAB focus
                //elNew = oTrigger.el;
                return;
            }
        }

        Ev.stopEvent(e);
        
        // Unselect all cells
        this.unselectAllCells();

        // Select the new cell
        this.selectCell(elNew);

        // Set new anchor
        this._oAnchorCell = {record:this.getRecord(elNew), column:this.getColumn(elNew)};
    }
},

/**
 * Returns array of selected TR elements on the page.
 *
 * @method getSelectedTrEls
 * @return {HTMLElement[]} Array of selected TR elements.
 */
getSelectedTrEls : function() {
    return Dom.getElementsByClassName(DT.CLASS_SELECTED,"tr",this._elTbody);
},

/**
 * Sets given row to the selected state.
 *
 * @method selectRow
 * @param row {HTMLElement | String | YAHOO.widget.Record | Number} HTML element
 * reference or ID string, Record instance, or RecordSet position index.
 */
selectRow : function(row) {
    var oRecord, elRow;

    if(row instanceof YAHOO.widget.Record) {
        oRecord = this._oRecordSet.getRecord(row);
        elRow = this.getTrEl(oRecord);
    }
    else if(lang.isNumber(row)) {
        oRecord = this.getRecord(row);
        elRow = this.getTrEl(oRecord);
    }
    else {
        elRow = this.getTrEl(row);
        oRecord = this.getRecord(elRow);
    }

    if(oRecord) {
        // Update selection trackers
        var tracker = this._aSelections || [];
        var sRecordId = oRecord.getId();
        var index = -1;

        // Remove if already there:
        // Use Array.indexOf if available...
        /*if(tracker.indexOf && (tracker.indexOf(sRecordId) >  -1)) {
            tracker.splice(tracker.indexOf(sRecordId),1);
        }*/
        if(tracker.indexOf) {
            index = tracker.indexOf(sRecordId);
            
        }
        // ...or do it the old-fashioned way
        else {
            for(var j=tracker.length-1; j>-1; j--) {
                if(tracker[j] === sRecordId){
                    index = j;
                    break;
                }
            }
        }
        if(index > -1) {
            tracker.splice(index,1);
        }
        
        // Add to the end
        tracker.push(sRecordId);
        this._aSelections = tracker;

        // Update trackers
        if(!this._oAnchorRecord) {
            this._oAnchorRecord = oRecord;
        }

        // Update UI
        if(elRow) {
            Dom.addClass(elRow, DT.CLASS_SELECTED);
        }

        this.fireEvent("rowSelectEvent", {record:oRecord, el:elRow});
        YAHOO.log("Selected " + elRow, "info", this.toString());
    }
    else {
        YAHOO.log("Could not select row " + row, "warn", this.toString());
    }
},

/**
 * Sets given row to the unselected state.
 *
 * @method unselectRow
 * @param row {HTMLElement | String | YAHOO.widget.Record | Number} HTML element
 * reference or ID string, Record instance, or RecordSet position index.
 */
unselectRow : function(row) {
    var elRow = this.getTrEl(row);

    var oRecord;
    if(row instanceof YAHOO.widget.Record) {
        oRecord = this._oRecordSet.getRecord(row);
    }
    else if(lang.isNumber(row)) {
        oRecord = this.getRecord(row);
    }
    else {
        oRecord = this.getRecord(elRow);
    }

    if(oRecord) {
        // Update selection trackers
        var tracker = this._aSelections || [];
        var sRecordId = oRecord.getId();
        var index = -1;

        // Use Array.indexOf if available...
        if(tracker.indexOf) {
            index = tracker.indexOf(sRecordId);
        }
        // ...or do it the old-fashioned way
        else {
            for(var j=tracker.length-1; j>-1; j--) {
                if(tracker[j] === sRecordId){
                    index = j;
                    break;
                }
            }
        }
        if(index > -1) {
            // Update tracker
            tracker.splice(index,1);
            this._aSelections = tracker;

            // Update the UI
            Dom.removeClass(elRow, DT.CLASS_SELECTED);

            this.fireEvent("rowUnselectEvent", {record:oRecord, el:elRow});
            YAHOO.log("Unselected " + elRow, "info", this.toString());

            return;
        }
    }
    YAHOO.log("Could not unselect row " + row, "warn", this.toString());
},

/**
 * Clears out all row selections.
 *
 * @method unselectAllRows
 */
unselectAllRows : function() {
    // Remove all rows from tracker
    var tracker = this._aSelections || [],
        recId,
        removed = [];
    for(var j=tracker.length-1; j>-1; j--) {
       if(lang.isString(tracker[j])){
            recId = tracker.splice(j,1);
            removed[removed.length] = this.getRecord(lang.isArray(recId) ? recId[0] : recId);
        }
    }

    // Update tracker
    this._aSelections = tracker;

    // Update UI
    this._unselectAllTrEls();

    this.fireEvent("unselectAllRowsEvent", {records: removed});
    YAHOO.log("Unselected all rows", "info", this.toString());
},

/**
 * Convenience method to remove the class YAHOO.widget.DataTable.CLASS_SELECTED
 * from all TD elements in the internal tracker.
 *
 * @method _unselectAllTdEls
 * @private
 */
_unselectAllTdEls : function() {
    var selectedCells = Dom.getElementsByClassName(DT.CLASS_SELECTED,"td",this._elTbody);
    Dom.removeClass(selectedCells, DT.CLASS_SELECTED);
},

/**
 * Returns array of selected TD elements on the page.
 *
 * @method getSelectedTdEls
 * @return {HTMLElement[]} Array of selected TD elements.
 */
getSelectedTdEls : function() {
    return Dom.getElementsByClassName(DT.CLASS_SELECTED,"td",this._elTbody);
},

/**
 * Sets given cell to the selected state.
 *
 * @method selectCell
 * @param cell {HTMLElement | String | Object} TD element or child of a TD element, or
 * object literal of syntax {record:oRecord, column:oColumn}.
 */
selectCell : function(cell) {
//TODO: accept {record} in selectCell()
    var elCell = this.getTdEl(cell);

    if(elCell) {
        var oRecord = this.getRecord(elCell);
        var oColumn = this.getColumn(this.getCellIndex(elCell));
        var sColumnKey = oColumn.getKey();

        if(oRecord && sColumnKey) {
            // Get Record ID
            var tracker = this._aSelections || [];
            var sRecordId = oRecord.getId();

            // Remove if there
            for(var j=tracker.length-1; j>-1; j--) {
               if((tracker[j].recordId === sRecordId) && (tracker[j].columnKey === sColumnKey)){
                    tracker.splice(j,1);
                    break;
                }
            }

            // Add to the end
            tracker.push({recordId:sRecordId, columnKey:sColumnKey});

            // Update trackers
            this._aSelections = tracker;
            if(!this._oAnchorCell) {
                this._oAnchorCell = {record:oRecord, column:oColumn};
            }

            // Update the UI
            Dom.addClass(elCell, DT.CLASS_SELECTED);

            this.fireEvent("cellSelectEvent", {record:oRecord, column:oColumn, key: sColumnKey, el:elCell});
            YAHOO.log("Selected " + elCell, "info", this.toString());
            return;
        }
    }
    YAHOO.log("Could not select cell " + cell, "warn", this.toString());
},

/**
 * Sets given cell to the unselected state.
 *
 * @method unselectCell
 * @param cell {HTMLElement | String | Object} TD element or child of a TD element, or
 * object literal of syntax {record:oRecord, column:oColumn}.
 * @param cell {HTMLElement | String} DOM element reference or ID string
 * to DataTable page element or RecordSet index.
 */
unselectCell : function(cell) {
    var elCell = this.getTdEl(cell);

    if(elCell) {
        var oRecord = this.getRecord(elCell);
        var oColumn = this.getColumn(this.getCellIndex(elCell));
        var sColumnKey = oColumn.getKey();

        if(oRecord && sColumnKey) {
            // Get Record ID
            var tracker = this._aSelections || [];
            var id = oRecord.getId();

            // Is it selected?
            for(var j=tracker.length-1; j>-1; j--) {
                if((tracker[j].recordId === id) && (tracker[j].columnKey === sColumnKey)){
                    // Remove from tracker
                    tracker.splice(j,1);

                    // Update tracker
                    this._aSelections = tracker;

                    // Update the UI
                    Dom.removeClass(elCell, DT.CLASS_SELECTED);

                    this.fireEvent("cellUnselectEvent", {record:oRecord, column: oColumn, key:sColumnKey, el:elCell});
                    YAHOO.log("Unselected " + elCell, "info", this.toString());
                    return;
                }
            }
        }
    }
    YAHOO.log("Could not unselect cell " + cell, "warn", this.toString());
},

/**
 * Clears out all cell selections.
 *
 * @method unselectAllCells
 */
unselectAllCells : function() {
    // Remove all cells from tracker
    var tracker = this._aSelections || [];
    for(var j=tracker.length-1; j>-1; j--) {
       if(lang.isObject(tracker[j])){
            tracker.splice(j,1);
        }
    }

    // Update tracker
    this._aSelections = tracker;

    // Update UI
    this._unselectAllTdEls();

    //TODO: send data to unselectAllCellsEvent handler
    this.fireEvent("unselectAllCellsEvent");
    YAHOO.log("Unselected all cells", "info", this.toString());
},

/**
 * Returns true if given item is selected, false otherwise.
 *
 * @method isSelected
 * @param o {String | HTMLElement | YAHOO.widget.Record | Number
 * {record:YAHOO.widget.Record, column:YAHOO.widget.Column} } TR or TD element by
 * reference or ID string, a Record instance, a RecordSet position index,
 * or an object literal representation
 * of a cell.
 * @return {Boolean} True if item is selected.
 */
isSelected : function(o) {
    if(o && (o.ownerDocument == document)) {
        return (Dom.hasClass(this.getTdEl(o),DT.CLASS_SELECTED) || Dom.hasClass(this.getTrEl(o),DT.CLASS_SELECTED));
    }
    else {
        var oRecord, sRecordId, j;
        var tracker = this._aSelections;
        if(tracker && tracker.length > 0) {
            // Looking for a Record?
            if(o instanceof YAHOO.widget.Record) {
                oRecord = o;
            }
            else if(lang.isNumber(o)) {
                oRecord = this.getRecord(o);
            }
            if(oRecord) {
                sRecordId = oRecord.getId();

                // Is it there?
                // Use Array.indexOf if available...
                if(tracker.indexOf) {
                    if(tracker.indexOf(sRecordId) >  -1) {
                        return true;
                    }
                }
                // ...or do it the old-fashioned way
                else {
                    for(j=tracker.length-1; j>-1; j--) {
                       if(tracker[j] === sRecordId){
                        return true;
                       }
                    }
                }
            }
            // Looking for a cell
            else if(o.record && o.column){
                sRecordId = o.record.getId();
                var sColumnKey = o.column.getKey();

                for(j=tracker.length-1; j>-1; j--) {
                    if((tracker[j].recordId === sRecordId) && (tracker[j].columnKey === sColumnKey)){
                        return true;
                    }
                }
            }
        }
    }
    return false;
},

/**
 * Returns selected rows as an array of Record IDs.
 *
 * @method getSelectedRows
 * @return {String[]} Array of selected rows by Record ID.
 */
getSelectedRows : function() {
    var aSelectedRows = [];
    var tracker = this._aSelections || [];
    for(var j=0; j<tracker.length; j++) {
       if(lang.isString(tracker[j])){
            aSelectedRows.push(tracker[j]);
        }
    }
    return aSelectedRows;
},

/**
 * Returns selected cells as an array of object literals:
 *     {recordId:sRecordId, columnKey:sColumnKey}.
 *
 * @method getSelectedCells
 * @return {Object[]} Array of selected cells by Record ID and Column ID.
 */
getSelectedCells : function() {
    var aSelectedCells = [];
    var tracker = this._aSelections || [];
    for(var j=0; j<tracker.length; j++) {
       if(tracker[j] && lang.isObject(tracker[j])){
            aSelectedCells.push(tracker[j]);
        }
    }
    return aSelectedCells;
},

/**
 * Returns last selected Record ID.
 *
 * @method getLastSelectedRecord
 * @return {String} Record ID of last selected row.
 */
getLastSelectedRecord : function() {
    var tracker = this._aSelections;
    if(tracker && tracker.length > 0) {
        for(var i=tracker.length-1; i>-1; i--) {
           if(lang.isString(tracker[i])){
                return tracker[i];
            }
        }
    }
},

/**
 * Returns last selected cell as an object literal:
 *     {recordId:sRecordId, columnKey:sColumnKey}.
 *
 * @method getLastSelectedCell
 * @return {Object} Object literal representation of a cell.
 */
getLastSelectedCell : function() {
    var tracker = this._aSelections;
    if(tracker && tracker.length > 0) {
        for(var i=tracker.length-1; i>-1; i--) {
           if(tracker[i].recordId && tracker[i].columnKey){
                return tracker[i];
            }
        }
    }
},

/**
 * Assigns the class YAHOO.widget.DataTable.CLASS_HIGHLIGHTED to the given row.
 *
 * @method highlightRow
 * @param row {HTMLElement | String} DOM element reference or ID string.
 */
highlightRow : function(row) {
    var elRow = this.getTrEl(row);

    if(elRow) {
        // Make sure previous row is unhighlighted
/*        if(this._sLastHighlightedTrElId) {
            Dom.removeClass(this._sLastHighlightedTrElId,DT.CLASS_HIGHLIGHTED);
        }*/
        var oRecord = this.getRecord(elRow);
        Dom.addClass(elRow,DT.CLASS_HIGHLIGHTED);
        //this._sLastHighlightedTrElId = elRow.id;
        this.fireEvent("rowHighlightEvent", {record:oRecord, el:elRow});
        YAHOO.log("Highlighted " + elRow, "info", this.toString());
        return;
    }
    YAHOO.log("Could not highlight row " + row, "warn", this.toString());
},

/**
 * Removes the class YAHOO.widget.DataTable.CLASS_HIGHLIGHTED from the given row.
 *
 * @method unhighlightRow
 * @param row {HTMLElement | String} DOM element reference or ID string.
 */
unhighlightRow : function(row) {
    var elRow = this.getTrEl(row);

    if(elRow) {
        var oRecord = this.getRecord(elRow);
        Dom.removeClass(elRow,DT.CLASS_HIGHLIGHTED);
        this.fireEvent("rowUnhighlightEvent", {record:oRecord, el:elRow});
        YAHOO.log("Unhighlighted " + elRow, "info", this.toString());
        return;
    }
    YAHOO.log("Could not unhighlight row " + row, "warn", this.toString());
},

/**
 * Assigns the class YAHOO.widget.DataTable.CLASS_HIGHLIGHTED to the given cell.
 *
 * @method highlightCell
 * @param cell {HTMLElement | String} DOM element reference or ID string.
 */
highlightCell : function(cell) {
    var elCell = this.getTdEl(cell);

    if(elCell) {
        // Make sure previous cell is unhighlighted
        if(this._elLastHighlightedTd) {
            this.unhighlightCell(this._elLastHighlightedTd);
        }

        var oRecord = this.getRecord(elCell);
        var oColumn = this.getColumn(this.getCellIndex(elCell));
        var sColumnKey = oColumn.getKey();
        Dom.addClass(elCell,DT.CLASS_HIGHLIGHTED);
        this._elLastHighlightedTd = elCell;
        this.fireEvent("cellHighlightEvent", {record:oRecord, column:oColumn, key:sColumnKey, el:elCell});
        YAHOO.log("Highlighted " + elCell, "info", this.toString());
        return;
    }
    YAHOO.log("Could not highlight cell " + cell, "warn", this.toString());
},

/**
 * Removes the class YAHOO.widget.DataTable.CLASS_HIGHLIGHTED from the given cell.
 *
 * @method unhighlightCell
 * @param cell {HTMLElement | String} DOM element reference or ID string.
 */
unhighlightCell : function(cell) {
    var elCell = this.getTdEl(cell);

    if(elCell) {
        var oRecord = this.getRecord(elCell);
        Dom.removeClass(elCell,DT.CLASS_HIGHLIGHTED);
        this._elLastHighlightedTd = null;
        this.fireEvent("cellUnhighlightEvent", {record:oRecord, column:this.getColumn(this.getCellIndex(elCell)), key:this.getColumn(this.getCellIndex(elCell)).getKey(), el:elCell});
        YAHOO.log("Unhighlighted " + elCell, "info", this.toString());
        return;
    }
    YAHOO.log("Could not unhighlight cell " + cell, "warn", this.toString());
},













































// INLINE EDITING

/**
 * Assigns CellEditor instance to existing Column.
 * @method addCellEditor
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param oEditor {YAHOO.wdiget.CellEditor} CellEditor instance.
 */
addCellEditor : function(oColumn, oEditor) {
    oColumn.editor = oEditor;
    oColumn.editor.subscribe("showEvent", this._onEditorShowEvent, this, true);
    oColumn.editor.subscribe("keydownEvent", this._onEditorKeydownEvent, this, true);
    oColumn.editor.subscribe("revertEvent", this._onEditorRevertEvent, this, true);
    oColumn.editor.subscribe("saveEvent", this._onEditorSaveEvent, this, true);
    oColumn.editor.subscribe("cancelEvent", this._onEditorCancelEvent, this, true);
    oColumn.editor.subscribe("blurEvent", this._onEditorBlurEvent, this, true);
    oColumn.editor.subscribe("blockEvent", this._onEditorBlockEvent, this, true);
    oColumn.editor.subscribe("unblockEvent", this._onEditorUnblockEvent, this, true);
},

/**
 * Returns current CellEditor instance, or null.
 * @method getCellEditor
 * @return {YAHOO.widget.CellEditor} CellEditor instance.
 */
getCellEditor : function() {
    return this._oCellEditor;
},


/**
 * Activates and shows CellEditor instance for the given cell while deactivating and
 * canceling previous CellEditor. It is baked into DataTable that only one CellEditor
 * can be active at any given time. 
 *
 * @method showCellEditor
 * @param elCell {HTMLElement | String} Cell to edit.
 */
showCellEditor : function(elCell, oRecord, oColumn) {
    // Get a particular CellEditor
    elCell = this.getTdEl(elCell);
    if(elCell) {
        oColumn = this.getColumn(elCell);
        if(oColumn && oColumn.editor) {
            var oCellEditor = this._oCellEditor;
            // Clean up active CellEditor
            if(oCellEditor) {
                if(this._oCellEditor.cancel) {
                    this._oCellEditor.cancel();
                }
                else if(oCellEditor.isActive) {
                    this.cancelCellEditor();
                }
            }
            
            if(oColumn.editor instanceof YAHOO.widget.BaseCellEditor) {
                // Get CellEditor
                oCellEditor = oColumn.editor;
                var ok = oCellEditor.attach(this, elCell);
                if(ok) {
                    oCellEditor.render();
                    oCellEditor.move();
                    ok = this.doBeforeShowCellEditor(oCellEditor);
                    if(ok) {
                        oCellEditor.show();
                        this._oCellEditor = oCellEditor;
                    }
                }
            }
            // Backward compatibility
            else {
                    if(!oRecord || !(oRecord instanceof YAHOO.widget.Record)) {
                        oRecord = this.getRecord(elCell);
                    }
                    if(!oColumn || !(oColumn instanceof YAHOO.widget.Column)) {
                        oColumn = this.getColumn(elCell);
                    }
                    if(oRecord && oColumn) {
                        if(!this._oCellEditor || this._oCellEditor.container) {
                            this._initCellEditorEl();
                        }
                        
                        // Update Editor values
                        oCellEditor = this._oCellEditor;
                        oCellEditor.cell = elCell;
                        oCellEditor.record = oRecord;
                        oCellEditor.column = oColumn;
                        oCellEditor.validator = (oColumn.editorOptions &&
                                lang.isFunction(oColumn.editorOptions.validator)) ?
                                oColumn.editorOptions.validator : null;
                        oCellEditor.value = oRecord.getData(oColumn.key);
                        oCellEditor.defaultValue = null;
            
                        // Move Editor
                        var elContainer = oCellEditor.container;
                        var x = Dom.getX(elCell);
                        var y = Dom.getY(elCell);
            
                        // SF doesn't get xy for cells in scrolling table
                        // when tbody display is set to block
                        if(isNaN(x) || isNaN(y)) {
                            x = elCell.offsetLeft + // cell pos relative to table
                                    Dom.getX(this._elTbody.parentNode) - // plus table pos relative to document
                                    this._elTbody.scrollLeft; // minus tbody scroll
                            y = elCell.offsetTop + // cell pos relative to table
                                    Dom.getY(this._elTbody.parentNode) - // plus table pos relative to document
                                    this._elTbody.scrollTop + // minus tbody scroll
                                    this._elThead.offsetHeight; // account for fixed THEAD cells
                        }
            
                        elContainer.style.left = x + "px";
                        elContainer.style.top = y + "px";
            
                        // Hook to customize the UI
                        this.doBeforeShowCellEditor(this._oCellEditor);
            
                        //TODO: This is temporarily up here due so elements can be focused
                        // Show Editor
                        elContainer.style.display = "";
            
                        // Handle ESC key
                        Ev.addListener(elContainer, "keydown", function(e, oSelf) {
                            // ESC hides Cell Editor
                            if((e.keyCode == 27)) {
                                oSelf.cancelCellEditor();
                                oSelf.focusTbodyEl();
                            }
                            else {
                                oSelf.fireEvent("editorKeydownEvent", {editor:oSelf._oCellEditor, event:e});
                            }
                        }, this);
            
                        // Render Editor markup
                        var fnEditor;
                        if(lang.isString(oColumn.editor)) {
                            switch(oColumn.editor) {
                                case "checkbox":
                                    fnEditor = DT.editCheckbox;
                                    break;
                                case "date":
                                    fnEditor = DT.editDate;
                                    break;
                                case "dropdown":
                                    fnEditor = DT.editDropdown;
                                    break;
                                case "radio":
                                    fnEditor = DT.editRadio;
                                    break;
                                case "textarea":
                                    fnEditor = DT.editTextarea;
                                    break;
                                case "textbox":
                                    fnEditor = DT.editTextbox;
                                    break;
                                default:
                                    fnEditor = null;
                            }
                        }
                        else if(lang.isFunction(oColumn.editor)) {
                            fnEditor = oColumn.editor;
                        }
            
                        if(fnEditor) {
                            // Create DOM input elements
                            fnEditor(this._oCellEditor, this);
            
                            // Show Save/Cancel buttons
                            if(!oColumn.editorOptions || !oColumn.editorOptions.disableBtns) {
                                this.showCellEditorBtns(elContainer);
                            }
            
                            oCellEditor.isActive = true;
            
                            //TODO: verify which args to pass
                            this.fireEvent("editorShowEvent", {editor:oCellEditor});
                            YAHOO.log("Cell Editor shown for " + elCell, "info", this.toString());
                            return;
                        }
                    }



            
            }
        }
    }
},

/**
 * Backward compatibility.
 *
 * @method _initCellEditorEl
 * @private
 * @deprecated Use BaseCellEditor class.
 */
_initCellEditorEl : function() {
    // Attach Cell Editor container element as first child of body
    var elCellEditor = document.createElement("div");
    elCellEditor.id = this._sId + "-celleditor";
    elCellEditor.style.display = "none";
    elCellEditor.tabIndex = 0;
    Dom.addClass(elCellEditor, DT.CLASS_EDITOR);
    var elFirstChild = Dom.getFirstChild(document.body);
    if(elFirstChild) {
        elCellEditor = Dom.insertBefore(elCellEditor, elFirstChild);
    }
    else {
        elCellEditor = document.body.appendChild(elCellEditor);
    }
    
    // Internal tracker of Cell Editor values
    var oCellEditor = {};
    oCellEditor.container = elCellEditor;
    oCellEditor.value = null;
    oCellEditor.isActive = false;
    this._oCellEditor = oCellEditor;
},

/**
 * Overridable abstract method to customize CellEditor before showing.
 *
 * @method doBeforeShowCellEditor
 * @param oCellEditor {YAHOO.widget.CellEditor} The CellEditor instance.
 * @return {Boolean} Return true to continue showing CellEditor.
 */
doBeforeShowCellEditor : function(oCellEditor) {
    return true;
},

/**
 * Saves active CellEditor input to Record and upates DOM UI.
 *
 * @method saveCellEditor
 */
saveCellEditor : function() {
    if(this._oCellEditor) {
        if(this._oCellEditor.save) {
            this._oCellEditor.save();
        }
        // Backward compatibility
        else if(this._oCellEditor.isActive) {
            var newData = this._oCellEditor.value;
            // Copy the data to pass to the event
            //var oldData = YAHOO.widget.DataTable._cloneObject(this._oCellEditor.record.getData(this._oCellEditor.column.key));
            var oldData = this._oCellEditor.record.getData(this._oCellEditor.column.key);
    
            // Validate input data
            if(this._oCellEditor.validator) {
                newData = this._oCellEditor.value = this._oCellEditor.validator.call(this, newData, oldData, this._oCellEditor);
                if(newData === null ) {
                    this.resetCellEditor();
                    this.fireEvent("editorRevertEvent",
                            {editor:this._oCellEditor, oldData:oldData, newData:newData});
                    YAHOO.log("Could not save Cell Editor input due to invalid data " +
                            lang.dump(newData), "warn", this.toString());
                    return;
                }
            }
            // Update the Record
            this._oRecordSet.updateRecordValue(this._oCellEditor.record, this._oCellEditor.column.key, this._oCellEditor.value);
            // Update the UI
            this.formatCell(this._oCellEditor.cell.firstChild, this._oCellEditor.record, this._oCellEditor.column);
            
            // Bug fix 1764044
            this._oChainRender.add({
                method: function() {
                    this.validateColumnWidths();
                },
                scope: this
            });
            this._oChainRender.run();
            // Clear out the Cell Editor
            this.resetCellEditor();
    
            this.fireEvent("editorSaveEvent",
                    {editor:this._oCellEditor, oldData:oldData, newData:newData});
            YAHOO.log("Cell Editor input saved", "info", this.toString());
        }
    }   
},

/**
 * Cancels active CellEditor.
 *
 * @method cancelCellEditor
 */
cancelCellEditor : function() {
    if(this._oCellEditor) {
        if(this._oCellEditor.cancel) {
            this._oCellEditor.cancel();
        }
        // Backward compatibility
        else if(this._oCellEditor.isActive) {
            this.resetCellEditor();
            //TODO: preserve values for the event?
            this.fireEvent("editorCancelEvent", {editor:this._oCellEditor});
            YAHOO.log("Cell Editor input canceled", "info", this.toString());
        }

        YAHOO.log("CellEditor input canceled", "info", this.toString());
    }
},

/**
 * Destroys active CellEditor instance and UI.
 *
 * @method destroyCellEditor
 */
destroyCellEditor : function() {
    if(this._oCellEditor) {
        this._oCellEditor.destroy();
        this._oCellEditor = null;
    }   
},

/**
 * Passes through showEvent of the active CellEditor.
 *
 * @method _onEditorShowEvent
 * @param oArgs {Object}  Custom Event args.
 * @private 
 */
_onEditorShowEvent : function(oArgs) {
    this.fireEvent("editorShowEvent", oArgs);
},

/**
 * Passes through keydownEvent of the active CellEditor.
 * @param oArgs {Object}  Custom Event args. 
 *
 * @method _onEditorKeydownEvent
 * @private 
 */
_onEditorKeydownEvent : function(oArgs) {
    this.fireEvent("editorKeydownEvent", oArgs);
},

/**
 * Passes through revertEvent of the active CellEditor.
 *
 * @method _onEditorRevertEvent
 * @param oArgs {Object}  Custom Event args. 
 * @private  
 */
_onEditorRevertEvent : function(oArgs) {
    this.fireEvent("editorRevertEvent", oArgs);
},

/**
 * Passes through saveEvent of the active CellEditor.
 *
 * @method _onEditorSaveEvent
 * @param oArgs {Object}  Custom Event args.  
 * @private 
 */
_onEditorSaveEvent : function(oArgs) {
    this.fireEvent("editorSaveEvent", oArgs);
},

/**
 * Passes through cancelEvent of the active CellEditor.
 *
 * @method _onEditorCancelEvent
 * @param oArgs {Object}  Custom Event args.
 * @private   
 */
_onEditorCancelEvent : function(oArgs) {
    this.fireEvent("editorCancelEvent", oArgs);
},

/**
 * Passes through blurEvent of the active CellEditor.
 *
 * @method _onEditorBlurEvent
 * @param oArgs {Object}  Custom Event args. 
 * @private  
 */
_onEditorBlurEvent : function(oArgs) {
    this.fireEvent("editorBlurEvent", oArgs);
},

/**
 * Passes through blockEvent of the active CellEditor.
 *
 * @method _onEditorBlockEvent
 * @param oArgs {Object}  Custom Event args. 
 * @private  
 */
_onEditorBlockEvent : function(oArgs) {
    this.fireEvent("editorBlockEvent", oArgs);
},

/**
 * Passes through unblockEvent of the active CellEditor.
 *
 * @method _onEditorUnblockEvent
 * @param oArgs {Object}  Custom Event args. 
 * @private  
 */
_onEditorUnblockEvent : function(oArgs) {
    this.fireEvent("editorUnblockEvent", oArgs);
},

/**
 * Public handler of the editorBlurEvent. By default, saves on blur if
 * disableBtns is true, otherwise cancels on blur. 
 *
 * @method onEditorBlurEvent
 * @param oArgs {Object}  Custom Event args.  
 */
onEditorBlurEvent : function(oArgs) {
    if(oArgs.editor.disableBtns) {
        // Save on blur
        if(oArgs.editor.save) { // Backward incompatible
            oArgs.editor.save();
        }
    }      
    else if(oArgs.editor.cancel) { // Backward incompatible
        // Cancel on blur
        oArgs.editor.cancel();
    }      
},

/**
 * Public handler of the editorBlockEvent. By default, disables DataTable UI.
 *
 * @method onEditorBlockEvent
 * @param oArgs {Object}  Custom Event args.  
 */
onEditorBlockEvent : function(oArgs) {
    this.disable();
},

/**
 * Public handler of the editorUnblockEvent. By default, undisables DataTable UI.
 *
 * @method onEditorUnblockEvent
 * @param oArgs {Object}  Custom Event args.  
 */
onEditorUnblockEvent : function(oArgs) {
    this.undisable();
},






































// ABSTRACT METHODS

/**
 * Overridable method gives implementers a hook to access data before
 * it gets added to RecordSet and rendered to the TBODY.
 *
 * @method doBeforeLoadData
 * @param sRequest {String} Original request.
 * @param oResponse {Object} <a href="http://developer.yahoo.com/yui/datasource/#ds_oParsedResponse">Response object</a>.
 * @param oPayload {MIXED} additional arguments
 * @return {Boolean} Return true to continue loading data into RecordSet and
 * updating DataTable with new Records, false to cancel.
 */
doBeforeLoadData : function(sRequest, oResponse, oPayload) {
    return true;
},































































/////////////////////////////////////////////////////////////////////////////
//
// Public Custom Event Handlers
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Custom event handler to sort Column.
 *
 * @method onEventSortColumn
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventSortColumn : function(oArgs) {
//TODO: support form elements in sortable columns
    var evt = oArgs.event;
    var target = oArgs.target;

    var el = this.getThEl(target) || this.getTdEl(target);
    if(el) {
        var oColumn = this.getColumn(el);
        if(oColumn.sortable) {
            Ev.stopEvent(evt);
            this.sortColumn(oColumn);
        }
    }
    else {
        YAHOO.log("Could not find Column for " + target, "warn", this.toString());
    }
},

/**
 * Custom event handler to select Column.
 *
 * @method onEventSelectColumn
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventSelectColumn : function(oArgs) {
    this.selectColumn(oArgs.target);
},

/**
 * Custom event handler to highlight Column. Accounts for spurious
 * caused-by-child events. 
 *
 * @method onEventHighlightColumn
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventHighlightColumn : function(oArgs) {
    this.highlightColumn(oArgs.target);
},

/**
 * Custom event handler to unhighlight Column. Accounts for spurious
 * caused-by-child events. 
 *
 * @method onEventUnhighlightColumn
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventUnhighlightColumn : function(oArgs) {
    this.unhighlightColumn(oArgs.target);
},

/**
 * Custom event handler to manage selection according to desktop paradigm.
 *
 * @method onEventSelectRow
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventSelectRow : function(oArgs) {
    var sMode = this.get("selectionMode");
    if(sMode == "single") {
        this._handleSingleSelectionByMouse(oArgs);
    }
    else {
        this._handleStandardSelectionByMouse(oArgs);
    }
},

/**
 * Custom event handler to select cell.
 *
 * @method onEventSelectCell
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventSelectCell : function(oArgs) {
    var sMode = this.get("selectionMode");
    if(sMode == "cellblock") {
        this._handleCellBlockSelectionByMouse(oArgs);
    }
    else if(sMode == "cellrange") {
        this._handleCellRangeSelectionByMouse(oArgs);
    }
    else {
        this._handleSingleCellSelectionByMouse(oArgs);
    }
},

/**
 * Custom event handler to highlight row. Accounts for spurious
 * caused-by-child events. 
 *
 * @method onEventHighlightRow
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventHighlightRow : function(oArgs) {
    this.highlightRow(oArgs.target);
},

/**
 * Custom event handler to unhighlight row. Accounts for spurious
 * caused-by-child events. 
 *
 * @method onEventUnhighlightRow
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventUnhighlightRow : function(oArgs) {
    this.unhighlightRow(oArgs.target);
},

/**
 * Custom event handler to highlight cell. Accounts for spurious
 * caused-by-child events. 
 *
 * @method onEventHighlightCell
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventHighlightCell : function(oArgs) {
    this.highlightCell(oArgs.target);
},

/**
 * Custom event handler to unhighlight cell. Accounts for spurious
 * caused-by-child events. 
 *
 * @method onEventUnhighlightCell
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventUnhighlightCell : function(oArgs) {
    this.unhighlightCell(oArgs.target);
},

/**
 * Custom event handler to format cell.
 *
 * @method onEventFormatCell
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventFormatCell : function(oArgs) {
    var target = oArgs.target;

    var elCell = this.getTdEl(target);
    if(elCell) {
        var oColumn = this.getColumn(this.getCellIndex(elCell));
        this.formatCell(elCell.firstChild, this.getRecord(elCell), oColumn);
    }
    else {
        YAHOO.log("Could not format cell " + target, "warn", this.toString());
    }
},

/**
 * Custom event handler to edit cell.
 *
 * @method onEventShowCellEditor
 * @param oArgs.event {HTMLEvent} Event object.
 * @param oArgs.target {HTMLElement} Target element.
 */
onEventShowCellEditor : function(oArgs) {
    if(!this.isDisabled()) {
        this.showCellEditor(oArgs.target);
    }
},

/**
 * Custom event handler to save active CellEditor input.
 *
 * @method onEventSaveCellEditor
 */
onEventSaveCellEditor : function(oArgs) {
    if(this._oCellEditor) {
        if(this._oCellEditor.save) {
            this._oCellEditor.save();
        }
        // Backward compatibility
        else {
            this.saveCellEditor();
        }
    }
},

/**
 * Custom event handler to cancel active CellEditor.
 *
 * @method onEventCancelCellEditor
 */
onEventCancelCellEditor : function(oArgs) {
    if(this._oCellEditor) {
        if(this._oCellEditor.cancel) {
            this._oCellEditor.cancel();
        }
        // Backward compatibility
        else {
            this.cancelCellEditor();
        }
    }
},

/**
 * Callback function receives data from DataSource and populates an entire
 * DataTable with Records and TR elements, clearing previous Records, if any.
 *
 * @method onDataReturnInitializeTable
 * @param sRequest {String} Original request.
 * @param oResponse {Object} <a href="http://developer.yahoo.com/yui/datasource/#ds_oParsedResponse">Response object</a>.
 * @param oPayload {MIXED} (optional) Additional argument(s)
 */
onDataReturnInitializeTable : function(sRequest, oResponse, oPayload) {
    if((this instanceof DT) && this._sId) {
        this.initializeTable();
    
        this.onDataReturnSetRows(sRequest,oResponse,oPayload);
    }
},

/**
 * Callback function receives reponse from DataSource, replaces all existing
 * Records in  RecordSet, updates TR elements with new data, and updates state
 * UI for pagination and sorting from payload data, if necessary. 
 *  
 * @method onDataReturnReplaceRows
 * @param oRequest {MIXED} Original generated request.
 * @param oResponse {Object} <a href="http://developer.yahoo.com/yui/datasource/#ds_oParsedResponse">Response object</a>.
 * @param oPayload {MIXED} (optional) Additional argument(s)
 */
onDataReturnReplaceRows : function(oRequest, oResponse, oPayload) {
    if((this instanceof DT) && this._sId) {
        this.fireEvent("dataReturnEvent", {request:oRequest,response:oResponse,payload:oPayload});
    
        // Pass data through abstract method for any transformations
        var ok    = this.doBeforeLoadData(oRequest, oResponse, oPayload),
            pag   = this.get('paginator'),
            index = 0;
    
        // Data ok to set
        if(ok && oResponse && !oResponse.error && lang.isArray(oResponse.results)) {
            // Update Records
            this._oRecordSet.reset();
    
            if (this.get('dynamicData')) {
                if (oPayload && oPayload.pagination &&
                    lang.isNumber(oPayload.pagination.recordOffset)) {
                    index = oPayload.pagination.recordOffset;
                } else if (pag) {
                    index = pag.getStartIndex();
                }
            }
    
            this._oRecordSet.setRecords(oResponse.results, index | 0);
            
            // Update state
            this._handleDataReturnPayload(oRequest, oResponse, oPayload);
            
            // Update UI
            this.render();    
        }
        // Error
        else if(ok && oResponse.error) {
            this.showTableMessage(this.get("MSG_ERROR"), DT.CLASS_ERROR);
        }
    }
},

/**
 * Callback function receives data from DataSource and appends to an existing
 * DataTable new Records and, if applicable, creates or updates
 * corresponding TR elements.
 *
 * @method onDataReturnAppendRows
 * @param sRequest {String} Original request.
 * @param oResponse {Object} <a href="http://developer.yahoo.com/yui/datasource/#ds_oParsedResponse">Response object</a>.
 * @param oPayload {MIXED} (optional) Additional argument(s)
 */
onDataReturnAppendRows : function(sRequest, oResponse, oPayload) {
    if((this instanceof DT) && this._sId) {
        this.fireEvent("dataReturnEvent", {request:sRequest,response:oResponse,payload:oPayload});
    
        // Pass data through abstract method for any transformations
        var ok = this.doBeforeLoadData(sRequest, oResponse, oPayload);
    
        // Data ok to append
        if(ok && oResponse && !oResponse.error && lang.isArray(oResponse.results)) {        
            // Append rows
            this.addRows(oResponse.results);
    
            // Update state
            this._handleDataReturnPayload(sRequest, oResponse, oPayload);
        }
        // Error
        else if(ok && oResponse.error) {
            this.showTableMessage(this.get("MSG_ERROR"), DT.CLASS_ERROR);
        }
    }
},

/**
 * Callback function receives data from DataSource and inserts new records
 * starting at the index specified in oPayload.insertIndex. The value for
 * oPayload.insertIndex can be populated when sending the request to the DataSource,
 * or by accessing oPayload.insertIndex with the doBeforeLoadData() method at runtime.
 * If applicable, creates or updates corresponding TR elements.
 *
 * @method onDataReturnInsertRows
 * @param sRequest {String} Original request.
 * @param oResponse {Object} <a href="http://developer.yahoo.com/yui/datasource/#ds_oParsedResponse">Response object</a>.
 * @param oPayload {MIXED} Argument payload, looks in oPayload.insertIndex.
 */
onDataReturnInsertRows : function(sRequest, oResponse, oPayload) {
    if((this instanceof DT) && this._sId) {
        this.fireEvent("dataReturnEvent", {request:sRequest,response:oResponse,payload:oPayload});
    
        // Pass data through abstract method for any transformations
        var ok = this.doBeforeLoadData(sRequest, oResponse, oPayload);
    
        // Data ok to append
        if(ok && oResponse && !oResponse.error && lang.isArray(oResponse.results)) {
            // Insert rows
            this.addRows(oResponse.results, (oPayload ? oPayload.insertIndex : 0));
    
            // Update state
            this._handleDataReturnPayload(sRequest, oResponse, oPayload);
        }
        // Error
        else if(ok && oResponse.error) {
            this.showTableMessage(this.get("MSG_ERROR"), DT.CLASS_ERROR);
        }
    }
},

/**
 * Callback function receives data from DataSource and incrementally updates Records
 * starting at the index specified in oPayload.updateIndex. The value for
 * oPayload.updateIndex can be populated when sending the request to the DataSource,
 * or by accessing oPayload.updateIndex with the doBeforeLoadData() method at runtime.
 * If applicable, creates or updates corresponding TR elements.
 *
 * @method onDataReturnUpdateRows
 * @param sRequest {String} Original request.
 * @param oResponse {Object} <a href="http://developer.yahoo.com/yui/datasource/#ds_oParsedResponse">Response object</a>.
 * @param oPayload {MIXED} Argument payload, looks in oPayload.updateIndex.
 */
onDataReturnUpdateRows : function(sRequest, oResponse, oPayload) {
    if((this instanceof DT) && this._sId) {
        this.fireEvent("dataReturnEvent", {request:sRequest,response:oResponse,payload:oPayload});
    
        // Pass data through abstract method for any transformations
        var ok = this.doBeforeLoadData(sRequest, oResponse, oPayload);
    
        // Data ok to append
        if(ok && oResponse && !oResponse.error && lang.isArray(oResponse.results)) {
            // Insert rows
            this.updateRows((oPayload ? oPayload.updateIndex : 0), oResponse.results);
    
            // Update state
            this._handleDataReturnPayload(sRequest, oResponse, oPayload);
        }
        // Error
        else if(ok && oResponse.error) {
            this.showTableMessage(this.get("MSG_ERROR"), DT.CLASS_ERROR);
        }
    }
},

/**
 * Callback function receives reponse from DataSource and populates the
 * RecordSet with the results.
 *  
 * @method onDataReturnSetRows
 * @param oRequest {MIXED} Original generated request.
 * @param oResponse {Object} <a href="http://developer.yahoo.com/yui/datasource/#ds_oParsedResponse">Response object</a>.
 * @param oPayload {MIXED} (optional) Additional argument(s)
 */
onDataReturnSetRows : function(oRequest, oResponse, oPayload) {
    if((this instanceof DT) && this._sId) {
        this.fireEvent("dataReturnEvent", {request:oRequest,response:oResponse,payload:oPayload});
    
        // Pass data through abstract method for any transformations
        var ok    = this.doBeforeLoadData(oRequest, oResponse, oPayload),
            pag   = this.get('paginator'),
            index = 0;
    
        // Data ok to set
        if(ok && oResponse && !oResponse.error && lang.isArray(oResponse.results)) {
            // Update Records
            if (this.get('dynamicData')) {
                if (oPayload && oPayload.pagination &&
                    lang.isNumber(oPayload.pagination.recordOffset)) {
                    index = oPayload.pagination.recordOffset;
                } else if (pag) {
                    index = pag.getStartIndex();
                }
                
                this._oRecordSet.reset(); // Bug 2290604: dyanmic data shouldn't keep accumulating by default
            }
    
            this._oRecordSet.setRecords(oResponse.results, index | 0);
    
            // Update state
            this._handleDataReturnPayload(oRequest, oResponse, oPayload);
            
            // Update UI
            this.render();
        }
        // Error
        else if(ok && oResponse.error) {
            this.showTableMessage(this.get("MSG_ERROR"), DT.CLASS_ERROR);
        }
    }
    else {
        YAHOO.log("Instance destroyed before data returned.","info",this.toString());
    }
},

/**
 * Hook to update oPayload before consumption.
 *  
 * @method handleDataReturnPayload
 * @param oRequest {MIXED} Original generated request.
 * @param oResponse {Object} <a href="http://developer.yahoo.com/yui/datasource/#ds_oParsedResponse">Response object</a>.
 * @param oPayload {MIXED} State values.
 * @return oPayload {MIXED} State values.
 */
handleDataReturnPayload : function (oRequest, oResponse, oPayload) {
    return oPayload || {};
},

/**
 * Updates the DataTable with state data sent in an onDataReturn* payload.
 *  
 * @method _handleDataReturnPayload
 * @param oRequest {MIXED} Original generated request.
 * @param oResponse {Object} <a href="http://developer.yahoo.com/yui/datasource/#ds_oParsedResponse">Response object</a>.
 * @param oPayload {MIXED} State values
 * @private
 */
_handleDataReturnPayload : function (oRequest, oResponse, oPayload) {
    oPayload = this.handleDataReturnPayload(oRequest, oResponse, oPayload);
    if(oPayload) {
        // Update pagination
        var oPaginator = this.get('paginator');
        if (oPaginator) {
            // Update totalRecords
            if(this.get("dynamicData")) {
                if (widget.Paginator.isNumeric(oPayload.totalRecords)) {
                    oPaginator.set('totalRecords',oPayload.totalRecords);
                }
            }
            else {
                oPaginator.set('totalRecords',this._oRecordSet.getLength());
            }
            // Update other paginator values
            if (lang.isObject(oPayload.pagination)) {
                oPaginator.set('rowsPerPage',oPayload.pagination.rowsPerPage);
                oPaginator.set('recordOffset',oPayload.pagination.recordOffset);
            }
        }

        // Update sorting
        if (oPayload.sortedBy) {
            // Set the sorting values in preparation for refresh
            this.set('sortedBy', oPayload.sortedBy);
        }
        // Backwards compatibility for sorting
        else if (oPayload.sorting) {
            // Set the sorting values in preparation for refresh
            this.set('sortedBy', oPayload.sorting);
        }
    }
},

































    /////////////////////////////////////////////////////////////////////////////
    //
    // Custom Events
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Fired when the DataTable's rows are rendered from an initialized state.
     *
     * @event initEvent
     */

    /**
     * Fired before the DataTable's DOM is rendered or modified.
     *
     * @event beforeRenderEvent
     */

    /**
     * Fired when the DataTable's DOM is rendered or modified.
     *
     * @event renderEvent
     */

    /**
     * Fired when the DataTable's post-render routine is complete, including
     * Column width validations.
     *
     * @event postRenderEvent
     */

    /**
     * Fired when the DataTable is disabled.
     *
     * @event disableEvent
     */

    /**
     * Fired when the DataTable is undisabled.
     *
     * @event undisableEvent
     */

    /**
     * Fired when data is returned from DataSource but before it is consumed by
     * DataTable.
     *
     * @event dataReturnEvent
     * @param oArgs.request {String} Original request.
     * @param oArgs.response {Object} Response object.
     */

    /**
     * Fired when the DataTable has a focus event.
     *
     * @event tableFocusEvent
     */

    /**
     * Fired when the DataTable THEAD element has a focus event.
     *
     * @event theadFocusEvent
     */

    /**
     * Fired when the DataTable TBODY element has a focus event.
     *
     * @event tbodyFocusEvent
     */

    /**
     * Fired when the DataTable has a blur event.
     *
     * @event tableBlurEvent
     */

    /*TODO implement theadBlurEvent
     * Fired when the DataTable THEAD element has a blur event.
     *
     * @event theadBlurEvent
     */

    /*TODO: implement tbodyBlurEvent
     * Fired when the DataTable TBODY element has a blur event.
     *
     * @event tbodyBlurEvent
     */

    /**
     * Fired when the DataTable has a key event.
     *
     * @event tableKeyEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's TABLE element.
     */

    /**
     * Fired when the DataTable THEAD element has a key event.
     *
     * @event theadKeyEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's TABLE element.
     */

    /**
     * Fired when the DataTable TBODY element has a key event.
     *
     * @event tbodyKeyEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's TABLE element.
     */

    /**
     * Fired when the DataTable has a mouseover.
     *
     * @event tableMouseoverEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's TABLE element.
     *
     */

    /**
     * Fired when the DataTable has a mouseout.
     *
     * @event tableMouseoutEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's TABLE element.
     *
     */

    /**
     * Fired when the DataTable has a mousedown.
     *
     * @event tableMousedownEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's TABLE element.
     *
     */

    /**
     * Fired when the DataTable has a mouseup.
     *
     * @event tableMouseupEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's TABLE element.
     *
     */

    /**
     * Fired when the DataTable has a click.
     *
     * @event tableClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's TABLE element.
     *
     */

    /**
     * Fired when the DataTable has a dblclick.
     *
     * @event tableDblclickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's TABLE element.
     *
     */

    /**
     * Fired when a message is shown in the DataTable's message element.
     *
     * @event tableMsgShowEvent
     * @param oArgs.html {HTML} The HTML displayed.
     * @param oArgs.className {String} The className assigned.
     *
     */

    /**
     * Fired when the DataTable's message element is hidden.
     *
     * @event tableMsgHideEvent
     */

    /**
     * Fired when a THEAD row has a mouseover.
     *
     * @event theadRowMouseoverEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a THEAD row has a mouseout.
     *
     * @event theadRowMouseoutEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a THEAD row has a mousedown.
     *
     * @event theadRowMousedownEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a THEAD row has a mouseup.
     *
     * @event theadRowMouseupEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a THEAD row has a click.
     *
     * @event theadRowClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a THEAD row has a dblclick.
     *
     * @event theadRowDblclickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a THEAD cell has a mouseover.
     *
     * @event theadCellMouseoverEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TH element.
     *
     */

    /**
     * Fired when a THEAD cell has a mouseout.
     *
     * @event theadCellMouseoutEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TH element.
     *
     */

    /**
     * Fired when a THEAD cell has a mousedown.
     *
     * @event theadCellMousedownEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TH element.
     */

    /**
     * Fired when a THEAD cell has a mouseup.
     *
     * @event theadCellMouseupEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TH element.
     */

    /**
     * Fired when a THEAD cell has a click.
     *
     * @event theadCellClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TH element.
     */

    /**
     * Fired when a THEAD cell has a dblclick.
     *
     * @event theadCellDblclickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TH element.
     */

    /**
     * Fired when a THEAD label has a mouseover.
     *
     * @event theadLabelMouseoverEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The SPAN element.
     *
     */

    /**
     * Fired when a THEAD label has a mouseout.
     *
     * @event theadLabelMouseoutEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The SPAN element.
     *
     */

    /**
     * Fired when a THEAD label has a mousedown.
     *
     * @event theadLabelMousedownEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The SPAN element.
     */

    /**
     * Fired when a THEAD label has a mouseup.
     *
     * @event theadLabelMouseupEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The SPAN element.
     */

    /**
     * Fired when a THEAD label has a click.
     *
     * @event theadLabelClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The SPAN element.
     */

    /**
     * Fired when a THEAD label has a dblclick.
     *
     * @event theadLabelDblclickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The SPAN element.
     */

    /**
     * Fired when a column is sorted.
     *
     * @event columnSortEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     * @param oArgs.dir {String} Sort direction: YAHOO.widget.DataTable.CLASS_ASC
     * or YAHOO.widget.DataTable.CLASS_DESC.
     */

    /**
     * Fired when a column width is set.
     *
     * @event columnSetWidthEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     * @param oArgs.width {Number} The width in pixels.
     */

    /**
     * Fired when a column width is unset.
     *
     * @event columnUnsetWidthEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     */

    /**
     * Fired when a column is drag-resized.
     *
     * @event columnResizeEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     * @param oArgs.target {HTMLElement} The TH element.
     * @param oArgs.width {Number} Width in pixels.     
     */

    /**
     * Fired when a Column is moved to a new index.
     *
     * @event columnReorderEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     * @param oArgs.oldIndex {Number} The previous tree index position.
     */

    /**
     * Fired when a column is hidden.
     *
     * @event columnHideEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     */

    /**
     * Fired when a column is shown.
     *
     * @event columnShowEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     */

    /**
     * Fired when a column is selected.
     *
     * @event columnSelectEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     */

    /**
     * Fired when a column is unselected.
     *
     * @event columnUnselectEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     */
    /**
     * Fired when a column is removed.
     *
     * @event columnRemoveEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     */

    /**
     * Fired when a column is inserted.
     *
     * @event columnInsertEvent
     * @param oArgs.column {YAHOO.widget.Column} The Column instance.
     * @param oArgs.index {Number} The index position.
     */

    /**
     * Fired when a column is highlighted.
     *
     * @event columnHighlightEvent
     * @param oArgs.column {YAHOO.widget.Column} The highlighted Column.
     */

    /**
     * Fired when a column is unhighlighted.
     *
     * @event columnUnhighlightEvent
     * @param oArgs.column {YAHOO.widget.Column} The unhighlighted Column.
     */


    /**
     * Fired when a row has a mouseover.
     *
     * @event rowMouseoverEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a row has a mouseout.
     *
     * @event rowMouseoutEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a row has a mousedown.
     *
     * @event rowMousedownEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a row has a mouseup.
     *
     * @event rowMouseupEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a row has a click.
     *
     * @event rowClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a row has a dblclick.
     *
     * @event rowDblclickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TR element.
     */

    /**
     * Fired when a row is added.
     *
     * @event rowAddEvent
     * @param oArgs.record {YAHOO.widget.Record} The added Record.
     */
     
    /**
     * Fired when rows are added.
     *
     * @event rowsAddEvent
     * @param oArgs.record {YAHOO.widget.Record[]} The added Records.
     */

    /**
     * Fired when a row is updated.
     *
     * @event rowUpdateEvent
     * @param oArgs.record {YAHOO.widget.Record} The updated Record.
     * @param oArgs.oldData {Object} Object literal of the old data.
     */

    /**
     * Fired when a row is deleted.
     *
     * @event rowDeleteEvent
     * @param oArgs.oldData {Object} Object literal of the deleted data.
     * @param oArgs.recordIndex {Number} Index of the deleted Record.
     * @param oArgs.trElIndex {Number} Index of the deleted TR element, if on current page.
     */
     
    /**
     * Fired when rows are deleted.
     *
     * @event rowsDeleteEvent
     * @param oArgs.oldData {Object[]} Array of object literals of the deleted data.
     * @param oArgs.recordIndex {Number} Index of the first deleted Record.
     * @param oArgs.count {Number} Number of deleted Records.
     */

    /**
     * Fired when a row is selected.
     *
     * @event rowSelectEvent
     * @param oArgs.el {HTMLElement} The selected TR element, if applicable.
     * @param oArgs.record {YAHOO.widget.Record} The selected Record.
     */

    /**
     * Fired when a row is unselected.
     *
     * @event rowUnselectEvent
     * @param oArgs.el {HTMLElement} The unselected TR element, if applicable.
     * @param oArgs.record {YAHOO.widget.Record} The unselected Record.
     */

    /**
     * Fired when all row selections are cleared.
     *
     * @event unselectAllRowsEvent
     */

    /**
     * Fired when a row is highlighted.
     *
     * @event rowHighlightEvent
     * @param oArgs.el {HTMLElement} The highlighted TR element.
     * @param oArgs.record {YAHOO.widget.Record} The highlighted Record.
     */

    /**
     * Fired when a row is unhighlighted.
     *
     * @event rowUnhighlightEvent
     * @param oArgs.el {HTMLElement} The highlighted TR element.
     * @param oArgs.record {YAHOO.widget.Record} The highlighted Record.
     */

    /**
     * Fired when a cell is updated.
     *
     * @event cellUpdateEvent
     * @param oArgs.record {YAHOO.widget.Record} The updated Record.
     * @param oArgs.column {YAHOO.widget.Column} The updated Column.
     * @param oArgs.oldData {Object} Original data value of the updated cell.
     */

    /**
     * Fired when a cell has a mouseover.
     *
     * @event cellMouseoverEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TD element.
     */

    /**
     * Fired when a cell has a mouseout.
     *
     * @event cellMouseoutEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TD element.
     */

    /**
     * Fired when a cell has a mousedown.
     *
     * @event cellMousedownEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TD element.
     */

    /**
     * Fired when a cell has a mouseup.
     *
     * @event cellMouseupEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TD element.
     */

    /**
     * Fired when a cell has a click.
     *
     * @event cellClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TD element.
     */

    /**
     * Fired when a cell has a dblclick.
     *
     * @event cellDblclickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The TD element.
     */

    /**
     * Fired when a cell is formatted.
     *
     * @event cellFormatEvent
     * @param oArgs.el {HTMLElement} The formatted TD element.
     * @param oArgs.record {YAHOO.widget.Record} The associated Record instance.
     * @param oArgs.column {YAHOO.widget.Column} The associated Column instance.
     * @param oArgs.key {String} (deprecated) The key of the formatted cell.
     */

    /**
     * Fired when a cell is selected.
     *
     * @event cellSelectEvent
     * @param oArgs.el {HTMLElement} The selected TD element.
     * @param oArgs.record {YAHOO.widget.Record} The associated Record instance.
     * @param oArgs.column {YAHOO.widget.Column} The associated Column instance.
     * @param oArgs.key {String} (deprecated) The key of the selected cell.
     */

    /**
     * Fired when a cell is unselected.
     *
     * @event cellUnselectEvent
     * @param oArgs.el {HTMLElement} The unselected TD element.
     * @param oArgs.record {YAHOO.widget.Record} The associated Record.
     * @param oArgs.column {YAHOO.widget.Column} The associated Column instance.
     * @param oArgs.key {String} (deprecated) The key of the unselected cell.

     */

    /**
     * Fired when a cell is highlighted.
     *
     * @event cellHighlightEvent
     * @param oArgs.el {HTMLElement} The highlighted TD element.
     * @param oArgs.record {YAHOO.widget.Record} The associated Record instance.
     * @param oArgs.column {YAHOO.widget.Column} The associated Column instance.
     * @param oArgs.key {String} (deprecated) The key of the highlighted cell.

     */

    /**
     * Fired when a cell is unhighlighted.
     *
     * @event cellUnhighlightEvent
     * @param oArgs.el {HTMLElement} The unhighlighted TD element.
     * @param oArgs.record {YAHOO.widget.Record} The associated Record instance.
     * @param oArgs.column {YAHOO.widget.Column} The associated Column instance.
     * @param oArgs.key {String} (deprecated) The key of the unhighlighted cell.

     */

    /**
     * Fired when all cell selections are cleared.
     *
     * @event unselectAllCellsEvent
     */

    /**
     * Fired when a CellEditor is shown.
     *
     * @event editorShowEvent
     * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance.
     */

    /**
     * Fired when a CellEditor has a keydown.
     *
     * @event editorKeydownEvent
     * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance.
     * @param oArgs.event {HTMLEvent} The event object.
     */

    /**
     * Fired when a CellEditor input is reverted.
     *
     * @event editorRevertEvent
     * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance.
     * @param oArgs.newData {Object} New data value from form input field.
     * @param oArgs.oldData {Object} Old data value.
     */

    /**
     * Fired when a CellEditor input is saved.
     *
     * @event editorSaveEvent
     * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance.
     * @param oArgs.newData {Object} New data value from form input field.
     * @param oArgs.oldData {Object} Old data value.
     */

    /**
     * Fired when a CellEditor input is canceled.
     *
     * @event editorCancelEvent
     * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance.
     */

    /**
     * Fired when a CellEditor has a blur event.
     *
     * @event editorBlurEvent
     * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance.
     */

    /**
     * Fired when a CellEditor is blocked.
     *
     * @event editorBlockEvent
     * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance.
     */

    /**
     * Fired when a CellEditor is unblocked.
     *
     * @event editorUnblockEvent
     * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance.
     */





    /**
     * Fired when a link is clicked.
     *
     * @event linkClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The A element.
     */

    /**
     * Fired when a BUTTON element or INPUT element of type "button", "image",
     * "submit", "reset" is clicked.
     *
     * @event buttonClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The BUTTON element.
     */

    /**
     * Fired when a CHECKBOX element is clicked.
     *
     * @event checkboxClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The CHECKBOX element.
     */

    /**
     * Fired when a SELECT element is changed.
     *
     * @event dropdownChangeEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The SELECT element.
     */

    /**
     * Fired when a RADIO element is clicked.
     *
     * @event radioClickEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The RADIO element.
     */


























/////////////////////////////////////////////////////////////////////////////
//
// Deprecated APIs
//
/////////////////////////////////////////////////////////////////////////////
  
/*
 * @method showCellEditorBtns
 * @deprecated Use CellEditor.renderBtns() 
 */
showCellEditorBtns : function(elContainer) {
    // Buttons
    var elBtnsDiv = elContainer.appendChild(document.createElement("div"));
    Dom.addClass(elBtnsDiv, DT.CLASS_BUTTON);

    // Save button
    var elSaveBtn = elBtnsDiv.appendChild(document.createElement("button"));
    Dom.addClass(elSaveBtn, DT.CLASS_DEFAULT);
    elSaveBtn.innerHTML = "OK";
    Ev.addListener(elSaveBtn, "click", function(oArgs, oSelf) {
        oSelf.onEventSaveCellEditor(oArgs, oSelf);
        oSelf.focusTbodyEl();
    }, this, true);

    // Cancel button
    var elCancelBtn = elBtnsDiv.appendChild(document.createElement("button"));
    elCancelBtn.innerHTML = "Cancel";
    Ev.addListener(elCancelBtn, "click", function(oArgs, oSelf) {
        oSelf.onEventCancelCellEditor(oArgs, oSelf);
        oSelf.focusTbodyEl();
    }, this, true);

    YAHOO.log("The method showCellEditorBtns() has been deprecated." +
            " Please use the CellEditor class.", "warn", this.toString());
},

/**
 * @method resetCellEditor
 * @deprecated Use destroyCellEditor 
 */
resetCellEditor : function() {
    var elContainer = this._oCellEditor.container;
    elContainer.style.display = "none";
    Ev.purgeElement(elContainer, true);
    elContainer.innerHTML = "";
    this._oCellEditor.value = null;
    this._oCellEditor.isActive = false;

    YAHOO.log("The method resetCellEditor() has been deprecated." +
            " Please use the CellEditor class.", "warn", this.toString());
},

/**
 * @event editorUpdateEvent
 * @deprecated Use CellEditor class.
 */

/**
 * @method getBody
 * @deprecated Use getTbodyEl().
 */
getBody : function() {
    // Backward compatibility
    YAHOO.log("The method getBody() has been deprecated" +
            " in favor of getTbodyEl()", "warn", this.toString());
    return this.getTbodyEl();
},

/**
 * @method getCell
 * @deprecated Use getTdEl().
 */
getCell : function(index) {
    // Backward compatibility
    YAHOO.log("The method getCell() has been deprecated" +
            " in favor of getTdEl()", "warn", this.toString());
    return this.getTdEl(index);
},

/**
 * @method getRow
 * @deprecated Use getTrEl().
 */
getRow : function(index) {
    // Backward compatibility
    YAHOO.log("The method getRow() has been deprecated" +
            " in favor of getTrEl()", "warn", this.toString());
    return this.getTrEl(index);
},

/**
 * @method refreshView
 * @deprecated Use render.
 */
refreshView : function() {
    // Backward compatibility
    YAHOO.log("The method refreshView() has been deprecated" +
            " in favor of render()", "warn", this.toString());
    this.render();
},

/**
 * @method select
 * @deprecated Use selectRow.
 */
select : function(els) {
    // Backward compatibility
    YAHOO.log("The method select() has been deprecated" +
            " in favor of selectRow()", "warn", this.toString());
    if(!lang.isArray(els)) {
        els = [els];
    }
    for(var i=0; i<els.length; i++) {
        this.selectRow(els[i]);
    }
},

/**
 * @method onEventEditCell
 * @deprecated Use onEventShowCellEditor.
 */
onEventEditCell : function(oArgs) {
    // Backward compatibility
    YAHOO.log("The method onEventEditCell() has been deprecated" +
        " in favor of onEventShowCellEditor()", "warn", this.toString());
    this.onEventShowCellEditor(oArgs);
},

/**
 * @method _syncColWidths
 * @deprecated Use validateColumnWidths.
 */
_syncColWidths : function() {
    // Backward compatibility
    YAHOO.log("The method _syncColWidths() has been deprecated" +
        " in favor of validateColumnWidths()", "warn", this.toString());
    this.validateColumnWidths();
}

/**
 * @event headerRowMouseoverEvent
 * @deprecated Use theadRowMouseoverEvent.
 */

/**
 * @event headerRowMouseoutEvent
 * @deprecated Use theadRowMouseoutEvent.
 */

/**
 * @event headerRowMousedownEvent
 * @deprecated Use theadRowMousedownEvent.
 */

/**
 * @event headerRowClickEvent
 * @deprecated Use theadRowClickEvent.
 */

/**
 * @event headerRowDblclickEvent
 * @deprecated Use theadRowDblclickEvent.
 */

/**
 * @event headerCellMouseoverEvent
 * @deprecated Use theadCellMouseoverEvent.
 */

/**
 * @event headerCellMouseoutEvent
 * @deprecated Use theadCellMouseoutEvent.
 */

/**
 * @event headerCellMousedownEvent
 * @deprecated Use theadCellMousedownEvent.
 */

/**
 * @event headerCellClickEvent
 * @deprecated Use theadCellClickEvent.
 */

/**
 * @event headerCellDblclickEvent
 * @deprecated Use theadCellDblclickEvent.
 */

/**
 * @event headerLabelMouseoverEvent
 * @deprecated Use theadLabelMouseoverEvent.
 */

/**
 * @event headerLabelMouseoutEvent
 * @deprecated Use theadLabelMouseoutEvent.
 */

/**
 * @event headerLabelMousedownEvent
 * @deprecated Use theadLabelMousedownEvent.
 */

/**
 * @event headerLabelClickEvent
 * @deprecated Use theadLabelClickEvent.
 */

/**
 * @event headerLabelDbllickEvent
 * @deprecated Use theadLabelDblclickEvent.
 */

});

/**
 * Alias for onDataReturnSetRows for backward compatibility
 * @method onDataReturnSetRecords
 * @deprecated Use onDataReturnSetRows
 */
DT.prototype.onDataReturnSetRecords = DT.prototype.onDataReturnSetRows;

/**
 * Alias for onPaginatorChange for backward compatibility
 * @method onPaginatorChange
 * @deprecated Use onPaginatorChangeRequest
 */
DT.prototype.onPaginatorChange = DT.prototype.onPaginatorChangeRequest;

/////////////////////////////////////////////////////////////////////////////
//
// Deprecated static APIs
//
/////////////////////////////////////////////////////////////////////////////
/**
 * @method DataTable.editCheckbox
 * @deprecated  Use YAHOO.widget.CheckboxCellEditor.
 */
DT.editCheckbox = function() {};

/**
 * @method DataTable.editDate
 * @deprecated Use YAHOO.widget.DateCellEditor.
 */
DT.editDate = function() {};

/**
 * @method DataTable.editDropdown
 * @deprecated Use YAHOO.widget.DropdownCellEditor.
 */
DT.editDropdown = function() {};

/**
 * @method DataTable.editRadio
 * @deprecated Use YAHOO.widget.RadioCellEditor.
 */
DT.editRadio = function() {};

/**
 * @method DataTable.editTextarea
 * @deprecated Use YAHOO.widget.TextareaCellEditor
 */
DT.editTextarea = function() {};

/**
 * @method DataTable.editTextbox
 * @deprecated Use YAHOO.widget.TextboxCellEditor
 */
DT.editTextbox= function() {};

})();

(function () {

var lang   = YAHOO.lang,
    util   = YAHOO.util,
    widget = YAHOO.widget,
    ua     = YAHOO.env.ua,
    
    Dom    = util.Dom,
    Ev     = util.Event,
    DS     = util.DataSourceBase,
    DT     = widget.DataTable,
    Pag    = widget.Paginator;
    
/**
 * The ScrollingDataTable class extends the DataTable class to provide
 * functionality for x-scrolling, y-scrolling, and xy-scrolling.
 *
 * @namespace YAHOO.widget
 * @class ScrollingDataTable
 * @extends YAHOO.widget.DataTable
 * @constructor
 * @param elContainer {HTMLElement} Container element for the TABLE.
 * @param aColumnDefs {Object[]} Array of object literal Column definitions.
 * @param oDataSource {YAHOO.util.DataSource} DataSource instance.
 * @param oConfigs {object} (optional) Object literal of configuration values.
 */
widget.ScrollingDataTable = function(elContainer,aColumnDefs,oDataSource,oConfigs) {
    oConfigs = oConfigs || {};
    
    // Prevent infinite loop
    if(oConfigs.scrollable) {
        oConfigs.scrollable = false;
    }

    this._init();

    widget.ScrollingDataTable.superclass.constructor.call(this, elContainer,aColumnDefs,oDataSource,oConfigs); 

    // Once per instance
    this.subscribe("columnShowEvent", this._onColumnChange);
};

var SDT = widget.ScrollingDataTable;

/////////////////////////////////////////////////////////////////////////////
//
// Public constants
//
/////////////////////////////////////////////////////////////////////////////
lang.augmentObject(SDT, {

    /**
     * Class name assigned to inner DataTable header container.
     *
     * @property DataTable.CLASS_HEADER
     * @type String
     * @static
     * @final
     * @default "yui-dt-hd"
     */
    CLASS_HEADER : "yui-dt-hd",
    
    /**
     * Class name assigned to inner DataTable body container.
     *
     * @property DataTable.CLASS_BODY
     * @type String
     * @static
     * @final
     * @default "yui-dt-bd"
     */
    CLASS_BODY : "yui-dt-bd"
});

lang.extend(SDT, DT, {

/**
 * Container for fixed header TABLE element.
 *
 * @property _elHdContainer
 * @type HTMLElement
 * @private
 */
_elHdContainer : null,

/**
 * Fixed header TABLE element.
 *
 * @property _elHdTable
 * @type HTMLElement
 * @private
 */
_elHdTable : null,

/**
 * Container for scrolling body TABLE element.
 *
 * @property _elBdContainer
 * @type HTMLElement
 * @private
 */
_elBdContainer : null,

/**
 * Body THEAD element.
 *
 * @property _elBdThead
 * @type HTMLElement
 * @private
 */
_elBdThead : null,

/**
 * Offscreen container to temporarily clone SDT for auto-width calculation.
 *
 * @property _elTmpContainer
 * @type HTMLElement
 * @private
 */
_elTmpContainer : null,

/**
 * Offscreen TABLE element for auto-width calculation.
 *
 * @property _elTmpTable
 * @type HTMLElement
 * @private
 */
_elTmpTable : null,

/**
 * True if x-scrollbar is currently visible.
 * @property _bScrollbarX
 * @type Boolean
 * @private 
 */
_bScrollbarX : null,















/////////////////////////////////////////////////////////////////////////////
//
// Superclass methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Implementation of Element's abstract method. Sets up config values.
 *
 * @method initAttributes
 * @param oConfigs {Object} (Optional) Object literal definition of configuration values.
 * @private
 */

initAttributes : function(oConfigs) {
    oConfigs = oConfigs || {};
    SDT.superclass.initAttributes.call(this, oConfigs);

    /**
    * @attribute width
    * @description Table width for scrollable tables (e.g., "40em").
    * @type String
    */
    this.setAttributeConfig("width", {
        value: null,
        validator: lang.isString,
        method: function(oParam) {
            if(this._elHdContainer && this._elBdContainer) {
                this._elHdContainer.style.width = oParam;
                this._elBdContainer.style.width = oParam;            
                this._syncScrollX();      
                this._syncScrollOverhang();
            }
        }
    });

    /**
    * @attribute height
    * @description Table body height for scrollable tables, not including headers (e.g., "40em").
    * @type String
    */
    this.setAttributeConfig("height", {
        value: null,
        validator: lang.isString,
        method: function(oParam) {
            if(this._elHdContainer && this._elBdContainer) {
                this._elBdContainer.style.height = oParam;    
                this._syncScrollX();   
                this._syncScrollY();
                this._syncScrollOverhang();
            }
        }
    });

    /**
    * @attribute COLOR_COLUMNFILLER
    * @description CSS color value assigned to header filler on scrollable tables.  
    * @type String
    * @default "#F2F2F2"
    */
    this.setAttributeConfig("COLOR_COLUMNFILLER", {
        value: "#F2F2F2",
        validator: lang.isString,
        method: function(oParam) {
            if(this._elHdContainer) {
                this._elHdContainer.style.backgroundColor = oParam;
            }
        }
    });
},

/**
 * Initializes internal variables.
 *
 * @method _init
 * @private
 */
_init : function() {
    this._elHdContainer = null;
    this._elHdTable = null;
    this._elBdContainer = null;
    this._elBdThead = null;
    this._elTmpContainer = null;
    this._elTmpTable = null;
},

/**
 * Initializes DOM elements for a ScrollingDataTable, including creation of
 * two separate TABLE elements.
 *
 * @method _initDomElements
 * @param elContainer {HTMLElement | String} HTML DIV element by reference or ID. 
 * return {Boolean} False in case of error, otherwise true 
 * @private
 */
_initDomElements : function(elContainer) {
    // Outer and inner containers
    this._initContainerEl(elContainer);
    if(this._elContainer && this._elHdContainer && this._elBdContainer) {
        // TABLEs
        this._initTableEl();
        
        if(this._elHdTable && this._elTable) {
            // COLGROUPs
            ///this._initColgroupEl(this._elHdTable, this._elTable);  
            this._initColgroupEl(this._elHdTable);        
            
            // THEADs
            this._initTheadEl(this._elHdTable, this._elTable);
            
            // Primary TBODY
            this._initTbodyEl(this._elTable);
            // Message TBODY
            this._initMsgTbodyEl(this._elTable);            
        }
    }
    if(!this._elContainer || !this._elTable || !this._elColgroup ||  !this._elThead || !this._elTbody || !this._elMsgTbody ||
            !this._elHdTable || !this._elBdThead) {
        YAHOO.log("Could not instantiate DataTable due to an invalid DOM elements", "error", this.toString());
        return false;
    }
    else {
        return true;
    }
},

/**
 * Destroy's the DataTable outer and inner container elements, if available.
 *
 * @method _destroyContainerEl
 * @param elContainer {HTMLElement} Reference to the container element. 
 * @private
 */
_destroyContainerEl : function(elContainer) {
    Dom.removeClass(elContainer, DT.CLASS_SCROLLABLE);
    SDT.superclass._destroyContainerEl.call(this, elContainer);
    this._elHdContainer = null;
    this._elBdContainer = null;
},

/**
 * Initializes the DataTable outer container element and creates inner header
 * and body container elements.
 *
 * @method _initContainerEl
 * @param elContainer {HTMLElement | String} HTML DIV element by reference or ID.
 * @private
 */
_initContainerEl : function(elContainer) {
    SDT.superclass._initContainerEl.call(this, elContainer);
    
    if(this._elContainer) {
        elContainer = this._elContainer; // was constructor input, now is DOM ref
        Dom.addClass(elContainer, DT.CLASS_SCROLLABLE);
        
        // Container for header TABLE
        var elHdContainer = document.createElement("div");
        elHdContainer.style.width = this.get("width") || "";
        elHdContainer.style.backgroundColor = this.get("COLOR_COLUMNFILLER");
        Dom.addClass(elHdContainer, SDT.CLASS_HEADER);
        this._elHdContainer = elHdContainer;
        elContainer.appendChild(elHdContainer);
    
        // Container for body TABLE
        var elBdContainer = document.createElement("div");
        elBdContainer.style.width = this.get("width") || "";
        elBdContainer.style.height = this.get("height") || "";
        Dom.addClass(elBdContainer, SDT.CLASS_BODY);
        Ev.addListener(elBdContainer, "scroll", this._onScroll, this); // to sync horiz scroll headers
        this._elBdContainer = elBdContainer;
        elContainer.appendChild(elBdContainer);
    }
},

/**
 * Creates HTML markup CAPTION element.
 *
 * @method _initCaptionEl
 * @param sCaption {String} Text for caption.
 * @private
 */
_initCaptionEl : function(sCaption) {
    // Not yet supported
    /*if(this._elHdTable && sCaption) {
        // Create CAPTION element
        if(!this._elCaption) { 
            this._elCaption = this._elHdTable.createCaption();
        }
        // Set CAPTION value
        this._elCaption.innerHTML = sCaption;
    }
    else if(this._elCaption) {
        this._elCaption.parentNode.removeChild(this._elCaption);
    }*/
},

/**
 * Destroy's the DataTable head TABLE element, if available.
 *
 * @method _destroyHdTableEl
 * @private
 */
_destroyHdTableEl : function() {
    var elTable = this._elHdTable;
    if(elTable) {
        Ev.purgeElement(elTable, true);
        elTable.parentNode.removeChild(elTable);
        
        // A little out of place, but where else can we null out these extra elements?
        ///this._elBdColgroup = null;
        this._elBdThead = null;
    }
},

/**
 * Initializes ScrollingDataTable TABLE elements into the two inner containers.
 *
 * @method _initTableEl
 * @private
 */
_initTableEl : function() {
    // Head TABLE
    if(this._elHdContainer) {
        this._destroyHdTableEl();
    
        // Create TABLE
        this._elHdTable = this._elHdContainer.appendChild(document.createElement("table"));   

        // Set up mouseover/mouseout events via mouseenter/mouseleave delegation
        Ev.delegate(this._elHdTable, "mouseenter", this._onTableMouseover, "thead ."+DT.CLASS_LABEL, this);
        Ev.delegate(this._elHdTable, "mouseleave", this._onTableMouseout, "thead ."+DT.CLASS_LABEL, this);
    }
    // Body TABLE
    SDT.superclass._initTableEl.call(this, this._elBdContainer);
},

/**
 * Initializes ScrollingDataTable THEAD elements into the two inner containers.
 *
 * @method _initTheadEl
 * @param elHdTable {HTMLElement} (optional) Fixed header TABLE element reference.
 * @param elTable {HTMLElement} (optional) TABLE element reference.
 * @private
 */
_initTheadEl : function(elHdTable, elTable) {
    elHdTable = elHdTable || this._elHdTable;
    elTable = elTable || this._elTable;
    
    // Scrolling body's THEAD
    this._initBdTheadEl(elTable);
    // Standard fixed head THEAD
    SDT.superclass._initTheadEl.call(this, elHdTable);
},

/**
 * SDT changes ID so as not to duplicate the accessibility TH IDs.
 *
 * @method _initThEl
 * @param elTh {HTMLElement} TH element reference.
 * @param oColumn {YAHOO.widget.Column} Column object.
 * @private
 */
_initThEl : function(elTh, oColumn) {
    SDT.superclass._initThEl.call(this, elTh, oColumn);
    elTh.id = this.getId() +"-fixedth-" + oColumn.getSanitizedKey(); // Needed for getColumn by TH and ColumnDD
},

/**
 * Destroy's the DataTable body THEAD element, if available.
 *
 * @method _destroyBdTheadEl
 * @private
 */
_destroyBdTheadEl : function() {
    var elBdThead = this._elBdThead;
    if(elBdThead) {
        var elTable = elBdThead.parentNode;
        Ev.purgeElement(elBdThead, true);
        elTable.removeChild(elBdThead);
        this._elBdThead = null;

        this._destroyColumnHelpers();
    }
},

/**
 * Initializes body THEAD element.
 *
 * @method _initBdTheadEl
 * @param elTable {HTMLElement} TABLE element into which to create THEAD.
 * @return {HTMLElement} Initialized THEAD element. 
 * @private
 */
_initBdTheadEl : function(elTable) {
    if(elTable) {
        // Destroy previous
        this._destroyBdTheadEl();

        var elThead = elTable.insertBefore(document.createElement("thead"), elTable.firstChild);
        
        // Add TRs to the THEAD;
        var oColumnSet = this._oColumnSet,
            colTree = oColumnSet.tree,
            elTh, elTheadTr, oColumn, i, j, k, len;

        for(i=0, k=colTree.length; i<k; i++) {
            elTheadTr = elThead.appendChild(document.createElement("tr"));
    
            // ...and create TH cells
            for(j=0, len=colTree[i].length; j<len; j++) {
                oColumn = colTree[i][j];
                elTh = elTheadTr.appendChild(document.createElement("th"));
                this._initBdThEl(elTh,oColumn,i,j);
            }
        }
        this._elBdThead = elThead;
        YAHOO.log("Accessibility TH cells for " + this._oColumnSet.keys.length + " keys created","info",this.toString());
    }
},

/**
 * Populates TH element for the body THEAD element.
 *
 * @method _initBdThEl
 * @param elTh {HTMLElement} TH element reference.
 * @param oColumn {YAHOO.widget.Column} Column object.
 * @private
 */
_initBdThEl : function(elTh, oColumn) {
    elTh.id = this.getId()+"-th-" + oColumn.getSanitizedKey(); // Needed for accessibility
    elTh.rowSpan = oColumn.getRowspan();
    elTh.colSpan = oColumn.getColspan();
    // Assign abbr attribute
    if(oColumn.abbr) {
        elTh.abbr = oColumn.abbr;
    }

    // TODO: strip links and form elements
    var sKey = oColumn.getKey();
    var sLabel = lang.isValue(oColumn.label) ? oColumn.label : sKey;
    elTh.innerHTML = sLabel;
},

/**
 * Initializes ScrollingDataTable TBODY element for data
 *
 * @method _initTbodyEl
 * @param elTable {HTMLElement} TABLE element into which to create TBODY .
 * @private
 */
_initTbodyEl : function(elTable) {
    SDT.superclass._initTbodyEl.call(this, elTable);
    
    // Bug 2105534 - Safari 3 gap
    // Bug 2492591 - IE8 offsetTop
    elTable.style.marginTop = (this._elTbody.offsetTop > 0) ?
            "-"+this._elTbody.offsetTop+"px" : 0;
},





























/**
 * Sets focus on the given element.
 *
 * @method _focusEl
 * @param el {HTMLElement} Element.
 * @private
 */
_focusEl : function(el) {
    el = el || this._elTbody;
    var oSelf = this;
    this._storeScrollPositions();
    // http://developer.mozilla.org/en/docs/index.php?title=Key-navigable_custom_DHTML_widgets
    // The timeout is necessary in both IE and Firefox 1.5, to prevent scripts from doing
    // strange unexpected things as the user clicks on buttons and other controls.
    
    // Bug 1921135: Wrap the whole thing in a setTimeout
    setTimeout(function() {
        setTimeout(function() {
            try {
                el.focus();
                oSelf._restoreScrollPositions();
            }
            catch(e) {
            }
        },0);
    }, 0);
},



















/**
 * Internal wrapper calls run() on render Chain instance.
 *
 * @method _runRenderChain
 * @private 
 */
_runRenderChain : function() {
    this._storeScrollPositions();
    this._oChainRender.run();
},

/**
 * Stores scroll positions so they can be restored after a render.
 *
 * @method _storeScrollPositions
 * @private
 */
 _storeScrollPositions : function() {
    this._nScrollTop = this._elBdContainer.scrollTop;
    this._nScrollLeft = this._elBdContainer.scrollLeft;
},

/**
 * Clears stored scroll positions to interrupt the automatic restore mechanism.
 * Useful for setting scroll positions programmatically rather than as part of
 * the post-render cleanup process.
 *
 * @method clearScrollPositions
 * @private
 */
 clearScrollPositions : function() {
    this._nScrollTop = 0;
    this._nScrollLeft = 0;
},

/**
 * Restores scroll positions to stored value. 
 *
 * @method _retoreScrollPositions
 * @private 
 */
 _restoreScrollPositions : function() {
    // Reset scroll positions
    if(this._nScrollTop) {
        this._elBdContainer.scrollTop = this._nScrollTop;
        this._nScrollTop = null;
    } 
    if(this._nScrollLeft) {
        this._elBdContainer.scrollLeft = this._nScrollLeft;
        // Bug 2529024
        this._elHdContainer.scrollLeft = this._nScrollLeft; 
        this._nScrollLeft = null;
    } 
},

/**
 * Helper function calculates and sets a validated width for a Column in a ScrollingDataTable.
 *
 * @method _validateColumnWidth
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param elTd {HTMLElement} TD element to validate against.
 * @private
 */
_validateColumnWidth : function(oColumn, elTd) {
    // Only Columns without widths that are not hidden
    if(!oColumn.width && !oColumn.hidden) {
        var elTh = oColumn.getThEl();
        // Unset a calculated auto-width
        if(oColumn._calculatedWidth) {
            this._setColumnWidth(oColumn, "auto", "visible");
        }
        // Compare auto-widths
        if(elTh.offsetWidth !== elTd.offsetWidth) {
            var elWider = (elTh.offsetWidth > elTd.offsetWidth) ?
                    oColumn.getThLinerEl() : elTd.firstChild;               

            // Grab the wider liner width, unless the minWidth is wider
            var newWidth = Math.max(0,
                (elWider.offsetWidth -(parseInt(Dom.getStyle(elWider,"paddingLeft"),10)|0) - (parseInt(Dom.getStyle(elWider,"paddingRight"),10)|0)),
                oColumn.minWidth);
                
            var sOverflow = 'visible';
            
            // Now validate against maxAutoWidth
            if((oColumn.maxAutoWidth > 0) && (newWidth > oColumn.maxAutoWidth)) {
                newWidth = oColumn.maxAutoWidth;
                sOverflow = "hidden";
            }

            // Set to the wider auto-width
            this._elTbody.style.display = "none";
            this._setColumnWidth(oColumn, newWidth+'px', sOverflow);
            oColumn._calculatedWidth = newWidth;
            this._elTbody.style.display = "";
        }
    }
},

/**
 * For one or all Columns of a ScrollingDataTable, when Column is not hidden,
 * and width is not set, syncs widths of header and body cells and 
 * validates that width against minWidth and/or maxAutoWidth as necessary.
 *
 * @method validateColumnWidths
 * @param oArg.column {YAHOO.widget.Column} (optional) One Column to validate. If null, all Columns' widths are validated.
 */
validateColumnWidths : function(oColumn) {
    // Validate there is at least one TR with proper TDs
    var allKeys   = this._oColumnSet.keys,
        allKeysLength = allKeys.length,
        elRow     = this.getFirstTrEl();

    // Reset overhang for IE
    if(ua.ie) {
        this._setOverhangValue(1);
    }

    if(allKeys && elRow && (elRow.childNodes.length === allKeysLength)) {
        // Temporarily unsnap container since it causes inaccurate calculations
        var sWidth = this.get("width");
        if(sWidth) {
            this._elHdContainer.style.width = "";
            this._elBdContainer.style.width = "";
        }
        this._elContainer.style.width = "";
        
        //Validate just one Column
        if(oColumn && lang.isNumber(oColumn.getKeyIndex())) {
            this._validateColumnWidth(oColumn, elRow.childNodes[oColumn.getKeyIndex()]);
        }
        // Iterate through all Columns to unset calculated widths in one pass
        else {
            var elTd, todos = [], thisTodo, i, len;
            for(i=0; i<allKeysLength; i++) {
                oColumn = allKeys[i];
                // Only Columns without widths that are not hidden, unset a calculated auto-width
                if(!oColumn.width && !oColumn.hidden && oColumn._calculatedWidth) {
                    todos[todos.length] = oColumn;      
                }
            }
            
            this._elTbody.style.display = "none";
            for(i=0, len=todos.length; i<len; i++) {
                this._setColumnWidth(todos[i], "auto", "visible");
            }
            this._elTbody.style.display = "";
            
            todos = [];

            // Iterate through all Columns and make the store the adjustments to make in one pass
            for(i=0; i<allKeysLength; i++) {
                oColumn = allKeys[i];
                elTd = elRow.childNodes[i];
                // Only Columns without widths that are not hidden
                if(!oColumn.width && !oColumn.hidden) {
                    var elTh = oColumn.getThEl();

                    // Compare auto-widths
                    if(elTh.offsetWidth !== elTd.offsetWidth) {
                        var elWider = (elTh.offsetWidth > elTd.offsetWidth) ?
                                oColumn.getThLinerEl() : elTd.firstChild;               
                
                        // Grab the wider liner width, unless the minWidth is wider
                        var newWidth = Math.max(0,
                            (elWider.offsetWidth -(parseInt(Dom.getStyle(elWider,"paddingLeft"),10)|0) - (parseInt(Dom.getStyle(elWider,"paddingRight"),10)|0)),
                            oColumn.minWidth);
                            
                        var sOverflow = 'visible';
                        
                        // Now validate against maxAutoWidth
                        if((oColumn.maxAutoWidth > 0) && (newWidth > oColumn.maxAutoWidth)) {
                            newWidth = oColumn.maxAutoWidth;
                            sOverflow = "hidden";
                        }
                
                        todos[todos.length] = [oColumn, newWidth, sOverflow];
                    }
                }
            }
            
            this._elTbody.style.display = "none";
            for(i=0, len=todos.length; i<len; i++) {
                thisTodo = todos[i];
                // Set to the wider auto-width
                this._setColumnWidth(thisTodo[0], thisTodo[1]+"px", thisTodo[2]);
                thisTodo[0]._calculatedWidth = thisTodo[1];
            }
            this._elTbody.style.display = "";
        }
    
        // Resnap unsnapped containers
        if(sWidth) {
            this._elHdContainer.style.width = sWidth;
            this._elBdContainer.style.width = sWidth;
        } 
    }
    
    this._syncScroll();
    this._restoreScrollPositions();
},

/**
 * Syncs padding around scrollable tables, including Column header right-padding
 * and container width and height.
 *
 * @method _syncScroll
 * @private 
 */
_syncScroll : function() {
    this._syncScrollX();
    this._syncScrollY();
    this._syncScrollOverhang();
    if(ua.opera) {
        // Bug 1925874
        this._elHdContainer.scrollLeft = this._elBdContainer.scrollLeft;
        if(!this.get("width")) {
            // Bug 1926125
            document.body.style += '';
        }
    }
 },

/**
 * Snaps container width for y-scrolling tables.
 *
 * @method _syncScrollY
 * @private
 */
_syncScrollY : function() {
    var elTbody = this._elTbody,
        elBdContainer = this._elBdContainer;
    
    // X-scrolling not enabled
    if(!this.get("width")) {
        // Snap outer container width to content
        this._elContainer.style.width = 
                (elBdContainer.scrollHeight > elBdContainer.clientHeight) ?
                // but account for y-scrollbar since it is visible
                (elTbody.parentNode.clientWidth + 19) + "px" :
                // no y-scrollbar, just borders
                (elTbody.parentNode.clientWidth + 2) + "px";
    }
},

/**
 * Snaps container height for x-scrolling tables in IE. Syncs message TBODY width.
 *
 * @method _syncScrollX
 * @private
 */
_syncScrollX : function() {
    var elTbody = this._elTbody,
        elBdContainer = this._elBdContainer;
    
    // IE 6 and 7 only when y-scrolling not enabled
    if(!this.get("height") && (ua.ie)) {
        // Snap outer container height to content
        elBdContainer.style.height = 
                // but account for x-scrollbar if it is visible
                (elBdContainer.scrollWidth > elBdContainer.offsetWidth ) ?
                (elTbody.parentNode.offsetHeight + 18) + "px" : 
                elTbody.parentNode.offsetHeight + "px";
    }

    // Sync message tbody
    if(this._elTbody.rows.length === 0) {
        this._elMsgTbody.parentNode.style.width = this.getTheadEl().parentNode.offsetWidth + "px";
    }
    else {
        this._elMsgTbody.parentNode.style.width = "";
    }
},

/**
 * Adds/removes Column header overhang as necesary.
 *
 * @method _syncScrollOverhang
 * @private
 */
_syncScrollOverhang : function() {
    var elBdContainer = this._elBdContainer,
        // Overhang should be either 1 (default) or 18px, depending on the location of the right edge of the table
        nPadding = 1;
    
    // Y-scrollbar is visible, which is when the overhang needs to jut out
    if((elBdContainer.scrollHeight > elBdContainer.clientHeight) &&
        // X-scrollbar is also visible, which means the right is jagged, not flush with the Column
        (elBdContainer.scrollWidth > elBdContainer.clientWidth)) {
        nPadding = 18;
    }
    
    this._setOverhangValue(nPadding);
    
},

/**
 * Sets Column header overhang to given width.
 *
 * @method _setOverhangValue
 * @param nBorderWidth {Number} Value of new border for overhang. 
 * @private
 */
_setOverhangValue : function(nBorderWidth) {
    var aLastHeaders = this._oColumnSet.headers[this._oColumnSet.headers.length-1] || [],
        len = aLastHeaders.length,
        sPrefix = this._sId+"-fixedth-",
        sValue = nBorderWidth + "px solid " + this.get("COLOR_COLUMNFILLER");

    this._elThead.style.display = "none";
    for(var i=0; i<len; i++) {
        Dom.get(sPrefix+aLastHeaders[i]).style.borderRight = sValue;
    }
    this._elThead.style.display = "";
},






































/**
 * Returns DOM reference to the DataTable's fixed header container element.
 *
 * @method getHdContainerEl
 * @return {HTMLElement} Reference to DIV element.
 */
getHdContainerEl : function() {
    return this._elHdContainer;
},

/**
 * Returns DOM reference to the DataTable's scrolling body container element.
 *
 * @method getBdContainerEl
 * @return {HTMLElement} Reference to DIV element.
 */
getBdContainerEl : function() {
    return this._elBdContainer;
},

/**
 * Returns DOM reference to the DataTable's fixed header TABLE element.
 *
 * @method getHdTableEl
 * @return {HTMLElement} Reference to TABLE element.
 */
getHdTableEl : function() {
    return this._elHdTable;
},

/**
 * Returns DOM reference to the DataTable's scrolling body TABLE element.
 *
 * @method getBdTableEl
 * @return {HTMLElement} Reference to TABLE element.
 */
getBdTableEl : function() {
    return this._elTable;
},

/**
 * Disables ScrollingDataTable UI.
 *
 * @method disable
 */
disable : function() {
    var elMask = this._elMask;
    elMask.style.width = this._elBdContainer.offsetWidth + "px";
    elMask.style.height = this._elHdContainer.offsetHeight + this._elBdContainer.offsetHeight + "px";
    elMask.style.display = "";
    this.fireEvent("disableEvent");
},

/**
 * Removes given Column. NOTE: You cannot remove nested Columns. You can only remove
 * non-nested Columns, and top-level parent Columns (which will remove all
 * children Columns).
 *
 * @method removeColumn
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @return oColumn {YAHOO.widget.Column} Removed Column instance.
 */
removeColumn : function(oColumn) {
    // Store scroll pos
    var hdPos = this._elHdContainer.scrollLeft;
    var bdPos = this._elBdContainer.scrollLeft;
    
    // Call superclass method
    oColumn = SDT.superclass.removeColumn.call(this, oColumn);
    
    // Restore scroll pos
    this._elHdContainer.scrollLeft = hdPos;
    this._elBdContainer.scrollLeft = bdPos;
    
    return oColumn;
},

/**
 * Inserts given Column at the index if given, otherwise at the end. NOTE: You
 * can only add non-nested Columns and top-level parent Columns. You cannot add
 * a nested Column to an existing parent.
 *
 * @method insertColumn
 * @param oColumn {Object | YAHOO.widget.Column} Object literal Column
 * definition or a Column instance.
 * @param index {Number} (optional) New tree index.
 * @return oColumn {YAHOO.widget.Column} Inserted Column instance. 
 */
insertColumn : function(oColumn, index) {
    // Store scroll pos
    var hdPos = this._elHdContainer.scrollLeft;
    var bdPos = this._elBdContainer.scrollLeft;
    
    // Call superclass method
    var oNewColumn = SDT.superclass.insertColumn.call(this, oColumn, index);
    
    // Restore scroll pos
    this._elHdContainer.scrollLeft = hdPos;
    this._elBdContainer.scrollLeft = bdPos;
    
    return oNewColumn;
},

/**
 * Removes given Column and inserts into given tree index. NOTE: You
 * can only reorder non-nested Columns and top-level parent Columns. You cannot
 * reorder a nested Column to an existing parent.
 *
 * @method reorderColumn
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param index {Number} New tree index.
 */
reorderColumn : function(oColumn, index) {
    // Store scroll pos
    var hdPos = this._elHdContainer.scrollLeft;
    var bdPos = this._elBdContainer.scrollLeft;
    
    // Call superclass method
    var oNewColumn = SDT.superclass.reorderColumn.call(this, oColumn, index);
    
    // Restore scroll pos
    this._elHdContainer.scrollLeft = hdPos;
    this._elBdContainer.scrollLeft = bdPos;

    return oNewColumn;
},

/**
 * Sets given Column to given pixel width. If new width is less than minWidth
 * width, sets to minWidth. Updates oColumn.width value.
 *
 * @method setColumnWidth
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param nWidth {Number} New width in pixels.
 */
setColumnWidth : function(oColumn, nWidth) {
    oColumn = this.getColumn(oColumn);
    if(oColumn) {
        this._storeScrollPositions();

        // Validate new width against minWidth
        if(lang.isNumber(nWidth)) {
            nWidth = (nWidth > oColumn.minWidth) ? nWidth : oColumn.minWidth;

            // Save state
            oColumn.width = nWidth;
            
            // Resize the DOM elements
            this._setColumnWidth(oColumn, nWidth+"px");
            this._syncScroll();
            
            this.fireEvent("columnSetWidthEvent",{column:oColumn,width:nWidth});
            YAHOO.log("Set width of Column " + oColumn + " to " + nWidth + "px", "info", this.toString());
        }
        // Unsets a width to auto-size
        else if(nWidth === null) {
            // Save state
            oColumn.width = nWidth;
            
            // Resize the DOM elements
            this._setColumnWidth(oColumn, "auto");
            this.validateColumnWidths(oColumn);
            this.fireEvent("columnUnsetWidthEvent",{column:oColumn});
            YAHOO.log("Column " + oColumn + " width unset", "info", this.toString());
        }
        
        // Bug 2339454: resize then sort misaligment
        this._clearTrTemplateEl();
    }
    else {
        YAHOO.log("Could not set width of Column " + oColumn + " to " + nWidth + "px", "warn", this.toString());
    }
},

/**
 * Scrolls to given row or cell
 *
 * @method scrollTo
 * @param to {YAHOO.widget.Record | HTMLElement } Itme to scroll to.
 */
scrollTo : function(to) {
        var td = this.getTdEl(to);
        if(td) {
            this.clearScrollPositions();
            this.getBdContainerEl().scrollLeft = td.offsetLeft;
            this.getBdContainerEl().scrollTop = td.parentNode.offsetTop;
        }
        else {
            var tr = this.getTrEl(to);
            if(tr) {
                this.clearScrollPositions();
                this.getBdContainerEl().scrollTop = tr.offsetTop;
            }
        }
},

/**
 * Displays message within secondary TBODY.
 *
 * @method showTableMessage
 * @param sHTML {String} (optional) Value for innerHTMlang.
 * @param sClassName {String} (optional) Classname.
 */
showTableMessage : function(sHTML, sClassName) {
    var elCell = this._elMsgTd;
    if(lang.isString(sHTML)) {
        elCell.firstChild.innerHTML = sHTML;
    }
    if(lang.isString(sClassName)) {
        Dom.addClass(elCell.firstChild, sClassName);
    }

    // Needed for SDT only
    var elThead = this.getTheadEl();
    var elTable = elThead.parentNode;
    var newWidth = elTable.offsetWidth;
    this._elMsgTbody.parentNode.style.width = this.getTheadEl().parentNode.offsetWidth + "px";

    this._elMsgTbody.style.display = "";

    this.fireEvent("tableMsgShowEvent", {html:sHTML, className:sClassName});
    YAHOO.log("DataTable showing message: " + sHTML, "info", this.toString());
},













/////////////////////////////////////////////////////////////////////////////
//
// Private Custom Event Handlers
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Handles Column mutations
 *
 * @method onColumnChange
 * @param oArgs {Object} Custom Event data.
 */
_onColumnChange : function(oArg) {
    // Figure out which Column changed
    var oColumn = (oArg.column) ? oArg.column :
            (oArg.editor) ? oArg.editor.column : null;
    this._storeScrollPositions();
    this.validateColumnWidths(oColumn);
},















/////////////////////////////////////////////////////////////////////////////
//
// Private DOM Event Handlers
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Syncs scrolltop and scrollleft of all TABLEs.
 *
 * @method _onScroll
 * @param e {HTMLEvent} The scroll event.
 * @param oSelf {YAHOO.widget.ScrollingDataTable} ScrollingDataTable instance.
 * @private
 */
_onScroll : function(e, oSelf) {
    oSelf._elHdContainer.scrollLeft = oSelf._elBdContainer.scrollLeft;

    if(oSelf._oCellEditor && oSelf._oCellEditor.isActive) {
        oSelf.fireEvent("editorBlurEvent", {editor:oSelf._oCellEditor});
        oSelf.cancelCellEditor();
    }

    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName.toLowerCase();
    oSelf.fireEvent("tableScrollEvent", {event:e, target:elTarget});
},

/**
 * Handles keydown events on the THEAD element.
 *
 * @method _onTheadKeydown
 * @param e {HTMLEvent} The key event.
 * @param oSelf {YAHOO.widget.ScrollingDataTable} ScrollingDataTable instance.
 * @private
 */
_onTheadKeydown : function(e, oSelf) {
    // If tabbing to next TH label link causes THEAD to scroll,
    // need to sync scrollLeft with TBODY
    if(Ev.getCharCode(e) === 9) {
        setTimeout(function() {
            if((oSelf instanceof SDT) && oSelf._sId) {
                oSelf._elBdContainer.scrollLeft = oSelf._elHdContainer.scrollLeft;
            }
        },0);
    }
    
    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "input":
            case "textarea":
                // TODO: implement textareaKeyEvent
                break;
            case "thead":
                bKeepBubbling = oSelf.fireEvent("theadKeyEvent",{target:elTarget,event:e});
                break;
            default:
                break;
        }
        if(bKeepBubbling === false) {
            return;
        }
        else {
            elTarget = elTarget.parentNode;
            if(elTarget) {
                elTag = elTarget.nodeName.toLowerCase();
            }
        }
    }
    oSelf.fireEvent("tableKeyEvent",{target:(elTarget || oSelf._elContainer),event:e});
}




/**
 * Fired when a fixed scrolling DataTable has a scroll.
 *
 * @event tableScrollEvent
 * @param oArgs.event {HTMLEvent} The event object.
 * @param oArgs.target {HTMLElement} The DataTable's CONTAINER element (in IE)
 * or the DataTable's TBODY element (everyone else).
 *
 */




});

})();

(function () {

var lang   = YAHOO.lang,
    util   = YAHOO.util,
    widget = YAHOO.widget,
    ua     = YAHOO.env.ua,
    
    Dom    = util.Dom,
    Ev     = util.Event,
    
    DT     = widget.DataTable;
/****************************************************************************/
/****************************************************************************/
/****************************************************************************/
    
/**
 * The BaseCellEditor class provides base functionality common to all inline cell
 * editors for a DataTable widget.
 *
 * @namespace YAHOO.widget
 * @class BaseCellEditor
 * @uses YAHOO.util.EventProvider 
 * @constructor
 * @param sType {String} Type indicator, to map to YAHOO.widget.DataTable.Editors.
 * @param oConfigs {Object} (Optional) Object literal of configs.
 */
widget.BaseCellEditor = function(sType, oConfigs) {
    this._sId = this._sId || Dom.generateId(null, "yui-ceditor"); // "yui-ceditor" + YAHOO.widget.BaseCellEditor._nCount++;
    YAHOO.widget.BaseCellEditor._nCount++;
    this._sType = sType;
    
    // Validate inputs
    this._initConfigs(oConfigs); 
    
    // Create Custom Events
    this._initEvents();
             
    // UI needs to be drawn
    this._needsRender = true;
};

var BCE = widget.BaseCellEditor;

/////////////////////////////////////////////////////////////////////////////
//
// Static members
//
/////////////////////////////////////////////////////////////////////////////
lang.augmentObject(BCE, {

/**
 * Global instance counter.
 *
 * @property CellEditor._nCount
 * @type Number
 * @static
 * @default 0
 * @private 
 */
_nCount : 0,

/**
 * Class applied to CellEditor container.
 *
 * @property CellEditor.CLASS_CELLEDITOR
 * @type String
 * @static
 * @default "yui-ceditor"
 */
CLASS_CELLEDITOR : "yui-ceditor"

});

BCE.prototype = {
/////////////////////////////////////////////////////////////////////////////
//
// Private members
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Unique id assigned to instance "yui-ceditorN", useful prefix for generating unique
 * DOM ID strings and log messages.
 *
 * @property _sId
 * @type String
 * @private
 */
_sId : null,

/**
 * Editor type.
 *
 * @property _sType
 * @type String
 * @private
 */
_sType : null,

/**
 * DataTable instance.
 *
 * @property _oDataTable
 * @type YAHOO.widget.DataTable
 * @private 
 */
_oDataTable : null,

/**
 * Column instance.
 *
 * @property _oColumn
 * @type YAHOO.widget.Column
 * @default null
 * @private 
 */
_oColumn : null,

/**
 * Record instance.
 *
 * @property _oRecord
 * @type YAHOO.widget.Record
 * @default null
 * @private 
 */
_oRecord : null,

/**
 * TD element.
 *
 * @property _elTd
 * @type HTMLElement
 * @default null
 * @private
 */
_elTd : null,

/**
 * Container for inline editor.
 *
 * @property _elContainer
 * @type HTMLElement
 * @private 
 */
_elContainer : null,

/**
 * Reference to Cancel button, if available.
 *
 * @property _elCancelBtn
 * @type HTMLElement
 * @default null
 * @private 
 */
_elCancelBtn : null,

/**
 * Reference to Save button, if available.
 *
 * @property _elSaveBtn
 * @type HTMLElement
 * @default null
 * @private 
 */
_elSaveBtn : null,








/////////////////////////////////////////////////////////////////////////////
//
// Private methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Initialize configs.
 *
 * @method _initConfigs
 * @private   
 */
_initConfigs : function(oConfigs) {
    // Object literal defines CellEditor configs
    if(oConfigs && YAHOO.lang.isObject(oConfigs)) {
        for(var sConfig in oConfigs) {
            if(sConfig) {
                this[sConfig] = oConfigs[sConfig];
            }
        }
    }
},

/**
 * Initialize Custom Events.
 *
 * @method _initEvents
 * @private   
 */
_initEvents : function() {
    this.createEvent("showEvent");
    this.createEvent("keydownEvent");
    this.createEvent("invalidDataEvent");
    this.createEvent("revertEvent");
    this.createEvent("saveEvent");
    this.createEvent("cancelEvent");
    this.createEvent("blurEvent");
    this.createEvent("blockEvent");
    this.createEvent("unblockEvent");
},

/**
 * Initialize container element.
 *
 * @method _initContainerEl
 * @private
 */
_initContainerEl : function() {
    if(this._elContainer) {
        YAHOO.util.Event.purgeElement(this._elContainer, true);
        this._elContainer.innerHTML = "";
    }

    var elContainer = document.createElement("div");
    elContainer.id = this.getId() + "-container"; // Needed for tracking blur event
    elContainer.style.display = "none";
    elContainer.tabIndex = 0;
    
    this.className = lang.isArray(this.className) ? this.className : this.className ? [this.className] : [];
    this.className[this.className.length] = DT.CLASS_EDITOR;
    elContainer.className = this.className.join(" ");
    
    document.body.insertBefore(elContainer, document.body.firstChild);
    this._elContainer = elContainer;
},

/**
 * Initialize container shim element.
 *
 * @method _initShimEl
 * @private
 */
_initShimEl : function() {
    // Iframe shim
    if(this.useIFrame) {
        if(!this._elIFrame) {
            var elIFrame = document.createElement("iframe");
            elIFrame.src = "javascript:false";
            elIFrame.frameBorder = 0;
            elIFrame.scrolling = "no";
            elIFrame.style.display = "none";
            elIFrame.className = DT.CLASS_EDITOR_SHIM;
            elIFrame.tabIndex = -1;
            elIFrame.role = "presentation";
            elIFrame.title = "Presentational iframe shim";
            document.body.insertBefore(elIFrame, document.body.firstChild);
            this._elIFrame = elIFrame;
        }
    }
},

/**
 * Hides CellEditor UI at end of interaction.
 *
 * @method _hide
 */
_hide : function() {
    this.getContainerEl().style.display = "none";
    if(this._elIFrame) {
        this._elIFrame.style.display = "none";
    }
    this.isActive = false;
    this.getDataTable()._oCellEditor =  null;
},











/////////////////////////////////////////////////////////////////////////////
//
// Public properties
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Implementer defined function that can submit the input value to a server. This
 * function must accept the arguments fnCallback and oNewValue. When the submission
 * is complete, the function must also call fnCallback(bSuccess, oNewValue) to 
 * finish the save routine in the CellEditor. This function can also be used to 
 * perform extra validation or input value manipulation. 
 *
 * @property asyncSubmitter
 * @type HTMLFunction
 */
asyncSubmitter : null,

/**
 * Current value.
 *
 * @property value
 * @type MIXED
 */
value : null,

/**
 * Default value in case Record data is undefined. NB: Null values will not trigger
 * the default value.
 *
 * @property defaultValue
 * @type MIXED
 * @default null
 */
defaultValue : null,

/**
 * Validator function for input data, called from the DataTable instance scope,
 * receives the arguments (inputValue, currentValue, editorInstance) and returns
 * either the validated (or type-converted) value or undefined.
 *
 * @property validator
 * @type HTMLFunction
 * @default null
 */
validator : null,

/**
 * If validation is enabled, resets input field of invalid data.
 *
 * @property resetInvalidData
 * @type Boolean
 * @default true
 */
resetInvalidData : true,

/**
 * True if currently active.
 *
 * @property isActive
 * @type Boolean
 */
isActive : false,

/**
 * Text to display on Save button.
 *
 * @property LABEL_SAVE
 * @type HTML
 * @default "Save"
 */
LABEL_SAVE : "Save",

/**
 * Text to display on Cancel button.
 *
 * @property LABEL_CANCEL
 * @type HTML
 * @default "Cancel"
 */
LABEL_CANCEL : "Cancel",

/**
 * True if Save/Cancel buttons should not be displayed in the CellEditor.
 *
 * @property disableBtns
 * @type Boolean
 * @default false
 */
disableBtns : false,

/**
 * True if iframe shim for container element should be enabled.
 *
 * @property useIFrame
 * @type Boolean
 * @default false
 */
useIFrame : false,

/**
 * Custom CSS class or array of classes applied to the container element.
 *
 * @property className
 * @type String || String[]
 */
className : null,





/////////////////////////////////////////////////////////////////////////////
//
// Public methods
//
/////////////////////////////////////////////////////////////////////////////
/**
 * CellEditor instance name, for logging.
 *
 * @method toString
 * @return {String} Unique name of the CellEditor instance.
 */

toString : function() {
    return "CellEditor instance " + this._sId;
},

/**
 * CellEditor unique ID.
 *
 * @method getId
 * @return {String} Unique ID of the CellEditor instance.
 */

getId : function() {
    return this._sId;
},

/**
 * Returns reference to associated DataTable instance.
 *
 * @method getDataTable
 * @return {YAHOO.widget.DataTable} DataTable instance.
 */

getDataTable : function() {
    return this._oDataTable;
},

/**
 * Returns reference to associated Column instance.
 *
 * @method getColumn
 * @return {YAHOO.widget.Column} Column instance.
 */

getColumn : function() {
    return this._oColumn;
},

/**
 * Returns reference to associated Record instance.
 *
 * @method getRecord
 * @return {YAHOO.widget.Record} Record instance.
 */

getRecord : function() {
    return this._oRecord;
},



/**
 * Returns reference to associated TD element.
 *
 * @method getTdEl
 * @return {HTMLElement} TD element.
 */

getTdEl : function() {
    return this._elTd;
},

/**
 * Returns container element.
 *
 * @method getContainerEl
 * @return {HTMLElement} Reference to container element.
 */

getContainerEl : function() {
    return this._elContainer;
},

/**
 * Nulls out the entire CellEditor instance and related objects, removes attached
 * event listeners, and clears out DOM elements inside the container, removes
 * container from the DOM.
 *
 * @method destroy
 */
destroy : function() {
    this.unsubscribeAll();
    
    // Column is late-binding in attach()
    var oColumn = this.getColumn();
    if(oColumn) {
        oColumn.editor = null;
    }
    
    var elContainer = this.getContainerEl();
    if (elContainer) {
        Ev.purgeElement(elContainer, true);
        elContainer.parentNode.removeChild(elContainer);
    }
},

/**
 * Renders DOM elements and attaches event listeners.
 *
 * @method render
 */
render : function() {
    if (!this._needsRender) {
        return;
    }

    this._initContainerEl();
    this._initShimEl();

    // Handle ESC key
    Ev.addListener(this.getContainerEl(), "keydown", function(e, oSelf) {
        // ESC cancels Cell Editor
        if((e.keyCode == 27)) {
            var target = Ev.getTarget(e);
            // workaround for Mac FF3 bug that disabled clicks when ESC hit when
            // select is open. [bug 2273056]
            if (target.nodeName && target.nodeName.toLowerCase() === 'select') {
                target.blur();
            }
            oSelf.cancel();
        }
        // Pass through event
        oSelf.fireEvent("keydownEvent", {editor:oSelf, event:e});
    }, this);

    this.renderForm();

    // Show Save/Cancel buttons
    if(!this.disableBtns) {
        this.renderBtns();
    }
    
    this.doAfterRender();
    this._needsRender = false;
},

/**
 * Renders Save/Cancel buttons.
 *
 * @method renderBtns
 */
renderBtns : function() {
    // Buttons
    var elBtnsDiv = this.getContainerEl().appendChild(document.createElement("div"));
    elBtnsDiv.className = DT.CLASS_BUTTON;

    // Save button
    var elSaveBtn = elBtnsDiv.appendChild(document.createElement("button"));
    elSaveBtn.className = DT.CLASS_DEFAULT;
    elSaveBtn.innerHTML = this.LABEL_SAVE;
    Ev.addListener(elSaveBtn, "click", function(oArgs) {
        this.save();
    }, this, true);
    this._elSaveBtn = elSaveBtn;

    // Cancel button
    var elCancelBtn = elBtnsDiv.appendChild(document.createElement("button"));
    elCancelBtn.innerHTML = this.LABEL_CANCEL;
    Ev.addListener(elCancelBtn, "click", function(oArgs) {
        this.cancel();
    }, this, true);
    this._elCancelBtn = elCancelBtn;
},

/**
 * Attach CellEditor for a new interaction.
 *
 * @method attach
 * @param oDataTable {YAHOO.widget.DataTable} Associated DataTable instance.
 * @param elCell {HTMLElement} Cell to edit.  
 */
attach : function(oDataTable, elCell) {
    // Validate 
    if(oDataTable instanceof YAHOO.widget.DataTable) {
        this._oDataTable = oDataTable;
        
        // Validate cell
        elCell = oDataTable.getTdEl(elCell);
        if(elCell) {
            this._elTd = elCell;

            // Validate Column
            var oColumn = oDataTable.getColumn(elCell);
            if(oColumn) {
                this._oColumn = oColumn;
                
                // Validate Record
                var oRecord = oDataTable.getRecord(elCell);
                if(oRecord) {
                    this._oRecord = oRecord;
                    var value = oRecord.getData(this.getColumn().getField());
                    this.value = (value !== undefined) ? value : this.defaultValue;
                    return true;
                }
            }            
        }
    }
    YAHOO.log("Could not attach CellEditor","error",this.toString());
    return false;
},

/**
 * Moves container into position for display.
 *
 * @method move
 */
move : function() {
    // Move Editor
    var elContainer = this.getContainerEl(),
        elTd = this.getTdEl(),
        x = Dom.getX(elTd),
        y = Dom.getY(elTd);

    //TODO: remove scrolling logic
    // SF doesn't get xy for cells in scrolling table
    // when tbody display is set to block
    if(isNaN(x) || isNaN(y)) {
        var elTbody = this.getDataTable().getTbodyEl();
        x = elTd.offsetLeft + // cell pos relative to table
                Dom.getX(elTbody.parentNode) - // plus table pos relative to document
                elTbody.scrollLeft; // minus tbody scroll
        y = elTd.offsetTop + // cell pos relative to table
                Dom.getY(elTbody.parentNode) - // plus table pos relative to document
                elTbody.scrollTop + // minus tbody scroll
                this.getDataTable().getTheadEl().offsetHeight; // account for fixed THEAD cells
    }

    elContainer.style.left = x + "px";
    elContainer.style.top = y + "px";

    if(this._elIFrame) {
        this._elIFrame.style.left = x + "px";
        this._elIFrame.style.top = y + "px";
    }
},

/**
 * Displays CellEditor UI in the correct position.
 *
 * @method show
 */
show : function() {
    var elContainer = this.getContainerEl(),
        elIFrame = this._elIFrame;
    this.resetForm();
    this.isActive = true;
    elContainer.style.display = "";
    if(elIFrame) {
        elIFrame.style.width = elContainer.offsetWidth + "px";
        elIFrame.style.height = elContainer.offsetHeight + "px";
        elIFrame.style.display = "";
    }
    this.focus();
    this.fireEvent("showEvent", {editor:this});
    YAHOO.log("CellEditor shown", "info", this.toString()); 
},

/**
 * Fires blockEvent
 *
 * @method block
 */
block : function() {
    this.fireEvent("blockEvent", {editor:this});
    YAHOO.log("CellEditor blocked", "info", this.toString()); 
},

/**
 * Fires unblockEvent
 *
 * @method unblock
 */
unblock : function() {
    this.fireEvent("unblockEvent", {editor:this});
    YAHOO.log("CellEditor unblocked", "info", this.toString()); 
},

/**
 * Saves value of CellEditor and hides UI.
 *
 * @method save
 */
save : function() {
    // Get new value
    var inputValue = this.getInputValue();
    var validValue = inputValue;
    
    // Validate new value
    if(this.validator) {
        validValue = this.validator.call(this.getDataTable(), inputValue, this.value, this);
        if(validValue === undefined ) {
            if(this.resetInvalidData) {
                this.resetForm();
            }
            this.fireEvent("invalidDataEvent",
                    {editor:this, oldData:this.value, newData:inputValue});
            YAHOO.log("Could not save Cell Editor input due to invalid data " +
                    lang.dump(inputValue), "warn", this.toString());
            return;
        }
    }
        
    var oSelf = this;
    var finishSave = function(bSuccess, oNewValue) {
        var oOrigValue = oSelf.value;
        if(bSuccess) {
            // Update new value
            oSelf.value = oNewValue;
            oSelf.getDataTable().updateCell(oSelf.getRecord(), oSelf.getColumn(), oNewValue);
            
            // Hide CellEditor
            oSelf._hide();
            
            oSelf.fireEvent("saveEvent",
                    {editor:oSelf, oldData:oOrigValue, newData:oSelf.value});
            YAHOO.log("Cell Editor input saved", "info", this.toString());
        }
        else {
            oSelf.resetForm();
            oSelf.fireEvent("revertEvent",
                    {editor:oSelf, oldData:oOrigValue, newData:oNewValue});
            YAHOO.log("Could not save Cell Editor input " +
                    lang.dump(oNewValue), "warn", oSelf.toString());
        }
        oSelf.unblock();
    };
    
    this.block();
    if(lang.isFunction(this.asyncSubmitter)) {
        this.asyncSubmitter.call(this, finishSave, validValue);
    } 
    else {   
        finishSave(true, validValue);
    }
},

/**
 * Cancels CellEditor input and hides UI.
 *
 * @method cancel
 */
cancel : function() {
    if(this.isActive) {
        this._hide();
        this.fireEvent("cancelEvent", {editor:this});
        YAHOO.log("CellEditor canceled", "info", this.toString());
    }
    else {
        YAHOO.log("Unable to cancel CellEditor", "warn", this.toString());
    }
},

/**
 * Renders form elements.
 *
 * @method renderForm
 */
renderForm : function() {
    // To be implemented by subclass
},

/**
 * Access to add additional event listeners.
 *
 * @method doAfterRender
 */
doAfterRender : function() {
    // To be implemented by subclass
},


/**
 * After rendering form, if disabledBtns is set to true, then sets up a mechanism
 * to save input without them. 
 *
 * @method handleDisabledBtns
 */
handleDisabledBtns : function() {
    // To be implemented by subclass
},

/**
 * Resets CellEditor UI to initial state.
 *
 * @method resetForm
 */
resetForm : function() {
    // To be implemented by subclass
},

/**
 * Sets focus in CellEditor.
 *
 * @method focus
 */
focus : function() {
    // To be implemented by subclass
},

/**
 * Retrieves input value from CellEditor.
 *
 * @method getInputValue
 */
getInputValue : function() {
    // To be implemented by subclass
}

};

lang.augmentProto(BCE, util.EventProvider);


/////////////////////////////////////////////////////////////////////////////
//
// Custom Events
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Fired when a CellEditor is shown.
 *
 * @event showEvent
 * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance.
 */

/**
 * Fired when a CellEditor has a keydown.
 *
 * @event keydownEvent
 * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance. 
 * @param oArgs.event {HTMLEvent} The event object.
 */

/**
 * Fired when a CellEditor input is reverted due to invalid data.
 *
 * @event invalidDataEvent
 * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance. 
 * @param oArgs.newData {Object} New data value from form input field.
 * @param oArgs.oldData {Object} Old data value.
 */

/**
 * Fired when a CellEditor input is reverted due to asyncSubmitter failure.
 *
 * @event revertEvent
 * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance. 
 * @param oArgs.newData {Object} New data value from form input field.
 * @param oArgs.oldData {Object} Old data value.
 */

/**
 * Fired when a CellEditor input is saved.
 *
 * @event saveEvent
 * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance. 
 * @param oArgs.newData {Object} New data value from form input field.
 * @param oArgs.oldData {Object} Old data value.
 */

/**
 * Fired when a CellEditor input is canceled.
 *
 * @event cancelEvent
 * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance. 
 */

/**
 * Fired when a CellEditor has a blur event.
 *
 * @event blurEvent
 * @param oArgs.editor {YAHOO.widget.CellEditor} The CellEditor instance. 
 */














/****************************************************************************/
/****************************************************************************/
/****************************************************************************/
    
/**
 * The CheckboxCellEditor class provides functionality for inline editing
 * DataTable cell data with checkboxes.
 *
 * @namespace YAHOO.widget
 * @class CheckboxCellEditor
 * @extends YAHOO.widget.BaseCellEditor
 * @constructor
 * @param oConfigs {Object} (Optional) Object literal of configs.
 */
widget.CheckboxCellEditor = function(oConfigs) {
    oConfigs = oConfigs || {};
    this._sId = this._sId || Dom.generateId(null, "yui-checkboxceditor"); // "yui-checkboxceditor" + YAHOO.widget.BaseCellEditor._nCount++;
    YAHOO.widget.BaseCellEditor._nCount++;
    widget.CheckboxCellEditor.superclass.constructor.call(this, oConfigs.type || "checkbox", oConfigs);
};

// CheckboxCellEditor extends BaseCellEditor
lang.extend(widget.CheckboxCellEditor, BCE, {

/////////////////////////////////////////////////////////////////////////////
//
// CheckboxCellEditor public properties
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Array of checkbox values. Can either be a simple array (e.g., ["red","green","blue"])
 * or a an array of objects (e.g., [{label:"red", value:"#FF0000"},
 * {label:"green", value:"#00FF00"}, {label:"blue", value:"#0000FF"}]). String
 * values are treated as markup and inserted into the DOM as innerHTML.
 *
 * @property checkboxOptions
 * @type HTML[] | Object[]
 */
checkboxOptions : null,

/**
 * Reference to the checkbox elements.
 *
 * @property checkboxes
 * @type HTMLElement[] 
 */
checkboxes : null,

/**
 * Array of checked values
 *
 * @property value
 * @type String[] 
 */
value : null,

/////////////////////////////////////////////////////////////////////////////
//
// CheckboxCellEditor public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Render a form with input(s) type=checkbox.
 *
 * @method renderForm
 */
renderForm : function() {
    if(lang.isArray(this.checkboxOptions)) {
        var checkboxOption, checkboxValue, checkboxId, elLabel, j, len;
        
        // Create the checkbox buttons in an IE-friendly way...
        for(j=0,len=this.checkboxOptions.length; j<len; j++) {
            checkboxOption = this.checkboxOptions[j];
            checkboxValue = lang.isValue(checkboxOption.value) ?
                    checkboxOption.value : checkboxOption;

            checkboxId = this.getId() + "-chk" + j;
            this.getContainerEl().innerHTML += "<input type=\"checkbox\"" +
                    " id=\"" + checkboxId + "\"" + // Needed for label
                    " value=\"" + checkboxValue + "\" />";
            
            // Create the labels in an IE-friendly way
            elLabel = this.getContainerEl().appendChild(document.createElement("label"));
            elLabel.htmlFor = checkboxId;
            elLabel.innerHTML = lang.isValue(checkboxOption.label) ?
                    checkboxOption.label : checkboxOption;
        }
        
        // Store the reference to the checkbox elements
        var allCheckboxes = [];
        for(j=0; j<len; j++) {
            allCheckboxes[allCheckboxes.length] = this.getContainerEl().childNodes[j*2];
        }
        this.checkboxes = allCheckboxes;

        if(this.disableBtns) {
            this.handleDisabledBtns();
        }
    }
    else {
        YAHOO.log("Could not find checkboxOptions", "error", this.toString());
    }
},

/**
 * After rendering form, if disabledBtns is set to true, then sets up a mechanism
 * to save input without them. 
 *
 * @method handleDisabledBtns
 */
handleDisabledBtns : function() {
    Ev.addListener(this.getContainerEl(), "click", function(v){
        if(Ev.getTarget(v).tagName.toLowerCase() === "input") {
            // Save on blur
            this.save();
        }
    }, this, true);
},

/**
 * Resets CheckboxCellEditor UI to initial state.
 *
 * @method resetForm
 */
resetForm : function() {
    // Normalize to array
    var originalValues = lang.isArray(this.value) ? this.value : [this.value];
    
    // Match checks to value
    for(var i=0, j=this.checkboxes.length; i<j; i++) {
        this.checkboxes[i].checked = false;
        for(var k=0, len=originalValues.length; k<len; k++) {
            if(this.checkboxes[i].value == originalValues[k]) {
                this.checkboxes[i].checked = true;
            }
        }
    }
},

/**
 * Sets focus in CheckboxCellEditor.
 *
 * @method focus
 */
focus : function() {
    this.checkboxes[0].focus();
},

/**
 * Retrieves input value from CheckboxCellEditor.
 *
 * @method getInputValue
 */
getInputValue : function() {
    var checkedValues = [];
    for(var i=0, j=this.checkboxes.length; i<j; i++) {
        if(this.checkboxes[i].checked) {
            checkedValues[checkedValues.length] = this.checkboxes[i].value;
        }
    }  
    return checkedValues;
}

});

// Copy static members to CheckboxCellEditor class
lang.augmentObject(widget.CheckboxCellEditor, BCE);








/****************************************************************************/
/****************************************************************************/
/****************************************************************************/
    
/**
 * The DataCellEditor class provides functionality for inline editing
 * DataTable cell data with a YUI Calendar.
 *
 * @namespace YAHOO.widget
 * @class DateCellEditor
 * @extends YAHOO.widget.BaseCellEditor 
 * @constructor
 * @param oConfigs {Object} (Optional) Object literal of configs.
 */
widget.DateCellEditor = function(oConfigs) {
    oConfigs = oConfigs || {};
    this._sId = this._sId || Dom.generateId(null, "yui-dateceditor"); // "yui-dateceditor" + YAHOO.widget.BaseCellEditor._nCount++;
    YAHOO.widget.BaseCellEditor._nCount++;
    widget.DateCellEditor.superclass.constructor.call(this, oConfigs.type || "date", oConfigs);
};

// CheckboxCellEditor extends BaseCellEditor
lang.extend(widget.DateCellEditor, BCE, {

/////////////////////////////////////////////////////////////////////////////
//
// DateCellEditor public properties
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Reference to Calendar instance.
 *
 * @property calendar
 * @type YAHOO.widget.Calendar
 */
calendar : null,

/**
 * Configs for the calendar instance, to be passed to Calendar constructor.
 *
 * @property calendarOptions
 * @type Object
 */
calendarOptions : null,

/**
 * Default value.
 *
 * @property defaultValue
 * @type Date
 * @default new Date()
 */
defaultValue : new Date(),


/////////////////////////////////////////////////////////////////////////////
//
// DateCellEditor public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Render a Calendar.
 *
 * @method renderForm
 */
renderForm : function() {
    // Calendar widget
    if(YAHOO.widget.Calendar) {
        var calContainer = this.getContainerEl().appendChild(document.createElement("div"));
        calContainer.id = this.getId() + "-dateContainer"; // Needed for Calendar constructor
        var calendar =
                new YAHOO.widget.Calendar(this.getId() + "-date",
                calContainer.id, this.calendarOptions);
        calendar.render();
        calContainer.style.cssFloat = "none";
        
        // Bug 2528576
        calendar.hideEvent.subscribe(function() {this.cancel();}, this, true);

        if(ua.ie) {
            var calFloatClearer = this.getContainerEl().appendChild(document.createElement("div"));
            calFloatClearer.style.clear = "both";
        }
        
        this.calendar = calendar;

        if(this.disableBtns) {
            this.handleDisabledBtns();
        }
    }
    else {
        YAHOO.log("Could not find YUI Calendar", "error", this.toString());
    }
    
},

/**
 * After rendering form, if disabledBtns is set to true, then sets up a mechanism
 * to save input without them. 
 *
 * @method handleDisabledBtns
 */
handleDisabledBtns : function() {
    this.calendar.selectEvent.subscribe(function(v){
        // Save on select
        this.save();
    }, this, true);
},

/**
 * Resets DateCellEditor UI to initial state.
 *
 * @method resetForm
 */
resetForm : function() {
    var value = this.value || (new Date());
    this.calendar.select(value);
    this.calendar.cfg.setProperty("pagedate",value,false);
	this.calendar.render();
	// Bug 2528576
	this.calendar.show();
},

/**
 * Sets focus in DateCellEditor.
 *
 * @method focus
 */
focus : function() {
    // To be impmlemented by subclass
},

/**
 * Retrieves input value from DateCellEditor.
 *
 * @method getInputValue
 */
getInputValue : function() {
    return this.calendar.getSelectedDates()[0];
}

});

// Copy static members to DateCellEditor class
lang.augmentObject(widget.DateCellEditor, BCE);









/****************************************************************************/
/****************************************************************************/
/****************************************************************************/
    
/**
 * The DropdownCellEditor class provides functionality for inline editing
 * DataTable cell data a SELECT element.
 *
 * @namespace YAHOO.widget
 * @class DropdownCellEditor
 * @extends YAHOO.widget.BaseCellEditor 
 * @constructor
 * @param oConfigs {Object} (Optional) Object literal of configs.
 */
widget.DropdownCellEditor = function(oConfigs) {
    oConfigs = oConfigs || {};
    this._sId = this._sId || Dom.generateId(null, "yui-dropdownceditor"); // "yui-dropdownceditor" + YAHOO.widget.BaseCellEditor._nCount++;
    YAHOO.widget.BaseCellEditor._nCount++;
    widget.DropdownCellEditor.superclass.constructor.call(this, oConfigs.type || "dropdown", oConfigs);
};

// DropdownCellEditor extends BaseCellEditor
lang.extend(widget.DropdownCellEditor, BCE, {

/////////////////////////////////////////////////////////////////////////////
//
// DropdownCellEditor public properties
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Array of dropdown values. Can either be a simple array (e.g.,
 * ["Alabama","Alaska","Arizona","Arkansas"]) or a an array of objects (e.g., 
 * [{label:"Alabama", value:"AL"}, {label:"Alaska", value:"AK"},
 * {label:"Arizona", value:"AZ"}, {label:"Arkansas", value:"AR"}]). String
 * values are treated as markup and inserted into the DOM as innerHTML.
 *
 * @property dropdownOptions
 * @type HTML[] | Object[]
 */
dropdownOptions : null,

/**
 * Reference to Dropdown element.
 *
 * @property dropdown
 * @type HTMLElement
 */
dropdown : null,

/**
 * Enables multi-select.
 *
 * @property multiple
 * @type Boolean
 */
multiple : false,

/**
 * Specifies number of visible options.
 *
 * @property size
 * @type Number
 */
size : null,

/////////////////////////////////////////////////////////////////////////////
//
// DropdownCellEditor public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Render a form with select element.
 *
 * @method renderForm
 */
renderForm : function() {
    var elDropdown = this.getContainerEl().appendChild(document.createElement("select"));
    elDropdown.style.zoom = 1;
    if(this.multiple) {
        elDropdown.multiple = "multiple";
    }
    if(lang.isNumber(this.size)) {
        elDropdown.size = this.size;
    }
    this.dropdown = elDropdown;
    
    if(lang.isArray(this.dropdownOptions)) {
        var dropdownOption, elOption;
        for(var i=0, j=this.dropdownOptions.length; i<j; i++) {
            dropdownOption = this.dropdownOptions[i];
            elOption = document.createElement("option");
            elOption.value = (lang.isValue(dropdownOption.value)) ?
                    dropdownOption.value : dropdownOption;
            elOption.innerHTML = (lang.isValue(dropdownOption.label)) ?
                    dropdownOption.label : dropdownOption;
            elOption = elDropdown.appendChild(elOption);
        }
        
        if(this.disableBtns) {
            this.handleDisabledBtns();
        }
    }
},

/**
 * After rendering form, if disabledBtns is set to true, then sets up a mechanism
 * to save input without them. 
 *
 * @method handleDisabledBtns
 */
handleDisabledBtns : function() {
    // Save on blur for multi-select
    if(this.multiple) {
        Ev.addListener(this.dropdown, "blur", function(v){
            // Save on change
            this.save();
        }, this, true);
    }
    // Save on change for single-select
    else {
        if(!ua.ie) {
            Ev.addListener(this.dropdown, "change", function(v){
                // Save on change
                this.save();
            }, this, true);
        }
        else {
            // Bug 2529274: "change" event is not keyboard accessible in IE6
            Ev.addListener(this.dropdown, "blur", function(v){
                this.save();
            }, this, true);
            Ev.addListener(this.dropdown, "click", function(v){
                this.save();
            }, this, true);
        }
    }
},

/**
 * Resets DropdownCellEditor UI to initial state.
 *
 * @method resetForm
 */
resetForm : function() {
    var allOptions = this.dropdown.options,
        i=0, j=allOptions.length;

    // Look for multi-select selections
    if(lang.isArray(this.value)) {
        var allValues = this.value,
            m=0, n=allValues.length,
            hash = {};
        // Reset all selections and stash options in a value hash
        for(; i<j; i++) {
            allOptions[i].selected = false;
            hash[allOptions[i].value] = allOptions[i];
        }
        for(; m<n; m++) {
            if(hash[allValues[m]]) {
                hash[allValues[m]].selected = true;
            }
        }
    }
    // Only need to look for a single selection
    else {
        for(; i<j; i++) {
            if(this.value == allOptions[i].value) {
                allOptions[i].selected = true;
            }
        }
    }
},

/**
 * Sets focus in DropdownCellEditor.
 *
 * @method focus
 */
focus : function() {
    this.getDataTable()._focusEl(this.dropdown);
},

/**
 * Retrieves input value from DropdownCellEditor.
 *
 * @method getInputValue
 */
getInputValue : function() {
    var allOptions = this.dropdown.options;
    
    // Look for multiple selections
    if(this.multiple) {
        var values = [],
            i=0, j=allOptions.length;
        for(; i<j; i++) {
            if(allOptions[i].selected) {
                values.push(allOptions[i].value);
            }
        }
        return values;
    }
    // Only need to look for single selection
    else {
        return allOptions[allOptions.selectedIndex].value;
    }
}

});

// Copy static members to DropdownCellEditor class
lang.augmentObject(widget.DropdownCellEditor, BCE);






/****************************************************************************/
/****************************************************************************/
/****************************************************************************/
    
/**
 * The RadioCellEditor class provides functionality for inline editing
 * DataTable cell data with radio buttons.
 *
 * @namespace YAHOO.widget
 * @class RadioCellEditor
 * @extends YAHOO.widget.BaseCellEditor 
 * @constructor
 * @param oConfigs {Object} (Optional) Object literal of configs.
 */
widget.RadioCellEditor = function(oConfigs) {
    oConfigs = oConfigs || {};
    this._sId = this._sId || Dom.generateId(null, "yui-radioceditor"); // "yui-radioceditor" + YAHOO.widget.BaseCellEditor._nCount++;
    YAHOO.widget.BaseCellEditor._nCount++;
    widget.RadioCellEditor.superclass.constructor.call(this, oConfigs.type || "radio", oConfigs);
};

// RadioCellEditor extends BaseCellEditor
lang.extend(widget.RadioCellEditor, BCE, {

/////////////////////////////////////////////////////////////////////////////
//
// RadioCellEditor public properties
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Reference to radio elements.
 *
 * @property radios
 * @type HTMLElement[]
 */
radios : null,

/**
 * Array of radio values. Can either be a simple array (e.g., ["yes","no","maybe"])
 * or a an array of objects (e.g., [{label:"yes", value:1}, {label:"no", value:-1},
 * {label:"maybe", value:0}]). String values are treated as markup and inserted
 * into the DOM as innerHTML.
 *
 * @property radioOptions
 * @type HTML[] | Object[]
 */
radioOptions : null,

/////////////////////////////////////////////////////////////////////////////
//
// RadioCellEditor public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Render a form with input(s) type=radio.
 *
 * @method renderForm
 */
renderForm : function() {
    if(lang.isArray(this.radioOptions)) {
        var radioOption, radioValue, radioId, elLabel;
        
        // Create the radio buttons in an IE-friendly way
        for(var i=0, len=this.radioOptions.length; i<len; i++) {
            radioOption = this.radioOptions[i];
            radioValue = lang.isValue(radioOption.value) ?
                    radioOption.value : radioOption;
            radioId = this.getId() + "-radio" + i;
            this.getContainerEl().innerHTML += "<input type=\"radio\"" +
                    " name=\"" + this.getId() + "\"" +
                    " value=\"" + radioValue + "\"" +
                    " id=\"" +  radioId + "\" />"; // Needed for label
            
            // Create the labels in an IE-friendly way
            elLabel = this.getContainerEl().appendChild(document.createElement("label"));
            elLabel.htmlFor = radioId;
            elLabel.innerHTML = (lang.isValue(radioOption.label)) ?
                    radioOption.label : radioOption;
        }
        
        // Store the reference to the checkbox elements
        var allRadios = [],
            elRadio;
        for(var j=0; j<len; j++) {
            elRadio = this.getContainerEl().childNodes[j*2];
            allRadios[allRadios.length] = elRadio;
        }
        this.radios = allRadios;

        if(this.disableBtns) {
            this.handleDisabledBtns();
        }
    }
    else {
        YAHOO.log("Could not find radioOptions", "error", this.toString());
    }
},

/**
 * After rendering form, if disabledBtns is set to true, then sets up a mechanism
 * to save input without them. 
 *
 * @method handleDisabledBtns
 */
handleDisabledBtns : function() {
    Ev.addListener(this.getContainerEl(), "click", function(v){
        if(Ev.getTarget(v).tagName.toLowerCase() === "input") {
            // Save on blur
            this.save();
        }
    }, this, true);
},

/**
 * Resets RadioCellEditor UI to initial state.
 *
 * @method resetForm
 */
resetForm : function() {
    for(var i=0, j=this.radios.length; i<j; i++) {
        var elRadio = this.radios[i];
        if(this.value == elRadio.value) {
            elRadio.checked = true;
            return;
        }
    }
},

/**
 * Sets focus in RadioCellEditor.
 *
 * @method focus
 */
focus : function() {
    for(var i=0, j=this.radios.length; i<j; i++) {
        if(this.radios[i].checked) {
            this.radios[i].focus();
            return;
        }
    }
},

/**
 * Retrieves input value from RadioCellEditor.
 *
 * @method getInputValue
 */
getInputValue : function() {
    for(var i=0, j=this.radios.length; i<j; i++) {
        if(this.radios[i].checked) {
            return this.radios[i].value;
        }
    }
}

});

// Copy static members to RadioCellEditor class
lang.augmentObject(widget.RadioCellEditor, BCE);






/****************************************************************************/
/****************************************************************************/
/****************************************************************************/
    
/**
 * The TextareaCellEditor class provides functionality for inline editing
 * DataTable cell data with a TEXTAREA element.
 *
 * @namespace YAHOO.widget
 * @class TextareaCellEditor
 * @extends YAHOO.widget.BaseCellEditor 
 * @constructor
 * @param oConfigs {Object} (Optional) Object literal of configs.
 */
widget.TextareaCellEditor = function(oConfigs) {
    oConfigs = oConfigs || {};
    this._sId = this._sId || Dom.generateId(null, "yui-textareaceditor");// "yui-textareaceditor" + ;
    YAHOO.widget.BaseCellEditor._nCount++;
    widget.TextareaCellEditor.superclass.constructor.call(this, oConfigs.type || "textarea", oConfigs);
};

// TextareaCellEditor extends BaseCellEditor
lang.extend(widget.TextareaCellEditor, BCE, {

/////////////////////////////////////////////////////////////////////////////
//
// TextareaCellEditor public properties
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Reference to textarea element.
 *
 * @property textarea
 * @type HTMLElement
 */
textarea : null,


/////////////////////////////////////////////////////////////////////////////
//
// TextareaCellEditor public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Render a form with textarea.
 *
 * @method renderForm
 */
renderForm : function() {
    var elTextarea = this.getContainerEl().appendChild(document.createElement("textarea"));
    this.textarea = elTextarea;

    if(this.disableBtns) {
        this.handleDisabledBtns();
    }
},

/**
 * After rendering form, if disabledBtns is set to true, then sets up a mechanism
 * to save input without them. 
 *
 * @method handleDisabledBtns
 */
handleDisabledBtns : function() {
    Ev.addListener(this.textarea, "blur", function(v){
        // Save on blur
        this.save();
    }, this, true);        
},

/**
 * Moves TextareaCellEditor UI to a cell.
 *
 * @method move
 */
move : function() {
    this.textarea.style.width = this.getTdEl().offsetWidth + "px";
    this.textarea.style.height = "3em";
    YAHOO.widget.TextareaCellEditor.superclass.move.call(this);
},

/**
 * Resets TextareaCellEditor UI to initial state.
 *
 * @method resetForm
 */
resetForm : function() {
    this.textarea.value = this.value;
},

/**
 * Sets focus in TextareaCellEditor.
 *
 * @method focus
 */
focus : function() {
    // Bug 2303181, Bug 2263600
    this.getDataTable()._focusEl(this.textarea);
    this.textarea.select();
},

/**
 * Retrieves input value from TextareaCellEditor.
 *
 * @method getInputValue
 */
getInputValue : function() {
    return this.textarea.value;
}

});

// Copy static members to TextareaCellEditor class
lang.augmentObject(widget.TextareaCellEditor, BCE);









/****************************************************************************/
/****************************************************************************/
/****************************************************************************/
    
/**
 * The TextboxCellEditor class provides functionality for inline editing
 * DataTable cell data with an INPUT TYPE=TEXT element.
 *
 * @namespace YAHOO.widget
 * @class TextboxCellEditor
 * @extends YAHOO.widget.BaseCellEditor 
 * @constructor
 * @param oConfigs {Object} (Optional) Object literal of configs.
 */
widget.TextboxCellEditor = function(oConfigs) {
    oConfigs = oConfigs || {};
    this._sId = this._sId || Dom.generateId(null, "yui-textboxceditor");// "yui-textboxceditor" + YAHOO.widget.BaseCellEditor._nCount++;
    YAHOO.widget.BaseCellEditor._nCount++;
    widget.TextboxCellEditor.superclass.constructor.call(this, oConfigs.type || "textbox", oConfigs);
};

// TextboxCellEditor extends BaseCellEditor
lang.extend(widget.TextboxCellEditor, BCE, {

/////////////////////////////////////////////////////////////////////////////
//
// TextboxCellEditor public properties
//
/////////////////////////////////////////////////////////////////////////////
/**
 * Reference to the textbox element.
 *
 * @property textbox
 */
textbox : null,

/////////////////////////////////////////////////////////////////////////////
//
// TextboxCellEditor public methods
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Render a form with input type=text.
 *
 * @method renderForm
 */
renderForm : function() {
    var elTextbox;
    // Bug 1802582: SF3/Mac needs a form element wrapping the input
    if(ua.webkit>420) {
        elTextbox = this.getContainerEl().appendChild(document.createElement("form")).appendChild(document.createElement("input"));
    }
    else {
        elTextbox = this.getContainerEl().appendChild(document.createElement("input"));
    }
    elTextbox.type = "text";
    this.textbox = elTextbox;

    // Save on enter by default
    // Bug: 1802582 Set up a listener on each textbox to track on keypress
    // since SF/OP can't preventDefault on keydown
    Ev.addListener(elTextbox, "keypress", function(v){
        if((v.keyCode === 13)) {
            // Prevent form submit
            YAHOO.util.Event.preventDefault(v);
            this.save();
        }
    }, this, true);

    if(this.disableBtns) {
        // By default this is no-op since enter saves by default
        this.handleDisabledBtns();
    }
},

/**
 * Moves TextboxCellEditor UI to a cell.
 *
 * @method move
 */
move : function() {
    this.textbox.style.width = this.getTdEl().offsetWidth + "px";
    widget.TextboxCellEditor.superclass.move.call(this);
},

/**
 * Resets TextboxCellEditor UI to initial state.
 *
 * @method resetForm
 */
resetForm : function() {
    this.textbox.value = lang.isValue(this.value) ? this.value.toString() : "";
},

/**
 * Sets focus in TextboxCellEditor.
 *
 * @method focus
 */
focus : function() {
    // Bug 2303181, Bug 2263600
    this.getDataTable()._focusEl(this.textbox);
    this.textbox.select();
},

/**
 * Returns new value for TextboxCellEditor.
 *
 * @method getInputValue
 */
getInputValue : function() {
    return this.textbox.value;
}

});

// Copy static members to TextboxCellEditor class
lang.augmentObject(widget.TextboxCellEditor, BCE);







/////////////////////////////////////////////////////////////////////////////
//
// DataTable extension
//
/////////////////////////////////////////////////////////////////////////////

/**
 * CellEditor subclasses.
 * @property DataTable.Editors
 * @type Object
 * @static
 */
DT.Editors = {
    checkbox : widget.CheckboxCellEditor,
    "date"   : widget.DateCellEditor,
    dropdown : widget.DropdownCellEditor,
    radio    : widget.RadioCellEditor,
    textarea : widget.TextareaCellEditor,
    textbox  : widget.TextboxCellEditor
};

/****************************************************************************/
/****************************************************************************/
/****************************************************************************/
    
/**
 * Factory class for instantiating a BaseCellEditor subclass.
 *
 * @namespace YAHOO.widget
 * @class CellEditor
 * @extends YAHOO.widget.BaseCellEditor 
 * @constructor
 * @param sType {String} Type indicator, to map to YAHOO.widget.DataTable.Editors.
 * @param oConfigs {Object} (Optional) Object literal of configs.
 */
widget.CellEditor = function(sType, oConfigs) {
    // Point to one of the subclasses
    if(sType && DT.Editors[sType]) {
        lang.augmentObject(BCE, DT.Editors[sType]);
        return new DT.Editors[sType](oConfigs);
    }
    else {
        return new BCE(null, oConfigs);
    }
};

var CE = widget.CellEditor;

// Copy static members to CellEditor class
lang.augmentObject(CE, BCE);


})();

YAHOO.register("datatable", YAHOO.widget.DataTable, {version: "2.9.0", build: "2800"});
/*
Copyright (c) 2011, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 2.9.0
*/
(function () {
/**
 * The Paginator widget provides a set of controls to navigate through paged
 * data.
 *
 * @module paginator
 * @uses YAHOO.util.EventProvider
 * @uses YAHOO.util.AttributeProvider
 */

var Dom        = YAHOO.util.Dom,
    lang       = YAHOO.lang,
    isObject   = lang.isObject,
    isFunction = lang.isFunction,
    isArray    = lang.isArray,
    isString   = lang.isString;

/**
 * Instantiate a Paginator, passing a configuration object to the contructor.
 * The configuration object should contain the following properties:
 * <ul>
 *   <li>rowsPerPage : <em>n</em> (int)</li>
 *   <li>totalRecords : <em>n</em> (int or Paginator.VALUE_UNLIMITED)</li>
 *   <li>containers : <em>id | el | arr</em> (HTMLElement reference, its id, or an array of either)</li>
 * </ul>
 *
 * @namespace YAHOO.widget
 * @class Paginator
 * @constructor
 * @param config {Object} Object literal to set instance and ui component
 * configuration.
 */
function Paginator(config) {
    var UNLIMITED = Paginator.VALUE_UNLIMITED,
        attrib, initialPage, records, perPage, startIndex;

    config = isObject(config) ? config : {};

    this.initConfig();

    this.initEvents();

    // Set the basic config keys first
    this.set('rowsPerPage',config.rowsPerPage,true);
    if (Paginator.isNumeric(config.totalRecords)) {
        this.set('totalRecords',config.totalRecords,true);
    }
    
    this.initUIComponents();

    // Update the other config values
    for (attrib in config) {
        if (config.hasOwnProperty(attrib)) {
            this.set(attrib,config[attrib],true);
        }
    }

    // Calculate the initial record offset
    initialPage = this.get('initialPage');
    records     = this.get('totalRecords');
    perPage     = this.get('rowsPerPage');
    if (initialPage > 1 && perPage !== UNLIMITED) {
        startIndex = (initialPage - 1) * perPage;
        if (records === UNLIMITED || startIndex < records) {
            this.set('recordOffset',startIndex,true);
        }
    }
}


// Static members
lang.augmentObject(Paginator, {
    /**
     * Incrementing index used to give instances unique ids.
     * @static
     * @property Paginator.id
     * @type number
     * @private
     */
    id : 0,

    /**
     * Base of id strings used for ui components.
     * @static
     * @property Paginator.ID_BASE
     * @type string
     * @private
     */
    ID_BASE : 'yui-pg',

    /**
     * Used to identify unset, optional configurations, or used explicitly in
     * the case of totalRecords to indicate unlimited pagination.
     * @static
     * @property Paginator.VALUE_UNLIMITED
     * @type number
     * @final
     */
    VALUE_UNLIMITED : -1,

    /**
     * Default template used by Paginator instances.  Update this if you want
     * all new Paginators to use a different default template.
     * @static
     * @property Paginator.TEMPLATE_DEFAULT
     * @type string
     */
    TEMPLATE_DEFAULT : "{FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink}",

    /**
     * Common alternate pagination format, including page links, links for
     * previous, next, first and last pages as well as a rows-per-page
     * dropdown.  Offered as a convenience.
     * @static
     * @property Paginator.TEMPLATE_ROWS_PER_PAGE
     * @type string
     */
    TEMPLATE_ROWS_PER_PAGE : "{FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}",

    /**
     * Storage object for UI Components
     * @static
     * @property Paginator.ui
     */
    ui : {},

    /**
     * Similar to YAHOO.lang.isNumber, but allows numeric strings.  This is
     * is used for attribute validation in conjunction with getters that return
     * numbers.
     *
     * @method Paginator.isNumeric
     * @param v {Number|String} value to be checked for number or numeric string
     * @returns {Boolean} true if the input is coercable into a finite number
     * @static
     */
    isNumeric : function (v) {
        return isFinite(+v);
    },

    /**
     * Return a number or null from input
     *
     * @method Paginator.toNumber
     * @param n {Number|String} a number or numeric string
     * @return Number
     * @static
     */
    toNumber : function (n) {
        return isFinite(+n) ? +n : null;
    }

},true);


// Instance members and methods
Paginator.prototype = {

    // Instance members

    /**
     * Array of nodes in which to render pagination controls.  This is set via
     * the &quot;containers&quot; attribute.
     * @property _containers
     * @type Array(HTMLElement)
     * @private
     */
    _containers : [],

    /**
     * Flag used to indicate multiple attributes are being updated via setState
     * @property _batch
     * @type boolean
     * @protected
     */
    _batch : false,

    /**
     * Used by setState to indicate when a page change has occurred
     * @property _pageChanged
     * @type boolean
     * @protected
     */
    _pageChanged : false,

    /**
     * Temporary state cache used by setState to keep track of the previous
     * state for eventual pageChange event firing
     * @property _state
     * @type Object
     * @protected
     */
    _state : null,


    // Instance methods

    /**
     * Initialize the Paginator's attributes (see YAHOO.util.Element class
     * AttributeProvider).
     * @method initConfig
     * @private
     */
    initConfig : function () {

        var UNLIMITED = Paginator.VALUE_UNLIMITED;

        /**
         * REQUIRED. Number of records constituting a &quot;page&quot;
         * @attribute rowsPerPage
         * @type integer
         */
        this.setAttributeConfig('rowsPerPage', {
            value     : 0,
            validator : Paginator.isNumeric,
            setter    : Paginator.toNumber
        });

        /**
         * REQUIRED. Node references or ids of nodes in which to render the
         * pagination controls.
         * @attribute containers
         * @type {string|HTMLElement|Array(string|HTMLElement)}
         */
        this.setAttributeConfig('containers', {
            value     : null,
            validator : function (val) {
                if (!isArray(val)) {
                    val = [val];
                }
                for (var i = 0, len = val.length; i < len; ++i) {
                    if (isString(val[i]) || 
                        (isObject(val[i]) && val[i].nodeType === 1)) {
                        continue;
                    }
                    return false;
                }
                return true;
            },
            method : function (val) {
                val = Dom.get(val);
                if (!isArray(val)) {
                    val = [val];
                }
                this._containers = val;
            }
        });

        /**
         * Total number of records to paginate through
         * @attribute totalRecords
         * @type integer
         * @default 0
         */
        this.setAttributeConfig('totalRecords', {
            value     : 0,
            validator : Paginator.isNumeric,
            setter    : Paginator.toNumber
        });

        /**
         * Zero based index of the record considered first on the current page.
         * For page based interactions, don't modify this attribute directly;
         * use setPage(n).
         * @attribute recordOffset
         * @type integer
         * @default 0
         */
        this.setAttributeConfig('recordOffset', {
            value     : 0,
            validator : function (val) {
                var total = this.get('totalRecords');
                if (Paginator.isNumeric(val)) {
                    val = +val;
                    return total === UNLIMITED || total > val ||
                           (total === 0 && val === 0);
                }

                return false;
            },
            setter    : Paginator.toNumber
        });

        /**
         * Page to display on initial paint
         * @attribute initialPage
         * @type integer
         * @default 1
         */
        this.setAttributeConfig('initialPage', {
            value     : 1,
            validator : Paginator.isNumeric,
            setter    : Paginator.toNumber
        });

        /**
         * Template used to render controls.  The string will be used as
         * innerHTML on all specified container nodes.  Bracketed keys
         * (e.g. {pageLinks}) in the string will be replaced with an instance
         * of the so named ui component.
         * @see Paginator.TEMPLATE_DEFAULT
         * @see Paginator.TEMPLATE_ROWS_PER_PAGE
         * @attribute template
         * @type string
         */
        this.setAttributeConfig('template', {
            value : Paginator.TEMPLATE_DEFAULT,
            validator : isString
        });

        /**
         * Class assigned to the element(s) containing pagination controls.
         * @attribute containerClass
         * @type string
         * @default 'yui-pg-container'
         */
        this.setAttributeConfig('containerClass', {
            value : 'yui-pg-container',
            validator : isString
        });

        /**
         * Display pagination controls even when there is only one page.  Set
         * to false to forgo rendering and/or hide the containers when there
         * is only one page of data.  Note if you are using the rowsPerPage
         * dropdown ui component, visibility will be maintained as long as the
         * number of records exceeds the smallest page size.
         * @attribute alwaysVisible
         * @type boolean
         * @default true
         */
        this.setAttributeConfig('alwaysVisible', {
            value : true,
            validator : lang.isBoolean
        });

        /**
         * Update the UI immediately upon interaction.  If false, changeRequest
         * subscribers or other external code will need to explicitly set the
         * new values in the paginator to trigger repaint.
         * @attribute updateOnChange
         * @type boolean
         * @default false
         * @deprecated use changeRequest listener that calls setState
         */
        this.setAttributeConfig('updateOnChange', {
            value     : false,
            validator : lang.isBoolean
        });



        // Read only attributes

        /**
         * Unique id assigned to this instance
         * @attribute id
         * @type integer
         * @final
         */
        this.setAttributeConfig('id', {
            value    : Paginator.id++,
            readOnly : true
        });

        /**
         * Indicator of whether the DOM nodes have been initially created
         * @attribute rendered
         * @type boolean
         * @final
         */
        this.setAttributeConfig('rendered', {
            value    : false,
            readOnly : true
        });

    },

    /**
     * Initialize registered ui components onto this instance.
     * @method initUIComponents
     * @private
     */
    initUIComponents : function () {
        var ui = Paginator.ui,
            name,UIComp;
        for (name in ui) {
            if (ui.hasOwnProperty(name)) {
                UIComp = ui[name];
                if (isObject(UIComp) && isFunction(UIComp.init)) {
                    UIComp.init(this);
                }
            }
        }
    },

    /**
     * Initialize this instance's CustomEvents.
     * @method initEvents
     * @private
     */
    initEvents : function () {
        /**
         * Event fired when the Paginator is initially rendered
         * @event render
         */
        this.createEvent('render');

        /**
         * Event fired when the Paginator is initially rendered
         * @event rendered
         * @deprecated use render event
         */
        this.createEvent('rendered'); // backward compatibility

        /**
         * Event fired when a change in pagination values is requested,
         * either by interacting with the various ui components or via the
         * setStartIndex(n) etc APIs.
         * Subscribers will receive the proposed state as the first parameter.
         * The proposed state object will contain the following keys:
         * <ul>
         *   <li>paginator - the Paginator instance</li>
         *   <li>page</li>
         *   <li>totalRecords</li>
         *   <li>recordOffset - index of the first record on the new page</li>
         *   <li>rowsPerPage</li>
         *   <li>records - array containing [start index, end index] for the records on the new page</li>
         *   <li>before - object literal with all these keys for the current state</li>
         * </ul>
         * @event changeRequest
         */
        this.createEvent('changeRequest');

        /**
         * Event fired when attribute changes have resulted in the calculated
         * current page changing.
         * @event pageChange
         */
        this.createEvent('pageChange');

        /**
         * Event that fires before the destroy event.
         * @event beforeDestroy
         */
        this.createEvent('beforeDestroy');

        /**
         * Event used to trigger cleanup of ui components
         * @event destroy
         */
        this.createEvent('destroy');

        this._selfSubscribe();
    },

    /**
     * Subscribes to instance attribute change events to automate certain
     * behaviors.
     * @method _selfSubscribe
     * @protected
     */
    _selfSubscribe : function () {
        // Listen for changes to totalRecords and alwaysVisible 
        this.subscribe('totalRecordsChange',this.updateVisibility,this,true);
        this.subscribe('alwaysVisibleChange',this.updateVisibility,this,true);

        // Fire the pageChange event when appropriate
        this.subscribe('totalRecordsChange',this._handleStateChange,this,true);
        this.subscribe('recordOffsetChange',this._handleStateChange,this,true);
        this.subscribe('rowsPerPageChange',this._handleStateChange,this,true);

        // Update recordOffset when totalRecords is reduced below
        this.subscribe('totalRecordsChange',this._syncRecordOffset,this,true);
    },

    /**
     * Sets recordOffset to the starting index of the previous page when
     * totalRecords is reduced below the current recordOffset.
     * @method _syncRecordOffset
     * @param e {Event} totalRecordsChange event
     * @protected
     */
    _syncRecordOffset : function (e) {
        var v = e.newValue,rpp,state;
        if (e.prevValue !== v) {
            if (v !== Paginator.VALUE_UNLIMITED) {
                rpp = this.get('rowsPerPage');

                if (rpp && this.get('recordOffset') >= v) {
                    state = this.getState({
                        totalRecords : e.prevValue,
                        recordOffset : this.get('recordOffset')
                    });

                    this.set('recordOffset', state.before.recordOffset);
                    this._firePageChange(state);
                }
            }
        }
    },

    /**
     * Fires the pageChange event when the state attributes have changed in
     * such a way as to locate the current recordOffset on a new page.
     * @method _handleStateChange
     * @param e {Event} the attribute change event
     * @protected
     */
    _handleStateChange : function (e) {
        if (e.prevValue !== e.newValue) {
            var change = this._state || {},
                state;

            change[e.type.replace(/Change$/,'')] = e.prevValue;
            state = this.getState(change);

            if (state.page !== state.before.page) {
                if (this._batch) {
                    this._pageChanged = true;
                } else {
                    this._firePageChange(state);
                }
            }
        }
    },

    /**
     * Fires a pageChange event in the form of a standard attribute change
     * event with additional properties prevState and newState.
     * @method _firePageChange
     * @param state {Object} the result of getState(oldState)
     * @protected
     */
    _firePageChange : function (state) {
        if (isObject(state)) {
            var current = state.before;
            delete state.before;
            this.fireEvent('pageChange',{
                type      : 'pageChange',
                prevValue : state.page,
                newValue  : current.page,
                prevState : state,
                newState  : current
            });
        }
    },

    /**
     * Render the pagination controls per the format attribute into the
     * specified container nodes.
     * @method render
     * @return the Paginator instance
     * @chainable
     */
    render : function () {
        if (this.get('rendered')) {
            return this;
        }

        var template = this.get('template'),
            state    = this.getState(),
            // ex. yui-pg0-1 (first paginator, second container)
            id_base  = Paginator.ID_BASE + this.get('id') + '-',
            i, len;

        // Assemble the containers, keeping them hidden
        for (i = 0, len = this._containers.length; i < len; ++i) {
            this._renderTemplate(this._containers[i],template,id_base+i,true);
        }

        // Show the containers if appropriate
        this.updateVisibility();

        // Set render attribute manually to support its readOnly contract
        if (this._containers.length) {
            this.setAttributeConfig('rendered', { value: true });

            this.fireEvent('render', state);
            // For backward compatibility
            this.fireEvent('rendered', state);
        }

        return this;
    },

    /**
     * Creates the individual ui components and renders them into a container.
     *
     * @method _renderTemplate
     * @param container {HTMLElement} where to add the ui components
     * @param template {String} the template to use as a guide for rendering
     * @param id_base {String} id base for the container's ui components
     * @param hide {Boolean} leave the container hidden after assembly
     * @protected
     */
    _renderTemplate : function (container, template, id_base, hide) {
        var containerClass = this.get('containerClass'),
            markers, i, len;

        if (!container) {
            return;
        }

        // Hide the container while its contents are rendered
        Dom.setStyle(container,'display','none');

        Dom.addClass(container, containerClass);

        // Place the template innerHTML, adding marker spans to the template
        // html to indicate drop zones for ui components
        container.innerHTML = template.replace(/\{([a-z0-9_ \-]+)\}/gi,
            '<span class="yui-pg-ui yui-pg-ui-$1"></span>');

        // Replace each marker with the ui component's render() output
        markers = Dom.getElementsByClassName('yui-pg-ui','span',container);

        for (i = 0, len = markers.length; i < len; ++i) {
            this.renderUIComponent(markers[i], id_base);
        }

        if (!hide) {
            // Show the container allowing page reflow
            Dom.setStyle(container,'display','');
        }
    },

    /**
     * Replaces a marker node with a rendered UI component, determined by the
     * yui-pg-ui-(UI component class name) in the marker's className. e.g.
     * yui-pg-ui-PageLinks => new YAHOO.widget.Paginator.ui.PageLinks(this)
     *
     * @method renderUIComponent
     * @param marker {HTMLElement} the marker node to replace
     * @param id_base {String} string base the component's generated id
     * @return the Paginator instance
     * @chainable
     */
    renderUIComponent : function (marker, id_base) {
        var par    = marker.parentNode,
            name   = /yui-pg-ui-(\w+)/.exec(marker.className),
            UIComp = name && Paginator.ui[name[1]],
            comp;

        if (isFunction(UIComp)) {
            comp = new UIComp(this);
            if (isFunction(comp.render)) {
                par.replaceChild(comp.render(id_base),marker);
            }
        }

        return this;
    },

    /**
     * Removes controls from the page and unhooks events.
     * @method destroy
     */
    destroy : function () {
        this.fireEvent('beforeDestroy');
        this.fireEvent('destroy');

        this.setAttributeConfig('rendered',{value:false});
        this.unsubscribeAll();
    },

    /**
     * Hides the containers if there is only one page of data and attribute
     * alwaysVisible is false.  Conversely, it displays the containers if either
     * there is more than one page worth of data or alwaysVisible is turned on.
     * @method updateVisibility
     */
    updateVisibility : function (e) {
        var alwaysVisible = this.get('alwaysVisible'),
            totalRecords, visible, rpp, rppOptions, i, len, opt;

        if (!e || e.type === 'alwaysVisibleChange' || !alwaysVisible) {
            totalRecords = this.get('totalRecords');
            visible      = true;
            rpp          = this.get('rowsPerPage');
            rppOptions   = this.get('rowsPerPageOptions');

            if (isArray(rppOptions)) {
                for (i = 0, len = rppOptions.length; i < len; ++i) {
                    opt = rppOptions[i];
                    // account for value 'all'
                    if (lang.isNumber(opt || opt.value)) {
                        rpp = Math.min(rpp, (opt.value || opt));
                    }
                }
            }

            if (totalRecords !== Paginator.VALUE_UNLIMITED &&
                totalRecords <= rpp) {
                visible = false;
            }

            visible = visible || alwaysVisible;

            for (i = 0, len = this._containers.length; i < len; ++i) {
                Dom.setStyle(this._containers[i],'display',
                    visible ? '' : 'none');
            }
        }
    },




    /**
     * Get the configured container nodes
     * @method getContainerNodes
     * @return {Array} array of HTMLElement nodes
     */
    getContainerNodes : function () {
        return this._containers;
    },

    /**
     * Get the total number of pages in the data set according to the current
     * rowsPerPage and totalRecords values.  If totalRecords is not set, or
     * set to YAHOO.widget.Paginator.VALUE_UNLIMITED, returns
     * YAHOO.widget.Paginator.VALUE_UNLIMITED.
     * @method getTotalPages
     * @return {number}
     */
    getTotalPages : function () {
        var records = this.get('totalRecords'),
            perPage = this.get('rowsPerPage');

        // rowsPerPage not set.  Can't calculate
        if (!perPage) {
            return null;
        }

        if (records === Paginator.VALUE_UNLIMITED) {
            return Paginator.VALUE_UNLIMITED;
        }

        return Math.ceil(records/perPage);
    },

    /**
     * Does the requested page have any records?
     * @method hasPage
     * @param page {number} the page in question
     * @return {boolean}
     */
    hasPage : function (page) {
        if (!lang.isNumber(page) || page < 1) {
            return false;
        }

        var totalPages = this.getTotalPages();

        return (totalPages === Paginator.VALUE_UNLIMITED || totalPages >= page);
    },

    /**
     * Get the page number corresponding to the current record offset.
     * @method getCurrentPage
     * @return {number}
     */
    getCurrentPage : function () {
        var perPage = this.get('rowsPerPage');
        if (!perPage || !this.get('totalRecords')) {
            return 0;
        }
        return Math.floor(this.get('recordOffset') / perPage) + 1;
    },

    /**
     * Are there records on the next page?
     * @method hasNextPage
     * @return {boolean}
     */
    hasNextPage : function () {
        var currentPage = this.getCurrentPage(),
            totalPages  = this.getTotalPages();

        return currentPage && (totalPages === Paginator.VALUE_UNLIMITED || currentPage < totalPages);
    },

    /**
     * Get the page number of the next page, or null if the current page is the
     * last page.
     * @method getNextPage
     * @return {number}
     */
    getNextPage : function () {
        return this.hasNextPage() ? this.getCurrentPage() + 1 : null;
    },

    /**
     * Is there a page before the current page?
     * @method hasPreviousPage
     * @return {boolean}
     */
    hasPreviousPage : function () {
        return (this.getCurrentPage() > 1);
    },

    /**
     * Get the page number of the previous page, or null if the current page
     * is the first page.
     * @method getPreviousPage
     * @return {number}
     */
    getPreviousPage : function () {
        return (this.hasPreviousPage() ? this.getCurrentPage() - 1 : 1);
    },

    /**
     * Get the start and end record indexes of the specified page.
     * @method getPageRecords
     * @param page {number} (optional) The page (current page if not specified)
     * @return {Array} [start_index, end_index]
     */
    getPageRecords : function (page) {
        if (!lang.isNumber(page)) {
            page = this.getCurrentPage();
        }

        var perPage = this.get('rowsPerPage'),
            records = this.get('totalRecords'),
            start, end;

        if (!page || !perPage) {
            return null;
        }

        start = (page - 1) * perPage;
        if (records !== Paginator.VALUE_UNLIMITED) {
            if (start >= records) {
                return null;
            }
            end = Math.min(start + perPage, records) - 1;
        } else {
            end = start + perPage - 1;
        }

        return [start,end];
    },

    /**
     * Set the current page to the provided page number if possible.
     * @method setPage
     * @param newPage {number} the new page number
     * @param silent {boolean} whether to forcibly avoid firing the
     * changeRequest event
     */
    setPage : function (page,silent) {
        if (this.hasPage(page) && page !== this.getCurrentPage()) {
            if (this.get('updateOnChange') || silent) {
                this.set('recordOffset', (page - 1) * this.get('rowsPerPage'));
            } else {
                this.fireEvent('changeRequest',this.getState({'page':page}));
            }
        }
    },

    /**
     * Get the number of rows per page.
     * @method getRowsPerPage
     * @return {number} the current setting of the rowsPerPage attribute
     */
    getRowsPerPage : function () {
        return this.get('rowsPerPage');
    },

    /**
     * Set the number of rows per page.
     * @method setRowsPerPage
     * @param rpp {number} the new number of rows per page
     * @param silent {boolean} whether to forcibly avoid firing the
     * changeRequest event
     */
    setRowsPerPage : function (rpp,silent) {
        if (Paginator.isNumeric(rpp) && +rpp > 0 &&
            +rpp !== this.get('rowsPerPage')) {
            if (this.get('updateOnChange') || silent) {
                this.set('rowsPerPage',rpp);
            } else {
                this.fireEvent('changeRequest',
                    this.getState({'rowsPerPage':+rpp}));
            }
        }
    },

    /**
     * Get the total number of records.
     * @method getTotalRecords
     * @return {number} the current setting of totalRecords attribute
     */
    getTotalRecords : function () {
        return this.get('totalRecords');
    },

    /**
     * Set the total number of records.
     * @method setTotalRecords
     * @param total {number} the new total number of records
     * @param silent {boolean} whether to forcibly avoid firing the changeRequest event
     */
    setTotalRecords : function (total,silent) {
        if (Paginator.isNumeric(total) && +total >= 0 &&
            +total !== this.get('totalRecords')) {
            if (this.get('updateOnChange') || silent) {
                this.set('totalRecords',total);
            } else {
                this.fireEvent('changeRequest',
                    this.getState({'totalRecords':+total}));
            }
        }
    },

    /**
     * Get the index of the first record on the current page
     * @method getStartIndex
     * @return {number} the index of the first record on the current page
     */
    getStartIndex : function () {
        return this.get('recordOffset');
    },

    /**
     * Move the record offset to a new starting index.  This will likely cause
     * the calculated current page to change.  You should probably use setPage.
     * @method setStartIndex
     * @param offset {number} the new record offset
     * @param silent {boolean} whether to forcibly avoid firing the changeRequest event
     */
    setStartIndex : function (offset,silent) {
        if (Paginator.isNumeric(offset) && +offset >= 0 &&
            +offset !== this.get('recordOffset')) {
            if (this.get('updateOnChange') || silent) {
                this.set('recordOffset',offset);
            } else {
                this.fireEvent('changeRequest',
                    this.getState({'recordOffset':+offset}));
            }
        }
    },

    /**
     * Get an object literal describing the current state of the paginator.  If
     * an object literal of proposed values is passed, the proposed state will
     * be returned as an object literal with the following keys:
     * <ul>
     * <li>paginator - instance of the Paginator</li>
     * <li>page - number</li>
     * <li>totalRecords - number</li>
     * <li>recordOffset - number</li>
     * <li>rowsPerPage - number</li>
     * <li>records - [ start_index, end_index ]</li>
     * <li>before - (OPTIONAL) { state object literal for current state }</li>
     * </ul>
     * @method getState
     * @return {object}
     * @param changes {object} OPTIONAL object literal with proposed values
     * Supported change keys include:
     * <ul>
     * <li>rowsPerPage</li>
     * <li>totalRecords</li>
     * <li>recordOffset OR</li>
     * <li>page</li>
     * </ul>
     */
    getState : function (changes) {
        var UNLIMITED = Paginator.VALUE_UNLIMITED,
            M = Math, max = M.max, ceil = M.ceil,
            currentState, state, offset;

        function normalizeOffset(offset,total,rpp) {
            if (offset <= 0 || total === 0) {
                return 0;
            }
            if (total === UNLIMITED || total > offset) {
                return offset - (offset % rpp);
            }
            return total - (total % rpp || rpp);
        }

        currentState = {
            paginator    : this,
            totalRecords : this.get('totalRecords'),
            rowsPerPage  : this.get('rowsPerPage'),
            records      : this.getPageRecords()
        };
        currentState.recordOffset = normalizeOffset(
                                        this.get('recordOffset'),
                                        currentState.totalRecords,
                                        currentState.rowsPerPage);
        currentState.page = ceil(currentState.recordOffset /
                                 currentState.rowsPerPage) + 1;

        if (!changes) {
            return currentState;
        }

        state = {
            paginator    : this,
            before       : currentState,

            rowsPerPage  : changes.rowsPerPage || currentState.rowsPerPage,
            totalRecords : (Paginator.isNumeric(changes.totalRecords) ?
                                max(changes.totalRecords,UNLIMITED) :
                                +currentState.totalRecords)
        };

        if (state.totalRecords === 0) {
            state.recordOffset =
            state.page         = 0;
        } else {
            offset = Paginator.isNumeric(changes.page) ?
                        (changes.page - 1) * state.rowsPerPage :
                        Paginator.isNumeric(changes.recordOffset) ?
                            +changes.recordOffset :
                            currentState.recordOffset;

            state.recordOffset = normalizeOffset(offset,
                                    state.totalRecords,
                                    state.rowsPerPage);

            state.page = ceil(state.recordOffset / state.rowsPerPage) + 1;
        }

        state.records = [ state.recordOffset,
                          state.recordOffset + state.rowsPerPage - 1 ];

        // limit upper index to totalRecords - 1
        if (state.totalRecords !== UNLIMITED &&
            state.recordOffset < state.totalRecords && state.records &&
            state.records[1] > state.totalRecords - 1) {
            state.records[1] = state.totalRecords - 1;
        }

        return state;
    },

    /**
     * Convenience method to facilitate setting state attributes rowsPerPage,
     * totalRecords, recordOffset in batch.  Also supports calculating
     * recordOffset from state.page if state.recordOffset is not provided.
     * Fires only a single pageChange event, if appropriate.
     * This will not fire a changeRequest event.
     * @method setState
     * @param state {Object} Object literal of attribute:value pairs to set
     */
    setState : function (state) {
        if (isObject(state)) {
            // get flux state based on current state with before state as well
            this._state = this.getState({});

            // use just the state props from the input obj
            state = {
                page         : state.page,
                rowsPerPage  : state.rowsPerPage,
                totalRecords : state.totalRecords,
                recordOffset : state.recordOffset
            };

            // calculate recordOffset from page if recordOffset not specified.
            // not using lang.isNumber for support of numeric strings
            if (state.page && state.recordOffset === undefined) {
                state.recordOffset = (state.page - 1) *
                    (state.rowsPerPage || this.get('rowsPerPage'));
            }

            this._batch = true;
            this._pageChanged = false;

            for (var k in state) {
                if (state.hasOwnProperty(k) && this._configs.hasOwnProperty(k)) {
                    this.set(k,state[k]);
                }
            }

            this._batch = false;
            
            if (this._pageChanged) {
                this._pageChanged = false;

                this._firePageChange(this.getState(this._state));
            }
        }
    }
};

lang.augmentProto(Paginator, YAHOO.util.AttributeProvider);

YAHOO.widget.Paginator = Paginator;
})();
(function () {

var Paginator = YAHOO.widget.Paginator,
    l         = YAHOO.lang,
    setId     = YAHOO.util.Dom.generateId;

/**
 * ui Component to generate the textual report of current pagination status.
 * E.g. "Now viewing page 1 of 13".
 *
 * @namespace YAHOO.widget.Paginator.ui
 * @class CurrentPageReport
 * @for YAHOO.widget.Paginator
 *
 * @constructor
 * @param p {Pagintor} Paginator instance to attach to
 */
Paginator.ui.CurrentPageReport = function (p) {
    this.paginator = p;

    p.subscribe('recordOffsetChange', this.update,this,true);
    p.subscribe('rowsPerPageChange', this.update,this,true);
    p.subscribe('totalRecordsChange',this.update,this,true);
    p.subscribe('pageReportTemplateChange', this.update,this,true);
    p.subscribe('destroy',this.destroy,this,true);

    //TODO: make this work
    p.subscribe('pageReportClassChange', this.update,this,true);
};

/**
 * Decorates Paginator instances with new attributes. Called during
 * Paginator instantiation.
 * @method init
 * @param p {Paginator} Paginator instance to decorate
 * @static
 */
Paginator.ui.CurrentPageReport.init = function (p) {

    /**
     * CSS class assigned to the span containing the info.
     * @attribute pageReportClass
     * @default 'yui-pg-current'
     */
    p.setAttributeConfig('pageReportClass', {
        value : 'yui-pg-current',
        validator : l.isString
    });

    /**
     * Used as innerHTML for the span.  Place holders in the form of {name}
     * will be replaced with the so named value from the key:value map
     * generated by the function held in the pageReportValueGenerator attribute.
     * @attribute pageReportTemplate
     * @default '({currentPage} of {totalPages})'
     * @see pageReportValueGenerator attribute
     */
    p.setAttributeConfig('pageReportTemplate', {
        value : '({currentPage} of {totalPages})',
        validator : l.isString
    });

    /**
     * Function to generate the value map used to populate the
     * pageReportTemplate.  The function is passed the Paginator instance as a
     * parameter.  The default function returns a map with the following keys:
     * <ul>
     * <li>currentPage</li>
     * <li>totalPages</li>
     * <li>startIndex</li>
     * <li>endIndex</li>
     * <li>startRecord</li>
     * <li>endRecord</li>
     * <li>totalRecords</li>
     * </ul>
     * @attribute pageReportValueGenarator
     */
    p.setAttributeConfig('pageReportValueGenerator', {
        value : function (paginator) {
            var curPage = paginator.getCurrentPage(),
                records = paginator.getPageRecords();

            return {
                'currentPage' : records ? curPage : 0,
                'totalPages'  : paginator.getTotalPages(),
                'startIndex'  : records ? records[0] : 0,
                'endIndex'    : records ? records[1] : 0,
                'startRecord' : records ? records[0] + 1 : 0,
                'endRecord'   : records ? records[1] + 1 : 0,
                'totalRecords': paginator.get('totalRecords')
            };
        },
        validator : l.isFunction
    });
};

/**
 * Replace place holders in a string with the named values found in an
 * object literal.
 * @static
 * @method sprintf
 * @param template {string} The content string containing place holders
 * @param values {object} The key:value pairs used to replace the place holders
 * @return {string}
 */
Paginator.ui.CurrentPageReport.sprintf = function (template, values) {
    return template.replace(/\{([\w\s\-]+)\}/g, function (x,key) {
            return (key in values) ? values[key] : '';
        });
};

Paginator.ui.CurrentPageReport.prototype = {

    /**
     * Span node containing the formatted info
     * @property span
     * @type HTMLElement
     * @private
     */
    span : null,


    /**
     * Generate the span containing info formatted per the pageReportTemplate
     * attribute.
     * @method render
     * @param id_base {string} used to create unique ids for generated nodes
     * @return {HTMLElement}
     */
    render : function (id_base) {
        this.span = document.createElement('span');
        this.span.className = this.paginator.get('pageReportClass');
        setId(this.span, id_base + '-page-report');
        this.update();
        
        return this.span;
    },
    
    /**
     * Regenerate the content of the span if appropriate. Calls
     * CurrentPageReport.sprintf with the value of the pageReportTemplate
     * attribute and the value map returned from pageReportValueGenerator
     * function.
     * @method update
     * @param e {CustomEvent} The calling change event
     */
    update : function (e) {
        if (e && e.prevValue === e.newValue) {
            return;
        }

        this.span.innerHTML = Paginator.ui.CurrentPageReport.sprintf(
            this.paginator.get('pageReportTemplate'),
            this.paginator.get('pageReportValueGenerator')(this.paginator));
    },

    /**
     * Removes the link/span node and clears event listeners
     * removal.
     * @method destroy
     * @private
     */
    destroy : function () {
        this.span.parentNode.removeChild(this.span);
        this.span = null;
    }

};

})();
(function () {

var Paginator = YAHOO.widget.Paginator,
    l         = YAHOO.lang,
    setId     = YAHOO.util.Dom.generateId;

/**
 * ui Component to generate the page links
 *
 * @namespace YAHOO.widget.Paginator.ui
 * @class PageLinks
 * @for YAHOO.widget.Paginator
 *
 * @constructor
 * @param p {Pagintor} Paginator instance to attach to
 */
Paginator.ui.PageLinks = function (p) {
    this.paginator = p;

    p.subscribe('recordOffsetChange',this.update,this,true);
    p.subscribe('rowsPerPageChange',this.update,this,true);
    p.subscribe('totalRecordsChange',this.update,this,true);
    p.subscribe('pageLinksChange',   this.rebuild,this,true);
    p.subscribe('pageLinkClassChange', this.rebuild,this,true);
    p.subscribe('currentPageClassChange', this.rebuild,this,true);
    p.subscribe('destroy',this.destroy,this,true);

    //TODO: Make this work
    p.subscribe('pageLinksContainerClassChange', this.rebuild,this,true);
};

/**
 * Decorates Paginator instances with new attributes. Called during
 * Paginator instantiation.
 * @method init
 * @param p {Paginator} Paginator instance to decorate
 * @static
 */
Paginator.ui.PageLinks.init = function (p) {

    /**
     * CSS class assigned to each page link/span.
     * @attribute pageLinkClass
     * @default 'yui-pg-page'
     */
    p.setAttributeConfig('pageLinkClass', {
        value : 'yui-pg-page',
        validator : l.isString
    });

    /**
     * CSS class assigned to the current page span.
     * @attribute currentPageClass
     * @default 'yui-pg-current-page'
     */
    p.setAttributeConfig('currentPageClass', {
        value : 'yui-pg-current-page',
        validator : l.isString
    });

    /**
     * CSS class assigned to the span containing the page links.
     * @attribute pageLinksContainerClass
     * @default 'yui-pg-pages'
     */
    p.setAttributeConfig('pageLinksContainerClass', {
        value : 'yui-pg-pages',
        validator : l.isString
    });

    /**
     * Maximum number of page links to display at one time.
     * @attribute pageLinks
     * @default 10
     */
    p.setAttributeConfig('pageLinks', {
        value : 10,
        validator : Paginator.isNumeric
    });

    /**
     * Function used generate the innerHTML for each page link/span.  The
     * function receives as parameters the page number and a reference to the
     * paginator object.
     * @attribute pageLabelBuilder
     * @default function (page, paginator) { return page; }
     */
    p.setAttributeConfig('pageLabelBuilder', {
        value : function (page, paginator) { return page; },
        validator : l.isFunction
    });

    /**
     * Function used generate the title for each page link.  The
     * function receives as parameters the page number and a reference to the
     * paginator object.
     * @attribute pageTitleBuilder
     * @default function (page, paginator) { return page; }
     */
    p.setAttributeConfig('pageTitleBuilder', {
        value : function (page, paginator) { return "Page " + page; },
        validator : l.isFunction
    });
};

/**
 * Calculates start and end page numbers given a current page, attempting
 * to keep the current page in the middle
 * @static
 * @method calculateRange
 * @param {int} currentPage  The current page
 * @param {int} totalPages   (optional) Maximum number of pages
 * @param {int} numPages     (optional) Preferred number of pages in range
 * @return {Array} [start_page_number, end_page_number]
 */
Paginator.ui.PageLinks.calculateRange = function (currentPage,totalPages,numPages) {
    var UNLIMITED = Paginator.VALUE_UNLIMITED,
        start, end, delta;

    // Either has no pages, or unlimited pages.  Show none.
    if (!currentPage || numPages === 0 || totalPages === 0 ||
        (totalPages === UNLIMITED && numPages === UNLIMITED)) {
        return [0,-1];
    }

    // Limit requested pageLinks if there are fewer totalPages
    if (totalPages !== UNLIMITED) {
        numPages = numPages === UNLIMITED ?
                    totalPages :
                    Math.min(numPages,totalPages);
    }

    // Determine start and end, trying to keep current in the middle
    start = Math.max(1,Math.ceil(currentPage - (numPages/2)));
    if (totalPages === UNLIMITED) {
        end = start + numPages - 1;
    } else {
        end = Math.min(totalPages, start + numPages - 1);
    }

    // Adjust the start index when approaching the last page
    delta = numPages - (end - start + 1);
    start = Math.max(1, start - delta);

    return [start,end];
};


Paginator.ui.PageLinks.prototype = {

    /**
     * Current page
     * @property current
     * @type number
     * @private
     */
    current     : 0,

    /**
     * Span node containing the page links
     * @property container
     * @type HTMLElement
     * @private
     */
    container   : null,


    /**
     * Generate the nodes and return the container node containing page links
     * appropriate to the current pagination state.
     * @method render
     * @param id_base {string} used to create unique ids for generated nodes
     * @return {HTMLElement}
     */
    render : function (id_base) {
        var p = this.paginator;

        // Set up container
        this.container = document.createElement('span');
        setId(this.container, id_base + '-pages');
        this.container.className = p.get('pageLinksContainerClass');
        YAHOO.util.Event.on(this.container,'click',this.onClick,this,true);

        // Call update, flagging a need to rebuild
        this.update({newValue : null, rebuild : true});

        return this.container;
    },

    /**
     * Update the links if appropriate
     * @method update
     * @param e {CustomEvent} The calling change event
     */
    update : function (e) {
        if (e && e.prevValue === e.newValue) {
            return;
        }

        var p           = this.paginator,
            currentPage = p.getCurrentPage();

        // Replace content if there's been a change
        if (this.current !== currentPage || !currentPage || e.rebuild) {
            var labelBuilder = p.get('pageLabelBuilder'),
                titleBuilder = p.get('pageTitleBuilder'),
                range        = Paginator.ui.PageLinks.calculateRange(
                                currentPage,
                                p.getTotalPages(),
                                p.get('pageLinks')),
                start        = range[0],
                end          = range[1],
                content      = '',
                linkTemplate,i,spanTemplate;

            linkTemplate = '<a href="#" class="{class}" page="{page}" title="{title}">{label}</a>';
            spanTemplate = '<span class="{class}">{label}</span>';
            for (i = start; i <= end; ++i) {

                if (i === currentPage) {
                    content += l.substitute(spanTemplate, {
                        'class' : p.get('currentPageClass') + ' ' + p.get('pageLinkClass'),
                        'label' : labelBuilder(i,p)
                    });

                } else {
                    content += l.substitute(linkTemplate, {
                        'class' : p.get('pageLinkClass'),
                        'page'  : i,
                        'label' : labelBuilder(i,p),
                        'title' : titleBuilder(i,p)
                    });
                }
            }

            this.container.innerHTML = content;
        }
    },

    /**
     * Force a rebuild of the page links.
     * @method rebuild
     * @param e {CustomEvent} The calling change event
     */
    rebuild     : function (e) {
        e.rebuild = true;
        this.update(e);
    },

    /**
     * Removes the page links container node and clears event listeners
     * @method destroy
     * @private
     */
    destroy : function () {
        YAHOO.util.Event.purgeElement(this.container,true);
        this.container.parentNode.removeChild(this.container);
        this.container = null;
    },

    /**
     * Listener for the container's onclick event.  Looks for qualifying link
     * clicks, and pulls the page number from the link's page attribute.
     * Sends link's page attribute to the Paginator's setPage method.
     * @method onClick
     * @param e {DOMEvent} The click event
     */
    onClick : function (e) {
        var t = YAHOO.util.Event.getTarget(e);
        if (t && YAHOO.util.Dom.hasClass(t,
                        this.paginator.get('pageLinkClass'))) {

            YAHOO.util.Event.stopEvent(e);

            this.paginator.setPage(parseInt(t.getAttribute('page'),10));
        }
    }

};

})();
(function () {

var Paginator = YAHOO.widget.Paginator,
    l         = YAHOO.lang,
    setId     = YAHOO.util.Dom.generateId;

/**
 * ui Component to generate the link to jump to the first page.
 *
 * @namespace YAHOO.widget.Paginator.ui
 * @class FirstPageLink
 * @for YAHOO.widget.Paginator
 *
 * @constructor
 * @param p {Pagintor} Paginator instance to attach to
 */
Paginator.ui.FirstPageLink = function (p) {
    this.paginator = p;

    p.subscribe('recordOffsetChange',this.update,this,true);
    p.subscribe('rowsPerPageChange',this.update,this,true);
    p.subscribe('totalRecordsChange',this.update,this,true);
    p.subscribe('destroy',this.destroy,this,true);

    // TODO: make this work
    p.subscribe('firstPageLinkLabelChange',this.update,this,true);
    p.subscribe('firstPageLinkClassChange',this.update,this,true);
};

/**
 * Decorates Paginator instances with new attributes. Called during
 * Paginator instantiation.
 * @method init
 * @param p {Paginator} Paginator instance to decorate
 * @static
 */
Paginator.ui.FirstPageLink.init = function (p) {

    /**
     * Used as innerHTML for the first page link/span.
     * @attribute firstPageLinkLabel
     * @default '&lt;&lt; first'
     */
    p.setAttributeConfig('firstPageLinkLabel', {
        value : '&lt;&lt; first',
        validator : l.isString
    });

    /**
     * CSS class assigned to the link/span
     * @attribute firstPageLinkClass
     * @default 'yui-pg-first'
     */
    p.setAttributeConfig('firstPageLinkClass', {
        value : 'yui-pg-first',
        validator : l.isString
    });

    /**
     * Used as title for the first page link.
     * @attribute firstPageLinkTitle
     * @default 'First Page'
     */
    p.setAttributeConfig('firstPageLinkTitle', {
        value : 'First Page',
        validator : l.isString
    });
};

// Instance members and methods
Paginator.ui.FirstPageLink.prototype = {

    /**
     * The currently placed HTMLElement node
     * @property current
     * @type HTMLElement
     * @private
     */
    current   : null,

    /**
     * Link node
     * @property link
     * @type HTMLElement
     * @private
     */
    link      : null,

    /**
     * Span node (inactive link)
     * @property span
     * @type HTMLElement
     * @private
     */
    span      : null,

    /**
     * Generate the nodes and return the appropriate node given the current
     * pagination state.
     * @method render
     * @param id_base {string} used to create unique ids for generated nodes
     * @return {HTMLElement}
     */
    render : function (id_base) {
        var p     = this.paginator,
            c     = p.get('firstPageLinkClass'),
            label = p.get('firstPageLinkLabel'),
            title = p.get('firstPageLinkTitle');

        this.link     = document.createElement('a');
        this.span     = document.createElement('span');

        setId(this.link, id_base + '-first-link');
        this.link.href      = '#';
        this.link.className = c;
        this.link.innerHTML = label;
        this.link.title     = title;
        YAHOO.util.Event.on(this.link,'click',this.onClick,this,true);

        setId(this.span, id_base + '-first-span');
        this.span.className = c;
        this.span.innerHTML = label;

        this.current = p.getCurrentPage() > 1 ? this.link : this.span;
        return this.current;
    },

    /**
     * Swap the link and span nodes if appropriate.
     * @method update
     * @param e {CustomEvent} The calling change event
     */
    update : function (e) {
        if (e && e.prevValue === e.newValue) {
            return;
        }

        var par = this.current ? this.current.parentNode : null;
        if (this.paginator.getCurrentPage() > 1) {
            if (par && this.current === this.span) {
                par.replaceChild(this.link,this.current);
                this.current = this.link;
            }
        } else {
            if (par && this.current === this.link) {
                par.replaceChild(this.span,this.current);
                this.current = this.span;
            }
        }
    },

    /**
     * Removes the link/span node and clears event listeners
     * removal.
     * @method destroy
     * @private
     */
    destroy : function () {
        YAHOO.util.Event.purgeElement(this.link);
        this.current.parentNode.removeChild(this.current);
        this.link = this.span = null;
    },

    /**
     * Listener for the link's onclick event.  Pass new value to setPage method.
     * @method onClick
     * @param e {DOMEvent} The click event
     */
    onClick : function (e) {
        YAHOO.util.Event.stopEvent(e);
        this.paginator.setPage(1);
    }
};

})();
(function () {

var Paginator = YAHOO.widget.Paginator,
    l         = YAHOO.lang,
    setId     = YAHOO.util.Dom.generateId;

/**
 * ui Component to generate the link to jump to the last page.
 *
 * @namespace YAHOO.widget.Paginator.ui
 * @class LastPageLink
 * @for YAHOO.widget.Paginator
 *
 * @constructor
 * @param p {Pagintor} Paginator instance to attach to
 */
Paginator.ui.LastPageLink = function (p) {
    this.paginator = p;

    p.subscribe('recordOffsetChange',this.update,this,true);
    p.subscribe('rowsPerPageChange',this.update,this,true);
    p.subscribe('totalRecordsChange',this.update,this,true);
    p.subscribe('destroy',this.destroy,this,true);

    // TODO: make this work
    p.subscribe('lastPageLinkLabelChange',this.update,this,true);
    p.subscribe('lastPageLinkClassChange', this.update,this,true);
};

/**
 * Decorates Paginator instances with new attributes. Called during
 * Paginator instantiation.
 * @method init
 * @param paginator {Paginator} Paginator instance to decorate
 * @static
 */
Paginator.ui.LastPageLink.init = function (p) {

    /**
     * Used as innerHTML for the last page link/span.
     * @attribute lastPageLinkLabel
     * @default 'last &gt;&gt;'
     */
    p.setAttributeConfig('lastPageLinkLabel', {
        value : 'last &gt;&gt;',
        validator : l.isString
    });

    /**
     * CSS class assigned to the link/span
     * @attribute lastPageLinkClass
     * @default 'yui-pg-last'
     */
    p.setAttributeConfig('lastPageLinkClass', {
        value : 'yui-pg-last',
        validator : l.isString
    });

   /**
     * Used as title for the last page link.
     * @attribute lastPageLinkTitle
     * @default 'Last Page'
     */
    p.setAttributeConfig('lastPageLinkTitle', {
        value : 'Last Page',
        validator : l.isString
    });

};

Paginator.ui.LastPageLink.prototype = {

    /**
     * Currently placed HTMLElement node
     * @property current
     * @type HTMLElement
     * @private
     */
    current   : null,

    /**
     * Link HTMLElement node
     * @property link
     * @type HTMLElement
     * @private
     */
    link      : null,

    /**
     * Span node (inactive link)
     * @property span
     * @type HTMLElement
     * @private
     */
    span      : null,

    /**
     * Empty place holder node for when the last page link is inappropriate to
     * display in any form (unlimited paging).
     * @property na
     * @type HTMLElement
     * @private
     */
    na        : null,


    /**
     * Generate the nodes and return the appropriate node given the current
     * pagination state.
     * @method render
     * @param id_base {string} used to create unique ids for generated nodes
     * @return {HTMLElement}
     */
    render : function (id_base) {
        var p     = this.paginator,
            c     = p.get('lastPageLinkClass'),
            label = p.get('lastPageLinkLabel'),
            last  = p.getTotalPages(),
            title = p.get('lastPageLinkTitle');

        this.link = document.createElement('a');
        this.span = document.createElement('span');
        this.na   = this.span.cloneNode(false);

        setId(this.link, id_base + '-last-link');
        this.link.href      = '#';
        this.link.className = c;
        this.link.innerHTML = label;
        this.link.title     = title;
        YAHOO.util.Event.on(this.link,'click',this.onClick,this,true);

        setId(this.span, id_base + '-last-span');
        this.span.className = c;
        this.span.innerHTML = label;

        setId(this.na, id_base + '-last-na');

        switch (last) {
            case Paginator.VALUE_UNLIMITED :
                    this.current = this.na; break;
            case p.getCurrentPage() :
                    this.current = this.span; break;
            default :
                    this.current = this.link;
        }

        return this.current;
    },

    /**
     * Swap the link, span, and na nodes if appropriate.
     * @method update
     * @param e {CustomEvent} The calling change event (ignored)
     */
    update : function (e) {
        if (e && e.prevValue === e.newValue) {
            return;
        }

        var par   = this.current ? this.current.parentNode : null,
            after = this.link;

        if (par) {
            switch (this.paginator.getTotalPages()) {
                case Paginator.VALUE_UNLIMITED :
                        after = this.na; break;
                case this.paginator.getCurrentPage() :
                        after = this.span; break;
            }

            if (this.current !== after) {
                par.replaceChild(after,this.current);
                this.current = after;
            }
        }
    },

    /**
     * Removes the link/span node and clears event listeners
     * @method destroy
     * @private
     */
    destroy : function () {
        YAHOO.util.Event.purgeElement(this.link);
        this.current.parentNode.removeChild(this.current);
        this.link = this.span = null;
    },

    /**
     * Listener for the link's onclick event.  Passes to setPage method.
     * @method onClick
     * @param e {DOMEvent} The click event
     */
    onClick : function (e) {
        YAHOO.util.Event.stopEvent(e);
        this.paginator.setPage(this.paginator.getTotalPages());
    }
};

})();
(function () {

var Paginator = YAHOO.widget.Paginator,
    l         = YAHOO.lang,
    setId     = YAHOO.util.Dom.generateId;

/**
 * ui Component to generate the link to jump to the next page.
 *
 * @namespace YAHOO.widget.Paginator.ui
 * @class NextPageLink
 * @for YAHOO.widget.Paginator
 *
 * @constructor
 * @param p {Pagintor} Paginator instance to attach to
 */
Paginator.ui.NextPageLink = function (p) {
    this.paginator = p;

    p.subscribe('recordOffsetChange', this.update,this,true);
    p.subscribe('rowsPerPageChange', this.update,this,true);
    p.subscribe('totalRecordsChange', this.update,this,true);
    p.subscribe('destroy',this.destroy,this,true);

    // TODO: make this work
    p.subscribe('nextPageLinkLabelChange', this.update,this,true);
    p.subscribe('nextPageLinkClassChange', this.update,this,true);
};

/**
 * Decorates Paginator instances with new attributes. Called during
 * Paginator instantiation.
 * @method init
 * @param p {Paginator} Paginator instance to decorate
 * @static
 */
Paginator.ui.NextPageLink.init = function (p) {

    /**
     * Used as innerHTML for the next page link/span.
     * @attribute nextPageLinkLabel
     * @default 'next &gt;'
     */
    p.setAttributeConfig('nextPageLinkLabel', {
        value : 'next &gt;',
        validator : l.isString
    });

    /**
     * CSS class assigned to the link/span
     * @attribute nextPageLinkClass
     * @default 'yui-pg-next'
     */
    p.setAttributeConfig('nextPageLinkClass', {
        value : 'yui-pg-next',
        validator : l.isString
    });

    /**
     * Used as title for the next page link.
     * @attribute nextPageLinkTitle
     * @default 'Next Page'
     */
    p.setAttributeConfig('nextPageLinkTitle', {
        value : 'Next Page',
        validator : l.isString
    });

};

Paginator.ui.NextPageLink.prototype = {

    /**
     * Currently placed HTMLElement node
     * @property current
     * @type HTMLElement
     * @private
     */
    current   : null,

    /**
     * Link node
     * @property link
     * @type HTMLElement
     * @private
     */
    link      : null,

    /**
     * Span node (inactive link)
     * @property span
     * @type HTMLElement
     * @private
     */
    span      : null,


    /**
     * Generate the nodes and return the appropriate node given the current
     * pagination state.
     * @method render
     * @param id_base {string} used to create unique ids for generated nodes
     * @return {HTMLElement}
     */
    render : function (id_base) {
        var p     = this.paginator,
            c     = p.get('nextPageLinkClass'),
            label = p.get('nextPageLinkLabel'),
            last  = p.getTotalPages(),
            title = p.get('nextPageLinkTitle');

        this.link     = document.createElement('a');
        this.span     = document.createElement('span');

        setId(this.link, id_base + '-next-link');
        this.link.href      = '#';
        this.link.className = c;
        this.link.innerHTML = label;
        this.link.title     = title;
        YAHOO.util.Event.on(this.link,'click',this.onClick,this,true);

        setId(this.span, id_base + '-next-span');
        this.span.className = c;
        this.span.innerHTML = label;

        this.current = p.getCurrentPage() === last ? this.span : this.link;

        return this.current;
    },

    /**
     * Swap the link and span nodes if appropriate.
     * @method update
     * @param e {CustomEvent} The calling change event
     */
    update : function (e) {
        if (e && e.prevValue === e.newValue) {
            return;
        }

        var last = this.paginator.getTotalPages(),
            par  = this.current ? this.current.parentNode : null;

        if (this.paginator.getCurrentPage() !== last) {
            if (par && this.current === this.span) {
                par.replaceChild(this.link,this.current);
                this.current = this.link;
            }
        } else if (this.current === this.link) {
            if (par) {
                par.replaceChild(this.span,this.current);
                this.current = this.span;
            }
        }
    },

    /**
     * Removes the link/span node and clears event listeners
     * @method destroy
     * @private
     */
    destroy : function () {
        YAHOO.util.Event.purgeElement(this.link);
        this.current.parentNode.removeChild(this.current);
        this.link = this.span = null;
    },

    /**
     * Listener for the link's onclick event.  Passes to setPage method.
     * @method onClick
     * @param e {DOMEvent} The click event
     */
    onClick : function (e) {
        YAHOO.util.Event.stopEvent(e);
        this.paginator.setPage(this.paginator.getNextPage());
    }
};

})();
(function () {

var Paginator = YAHOO.widget.Paginator,
    l         = YAHOO.lang,
    setId     = YAHOO.util.Dom.generateId;

/**
 * ui Component to generate the link to jump to the previous page.
 *
 * @namespace YAHOO.widget.Paginator.ui
 * @class PreviousPageLink
 * @for YAHOO.widget.Paginator
 *
 * @constructor
 * @param p {Pagintor} Paginator instance to attach to
 */
Paginator.ui.PreviousPageLink = function (p) {
    this.paginator = p;

    p.subscribe('recordOffsetChange',this.update,this,true);
    p.subscribe('rowsPerPageChange',this.update,this,true);
    p.subscribe('totalRecordsChange',this.update,this,true);
    p.subscribe('destroy',this.destroy,this,true);

    // TODO: make this work
    p.subscribe('previousPageLinkLabelChange',this.update,this,true);
    p.subscribe('previousPageLinkClassChange',this.update,this,true);
};

/**
 * Decorates Paginator instances with new attributes. Called during
 * Paginator instantiation.
 * @method init
 * @param p {Paginator} Paginator instance to decorate
 * @static
 */
Paginator.ui.PreviousPageLink.init = function (p) {

    /**
     * Used as innerHTML for the previous page link/span.
     * @attribute previousPageLinkLabel
     * @default '&lt; prev'
     */
    p.setAttributeConfig('previousPageLinkLabel', {
        value : '&lt; prev',
        validator : l.isString
    });

    /**
     * CSS class assigned to the link/span
     * @attribute previousPageLinkClass
     * @default 'yui-pg-previous'
     */
    p.setAttributeConfig('previousPageLinkClass', {
        value : 'yui-pg-previous',
        validator : l.isString
    });

    /**
     * Used as title for the previous page link.
     * @attribute previousPageLinkTitle
     * @default 'Previous Page'
     */
    p.setAttributeConfig('previousPageLinkTitle', {
        value : 'Previous Page',
        validator : l.isString
    });

};

Paginator.ui.PreviousPageLink.prototype = {

    /**
     * Currently placed HTMLElement node
     * @property current
     * @type HTMLElement
     * @private
     */
    current   : null,

    /**
     * Link node
     * @property link
     * @type HTMLElement
     * @private
     */
    link      : null,

    /**
     * Span node (inactive link)
     * @property span
     * @type HTMLElement
     * @private
     */
    span      : null,


    /**
     * Generate the nodes and return the appropriate node given the current
     * pagination state.
     * @method render
     * @param id_base {string} used to create unique ids for generated nodes
     * @return {HTMLElement}
     */
    render : function (id_base) {
        var p     = this.paginator,
            c     = p.get('previousPageLinkClass'),
            label = p.get('previousPageLinkLabel'),
            title = p.get('previousPageLinkTitle');

        this.link     = document.createElement('a');
        this.span     = document.createElement('span');

        setId(this.link, id_base + '-prev-link');
        this.link.href      = '#';
        this.link.className = c;
        this.link.innerHTML = label;
        this.link.title     = title;
        YAHOO.util.Event.on(this.link,'click',this.onClick,this,true);

        setId(this.span, id_base + '-prev-span');
        this.span.className = c;
        this.span.innerHTML = label;

        this.current = p.getCurrentPage() > 1 ? this.link : this.span;
        return this.current;
    },

    /**
     * Swap the link and span nodes if appropriate.
     * @method update
     * @param e {CustomEvent} The calling change event
     */
    update : function (e) {
        if (e && e.prevValue === e.newValue) {
            return;
        }

        var par = this.current ? this.current.parentNode : null;
        if (this.paginator.getCurrentPage() > 1) {
            if (par && this.current === this.span) {
                par.replaceChild(this.link,this.current);
                this.current = this.link;
            }
        } else {
            if (par && this.current === this.link) {
                par.replaceChild(this.span,this.current);
                this.current = this.span;
            }
        }
    },

    /**
     * Removes the link/span node and clears event listeners
     * @method destroy
     * @private
     */
    destroy : function () {
        YAHOO.util.Event.purgeElement(this.link);
        this.current.parentNode.removeChild(this.current);
        this.link = this.span = null;
    },

    /**
     * Listener for the link's onclick event.  Passes to setPage method.
     * @method onClick
     * @param e {DOMEvent} The click event
     */
    onClick : function (e) {
        YAHOO.util.Event.stopEvent(e);
        this.paginator.setPage(this.paginator.getPreviousPage());
    }
};

})();
(function () {

var Paginator = YAHOO.widget.Paginator,
    l         = YAHOO.lang,
    setId     = YAHOO.util.Dom.generateId;

/**
 * ui Component to generate the rows-per-page dropdown
 *
 * @namespace YAHOO.widget.Paginator.ui
 * @class RowsPerPageDropdown
 * @for YAHOO.widget.Paginator
 *
 * @constructor
 * @param p {Pagintor} Paginator instance to attach to
 */
Paginator.ui.RowsPerPageDropdown = function (p) {
    this.paginator = p;

    p.subscribe('rowsPerPageChange',this.update,this,true);
    p.subscribe('rowsPerPageOptionsChange',this.rebuild,this,true);
    p.subscribe('totalRecordsChange',this._handleTotalRecordsChange,this,true);
    p.subscribe('destroy',this.destroy,this,true);

    // TODO: make this work
    p.subscribe('rowsPerPageDropdownClassChange',this.rebuild,this,true);
};

/**
 * Decorates Paginator instances with new attributes. Called during
 * Paginator instantiation.
 * @method init
 * @param p {Paginator} Paginator instance to decorate
 * @static
 */
Paginator.ui.RowsPerPageDropdown.init = function (p) {

    /**
     * Array of available rows-per-page sizes.  Converted into select options.
     * Array values may be positive integers or object literals in the form<br>
     * { value : NUMBER, text : STRING }
     * @attribute rowsPerPageOptions
     * @default []
     */
    p.setAttributeConfig('rowsPerPageOptions', {
        value : [],
        validator : l.isArray
    });

    /**
     * CSS class assigned to the select node
     * @attribute rowsPerPageDropdownClass
     * @default 'yui-pg-rpp-options'
     */
    p.setAttributeConfig('rowsPerPageDropdownClass', {
        value : 'yui-pg-rpp-options',
        validator : l.isString
    });
};

Paginator.ui.RowsPerPageDropdown.prototype = {

    /**
     * select node
     * @property select
     * @type HTMLElement
     * @private
     */
    select  : null,


    /**
     * option node for the optional All value
     *
     * @property all
     * @type HTMLElement
     * @protected
     */
    all : null,

    /**
     * Generate the select and option nodes and returns the select node.
     * @method render
     * @param id_base {string} used to create unique ids for generated nodes
     * @return {HTMLElement}
     */
    render : function (id_base) {
        this.select = document.createElement('select');
        setId(this.select, id_base + '-rpp');
        this.select.className = this.paginator.get('rowsPerPageDropdownClass');
        this.select.title = 'Rows per page';

        YAHOO.util.Event.on(this.select,'change',this.onChange,this,true);

        this.rebuild();

        return this.select;
    },

    /**
     * (Re)generate the select options.
     * @method rebuild
     */
    rebuild : function (e) {
        var p       = this.paginator,
            sel     = this.select,
            options = p.get('rowsPerPageOptions'),
            opt,cfg,val,i,len;

        this.all = null;

        for (i = 0, len = options.length; i < len; ++i) {
            cfg = options[i];
            opt = sel.options[i] ||
                  sel.appendChild(document.createElement('option'));
            val = l.isValue(cfg.value) ? cfg.value : cfg;
            opt.text = l.isValue(cfg.text) ? cfg.text : cfg;

            if (l.isString(val) && val.toLowerCase() === 'all') {
                this.all  = opt;
                opt.value = p.get('totalRecords');
            } else{
                opt.value = val;
            }

        }

        while (sel.options.length > options.length) {
            sel.removeChild(sel.firstChild);
        }

        this.update();
    },

    /**
     * Select the appropriate option if changed.
     * @method update
     * @param e {CustomEvent} The calling change event
     */
    update : function (e) {
        if (e && e.prevValue === e.newValue) {
            return;
        }

        var rpp     = this.paginator.get('rowsPerPage')+'',
            options = this.select.options,
            i,len;

        for (i = 0, len = options.length; i < len; ++i) {
            if (options[i].value === rpp) {
                options[i].selected = true;
                break;
            }
        }
    },

    /**
     * Listener for the select's onchange event.  Sent to setRowsPerPage method.
     * @method onChange
     * @param e {DOMEvent} The change event
     */
    onChange : function (e) {
        this.paginator.setRowsPerPage(
                parseInt(this.select.options[this.select.selectedIndex].value,10));
    },

    /**
     * Updates the all option value (and Paginator's rowsPerPage attribute if
     * necessary) in response to a change in the Paginator's totalRecords.
     *
     * @method _handleTotalRecordsChange
     * @param e {Event} attribute change event
     * @protected
     */
    _handleTotalRecordsChange : function (e) {
        if (!this.all || (e && e.prevValue === e.newValue)) {
            return;
        }

        this.all.value = e.newValue;
        if (this.all.selected) {
            this.paginator.set('rowsPerPage',e.newValue);
        }
    },

    /**
     * Removes the select node and clears event listeners
     * @method destroy
     * @private
     */
    destroy : function () {
        YAHOO.util.Event.purgeElement(this.select);
        this.select.parentNode.removeChild(this.select);
        this.select = null;
    }
};

})();
(function () {

var Paginator = YAHOO.widget.Paginator,
    l         = YAHOO.lang,
    setId     = YAHOO.util.Dom.generateId;

/**
 * ui Component to generate the jump-to-page dropdown
 *
 * @namespace YAHOO.widget.Paginator.ui
 * @class JumpToPageDropdown
 * @for YAHOO.widget.Paginator
 *
 * @constructor
 * @param p {Pagintor} Paginator instance to attach to
 */
Paginator.ui.JumpToPageDropdown = function (p) {
    this.paginator = p;

    p.subscribe('rowsPerPageChange',this.rebuild,this,true);
    p.subscribe('rowsPerPageOptionsChange',this.rebuild,this,true);
    p.subscribe('pageChange',this.update,this,true);
    p.subscribe('totalRecordsChange',this.rebuild,this,true);
    p.subscribe('destroy',this.destroy,this,true);

};

/**
 * Decorates Paginator instances with new attributes. Called during
 * Paginator instantiation.
 * @method init
 * @param p {Paginator} Paginator instance to decorate
 * @static
 */
Paginator.ui.JumpToPageDropdown.init = function (p) {



    /**
     * CSS class assigned to the select node
     * @attribute jumpToPageDropdownClass
     * @default 'yui-pg-jtp-options'
     */
    p.setAttributeConfig('jumpToPageDropdownClass', {
        value : 'yui-pg-jtp-options',
        validator : l.isString
    });
};

Paginator.ui.JumpToPageDropdown.prototype = {

    /**
     * select node
     * @property select
     * @type HTMLElement
     * @private
     */
    select  : null,



    /**
     * Generate the select and option nodes and returns the select node.
     * @method render
     * @param id_base {string} used to create unique ids for generated nodes
     * @return {HTMLElement}
     */
    render : function (id_base) {
        this.select = document.createElement('select');
        setId(this.select, id_base + '-jtp');
        this.select.className = this.paginator.get('jumpToPageDropdownClass');
        this.select.title = 'Jump to page';

        YAHOO.util.Event.on(this.select,'change',this.onChange,this,true);

        this.rebuild();

        return this.select;
    },

    /**
     * (Re)generate the select options.
     * @method rebuild
     */
    rebuild : function (e) {
        var p       = this.paginator,
            sel     = this.select,
            numPages = p.getTotalPages(),
            opt,i,len;

        this.all = null;

        for (i = 0, len = numPages; i < len; ++i ) {
            opt = sel.options[i] ||
                  sel.appendChild(document.createElement('option'));

            opt.innerHTML = i + 1;

            opt.value = i + 1;


        }

        for ( i = numPages, len = sel.options.length ; i < len ; i++ ) {
            sel.removeChild(sel.lastChild);
        }

        this.update();
    },

    /**
     * Select the appropriate option if changed.
     * @method update
     * @param e {CustomEvent} The calling change event
     */
    update : function (e) {

        if (e && e.prevValue === e.newValue) {
            return;
        }

        var cp      = this.paginator.getCurrentPage()+'',
            options = this.select.options,
            i,len;

        for (i = 0, len = options.length; i < len; ++i) {
            if (options[i].value === cp) {
                options[i].selected = true;
                break;
            }
        }
    },

    /**
     * Listener for the select's onchange event.  Sent to setPage method.
     * @method onChange
     * @param e {DOMEvent} The change event
     */
    onChange : function (e) {
        this.paginator.setPage(
                parseInt(this.select.options[this.select.selectedIndex].value,false));
    },



    /**
     * Removes the select node and clears event listeners
     * @method destroy
     * @private
     */
    destroy : function () {
        YAHOO.util.Event.purgeElement(this.select);
        this.select.parentNode.removeChild(this.select);
        this.select = null;
    }
};

})();
YAHOO.register("paginator", YAHOO.widget.Paginator, {version: "2.9.0", build: "2800"});
/*
Copyright (c) 2011, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 2.9.0
*/
/**
 * Provides methods to parse JSON strings and convert objects to JSON strings.
 *
 * @module json
 * @class JSON
 * @namespace YAHOO.lang
 * @static
 */
(function () {

var l = YAHOO.lang,
    isFunction = l.isFunction,
    isObject   = l.isObject,
    isArray    = l.isArray,
    _toStr     = Object.prototype.toString,
                 // 'this' is the global object.  window in browser env.  Keep
                 // the code env agnostic.  Caja requies window, unfortunately.
    Native     = (YAHOO.env.ua.caja ? window : this).JSON,

/* Variables used by parse */

    /**
     * Replace certain Unicode characters that JavaScript may handle incorrectly
     * during eval--either by deleting them or treating them as line
     * endings--with escape sequences.
     * IMPORTANT NOTE: This regex will be used to modify the input if a match is
     * found.
     *
     * @property _UNICODE_EXCEPTIONS
     * @type {RegExp}
     * @private
     */
    _UNICODE_EXCEPTIONS = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,

    /**
     * First step in the safety evaluation.  Regex used to replace all escape
     * sequences (i.e. "\\", etc) with '@' characters (a non-JSON character).
     *
     * @property _ESCAPES
     * @type {RegExp}
     * @static
     * @private
     */
    _ESCAPES = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,

    /**
     * Second step in the safety evaluation.  Regex used to replace all simple
     * values with ']' characters.
     *
     * @property _VALUES
     * @type {RegExp}
     * @static
     * @private
     */
    _VALUES  = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,

    /**
     * Third step in the safety evaluation.  Regex used to remove all open
     * square brackets following a colon, comma, or at the beginning of the
     * string.
     *
     * @property _BRACKETS
     * @type {RegExp}
     * @static
     * @private
     */
    _BRACKETS = /(?:^|:|,)(?:\s*\[)+/g,

    /**
     * Final step in the safety evaluation.  Regex used to test the string left
     * after all previous replacements for invalid characters.
     *
     * @property _UNSAFE
     * @type {RegExp}
     * @static
     * @private
     */
    _UNSAFE  = /[^\],:{}\s]/,


/* Variables used by stringify */

    /**
     * Regex used to replace special characters in strings for JSON
     * stringification.
     *
     * @property _SPECIAL_CHARS
     * @type {RegExp}
     * @static
     * @private
     */
    _SPECIAL_CHARS = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,

    /**
     * Character substitution map for common escapes and special characters.
     *
     * @property _CHARS
     * @type {Object}
     * @static
     * @private
     */
    _CHARS = {
        '\b': '\\b',
        '\t': '\\t',
        '\n': '\\n',
        '\f': '\\f',
        '\r': '\\r',
        '"' : '\\"',
        '\\': '\\\\'
    },
    
    UNDEFINED = 'undefined',
    OBJECT    = 'object',
    NULL      = 'null',
    STRING    = 'string',
    NUMBER    = 'number',
    BOOLEAN   = 'boolean',
    DATE      = 'date',
    _allowable = {
        'undefined'        : UNDEFINED,
        'string'           : STRING,
        '[object String]'  : STRING,
        'number'           : NUMBER,
        '[object Number]'  : NUMBER,
        'boolean'          : BOOLEAN,
        '[object Boolean]' : BOOLEAN,
        '[object Date]'    : DATE,
        '[object RegExp]'  : OBJECT
    },
    EMPTY     = '',
    OPEN_O    = '{',
    CLOSE_O   = '}',
    OPEN_A    = '[',
    CLOSE_A   = ']',
    COMMA     = ',',
    COMMA_CR  = ",\n",
    CR        = "\n",
    COLON     = ':',
    COLON_SP  = ': ',
    QUOTE     = '"';

// Only accept JSON objects that report a [[Class]] of JSON
Native = _toStr.call(Native) === '[object JSON]' && Native;

// Escapes a special character to a safe Unicode representation
function _char(c) {
    if (!_CHARS[c]) {
        _CHARS[c] =  '\\u'+('0000'+(+(c.charCodeAt(0))).toString(16)).slice(-4);
    }
    return _CHARS[c];
}


/* functions used by parse */

/**
 * Traverses nested objects, applying a filter or reviver function to
 * each value.  The value returned from the function will replace the
 * original value in the key:value pair.  If the value returned is
 * undefined, the key will be omitted from the returned object.
 *
 * @method _revive
 * @param data {MIXED} Any JavaScript data
 * @param reviver {Function} filter or mutation function
 * @return {MIXED} The results of the filtered/mutated data structure
 * @private
 */
function _revive(data, reviver) {
    var walk = function (o,key) {
        var k,v,value = o[key];
        if (value && typeof value === 'object') {
            for (k in value) {
                if (l.hasOwnProperty(value,k)) {
                    v = walk(value, k);
                    if (v === undefined) {
                        delete value[k];
                    } else {
                        value[k] = v;
                    }
                }
            }
        }
        return reviver.call(o,key,value);
    };

    return typeof reviver === 'function' ? walk({'':data},'') : data;
}

/**
 * Replace certain Unicode characters that may be handled incorrectly by
 * some browser implementations.
 *
 * @method _prepare
 * @param s {String} parse input
 * @return {String} sanitized JSON string ready to be validated/parsed
 * @private
 */
function _prepare(s) {
    return s.replace(_UNICODE_EXCEPTIONS, _char);
}

function _isSafe(str) {
    return l.isString(str) &&
            !_UNSAFE.test(str.replace(_ESCAPES,'@').
                             replace(_VALUES,']').
                             replace(_BRACKETS,''));
}

function _parse(s,reviver) {
    // sanitize
    s = _prepare(s);

    // Ensure valid JSON
    if (_isSafe(s)) {
        // Eval the text into a JavaScript data structure, apply the
        // reviver function if provided, and return
        return _revive( eval('(' + s + ')'), reviver );
    }

    // The text is not valid JSON
    throw new SyntaxError('JSON.parse');
}



/* functions used by stringify */

// Utility function used to determine how to serialize a variable.
function _type(o) {
    var t = typeof o;
    return  _allowable[t] ||              // number, string, boolean, undefined
            _allowable[_toStr.call(o)] || // Number, String, Boolean, Date
            (t === OBJECT ?
                (o ? OBJECT : NULL) :     // object, array, null, misc natives
                UNDEFINED);               // function, unknown
}

// Enclose escaped strings in quotes
function _string(s) {
    return QUOTE + s.replace(_SPECIAL_CHARS, _char) + QUOTE;
}

// Adds the provided space to the beginning of every line in the input string
function _indent(s,space) {
    return s.replace(/^/gm, space);
}

// JavaScript implementation of stringify (see API declaration of stringify)
function _stringify(o,w,space) {
    if (o === undefined) {
        return undefined;
    }

    var replacer = isFunction(w) ? w : null,
        format   = _toStr.call(space).match(/String|Number/) || [],
        _date    = YAHOO.lang.JSON.dateToString,
        stack    = [],
        tmp,i,len;

    if (replacer || !isArray(w)) {
        w = undefined;
    }

    // Ensure whitelist keys are unique (bug 2110391)
    if (w) {
        tmp = {};
        for (i = 0, len = w.length; i < len; ++i) {
            tmp[w[i]] = true;
        }
        w = tmp;
    }

    // Per the spec, strings are truncated to 10 characters and numbers
    // are converted to that number of spaces (max 10)
    space = format[0] === 'Number' ?
                new Array(Math.min(Math.max(0,space),10)+1).join(" ") :
                (space || EMPTY).slice(0,10);

    function _serialize(h,key) {
        var value = h[key],
            t     = _type(value),
            a     = [],
            colon = space ? COLON_SP : COLON,
            arr, i, keys, k, v;

        // Per the ECMA 5 spec, toJSON is applied before the replacer is
        // called.  Also per the spec, Date.prototype.toJSON has been added, so
        // Date instances should be serialized prior to exposure to the
        // replacer.  I disagree with this decision, but the spec is the spec.
        if (isObject(value) && isFunction(value.toJSON)) {
            value = value.toJSON(key);
        } else if (t === DATE) {
            value = _date(value);
        }

        if (isFunction(replacer)) {
            value = replacer.call(h,key,value);
        }

        if (value !== h[key]) {
            t = _type(value);
        }

        switch (t) {
            case DATE    : // intentional fallthrough.  Pre-replacer Dates are
                           // serialized in the toJSON stage.  Dates here would
                           // have been produced by the replacer.
            case OBJECT  : break;
            case STRING  : return _string(value);
            case NUMBER  : return isFinite(value) ? value+EMPTY : NULL;
            case BOOLEAN : return value+EMPTY;
            case NULL    : return NULL;
            default      : return undefined;
        }

        // Check for cyclical references in nested objects
        for (i = stack.length - 1; i >= 0; --i) {
            if (stack[i] === value) {
                throw new Error("JSON.stringify. Cyclical reference");
            }
        }

        arr = isArray(value);

        // Add the object to the processing stack
        stack.push(value);

        if (arr) { // Array
            for (i = value.length - 1; i >= 0; --i) {
                a[i] = _serialize(value, i) || NULL;
            }
        } else {   // Object
            // If whitelist provided, take only those keys
            keys = w || value;
            i = 0;

            for (k in keys) {
                if (l.hasOwnProperty(keys, k)) {
                    v = _serialize(value, k);
                    if (v) {
                        a[i++] = _string(k) + colon + v;
                    }
                }
            }
        }

        // remove the array from the stack
        stack.pop();

        if (space && a.length) {
            return arr ?
                OPEN_A + CR + _indent(a.join(COMMA_CR), space) + CR + CLOSE_A :
                OPEN_O + CR + _indent(a.join(COMMA_CR), space) + CR + CLOSE_O;
        } else {
            return arr ?
                OPEN_A + a.join(COMMA) + CLOSE_A :
                OPEN_O + a.join(COMMA) + CLOSE_O;
        }
    }

    // process the input
    return _serialize({'':o},'');
}


/* Public API */
YAHOO.lang.JSON = {
    /**
     * Leverage native JSON parse if the browser has a native implementation.
     * In general, this is a good idea.  See the Known Issues section in the
     * JSON user guide for caveats.  The default value is true for browsers with
     * native JSON support.
     *
     * @property useNativeParse
     * @type Boolean
     * @default true
     * @static
     */
    useNativeParse : !!Native,

    /**
     * Leverage native JSON stringify if the browser has a native
     * implementation.  In general, this is a good idea.  See the Known Issues
     * section in the JSON user guide for caveats.  The default value is true
     * for browsers with native JSON support.
     *
     * @property useNativeStringify
     * @type Boolean
     * @default true
     * @static
     */
    useNativeStringify : !!Native,

    /**
     * Four step determination whether a string is safe to eval. In three steps,
     * escape sequences, safe values, and properly placed open square brackets
     * are replaced with placeholders or removed.  Then in the final step, the
     * result of all these replacements is checked for invalid characters.
     *
     * @method isSafe
     * @param str {String} JSON string to be tested
     * @return {boolean} is the string safe for eval?
     * @static
     */
    isSafe : function (s) {
        return _isSafe(_prepare(s));
    },

    /**
     * <p>Parse a JSON string, returning the native JavaScript
     * representation.</p>
     *
     * <p>When lang.JSON.useNativeParse is true, this will defer to the native
     * JSON.parse if the browser has a native implementation.  Otherwise, a
     * JavaScript implementation based on http://www.json.org/json2.js
     * is used.</p>
     *
     * @method parse
     * @param s {string} JSON string data
     * @param reviver {function} (optional) function(k,v) passed each key:value
     *          pair of object literals, allowing pruning or altering values
     * @return {MIXED} the native JavaScript representation of the JSON string
     * @throws SyntaxError
     * @static
     */
    parse : function (s,reviver) {
        if (typeof s !== 'string') {
            s += '';
        }

        return Native && YAHOO.lang.JSON.useNativeParse ?
            Native.parse(s,reviver) : _parse(s,reviver);
    },

    /**
     * <p>Converts an arbitrary value to a JSON string representation.</p>
     *
     * <p>Objects with cyclical references will trigger an exception.</p>
     *
     * <p>If a whitelist is provided, only matching object keys will be
     * included.  Alternately, a replacer function may be passed as the
     * second parameter.  This function is executed on every value in the
     * input, and its return value will be used in place of the original value.
     * This is useful to serialize specialized objects or class instances.</p>
     *
     * <p>If a positive integer or non-empty string is passed as the third
     * parameter, the output will be formatted with carriage returns and
     * indentation for readability.  If a String is passed (such as "\t") it
     * will be used once for each indentation level.  If a number is passed,
     * that number of spaces will be used.</p>
     *
     * <p>When lang.JSON.useNativeStringify is true, this will defer to the
     * native JSON.stringify if the browser has a native implementation.
     * Otherwise, a JavaScript implementation is used.</p>
     *
     * @method stringify
     * @param o {MIXED} any arbitrary object to convert to JSON string
     * @param w {Array|Function} (optional) whitelist of acceptable object keys
     *                  to include OR a function(value,key) to alter values
     *                  before serialization
     * @param space {Number|String} (optional) indentation character(s) or
     *                  depthy of spaces to format the output 
     * @return {string} JSON string representation of the input
     * @throws Error
     * @static
     */
    stringify : function (o,w,space) {
        return Native && YAHOO.lang.JSON.useNativeStringify ?
            Native.stringify(o,w,space) : _stringify(o,w,space);
    },

    /**
     * Serializes a Date instance as a UTC date string.  Used internally by
     * the JavaScript implementation of stringify.  If you need a different
     * Date serialization format, override this method.  If you change this,
     * you should also set useNativeStringify to false, since native JSON
     * implementations serialize Dates per the ECMAScript 5 spec.  You've been
     * warned.
     *
     * @method dateToString
     * @param d {Date} The Date to serialize
     * @return {String} stringified Date in UTC format YYYY-MM-DDTHH:mm:SSZ
     * @static
     */
    dateToString : function (d) {
        function _zeroPad(v) {
            return v < 10 ? '0' + v : v;
        }

        return d.getUTCFullYear()         + '-' +
            _zeroPad(d.getUTCMonth() + 1) + '-' +
            _zeroPad(d.getUTCDate())      + 'T' +
            _zeroPad(d.getUTCHours())     + COLON +
            _zeroPad(d.getUTCMinutes())   + COLON +
            _zeroPad(d.getUTCSeconds())   + 'Z';
    },

    /**
     * Reconstitute Date instances from the default JSON UTC serialization.
     * Reference this from a reviver function to rebuild Dates during the
     * parse operation.
     *
     * @method stringToDate
     * @param str {String} String serialization of a Date
     * @return {Date}
     */
    stringToDate : function (str) {
        var m = str.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?Z$/);
        if (m) {
            var d = new Date();
            d.setUTCFullYear(m[1], m[2]-1, m[3]);
            d.setUTCHours(m[4], m[5], m[6], (m[7] || 0));
            return d;
        }
        return str;
    }
};

/**
 * <p>Four step determination whether a string is safe to eval. In three steps,
 * escape sequences, safe values, and properly placed open square brackets
 * are replaced with placeholders or removed.  Then in the final step, the
 * result of all these replacements is checked for invalid characters.</p>
 *
 * <p>This is an alias for isSafe.</p>
 *
 * @method isValid
 * @param str {String} JSON string to be tested
 * @return {boolean} is the string safe for eval?
 * @static
 * @deprecated use isSafe
 */
YAHOO.lang.JSON.isValid = YAHOO.lang.JSON.isSafe;

})();
YAHOO.register("json", YAHOO.lang.JSON, {version: "2.9.0", build: "2800"});
/*
Copyright (c) 2011, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 2.9.0
*/


/**
* @module menu
* @description <p>The Menu family of components features a collection of 
* controls that make it easy to add menus to your website or web application.  
* With the Menu Controls you can create website fly-out menus, customized 
* context menus, or application-style menu bars with just a small amount of 
* scripting.</p><p>The Menu family of controls features:</p>
* <ul>
*    <li>Keyboard and mouse navigation.</li>
*    <li>A rich event model that provides access to all of a menu's 
*    interesting moments.</li>
*    <li>Support for 
*    <a href="http://en.wikipedia.org/wiki/Progressive_Enhancement">Progressive
*    Enhancement</a>; Menus can be created from simple, 
*    semantic markup on the page or purely through JavaScript.</li>
* </ul>
* @title Menu
* @namespace YAHOO.widget
* @requires Event, Dom, Container
*/
(function () {

    var UA = YAHOO.env.ua,
        Dom = YAHOO.util.Dom,
        Event = YAHOO.util.Event,
        Lang = YAHOO.lang,

        _DIV = "DIV",
        _HD = "hd",
        _BD = "bd",
        _FT = "ft",
        _LI = "LI",
        _DISABLED = "disabled",
        _MOUSEOVER = "mouseover",
        _MOUSEOUT = "mouseout",
        _MOUSEDOWN = "mousedown",
        _MOUSEUP = "mouseup",
        _CLICK = "click",
        _KEYDOWN = "keydown",
        _KEYUP = "keyup",
        _KEYPRESS = "keypress",
        _CLICK_TO_HIDE = "clicktohide",
        _POSITION = "position", 
        _DYNAMIC = "dynamic",
        _SHOW_DELAY = "showdelay",
        _SELECTED = "selected",
        _VISIBLE = "visible",
        _UL = "UL",
        _MENUMANAGER = "MenuManager";


    /**
    * Singleton that manages a collection of all menus and menu items.  Listens 
    * for DOM events at the document level and dispatches the events to the 
    * corresponding menu or menu item.
    *
    * @namespace YAHOO.widget
    * @class MenuManager
    * @static
    */
    YAHOO.widget.MenuManager = function () {
    
        // Private member variables
    
    
        // Flag indicating if the DOM event handlers have been attached
    
        var m_bInitializedEventHandlers = false,
    
    
        // Collection of menus

        m_oMenus = {},


        // Collection of visible menus
    
        m_oVisibleMenus = {},
    
    
        //  Collection of menu items 

        m_oItems = {},


        // Map of DOM event types to their equivalent CustomEvent types
        
        m_oEventTypes = {
            "click": "clickEvent",
            "mousedown": "mouseDownEvent",
            "mouseup": "mouseUpEvent",
            "mouseover": "mouseOverEvent",
            "mouseout": "mouseOutEvent",
            "keydown": "keyDownEvent",
            "keyup": "keyUpEvent",
            "keypress": "keyPressEvent",
            "focus": "focusEvent",
            "focusin": "focusEvent",
            "blur": "blurEvent",
            "focusout": "blurEvent"
        },
    
    
        m_oFocusedMenuItem = null;
    
    
    
        // Private methods
    
    
        /**
        * @method getMenuRootElement
        * @description Finds the root DIV node of a menu or the root LI node of 
        * a menu item.
        * @private
        * @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/
        * level-one-html.html#ID-58190037">HTMLElement</a>} p_oElement Object 
        * specifying an HTML element.
        */
        function getMenuRootElement(p_oElement) {
        
            var oParentNode,
                returnVal;
    
            if (p_oElement && p_oElement.tagName) {
            
                switch (p_oElement.tagName.toUpperCase()) {
                        
                case _DIV:
    
                    oParentNode = p_oElement.parentNode;
    
                    // Check if the DIV is the inner "body" node of a menu

                    if ((
                            Dom.hasClass(p_oElement, _HD) ||
                            Dom.hasClass(p_oElement, _BD) ||
                            Dom.hasClass(p_oElement, _FT)
                        ) && 
                        oParentNode && 
                        oParentNode.tagName && 
                        oParentNode.tagName.toUpperCase() == _DIV) {
                    
                        returnVal = oParentNode;
                    
                    }
                    else {
                    
                        returnVal = p_oElement;
                    
                    }
                
                    break;

                case _LI:
    
                    returnVal = p_oElement;
                    
                    break;

                default:
    
                    oParentNode = p_oElement.parentNode;
    
                    if (oParentNode) {
                    
                        returnVal = getMenuRootElement(oParentNode);
                    
                    }
                
                    break;
                
                }
    
            }
            
            return returnVal;
            
        }
    
    
    
        // Private event handlers
    
    
        /**
        * @method onDOMEvent
        * @description Generic, global event handler for all of a menu's 
        * DOM-based events.  This listens for events against the document 
        * object.  If the target of a given event is a member of a menu or 
        * menu item's DOM, the instance's corresponding Custom Event is fired.
        * @private
        * @param {Event} p_oEvent Object representing the DOM event object  
        * passed back by the event utility (YAHOO.util.Event).
        */
        function onDOMEvent(p_oEvent) {
    
            // Get the target node of the DOM event
        
            var oTarget = Event.getTarget(p_oEvent),
                
            // See if the target of the event was a menu, or a menu item
    
            oElement = getMenuRootElement(oTarget),
            bFireEvent = true,
            sEventType = p_oEvent.type,
            sCustomEventType,
            sTagName,
            sId,
            oMenuItem,
            oMenu; 
    
    
            if (oElement) {
    
                sTagName = oElement.tagName.toUpperCase();
        
                if (sTagName == _LI) {
            
                    sId = oElement.id;
            
                    if (sId && m_oItems[sId]) {
            
                        oMenuItem = m_oItems[sId];
                        oMenu = oMenuItem.parent;
            
                    }
                
                }
                else if (sTagName == _DIV) {
                
                    if (oElement.id) {
                    
                        oMenu = m_oMenus[oElement.id];
                    
                    }
                
                }
    
            }
    
    
            if (oMenu) {
    
                sCustomEventType = m_oEventTypes[sEventType];

                /*
                    There is an inconsistency between Firefox for Mac OS X and 
                    Firefox Windows & Linux regarding the triggering of the 
                    display of the browser's context menu and the subsequent 
                    firing of the "click" event. In Firefox for Windows & Linux, 
                    when the user triggers the display of the browser's context 
                    menu the "click" event also fires for the document object, 
                    even though the "click" event did not fire for the element 
                    that was the original target of the "contextmenu" event. 
                    This is unique to Firefox on Windows & Linux.  For all 
                    other A-Grade browsers, including Firefox for Mac OS X, the 
                    "click" event doesn't fire for the document object. 

                    This bug in Firefox for Windows affects Menu, as Menu 
                    instances listen for events at the document level and 
                    dispatches Custom Events of the same name.  Therefore users
                    of Menu will get an unwanted firing of the "click" 
                    custom event.  The following line fixes this bug.
                */
                


                if (sEventType == "click" && 
                    (UA.gecko && oMenu.platform != "mac") && 
                    p_oEvent.button > 0) {

                    bFireEvent = false;

                }
    
                // Fire the Custom Event that corresponds the current DOM event    
        
                if (bFireEvent && oMenuItem && !oMenuItem.cfg.getProperty(_DISABLED)) {
                    oMenuItem[sCustomEventType].fire(p_oEvent);                   
                }
        
                if (bFireEvent) {
                    oMenu[sCustomEventType].fire(p_oEvent, oMenuItem);
                }
            
            }
            else if (sEventType == _MOUSEDOWN) {
    
                /*
                    If the target of the event wasn't a menu, hide all 
                    dynamically positioned menus
                */
                
                for (var i in m_oVisibleMenus) {
        
                    if (Lang.hasOwnProperty(m_oVisibleMenus, i)) {
        
                        oMenu = m_oVisibleMenus[i];

                        if (oMenu.cfg.getProperty(_CLICK_TO_HIDE) && 
                            !(oMenu instanceof YAHOO.widget.MenuBar) && 
                            oMenu.cfg.getProperty(_POSITION) == _DYNAMIC) {

                            oMenu.hide();

                            //	In IE when the user mouses down on a focusable 
                            //	element that element will be focused and become 
                            //	the "activeElement".
                            //	(http://msdn.microsoft.com/en-us/library/ms533065(VS.85).aspx)
                            //	However, there is a bug in IE where if there is 
                            //	a positioned element with a focused descendant 
                            //	that is hidden in response to the mousedown 
                            //	event, the target of the mousedown event will 
                            //	appear to have focus, but will not be set as 
                            //	the activeElement.  This will result in the 
                            //	element not firing key events, even though it
                            //	appears to have focus.  The following call to 
                            //	"setActive" fixes this bug.

                            if (UA.ie && oTarget.focus && (UA.ie < 9)) {
                                oTarget.setActive();
                            }
        
                        }
                        else {
                            
                            if (oMenu.cfg.getProperty(_SHOW_DELAY) > 0) {
                            
                                oMenu._cancelShowDelay();
                            
                            }


                            if (oMenu.activeItem) {
                        
                                oMenu.activeItem.blur();
                                oMenu.activeItem.cfg.setProperty(_SELECTED, false);
                        
                                oMenu.activeItem = null;            
                        
                            }
        
                        }
        
                    }
        
                } 
    
            }
            
        }
    
    
        /**
        * @method onMenuDestroy
        * @description "destroy" event handler for a menu.
        * @private
        * @param {String} p_sType String representing the name of the event 
        * that was fired.
        * @param {Array} p_aArgs Array of arguments sent when the event 
        * was fired.
        * @param {YAHOO.widget.Menu} p_oMenu The menu that fired the event.
        */
        function onMenuDestroy(p_sType, p_aArgs, p_oMenu) {
    
            if (m_oMenus[p_oMenu.id]) {
    
                this.removeMenu(p_oMenu);
    
            }
    
        }
    
    
        /**
        * @method onMenuFocus
        * @description "focus" event handler for a MenuItem instance.
        * @private
        * @param {String} p_sType String representing the name of the event 
        * that was fired.
        * @param {Array} p_aArgs Array of arguments sent when the event 
        * was fired.
        */
        function onMenuFocus(p_sType, p_aArgs) {
    
            var oItem = p_aArgs[1];
    
            if (oItem) {
    
                m_oFocusedMenuItem = oItem;
            
            }
    
        }
    
    
        /**
        * @method onMenuBlur
        * @description "blur" event handler for a MenuItem instance.
        * @private
        * @param {String} p_sType String representing the name of the event  
        * that was fired.
        * @param {Array} p_aArgs Array of arguments sent when the event 
        * was fired.
        */
        function onMenuBlur(p_sType, p_aArgs) {
    
            m_oFocusedMenuItem = null;
    
        }

    
        /**
        * @method onMenuVisibleConfigChange
        * @description Event handler for when the "visible" configuration  
        * property of a Menu instance changes.
        * @private
        * @param {String} p_sType String representing the name of the event  
        * that was fired.
        * @param {Array} p_aArgs Array of arguments sent when the event 
        * was fired.
        */
        function onMenuVisibleConfigChange(p_sType, p_aArgs) {
    
            var bVisible = p_aArgs[0],
                sId = this.id;
            
            if (bVisible) {
    
                m_oVisibleMenus[sId] = this;
                
                YAHOO.log(this + " added to the collection of visible menus.", 
                    "info", _MENUMANAGER);
            
            }
            else if (m_oVisibleMenus[sId]) {
            
                delete m_oVisibleMenus[sId];
                
                YAHOO.log(this + " removed from the collection of visible menus.", 
                    "info", _MENUMANAGER);
            
            }
        
        }
    
    
        /**
        * @method onItemDestroy
        * @description "destroy" event handler for a MenuItem instance.
        * @private
        * @param {String} p_sType String representing the name of the event  
        * that was fired.
        * @param {Array} p_aArgs Array of arguments sent when the event 
        * was fired.
        */
        function onItemDestroy(p_sType, p_aArgs) {
    
            removeItem(this);
    
        }


        /**
        * @method removeItem
        * @description Removes a MenuItem instance from the MenuManager's collection of MenuItems.
        * @private
        * @param {MenuItem} p_oMenuItem The MenuItem instance to be removed.
        */    
        function removeItem(p_oMenuItem) {

            var sId = p_oMenuItem.id;
    
            if (sId && m_oItems[sId]) {
    
                if (m_oFocusedMenuItem == p_oMenuItem) {
    
                    m_oFocusedMenuItem = null;
    
                }
    
                delete m_oItems[sId];
                
                p_oMenuItem.destroyEvent.unsubscribe(onItemDestroy);
    
                YAHOO.log(p_oMenuItem + " successfully unregistered.", "info", _MENUMANAGER);
    
            }

        }
    
    
        /**
        * @method onItemAdded
        * @description "itemadded" event handler for a Menu instance.
        * @private
        * @param {String} p_sType String representing the name of the event  
        * that was fired.
        * @param {Array} p_aArgs Array of arguments sent when the event 
        * was fired.
        */
        function onItemAdded(p_sType, p_aArgs) {
    
            var oItem = p_aArgs[0],
                sId;
    
            if (oItem instanceof YAHOO.widget.MenuItem) { 
    
                sId = oItem.id;
        
                if (!m_oItems[sId]) {
            
                    m_oItems[sId] = oItem;
        
                    oItem.destroyEvent.subscribe(onItemDestroy);
        
                    YAHOO.log(oItem + " successfully registered.", "info", _MENUMANAGER);
        
                }
    
            }
        
        }
    
    
        return {
    
            // Privileged methods
    
    
            /**
            * @method addMenu
            * @description Adds a menu to the collection of known menus.
            * @param {YAHOO.widget.Menu} p_oMenu Object specifying the Menu  
            * instance to be added.
            */
            addMenu: function (p_oMenu) {
    
                var oDoc;
    
                if (p_oMenu instanceof YAHOO.widget.Menu && p_oMenu.id && 
                    !m_oMenus[p_oMenu.id]) {
        
                    m_oMenus[p_oMenu.id] = p_oMenu;
                
            
                    if (!m_bInitializedEventHandlers) {
            
                        oDoc = document;
                
                        Event.on(oDoc, _MOUSEOVER, onDOMEvent, this, true);
                        Event.on(oDoc, _MOUSEOUT, onDOMEvent, this, true);
                        Event.on(oDoc, _MOUSEDOWN, onDOMEvent, this, true);
                        Event.on(oDoc, _MOUSEUP, onDOMEvent, this, true);
                        Event.on(oDoc, _CLICK, onDOMEvent, this, true);
                        Event.on(oDoc, _KEYDOWN, onDOMEvent, this, true);
                        Event.on(oDoc, _KEYUP, onDOMEvent, this, true);
                        Event.on(oDoc, _KEYPRESS, onDOMEvent, this, true);
    
                        Event.onFocus(oDoc, onDOMEvent, this, true);
                        Event.onBlur(oDoc, onDOMEvent, this, true);						
    
                        m_bInitializedEventHandlers = true;
                        
                        YAHOO.log("DOM event handlers initialized.", "info", _MENUMANAGER);
            
                    }
            
                    p_oMenu.cfg.subscribeToConfigEvent(_VISIBLE, onMenuVisibleConfigChange);
                    p_oMenu.destroyEvent.subscribe(onMenuDestroy, p_oMenu, this);
                    p_oMenu.itemAddedEvent.subscribe(onItemAdded);
                    p_oMenu.focusEvent.subscribe(onMenuFocus);
                    p_oMenu.blurEvent.subscribe(onMenuBlur);
        
                    YAHOO.log(p_oMenu + " successfully registered.", "info", _MENUMANAGER);
        
                }
        
            },
    
        
            /**
            * @method removeMenu
            * @description Removes a menu from the collection of known menus.
            * @param {YAHOO.widget.Menu} p_oMenu Object specifying the Menu  
            * instance to be removed.
            */
            removeMenu: function (p_oMenu) {
    
                var sId,
                    aItems,
                    i;
        
                if (p_oMenu) {
    
                    sId = p_oMenu.id;
        
                    if ((sId in m_oMenus) && (m_oMenus[sId] == p_oMenu)) {

                        // Unregister each menu item

                        aItems = p_oMenu.getItems();

                        if (aItems && aItems.length > 0) {

                            i = aItems.length - 1;

                            do {

                                removeItem(aItems[i]);

                            }
                            while (i--);

                        }


                        // Unregister the menu

                        delete m_oMenus[sId];
            
                        YAHOO.log(p_oMenu + " successfully unregistered.", "info", _MENUMANAGER);
        

                        /*
                             Unregister the menu from the collection of 
                             visible menus
                        */

                        if ((sId in m_oVisibleMenus) && (m_oVisibleMenus[sId] == p_oMenu)) {
            
                            delete m_oVisibleMenus[sId];
                            
                            YAHOO.log(p_oMenu + " unregistered from the" + 
                                        " collection of visible menus.", "info", _MENUMANAGER);
       
                        }


                        // Unsubscribe event listeners

                        if (p_oMenu.cfg) {

                            p_oMenu.cfg.unsubscribeFromConfigEvent(_VISIBLE, 
                                onMenuVisibleConfigChange);
                            
                        }

                        p_oMenu.destroyEvent.unsubscribe(onMenuDestroy, 
                            p_oMenu);
                
                        p_oMenu.itemAddedEvent.unsubscribe(onItemAdded);
                        p_oMenu.focusEvent.unsubscribe(onMenuFocus);
                        p_oMenu.blurEvent.unsubscribe(onMenuBlur);

                    }
                
                }
    
            },
        
        
            /**
            * @method hideVisible
            * @description Hides all visible, dynamically positioned menus 
            * (excluding instances of YAHOO.widget.MenuBar).
            */
            hideVisible: function () {
        
                var oMenu;
        
                for (var i in m_oVisibleMenus) {
        
                    if (Lang.hasOwnProperty(m_oVisibleMenus, i)) {
        
                        oMenu = m_oVisibleMenus[i];
        
                        if (!(oMenu instanceof YAHOO.widget.MenuBar) && 
                            oMenu.cfg.getProperty(_POSITION) == _DYNAMIC) {
        
                            oMenu.hide();
        
                        }
        
                    }
        
                }        
    
            },


            /**
            * @method getVisible
            * @description Returns a collection of all visible menus registered
            * with the menu manger.
            * @return {Object}
            */
            getVisible: function () {
            
                return m_oVisibleMenus;
            
            },

    
            /**
            * @method getMenus
            * @description Returns a collection of all menus registered with the 
            * menu manger.
            * @return {Object}
            */
            getMenus: function () {
    
                return m_oMenus;
            
            },
    
    
            /**
            * @method getMenu
            * @description Returns a menu with the specified id.
            * @param {String} p_sId String specifying the id of the 
            * <code>&#60;div&#62;</code> element representing the menu to
            * be retrieved.
            * @return {YAHOO.widget.Menu}
            */
            getMenu: function (p_sId) {
                
                var returnVal;
                
                if (p_sId in m_oMenus) {
                
                    returnVal = m_oMenus[p_sId];
                
                }
            
                return returnVal;
            
            },
    
    
            /**
            * @method getMenuItem
            * @description Returns a menu item with the specified id.
            * @param {String} p_sId String specifying the id of the 
            * <code>&#60;li&#62;</code> element representing the menu item to
            * be retrieved.
            * @return {YAHOO.widget.MenuItem}
            */
            getMenuItem: function (p_sId) {
    
                var returnVal;
    
                if (p_sId in m_oItems) {
    
                    returnVal = m_oItems[p_sId];
                
                }
                
                return returnVal;
            
            },


            /**
            * @method getMenuItemGroup
            * @description Returns an array of menu item instances whose 
            * corresponding <code>&#60;li&#62;</code> elements are child 
            * nodes of the <code>&#60;ul&#62;</code> element with the 
            * specified id.
            * @param {String} p_sId String specifying the id of the 
            * <code>&#60;ul&#62;</code> element representing the group of 
            * menu items to be retrieved.
            * @return {Array}
            */
            getMenuItemGroup: function (p_sId) {

                var oUL = Dom.get(p_sId),
                    aItems,
                    oNode,
                    oItem,
                    sId,
                    returnVal;
    

                if (oUL && oUL.tagName && oUL.tagName.toUpperCase() == _UL) {

                    oNode = oUL.firstChild;

                    if (oNode) {

                        aItems = [];
                        
                        do {

                            sId = oNode.id;

                            if (sId) {
                            
                                oItem = this.getMenuItem(sId);
                                
                                if (oItem) {
                                
                                    aItems[aItems.length] = oItem;
                                
                                }
                            
                            }
                        
                        }
                        while ((oNode = oNode.nextSibling));


                        if (aItems.length > 0) {

                            returnVal = aItems;
                        
                        }

                    }
                
                }

                return returnVal;
            
            },

    
            /**
            * @method getFocusedMenuItem
            * @description Returns a reference to the menu item that currently 
            * has focus.
            * @return {YAHOO.widget.MenuItem}
            */
            getFocusedMenuItem: function () {
    
                return m_oFocusedMenuItem;
    
            },
    
    
            /**
            * @method getFocusedMenu
            * @description Returns a reference to the menu that currently 
            * has focus.
            * @return {YAHOO.widget.Menu}
            */
            getFocusedMenu: function () {

                var returnVal;
    
                if (m_oFocusedMenuItem) {
    
                    returnVal = m_oFocusedMenuItem.parent.getRoot();
                
                }
    
                return returnVal;
    
            },
    
        
            /**
            * @method toString
            * @description Returns a string representing the menu manager.
            * @return {String}
            */
            toString: function () {
            
                return _MENUMANAGER;
            
            }
    
        };
    
    }();

})();



(function () {

    var Lang = YAHOO.lang,

    // String constants
    
        _MENU = "Menu",
        _DIV_UPPERCASE = "DIV",
        _DIV_LOWERCASE = "div",
        _ID = "id",
        _SELECT = "SELECT",
        _XY = "xy",
        _Y = "y",
        _UL_UPPERCASE = "UL",
        _UL_LOWERCASE = "ul",
        _FIRST_OF_TYPE = "first-of-type",
        _LI = "LI",
        _OPTGROUP = "OPTGROUP",
        _OPTION = "OPTION",
        _DISABLED = "disabled",
        _NONE = "none",
        _SELECTED = "selected",
        _GROUP_INDEX = "groupindex",
        _INDEX = "index",
        _SUBMENU = "submenu",
        _VISIBLE = "visible",
        _HIDE_DELAY = "hidedelay",
        _POSITION = "position",
        _DYNAMIC = "dynamic",
        _STATIC = "static",
        _DYNAMIC_STATIC = _DYNAMIC + "," + _STATIC,
        _URL = "url",
        _HASH = "#",
        _TARGET = "target",
        _MAX_HEIGHT = "maxheight",
        _TOP_SCROLLBAR = "topscrollbar",
        _BOTTOM_SCROLLBAR = "bottomscrollbar",
        _UNDERSCORE = "_",
        _TOP_SCROLLBAR_DISABLED = _TOP_SCROLLBAR + _UNDERSCORE + _DISABLED,
        _BOTTOM_SCROLLBAR_DISABLED = _BOTTOM_SCROLLBAR + _UNDERSCORE + _DISABLED,
        _MOUSEMOVE = "mousemove",
        _SHOW_DELAY = "showdelay",
        _SUBMENU_HIDE_DELAY = "submenuhidedelay",
        _IFRAME = "iframe",
        _CONSTRAIN_TO_VIEWPORT = "constraintoviewport",
        _PREVENT_CONTEXT_OVERLAP = "preventcontextoverlap",
        _SUBMENU_ALIGNMENT = "submenualignment",
        _AUTO_SUBMENU_DISPLAY = "autosubmenudisplay",
        _CLICK_TO_HIDE = "clicktohide",
        _CONTAINER = "container",
        _SCROLL_INCREMENT = "scrollincrement",
        _MIN_SCROLL_HEIGHT = "minscrollheight",
        _CLASSNAME = "classname",
        _SHADOW = "shadow",
        _KEEP_OPEN = "keepopen",
        _HD = "hd",
        _HAS_TITLE = "hastitle",
        _CONTEXT = "context",
        _EMPTY_STRING = "",
        _MOUSEDOWN = "mousedown",
        _KEYDOWN = "keydown",
        _HEIGHT = "height",
        _WIDTH = "width",
        _PX = "px",
        _EFFECT = "effect",
        _MONITOR_RESIZE = "monitorresize",
        _DISPLAY = "display",
        _BLOCK = "block",
        _VISIBILITY = "visibility",
        _ABSOLUTE = "absolute",
        _ZINDEX = "zindex",
        _YUI_MENU_BODY_SCROLLED = "yui-menu-body-scrolled",
        _NON_BREAKING_SPACE = "&#32;",
        _SPACE = " ",
        _MOUSEOVER = "mouseover",
        _MOUSEOUT = "mouseout",
        _ITEM_ADDED = "itemAdded",
        _ITEM_REMOVED = "itemRemoved",
        _HIDDEN = "hidden",
        _YUI_MENU_SHADOW = "yui-menu-shadow",
        _YUI_MENU_SHADOW_VISIBLE = _YUI_MENU_SHADOW + "-visible",
        _YUI_MENU_SHADOW_YUI_MENU_SHADOW_VISIBLE = _YUI_MENU_SHADOW + _SPACE + _YUI_MENU_SHADOW_VISIBLE;


/**
* The Menu class creates a container that holds a vertical list representing 
* a set of options or commands.  Menu is the base class for all 
* menu containers. 
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;div&#62;</code> element of the menu.
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;select&#62;</code> element to be used as the data source 
* for the menu.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/
* level-one-html.html#ID-22445964">HTMLDivElement</a>} p_oElement Object 
* specifying the <code>&#60;div&#62;</code> element of the menu.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/
* level-one-html.html#ID-94282980">HTMLSelectElement</a>} p_oElement 
* Object specifying the <code>&#60;select&#62;</code> element to be used as 
* the data source for the menu.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the menu. See configuration class documentation for 
* more details.
* @namespace YAHOO.widget
* @class Menu
* @constructor
* @extends YAHOO.widget.Overlay
*/
YAHOO.widget.Menu = function (p_oElement, p_oConfig) {

    if (p_oConfig) {
        this.parent = p_oConfig.parent;
        this.lazyLoad = p_oConfig.lazyLoad || p_oConfig.lazyload;
        this.itemData = p_oConfig.itemData || p_oConfig.itemdata;
    }

    YAHOO.widget.Menu.superclass.constructor.call(this, p_oElement, p_oConfig);
};



/**
* @method checkPosition
* @description Checks to make sure that the value of the "position" property 
* is one of the supported strings. Returns true if the position is supported.
* @private
* @param {Object} p_sPosition String specifying the position of the menu.
* @return {Boolean}
*/
function checkPosition(p_sPosition) {

    var returnVal = false;

    if (Lang.isString(p_sPosition)) {

        returnVal = (_DYNAMIC_STATIC.indexOf((p_sPosition.toLowerCase())) != -1);

    }

    return returnVal;

}


var Dom = YAHOO.util.Dom,
    Event = YAHOO.util.Event,
    Module = YAHOO.widget.Module,
    Overlay = YAHOO.widget.Overlay,
    Menu = YAHOO.widget.Menu,
    MenuManager = YAHOO.widget.MenuManager,
    CustomEvent = YAHOO.util.CustomEvent,
    UA = YAHOO.env.ua,
    
    m_oShadowTemplate,

    bFocusListenerInitialized = false,

    oFocusedElement,

    EVENT_TYPES = [
    
        ["mouseOverEvent", _MOUSEOVER],
        ["mouseOutEvent", _MOUSEOUT],
        ["mouseDownEvent", _MOUSEDOWN],
        ["mouseUpEvent", "mouseup"],
        ["clickEvent", "click"],
        ["keyPressEvent", "keypress"],
        ["keyDownEvent", _KEYDOWN],
        ["keyUpEvent", "keyup"],
        ["focusEvent", "focus"],
        ["blurEvent", "blur"],
        ["itemAddedEvent", _ITEM_ADDED],
        ["itemRemovedEvent", _ITEM_REMOVED]

    ],

    VISIBLE_CONFIG =  { 
        key: _VISIBLE, 
        value: false, 
        validator: Lang.isBoolean
    }, 

    CONSTRAIN_TO_VIEWPORT_CONFIG =  {
        key: _CONSTRAIN_TO_VIEWPORT, 
        value: true, 
        validator: Lang.isBoolean, 
        supercedes: [_IFRAME,"x",_Y,_XY]
    }, 

    PREVENT_CONTEXT_OVERLAP_CONFIG =  {
        key: _PREVENT_CONTEXT_OVERLAP,
        value: true,
        validator: Lang.isBoolean,  
        supercedes: [_CONSTRAIN_TO_VIEWPORT]
    },

    POSITION_CONFIG =  { 
        key: _POSITION, 
        value: _DYNAMIC, 
        validator: checkPosition, 
        supercedes: [_VISIBLE, _IFRAME]
    }, 

    SUBMENU_ALIGNMENT_CONFIG =  { 
        key: _SUBMENU_ALIGNMENT, 
        value: ["tl","tr"]
    },

    AUTO_SUBMENU_DISPLAY_CONFIG =  { 
        key: _AUTO_SUBMENU_DISPLAY, 
        value: true, 
        validator: Lang.isBoolean,
        suppressEvent: true
    }, 

    SHOW_DELAY_CONFIG =  { 
        key: _SHOW_DELAY, 
        value: 250, 
        validator: Lang.isNumber, 
        suppressEvent: true
    }, 

    HIDE_DELAY_CONFIG =  { 
        key: _HIDE_DELAY, 
        value: 0, 
        validator: Lang.isNumber, 
        suppressEvent: true
    }, 

    SUBMENU_HIDE_DELAY_CONFIG =  { 
        key: _SUBMENU_HIDE_DELAY, 
        value: 250, 
        validator: Lang.isNumber,
        suppressEvent: true
    }, 

    CLICK_TO_HIDE_CONFIG =  { 
        key: _CLICK_TO_HIDE, 
        value: true, 
        validator: Lang.isBoolean,
        suppressEvent: true
    },

    CONTAINER_CONFIG =  { 
        key: _CONTAINER,
        suppressEvent: true
    }, 

    SCROLL_INCREMENT_CONFIG =  { 
        key: _SCROLL_INCREMENT, 
        value: 1, 
        validator: Lang.isNumber,
        supercedes: [_MAX_HEIGHT],
        suppressEvent: true
    },

    MIN_SCROLL_HEIGHT_CONFIG =  { 
        key: _MIN_SCROLL_HEIGHT, 
        value: 90, 
        validator: Lang.isNumber,
        supercedes: [_MAX_HEIGHT],
        suppressEvent: true
    },    

    MAX_HEIGHT_CONFIG =  { 
        key: _MAX_HEIGHT, 
        value: 0, 
        validator: Lang.isNumber,
        supercedes: [_IFRAME],
        suppressEvent: true
    }, 

    CLASS_NAME_CONFIG =  { 
        key: _CLASSNAME, 
        value: null, 
        validator: Lang.isString,
        suppressEvent: true
    }, 

    DISABLED_CONFIG =  { 
        key: _DISABLED, 
        value: false, 
        validator: Lang.isBoolean,
        suppressEvent: true
    },
    
    SHADOW_CONFIG =  { 
        key: _SHADOW, 
        value: true, 
        validator: Lang.isBoolean,
        suppressEvent: true,
        supercedes: [_VISIBLE]
    },
    
    KEEP_OPEN_CONFIG = {
        key: _KEEP_OPEN, 
        value: false, 
        validator: Lang.isBoolean
    };


function onDocFocus(event) {

    oFocusedElement = Event.getTarget(event);

}



YAHOO.lang.extend(Menu, Overlay, {


// Constants


/**
* @property CSS_CLASS_NAME
* @description String representing the CSS class(es) to be applied to the 
* menu's <code>&#60;div&#62;</code> element.
* @default "yuimenu"
* @final
* @type String
*/
CSS_CLASS_NAME: "yuimenu",


/**
* @property ITEM_TYPE
* @description Object representing the type of menu item to instantiate and 
* add when parsing the child nodes (either <code>&#60;li&#62;</code> element, 
* <code>&#60;optgroup&#62;</code> element or <code>&#60;option&#62;</code>) 
* of the menu's source HTML element.
* @default YAHOO.widget.MenuItem
* @final
* @type YAHOO.widget.MenuItem
*/
ITEM_TYPE: null,


/**
* @property GROUP_TITLE_TAG_NAME
* @description String representing the tagname of the HTML element used to 
* title the menu's item groups.
* @default H6
* @final
* @type String
*/
GROUP_TITLE_TAG_NAME: "h6",


/**
* @property OFF_SCREEN_POSITION
* @description Array representing the default x and y position that a menu 
* should have when it is positioned outside the viewport by the 
* "poistionOffScreen" method.
* @default "-999em"
* @final
* @type String
*/
OFF_SCREEN_POSITION: "-999em",


// Private properties


/** 
* @property _useHideDelay
* @description Boolean indicating if the "mouseover" and "mouseout" event 
* handlers used for hiding the menu via a call to "YAHOO.lang.later" have 
* already been assigned.
* @default false
* @private
* @type Boolean
*/
_useHideDelay: false,


/**
* @property _bHandledMouseOverEvent
* @description Boolean indicating the current state of the menu's 
* "mouseover" event.
* @default false
* @private
* @type Boolean
*/
_bHandledMouseOverEvent: false,


/**
* @property _bHandledMouseOutEvent
* @description Boolean indicating the current state of the menu's
* "mouseout" event.
* @default false
* @private
* @type Boolean
*/
_bHandledMouseOutEvent: false,


/**
* @property _aGroupTitleElements
* @description Array of HTML element used to title groups of menu items.
* @default []
* @private
* @type Array
*/
_aGroupTitleElements: null,


/**
* @property _aItemGroups
* @description Multi-dimensional Array representing the menu items as they
* are grouped in the menu.
* @default []
* @private
* @type Array
*/
_aItemGroups: null,


/**
* @property _aListElements
* @description Array of <code>&#60;ul&#62;</code> elements, each of which is 
* the parent node for each item's <code>&#60;li&#62;</code> element.
* @default []
* @private
* @type Array
*/
_aListElements: null,


/**
* @property _nCurrentMouseX
* @description The current x coordinate of the mouse inside the area of 
* the menu.
* @default 0
* @private
* @type Number
*/
_nCurrentMouseX: 0,


/**
* @property _bStopMouseEventHandlers
* @description Stops "mouseover," "mouseout," and "mousemove" event handlers 
* from executing.
* @default false
* @private
* @type Boolean
*/
_bStopMouseEventHandlers: false,


/**
* @property _sClassName
* @description The current value of the "classname" configuration attribute.
* @default null
* @private
* @type String
*/
_sClassName: null,



// Public properties


/**
* @property lazyLoad
* @description Boolean indicating if the menu's "lazy load" feature is 
* enabled.  If set to "true," initialization and rendering of the menu's 
* items will be deferred until the first time it is made visible.  This 
* property should be set via the constructor using the configuration 
* object literal.
* @default false
* @type Boolean
*/
lazyLoad: false,


/**
* @property itemData
* @description Array of items to be added to the menu.  The array can contain 
* strings representing the text for each item to be created, object literals 
* representing the menu item configuration properties, or MenuItem instances.  
* This property should be set via the constructor using the configuration 
* object literal.
* @default null
* @type Array
*/
itemData: null,


/**
* @property activeItem
* @description Object reference to the item in the menu that has is selected.
* @default null
* @type YAHOO.widget.MenuItem
*/
activeItem: null,


/**
* @property parent
* @description Object reference to the menu's parent menu or menu item.  
* This property can be set via the constructor using the configuration 
* object literal.
* @default null
* @type YAHOO.widget.MenuItem
*/
parent: null,


/**
* @property srcElement
* @description Object reference to the HTML element (either 
* <code>&#60;select&#62;</code> or <code>&#60;div&#62;</code>) used to 
* create the menu.
* @default null
* @type <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/
* level-one-html.html#ID-94282980">HTMLSelectElement</a>|<a 
* href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.
* html#ID-22445964">HTMLDivElement</a>
*/
srcElement: null,



// Events


/**
* @event mouseOverEvent
* @description Fires when the mouse has entered the menu.  Passes back 
* the DOM Event object as an argument.
*/


/**
* @event mouseOutEvent
* @description Fires when the mouse has left the menu.  Passes back the DOM 
* Event object as an argument.
* @type YAHOO.util.CustomEvent
*/


/**
* @event mouseDownEvent
* @description Fires when the user mouses down on the menu.  Passes back the 
* DOM Event object as an argument.
* @type YAHOO.util.CustomEvent
*/


/**
* @event mouseUpEvent
* @description Fires when the user releases a mouse button while the mouse is 
* over the menu.  Passes back the DOM Event object as an argument.
* @type YAHOO.util.CustomEvent
*/


/**
* @event clickEvent
* @description Fires when the user clicks the on the menu.  Passes back the 
* DOM Event object as an argument.
* @type YAHOO.util.CustomEvent
*/


/**
* @event keyPressEvent
* @description Fires when the user presses an alphanumeric key when one of the
* menu's items has focus.  Passes back the DOM Event object as an argument.
* @type YAHOO.util.CustomEvent
*/


/**
* @event keyDownEvent
* @description Fires when the user presses a key when one of the menu's items 
* has focus.  Passes back the DOM Event object as an argument.
* @type YAHOO.util.CustomEvent
*/


/**
* @event keyUpEvent
* @description Fires when the user releases a key when one of the menu's items 
* has focus.  Passes back the DOM Event object as an argument.
* @type YAHOO.util.CustomEvent
*/


/**
* @event itemAddedEvent
* @description Fires when an item is added to the menu.
* @type YAHOO.util.CustomEvent
*/


/**
* @event itemRemovedEvent
* @description Fires when an item is removed to the menu.
* @type YAHOO.util.CustomEvent
*/


/**
* @method init
* @description The Menu class's initialization method. 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.
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;div&#62;</code> element of the menu.
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;select&#62;</code> element to be used as the data source 
* for the menu.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/
* level-one-html.html#ID-22445964">HTMLDivElement</a>} p_oElement Object 
* specifying the <code>&#60;div&#62;</code> element of the menu.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/
* level-one-html.html#ID-94282980">HTMLSelectElement</a>} p_oElement 
* Object specifying the <code>&#60;select&#62;</code> element to be used as 
* the data source for the menu.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the menu. See configuration class documentation for 
* more details.
*/
init: function (p_oElement, p_oConfig) {

    this._aItemGroups = [];
    this._aListElements = [];
    this._aGroupTitleElements = [];

    if (!this.ITEM_TYPE) {

        this.ITEM_TYPE = YAHOO.widget.MenuItem;

    }


    var oElement;

    if (Lang.isString(p_oElement)) {

        oElement = Dom.get(p_oElement);

    }
    else if (p_oElement.tagName) {

        oElement = p_oElement;

    }


    if (oElement && oElement.tagName) {

        switch(oElement.tagName.toUpperCase()) {
    
            case _DIV_UPPERCASE:

                this.srcElement = oElement;

                if (!oElement.id) {

                    oElement.setAttribute(_ID, Dom.generateId());

                }


                /* 
                    Note: we don't pass the user config in here yet 
                    because we only want it executed once, at the lowest 
                    subclass level.
                */ 
            
                Menu.superclass.init.call(this, oElement);

                this.beforeInitEvent.fire(Menu);

                YAHOO.log("Source element: " + this.srcElement.tagName, "info", this.toString());
    
            break;
    
            case _SELECT:
    
                this.srcElement = oElement;

    
                /*
                    The source element is not something that we can use 
                    outright, so we need to create a new Overlay

                    Note: we don't pass the user config in here yet 
                    because we only want it executed once, at the lowest 
                    subclass level.
                */ 

                Menu.superclass.init.call(this, Dom.generateId());

                this.beforeInitEvent.fire(Menu);

                YAHOO.log("Source element: " + this.srcElement.tagName, "info", this.toString());

            break;

        }

    }
    else {

        /* 
            Note: we don't pass the user config in here yet 
            because we only want it executed once, at the lowest 
            subclass level.
        */ 
    
        Menu.superclass.init.call(this, p_oElement);

        this.beforeInitEvent.fire(Menu);

        YAHOO.log("No source element found.  Created element with id: " + this.id, "info", this.toString());

    }


    if (this.element) {
        Dom.addClass(this.element, this.CSS_CLASS_NAME);

        // Subscribe to Custom Events
        this.initEvent.subscribe(this._onInit);
        this.beforeRenderEvent.subscribe(this._onBeforeRender);
        this.renderEvent.subscribe(this._onRender);
        this.beforeShowEvent.subscribe(this._onBeforeShow);
        this.hideEvent.subscribe(this._onHide);
        this.showEvent.subscribe(this._onShow);
        this.beforeHideEvent.subscribe(this._onBeforeHide);
        this.mouseOverEvent.subscribe(this._onMouseOver);
        this.mouseOutEvent.subscribe(this._onMouseOut);
        this.clickEvent.subscribe(this._onClick);
        this.keyDownEvent.subscribe(this._onKeyDown);
        this.keyPressEvent.subscribe(this._onKeyPress);
        this.blurEvent.subscribe(this._onBlur);

        if (!bFocusListenerInitialized) {
            Event.onFocus(document, onDocFocus);
            bFocusListenerInitialized = true;
        }

        //	Fixes an issue in Firefox 2 and Webkit where Dom's "getX" and "getY" 
        //	methods return values that don't take scrollTop into consideration 

        if ((UA.gecko && UA.gecko < 1.9) || (UA.webkit && UA.webkit < 523)) {
            this.cfg.subscribeToConfigEvent(_Y, this._onYChange);
        }


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

        // Register the Menu instance with the MenuManager
        MenuManager.addMenu(this);

        this.initEvent.fire(Menu);
    }
},



// Private methods


/**
* @method _initSubTree
* @description Iterates the childNodes of the source element to find nodes 
* used to instantiate menu and menu items.
* @private
*/
_initSubTree: function () {

    var oSrcElement = this.srcElement,
        sSrcElementTagName,
        nGroup,
        sGroupTitleTagName,
        oNode,
        aListElements,
        nListElements,
        i;


    if (oSrcElement) {
    
        sSrcElementTagName = 
            (oSrcElement.tagName && oSrcElement.tagName.toUpperCase());


        if (sSrcElementTagName == _DIV_UPPERCASE) {
    
            //  Populate the collection of item groups and item group titles
    
            oNode = this.body.firstChild;
    

            if (oNode) {
    
                nGroup = 0;
                sGroupTitleTagName = this.GROUP_TITLE_TAG_NAME.toUpperCase();
        
                do {
        

                    if (oNode && oNode.tagName) {
        
                        switch (oNode.tagName.toUpperCase()) {
        
                            case sGroupTitleTagName:
                            
                                this._aGroupTitleElements[nGroup] = oNode;
        
                            break;
        
                            case _UL_UPPERCASE:
        
                                this._aListElements[nGroup] = oNode;
                                this._aItemGroups[nGroup] = [];
                                nGroup++;
        
                            break;
        
                        }
                    
                    }
        
                }
                while ((oNode = oNode.nextSibling));
        
        
                /*
                    Apply the "first-of-type" class to the first UL to mimic 
                    the ":first-of-type" CSS3 psuedo class.
                */
        
                if (this._aListElements[0]) {
        
                    Dom.addClass(this._aListElements[0], _FIRST_OF_TYPE);
        
                }
            
            }
    
        }
    
    
        oNode = null;
    
        YAHOO.log("Searching DOM for items to initialize.", "info", this.toString());
    

        if (sSrcElementTagName) {
    
            switch (sSrcElementTagName) {
        
                case _DIV_UPPERCASE:

                    aListElements = this._aListElements;
                    nListElements = aListElements.length;
        
                    if (nListElements > 0) {
        
                        YAHOO.log("Found " + nListElements + " item groups to initialize.", 
                                    "info", this.toString());
        
                        i = nListElements - 1;
        
                        do {
        
                            oNode = aListElements[i].firstChild;
            
                            if (oNode) {

                                YAHOO.log("Scanning " + 
                                    aListElements[i].childNodes.length + 
                                    " child nodes for items to initialize.", "info", this.toString());
            
                                do {
                
                                    if (oNode && oNode.tagName && 
                                        oNode.tagName.toUpperCase() == _LI) {
                
                                        YAHOO.log("Initializing " + 
                                            oNode.tagName + " node.", "info", this.toString());
        
                                        this.addItem(new this.ITEM_TYPE(oNode, 
                                                    { parent: this }), i);
            
                                    }
                        
                                }
                                while ((oNode = oNode.nextSibling));
                            
                            }
                    
                        }
                        while (i--);
        
                    }
        
                break;
        
                case _SELECT:
        
                    YAHOO.log("Scanning " +  
                        oSrcElement.childNodes.length + 
                        " child nodes for items to initialize.", "info", this.toString());
        
                    oNode = oSrcElement.firstChild;
        
                    do {
        
                        if (oNode && oNode.tagName) {
                        
                            switch (oNode.tagName.toUpperCase()) {
            
                                case _OPTGROUP:
                                case _OPTION:
            
                                    YAHOO.log("Initializing " +  
                                        oNode.tagName + " node.", "info", this.toString());
            
                                    this.addItem(
                                            new this.ITEM_TYPE(
                                                    oNode, 
                                                    { parent: this }
                                                )
                                            );
            
                                break;
            
                            }
    
                        }
        
                    }
                    while ((oNode = oNode.nextSibling));
        
                break;
        
            }
    
        }    
    
    }

},


/**
* @method _getFirstEnabledItem
* @description Returns the first enabled item in the menu.
* @return {YAHOO.widget.MenuItem}
* @private
*/
_getFirstEnabledItem: function () {

    var aItems = this.getItems(),
        nItems = aItems.length,
        oItem,
        returnVal;
    

    for(var i=0; i<nItems; i++) {

        oItem = aItems[i];

        if (oItem && !oItem.cfg.getProperty(_DISABLED) && oItem.element.style.display != _NONE) {

            returnVal = oItem;
            break;

        }
    
    }
    
    return returnVal;
    
},


/**
* @method _addItemToGroup
* @description Adds a menu item to a group.
* @private
* @param {Number} p_nGroupIndex Number indicating the group to which the 
* item belongs.
* @param {YAHOO.widget.MenuItem} p_oItem Object reference for the MenuItem 
* instance to be added to the menu.
* @param {HTML} p_oItem String or markup specifying the content of the item to be added 
* to the menu. The item is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
* @param {Object} p_oItem Object literal containing a set of menu item 
* configuration properties.
* @param {Number} p_nItemIndex Optional. Number indicating the index at 
* which the menu item should be added.
* @return {YAHOO.widget.MenuItem}
*/
_addItemToGroup: function (p_nGroupIndex, p_oItem, p_nItemIndex) {

    var oItem,
        nGroupIndex,
        aGroup,
        oGroupItem,
        bAppend,
        oNextItemSibling,
        nItemIndex,
        returnVal;


    function getNextItemSibling(p_aArray, p_nStartIndex) {

        return (p_aArray[p_nStartIndex] || getNextItemSibling(p_aArray, (p_nStartIndex+1)));

    }


    if (p_oItem instanceof this.ITEM_TYPE) {

        oItem = p_oItem;
        oItem.parent = this;

    }
    else if (Lang.isString(p_oItem)) {

        oItem = new this.ITEM_TYPE(p_oItem, { parent: this });
    
    }
    else if (Lang.isObject(p_oItem)) {

        p_oItem.parent = this;

        oItem = new this.ITEM_TYPE(p_oItem.text, p_oItem);

    }


    if (oItem) {

        if (oItem.cfg.getProperty(_SELECTED)) {

            this.activeItem = oItem;
        
        }


        nGroupIndex = Lang.isNumber(p_nGroupIndex) ? p_nGroupIndex : 0;
        aGroup = this._getItemGroup(nGroupIndex);



        if (!aGroup) {

            aGroup = this._createItemGroup(nGroupIndex);

        }


        if (Lang.isNumber(p_nItemIndex)) {

            bAppend = (p_nItemIndex >= aGroup.length);            


            if (aGroup[p_nItemIndex]) {
    
                aGroup.splice(p_nItemIndex, 0, oItem);
    
            }
            else {
    
                aGroup[p_nItemIndex] = oItem;
    
            }


            oGroupItem = aGroup[p_nItemIndex];

            if (oGroupItem) {

                if (bAppend && (!oGroupItem.element.parentNode || 
                        oGroupItem.element.parentNode.nodeType == 11)) {
        
                    this._aListElements[nGroupIndex].appendChild(oGroupItem.element);
    
                }
                else {
    
                    oNextItemSibling = getNextItemSibling(aGroup, (p_nItemIndex+1));
    
                    if (oNextItemSibling && (!oGroupItem.element.parentNode || 
                            oGroupItem.element.parentNode.nodeType == 11)) {
            
                        this._aListElements[nGroupIndex].insertBefore(
                                oGroupItem.element, oNextItemSibling.element);
        
                    }
    
                }
    

                oGroupItem.parent = this;
        
                this._subscribeToItemEvents(oGroupItem);
    
                this._configureSubmenu(oGroupItem);
                
                this._updateItemProperties(nGroupIndex);
        
                YAHOO.log("Item inserted." + 
                    " Text: " + oGroupItem.cfg.getProperty("text") + ", " + 
                    " Index: " + oGroupItem.index + ", " + 
                    " Group Index: " + oGroupItem.groupIndex, "info", this.toString());

                this.itemAddedEvent.fire(oGroupItem);
                this.changeContentEvent.fire();

                returnVal = oGroupItem;
    
            }

        }
        else {
    
            nItemIndex = aGroup.length;
    
            aGroup[nItemIndex] = oItem;

            oGroupItem = aGroup[nItemIndex];
    

            if (oGroupItem) {
    
                if (!Dom.isAncestor(this._aListElements[nGroupIndex], oGroupItem.element)) {
    
                    this._aListElements[nGroupIndex].appendChild(oGroupItem.element);
    
                }
    
                oGroupItem.element.setAttribute(_GROUP_INDEX, nGroupIndex);
                oGroupItem.element.setAttribute(_INDEX, nItemIndex);
        
                oGroupItem.parent = this;
    
                oGroupItem.index = nItemIndex;
                oGroupItem.groupIndex = nGroupIndex;
        
                this._subscribeToItemEvents(oGroupItem);
    
                this._configureSubmenu(oGroupItem);
    
                if (nItemIndex === 0) {
        
                    Dom.addClass(oGroupItem.element, _FIRST_OF_TYPE);
        
                }

                YAHOO.log("Item added." + 
                    " Text: " + oGroupItem.cfg.getProperty("text") + ", " + 
                    " Index: " + oGroupItem.index + ", " + 
                    " Group Index: " + oGroupItem.groupIndex, "info", this.toString());
        

                this.itemAddedEvent.fire(oGroupItem);
                this.changeContentEvent.fire();

                returnVal = oGroupItem;
    
            }
    
        }

    }
    
    return returnVal;
    
},


/**
* @method _removeItemFromGroupByIndex
* @description Removes a menu item from a group by index.  Returns the menu 
* item that was removed.
* @private
* @param {Number} p_nGroupIndex Number indicating the group to which the menu 
* item belongs.
* @param {Number} p_nItemIndex Number indicating the index of the menu item 
* to be removed.
* @return {YAHOO.widget.MenuItem}
*/
_removeItemFromGroupByIndex: function (p_nGroupIndex, p_nItemIndex) {

    var nGroupIndex = Lang.isNumber(p_nGroupIndex) ? p_nGroupIndex : 0,
        aGroup = this._getItemGroup(nGroupIndex),
        aArray,
        oItem,
        oUL;

    if (aGroup) {

        aArray = aGroup.splice(p_nItemIndex, 1);
        oItem = aArray[0];
    
        if (oItem) {
    
            // Update the index and className properties of each member        
            
            this._updateItemProperties(nGroupIndex);
    
            if (aGroup.length === 0) {
    
                // Remove the UL
    
                oUL = this._aListElements[nGroupIndex];
    
                if (oUL && oUL.parentNode) {
                    oUL.parentNode.removeChild(oUL);
                }
    
                // Remove the group from the array of items
    
                this._aItemGroups.splice(nGroupIndex, 1);
    
    
                // Remove the UL from the array of ULs
    
                this._aListElements.splice(nGroupIndex, 1);
    
    
                /*
                     Assign the "first-of-type" class to the new first UL 
                     in the collection
                */
    
                oUL = this._aListElements[0];
    
                if (oUL) {
    
                    Dom.addClass(oUL, _FIRST_OF_TYPE);
    
                }            
    
            }
    

            this.itemRemovedEvent.fire(oItem);
            this.changeContentEvent.fire();
    
        }

    }

    // Return a reference to the item that was removed

    return oItem;
    
},


/**
* @method _removeItemFromGroupByValue
* @description Removes a menu item from a group by reference.  Returns the 
* menu item that was removed.
* @private
* @param {Number} p_nGroupIndex Number indicating the group to which the
* menu item belongs.
* @param {YAHOO.widget.MenuItem} p_oItem Object reference for the MenuItem 
* instance to be removed.
* @return {YAHOO.widget.MenuItem}
*/    
_removeItemFromGroupByValue: function (p_nGroupIndex, p_oItem) {

    var aGroup = this._getItemGroup(p_nGroupIndex),
        nItems,
        nItemIndex,
        returnVal,
        i;

    if (aGroup) {

        nItems = aGroup.length;
        nItemIndex = -1;
    
        if (nItems > 0) {
    
            i = nItems-1;
        
            do {
        
                if (aGroup[i] == p_oItem) {
        
                    nItemIndex = i;
                    break;    
        
                }
        
            }
            while (i--);
        
            if (nItemIndex > -1) {
        
                returnVal = this._removeItemFromGroupByIndex(p_nGroupIndex, nItemIndex);
        
            }
    
        }
    
    }
    
    return returnVal;

},


/**
* @method _updateItemProperties
* @description Updates the "index," "groupindex," and "className" properties 
* of the menu items in the specified group. 
* @private
* @param {Number} p_nGroupIndex Number indicating the group of items to update.
*/
_updateItemProperties: function (p_nGroupIndex) {

    var aGroup = this._getItemGroup(p_nGroupIndex),
        nItems = aGroup.length,
        oItem,
        oLI,
        i;


    if (nItems > 0) {

        i = nItems - 1;

        // Update the index and className properties of each member
    
        do {

            oItem = aGroup[i];

            if (oItem) {
    
                oLI = oItem.element;

                oItem.index = i;
                oItem.groupIndex = p_nGroupIndex;

                oLI.setAttribute(_GROUP_INDEX, p_nGroupIndex);
                oLI.setAttribute(_INDEX, i);

                Dom.removeClass(oLI, _FIRST_OF_TYPE);

            }
    
        }
        while (i--);


        if (oLI) {

            Dom.addClass(oLI, _FIRST_OF_TYPE);

        }

    }

},


/**
* @method _createItemGroup
* @description Creates a new menu item group (array) and its associated 
* <code>&#60;ul&#62;</code> element. Returns an aray of menu item groups.
* @private
* @param {Number} p_nIndex Number indicating the group to create.
* @return {Array}
*/
_createItemGroup: function (p_nIndex) {

    var oUL,
        returnVal;

    if (!this._aItemGroups[p_nIndex]) {

        this._aItemGroups[p_nIndex] = [];

        oUL = document.createElement(_UL_LOWERCASE);

        this._aListElements[p_nIndex] = oUL;

        returnVal = this._aItemGroups[p_nIndex];

    }
    
    return returnVal;

},


/**
* @method _getItemGroup
* @description Returns the menu item group at the specified index.
* @private
* @param {Number} p_nIndex Number indicating the index of the menu item group 
* to be retrieved.
* @return {Array}
*/
_getItemGroup: function (p_nIndex) {

    var nIndex = Lang.isNumber(p_nIndex) ? p_nIndex : 0,
        aGroups = this._aItemGroups,
        returnVal;

    if (nIndex in aGroups) {

        returnVal = aGroups[nIndex];

    }
    
    return returnVal;

},


/**
* @method _configureSubmenu
* @description Subscribes the menu item's submenu to its parent menu's events.
* @private
* @param {YAHOO.widget.MenuItem} p_oItem Object reference for the MenuItem 
* instance with the submenu to be configured.
*/
_configureSubmenu: function (p_oItem) {

    var oSubmenu = p_oItem.cfg.getProperty(_SUBMENU);

    if (oSubmenu) {
            
        /*
            Listen for configuration changes to the parent menu 
            so they they can be applied to the submenu.
        */

        this.cfg.configChangedEvent.subscribe(this._onParentMenuConfigChange, oSubmenu, true);

        this.renderEvent.subscribe(this._onParentMenuRender, oSubmenu, true);

    }

},




/**
* @method _subscribeToItemEvents
* @description Subscribes a menu to a menu item's event.
* @private
* @param {YAHOO.widget.MenuItem} p_oItem Object reference for the MenuItem 
* instance whose events should be subscribed to.
*/
_subscribeToItemEvents: function (p_oItem) {

    p_oItem.destroyEvent.subscribe(this._onMenuItemDestroy, p_oItem, this);
    p_oItem.cfg.configChangedEvent.subscribe(this._onMenuItemConfigChange, p_oItem, this);

},


/**
* @method _onVisibleChange
* @description Change event handler for the the menu's "visible" configuration
* property.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onVisibleChange: function (p_sType, p_aArgs) {

    var bVisible = p_aArgs[0];
    
    if (bVisible) {

        Dom.addClass(this.element, _VISIBLE);

    }
    else {

        Dom.removeClass(this.element, _VISIBLE);

    }

},


/**
* @method _cancelHideDelay
* @description Cancels the call to "hideMenu."
* @private
*/
_cancelHideDelay: function () {

    var oTimer = this.getRoot()._hideDelayTimer;

    if (oTimer) {

        oTimer.cancel();

    }

},


/**
* @method _execHideDelay
* @description Hides the menu after the number of milliseconds specified by 
* the "hidedelay" configuration property.
* @private
*/
_execHideDelay: function () {

    this._cancelHideDelay();

    var oRoot = this.getRoot();

    oRoot._hideDelayTimer = Lang.later(oRoot.cfg.getProperty(_HIDE_DELAY), this, function () {
    
        if (oRoot.activeItem) {
            if (oRoot.hasFocus()) {
                oRoot.activeItem.focus();
            }
            oRoot.clearActiveItem();
        }

        if (oRoot == this && !(this instanceof YAHOO.widget.MenuBar) && 
            this.cfg.getProperty(_POSITION) == _DYNAMIC) {
            this.hide();
        }
    });

},


/**
* @method _cancelShowDelay
* @description Cancels the call to the "showMenu."
* @private
*/
_cancelShowDelay: function () {
    var oTimer = this.getRoot()._showDelayTimer;
    if (oTimer) {
        oTimer.cancel();
    }
},


/**
* @method _execSubmenuHideDelay
* @description Hides a submenu after the number of milliseconds specified by 
* the "submenuhidedelay" configuration property have elapsed.
* @private
* @param {YAHOO.widget.Menu} p_oSubmenu Object specifying the submenu that  
* should be hidden.
* @param {Number} p_nMouseX The x coordinate of the mouse when it left 
* the specified submenu's parent menu item.
* @param {Number} p_nHideDelay The number of milliseconds that should ellapse
* before the submenu is hidden.
*/
_execSubmenuHideDelay: function (p_oSubmenu, p_nMouseX, p_nHideDelay) {

    p_oSubmenu._submenuHideDelayTimer = Lang.later(50, this, function () {

        if (this._nCurrentMouseX > (p_nMouseX + 10)) {

            p_oSubmenu._submenuHideDelayTimer = Lang.later(p_nHideDelay, p_oSubmenu, function () {
        
                this.hide();

            });

        }
        else {

            p_oSubmenu.hide();
        
        }
    
    });

},



// Protected methods


/**
* @method _disableScrollHeader
* @description Disables the header used for scrolling the body of the menu.
* @protected
*/
_disableScrollHeader: function () {

    if (!this._bHeaderDisabled) {

        Dom.addClass(this.header, _TOP_SCROLLBAR_DISABLED);
        this._bHeaderDisabled = true;

    }

},


/**
* @method _disableScrollFooter
* @description Disables the footer used for scrolling the body of the menu.
* @protected
*/
_disableScrollFooter: function () {

    if (!this._bFooterDisabled) {

        Dom.addClass(this.footer, _BOTTOM_SCROLLBAR_DISABLED);
        this._bFooterDisabled = true;

    }

},


/**
* @method _enableScrollHeader
* @description Enables the header used for scrolling the body of the menu.
* @protected
*/
_enableScrollHeader: function () {

    if (this._bHeaderDisabled) {

        Dom.removeClass(this.header, _TOP_SCROLLBAR_DISABLED);
        this._bHeaderDisabled = false;

    }

},


/**
* @method _enableScrollFooter
* @description Enables the footer used for scrolling the body of the menu.
* @protected
*/
_enableScrollFooter: function () {

    if (this._bFooterDisabled) {

        Dom.removeClass(this.footer, _BOTTOM_SCROLLBAR_DISABLED);
        this._bFooterDisabled = false;

    }

},


/**
* @method _onMouseOver
* @description "mouseover" event handler for the menu.
* @protected
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onMouseOver: function (p_sType, p_aArgs) {

    var oEvent = p_aArgs[0],
        oItem = p_aArgs[1],
        oTarget = Event.getTarget(oEvent),
        oRoot = this.getRoot(),
        oSubmenuHideDelayTimer = this._submenuHideDelayTimer,
        oParentMenu,
        nShowDelay,
        bShowDelay,
        oActiveItem,
        oItemCfg,
        oSubmenu;


    var showSubmenu = function () {

        if (this.parent.cfg.getProperty(_SELECTED)) {

            this.show();

        }

    };


    if (!this._bStopMouseEventHandlers) {
    
        if (!this._bHandledMouseOverEvent && (oTarget == this.element || 
                Dom.isAncestor(this.element, oTarget))) {
    
            // Menu mouseover logic

            if (this._useHideDelay) {
                this._cancelHideDelay();
            }
    
            this._nCurrentMouseX = 0;
    
            Event.on(this.element, _MOUSEMOVE, this._onMouseMove, this, true);


            /*
                If the mouse is moving from the submenu back to its corresponding menu item, 
                don't hide the submenu or clear the active MenuItem.
            */

            if (!(oItem && Dom.isAncestor(oItem.element, Event.getRelatedTarget(oEvent)))) {

                this.clearActiveItem();

            }
    

            if (this.parent && oSubmenuHideDelayTimer) {
    
                oSubmenuHideDelayTimer.cancel();
    
                this.parent.cfg.setProperty(_SELECTED, true);
    
                oParentMenu = this.parent.parent;
    
                oParentMenu._bHandledMouseOutEvent = true;
                oParentMenu._bHandledMouseOverEvent = false;
    
            }
    
    
            this._bHandledMouseOverEvent = true;
            this._bHandledMouseOutEvent = false;
        
        }
    
    
        if (oItem && !oItem.handledMouseOverEvent && !oItem.cfg.getProperty(_DISABLED) && 
            (oTarget == oItem.element || Dom.isAncestor(oItem.element, oTarget))) {
    
            // Menu Item mouseover logic
    
            nShowDelay = this.cfg.getProperty(_SHOW_DELAY);
            bShowDelay = (nShowDelay > 0);
    
    
            if (bShowDelay) {
            
                this._cancelShowDelay();
            
            }
    
    
            oActiveItem = this.activeItem;
        
            if (oActiveItem) {
        
                oActiveItem.cfg.setProperty(_SELECTED, false);
        
            }
    
    
            oItemCfg = oItem.cfg;
        
            // Select and focus the current menu item
        
            oItemCfg.setProperty(_SELECTED, true);
    
    
            if (this.hasFocus() || oRoot._hasFocus) {
            
                oItem.focus();
                
                oRoot._hasFocus = false;
            
            }
    
    
            if (this.cfg.getProperty(_AUTO_SUBMENU_DISPLAY)) {
    
                // Show the submenu this menu item
    
                oSubmenu = oItemCfg.getProperty(_SUBMENU);
            
                if (oSubmenu) {
            
                    if (bShowDelay) {
    
                        oRoot._showDelayTimer = 
                            Lang.later(oRoot.cfg.getProperty(_SHOW_DELAY), oSubmenu, showSubmenu);
            
                    }
                    else {
    
                        oSubmenu.show();
    
                    }
    
                }
    
            }                        
    
            oItem.handledMouseOverEvent = true;
            oItem.handledMouseOutEvent = false;
    
        }
    
    }

},


/**
* @method _onMouseOut
* @description "mouseout" event handler for the menu.
* @protected
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onMouseOut: function (p_sType, p_aArgs) {

    var oEvent = p_aArgs[0],
        oItem = p_aArgs[1],
        oRelatedTarget = Event.getRelatedTarget(oEvent),
        bMovingToSubmenu = false,
        oItemCfg,
        oSubmenu,
        nSubmenuHideDelay,
        nShowDelay;


    YAHOO.log("onMouseout: this == " + this);

    if (!this._bStopMouseEventHandlers) {
    
        if (oItem && !oItem.cfg.getProperty(_DISABLED)) {
    
            oItemCfg = oItem.cfg;
            oSubmenu = oItemCfg.getProperty(_SUBMENU);
    
    
            if (oSubmenu && (oRelatedTarget == oSubmenu.element || Dom.isAncestor(oSubmenu.element, oRelatedTarget))) {
                bMovingToSubmenu = true;
            }
    
            if (!oItem.handledMouseOutEvent && ((oRelatedTarget != oItem.element && !Dom.isAncestor(oItem.element, oRelatedTarget)) || bMovingToSubmenu)) {
                if (!bMovingToSubmenu) {
                    oItem.cfg.setProperty(_SELECTED, false);
                    if (oSubmenu) {
                        
                        nSubmenuHideDelay = this.cfg.getProperty(_SUBMENU_HIDE_DELAY);
                        nShowDelay = this.cfg.getProperty(_SHOW_DELAY);
                        if (!(this instanceof YAHOO.widget.MenuBar) && nSubmenuHideDelay > 0 && nSubmenuHideDelay >= nShowDelay) {
                            this._execSubmenuHideDelay(oSubmenu, Event.getPageX(oEvent), nSubmenuHideDelay);
                        } else {
                            oSubmenu.hide();
                        }
                    }
                }
    
                oItem.handledMouseOutEvent = true;
                oItem.handledMouseOverEvent = false;
            }
        }

        YAHOO.log("onMouseout: oRelatedTarget = " + oRelatedTarget.className);
        YAHOO.log("onMouseout: this.element = " + this.element.id);
        YAHOO.log("onMouseout: Ancestorthis.element = " + Dom.isAncestor(this.element, oRelatedTarget));
        YAHOO.log("onMouseout: canHide = " + this._didMouseLeave(oRelatedTarget));        

        if (!this._bHandledMouseOutEvent) {
            if (this._didMouseLeave(oRelatedTarget) || bMovingToSubmenu) {
                // Menu mouseout logic
                if (this._useHideDelay) {
                    this._execHideDelay();
                }
    
                Event.removeListener(this.element, _MOUSEMOVE, this._onMouseMove);
        
                this._nCurrentMouseX = Event.getPageX(oEvent);
        
                this._bHandledMouseOutEvent = true;
                this._bHandledMouseOverEvent = false;
            }
        }
    }

},

/**
 * Utilility method to determine if we really moused out of the menu based on the related target
 * @method _didMouseLeave
 * @protected
 * @param {HTMLElement} oRelatedTarget The related target based on which we're making the decision
 * @return {boolean} true if it's OK to hide based on the related target.
 */
_didMouseLeave : function(oRelatedTarget) {
    // Hide if we're not moving back to the element from somewhere inside the element, or we're moving to an element inside the menu.
    // The shadow is treated as an edge case, inside inside the menu, but we get no further mouseouts, because it overflows the element,
    // so we need to close when moving to the menu. 
    return (oRelatedTarget === this._shadow || (oRelatedTarget != this.element && !Dom.isAncestor(this.element, oRelatedTarget)));
},

/**
* @method _onMouseMove
* @description "click" event handler for the menu.
* @protected
* @param {Event} p_oEvent Object representing the DOM event object passed 
* back by the event utility (YAHOO.util.Event).
* @param {YAHOO.widget.Menu} p_oMenu Object representing the menu that 
* fired the event.
*/
_onMouseMove: function (p_oEvent, p_oMenu) {

    if (!this._bStopMouseEventHandlers) {
    
        this._nCurrentMouseX = Event.getPageX(p_oEvent);
    
    }

},


/**
* @method _onClick
* @description "click" event handler for the menu.
* @protected
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onClick: function (p_sType, p_aArgs) {

    var oEvent = p_aArgs[0],
        oItem = p_aArgs[1],
        bInMenuAnchor = false,
        oSubmenu,
        oMenu,
        oRoot,
        sId,
        sURL,
        nHashPos,
        nLen;


    var hide = function () {
        
        oRoot = this.getRoot();

        if (oRoot instanceof YAHOO.widget.MenuBar || 
            oRoot.cfg.getProperty(_POSITION) == _STATIC) {

            oRoot.clearActiveItem();

        }
        else {

            oRoot.hide();
        
        }
    
    };


    if (oItem) {
    
        if (oItem.cfg.getProperty(_DISABLED)) {
        
            Event.preventDefault(oEvent);

            hide.call(this);

        }
        else {

            oSubmenu = oItem.cfg.getProperty(_SUBMENU);
    
            
            /*
                 Check if the URL of the anchor is pointing to an element that is 
                 a child of the menu.
            */
            
            sURL = oItem.cfg.getProperty(_URL);

        
            if (sURL) {
    
                nHashPos = sURL.indexOf(_HASH);
    
                nLen = sURL.length;
    
    
                if (nHashPos != -1) {
    
                    sURL = sURL.substr(nHashPos, nLen);
        
                    nLen = sURL.length;
    
    
                    if (nLen > 1) {
    
                        sId = sURL.substr(1, nLen);
    
                        oMenu = YAHOO.widget.MenuManager.getMenu(sId);
                        
                        if (oMenu) {

                            bInMenuAnchor = 
                                (this.getRoot() === oMenu.getRoot());

                        }
                        
                    }
                    else if (nLen === 1) {
    
                        bInMenuAnchor = true;
                    
                    }
    
                }
            
            }

    
            if (bInMenuAnchor && !oItem.cfg.getProperty(_TARGET)) {
    
                Event.preventDefault(oEvent);
                

                if (UA.webkit) {
                
                    oItem.focus();
                
                }
                else {

                    oItem.focusEvent.fire();
                
                }
            
            }
    
    
            if (!oSubmenu && !this.cfg.getProperty(_KEEP_OPEN)) {
    
                hide.call(this);
    
            }
            
        }
    
    }

},

/*
    This function is called to prevent a bug in Firefox.  In Firefox,
    moving a DOM element into a stationary mouse pointer will cause the 
    browser to fire mouse events.  This can result in the menu mouse
    event handlers being called uncessarily, especially when menus are 
    moved into a stationary mouse pointer as a result of a 
    key event handler.
*/
/**
 * Utility method to stop mouseevents from being fired if the DOM
 * changes under a stationary mouse pointer (as opposed to the mouse moving
 * over a DOM element).
 * 
 * @method _stopMouseEventHandlers
 * @private
 */
_stopMouseEventHandlers: function() {
    this._bStopMouseEventHandlers = true;

    Lang.later(10, this, function () {
        this._bStopMouseEventHandlers = false;
    });
},

/**
* @method _onKeyDown
* @description "keydown" event handler for the menu.
* @protected
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onKeyDown: function (p_sType, p_aArgs) {

    var oEvent = p_aArgs[0],
        oItem = p_aArgs[1],
        oSubmenu,
        oItemCfg,
        oParentItem,
        oRoot,
        oNextItem,
        oBody,
        nBodyScrollTop,
        nBodyOffsetHeight,
        aItems,
        nItems,
        nNextItemOffsetTop,
        nScrollTarget,
        oParentMenu,
        oFocusedEl;


    if (this._useHideDelay) {
        this._cancelHideDelay();
    }

    if (oItem && !oItem.cfg.getProperty(_DISABLED)) {

        oItemCfg = oItem.cfg;
        oParentItem = this.parent;

        switch(oEvent.keyCode) {
    
            case 38:    // Up arrow
            case 40:    // Down arrow
    
                oNextItem = (oEvent.keyCode == 38) ? 
                    oItem.getPreviousEnabledSibling() : 
                    oItem.getNextEnabledSibling();
        
                if (oNextItem) {

                    this.clearActiveItem();

                    oNextItem.cfg.setProperty(_SELECTED, true);
                    oNextItem.focus();

                    if (this.cfg.getProperty(_MAX_HEIGHT) > 0 || Dom.hasClass(this.body, _YUI_MENU_BODY_SCROLLED)) {

                        oBody = this.body;
                        nBodyScrollTop = oBody.scrollTop;
                        nBodyOffsetHeight = oBody.offsetHeight;
                        aItems = this.getItems();
                        nItems = aItems.length - 1;
                        nNextItemOffsetTop = oNextItem.element.offsetTop;


                        if (oEvent.keyCode == 40 ) {    // Down
                       
                            if (nNextItemOffsetTop >= (nBodyOffsetHeight + nBodyScrollTop)) {

                                oBody.scrollTop = nNextItemOffsetTop - nBodyOffsetHeight;

                            }
                            else if (nNextItemOffsetTop <= nBodyScrollTop) {
                            
                                oBody.scrollTop = 0;
                            
                            }


                            if (oNextItem == aItems[nItems]) {

                                oBody.scrollTop = oNextItem.element.offsetTop;

                            }

                        }
                        else {  // Up

                            if (nNextItemOffsetTop <= nBodyScrollTop) {

                                oBody.scrollTop = nNextItemOffsetTop - oNextItem.element.offsetHeight;
                            
                            }
                            else if (nNextItemOffsetTop >= (nBodyScrollTop + nBodyOffsetHeight)) {
                            
                                oBody.scrollTop = nNextItemOffsetTop;
                            
                            }


                            if (oNextItem == aItems[0]) {
                            
                                oBody.scrollTop = 0;
                            
                            }

                        }


                        nBodyScrollTop = oBody.scrollTop;
                        nScrollTarget = oBody.scrollHeight - oBody.offsetHeight;

                        if (nBodyScrollTop === 0) {

                            this._disableScrollHeader();
                            this._enableScrollFooter();

                        }
                        else if (nBodyScrollTop == nScrollTarget) {

                             this._enableScrollHeader();
                             this._disableScrollFooter();

                        }
                        else {

                            this._enableScrollHeader();
                            this._enableScrollFooter();

                        }

                    }

                }

    
                Event.preventDefault(oEvent);

                this._stopMouseEventHandlers();
    
            break;
            
    
            case 39:    // Right arrow
    
                oSubmenu = oItemCfg.getProperty(_SUBMENU);
    
                if (oSubmenu) {
    
                    if (!oItemCfg.getProperty(_SELECTED)) {
        
                        oItemCfg.setProperty(_SELECTED, true);
        
                    }
    
                    oSubmenu.show();
                    oSubmenu.setInitialFocus();
                    oSubmenu.setInitialSelection();
    
                }
                else {
    
                    oRoot = this.getRoot();
                    
                    if (oRoot instanceof YAHOO.widget.MenuBar) {
    
                        oNextItem = oRoot.activeItem.getNextEnabledSibling();
    
                        if (oNextItem) {
                        
                            oRoot.clearActiveItem();
    
                            oNextItem.cfg.setProperty(_SELECTED, true);
    
                            oSubmenu = oNextItem.cfg.getProperty(_SUBMENU);
    
                            if (oSubmenu) {
    
                                oSubmenu.show();
                                oSubmenu.setInitialFocus();
                            
                            }
                            else {
    
                                oNextItem.focus();
                            
                            }
                        
                        }
                    
                    }
                
                }
    
    
                Event.preventDefault(oEvent);

                this._stopMouseEventHandlers();

            break;
    
    
            case 37:    // Left arrow
    
                if (oParentItem) {
    
                    oParentMenu = oParentItem.parent;
    
                    if (oParentMenu instanceof YAHOO.widget.MenuBar) {
    
                        oNextItem = 
                            oParentMenu.activeItem.getPreviousEnabledSibling();
    
                        if (oNextItem) {
                        
                            oParentMenu.clearActiveItem();
    
                            oNextItem.cfg.setProperty(_SELECTED, true);
    
                            oSubmenu = oNextItem.cfg.getProperty(_SUBMENU);
    
                            if (oSubmenu) {
                            
                                oSubmenu.show();
                                oSubmenu.setInitialFocus();
                            
                            }
                            else {
    
                                oNextItem.focus();
                            
                            }
                        
                        } 
                    
                    }
                    else {
    
                        this.hide();
    
                        oParentItem.focus();
                    
                    }
    
                }
    
                Event.preventDefault(oEvent);

                this._stopMouseEventHandlers();

            break;        
    
        }


    }


    if (oEvent.keyCode == 27) { // Esc key

        if (this.cfg.getProperty(_POSITION) == _DYNAMIC) {
        
            this.hide();

            if (this.parent) {

                this.parent.focus();
            
            }
            else {
                // Focus the element that previously had focus

                oFocusedEl = this._focusedElement;

                if (oFocusedEl && oFocusedEl.focus) {

                    try {
                        oFocusedEl.focus();
                    }
                    catch(ex) {
                    }

                }
                
            }

        }
        else if (this.activeItem) {

            oSubmenu = this.activeItem.cfg.getProperty(_SUBMENU);

            if (oSubmenu && oSubmenu.cfg.getProperty(_VISIBLE)) {
            
                oSubmenu.hide();
                this.activeItem.focus();
            
            }
            else {

                this.activeItem.blur();
                this.activeItem.cfg.setProperty(_SELECTED, false);
        
            }
        
        }


        Event.preventDefault(oEvent);
    
    }
    
},


/**
* @method _onKeyPress
* @description "keypress" event handler for a Menu instance.
* @protected
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event 
* was fired.
*/
_onKeyPress: function (p_sType, p_aArgs) {
    
    var oEvent = p_aArgs[0];


    if (oEvent.keyCode == 40 || oEvent.keyCode == 38) {

        Event.preventDefault(oEvent);

    }

},


/**
* @method _onBlur
* @description "blur" event handler for a Menu instance.
* @protected
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event 
* was fired.
*/
_onBlur: function (p_sType, p_aArgs) {
        
    if (this._hasFocus) {
        this._hasFocus = false;
    }

},

/**
* @method _onYChange
* @description "y" event handler for a Menu instance.
* @protected
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event 
* was fired.
*/
_onYChange: function (p_sType, p_aArgs) {

    var oParent = this.parent,
        nScrollTop,
        oIFrame,
        nY;


    if (oParent) {

        nScrollTop = oParent.parent.body.scrollTop;


        if (nScrollTop > 0) {
    
            nY = (this.cfg.getProperty(_Y) - nScrollTop);
            
            Dom.setY(this.element, nY);

            oIFrame = this.iframe;            
    

            if (oIFrame) {
    
                Dom.setY(oIFrame, nY);
    
            }
            
            this.cfg.setProperty(_Y, nY, true);
        
        }
    
    }

},


/**
* @method _onScrollTargetMouseOver
* @description "mouseover" event handler for the menu's "header" and "footer" 
* elements.  Used to scroll the body of the menu up and down when the 
* menu's "maxheight" configuration property is set to a value greater than 0.
* @protected
* @param {Event} p_oEvent Object representing the DOM event object passed 
* back by the event utility (YAHOO.util.Event).
* @param {YAHOO.widget.Menu} p_oMenu Object representing the menu that 
* fired the event.
*/
_onScrollTargetMouseOver: function (p_oEvent, p_oMenu) {

    var oBodyScrollTimer = this._bodyScrollTimer;


    if (oBodyScrollTimer) {

        oBodyScrollTimer.cancel();

    }


    this._cancelHideDelay();


    var oTarget = Event.getTarget(p_oEvent),
        oBody = this.body,
        nScrollIncrement = this.cfg.getProperty(_SCROLL_INCREMENT),
        nScrollTarget,
        fnScrollFunction;


    function scrollBodyDown() {

        var nScrollTop = oBody.scrollTop;


        if (nScrollTop < nScrollTarget) {

            oBody.scrollTop = (nScrollTop + nScrollIncrement);

            this._enableScrollHeader();

        }
        else {

            oBody.scrollTop = nScrollTarget;

            this._bodyScrollTimer.cancel();

            this._disableScrollFooter();

        }

    }


    function scrollBodyUp() {

        var nScrollTop = oBody.scrollTop;


        if (nScrollTop > 0) {

            oBody.scrollTop = (nScrollTop - nScrollIncrement);

            this._enableScrollFooter();

        }
        else {

            oBody.scrollTop = 0;

            this._bodyScrollTimer.cancel();

            this._disableScrollHeader();

        }

    }

    
    if (Dom.hasClass(oTarget, _HD)) {

        fnScrollFunction = scrollBodyUp;
    
    }
    else {

        nScrollTarget = oBody.scrollHeight - oBody.offsetHeight;

        fnScrollFunction = scrollBodyDown;
    
    }
    

    this._bodyScrollTimer = Lang.later(10, this, fnScrollFunction, null, true);

},


/**
* @method _onScrollTargetMouseOut
* @description "mouseout" event handler for the menu's "header" and "footer" 
* elements.  Used to stop scrolling the body of the menu up and down when the 
* menu's "maxheight" configuration property is set to a value greater than 0.
* @protected
* @param {Event} p_oEvent Object representing the DOM event object passed 
* back by the event utility (YAHOO.util.Event).
* @param {YAHOO.widget.Menu} p_oMenu Object representing the menu that 
* fired the event.
*/
_onScrollTargetMouseOut: function (p_oEvent, p_oMenu) {

    var oBodyScrollTimer = this._bodyScrollTimer;

    if (oBodyScrollTimer) {

        oBodyScrollTimer.cancel();

    }
    
    this._cancelHideDelay();

},



// Private methods


/**
* @method _onInit
* @description "init" event handler for the menu.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onInit: function (p_sType, p_aArgs) {

    this.cfg.subscribeToConfigEvent(_VISIBLE, this._onVisibleChange);

    var bRootMenu = !this.parent,
        bLazyLoad = this.lazyLoad;


    /*
        Automatically initialize a menu's subtree if:

        1) This is the root menu and lazyload is off
        
        2) This is the root menu, lazyload is on, but the menu is 
           already visible

        3) This menu is a submenu and lazyload is off
    */



    if (((bRootMenu && !bLazyLoad) || 
        (bRootMenu && (this.cfg.getProperty(_VISIBLE) || 
        this.cfg.getProperty(_POSITION) == _STATIC)) || 
        (!bRootMenu && !bLazyLoad)) && this.getItemGroups().length === 0) {

        if (this.srcElement) {

            this._initSubTree();
        
        }


        if (this.itemData) {

            this.addItems(this.itemData);

        }
    
    }
    else if (bLazyLoad) {

        this.cfg.fireQueue();
    
    }

},


/**
* @method _onBeforeRender
* @description "beforerender" event handler for the menu.  Appends all of the 
* <code>&#60;ul&#62;</code>, <code>&#60;li&#62;</code> and their accompanying 
* title elements to the body element of the menu.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onBeforeRender: function (p_sType, p_aArgs) {

    var oEl = this.element,
        nListElements = this._aListElements.length,
        bFirstList = true,
        i = 0,
        oUL,
        oGroupTitle;

    if (nListElements > 0) {

        do {

            oUL = this._aListElements[i];

            if (oUL) {

                if (bFirstList) {
        
                    Dom.addClass(oUL, _FIRST_OF_TYPE);
                    bFirstList = false;
        
                }


                if (!Dom.isAncestor(oEl, oUL)) {

                    this.appendToBody(oUL);

                }


                oGroupTitle = this._aGroupTitleElements[i];

                if (oGroupTitle) {

                    if (!Dom.isAncestor(oEl, oGroupTitle)) {

                        oUL.parentNode.insertBefore(oGroupTitle, oUL);

                    }


                    Dom.addClass(oUL, _HAS_TITLE);

                }

            }

            i++;

        }
        while (i < nListElements);

    }

},


/**
* @method _onRender
* @description "render" event handler for the menu.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onRender: function (p_sType, p_aArgs) {

    if (this.cfg.getProperty(_POSITION) == _DYNAMIC) { 

        if (!this.cfg.getProperty(_VISIBLE)) {

            this.positionOffScreen();

        }
    
    }

},





/**
* @method _onBeforeShow
* @description "beforeshow" event handler for the menu.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onBeforeShow: function (p_sType, p_aArgs) {

    var nOptions,
        n,
        oSrcElement,
        oContainer = this.cfg.getProperty(_CONTAINER);


    if (this.lazyLoad && this.getItemGroups().length === 0) {

        if (this.srcElement) {
        
            this._initSubTree();

        }


        if (this.itemData) {

            if (this.parent && this.parent.parent && 
                this.parent.parent.srcElement && 
                this.parent.parent.srcElement.tagName.toUpperCase() == 
                _SELECT) {

                nOptions = this.itemData.length;
    
                for(n=0; n<nOptions; n++) {

                    if (this.itemData[n].tagName) {

                        this.addItem((new this.ITEM_TYPE(this.itemData[n])));
    
                    }
    
                }
            
            }
            else {

                this.addItems(this.itemData);
            
            }
        
        }


        oSrcElement = this.srcElement;

        if (oSrcElement) {

            if (oSrcElement.tagName.toUpperCase() == _SELECT) {

                if (Dom.inDocument(oSrcElement)) {

                    this.render(oSrcElement.parentNode);
                
                }
                else {
                
                    this.render(oContainer);
                
                }

            }
            else {

                this.render();

            }

        }
        else {

            if (this.parent) {

                this.render(this.parent.element);     

            }
            else {

                this.render(oContainer);

            }                

        }

    }



    var oParent = this.parent,
        aAlignment;


    if (!oParent && this.cfg.getProperty(_POSITION) == _DYNAMIC) {

        this.cfg.refireEvent(_XY);
   
    }


    if (oParent) {

        aAlignment = oParent.parent.cfg.getProperty(_SUBMENU_ALIGNMENT);
        
        this.cfg.setProperty(_CONTEXT, [oParent.element, aAlignment[0], aAlignment[1]]);
        this.align();
    
    }

},


getConstrainedY: function (y) {

    var oMenu = this,
    
        aContext = oMenu.cfg.getProperty(_CONTEXT),
        nInitialMaxHeight = oMenu.cfg.getProperty(_MAX_HEIGHT),

        nMaxHeight,

        oOverlapPositions = {

            "trbr": true,
            "tlbl": true,
            "bltl": true,
            "brtr": true

        },

        bPotentialContextOverlap = (aContext && oOverlapPositions[aContext[1] + aContext[2]]),
    
        oMenuEl = oMenu.element,
        nMenuOffsetHeight = oMenuEl.offsetHeight,
    
        nViewportOffset = Overlay.VIEWPORT_OFFSET,
        viewPortHeight = Dom.getViewportHeight(),
        scrollY = Dom.getDocumentScrollTop(),

        bCanConstrain = 
            (oMenu.cfg.getProperty(_MIN_SCROLL_HEIGHT) + nViewportOffset < viewPortHeight),

        nAvailableHeight,

        oContextEl,
        nContextElY,
        nContextElHeight,

        bFlipped = false,

        nTopRegionHeight,
        nBottomRegionHeight,

        topConstraint = scrollY + nViewportOffset,
        bottomConstraint = scrollY + viewPortHeight - nMenuOffsetHeight - nViewportOffset,

        yNew = y;
        

    var flipVertical = function () {

        var nNewY;
    
        // The Menu is below the context element, flip it above
        if ((oMenu.cfg.getProperty(_Y) - scrollY) > nContextElY) { 
            nNewY = (nContextElY - nMenuOffsetHeight);
        }
        else {	// The Menu is above the context element, flip it below
            nNewY = (nContextElY + nContextElHeight);
        }

        oMenu.cfg.setProperty(_Y, (nNewY + scrollY), true);
        
        return nNewY;
    
    };


    /*
         Uses the context element's position to calculate the availble height 
         above and below it to display its corresponding Menu.
    */

    var getDisplayRegionHeight = function () {

        // The Menu is below the context element
        if ((oMenu.cfg.getProperty(_Y) - scrollY) > nContextElY) {
            return (nBottomRegionHeight - nViewportOffset);				
        }
        else {	// The Menu is above the context element
            return (nTopRegionHeight - nViewportOffset);				
        }

    };


    /*
        Sets the Menu's "y" configuration property to the correct value based on its
        current orientation.
    */ 

    var alignY = function () {

        var nNewY;

        if ((oMenu.cfg.getProperty(_Y) - scrollY) > nContextElY) { 
            nNewY = (nContextElY + nContextElHeight);
        }
        else {	
            nNewY = (nContextElY - oMenuEl.offsetHeight);
        }

        oMenu.cfg.setProperty(_Y, (nNewY + scrollY), true);
    
    };


    //	Resets the maxheight of the Menu to the value set by the user

    var resetMaxHeight = function () {

        oMenu._setScrollHeight(this.cfg.getProperty(_MAX_HEIGHT));

        oMenu.hideEvent.unsubscribe(resetMaxHeight);
    
    };


    /*
        Trys to place the Menu in the best possible position (either above or 
        below its corresponding context element).
    */

    var setVerticalPosition = function () {

        var nDisplayRegionHeight = getDisplayRegionHeight(),
            bMenuHasItems = (oMenu.getItems().length > 0),
            nMenuMinScrollHeight,
            fnReturnVal;


        if (nMenuOffsetHeight > nDisplayRegionHeight) {

            nMenuMinScrollHeight = 
                bMenuHasItems ? oMenu.cfg.getProperty(_MIN_SCROLL_HEIGHT) : nMenuOffsetHeight;


            if ((nDisplayRegionHeight > nMenuMinScrollHeight) && bMenuHasItems) {
                nMaxHeight = nDisplayRegionHeight;
            }
            else {
                nMaxHeight = nInitialMaxHeight;
            }


            oMenu._setScrollHeight(nMaxHeight);
            oMenu.hideEvent.subscribe(resetMaxHeight);
            

            // Re-align the Menu since its height has just changed
            // as a result of the setting of the maxheight property.

            alignY();
            

            if (nDisplayRegionHeight < nMenuMinScrollHeight) {

                if (bFlipped) {
    
                    /*
                         All possible positions and values for the "maxheight" 
                         configuration property have been tried, but none were 
                         successful, so fall back to the original size and position.
                    */

                    flipVertical();
                    
                }
                else {
    
                    flipVertical();

                    bFlipped = true;
    
                    fnReturnVal = setVerticalPosition();
    
                }
                
            }
        
        }
        else if (nMaxHeight && (nMaxHeight !== nInitialMaxHeight)) {
        
            oMenu._setScrollHeight(nInitialMaxHeight);
            oMenu.hideEvent.subscribe(resetMaxHeight);

            // Re-align the Menu since its height has just changed
            // as a result of the setting of the maxheight property.

            alignY();
        
        }

        return fnReturnVal;

    };


    // Determine if the current value for the Menu's "y" configuration property will
    // result in the Menu being positioned outside the boundaries of the viewport

    if (y < topConstraint || y  > bottomConstraint) {

        // The current value for the Menu's "y" configuration property WILL
        // result in the Menu being positioned outside the boundaries of the viewport

        if (bCanConstrain) {

            if (oMenu.cfg.getProperty(_PREVENT_CONTEXT_OVERLAP) && bPotentialContextOverlap) {
        
                //	SOLUTION #1:
                //	If the "preventcontextoverlap" configuration property is set to "true", 
                //	try to flip and/or scroll the Menu to both keep it inside the boundaries of the 
                //	viewport AND from overlaping its context element (MenuItem or MenuBarItem).

                oContextEl = aContext[0];
                nContextElHeight = oContextEl.offsetHeight;
                nContextElY = (Dom.getY(oContextEl) - scrollY);
    
                nTopRegionHeight = nContextElY;
                nBottomRegionHeight = (viewPortHeight - (nContextElY + nContextElHeight));
    
                setVerticalPosition();
                
                yNew = oMenu.cfg.getProperty(_Y);
        
            }
            else if (!(oMenu instanceof YAHOO.widget.MenuBar) && 
                nMenuOffsetHeight >= viewPortHeight) {

                //	SOLUTION #2:
                //	If the Menu exceeds the height of the viewport, introduce scroll bars
                //	to keep the Menu inside the boundaries of the viewport

                nAvailableHeight = (viewPortHeight - (nViewportOffset * 2));
        
                if (nAvailableHeight > oMenu.cfg.getProperty(_MIN_SCROLL_HEIGHT)) {
        
                    oMenu._setScrollHeight(nAvailableHeight);
                    oMenu.hideEvent.subscribe(resetMaxHeight);
        
                    alignY();
                    
                    yNew = oMenu.cfg.getProperty(_Y);
                
                }
        
            }	
            else {

                //	SOLUTION #3:
            
                if (y < topConstraint) {
                    yNew  = topConstraint;
                } else if (y  > bottomConstraint) {
                    yNew  = bottomConstraint;
                }				
            
            }

        }
        else {
            //	The "y" configuration property cannot be set to a value that will keep
            //	entire Menu inside the boundary of the viewport.  Therefore, set  
            //	the "y" configuration property to scrollY to keep as much of the 
            //	Menu inside the viewport as possible.
            yNew = nViewportOffset + scrollY;
        }	

    }

    return yNew;

},


/**
* @method _onHide
* @description "hide" event handler for the menu.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onHide: function (p_sType, p_aArgs) {

    if (this.cfg.getProperty(_POSITION) === _DYNAMIC) {
    
        this.positionOffScreen();
    
    }

},


/**
* @method _onShow
* @description "show" event handler for the menu.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onShow: function (p_sType, p_aArgs) {

    var oParent = this.parent,
        oParentMenu,
        oElement,
        nOffsetWidth,
        sWidth;        


    function disableAutoSubmenuDisplay(p_oEvent) {

        var oTarget;

        if (p_oEvent.type == _MOUSEDOWN || (p_oEvent.type == _KEYDOWN && p_oEvent.keyCode == 27)) {

            /*  
                Set the "autosubmenudisplay" to "false" if the user
                clicks outside the menu bar.
            */

            oTarget = Event.getTarget(p_oEvent);

            if (oTarget != oParentMenu.element || !Dom.isAncestor(oParentMenu.element, oTarget)) {

                oParentMenu.cfg.setProperty(_AUTO_SUBMENU_DISPLAY, false);

                Event.removeListener(document, _MOUSEDOWN, disableAutoSubmenuDisplay);
                Event.removeListener(document, _KEYDOWN, disableAutoSubmenuDisplay);

            }
        
        }

    }


    function onSubmenuHide(p_sType, p_aArgs, p_sWidth) {
    
        this.cfg.setProperty(_WIDTH, _EMPTY_STRING);
        this.hideEvent.unsubscribe(onSubmenuHide, p_sWidth);
    
    }


    if (oParent) {

        oParentMenu = oParent.parent;


        if (!oParentMenu.cfg.getProperty(_AUTO_SUBMENU_DISPLAY) && 
            (oParentMenu instanceof YAHOO.widget.MenuBar || 
            oParentMenu.cfg.getProperty(_POSITION) == _STATIC)) {

            oParentMenu.cfg.setProperty(_AUTO_SUBMENU_DISPLAY, true);

            Event.on(document, _MOUSEDOWN, disableAutoSubmenuDisplay);                             
            Event.on(document, _KEYDOWN, disableAutoSubmenuDisplay);

        }


        //	The following fixes an issue with the selected state of a MenuItem 
        //	not rendering correctly when a submenu is aligned to the left of
        //	its parent Menu instance.

        if ((this.cfg.getProperty("x") < oParentMenu.cfg.getProperty("x")) && 
            (UA.gecko && UA.gecko < 1.9) && !this.cfg.getProperty(_WIDTH)) {

            oElement = this.element;
            nOffsetWidth = oElement.offsetWidth;
            
            /*
                Measuring the difference of the offsetWidth before and after
                setting the "width" style attribute allows us to compute the 
                about of padding and borders applied to the element, which in 
                turn allows us to set the "width" property correctly.
            */
            
            oElement.style.width = nOffsetWidth + _PX;
            
            sWidth = (nOffsetWidth - (oElement.offsetWidth - nOffsetWidth)) + _PX;
            
            this.cfg.setProperty(_WIDTH, sWidth);
        
            this.hideEvent.subscribe(onSubmenuHide, sWidth);
        
        }

    }


    /*
        Dynamically positioned, root Menus focus themselves when visible, and 
        will then, when hidden, restore focus to the UI control that had focus 
        before the Menu was made visible.
    */ 

    if (this === this.getRoot() && this.cfg.getProperty(_POSITION) === _DYNAMIC) {

        this._focusedElement = oFocusedElement;
        
        this.focus();
    
    }


},


/**
* @method _onBeforeHide
* @description "beforehide" event handler for the menu.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
*/
_onBeforeHide: function (p_sType, p_aArgs) {

    var oActiveItem = this.activeItem,
        oRoot = this.getRoot(),
        oConfig,
        oSubmenu;


    if (oActiveItem) {

        oConfig = oActiveItem.cfg;

        oConfig.setProperty(_SELECTED, false);

        oSubmenu = oConfig.getProperty(_SUBMENU);

        if (oSubmenu) {

            oSubmenu.hide();

        }

    }


    /*
        Focus can get lost in IE when the mouse is moving from a submenu back to its parent Menu.  
        For this reason, it is necessary to maintain the focused state in a private property 
        so that the _onMouseOver event handler is able to determined whether or not to set focus
        to MenuItems as the user is moving the mouse.
    */ 

    if (UA.ie && this.cfg.getProperty(_POSITION) === _DYNAMIC && this.parent) {

        oRoot._hasFocus = this.hasFocus();
    
    }


    if (oRoot == this) {

        oRoot.blur();
    
    }

},


/**
* @method _onParentMenuConfigChange
* @description "configchange" event handler for a submenu.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oSubmenu Object representing the submenu that 
* subscribed to the event.
*/
_onParentMenuConfigChange: function (p_sType, p_aArgs, p_oSubmenu) {
    
    var sPropertyName = p_aArgs[0][0],
        oPropertyValue = p_aArgs[0][1];

    switch(sPropertyName) {

        case _IFRAME:
        case _CONSTRAIN_TO_VIEWPORT:
        case _HIDE_DELAY:
        case _SHOW_DELAY:
        case _SUBMENU_HIDE_DELAY:
        case _CLICK_TO_HIDE:
        case _EFFECT:
        case _CLASSNAME:
        case _SCROLL_INCREMENT:
        case _MAX_HEIGHT:
        case _MIN_SCROLL_HEIGHT:
        case _MONITOR_RESIZE:
        case _SHADOW:
        case _PREVENT_CONTEXT_OVERLAP:
        case _KEEP_OPEN:

            p_oSubmenu.cfg.setProperty(sPropertyName, oPropertyValue);
                
        break;
        
        case _SUBMENU_ALIGNMENT:

            if (!(this.parent.parent instanceof YAHOO.widget.MenuBar)) {
        
                p_oSubmenu.cfg.setProperty(sPropertyName, oPropertyValue);
        
            }
        
        break;
        
    }
    
},


/**
* @method _onParentMenuRender
* @description "render" event handler for a submenu.  Renders a  
* submenu in response to the firing of its parent's "render" event.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oSubmenu Object representing the submenu that 
* subscribed to the event.
*/
_onParentMenuRender: function (p_sType, p_aArgs, p_oSubmenu) {

    var oParentMenu = p_oSubmenu.parent.parent,
        oParentCfg = oParentMenu.cfg,

        oConfig = {

            constraintoviewport: oParentCfg.getProperty(_CONSTRAIN_TO_VIEWPORT),

            xy: [0,0],

            clicktohide: oParentCfg.getProperty(_CLICK_TO_HIDE),
                
            effect: oParentCfg.getProperty(_EFFECT),

            showdelay: oParentCfg.getProperty(_SHOW_DELAY),
            
            hidedelay: oParentCfg.getProperty(_HIDE_DELAY),

            submenuhidedelay: oParentCfg.getProperty(_SUBMENU_HIDE_DELAY),

            classname: oParentCfg.getProperty(_CLASSNAME),
            
            scrollincrement: oParentCfg.getProperty(_SCROLL_INCREMENT),
            
            maxheight: oParentCfg.getProperty(_MAX_HEIGHT),

            minscrollheight: oParentCfg.getProperty(_MIN_SCROLL_HEIGHT),
            
            iframe: oParentCfg.getProperty(_IFRAME),
            
            shadow: oParentCfg.getProperty(_SHADOW),

            preventcontextoverlap: oParentCfg.getProperty(_PREVENT_CONTEXT_OVERLAP),
            
            monitorresize: oParentCfg.getProperty(_MONITOR_RESIZE),

            keepopen: oParentCfg.getProperty(_KEEP_OPEN)

        },
        
        oLI;


    
    if (!(oParentMenu instanceof YAHOO.widget.MenuBar)) {

        oConfig[_SUBMENU_ALIGNMENT] = oParentCfg.getProperty(_SUBMENU_ALIGNMENT);

    }


    p_oSubmenu.cfg.applyConfig(oConfig);


    if (!this.lazyLoad) {

        oLI = this.parent.element;

        if (this.element.parentNode == oLI) {
    
            this.render();
    
        }
        else {

            this.render(oLI);
    
        }

    }
    
},


/**
* @method _onMenuItemDestroy
* @description "destroy" event handler for the menu's items.
* @private
* @param {String} p_sType String representing the name of the event 
* that was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item 
* that fired the event.
*/
_onMenuItemDestroy: function (p_sType, p_aArgs, p_oItem) {

    this._removeItemFromGroupByValue(p_oItem.groupIndex, p_oItem);

},


/**
* @method _onMenuItemConfigChange
* @description "configchange" event handler for the menu's items.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item 
* that fired the event.
*/
_onMenuItemConfigChange: function (p_sType, p_aArgs, p_oItem) {

    var sPropertyName = p_aArgs[0][0],
        oPropertyValue = p_aArgs[0][1],
        oSubmenu;


    switch(sPropertyName) {

        case _SELECTED:

            if (oPropertyValue === true) {

                this.activeItem = p_oItem;
            
            }

        break;

        case _SUBMENU:

            oSubmenu = p_aArgs[0][1];

            if (oSubmenu) {

                this._configureSubmenu(p_oItem);

            }

        break;

    }

},



// Public event handlers for configuration properties


/**
* @method configVisible
* @description Event handler for when the "visible" configuration property 
* the menu changes.
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oMenu Object representing the menu that 
* fired the event.
*/
configVisible: function (p_sType, p_aArgs, p_oMenu) {

    var bVisible,
        sDisplay;

    if (this.cfg.getProperty(_POSITION) == _DYNAMIC) {

        Menu.superclass.configVisible.call(this, p_sType, p_aArgs, p_oMenu);

    }
    else {

        bVisible = p_aArgs[0];
        sDisplay = Dom.getStyle(this.element, _DISPLAY);

        Dom.setStyle(this.element, _VISIBILITY, _VISIBLE);

        if (bVisible) {

            if (sDisplay != _BLOCK) {
                this.beforeShowEvent.fire();
                Dom.setStyle(this.element, _DISPLAY, _BLOCK);
                this.showEvent.fire();
            }
        
        }
        else {

            if (sDisplay == _BLOCK) {
                this.beforeHideEvent.fire();
                Dom.setStyle(this.element, _DISPLAY, _NONE);
                this.hideEvent.fire();
            }
        
        }

    }

},


/**
* @method configPosition
* @description Event handler for when the "position" configuration property 
* of the menu changes.
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oMenu Object representing the menu that 
* fired the event.
*/
configPosition: function (p_sType, p_aArgs, p_oMenu) {

    var oElement = this.element,
        sCSSPosition = p_aArgs[0] == _STATIC ? _STATIC : _ABSOLUTE,
        oCfg = this.cfg,
        nZIndex;


    Dom.setStyle(oElement, _POSITION, sCSSPosition);


    if (sCSSPosition == _STATIC) {

        // Statically positioned menus are visible by default
        
        Dom.setStyle(oElement, _DISPLAY, _BLOCK);

        oCfg.setProperty(_VISIBLE, true);

    }
    else {

        /*
            Even though the "visible" property is queued to 
            "false" by default, we need to set the "visibility" property to 
            "hidden" since Overlay's "configVisible" implementation checks the 
            element's "visibility" style property before deciding whether 
            or not to show an Overlay instance.
        */

        Dom.setStyle(oElement, _VISIBILITY, _HIDDEN);
    
    }


     if (sCSSPosition == _ABSOLUTE) {
         nZIndex = oCfg.getProperty(_ZINDEX);

         if (!nZIndex || nZIndex === 0) {
             oCfg.setProperty(_ZINDEX, 1);
         }

     }

},


/**
* @method configIframe
* @description Event handler for when the "iframe" configuration property of 
* the menu changes.
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oMenu Object representing the menu that 
* fired the event.
*/
configIframe: function (p_sType, p_aArgs, p_oMenu) {    

    if (this.cfg.getProperty(_POSITION) == _DYNAMIC) {

        Menu.superclass.configIframe.call(this, p_sType, p_aArgs, p_oMenu);

    }

},


/**
* @method configHideDelay
* @description Event handler for when the "hidedelay" configuration property 
* of the menu changes.
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oMenu Object representing the menu that 
* fired the event.
*/
configHideDelay: function (p_sType, p_aArgs, p_oMenu) {

    var nHideDelay = p_aArgs[0];

    this._useHideDelay = (nHideDelay > 0);

},


/**
* @method configContainer
* @description Event handler for when the "container" configuration property 
* of the menu changes.
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oMenu Object representing the menu that 
* fired the event.
*/
configContainer: function (p_sType, p_aArgs, p_oMenu) {

    var oElement = p_aArgs[0];

    if (Lang.isString(oElement)) {

        this.cfg.setProperty(_CONTAINER, Dom.get(oElement), true);

    }

},


/**
* @method _clearSetWidthFlag
* @description Change event listener for the "width" configuration property.  This listener is 
* added when a Menu's "width" configuration property is set by the "_setScrollHeight" method, and 
* is used to set the "_widthSetForScroll" property to "false" if the "width" configuration property 
* is changed after it was set by the "_setScrollHeight" method.  If the "_widthSetForScroll" 
* property is set to "false", and the "_setScrollHeight" method is in the process of tearing down 
* scrolling functionality, it will maintain the Menu's new width rather than reseting it.
* @private
*/
_clearSetWidthFlag: function () {

    this._widthSetForScroll = false;
    
    this.cfg.unsubscribeFromConfigEvent(_WIDTH, this._clearSetWidthFlag);

},

/**
 * @method _subscribeScrollHandlers
 * @param {HTMLElement} oHeader The scroll header element
 * @param {HTMLElement} oFooter The scroll footer element
 */
_subscribeScrollHandlers : function(oHeader, oFooter) {
    var fnMouseOver = this._onScrollTargetMouseOver;
    var fnMouseOut = this._onScrollTargetMouseOut;

    Event.on(oHeader, _MOUSEOVER, fnMouseOver, this, true);
    Event.on(oHeader, _MOUSEOUT, fnMouseOut, this, true);
    Event.on(oFooter, _MOUSEOVER, fnMouseOver, this, true);
    Event.on(oFooter, _MOUSEOUT, fnMouseOut, this, true);
},

/**
 * @method _unsubscribeScrollHandlers 
 * @param {HTMLElement} oHeader The scroll header element
 * @param {HTMLElement} oFooter The scroll footer element
 */
_unsubscribeScrollHandlers : function(oHeader, oFooter) {
    var fnMouseOver = this._onScrollTargetMouseOver;
    var fnMouseOut = this._onScrollTargetMouseOut;
    
    Event.removeListener(oHeader, _MOUSEOVER, fnMouseOver);
    Event.removeListener(oHeader, _MOUSEOUT, fnMouseOut);
    Event.removeListener(oFooter, _MOUSEOVER, fnMouseOver);
    Event.removeListener(oFooter, _MOUSEOUT, fnMouseOut);
},

/**
* @method _setScrollHeight
* @description 
* @param {String} p_nScrollHeight Number representing the scrolling height of the Menu.
* @private
*/
_setScrollHeight: function (p_nScrollHeight) {

    var nScrollHeight = p_nScrollHeight,
        bRefireIFrameAndShadow = false,
        bSetWidth = false,
        oElement,
        oBody,
        oHeader,
        oFooter,
        nMinScrollHeight,
        nHeight,
        nOffsetWidth,
        sWidth;

    if (this.getItems().length > 0) {

        oElement = this.element;
        oBody = this.body;
        oHeader = this.header;
        oFooter = this.footer;
        nMinScrollHeight = this.cfg.getProperty(_MIN_SCROLL_HEIGHT);

        if (nScrollHeight > 0 && nScrollHeight < nMinScrollHeight) {
            nScrollHeight = nMinScrollHeight;
        }

        Dom.setStyle(oBody, _HEIGHT, _EMPTY_STRING);
        Dom.removeClass(oBody, _YUI_MENU_BODY_SCROLLED);
        oBody.scrollTop = 0;

        //	Need to set a width for the Menu to fix the following problems in 
        //	Firefox 2 and IE:

        //	#1) Scrolled Menus will render at 1px wide in Firefox 2

        //	#2) There is a bug in gecko-based browsers where an element whose 
        //	"position" property is set to "absolute" and "overflow" property is 
        //	set to "hidden" will not render at the correct width when its 
        //	offsetParent's "position" property is also set to "absolute."  It is 
        //	possible to work around this bug by specifying a value for the width 
        //	property in addition to overflow.

        //	#3) In IE it is necessary to give the Menu a width before the 
        //	scrollbars are rendered to prevent the Menu from rendering with a 
        //	width that is 100% of the browser viewport.

        bSetWidth = ((UA.gecko && UA.gecko < 1.9) || UA.ie);

        if (nScrollHeight > 0 && bSetWidth && !this.cfg.getProperty(_WIDTH)) {

            nOffsetWidth = oElement.offsetWidth;
    
            /*
                Measuring the difference of the offsetWidth before and after
                setting the "width" style attribute allows us to compute the 
                about of padding and borders applied to the element, which in 
                turn allows us to set the "width" property correctly.
            */
            
            oElement.style.width = nOffsetWidth + _PX;
    
            sWidth = (nOffsetWidth - (oElement.offsetWidth - nOffsetWidth)) + _PX;


            this.cfg.unsubscribeFromConfigEvent(_WIDTH, this._clearSetWidthFlag);

            YAHOO.log("Setting the \"width\" configuration property to " + sWidth + " for srolling.", 
                "info", this.toString());

            this.cfg.setProperty(_WIDTH, sWidth);


            /*
                Set a flag (_widthSetForScroll) to maintain some history regarding how the 
                "width" configuration property was set.  If the "width" configuration property 
                is set by something other than the "_setScrollHeight" method, it will be 
                necessary to maintain that new value and not clear the width if scrolling 
                is turned off.
            */

            this._widthSetForScroll = true;

            this.cfg.subscribeToConfigEvent(_WIDTH, this._clearSetWidthFlag);
    
        }


        if (nScrollHeight > 0 && (!oHeader && !oFooter)) {

            YAHOO.log("Creating header and footer for scrolling.", "info", this.toString());

            this.setHeader(_NON_BREAKING_SPACE);
            this.setFooter(_NON_BREAKING_SPACE);

            oHeader = this.header;
            oFooter = this.footer;

            Dom.addClass(oHeader, _TOP_SCROLLBAR);
            Dom.addClass(oFooter, _BOTTOM_SCROLLBAR);

            oElement.insertBefore(oHeader, oBody);
            oElement.appendChild(oFooter);
        
        }

        nHeight = nScrollHeight;

        if (oHeader && oFooter) {
            nHeight = (nHeight - (oHeader.offsetHeight + oFooter.offsetHeight));
        }
    
    
        if ((nHeight > 0) && (oBody.offsetHeight > nScrollHeight)) {

            YAHOO.log("Setting up styles and event handlers for scrolling.", 
                "info", this.toString());
    
            Dom.addClass(oBody, _YUI_MENU_BODY_SCROLLED);
            Dom.setStyle(oBody, _HEIGHT, (nHeight + _PX));

            if (!this._hasScrollEventHandlers) {
                this._subscribeScrollHandlers(oHeader, oFooter);
                this._hasScrollEventHandlers = true;
            }
    
            this._disableScrollHeader();
            this._enableScrollFooter();
            
            bRefireIFrameAndShadow = true;			
    
        }
        else if (oHeader && oFooter) {

            YAHOO.log("Removing styles and event handlers for scrolling.", "info", this.toString());
    

            /*
                Only clear the the "width" configuration property if it was set the 
                "_setScrollHeight" method and wasn't changed by some other means after it was set.
            */	
    
            if (this._widthSetForScroll) {
    
                YAHOO.log("Clearing width used for scrolling.", "info", this.toString());

                this._widthSetForScroll = false;

                this.cfg.unsubscribeFromConfigEvent(_WIDTH, this._clearSetWidthFlag);
    
                this.cfg.setProperty(_WIDTH, _EMPTY_STRING);
            
            }
    
    
            this._enableScrollHeader();
            this._enableScrollFooter();
    
            if (this._hasScrollEventHandlers) {
                this._unsubscribeScrollHandlers(oHeader, oFooter);    
                this._hasScrollEventHandlers = false;
            }

            oElement.removeChild(oHeader);
            oElement.removeChild(oFooter);
    
            this.header = null;
            this.footer = null;
            
            bRefireIFrameAndShadow = true;
        
        }


        if (bRefireIFrameAndShadow) {
    
            this.cfg.refireEvent(_IFRAME);
            this.cfg.refireEvent(_SHADOW);
        
        }
    
    }

},


/**
* @method _setMaxHeight
* @description "renderEvent" handler used to defer the setting of the 
* "maxheight" configuration property until the menu is rendered in lazy 
* load scenarios.
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event 
* was fired.
* @param {Number} p_nMaxHeight Number representing the value to set for the 
* "maxheight" configuration property.
* @private
*/
_setMaxHeight: function (p_sType, p_aArgs, p_nMaxHeight) {

    this._setScrollHeight(p_nMaxHeight);
    this.renderEvent.unsubscribe(this._setMaxHeight);

},


/**
* @method configMaxHeight
* @description Event handler for when the "maxheight" configuration property of 
* a Menu changes.
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event 
* was fired.
* @param {YAHOO.widget.Menu} p_oMenu The Menu instance fired
* the event.
*/
configMaxHeight: function (p_sType, p_aArgs, p_oMenu) {

    var nMaxHeight = p_aArgs[0];

    if (this.lazyLoad && !this.body && nMaxHeight > 0) {
    
        this.renderEvent.subscribe(this._setMaxHeight, nMaxHeight, this);

    }
    else {

        this._setScrollHeight(nMaxHeight);
    
    }

},


/**
* @method configClassName
* @description Event handler for when the "classname" configuration property of 
* a menu changes.
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oMenu The Menu instance fired the event.
*/
configClassName: function (p_sType, p_aArgs, p_oMenu) {

    var sClassName = p_aArgs[0];

    if (this._sClassName) {

        Dom.removeClass(this.element, this._sClassName);

    }

    Dom.addClass(this.element, sClassName);
    this._sClassName = sClassName;

},


/**
* @method _onItemAdded
* @description "itemadded" event handler for a Menu instance.
* @private
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event 
* was fired.
*/
_onItemAdded: function (p_sType, p_aArgs) {

    var oItem = p_aArgs[0];
    
    if (oItem) {

        oItem.cfg.setProperty(_DISABLED, true);
    
    }

},


/**
* @method configDisabled
* @description Event handler for when the "disabled" configuration property of 
* a menu changes.
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oMenu The Menu instance fired the event.
*/
configDisabled: function (p_sType, p_aArgs, p_oMenu) {

    var bDisabled = p_aArgs[0],
        aItems = this.getItems(),
        nItems,
        i;

    if (Lang.isArray(aItems)) {

        nItems = aItems.length;
    
        if (nItems > 0) {
        
            i = nItems - 1;
    
            do {
    
                aItems[i].cfg.setProperty(_DISABLED, bDisabled);
            
            }
            while (i--);
        
        }


        if (bDisabled) {

            this.clearActiveItem(true);

            Dom.addClass(this.element, _DISABLED);

            this.itemAddedEvent.subscribe(this._onItemAdded);

        }
        else {

            Dom.removeClass(this.element, _DISABLED);

            this.itemAddedEvent.unsubscribe(this._onItemAdded);

        }
        
    }

},

/**
 * Resizes the shadow to match the container bounding element
 * 
 * @method _sizeShadow
 * @protected
 */
_sizeShadow : function () {

    var oElement = this.element,
        oShadow = this._shadow;

    if (oShadow && oElement) {
        // Clear the previous width
        if (oShadow.style.width && oShadow.style.height) {
            oShadow.style.width = _EMPTY_STRING;
            oShadow.style.height = _EMPTY_STRING;
        }

        oShadow.style.width = (oElement.offsetWidth + 6) + _PX;
        oShadow.style.height = (oElement.offsetHeight + 1) + _PX;
    }
},

/**
 * Replaces the shadow element in the DOM with the current shadow element (this._shadow)
 * 
 * @method _replaceShadow
 * @protected 
 */
_replaceShadow : function () {
    this.element.appendChild(this._shadow);
},

/**
 * Adds the classname marker for a visible shadow, to the shadow element
 * 
 * @method _addShadowVisibleClass
 * @protected
 */
_addShadowVisibleClass : function () {
    Dom.addClass(this._shadow, _YUI_MENU_SHADOW_VISIBLE);
},

/**
 * Removes the classname marker for a visible shadow, from the shadow element
 * 
 * @method _removeShadowVisibleClass
 * @protected
 */
_removeShadowVisibleClass : function () {
    Dom.removeClass(this._shadow, _YUI_MENU_SHADOW_VISIBLE);
},

/**
 * Removes the shadow element from the DOM, and unsubscribes all the listeners used to keep it in sync. Used
 * to handle setting the shadow to false.
 * 
 * @method _removeShadow
 * @protected
 */
_removeShadow : function() {

    var p = (this._shadow && this._shadow.parentNode);

    if (p) {
        p.removeChild(this._shadow);
    }

    this.beforeShowEvent.unsubscribe(this._addShadowVisibleClass);
    this.beforeHideEvent.unsubscribe(this._removeShadowVisibleClass);

    this.cfg.unsubscribeFromConfigEvent(_WIDTH, this._sizeShadow);
    this.cfg.unsubscribeFromConfigEvent(_HEIGHT, this._sizeShadow);
    this.cfg.unsubscribeFromConfigEvent(_MAX_HEIGHT, this._sizeShadow);
    this.cfg.unsubscribeFromConfigEvent(_MAX_HEIGHT, this._replaceShadow);

    this.changeContentEvent.unsubscribe(this._sizeShadow);

    Module.textResizeEvent.unsubscribe(this._sizeShadow);
},

/**
 * Used to create the shadow element, add it to the DOM, and subscribe listeners to keep it in sync.
 *
 * @method _createShadow
 * @protected
 */
_createShadow : function () {

    var oShadow = this._shadow,
        oElement;

    if (!oShadow) {
        oElement = this.element;

        if (!m_oShadowTemplate) {
            m_oShadowTemplate = document.createElement(_DIV_LOWERCASE);
            m_oShadowTemplate.className = _YUI_MENU_SHADOW_YUI_MENU_SHADOW_VISIBLE;
        }

        oShadow = m_oShadowTemplate.cloneNode(false);

        oElement.appendChild(oShadow);
        
        this._shadow = oShadow;

        this.beforeShowEvent.subscribe(this._addShadowVisibleClass);
        this.beforeHideEvent.subscribe(this._removeShadowVisibleClass);

        if (UA.ie) {
            /*
                 Need to call sizeShadow & syncIframe via setTimeout for 
                 IE 7 Quirks Mode and IE 6 Standards Mode and Quirks Mode 
                 or the shadow and iframe shim will not be sized and 
                 positioned properly.
            */
            Lang.later(0, this, function () {
                this._sizeShadow(); 
                this.syncIframe();
            });

            this.cfg.subscribeToConfigEvent(_WIDTH, this._sizeShadow);
            this.cfg.subscribeToConfigEvent(_HEIGHT, this._sizeShadow);
            this.cfg.subscribeToConfigEvent(_MAX_HEIGHT, this._sizeShadow);
            this.changeContentEvent.subscribe(this._sizeShadow);

            Module.textResizeEvent.subscribe(this._sizeShadow, this, true);

            this.destroyEvent.subscribe(function () {
                Module.textResizeEvent.unsubscribe(this._sizeShadow, this);
            });
        }

        this.cfg.subscribeToConfigEvent(_MAX_HEIGHT, this._replaceShadow);
    }
},

/**
 * The beforeShow event handler used to set up the shadow lazily when the menu is made visible.
 * @method _shadowBeforeShow
 * @protected 
 */
_shadowBeforeShow : function () {
    if (this._shadow) {

        // If called because the "shadow" event was refired - just append again and resize
        this._replaceShadow();

        if (UA.ie) {
            this._sizeShadow();
        }
    } else {
        this._createShadow();
    }

    this.beforeShowEvent.unsubscribe(this._shadowBeforeShow);
},

/**
* @method configShadow
* @description Event handler for when the "shadow" configuration property of 
* a menu changes.
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event was fired.
* @param {YAHOO.widget.Menu} p_oMenu The Menu instance fired the event.
*/
configShadow: function (p_sType, p_aArgs, p_oMenu) {

    var bShadow = p_aArgs[0];

    if (bShadow && this.cfg.getProperty(_POSITION) == _DYNAMIC) {
        if (this.cfg.getProperty(_VISIBLE)) {
            if (this._shadow) {
                // If the "shadow" event was refired - just append again and resize
                this._replaceShadow();
                
                if (UA.ie) {
                    this._sizeShadow();
                }
            } else {
                this._createShadow();
            }
        } else {
            this.beforeShowEvent.subscribe(this._shadowBeforeShow);
        }
    } else if (!bShadow) {
        this.beforeShowEvent.unsubscribe(this._shadowBeforeShow);
        this._removeShadow();
    }
},

// Public methods

/**
* @method initEvents
* @description Initializes the custom events for the menu.
*/
initEvents: function () {

    Menu.superclass.initEvents.call(this);

    // Create custom events

    var i = EVENT_TYPES.length - 1,
        aEventData,
        oCustomEvent;


    do {

        aEventData = EVENT_TYPES[i];

        oCustomEvent = this.createEvent(aEventData[1]);
        oCustomEvent.signature = CustomEvent.LIST;
        
        this[aEventData[0]] = oCustomEvent;

    }
    while (i--);

},


/**
* @method positionOffScreen
* @description Positions the menu outside of the boundaries of the browser's 
* viewport.  Called automatically when a menu is hidden to ensure that 
* it doesn't force the browser to render uncessary scrollbars.
*/
positionOffScreen: function () {

    var oIFrame = this.iframe,
        oElement = this.element,
        sPos = this.OFF_SCREEN_POSITION;
    
    oElement.style.top = _EMPTY_STRING;
    oElement.style.left = _EMPTY_STRING;
    
    if (oIFrame) {

        oIFrame.style.top = sPos;
        oIFrame.style.left = sPos;
    
    }

},


/**
* @method getRoot
* @description Finds the menu's root menu.
*/
getRoot: function () {

    var oItem = this.parent,
        oParentMenu,
        returnVal;

    if (oItem) {

        oParentMenu = oItem.parent;

        returnVal = oParentMenu ? oParentMenu.getRoot() : this;

    }
    else {
    
        returnVal = this;
    
    }
    
    return returnVal;

},


/**
* @method toString
* @description Returns a string representing the menu.
* @return {String}
*/
toString: function () {

    var sReturnVal = _MENU,
        sId = this.id;

    if (sId) {

        sReturnVal += (_SPACE + sId);
    
    }

    return sReturnVal;

},


/**
* @method setItemGroupTitle
* @description Sets the title of a group of menu items.
* @param {HTML} p_sGroupTitle String or markup specifying the title of the group. The title is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
* @param {Number} p_nGroupIndex Optional. Number specifying the group to which
* the title belongs.
*/
setItemGroupTitle: function (p_sGroupTitle, p_nGroupIndex) {

    var nGroupIndex,
        oTitle,
        i,
        nFirstIndex;
        
    if (Lang.isString(p_sGroupTitle) && p_sGroupTitle.length > 0) {

        nGroupIndex = Lang.isNumber(p_nGroupIndex) ? p_nGroupIndex : 0;
        oTitle = this._aGroupTitleElements[nGroupIndex];


        if (oTitle) {

            oTitle.innerHTML = p_sGroupTitle;
            
        }
        else {

            oTitle = document.createElement(this.GROUP_TITLE_TAG_NAME);
                    
            oTitle.innerHTML = p_sGroupTitle;

            this._aGroupTitleElements[nGroupIndex] = oTitle;

        }


        i = this._aGroupTitleElements.length - 1;

        do {

            if (this._aGroupTitleElements[i]) {

                Dom.removeClass(this._aGroupTitleElements[i], _FIRST_OF_TYPE);

                nFirstIndex = i;

            }

        }
        while (i--);


        if (nFirstIndex !== null) {

            Dom.addClass(this._aGroupTitleElements[nFirstIndex], 
                _FIRST_OF_TYPE);

        }

        this.changeContentEvent.fire();

    }

},



/**
* @method addItem
* @description Appends an item to the menu.
* @param {YAHOO.widget.MenuItem} p_oItem Object reference for the MenuItem 
* instance to be added to the menu.
* @param {HTML} p_oItem String or markup specifying content of the item to be added 
* to the menu. The item text is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
* @param {Object} p_oItem Object literal containing a set of menu item 
* configuration properties.
* @param {Number} p_nGroupIndex Optional. Number indicating the group to
* which the item belongs.
* @return {YAHOO.widget.MenuItem}
*/
addItem: function (p_oItem, p_nGroupIndex) {

    return this._addItemToGroup(p_nGroupIndex, p_oItem);

},


/**
* @method addItems
* @description Adds an array of items to the menu.
* @param {Array} p_aItems Array of items to be added to the menu.  The array 
* can contain strings specifying the markup for the content of each item to be created, object
* literals specifying each of the menu item configuration properties, 
* or MenuItem instances. The item content if provided as a string is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
* @param {Number} p_nGroupIndex Optional. Number specifying the group to 
* which the items belongs.
* @return {Array}
*/
addItems: function (p_aItems, p_nGroupIndex) {

    var nItems,
        aItems,
        oItem,
        i,
        returnVal;


    if (Lang.isArray(p_aItems)) {

        nItems = p_aItems.length;
        aItems = [];

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

            oItem = p_aItems[i];

            if (oItem) {

                if (Lang.isArray(oItem)) {
    
                    aItems[aItems.length] = this.addItems(oItem, i);
    
                }
                else {
    
                    aItems[aItems.length] = this._addItemToGroup(p_nGroupIndex, oItem);
                
                }

            }
    
        }


        if (aItems.length) {
        
            returnVal = aItems;
        
        }

    }

    return returnVal;

},


/**
* @method insertItem
* @description Inserts an item into the menu at the specified index.
* @param {YAHOO.widget.MenuItem} p_oItem Object reference for the MenuItem 
* instance to be added to the menu.
* @param {String} p_oItem String specifying the text of the item to be added 
* to the menu.
* @param {Object} p_oItem Object literal containing a set of menu item 
* configuration properties.
* @param {Number} p_nItemIndex Number indicating the ordinal position at which
* the item should be added.
* @param {Number} p_nGroupIndex Optional. Number indicating the group to which 
* the item belongs.
* @return {YAHOO.widget.MenuItem}
*/
insertItem: function (p_oItem, p_nItemIndex, p_nGroupIndex) {
    
    return this._addItemToGroup(p_nGroupIndex, p_oItem, p_nItemIndex);

},


/**
* @method removeItem
* @description Removes the specified item from the menu.
* @param {YAHOO.widget.MenuItem} p_oObject Object reference for the MenuItem 
* instance to be removed from the menu.
* @param {Number} p_oObject Number specifying the index of the item 
* to be removed.
* @param {Number} p_nGroupIndex Optional. Number specifying the group to 
* which the item belongs.
* @return {YAHOO.widget.MenuItem}
*/
removeItem: function (p_oObject, p_nGroupIndex) {

    var oItem,
        returnVal;
    
    if (!Lang.isUndefined(p_oObject)) {

        if (p_oObject instanceof YAHOO.widget.MenuItem) {

            oItem = this._removeItemFromGroupByValue(p_nGroupIndex, p_oObject);           

        }
        else if (Lang.isNumber(p_oObject)) {

            oItem = this._removeItemFromGroupByIndex(p_nGroupIndex, p_oObject);

        }

        if (oItem) {

            oItem.destroy();

            YAHOO.log("Item removed." + 
                " Text: " + oItem.cfg.getProperty("text") + ", " + 
                " Index: " + oItem.index + ", " + 
                " Group Index: " + oItem.groupIndex, "info", this.toString());

            returnVal = oItem;

        }

    }

    return returnVal;

},


/**
* @method getItems
* @description Returns an array of all of the items in the menu.
* @return {Array}
*/
getItems: function () {

    var aGroups = this._aItemGroups,
        nGroups,
        returnVal,
        aItems = [];


    if (Lang.isArray(aGroups)) {

        nGroups = aGroups.length;

        returnVal = ((nGroups == 1) ? aGroups[0] : (Array.prototype.concat.apply(aItems, aGroups)));

    }

    return returnVal;

},


/**
* @method getItemGroups
* @description Multi-dimensional Array representing the menu items as they 
* are grouped in the menu.
* @return {Array}
*/        
getItemGroups: function () {

    return this._aItemGroups;

},


/**
* @method getItem
* @description Returns the item at the specified index.
* @param {Number} p_nItemIndex Number indicating the ordinal position of the 
* item to be retrieved.
* @param {Number} p_nGroupIndex Optional. Number indicating the group to which 
* the item belongs.
* @return {YAHOO.widget.MenuItem}
*/
getItem: function (p_nItemIndex, p_nGroupIndex) {
    
    var aGroup,
        returnVal;
    
    if (Lang.isNumber(p_nItemIndex)) {

        aGroup = this._getItemGroup(p_nGroupIndex);

        if (aGroup) {

            returnVal = aGroup[p_nItemIndex];
        
        }

    }
    
    return returnVal;
    
},


/**
* @method getSubmenus
* @description Returns an array of all of the submenus that are immediate 
* children of the menu.
* @return {Array}
*/
getSubmenus: function () {

    var aItems = this.getItems(),
        nItems = aItems.length,
        aSubmenus,
        oSubmenu,
        oItem,
        i;


    if (nItems > 0) {
        
        aSubmenus = [];

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

            oItem = aItems[i];
            
            if (oItem) {

                oSubmenu = oItem.cfg.getProperty(_SUBMENU);
                
                if (oSubmenu) {

                    aSubmenus[aSubmenus.length] = oSubmenu;

                }
            
            }
        
        }
    
    }

    return aSubmenus;

},


/**
* @method clearContent
* @description Removes all of the content from the menu, including the menu 
* items, group titles, header and footer.
*/
clearContent: function () {

    var aItems = this.getItems(),
        nItems = aItems.length,
        oElement = this.element,
        oBody = this.body,
        oHeader = this.header,
        oFooter = this.footer,
        oItem,
        oSubmenu,
        i;


    if (nItems > 0) {

        i = nItems - 1;

        do {

            oItem = aItems[i];

            if (oItem) {

                oSubmenu = oItem.cfg.getProperty(_SUBMENU);

                if (oSubmenu) {

                    this.cfg.configChangedEvent.unsubscribe(
                        this._onParentMenuConfigChange, oSubmenu);

                    this.renderEvent.unsubscribe(this._onParentMenuRender, 
                        oSubmenu);

                }
                
                this.removeItem(oItem, oItem.groupIndex);

            }
        
        }
        while (i--);

    }


    if (oHeader) {

        Event.purgeElement(oHeader);
        oElement.removeChild(oHeader);

    }
    

    if (oFooter) {

        Event.purgeElement(oFooter);
        oElement.removeChild(oFooter);
    }


    if (oBody) {

        Event.purgeElement(oBody);

        oBody.innerHTML = _EMPTY_STRING;

    }

    this.activeItem = null;

    this._aItemGroups = [];
    this._aListElements = [];
    this._aGroupTitleElements = [];

    this.cfg.setProperty(_WIDTH, null);

},


/**
* @method destroy
* @description Removes the menu's <code>&#60;div&#62;</code> element 
* (and accompanying child nodes) from the document.
* @param {boolean} shallowPurge If true, only the parent element's DOM event listeners are purged. If false, or not provided, all children are also purged of DOM event listeners. 
* NOTE: The flag is a "shallowPurge" flag, as opposed to what may be a more intuitive "purgeChildren" flag to maintain backwards compatibility with behavior prior to 2.9.0.
* 
*/
destroy: function (shallowPurge) {

    // Remove all items

    this.clearContent();

    this._aItemGroups = null;
    this._aListElements = null;
    this._aGroupTitleElements = null;


    // Continue with the superclass implementation of this method

    Menu.superclass.destroy.call(this, shallowPurge);
    
    YAHOO.log("Destroyed.", "info", this.toString());

},


/**
* @method setInitialFocus
* @description Sets focus to the menu's first enabled item.
*/
setInitialFocus: function () {

    var oItem = this._getFirstEnabledItem();
    
    if (oItem) {

        oItem.focus();

    }
    
},


/**
* @method setInitialSelection
* @description Sets the "selected" configuration property of the menu's first 
* enabled item to "true."
*/
setInitialSelection: function () {

    var oItem = this._getFirstEnabledItem();
    
    if (oItem) {
    
        oItem.cfg.setProperty(_SELECTED, true);
    }        

},


/**
* @method clearActiveItem
* @description Sets the "selected" configuration property of the menu's active
* item to "false" and hides the item's submenu.
* @param {Boolean} p_bBlur Boolean indicating if the menu's active item 
* should be blurred.  
*/
clearActiveItem: function (p_bBlur) {

    if (this.cfg.getProperty(_SHOW_DELAY) > 0) {
    
        this._cancelShowDelay();
    
    }


    var oActiveItem = this.activeItem,
        oConfig,
        oSubmenu;

    if (oActiveItem) {

        oConfig = oActiveItem.cfg;

        if (p_bBlur) {

            oActiveItem.blur();
            
            this.getRoot()._hasFocus = true;
        
        }

        oConfig.setProperty(_SELECTED, false);

        oSubmenu = oConfig.getProperty(_SUBMENU);


        if (oSubmenu) {

            oSubmenu.hide();

        }

        this.activeItem = null;  

    }

},


/**
* @method focus
* @description Causes the menu to receive focus and fires the "focus" event.
*/
focus: function () {

    if (!this.hasFocus()) {

        this.setInitialFocus();
    
    }

},


/**
* @method blur
* @description Causes the menu to lose focus and fires the "blur" event.
*/    
blur: function () {

    var oItem;

    if (this.hasFocus()) {
    
        oItem = MenuManager.getFocusedMenuItem();
        
        if (oItem) {

            oItem.blur();

        }

    }

},


/**
* @method hasFocus
* @description Returns a boolean indicating whether or not the menu has focus.
* @return {Boolean}
*/
hasFocus: function () {

    return (MenuManager.getFocusedMenu() == this.getRoot());

},


_doItemSubmenuSubscribe: function (p_sType, p_aArgs, p_oObject) {

    var oItem = p_aArgs[0],
        oSubmenu = oItem.cfg.getProperty(_SUBMENU);

    if (oSubmenu) {
        oSubmenu.subscribe.apply(oSubmenu, p_oObject);
    }

},


_doSubmenuSubscribe: function (p_sType, p_aArgs, p_oObject) { 

    var oSubmenu = this.cfg.getProperty(_SUBMENU);
    
    if (oSubmenu) {
        oSubmenu.subscribe.apply(oSubmenu, p_oObject);
    }

},


/**
* Adds the specified CustomEvent subscriber to the menu and each of 
* its submenus.
* @method subscribe
* @param p_type     {string}   the type, or name of the event
* @param p_fn       {function} the function to exectute when the event fires
* @param p_obj      {Object}   An object to be passed along when the event 
*                              fires
* @param p_override {boolean}  If true, the obj passed in becomes the 
*                              execution scope of the listener
*/
subscribe: function () {

    //	Subscribe to the event for this Menu instance
    Menu.superclass.subscribe.apply(this, arguments);

    //	Subscribe to the "itemAdded" event so that all future submenus
    //	also subscribe to this event
    Menu.superclass.subscribe.call(this, _ITEM_ADDED, this._doItemSubmenuSubscribe, arguments);


    var aItems = this.getItems(),
        nItems,
        oItem,
        oSubmenu,
        i;
        

    if (aItems) {

        nItems = aItems.length;
        
        if (nItems > 0) {
        
            i = nItems - 1;
            
            do {

                oItem = aItems[i];
                oSubmenu = oItem.cfg.getProperty(_SUBMENU);
                
                if (oSubmenu) {
                    oSubmenu.subscribe.apply(oSubmenu, arguments);
                }
                else {
                    oItem.cfg.subscribeToConfigEvent(_SUBMENU, this._doSubmenuSubscribe, arguments);
                }

            }
            while (i--);
        
        }

    }

},


unsubscribe: function () {

    //	Remove the event for this Menu instance
    Menu.superclass.unsubscribe.apply(this, arguments);

    //	Remove the "itemAdded" event so that all future submenus don't have 
    //	the event handler
    Menu.superclass.unsubscribe.call(this, _ITEM_ADDED, this._doItemSubmenuSubscribe, arguments);


    var aItems = this.getItems(),
        nItems,
        oItem,
        oSubmenu,
        i;
        

    if (aItems) {

        nItems = aItems.length;
        
        if (nItems > 0) {
        
            i = nItems - 1;
            
            do {

                oItem = aItems[i];
                oSubmenu = oItem.cfg.getProperty(_SUBMENU);
                
                if (oSubmenu) {
                    oSubmenu.unsubscribe.apply(oSubmenu, arguments);
                }
                else {
                    oItem.cfg.unsubscribeFromConfigEvent(_SUBMENU, this._doSubmenuSubscribe, arguments);
                }

            }
            while (i--);
        
        }

    }

},


/**
* @description Initializes the class's configurable properties which can be
* changed using the menu's Config object ("cfg").
* @method initDefaultConfig
*/
initDefaultConfig: function () {

    Menu.superclass.initDefaultConfig.call(this);

    var oConfig = this.cfg;


    // Module documentation overrides

    /**
    * @config effect
    * @description Object or array of objects representing the ContainerEffect 
    * classes that are active for animating the container.  When set this 
    * property is automatically applied to all submenus.
    * @type Object
    * @default null
    */

    // Overlay documentation overrides


    /**
    * @config x
    * @description Number representing the absolute x-coordinate position of 
    * the Menu.  This property is only applied when the "position" 
    * configuration property is set to dynamic.
    * @type Number
    * @default null
    */
    

    /**
    * @config y
    * @description Number representing the absolute y-coordinate position of 
    * the Menu.  This property is only applied when the "position" 
    * configuration property is set to dynamic.
    * @type Number
    * @default null
    */


    /**
    * @description Array of the absolute x and y positions of the Menu.  This 
    * property is only applied when the "position" configuration property is 
    * set to dynamic.
    * @config xy
    * @type Number[]
    * @default null
    */
    

    /**
    * @config context
    * @description Array of context arguments for context-sensitive positioning.  
    * The format is: [id or element, element corner, context corner]. 
    * For example, setting this property to ["img1", "tl", "bl"] would 
    * align the Menu's top left corner to the context element's 
    * bottom left corner.  This property is only applied when the "position" 
    * configuration property is set to dynamic.
    * @type Array
    * @default null
    */
    
    
    /**
    * @config fixedcenter
    * @description Boolean indicating if the Menu should be anchored to the 
    * center of the viewport.  This property is only applied when the 
    * "position" configuration property is set to dynamic.
    * @type Boolean
    * @default false
    */
    
    
    /**
    * @config iframe
    * @description Boolean indicating whether or not the Menu should 
    * have an IFRAME shim; used to prevent SELECT elements from 
    * poking through an Overlay instance in IE6.  When set to "true", 
    * the iframe shim is created when the Menu instance is intially
    * made visible.  This property is only applied when the "position" 
    * configuration property is set to dynamic and is automatically applied 
    * to all submenus.
    * @type Boolean
    * @default true for IE6 and below, false for all other browsers.
    */


    // Add configuration attributes

    /*
        Change the default value for the "visible" configuration 
        property to "false" by re-adding the property.
    */

    /**
    * @config visible
    * @description Boolean indicating whether or not the menu is visible.  If 
    * the menu's "position" configuration property is set to "dynamic" (the 
    * default), this property toggles the menu's <code>&#60;div&#62;</code> 
    * element's "visibility" style property between "visible" (true) or 
    * "hidden" (false).  If the menu's "position" configuration property is 
    * set to "static" this property toggles the menu's 
    * <code>&#60;div&#62;</code> element's "display" style property 
    * between "block" (true) or "none" (false).
    * @default false
    * @type Boolean
    */
    oConfig.addProperty(
        VISIBLE_CONFIG.key, 
        {
            handler: this.configVisible, 
            value: VISIBLE_CONFIG.value, 
            validator: VISIBLE_CONFIG.validator
        }
     );


    /*
        Change the default value for the "constraintoviewport" configuration 
        property (inherited by YAHOO.widget.Overlay) to "true" by re-adding the property.
    */

    /**
    * @config constraintoviewport
    * @description Boolean indicating if the menu will try to remain inside 
    * the boundaries of the size of viewport.  This property is only applied 
    * when the "position" configuration property is set to dynamic and is 
    * automatically applied to all submenus.
    * @default true
    * @type Boolean
    */
    oConfig.addProperty(
        CONSTRAIN_TO_VIEWPORT_CONFIG.key, 
        {
            handler: this.configConstrainToViewport, 
            value: CONSTRAIN_TO_VIEWPORT_CONFIG.value, 
            validator: CONSTRAIN_TO_VIEWPORT_CONFIG.validator, 
            supercedes: CONSTRAIN_TO_VIEWPORT_CONFIG.supercedes 
        } 
    );


    /*
        Change the default value for the "preventcontextoverlap" configuration 
        property (inherited by YAHOO.widget.Overlay) to "true" by re-adding the property.
    */

    /**
    * @config preventcontextoverlap
    * @description Boolean indicating whether or not a submenu should overlap its parent MenuItem 
    * when the "constraintoviewport" configuration property is set to "true".
    * @type Boolean
    * @default true
    */
    oConfig.addProperty(PREVENT_CONTEXT_OVERLAP_CONFIG.key, {

        value: PREVENT_CONTEXT_OVERLAP_CONFIG.value, 
        validator: PREVENT_CONTEXT_OVERLAP_CONFIG.validator, 
        supercedes: PREVENT_CONTEXT_OVERLAP_CONFIG.supercedes

    });


    /**
    * @config position
    * @description String indicating how a menu should be positioned on the 
    * screen.  Possible values are "static" and "dynamic."  Static menus are 
    * visible by default and reside in the normal flow of the document 
    * (CSS position: static).  Dynamic menus are hidden by default, reside 
    * out of the normal flow of the document (CSS position: absolute), and 
    * can overlay other elements on the screen.
    * @default dynamic
    * @type String
    */
    oConfig.addProperty(
        POSITION_CONFIG.key, 
        {
            handler: this.configPosition,
            value: POSITION_CONFIG.value, 
            validator: POSITION_CONFIG.validator,
            supercedes: POSITION_CONFIG.supercedes
        }
    );


    /**
    * @config submenualignment
    * @description Array defining how submenus should be aligned to their 
    * parent menu item. The format is: [itemCorner, submenuCorner]. By default
    * a submenu's top left corner is aligned to its parent menu item's top 
    * right corner.
    * @default ["tl","tr"]
    * @type Array
    */
    oConfig.addProperty(
        SUBMENU_ALIGNMENT_CONFIG.key, 
        { 
            value: SUBMENU_ALIGNMENT_CONFIG.value,
            suppressEvent: SUBMENU_ALIGNMENT_CONFIG.suppressEvent
        }
    );


    /**
    * @config autosubmenudisplay
    * @description Boolean indicating if submenus are automatically made 
    * visible when the user mouses over the menu's items.
    * @default true
    * @type Boolean
    */
    oConfig.addProperty(
       AUTO_SUBMENU_DISPLAY_CONFIG.key, 
       { 
           value: AUTO_SUBMENU_DISPLAY_CONFIG.value, 
           validator: AUTO_SUBMENU_DISPLAY_CONFIG.validator,
           suppressEvent: AUTO_SUBMENU_DISPLAY_CONFIG.suppressEvent
       } 
    );


    /**
    * @config showdelay
    * @description Number indicating the time (in milliseconds) that should 
    * expire before a submenu is made visible when the user mouses over 
    * the menu's items.  This property is only applied when the "position" 
    * configuration property is set to dynamic and is automatically applied 
    * to all submenus.
    * @default 250
    * @type Number
    */
    oConfig.addProperty(
       SHOW_DELAY_CONFIG.key, 
       { 
           value: SHOW_DELAY_CONFIG.value, 
           validator: SHOW_DELAY_CONFIG.validator,
           suppressEvent: SHOW_DELAY_CONFIG.suppressEvent
       } 
    );


    /**
    * @config hidedelay
    * @description Number indicating the time (in milliseconds) that should 
    * expire before the menu is hidden.  This property is only applied when 
    * the "position" configuration property is set to dynamic and is 
    * automatically applied to all submenus.
    * @default 0
    * @type Number
    */
    oConfig.addProperty(
       HIDE_DELAY_CONFIG.key, 
       { 
           handler: this.configHideDelay,
           value: HIDE_DELAY_CONFIG.value, 
           validator: HIDE_DELAY_CONFIG.validator, 
           suppressEvent: HIDE_DELAY_CONFIG.suppressEvent
       } 
    );


    /**
    * @config submenuhidedelay
    * @description Number indicating the time (in milliseconds) that should 
    * expire before a submenu is hidden when the user mouses out of a menu item 
    * heading in the direction of a submenu.  The value must be greater than or 
    * equal to the value specified for the "showdelay" configuration property.
    * This property is only applied when the "position" configuration property 
    * is set to dynamic and is automatically applied to all submenus.
    * @default 250
    * @type Number
    */
    oConfig.addProperty(
       SUBMENU_HIDE_DELAY_CONFIG.key, 
       { 
           value: SUBMENU_HIDE_DELAY_CONFIG.value, 
           validator: SUBMENU_HIDE_DELAY_CONFIG.validator,
           suppressEvent: SUBMENU_HIDE_DELAY_CONFIG.suppressEvent
       } 
    );


    /**
    * @config clicktohide
    * @description Boolean indicating if the menu will automatically be 
    * hidden if the user clicks outside of it.  This property is only 
    * applied when the "position" configuration property is set to dynamic 
    * and is automatically applied to all submenus.
    * @default true
    * @type Boolean
    */
    oConfig.addProperty(
        CLICK_TO_HIDE_CONFIG.key,
        {
            value: CLICK_TO_HIDE_CONFIG.value,
            validator: CLICK_TO_HIDE_CONFIG.validator,
            suppressEvent: CLICK_TO_HIDE_CONFIG.suppressEvent
        }
    );


    /**
    * @config container
    * @description HTML element reference or string specifying the id 
    * attribute of the HTML element that the menu's markup should be 
    * rendered into.
    * @type <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/
    * level-one-html.html#ID-58190037">HTMLElement</a>|String
    * @default document.body
    */
    oConfig.addProperty(
       CONTAINER_CONFIG.key, 
       { 
           handler: this.configContainer,
           value: document.body,
           suppressEvent: CONTAINER_CONFIG.suppressEvent
       } 
   );


    /**
    * @config scrollincrement
    * @description Number used to control the scroll speed of a menu.  Used to 
    * increment the "scrollTop" property of the menu's body by when a menu's 
    * content is scrolling.  When set this property is automatically applied 
    * to all submenus.
    * @default 1
    * @type Number
    */
    oConfig.addProperty(
        SCROLL_INCREMENT_CONFIG.key, 
        { 
            value: SCROLL_INCREMENT_CONFIG.value, 
            validator: SCROLL_INCREMENT_CONFIG.validator,
            supercedes: SCROLL_INCREMENT_CONFIG.supercedes,
            suppressEvent: SCROLL_INCREMENT_CONFIG.suppressEvent
        }
    );


    /**
    * @config minscrollheight
    * @description Number defining the minimum threshold for the "maxheight" 
    * configuration property.  When set this property is automatically applied 
    * to all submenus.
    * @default 90
    * @type Number
    */
    oConfig.addProperty(
        MIN_SCROLL_HEIGHT_CONFIG.key, 
        { 
            value: MIN_SCROLL_HEIGHT_CONFIG.value, 
            validator: MIN_SCROLL_HEIGHT_CONFIG.validator,
            supercedes: MIN_SCROLL_HEIGHT_CONFIG.supercedes,
            suppressEvent: MIN_SCROLL_HEIGHT_CONFIG.suppressEvent
        }
    );


    /**
    * @config maxheight
    * @description Number defining the maximum height (in pixels) for a menu's 
    * body element (<code>&#60;div class="bd"&#62;</code>).  Once a menu's body 
    * exceeds this height, the contents of the body are scrolled to maintain 
    * this value.  This value cannot be set lower than the value of the 
    * "minscrollheight" configuration property.
    * @default 0
    * @type Number
    */
    oConfig.addProperty(
       MAX_HEIGHT_CONFIG.key, 
       {
            handler: this.configMaxHeight,
            value: MAX_HEIGHT_CONFIG.value,
            validator: MAX_HEIGHT_CONFIG.validator,
            suppressEvent: MAX_HEIGHT_CONFIG.suppressEvent,
            supercedes: MAX_HEIGHT_CONFIG.supercedes            
       } 
    );


    /**
    * @config classname
    * @description String representing the CSS class to be applied to the 
    * menu's root <code>&#60;div&#62;</code> element.  The specified class(es)  
    * are appended in addition to the default class as specified by the menu's
    * CSS_CLASS_NAME constant. When set this property is automatically 
    * applied to all submenus.
    * @default null
    * @type String
    */
    oConfig.addProperty(
        CLASS_NAME_CONFIG.key, 
        { 
            handler: this.configClassName,
            value: CLASS_NAME_CONFIG.value, 
            validator: CLASS_NAME_CONFIG.validator,
            supercedes: CLASS_NAME_CONFIG.supercedes      
        }
    );


    /**
    * @config disabled
    * @description Boolean indicating if the menu should be disabled.  
    * Disabling a menu disables each of its items.  (Disabled menu items are 
    * dimmed and will not respond to user input or fire events.)  Disabled
    * menus have a corresponding "disabled" CSS class applied to their root
    * <code>&#60;div&#62;</code> element.
    * @default false
    * @type Boolean
    */
    oConfig.addProperty(
        DISABLED_CONFIG.key, 
        { 
            handler: this.configDisabled,
            value: DISABLED_CONFIG.value, 
            validator: DISABLED_CONFIG.validator,
            suppressEvent: DISABLED_CONFIG.suppressEvent
        }
    );


    /**
    * @config shadow
    * @description Boolean indicating if the menu should have a shadow.
    * @default true
    * @type Boolean
    */
    oConfig.addProperty(
        SHADOW_CONFIG.key, 
        { 
            handler: this.configShadow,
            value: SHADOW_CONFIG.value, 
            validator: SHADOW_CONFIG.validator
        }
    );


    /**
    * @config keepopen
    * @description Boolean indicating if the menu should remain open when clicked.
    * @default false
    * @type Boolean
    */
    oConfig.addProperty(
        KEEP_OPEN_CONFIG.key, 
        { 
            value: KEEP_OPEN_CONFIG.value, 
            validator: KEEP_OPEN_CONFIG.validator
        }
    );

}

}); // END YAHOO.lang.extend

})();



(function () {

/**
* Creates an item for a menu.
* 
* @param {HTML} p_oObject Markup for the menu item content. The markup is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-74680021">HTMLLIElement</a>} p_oObject Object specifying 
* the <code>&#60;li&#62;</code> element of the menu item.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-38450247">HTMLOptGroupElement</a>} p_oObject Object 
* specifying the <code>&#60;optgroup&#62;</code> element of the menu item.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-70901257">HTMLOptionElement</a>} p_oObject Object 
* specifying the <code>&#60;option&#62;</code> element of the menu item.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the menu item. See configuration class documentation 
* for more details.
* @class MenuItem
* @constructor
*/
YAHOO.widget.MenuItem = function (p_oObject, p_oConfig) {

    if (p_oObject) {

        if (p_oConfig) {
    
            this.parent = p_oConfig.parent;
            this.value = p_oConfig.value;
            this.id = p_oConfig.id;

        }

        this.init(p_oObject, p_oConfig);

    }

};


var Dom = YAHOO.util.Dom,
    Module = YAHOO.widget.Module,
    Menu = YAHOO.widget.Menu,
    MenuItem = YAHOO.widget.MenuItem,
    CustomEvent = YAHOO.util.CustomEvent,
    UA = YAHOO.env.ua,
    Lang = YAHOO.lang,

    // Private string constants

    _TEXT = "text",
    _HASH = "#",
    _HYPHEN = "-",
    _HELP_TEXT = "helptext",
    _URL = "url",
    _TARGET = "target",
    _EMPHASIS = "emphasis",
    _STRONG_EMPHASIS = "strongemphasis",
    _CHECKED = "checked",
    _SUBMENU = "submenu",
    _DISABLED = "disabled",
    _SELECTED = "selected",
    _HAS_SUBMENU = "hassubmenu",
    _CHECKED_DISABLED = "checked-disabled",
    _HAS_SUBMENU_DISABLED = "hassubmenu-disabled",
    _HAS_SUBMENU_SELECTED = "hassubmenu-selected",
    _CHECKED_SELECTED = "checked-selected",
    _ONCLICK = "onclick",
    _CLASSNAME = "classname",
    _EMPTY_STRING = "",
    _OPTION = "OPTION",
    _OPTGROUP = "OPTGROUP",
    _LI_UPPERCASE = "LI",
    _HREF = "href",
    _SELECT = "SELECT",
    _DIV = "DIV",
    _START_HELP_TEXT = "<em class=\"helptext\">",
    _START_EM = "<em>",
    _END_EM = "</em>",
    _START_STRONG = "<strong>",
    _END_STRONG = "</strong>",
    _PREVENT_CONTEXT_OVERLAP = "preventcontextoverlap",
    _OBJ = "obj",
    _SCOPE = "scope",
    _NONE = "none",
    _VISIBLE = "visible",
    _SPACE = " ",
    _MENUITEM = "MenuItem",
    _CLICK = "click",
    _SHOW = "show",
    _HIDE = "hide",
    _LI_LOWERCASE = "li",
    _ANCHOR_TEMPLATE = "<a href=\"#\"></a>",

    EVENT_TYPES = [
    
        ["mouseOverEvent", "mouseover"],
        ["mouseOutEvent", "mouseout"],
        ["mouseDownEvent", "mousedown"],
        ["mouseUpEvent", "mouseup"],
        ["clickEvent", _CLICK],
        ["keyPressEvent", "keypress"],
        ["keyDownEvent", "keydown"],
        ["keyUpEvent", "keyup"],
        ["focusEvent", "focus"],
        ["blurEvent", "blur"],
        ["destroyEvent", "destroy"]
    
    ],

    TEXT_CONFIG = { 
        key: _TEXT, 
        value: _EMPTY_STRING, 
        validator: Lang.isString, 
        suppressEvent: true 
    }, 

    HELP_TEXT_CONFIG = { 
        key: _HELP_TEXT,
        supercedes: [_TEXT], 
        suppressEvent: true 
    },

    URL_CONFIG = { 
        key: _URL, 
        value: _HASH, 
        suppressEvent: true 
    }, 

    TARGET_CONFIG = { 
        key: _TARGET, 
        suppressEvent: true 
    }, 

    EMPHASIS_CONFIG = { 
        key: _EMPHASIS, 
        value: false, 
        validator: Lang.isBoolean, 
        suppressEvent: true, 
        supercedes: [_TEXT]
    }, 

    STRONG_EMPHASIS_CONFIG = { 
        key: _STRONG_EMPHASIS, 
        value: false, 
        validator: Lang.isBoolean, 
        suppressEvent: true,
        supercedes: [_TEXT]
    },

    CHECKED_CONFIG = { 
        key: _CHECKED, 
        value: false, 
        validator: Lang.isBoolean, 
        suppressEvent: true, 
        supercedes: [_DISABLED, _SELECTED]
    }, 

    SUBMENU_CONFIG = { 
        key: _SUBMENU,
        suppressEvent: true,
        supercedes: [_DISABLED, _SELECTED]
    },

    DISABLED_CONFIG = { 
        key: _DISABLED, 
        value: false, 
        validator: Lang.isBoolean, 
        suppressEvent: true,
        supercedes: [_TEXT, _SELECTED]
    },

    SELECTED_CONFIG = { 
        key: _SELECTED, 
        value: false, 
        validator: Lang.isBoolean, 
        suppressEvent: true
    },

    ONCLICK_CONFIG = { 
        key: _ONCLICK,
        suppressEvent: true
    },

    CLASS_NAME_CONFIG = { 
        key: _CLASSNAME, 
        value: null, 
        validator: Lang.isString,
        suppressEvent: true
    },
    
    KEY_LISTENER_CONFIG = {
        key: "keylistener", 
        value: null, 
        suppressEvent: true
    },

    m_oMenuItemTemplate = null,

    CLASS_NAMES = {};


/**
* @method getClassNameForState
* @description Returns a class name for the specified prefix and state.  If the class name does not 
* yet exist, it is created and stored in the CLASS_NAMES object to increase performance.
* @private
* @param {String} prefix String representing the prefix for the class name
* @param {String} state String representing a state - "disabled," "checked," etc.
*/  
var getClassNameForState = function (prefix, state) {

    var oClassNames = CLASS_NAMES[prefix];
    
    if (!oClassNames) {
        CLASS_NAMES[prefix] = {};
        oClassNames = CLASS_NAMES[prefix];
    }


    var sClassName = oClassNames[state];

    if (!sClassName) {
        sClassName = prefix + _HYPHEN + state;
        oClassNames[state] = sClassName;
    }

    return sClassName;
    
};


/**
* @method addClassNameForState
* @description Applies a class name to a MenuItem instance's &#60;LI&#62; and &#60;A&#62; elements
* that represents a MenuItem's state - "disabled," "checked," etc.
* @private
* @param {String} state String representing a state - "disabled," "checked," etc.
*/  
var addClassNameForState = function (state) {

    Dom.addClass(this.element, getClassNameForState(this.CSS_CLASS_NAME, state));
    Dom.addClass(this._oAnchor, getClassNameForState(this.CSS_LABEL_CLASS_NAME, state));

};

/**
* @method removeClassNameForState
* @description Removes a class name from a MenuItem instance's &#60;LI&#62; and &#60;A&#62; elements
* that represents a MenuItem's state - "disabled," "checked," etc.
* @private
* @param {String} state String representing a state - "disabled," "checked," etc.
*/  
var removeClassNameForState = function (state) {

    Dom.removeClass(this.element, getClassNameForState(this.CSS_CLASS_NAME, state));
    Dom.removeClass(this._oAnchor, getClassNameForState(this.CSS_LABEL_CLASS_NAME, state));

};


MenuItem.prototype = {

    /**
    * @property CSS_CLASS_NAME
    * @description String representing the CSS class(es) to be applied to the 
    * <code>&#60;li&#62;</code> element of the menu item.
    * @default "yuimenuitem"
    * @final
    * @type String
    */
    CSS_CLASS_NAME: "yuimenuitem",


    /**
    * @property CSS_LABEL_CLASS_NAME
    * @description String representing the CSS class(es) to be applied to the 
    * menu item's <code>&#60;a&#62;</code> element.
    * @default "yuimenuitemlabel"
    * @final
    * @type String
    */
    CSS_LABEL_CLASS_NAME: "yuimenuitemlabel",


    /**
    * @property SUBMENU_TYPE
    * @description Object representing the type of menu to instantiate and 
    * add when parsing the child nodes of the menu item's source HTML element.
    * @final
    * @type YAHOO.widget.Menu
    */
    SUBMENU_TYPE: null,



    // Private member variables
    

    /**
    * @property _oAnchor
    * @description Object reference to the menu item's 
    * <code>&#60;a&#62;</code> element.
    * @default null 
    * @private
    * @type <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
    * one-html.html#ID-48250443">HTMLAnchorElement</a>
    */
    _oAnchor: null,
    
    
    /**
    * @property _oHelpTextEM
    * @description Object reference to the menu item's help text 
    * <code>&#60;em&#62;</code> element.
    * @default null
    * @private
    * @type <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
    * one-html.html#ID-58190037">HTMLElement</a>
    */
    _oHelpTextEM: null,
    
    
    /**
    * @property _oSubmenu
    * @description Object reference to the menu item's submenu.
    * @default null
    * @private
    * @type YAHOO.widget.Menu
    */
    _oSubmenu: null,


    /** 
    * @property _oOnclickAttributeValue
    * @description Object reference to the menu item's current value for the 
    * "onclick" configuration attribute.
    * @default null
    * @private
    * @type Object
    */
    _oOnclickAttributeValue: null,


    /**
    * @property _sClassName
    * @description The current value of the "classname" configuration attribute.
    * @default null
    * @private
    * @type String
    */
    _sClassName: null,



    // Public properties


    /**
    * @property constructor
    * @description Object reference to the menu item's constructor function.
    * @default YAHOO.widget.MenuItem
    * @type YAHOO.widget.MenuItem
    */
    constructor: MenuItem,


    /**
    * @property index
    * @description Number indicating the ordinal position of the menu item in 
    * its group.
    * @default null
    * @type Number
    */
    index: null,


    /**
    * @property groupIndex
    * @description Number indicating the index of the group to which the menu 
    * item belongs.
    * @default null
    * @type Number
    */
    groupIndex: null,


    /**
    * @property parent
    * @description Object reference to the menu item's parent menu.
    * @default null
    * @type YAHOO.widget.Menu
    */
    parent: null,


    /**
    * @property element
    * @description Object reference to the menu item's 
    * <code>&#60;li&#62;</code> element.
    * @default <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level
    * -one-html.html#ID-74680021">HTMLLIElement</a>
    * @type <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
    * one-html.html#ID-74680021">HTMLLIElement</a>
    */
    element: null,


    /**
    * @property srcElement
    * @description Object reference to the HTML element (either 
    * <code>&#60;li&#62;</code>, <code>&#60;optgroup&#62;</code> or 
    * <code>&#60;option&#62;</code>) used create the menu item.
    * @default <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/
    * level-one-html.html#ID-74680021">HTMLLIElement</a>|<a href="http://www.
    * w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-38450247"
    * >HTMLOptGroupElement</a>|<a href="http://www.w3.org/TR/2000/WD-DOM-
    * Level-1-20000929/level-one-html.html#ID-70901257">HTMLOptionElement</a>
    * @type <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
    * one-html.html#ID-74680021">HTMLLIElement</a>|<a href="http://www.w3.
    * org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-38450247">
    * HTMLOptGroupElement</a>|<a href="http://www.w3.org/TR/2000/WD-DOM-
    * Level-1-20000929/level-one-html.html#ID-70901257">HTMLOptionElement</a>
    */
    srcElement: null,


    /**
    * @property value
    * @description Object reference to the menu item's value.
    * @default null
    * @type Object
    */
    value: null,


    /**
    * @property browser
    * @deprecated Use YAHOO.env.ua
    * @description String representing the browser.
    * @type String
    */
    browser: Module.prototype.browser,


    /**
    * @property id
    * @description Id of the menu item's root <code>&#60;li&#62;</code> 
    * element.  This property should be set via the constructor using the 
    * configuration object literal.  If an id is not specified, then one will 
    * be created using the "generateId" method of the Dom utility.
    * @default null
    * @type String
    */
    id: null,



    // Events


    /**
    * @event destroyEvent
    * @description Fires when the menu item's <code>&#60;li&#62;</code> 
    * element is removed from its parent <code>&#60;ul&#62;</code> element.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event mouseOverEvent
    * @description Fires when the mouse has entered the menu item.  Passes 
    * back the DOM Event object as an argument.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event mouseOutEvent
    * @description Fires when the mouse has left the menu item.  Passes back 
    * the DOM Event object as an argument.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event mouseDownEvent
    * @description Fires when the user mouses down on the menu item.  Passes 
    * back the DOM Event object as an argument.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event mouseUpEvent
    * @description Fires when the user releases a mouse button while the mouse 
    * is over the menu item.  Passes back the DOM Event object as an argument.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event clickEvent
    * @description Fires when the user clicks the on the menu item.  Passes 
    * back the DOM Event object as an argument.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event keyPressEvent
    * @description Fires when the user presses an alphanumeric key when the 
    * menu item has focus.  Passes back the DOM Event object as an argument.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event keyDownEvent
    * @description Fires when the user presses a key when the menu item has 
    * focus.  Passes back the DOM Event object as an argument.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event keyUpEvent
    * @description Fires when the user releases a key when the menu item has 
    * focus.  Passes back the DOM Event object as an argument.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event focusEvent
    * @description Fires when the menu item receives focus.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @event blurEvent
    * @description Fires when the menu item loses the input focus.
    * @type YAHOO.util.CustomEvent
    */


    /**
    * @method init
    * @description The MenuItem class's initialization method. 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.
    * @param {HTML} p_oObject Markup for the menu item content. The markup is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
    * @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
    * one-html.html#ID-74680021">HTMLLIElement</a>} p_oObject Object specifying 
    * the <code>&#60;li&#62;</code> element of the menu item.
    * @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
    * one-html.html#ID-38450247">HTMLOptGroupElement</a>} p_oObject Object 
    * specifying the <code>&#60;optgroup&#62;</code> element of the menu item.
    * @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
    * one-html.html#ID-70901257">HTMLOptionElement</a>} p_oObject Object 
    * specifying the <code>&#60;option&#62;</code> element of the menu item.
    * @param {Object} p_oConfig Optional. Object literal specifying the 
    * configuration for the menu item. See configuration class documentation 
    * for more details.
    */
    init: function (p_oObject, p_oConfig) {


        if (!this.SUBMENU_TYPE) {
    
            this.SUBMENU_TYPE = Menu;
    
        }


        // Create the config object

        this.cfg = new YAHOO.util.Config(this);

        this.initDefaultConfig();

        var oConfig = this.cfg,
            sURL = _HASH,
            oCustomEvent,
            aEventData,
            oAnchor,
            sTarget,
            sText,
            sId,
            i;


        if (Lang.isString(p_oObject)) {

            this._createRootNodeStructure();

            oConfig.queueProperty(_TEXT, p_oObject);

        }
        else if (p_oObject && p_oObject.tagName) {

            switch(p_oObject.tagName.toUpperCase()) {

                case _OPTION:

                    this._createRootNodeStructure();

                    oConfig.queueProperty(_TEXT, p_oObject.text);
                    oConfig.queueProperty(_DISABLED, p_oObject.disabled);

                    this.value = p_oObject.value;

                    this.srcElement = p_oObject;

                break;

                case _OPTGROUP:

                    this._createRootNodeStructure();

                    oConfig.queueProperty(_TEXT, p_oObject.label);
                    oConfig.queueProperty(_DISABLED, p_oObject.disabled);

                    this.srcElement = p_oObject;

                    this._initSubTree();

                break;

                case _LI_UPPERCASE:

                    // Get the anchor node (if it exists)
                    
                    oAnchor = Dom.getFirstChild(p_oObject);


                    // Capture the "text" and/or the "URL"

                    if (oAnchor) {

                        sURL = oAnchor.getAttribute(_HREF, 2);
                        sTarget = oAnchor.getAttribute(_TARGET);

                        sText = oAnchor.innerHTML;

                    }

                    this.srcElement = p_oObject;
                    this.element = p_oObject;
                    this._oAnchor = oAnchor;

                    /*
                        Set these properties silently to sync up the 
                        configuration object without making changes to the 
                        element's DOM
                    */ 

                    oConfig.setProperty(_TEXT, sText, true);
                    oConfig.setProperty(_URL, sURL, true);
                    oConfig.setProperty(_TARGET, sTarget, true);

                    this._initSubTree();

                break;

            }            

        }


        if (this.element) {

            sId = (this.srcElement || this.element).id;

            if (!sId) {

                sId = this.id || Dom.generateId();

                this.element.id = sId;

            }

            this.id = sId;


            Dom.addClass(this.element, this.CSS_CLASS_NAME);
            Dom.addClass(this._oAnchor, this.CSS_LABEL_CLASS_NAME);


            i = EVENT_TYPES.length - 1;

            do {

                aEventData = EVENT_TYPES[i];

                oCustomEvent = this.createEvent(aEventData[1]);
                oCustomEvent.signature = CustomEvent.LIST;
                
                this[aEventData[0]] = oCustomEvent;

            }
            while (i--);


            if (p_oConfig) {
    
                oConfig.applyConfig(p_oConfig);
    
            }        

            oConfig.fireQueue();

        }

    },



    // Private methods

    /**
    * @method _createRootNodeStructure
    * @description Creates the core DOM structure for the menu item.
    * @private
    */
    _createRootNodeStructure: function () {

        var oElement,
            oAnchor;

        if (!m_oMenuItemTemplate) {

            m_oMenuItemTemplate = document.createElement(_LI_LOWERCASE);
            m_oMenuItemTemplate.innerHTML = _ANCHOR_TEMPLATE;

        }

        oElement = m_oMenuItemTemplate.cloneNode(true);
        oElement.className = this.CSS_CLASS_NAME;

        oAnchor = oElement.firstChild;
        oAnchor.className = this.CSS_LABEL_CLASS_NAME;

        this.element = oElement;
        this._oAnchor = oAnchor;

    },


    /**
    * @method _initSubTree
    * @description Iterates the source element's childNodes collection and uses 
    * the child nodes to instantiate other menus.
    * @private
    */
    _initSubTree: function () {

        var oSrcEl = this.srcElement,
            oConfig = this.cfg,
            oNode,
            aOptions,
            nOptions,
            oMenu,
            n;


        if (oSrcEl.childNodes.length > 0) {

            if (this.parent.lazyLoad && this.parent.srcElement && 
                this.parent.srcElement.tagName.toUpperCase() == _SELECT) {

                oConfig.setProperty(
                        _SUBMENU, 
                        { id: Dom.generateId(), itemdata: oSrcEl.childNodes }
                    );

            }
            else {

                oNode = oSrcEl.firstChild;
                aOptions = [];
    
                do {
    
                    if (oNode && oNode.tagName) {
    
                        switch(oNode.tagName.toUpperCase()) {
                
                            case _DIV:
                
                                oConfig.setProperty(_SUBMENU, oNode);
                
                            break;
         
                            case _OPTION:
        
                                aOptions[aOptions.length] = oNode;
        
                            break;
               
                        }
                    
                    }
                
                }        
                while((oNode = oNode.nextSibling));
    
    
                nOptions = aOptions.length;
    
                if (nOptions > 0) {
    
                    oMenu = new this.SUBMENU_TYPE(Dom.generateId());
                    
                    oConfig.setProperty(_SUBMENU, oMenu);
    
                    for(n=0; n<nOptions; n++) {
        
                        oMenu.addItem((new oMenu.ITEM_TYPE(aOptions[n])));
        
                    }
        
                }
            
            }

        }

    },



    // Event handlers for configuration properties


    /**
    * @method configText
    * @description Event handler for when the "text" configuration property of 
    * the menu item changes.
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */
    configText: function (p_sType, p_aArgs, p_oItem) {

        var sText = p_aArgs[0],
            oConfig = this.cfg,
            oAnchor = this._oAnchor,
            sHelpText = oConfig.getProperty(_HELP_TEXT),
            sHelpTextHTML = _EMPTY_STRING,
            sEmphasisStartTag = _EMPTY_STRING,
            sEmphasisEndTag = _EMPTY_STRING;


        if (sText) {


            if (sHelpText) {
                    
                sHelpTextHTML = _START_HELP_TEXT + sHelpText + _END_EM;
            
            }


            if (oConfig.getProperty(_EMPHASIS)) {

                sEmphasisStartTag = _START_EM;
                sEmphasisEndTag = _END_EM;

            }


            if (oConfig.getProperty(_STRONG_EMPHASIS)) {

                sEmphasisStartTag = _START_STRONG;
                sEmphasisEndTag = _END_STRONG;
            
            }


            oAnchor.innerHTML = (sEmphasisStartTag + sText + sEmphasisEndTag + sHelpTextHTML);

        }

    },


    /**
    * @method configHelpText
    * @description Event handler for when the "helptext" configuration property 
    * of the menu item changes.
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */    
    configHelpText: function (p_sType, p_aArgs, p_oItem) {

        this.cfg.refireEvent(_TEXT);

    },


    /**
    * @method configURL
    * @description Event handler for when the "url" configuration property of 
    * the menu item changes.
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */    
    configURL: function (p_sType, p_aArgs, p_oItem) {

        var sURL = p_aArgs[0];

        if (!sURL) {

            sURL = _HASH;

        }

        var oAnchor = this._oAnchor;

        if (UA.opera) {

            oAnchor.removeAttribute(_HREF);
        
        }

        oAnchor.setAttribute(_HREF, sURL);

    },


    /**
    * @method configTarget
    * @description Event handler for when the "target" configuration property 
    * of the menu item changes.  
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */    
    configTarget: function (p_sType, p_aArgs, p_oItem) {

        var sTarget = p_aArgs[0],
            oAnchor = this._oAnchor;

        if (sTarget && sTarget.length > 0) {

            oAnchor.setAttribute(_TARGET, sTarget);

        }
        else {

            oAnchor.removeAttribute(_TARGET);
        
        }

    },


    /**
    * @method configEmphasis
    * @description Event handler for when the "emphasis" configuration property
    * of the menu item changes.
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */    
    configEmphasis: function (p_sType, p_aArgs, p_oItem) {

        var bEmphasis = p_aArgs[0],
            oConfig = this.cfg;


        if (bEmphasis && oConfig.getProperty(_STRONG_EMPHASIS)) {

            oConfig.setProperty(_STRONG_EMPHASIS, false);

        }


        oConfig.refireEvent(_TEXT);

    },


    /**
    * @method configStrongEmphasis
    * @description Event handler for when the "strongemphasis" configuration 
    * property of the menu item changes.
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */    
    configStrongEmphasis: function (p_sType, p_aArgs, p_oItem) {

        var bStrongEmphasis = p_aArgs[0],
            oConfig = this.cfg;


        if (bStrongEmphasis && oConfig.getProperty(_EMPHASIS)) {

            oConfig.setProperty(_EMPHASIS, false);

        }

        oConfig.refireEvent(_TEXT);

    },


    /**
    * @method configChecked
    * @description Event handler for when the "checked" configuration property 
    * of the menu item changes. 
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */    
    configChecked: function (p_sType, p_aArgs, p_oItem) {

        var bChecked = p_aArgs[0],
            oConfig = this.cfg;


        if (bChecked) {

            addClassNameForState.call(this, _CHECKED);

        }
        else {

            removeClassNameForState.call(this, _CHECKED);
        }


        oConfig.refireEvent(_TEXT);


        if (oConfig.getProperty(_DISABLED)) {

            oConfig.refireEvent(_DISABLED);

        }


        if (oConfig.getProperty(_SELECTED)) {

            oConfig.refireEvent(_SELECTED);

        }

    },



    /**
    * @method configDisabled
    * @description Event handler for when the "disabled" configuration property 
    * of the menu item changes. 
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */    
    configDisabled: function (p_sType, p_aArgs, p_oItem) {

        var bDisabled = p_aArgs[0],
            oConfig = this.cfg,
            oSubmenu = oConfig.getProperty(_SUBMENU),
            bChecked = oConfig.getProperty(_CHECKED);


        if (bDisabled) {

            if (oConfig.getProperty(_SELECTED)) {

                oConfig.setProperty(_SELECTED, false);

            }


            addClassNameForState.call(this, _DISABLED);


            if (oSubmenu) {

                addClassNameForState.call(this, _HAS_SUBMENU_DISABLED);
            
            }
            

            if (bChecked) {

                addClassNameForState.call(this, _CHECKED_DISABLED);

            }

        }
        else {

            removeClassNameForState.call(this, _DISABLED);


            if (oSubmenu) {

                removeClassNameForState.call(this, _HAS_SUBMENU_DISABLED);
            
            }
            

            if (bChecked) {

                removeClassNameForState.call(this, _CHECKED_DISABLED);

            }

        }

    },


    /**
    * @method configSelected
    * @description Event handler for when the "selected" configuration property 
    * of the menu item changes. 
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */    
    configSelected: function (p_sType, p_aArgs, p_oItem) {

        var oConfig = this.cfg,
            oAnchor = this._oAnchor,
            
            bSelected = p_aArgs[0],
            bChecked = oConfig.getProperty(_CHECKED),
            oSubmenu = oConfig.getProperty(_SUBMENU);


        if (UA.opera) {

            oAnchor.blur();
        
        }


        if (bSelected && !oConfig.getProperty(_DISABLED)) {

            addClassNameForState.call(this, _SELECTED);


            if (oSubmenu) {

                addClassNameForState.call(this, _HAS_SUBMENU_SELECTED);
            
            }


            if (bChecked) {

                addClassNameForState.call(this, _CHECKED_SELECTED);

            }

        }
        else {

            removeClassNameForState.call(this, _SELECTED);


            if (oSubmenu) {

                removeClassNameForState.call(this, _HAS_SUBMENU_SELECTED);
            
            }


            if (bChecked) {

                removeClassNameForState.call(this, _CHECKED_SELECTED);

            }

        }


        if (this.hasFocus() && UA.opera) {
        
            oAnchor.focus();
        
        }

    },


    /**
    * @method _onSubmenuBeforeHide
    * @description "beforehide" Custom Event handler for a submenu.
    * @private
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    */
    _onSubmenuBeforeHide: function (p_sType, p_aArgs) {

        var oItem = this.parent,
            oMenu;

        function onHide() {

            oItem._oAnchor.blur();
            oMenu.beforeHideEvent.unsubscribe(onHide);
        
        }


        if (oItem.hasFocus()) {

            oMenu = oItem.parent;

            oMenu.beforeHideEvent.subscribe(onHide);
        
        }
    
    },


    /**
    * @method configSubmenu
    * @description Event handler for when the "submenu" configuration property 
    * of the menu item changes. 
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */
    configSubmenu: function (p_sType, p_aArgs, p_oItem) {

        var oSubmenu = p_aArgs[0],
            oConfig = this.cfg,
            bLazyLoad = this.parent && this.parent.lazyLoad,
            oMenu,
            sSubmenuId,
            oSubmenuConfig;


        if (oSubmenu) {

            if (oSubmenu instanceof Menu) {

                oMenu = oSubmenu;
                oMenu.parent = this;
                oMenu.lazyLoad = bLazyLoad;

            }
            else if (Lang.isObject(oSubmenu) && oSubmenu.id && !oSubmenu.nodeType) {

                sSubmenuId = oSubmenu.id;
                oSubmenuConfig = oSubmenu;

                oSubmenuConfig.lazyload = bLazyLoad;
                oSubmenuConfig.parent = this;

                oMenu = new this.SUBMENU_TYPE(sSubmenuId, oSubmenuConfig);


                // Set the value of the property to the Menu instance

                oConfig.setProperty(_SUBMENU, oMenu, true);

            }
            else {

                oMenu = new this.SUBMENU_TYPE(oSubmenu, { lazyload: bLazyLoad, parent: this });


                // Set the value of the property to the Menu instance
                
                oConfig.setProperty(_SUBMENU, oMenu, true);

            }


            if (oMenu) {

                oMenu.cfg.setProperty(_PREVENT_CONTEXT_OVERLAP, true);

                addClassNameForState.call(this, _HAS_SUBMENU);


                if (oConfig.getProperty(_URL) === _HASH) {
                
                    oConfig.setProperty(_URL, (_HASH + oMenu.id));
                
                }


                this._oSubmenu = oMenu;


                if (UA.opera) {
                
                    oMenu.beforeHideEvent.subscribe(this._onSubmenuBeforeHide);               
                
                }
            
            }

        }
        else {

            removeClassNameForState.call(this, _HAS_SUBMENU);

            if (this._oSubmenu) {

                this._oSubmenu.destroy();

            }

        }


        if (oConfig.getProperty(_DISABLED)) {

            oConfig.refireEvent(_DISABLED);

        }


        if (oConfig.getProperty(_SELECTED)) {

            oConfig.refireEvent(_SELECTED);

        }

    },


    /**
    * @method configOnClick
    * @description Event handler for when the "onclick" configuration property 
    * of the menu item changes. 
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */
    configOnClick: function (p_sType, p_aArgs, p_oItem) {

        var oObject = p_aArgs[0];

        /*
            Remove any existing listeners if a "click" event handler has 
            already been specified.
        */

        if (this._oOnclickAttributeValue && (this._oOnclickAttributeValue != oObject)) {

            this.clickEvent.unsubscribe(this._oOnclickAttributeValue.fn, 
                                this._oOnclickAttributeValue.obj);

            this._oOnclickAttributeValue = null;

        }


        if (!this._oOnclickAttributeValue && Lang.isObject(oObject) && 
            Lang.isFunction(oObject.fn)) {
            
            this.clickEvent.subscribe(oObject.fn, 
                ((_OBJ in oObject) ? oObject.obj : this), 
                ((_SCOPE in oObject) ? oObject.scope : null) );

            this._oOnclickAttributeValue = oObject;

        }
    
    },


    /**
    * @method configClassName
    * @description Event handler for when the "classname" configuration 
    * property of a menu item changes.
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    * @param {YAHOO.widget.MenuItem} p_oItem Object representing the menu item
    * that fired the event.
    */
    configClassName: function (p_sType, p_aArgs, p_oItem) {
    
        var sClassName = p_aArgs[0];
    
        if (this._sClassName) {
    
            Dom.removeClass(this.element, this._sClassName);
    
        }
    
        Dom.addClass(this.element, sClassName);
        this._sClassName = sClassName;
    
    },


    /**
    * @method _dispatchClickEvent
    * @description Dispatches a DOM "click" event to the anchor element of a 
    * MenuItem instance.
    * @private	
    */
    _dispatchClickEvent: function () {

        var oMenuItem = this,
            oAnchor;

        if (!oMenuItem.cfg.getProperty(_DISABLED)) {
            oAnchor = Dom.getFirstChild(oMenuItem.element);

            //	Dispatch a "click" event to the MenuItem's anchor so that its
            //	"click" event handlers will get called in response to the user 
            //	pressing the keyboard shortcut defined by the "keylistener"
            //	configuration property.

            this._dispatchDOMClick(oAnchor);
        }
    },

    /**
     * Utility method to dispatch a DOM click event on the HTMLElement passed in
     *
     * @method _dispatchDOMClick
     * @protected
     * @param {HTMLElement} el
     */    
    _dispatchDOMClick : function(el) {
        var oEvent;

        // Choose the standards path for IE9
        if (UA.ie && UA.ie < 9) {
            el.fireEvent(_ONCLICK);
        } else {
            if ((UA.gecko && UA.gecko >= 1.9) || UA.opera || UA.webkit) {
                oEvent = document.createEvent("HTMLEvents");
                oEvent.initEvent(_CLICK, true, true);
            } else {
                oEvent = document.createEvent("MouseEvents");
                oEvent.initMouseEvent(_CLICK, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
            }
            el.dispatchEvent(oEvent);
        }
    },

    /**
    * @method _createKeyListener
    * @description "show" event handler for a Menu instance - responsible for 
    * setting up the KeyListener instance for a MenuItem.
    * @private	
    * @param {String} type String representing the name of the event that 
    * was fired.
    * @param {Array} args Array of arguments sent when the event was fired.
    * @param {Array} keyData Array of arguments sent when the event was fired.
    */
    _createKeyListener: function (type, args, keyData) {

        var oMenuItem = this,
            oMenu = oMenuItem.parent;

        var oKeyListener = new YAHOO.util.KeyListener(
                                        oMenu.element.ownerDocument, 
                                        keyData, 
                                        {
                                            fn: oMenuItem._dispatchClickEvent, 
                                            scope: oMenuItem, 
                                            correctScope: true });


        if (oMenu.cfg.getProperty(_VISIBLE)) {
            oKeyListener.enable();
        }


        oMenu.subscribe(_SHOW, oKeyListener.enable, null, oKeyListener);
        oMenu.subscribe(_HIDE, oKeyListener.disable, null, oKeyListener);
        
        oMenuItem._keyListener = oKeyListener;
        
        oMenu.unsubscribe(_SHOW, oMenuItem._createKeyListener, keyData);
        
    },


    /**
    * @method configKeyListener
    * @description Event handler for when the "keylistener" configuration 
    * property of a menu item changes.
    * @param {String} p_sType String representing the name of the event that 
    * was fired.
    * @param {Array} p_aArgs Array of arguments sent when the event was fired.
    */
    configKeyListener: function (p_sType, p_aArgs) {

        var oKeyData = p_aArgs[0],
            oMenuItem = this,
            oMenu = oMenuItem.parent;

        if (oMenuItem._keyData) {

            //	Unsubscribe from the "show" event in case the keylistener 
            //	config was changed before the Menu was ever made visible.

            oMenu.unsubscribe(_SHOW, 
                    oMenuItem._createKeyListener, oMenuItem._keyData);

            oMenuItem._keyData = null;					
                    
        }


        //	Tear down for the previous value of the "keylistener" property

        if (oMenuItem._keyListener) {

            oMenu.unsubscribe(_SHOW, oMenuItem._keyListener.enable);
            oMenu.unsubscribe(_HIDE, oMenuItem._keyListener.disable);

            oMenuItem._keyListener.disable();
            oMenuItem._keyListener = null;

        }


        if (oKeyData) {
    
            oMenuItem._keyData = oKeyData;

            //	Defer the creation of the KeyListener instance until the 
            //	parent Menu is visible.  This is necessary since the 
            //	KeyListener instance needs to be bound to the document the 
            //	Menu has been rendered into.  Deferring creation of the 
            //	KeyListener instance also improves performance.

            oMenu.subscribe(_SHOW, oMenuItem._createKeyListener, 
                oKeyData, oMenuItem);
        }
    
    },


    // Public methods


    /**
    * @method initDefaultConfig
    * @description Initializes an item's configurable properties.
    */
    initDefaultConfig : function () {

        var oConfig = this.cfg;


        // Define the configuration attributes

        /**
        * @config text
        * @description String or markup specifying the text label for the menu item.  
        * When building a menu from existing HTML the value of this property
        * will be interpreted from the menu's markup. The text is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @default ""
        * @type HTML
        */
        oConfig.addProperty(
            TEXT_CONFIG.key, 
            { 
                handler: this.configText, 
                value: TEXT_CONFIG.value, 
                validator: TEXT_CONFIG.validator, 
                suppressEvent: TEXT_CONFIG.suppressEvent 
            }
        );
        

        /**
        * @config helptext
        * @description String or markup specifying additional instructional text to 
        * accompany the text for the menu item. The helptext is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
        * @deprecated Use "text" configuration property to add help text markup.  
        * For example: <code>oMenuItem.cfg.setProperty("text", "Copy &#60;em 
        * class=\"helptext\"&#62;Ctrl + C&#60;/em&#62;");</code>
        * @default null
        * @type HTML|<a href="http://www.w3.org/TR/
        * 2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-58190037">
        * HTMLElement</a>
        */
        oConfig.addProperty(
            HELP_TEXT_CONFIG.key,
            {
                handler: this.configHelpText, 
                supercedes: HELP_TEXT_CONFIG.supercedes,
                suppressEvent: HELP_TEXT_CONFIG.suppressEvent 
            }
        );


        /**
        * @config url
        * @description String specifying the URL for the menu item's anchor's 
        * "href" attribute.  When building a menu from existing HTML the value 
        * of this property will be interpreted from the menu's markup. Markup for the menu item content. The url is inserted into the DOM as an attribute value, and should be escaped by the implementor if coming from an external source.
        * @default "#"
        * @type String
        */        
        oConfig.addProperty(
            URL_CONFIG.key, 
            {
                handler: this.configURL, 
                value: URL_CONFIG.value, 
                suppressEvent: URL_CONFIG.suppressEvent
            }
        );


        /**
        * @config target
        * @description String specifying the value for the "target" attribute 
        * of the menu item's anchor element. <strong>Specifying a target will 
        * require the user to click directly on the menu item's anchor node in
        * order to cause the browser to navigate to the specified URL.</strong> 
        * When building a menu from existing HTML the value of this property 
        * will be interpreted from the menu's markup. The target is inserted into the DOM as an attribute value, and should be escaped by the implementor if coming from an external source.
        * @default null
        * @type String
        */        
        oConfig.addProperty(
            TARGET_CONFIG.key, 
            {
                handler: this.configTarget, 
                suppressEvent: TARGET_CONFIG.suppressEvent
            }
        );


        /**
        * @config emphasis
        * @description Boolean indicating if the text of the menu item will be 
        * rendered with emphasis.
        * @deprecated Use the "text" configuration property to add emphasis.  
        * For example: <code>oMenuItem.cfg.setProperty("text", "&#60;em&#62;Some 
        * Text&#60;/em&#62;");</code>
        * @default false
        * @type Boolean
        */
        oConfig.addProperty(
            EMPHASIS_CONFIG.key, 
            { 
                handler: this.configEmphasis, 
                value: EMPHASIS_CONFIG.value, 
                validator: EMPHASIS_CONFIG.validator, 
                suppressEvent: EMPHASIS_CONFIG.suppressEvent,
                supercedes: EMPHASIS_CONFIG.supercedes
            }
        );


        /**
        * @config strongemphasis
        * @description Boolean indicating if the text of the menu item will be 
        * rendered with strong emphasis.
        * @deprecated Use the "text" configuration property to add strong emphasis.  
        * For example: <code>oMenuItem.cfg.setProperty("text", "&#60;strong&#62; 
        * Some Text&#60;/strong&#62;");</code>
        * @default false
        * @type Boolean
        */
        oConfig.addProperty(
            STRONG_EMPHASIS_CONFIG.key,
            {
                handler: this.configStrongEmphasis,
                value: STRONG_EMPHASIS_CONFIG.value,
                validator: STRONG_EMPHASIS_CONFIG.validator,
                suppressEvent: STRONG_EMPHASIS_CONFIG.suppressEvent,
                supercedes: STRONG_EMPHASIS_CONFIG.supercedes
            }
        );


        /**
        * @config checked
        * @description Boolean indicating if the menu item should be rendered 
        * with a checkmark.
        * @default false
        * @type Boolean
        */
        oConfig.addProperty(
            CHECKED_CONFIG.key, 
            {
                handler: this.configChecked, 
                value: CHECKED_CONFIG.value, 
                validator: CHECKED_CONFIG.validator, 
                suppressEvent: CHECKED_CONFIG.suppressEvent,
                supercedes: CHECKED_CONFIG.supercedes
            } 
        );


        /**
        * @config disabled
        * @description Boolean indicating if the menu item should be disabled.  
        * (Disabled menu items are  dimmed and will not respond to user input 
        * or fire events.)
        * @default false
        * @type Boolean
        */
        oConfig.addProperty(
            DISABLED_CONFIG.key,
            {
                handler: this.configDisabled,
                value: DISABLED_CONFIG.value,
                validator: DISABLED_CONFIG.validator,
                suppressEvent: DISABLED_CONFIG.suppressEvent
            }
        );


        /**
        * @config selected
        * @description Boolean indicating if the menu item should 
        * be highlighted.
        * @default false
        * @type Boolean
        */
        oConfig.addProperty(
            SELECTED_CONFIG.key,
            {
                handler: this.configSelected,
                value: SELECTED_CONFIG.value,
                validator: SELECTED_CONFIG.validator,
                suppressEvent: SELECTED_CONFIG.suppressEvent
            }
        );


        /**
        * @config submenu
        * @description Object specifying the submenu to be appended to the 
        * menu item.  The value can be one of the following: <ul><li>Object 
        * specifying a Menu instance.</li><li>Object literal specifying the
        * menu to be created.  Format: <code>{ id: [menu id], itemdata: 
        * [<a href="YAHOO.widget.Menu.html#itemData">array of values for 
        * items</a>] }</code>.</li><li>String specifying the id attribute 
        * of the <code>&#60;div&#62;</code> element of the menu.</li><li>
        * Object specifying the <code>&#60;div&#62;</code> element of the 
        * menu.</li></ul>
        * @default null
        * @type Menu|String|Object|<a href="http://www.w3.org/TR/2000/
        * WD-DOM-Level-1-20000929/level-one-html.html#ID-58190037">
        * HTMLElement</a>
        */
        oConfig.addProperty(
            SUBMENU_CONFIG.key, 
            {
                handler: this.configSubmenu, 
                supercedes: SUBMENU_CONFIG.supercedes,
                suppressEvent: SUBMENU_CONFIG.suppressEvent
            }
        );


        /**
        * @config onclick
        * @description Object literal representing the code to be executed when 
        * the item is clicked.  Format:<br> <code> {<br> 
        * <strong>fn:</strong> Function,   &#47;&#47; The handler to call when 
        * the event fires.<br> <strong>obj:</strong> Object, &#47;&#47; An 
        * object to  pass back to the handler.<br> <strong>scope:</strong> 
        * Object &#47;&#47; The object to use for the scope of the handler.
        * <br> } </code>
        * @type Object
        * @default null
        */
        oConfig.addProperty(
            ONCLICK_CONFIG.key, 
            {
                handler: this.configOnClick, 
                suppressEvent: ONCLICK_CONFIG.suppressEvent 
            }
        );


        /**
        * @config classname
        * @description CSS class to be applied to the menu item's root 
        * <code>&#60;li&#62;</code> element.  The specified class(es) are 
        * appended in addition to the default class as specified by the menu 
        * item's CSS_CLASS_NAME constant.
        * @default null
        * @type String
        */
        oConfig.addProperty(
            CLASS_NAME_CONFIG.key, 
            { 
                handler: this.configClassName,
                value: CLASS_NAME_CONFIG.value, 
                validator: CLASS_NAME_CONFIG.validator,
                suppressEvent: CLASS_NAME_CONFIG.suppressEvent 
            }
        );


        /**
        * @config keylistener
        * @description Object literal representing the key(s) that can be used 
        * to trigger the MenuItem's "click" event.  Possible attributes are 
        * shift (boolean), alt (boolean), ctrl (boolean) and keys (either an int 
        * or an array of ints representing keycodes).
        * @default null
        * @type Object
        */
        oConfig.addProperty(
            KEY_LISTENER_CONFIG.key, 
            { 
                handler: this.configKeyListener,
                value: KEY_LISTENER_CONFIG.value, 
                suppressEvent: KEY_LISTENER_CONFIG.suppressEvent 
            }
        );

    },

    /**
    * @method getNextSibling
    * @description Finds the menu item's next sibling.
    * @return YAHOO.widget.MenuItem
    */
    getNextSibling: function () {
    
        var isUL = function (el) {
                return (el.nodeName.toLowerCase() === "ul");
            },
    
            menuitemEl = this.element,
            next = Dom.getNextSibling(menuitemEl),
            parent,
            sibling,
            list;
        
        if (!next) {
            
            parent = menuitemEl.parentNode;
            sibling = Dom.getNextSiblingBy(parent, isUL);
            
            if (sibling) {
                list = sibling;
            }
            else {
                list = Dom.getFirstChildBy(parent.parentNode, isUL);
            }
            
            next = Dom.getFirstChild(list);
            
        }

        return YAHOO.widget.MenuManager.getMenuItem(next.id);

    },

    /**
    * @method getNextEnabledSibling
    * @description Finds the menu item's next enabled sibling.
    * @return YAHOO.widget.MenuItem
    */
    getNextEnabledSibling: function () {
        
        var next = this.getNextSibling();
        
        return (next.cfg.getProperty(_DISABLED) || next.element.style.display == _NONE) ? next.getNextEnabledSibling() : next;
        
    },


    /**
    * @method getPreviousSibling
    * @description Finds the menu item's previous sibling.
    * @return {YAHOO.widget.MenuItem}
    */	
    getPreviousSibling: function () {

        var isUL = function (el) {
                return (el.nodeName.toLowerCase() === "ul");
            },

            menuitemEl = this.element,
            next = Dom.getPreviousSibling(menuitemEl),
            parent,
            sibling,
            list;
        
        if (!next) {
            
            parent = menuitemEl.parentNode;
            sibling = Dom.getPreviousSiblingBy(parent, isUL);
            
            if (sibling) {
                list = sibling;
            }
            else {
                list = Dom.getLastChildBy(parent.parentNode, isUL);
            }
            
            next = Dom.getLastChild(list);
            
        }

        return YAHOO.widget.MenuManager.getMenuItem(next.id);
        
    },


    /**
    * @method getPreviousEnabledSibling
    * @description Finds the menu item's previous enabled sibling.
    * @return {YAHOO.widget.MenuItem}
    */
    getPreviousEnabledSibling: function () {
        
        var next = this.getPreviousSibling();
        
        return (next.cfg.getProperty(_DISABLED) || next.element.style.display == _NONE) ? next.getPreviousEnabledSibling() : next;
        
    },


    /**
    * @method focus
    * @description Causes the menu item to receive the focus and fires the 
    * focus event.
    */
    focus: function () {

        var oParent = this.parent,
            oAnchor = this._oAnchor,
            oActiveItem = oParent.activeItem;


        function setFocus() {

            try {

                if (!(UA.ie && !document.hasFocus())) {
                
                    if (oActiveItem) {
        
                        oActiveItem.blurEvent.fire();
        
                    }
    
                    oAnchor.focus();
                    
                    this.focusEvent.fire();
                
                }

            }
            catch(e) {
            
            }

        }


        if (!this.cfg.getProperty(_DISABLED) && oParent && oParent.cfg.getProperty(_VISIBLE) && 
            this.element.style.display != _NONE) {


            /*
                Setting focus via a timer fixes a race condition in Firefox, IE 
                and Opera where the browser viewport jumps as it trys to 
                position and focus the menu.
            */

            Lang.later(0, this, setFocus);

        }

    },


    /**
    * @method blur
    * @description Causes the menu item to lose focus and fires the 
    * blur event.
    */    
    blur: function () {

        var oParent = this.parent;

        if (!this.cfg.getProperty(_DISABLED) && oParent && oParent.cfg.getProperty(_VISIBLE)) {

            Lang.later(0, this, function () {

                try {
    
                    this._oAnchor.blur();
                    this.blurEvent.fire();    

                } 
                catch (e) {
                
                }
                
            }, 0);

        }

    },


    /**
    * @method hasFocus
    * @description Returns a boolean indicating whether or not the menu item
    * has focus.
    * @return {Boolean}
    */
    hasFocus: function () {
    
        return (YAHOO.widget.MenuManager.getFocusedMenuItem() == this);
    
    },


    /**
    * @method destroy
    * @description Removes the menu item's <code>&#60;li&#62;</code> element 
    * from its parent <code>&#60;ul&#62;</code> element.
    */
    destroy: function () {

        var oEl = this.element,
            oSubmenu,
            oParentNode,
            aEventData,
            i;


        if (oEl) {


            // If the item has a submenu, destroy it first

            oSubmenu = this.cfg.getProperty(_SUBMENU);

            if (oSubmenu) {
            
                oSubmenu.destroy();
            
            }


            // Remove the element from the parent node

            oParentNode = oEl.parentNode;

            if (oParentNode) {

                oParentNode.removeChild(oEl);

                this.destroyEvent.fire();

            }


            // Remove CustomEvent listeners

            i = EVENT_TYPES.length - 1;

            do {

                aEventData = EVENT_TYPES[i];
                
                this[aEventData[0]].unsubscribeAll();

            }
            while (i--);
            
            
            this.cfg.configChangedEvent.unsubscribeAll();

        }

    },


    /**
    * @method toString
    * @description Returns a string representing the menu item.
    * @return {String}
    */
    toString: function () {

        var sReturnVal = _MENUITEM,
            sId = this.id;

        if (sId) {
    
            sReturnVal += (_SPACE + sId);
        
        }

        return sReturnVal;
    
    }

};

Lang.augmentProto(MenuItem, YAHOO.util.EventProvider);

})();
(function () {

    var _XY = "xy",
        _MOUSEDOWN = "mousedown",
        _CONTEXTMENU = "ContextMenu",
        _SPACE = " ";

/**
* Creates a list of options or commands which are made visible in response to 
* an HTML element's "contextmenu" event ("mousedown" for Opera).
*
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;div&#62;</code> element of the context menu.
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;select&#62;</code> element to be used as the data source for the 
* context menu.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-
* html.html#ID-22445964">HTMLDivElement</a>} p_oElement Object specifying the 
* <code>&#60;div&#62;</code> element of the context menu.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-
* html.html#ID-94282980">HTMLSelectElement</a>} p_oElement Object specifying 
* the <code>&#60;select&#62;</code> element to be used as the data source for 
* the context menu.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the context menu. See configuration class documentation 
* for more details.
* @class ContextMenu
* @constructor
* @extends YAHOO.widget.Menu
* @namespace YAHOO.widget
*/
YAHOO.widget.ContextMenu = function(p_oElement, p_oConfig) {
    YAHOO.widget.ContextMenu.superclass.constructor.call(this, p_oElement, p_oConfig);
};


var Event = YAHOO.util.Event,
    UA = YAHOO.env.ua,
    ContextMenu = YAHOO.widget.ContextMenu,



    /**
    * Constant representing the name of the ContextMenu's events
    * @property EVENT_TYPES
    * @private
    * @final
    * @type Object
    */
    EVENT_TYPES = {

        "TRIGGER_CONTEXT_MENU": "triggerContextMenu",
        "CONTEXT_MENU": (UA.opera ? _MOUSEDOWN : "contextmenu"),
        "CLICK": "click"

    },
    
    
    /**
    * Constant representing the ContextMenu's configuration properties
    * @property DEFAULT_CONFIG
    * @private
    * @final
    * @type Object
    */
    TRIGGER_CONFIG = { 
        key: "trigger",
        suppressEvent: true
    };


/**
* @method position
* @description "beforeShow" event handler used to position the contextmenu.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {Array} p_aPos Array representing the xy position for the context menu.
*/
function position(p_sType, p_aArgs, p_aPos) {
    this.cfg.setProperty(_XY, p_aPos);
    this.beforeShowEvent.unsubscribe(position, p_aPos);
}


YAHOO.lang.extend(ContextMenu, YAHOO.widget.Menu, {



// Private properties


/**
* @property _oTrigger
* @description Object reference to the current value of the "trigger" 
* configuration property.
* @default null
* @private
* @type String|<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/leve
* l-one-html.html#ID-58190037">HTMLElement</a>|Array
*/
_oTrigger: null,


/**
* @property _bCancelled
* @description Boolean indicating if the display of the context menu should 
* be cancelled.
* @default false
* @private
* @type Boolean
*/
_bCancelled: false,



// Public properties


/**
* @property contextEventTarget
* @description Object reference for the HTML element that was the target of the
* "contextmenu" DOM event ("mousedown" for Opera) that triggered the display of 
* the context menu.
* @default null
* @type <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-
* html.html#ID-58190037">HTMLElement</a>
*/
contextEventTarget: null,



// Events


/**
* @event triggerContextMenuEvent
* @param type {String} The name of the event, "triggerContextMenu"
* @param args {Array} The array of event arguments. For this event, the underlying
* DOM event is the only argument, available from args[0].
* @description Custom Event wrapper for the "contextmenu" DOM event 
* ("mousedown" for Opera) fired by the element(s) that trigger the display of 
* the context menu.
*/
triggerContextMenuEvent: null,



/**
* @method init
* @description The ContextMenu class's initialization method. 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.
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;div&#62;</code> element of the context menu.
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;select&#62;</code> element to be used as the data source for 
* the context menu.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-
* html.html#ID-22445964">HTMLDivElement</a>} p_oElement Object specifying the 
* <code>&#60;div&#62;</code> element of the context menu.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-
* html.html#ID-94282980">HTMLSelectElement</a>} p_oElement Object specifying 
* the <code>&#60;select&#62;</code> element to be used as the data source for 
* the context menu.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the context menu. See configuration class documentation 
* for more details.
*/
init: function(p_oElement, p_oConfig) {


    // Call the init of the superclass (YAHOO.widget.Menu)
    
    ContextMenu.superclass.init.call(this, p_oElement);

    this.beforeInitEvent.fire(ContextMenu);

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

    this.initEvent.fire(ContextMenu);
},


/**
* @method initEvents
* @description Initializes the custom events for the context menu.
*/
initEvents: function() {
    ContextMenu.superclass.initEvents.call(this);

    // Create custom events
    this.triggerContextMenuEvent = this.createEvent(EVENT_TYPES.TRIGGER_CONTEXT_MENU);
    this.triggerContextMenuEvent.signature = YAHOO.util.CustomEvent.LIST;
},

/**
* @method cancel
* @description Cancels the display of the context menu.
*/
cancel: function() {
    this._bCancelled = true;
},

// Private methods


/**
* @method _removeEventHandlers
* @description Removes all of the DOM event handlers from the HTML element(s) 
* whose "context menu" event ("click" for Opera) trigger the display of 
* the context menu.
* @private
*/
_removeEventHandlers: function() {

    var oTrigger = this._oTrigger;

    // Remove the event handlers from the trigger(s)
    if (oTrigger) {
        Event.removeListener(oTrigger, EVENT_TYPES.CONTEXT_MENU, this._onTriggerContextMenu);    

        if (UA.opera) {
            Event.removeListener(oTrigger, EVENT_TYPES.CLICK, this._onTriggerClick);
        }
    }

},

// Private event handlers

/**
* @method _onTriggerClick
* @description "click" event handler for the HTML element(s) identified as the 
* "trigger" for the context menu.  Used to cancel default behaviors in Opera.
* @private
* @param {Event} p_oEvent Object representing the DOM event object passed back 
* by the event utility (YAHOO.util.Event).
* @param {YAHOO.widget.ContextMenu} p_oMenu Object representing the context 
* menu that is handling the event.
*/
_onTriggerClick: function(p_oEvent, p_oMenu) {

    if (p_oEvent.ctrlKey) {
        Event.stopEvent(p_oEvent);
    }
    
},


/**
* @method _onTriggerContextMenu
* @description "contextmenu" event handler ("mousedown" for Opera) for the HTML 
* element(s) that trigger the display of the context menu.
* @private
* @param {Event} p_oEvent Object representing the DOM event object passed back 
* by the event utility (YAHOO.util.Event).
* @param {YAHOO.widget.ContextMenu} p_oMenu Object representing the context 
* menu that is handling the event.
*/
_onTriggerContextMenu: function(p_oEvent, p_oMenu) {

    var aXY;

    if (!(p_oEvent.type == _MOUSEDOWN && !p_oEvent.ctrlKey)) {
    
        this.contextEventTarget = Event.getTarget(p_oEvent);
    
        this.triggerContextMenuEvent.fire(p_oEvent);
        
    
        if (!this._bCancelled) {

            /*
                Prevent the browser's default context menu from appearing and 
                stop the propagation of the "contextmenu" event so that 
                other ContextMenu instances are not displayed.
            */

            Event.stopEvent(p_oEvent);


            // Hide any other Menu instances that might be visible

            YAHOO.widget.MenuManager.hideVisible();
            
    

            // Position and display the context menu
    
            aXY = Event.getXY(p_oEvent);
    
    
            if (!YAHOO.util.Dom.inDocument(this.element)) {
    
                this.beforeShowEvent.subscribe(position, aXY);
    
            }
            else {
    
                this.cfg.setProperty(_XY, aXY);
            
            }
    
    
            this.show();
    
        }
    
        this._bCancelled = false;

    }

},



// Public methods


/**
* @method toString
* @description Returns a string representing the context menu.
* @return {String}
*/
toString: function() {

    var sReturnVal = _CONTEXTMENU,
        sId = this.id;

    if (sId) {

        sReturnVal += (_SPACE + sId);
    
    }

    return sReturnVal;

},


/**
* @method initDefaultConfig
* @description Initializes the class's configurable properties which can be 
* changed using the context menu's Config object ("cfg").
*/
initDefaultConfig: function() {

    ContextMenu.superclass.initDefaultConfig.call(this);

    /**
    * @config trigger
    * @description The HTML element(s) whose "contextmenu" event ("mousedown" 
    * for Opera) trigger the display of the context menu.  Can be a string 
    * representing the id attribute of the HTML element, an object reference 
    * for the HTML element, or an array of strings or HTML element references.
    * @default null
    * @type String|<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/
    * level-one-html.html#ID-58190037">HTMLElement</a>|Array
    */
    this.cfg.addProperty(TRIGGER_CONFIG.key, 
        {
            handler: this.configTrigger, 
            suppressEvent: TRIGGER_CONFIG.suppressEvent 
        }
    );

},


/**
* @method destroy
* @description Removes the context menu's <code>&#60;div&#62;</code> element 
* (and accompanying child nodes) from the document.
* @param {boolean} shallowPurge If true, only the parent element's DOM event listeners are purged. If false, or not provided, all children are also purged of DOM event listeners. 
* NOTE: The flag is a "shallowPurge" flag, as opposed to what may be a more intuitive "purgeChildren" flag to maintain backwards compatibility with behavior prior to 2.9.0.
*/
destroy: function(shallowPurge) {

    // Remove the DOM event handlers from the current trigger(s)

    this._removeEventHandlers();


    // Continue with the superclass implementation of this method

    ContextMenu.superclass.destroy.call(this, shallowPurge);

},



// Public event handlers for configuration properties


/**
* @method configTrigger
* @description Event handler for when the value of the "trigger" configuration 
* property changes. 
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.ContextMenu} p_oMenu Object representing the context 
* menu that fired the event.
*/
configTrigger: function(p_sType, p_aArgs, p_oMenu) {
    
    var oTrigger = p_aArgs[0];

    if (oTrigger) {

        /*
            If there is a current "trigger" - remove the event handlers 
            from that element(s) before assigning new ones
        */

        if (this._oTrigger) {
        
            this._removeEventHandlers();

        }

        this._oTrigger = oTrigger;


        /*
            Listen for the "mousedown" event in Opera b/c it does not 
            support the "contextmenu" event
        */ 
  
        Event.on(oTrigger, EVENT_TYPES.CONTEXT_MENU, this._onTriggerContextMenu, this, true);


        /*
            Assign a "click" event handler to the trigger element(s) for
            Opera to prevent default browser behaviors.
        */

        if (UA.opera) {
        
            Event.on(oTrigger, EVENT_TYPES.CLICK, this._onTriggerClick, this, true);

        }

    }
    else {
   
        this._removeEventHandlers();
    
    }
    
}

}); // END YAHOO.lang.extend

}());



/**
* Creates an item for a context menu.
* 
* @param {String} p_oObject String specifying the text of the context menu item.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-74680021">HTMLLIElement</a>} p_oObject Object specifying the 
* <code>&#60;li&#62;</code> element of the context menu item.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-38450247">HTMLOptGroupElement</a>} p_oObject Object 
* specifying the <code>&#60;optgroup&#62;</code> element of the context 
* menu item.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-70901257">HTMLOptionElement</a>} p_oObject Object specifying 
* the <code>&#60;option&#62;</code> element of the context menu item.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the context menu item. See configuration class 
* documentation for more details.
* @class ContextMenuItem
* @constructor
* @extends YAHOO.widget.MenuItem
* @deprecated As of version 2.4.0 items for YAHOO.widget.ContextMenu instances
* are of type YAHOO.widget.MenuItem.
*/
YAHOO.widget.ContextMenuItem = YAHOO.widget.MenuItem;
(function () {

    var Lang = YAHOO.lang,

        // String constants
    
        _STATIC = "static",
        _DYNAMIC_STATIC = "dynamic," + _STATIC,
        _DISABLED = "disabled",
        _SELECTED = "selected",
        _AUTO_SUBMENU_DISPLAY = "autosubmenudisplay",
        _SUBMENU = "submenu",
        _VISIBLE = "visible",
        _SPACE = " ",
        _SUBMENU_TOGGLE_REGION = "submenutoggleregion",
        _MENUBAR = "MenuBar";

/**
* Horizontal collection of items, each of which can contain a submenu.
* 
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;div&#62;</code> element of the menu bar.
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;select&#62;</code> element to be used as the data source for the 
* menu bar.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-22445964">HTMLDivElement</a>} p_oElement Object specifying 
* the <code>&#60;div&#62;</code> element of the menu bar.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-94282980">HTMLSelectElement</a>} p_oElement Object 
* specifying the <code>&#60;select&#62;</code> element to be used as the data 
* source for the menu bar.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the menu bar. See configuration class documentation for
* more details.
* @class MenuBar
* @constructor
* @extends YAHOO.widget.Menu
* @namespace YAHOO.widget
*/
YAHOO.widget.MenuBar = function(p_oElement, p_oConfig) {

    YAHOO.widget.MenuBar.superclass.constructor.call(this, p_oElement, p_oConfig);

};


/**
* @method checkPosition
* @description Checks to make sure that the value of the "position" property 
* is one of the supported strings. Returns true if the position is supported.
* @private
* @param {Object} p_sPosition String specifying the position of the menu.
* @return {Boolean}
*/
function checkPosition(p_sPosition) {

    var returnVal = false;

    if (Lang.isString(p_sPosition)) {

        returnVal = (_DYNAMIC_STATIC.indexOf((p_sPosition.toLowerCase())) != -1);

    }
    
    return returnVal;

}


var Event = YAHOO.util.Event,
    MenuBar = YAHOO.widget.MenuBar,

    POSITION_CONFIG =  { 
        key: "position", 
        value: _STATIC, 
        validator: checkPosition, 
        supercedes: [_VISIBLE] 
    }, 

    SUBMENU_ALIGNMENT_CONFIG =  { 
        key: "submenualignment", 
        value: ["tl","bl"]
    },

    AUTO_SUBMENU_DISPLAY_CONFIG =  { 
        key: _AUTO_SUBMENU_DISPLAY, 
        value: false, 
        validator: Lang.isBoolean,
        suppressEvent: true
    },
    
    SUBMENU_TOGGLE_REGION_CONFIG = {
        key: _SUBMENU_TOGGLE_REGION, 
        value: false, 
        validator: Lang.isBoolean
    };



Lang.extend(MenuBar, YAHOO.widget.Menu, {

/**
* @method init
* @description The MenuBar class's initialization method. 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.
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;div&#62;</code> element of the menu bar.
* @param {String} p_oElement String specifying the id attribute of the 
* <code>&#60;select&#62;</code> element to be used as the data source for the 
* menu bar.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-22445964">HTMLDivElement</a>} p_oElement Object specifying 
* the <code>&#60;div&#62;</code> element of the menu bar.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-94282980">HTMLSelectElement</a>} p_oElement Object 
* specifying the <code>&#60;select&#62;</code> element to be used as the data 
* source for the menu bar.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the menu bar. See configuration class documentation for
* more details.
*/
init: function(p_oElement, p_oConfig) {

    if(!this.ITEM_TYPE) {

        this.ITEM_TYPE = YAHOO.widget.MenuBarItem;

    }


    // Call the init of the superclass (YAHOO.widget.Menu)

    MenuBar.superclass.init.call(this, p_oElement);


    this.beforeInitEvent.fire(MenuBar);


    if(p_oConfig) {

        this.cfg.applyConfig(p_oConfig, true);

    }

    this.initEvent.fire(MenuBar);

},



// Constants


/**
* @property CSS_CLASS_NAME
* @description String representing the CSS class(es) to be applied to the menu 
* bar's <code>&#60;div&#62;</code> element.
* @default "yuimenubar"
* @final
* @type String
*/
CSS_CLASS_NAME: "yuimenubar",


/**
* @property SUBMENU_TOGGLE_REGION_WIDTH
* @description Width (in pixels) of the area of a MenuBarItem that, when pressed, will toggle the
* display of the MenuBarItem's submenu.
* @default 20
* @final
* @type Number
*/
SUBMENU_TOGGLE_REGION_WIDTH: 20,


// Protected event handlers


/**
* @method _onKeyDown
* @description "keydown" Custom Event handler for the menu bar.
* @private
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.MenuBar} p_oMenuBar Object representing the menu bar 
* that fired the event.
*/
_onKeyDown: function(p_sType, p_aArgs, p_oMenuBar) {

    var oEvent = p_aArgs[0],
        oItem = p_aArgs[1],
        oSubmenu,
        oItemCfg,
        oNextItem;


    if(oItem && !oItem.cfg.getProperty(_DISABLED)) {

        oItemCfg = oItem.cfg;

        switch(oEvent.keyCode) {
    
            case 37:    // Left arrow
            case 39:    // Right arrow
    
                if(oItem == this.activeItem && !oItemCfg.getProperty(_SELECTED)) {
    
                    oItemCfg.setProperty(_SELECTED, true);
    
                }
                else {
    
                    oNextItem = (oEvent.keyCode == 37) ? 
                        oItem.getPreviousEnabledSibling() : 
                        oItem.getNextEnabledSibling();
            
                    if(oNextItem) {
    
                        this.clearActiveItem();
    
                        oNextItem.cfg.setProperty(_SELECTED, true);
                        
                        oSubmenu = oNextItem.cfg.getProperty(_SUBMENU);
                        
                        if(oSubmenu) {
                    
                            oSubmenu.show();
                            oSubmenu.setInitialFocus();
                        
                        }
                        else {
                            oNextItem.focus();  
                        }
    
                    }
    
                }
    
                Event.preventDefault(oEvent);
    
            break;
    
            case 40:    // Down arrow
    
                if(this.activeItem != oItem) {
    
                    this.clearActiveItem();
    
                    oItemCfg.setProperty(_SELECTED, true);
                    oItem.focus();
                
                }
    
                oSubmenu = oItemCfg.getProperty(_SUBMENU);
    
                if(oSubmenu) {
    
                    if(oSubmenu.cfg.getProperty(_VISIBLE)) {
    
                        oSubmenu.setInitialSelection();
                        oSubmenu.setInitialFocus();
                    
                    }
                    else {
    
                        oSubmenu.show();
                        oSubmenu.setInitialFocus();
                    
                    }
    
                }
    
                Event.preventDefault(oEvent);
    
            break;
    
        }

    }


    if(oEvent.keyCode == 27 && this.activeItem) { // Esc key

        oSubmenu = this.activeItem.cfg.getProperty(_SUBMENU);

        if(oSubmenu && oSubmenu.cfg.getProperty(_VISIBLE)) {
        
            oSubmenu.hide();
            this.activeItem.focus();
        
        }
        else {

            this.activeItem.cfg.setProperty(_SELECTED, false);
            this.activeItem.blur();
    
        }

        Event.preventDefault(oEvent);
    
    }

},


/**
* @method _onClick
* @description "click" event handler for the menu bar.
* @protected
* @param {String} p_sType String representing the name of the event that 
* was fired.
* @param {Array} p_aArgs Array of arguments sent when the event was fired.
* @param {YAHOO.widget.MenuBar} p_oMenuBar Object representing the menu bar 
* that fired the event.
*/
_onClick: function(p_sType, p_aArgs, p_oMenuBar) {

    MenuBar.superclass._onClick.call(this, p_sType, p_aArgs, p_oMenuBar);

    var oItem = p_aArgs[1],
        bReturnVal = true,
        oItemEl,
        oEvent,
        oTarget,
        oActiveItem,
        oConfig,
        oSubmenu,
        nMenuItemX,
        nToggleRegion;


    var toggleSubmenuDisplay = function () {

        if(oSubmenu.cfg.getProperty(_VISIBLE)) {
        
            oSubmenu.hide();
        
        }
        else {
        
            oSubmenu.show();                    
        
        }
    
    };
    

    if(oItem && !oItem.cfg.getProperty(_DISABLED)) {

        oEvent = p_aArgs[0];
        oTarget = Event.getTarget(oEvent);
        oActiveItem = this.activeItem;
        oConfig = this.cfg;


        // Hide any other submenus that might be visible
    
        if(oActiveItem && oActiveItem != oItem) {
    
            this.clearActiveItem();
    
        }

    
        oItem.cfg.setProperty(_SELECTED, true);
    

        // Show the submenu for the item
    
        oSubmenu = oItem.cfg.getProperty(_SUBMENU);


        if(oSubmenu) {

            oItemEl = oItem.element;
            nMenuItemX = YAHOO.util.Dom.getX(oItemEl);
            nToggleRegion = nMenuItemX + (oItemEl.offsetWidth - this.SUBMENU_TOGGLE_REGION_WIDTH);

            if (oConfig.getProperty(_SUBMENU_TOGGLE_REGION)) {

                if (Event.getPageX(oEvent) > nToggleRegion) {

                    toggleSubmenuDisplay();

                    Event.preventDefault(oEvent);

                    /*
                         Return false so that other click event handlers are not called when the 
                         user clicks inside the toggle region.
                    */
                    bReturnVal = false;
                
                }
        
            }
            else {

                toggleSubmenuDisplay();
            
            }
        
        }
    
    }


    return bReturnVal;

},



// Public methods

/**
* @method configSubmenuToggle
* @description Event handler for when the "submenutoggleregion" configuration property of 
* a MenuBar changes.
* @param {String} p_sType The name of the event that was fired.
* @param {Array} p_aArgs Collection of arguments sent when the event was fired.
*/
configSubmenuToggle: function (p_sType, p_aArgs) {

    var bSubmenuToggle = p_aArgs[0];
    
    if (bSubmenuToggle) {
    
        this.cfg.setProperty(_AUTO_SUBMENU_DISPLAY, false);
    
    }

},


/**
* @method toString
* @description Returns a string representing the menu bar.
* @return {String}
*/
toString: function() {

    var sReturnVal = _MENUBAR,
        sId = this.id;

    if(sId) {

        sReturnVal += (_SPACE + sId);
    
    }

    return sReturnVal;

},


/**
* @description Initializes the class's configurable properties which can be
* changed using the menu bar's Config object ("cfg").
* @method initDefaultConfig
*/
initDefaultConfig: function() {

    MenuBar.superclass.initDefaultConfig.call(this);

    var oConfig = this.cfg;

    // Add configuration properties


    /*
        Set the default value for the "position" configuration property
        to "static" by re-adding the property.
    */


    /**
    * @config position
    * @description String indicating how a menu bar should be positioned on the 
    * screen.  Possible values are "static" and "dynamic."  Static menu bars 
    * are visible by default and reside in the normal flow of the document 
    * (CSS position: static).  Dynamic menu bars are hidden by default, reside
    * out of the normal flow of the document (CSS position: absolute), and can 
    * overlay other elements on the screen.
    * @default static
    * @type String
    */
    oConfig.addProperty(
        POSITION_CONFIG.key, 
        {
            handler: this.configPosition, 
            value: POSITION_CONFIG.value, 
            validator: POSITION_CONFIG.validator,
            supercedes: POSITION_CONFIG.supercedes
        }
    );


    /*
        Set the default value for the "submenualignment" configuration property
        to ["tl","bl"] by re-adding the property.
    */

    /**
    * @config submenualignment
    * @description Array defining how submenus should be aligned to their 
    * parent menu bar item. The format is: [itemCorner, submenuCorner].
    * @default ["tl","bl"]
    * @type Array
    */
    oConfig.addProperty(
        SUBMENU_ALIGNMENT_CONFIG.key, 
        {
            value: SUBMENU_ALIGNMENT_CONFIG.value,
            suppressEvent: SUBMENU_ALIGNMENT_CONFIG.suppressEvent
        }
    );


    /*
        Change the default value for the "autosubmenudisplay" configuration 
        property to "false" by re-adding the property.
    */

    /**
    * @config autosubmenudisplay
    * @description Boolean indicating if submenus are automatically made 
    * visible when the user mouses over the menu bar's items.
    * @default false
    * @type Boolean
    */
    oConfig.addProperty(
       AUTO_SUBMENU_DISPLAY_CONFIG.key, 
       {
           value: AUTO_SUBMENU_DISPLAY_CONFIG.value, 
           validator: AUTO_SUBMENU_DISPLAY_CONFIG.validator,
           suppressEvent: AUTO_SUBMENU_DISPLAY_CONFIG.suppressEvent
       } 
    );


    /**
    * @config submenutoggleregion
    * @description Boolean indicating if only a specific region of a MenuBarItem should toggle the 
    * display of a submenu.  The default width of the region is determined by the value of the
    * SUBMENU_TOGGLE_REGION_WIDTH property.  If set to true, the autosubmenudisplay 
    * configuration property will be set to false, and any click event listeners will not be 
    * called when the user clicks inside the submenu toggle region of a MenuBarItem.  If the 
    * user clicks outside of the submenu toggle region, the MenuBarItem will maintain its 
    * standard behavior.
    * @default false
    * @type Boolean
    */
    oConfig.addProperty(
       SUBMENU_TOGGLE_REGION_CONFIG.key, 
       {
           value: SUBMENU_TOGGLE_REGION_CONFIG.value, 
           validator: SUBMENU_TOGGLE_REGION_CONFIG.validator,
           handler: this.configSubmenuToggle
       } 
    );

}
 
}); // END YAHOO.lang.extend

}());



/**
* Creates an item for a menu bar.
* 
* @param {HTML} p_oObject Markup for the menu item content. The markup is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-74680021">HTMLLIElement</a>} p_oObject Object specifying the 
* <code>&#60;li&#62;</code> element of the menu bar item.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-38450247">HTMLOptGroupElement</a>} p_oObject Object 
* specifying the <code>&#60;optgroup&#62;</code> element of the menu bar item.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-70901257">HTMLOptionElement</a>} p_oObject Object specifying 
* the <code>&#60;option&#62;</code> element of the menu bar item.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the menu bar item. See configuration class documentation 
* for more details.
* @class MenuBarItem
* @constructor
* @extends YAHOO.widget.MenuItem
*/
YAHOO.widget.MenuBarItem = function(p_oObject, p_oConfig) {

    YAHOO.widget.MenuBarItem.superclass.constructor.call(this, p_oObject, p_oConfig);

};

YAHOO.lang.extend(YAHOO.widget.MenuBarItem, YAHOO.widget.MenuItem, {



/**
* @method init
* @description The MenuBarItem class's initialization method. 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.
* @param {HTML} p_oObject Markup for the menu item content. The markup is inserted into the DOM as HTML, and should be escaped by the implementor if coming from an external source.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-74680021">HTMLLIElement</a>} p_oObject Object specifying the 
* <code>&#60;li&#62;</code> element of the menu bar item.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-38450247">HTMLOptGroupElement</a>} p_oObject Object 
* specifying the <code>&#60;optgroup&#62;</code> element of the menu bar item.
* @param {<a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-
* one-html.html#ID-70901257">HTMLOptionElement</a>} p_oObject Object specifying 
* the <code>&#60;option&#62;</code> element of the menu bar item.
* @param {Object} p_oConfig Optional. Object literal specifying the 
* configuration for the menu bar item. See configuration class documentation 
* for more details.
*/
init: function(p_oObject, p_oConfig) {

    if(!this.SUBMENU_TYPE) {

        this.SUBMENU_TYPE = YAHOO.widget.Menu;

    }


    /* 
        Call the init of the superclass (YAHOO.widget.MenuItem)
        Note: We don't pass the user config in here yet 
        because we only want it executed once, at the lowest 
        subclass level.
    */ 

    YAHOO.widget.MenuBarItem.superclass.init.call(this, p_oObject);  


    var oConfig = this.cfg;

    if(p_oConfig) {

        oConfig.applyConfig(p_oConfig, true);

    }

    oConfig.fireQueue();

},



// Constants


/**
* @property CSS_CLASS_NAME
* @description String representing the CSS class(es) to be applied to the 
* <code>&#60;li&#62;</code> element of the menu bar item.
* @default "yuimenubaritem"
* @final
* @type String
*/
CSS_CLASS_NAME: "yuimenubaritem",


/**
* @property CSS_LABEL_CLASS_NAME
* @description String representing the CSS class(es) to be applied to the 
* menu bar item's <code>&#60;a&#62;</code> element.
* @default "yuimenubaritemlabel"
* @final
* @type String
*/
CSS_LABEL_CLASS_NAME: "yuimenubaritemlabel",



// Public methods


/**
* @method toString
* @description Returns a string representing the menu bar item.
* @return {String}
*/
toString: function() {

    var sReturnVal = "MenuBarItem";

    if(this.cfg && this.cfg.getProperty("text")) {

        sReturnVal += (": " + this.cfg.getProperty("text"));

    }

    return sReturnVal;

}
    
}); // END YAHOO.lang.extend
YAHOO.register("menu", YAHOO.widget.Menu, {version: "2.9.0", build: "2800"});
/*
Copyright (c) 2011, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 2.9.0
*/
(function () {
    var Dom = YAHOO.util.Dom,
        Event = YAHOO.util.Event,
        Lang = YAHOO.lang,
        Widget = YAHOO.widget;



/**
 * The treeview widget is a generic tree building tool.
 * @module treeview
 * @title TreeView Widget
 * @requires yahoo, dom, event
 * @optional animation, json, calendar
 * @namespace YAHOO.widget
 */

/**
 * Contains the tree view state data and the root node.
 *
 * @class TreeView
 * @uses YAHOO.util.EventProvider
 * @constructor
 * @param {string|HTMLElement} id The id of the element, or the element itself that the tree will be inserted into.
 *        Existing markup in this element, if valid, will be used to build the tree
 * @param {Array|Object|String}  oConfig (optional)  If present, it will be used to build the tree via method <a href="#method_buildTreeFromObject">buildTreeFromObject</a>
 *
 */
YAHOO.widget.TreeView = function(id, oConfig) {
    if (id) { this.init(id); }
    if (oConfig) {
        this.buildTreeFromObject(oConfig);
    } else if (Lang.trim(this._el.innerHTML)) {
        this.buildTreeFromMarkup(id);
    }
};

var TV = Widget.TreeView;

TV.prototype = {

    /**
     * The id of tree container element
     * @property id
     * @type String
     */
    id: null,

    /**
     * The host element for this tree
     * @property _el
     * @private
     * @type HTMLelement
     */
    _el: null,

     /**
     * Flat collection of all nodes in this tree.  This is a sparse
     * array, so the length property can't be relied upon for a
     * node count for the tree.
     * @property _nodes
     * @type Node[]
     * @private
     */
    _nodes: null,

    /**
     * We lock the tree control while waiting for the dynamic loader to return
     * @property locked
     * @type boolean
     */
    locked: false,

    /**
     * The animation to use for expanding children, if any
     * @property _expandAnim
     * @type string
     * @private
     */
    _expandAnim: null,

    /**
     * The animation to use for collapsing children, if any
     * @property _collapseAnim
     * @type string
     * @private
     */
    _collapseAnim: null,

    /**
     * The current number of animations that are executing
     * @property _animCount
     * @type int
     * @private
     */
    _animCount: 0,

    /**
     * The maximum number of animations to run at one time.
     * @property maxAnim
     * @type int
     */
    maxAnim: 2,

    /**
     * Whether there is any subscriber to dblClickEvent
     * @property _hasDblClickSubscriber
     * @type boolean
     * @private
     */
    _hasDblClickSubscriber: false,

    /**
     * Stores the timer used to check for double clicks
     * @property _dblClickTimer
     * @type window.timer object
     * @private
     */
    _dblClickTimer: null,

  /**
     * A reference to the Node currently having the focus or null if none.
     * @property currentFocus
     * @type YAHOO.widget.Node
     */
    currentFocus: null,

    /**
    * If true, only one Node can be highlighted at a time
    * @property singleNodeHighlight
    * @type boolean
    * @default false
    */

    singleNodeHighlight: false,

    /**
    * A reference to the Node that is currently highlighted.
    * It is only meaningful if singleNodeHighlight is enabled
    * @property _currentlyHighlighted
    * @type YAHOO.widget.Node
    * @default null
    * @private
    */

    _currentlyHighlighted: null,

    /**
     * Sets up the animation for expanding children
     * @method setExpandAnim
     * @param {string} type the type of animation (acceptable values defined
     * in YAHOO.widget.TVAnim)
     */
    setExpandAnim: function(type) {
        this._expandAnim = (Widget.TVAnim.isValid(type)) ? type : null;
    },

    /**
     * Sets up the animation for collapsing children
     * @method setCollapseAnim
     * @param {string} type of animation (acceptable values defined in
     * YAHOO.widget.TVAnim)
     */
    setCollapseAnim: function(type) {
        this._collapseAnim = (Widget.TVAnim.isValid(type)) ? type : null;
    },

    /**
     * Perform the expand animation if configured, or just show the
     * element if not configured or too many animations are in progress
     * @method animateExpand
     * @param el {HTMLElement} the element to animate
     * @param node {YAHOO.util.Node} the node that was expanded
     * @return {boolean} true if animation could be invoked, false otherwise
     */
    animateExpand: function(el, node) {
        this.logger.log("animating expand");

        if (this._expandAnim && this._animCount < this.maxAnim) {
            // this.locked = true;
            var tree = this;
            var a = Widget.TVAnim.getAnim(this._expandAnim, el,
                            function() { tree.expandComplete(node); });
            if (a) {
                ++this._animCount;
                this.fireEvent("animStart", {
                        "node": node,
                        "type": "expand"
                    });
                a.animate();
            }

            return true;
        }

        return false;
    },

    /**
     * Perform the collapse animation if configured, or just show the
     * element if not configured or too many animations are in progress
     * @method animateCollapse
     * @param el {HTMLElement} the element to animate
     * @param node {YAHOO.util.Node} the node that was expanded
     * @return {boolean} true if animation could be invoked, false otherwise
     */
    animateCollapse: function(el, node) {
        this.logger.log("animating collapse");

        if (this._collapseAnim && this._animCount < this.maxAnim) {
            // this.locked = true;
            var tree = this;
            var a = Widget.TVAnim.getAnim(this._collapseAnim, el,
                            function() { tree.collapseComplete(node); });
            if (a) {
                ++this._animCount;
                this.fireEvent("animStart", {
                        "node": node,
                        "type": "collapse"
                    });
                a.animate();
            }

            return true;
        }

        return false;
    },

    /**
     * Function executed when the expand animation completes
     * @method expandComplete
     */
    expandComplete: function(node) {
        this.logger.log("expand complete: " + this.id);
        --this._animCount;
        this.fireEvent("animComplete", {
                "node": node,
                "type": "expand"
            });
        // this.locked = false;
    },

    /**
     * Function executed when the collapse animation completes
     * @method collapseComplete
     */
    collapseComplete: function(node) {
        this.logger.log("collapse complete: " + this.id);
        --this._animCount;
        this.fireEvent("animComplete", {
                "node": node,
                "type": "collapse"
            });
        // this.locked = false;
    },

    /**
     * Initializes the tree
     * @method init
     * @parm {string|HTMLElement} id the id of the element that will hold the tree
     * @private
     */
    init: function(id) {
        this._el = Dom.get(id);
        this.id = Dom.generateId(this._el,"yui-tv-auto-id-");

    /**
         * When animation is enabled, this event fires when the animation
         * starts
         * @event animStart
         * @type CustomEvent
         * @param {YAHOO.widget.Node} oArgs.node the node that is expanding/collapsing
         * @param {String} oArgs.type the type of animation ("expand" or "collapse")
         */
        this.createEvent("animStart", this);

        /**
         * When animation is enabled, this event fires when the animation
         * completes
         * @event animComplete
         * @type CustomEvent
         * @param {YAHOO.widget.Node} oArgs.node the node that is expanding/collapsing
         * @param {String} oArgs.type the type of animation ("expand" or "collapse")
         */
        this.createEvent("animComplete", this);

        /**
         * Fires when a node is going to be collapsed.  Return false to stop
         * the collapse.
         * @event collapse
         * @type CustomEvent
         * @param {YAHOO.widget.Node} node the node that is collapsing
         */
        this.createEvent("collapse", this);

        /**
         * Fires after a node is successfully collapsed.  This event will not fire
         * if the "collapse" event was cancelled.
         * @event collapseComplete
         * @type CustomEvent
         * @param {YAHOO.widget.Node} node the node that was collapsed
         */
        this.createEvent("collapseComplete", this);

        /**
         * Fires when a node is going to be expanded.  Return false to stop
         * the collapse.
         * @event expand
         * @type CustomEvent
         * @param {YAHOO.widget.Node} node the node that is expanding
         */
        this.createEvent("expand", this);

        /**
         * Fires after a node is successfully expanded.  This event will not fire
         * if the "expand" event was cancelled.
         * @event expandComplete
         * @type CustomEvent
         * @param {YAHOO.widget.Node} node the node that was expanded
         */
        this.createEvent("expandComplete", this);

    /**
         * Fires when the Enter key is pressed on a node that has the focus
         * @event enterKeyPressed
         * @type CustomEvent
         * @param {YAHOO.widget.Node} node the node that has the focus
         */
        this.createEvent("enterKeyPressed", this);

    /**
         * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a Click.
    * The listener may return false to cancel toggling and focusing on the node.
         * @event clickEvent
         * @type CustomEvent
         * @param oArgs.event  {HTMLEvent} The event object
         * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked
         */
        this.createEvent("clickEvent", this);

    /**
         * Fires when the focus receives the focus, when it changes from a Node
    * to another Node or when it is completely lost (blurred)
         * @event focusChanged
         * @type CustomEvent
         * @param oArgs.oldNode  {YAHOO.widget.Node} Node that had the focus or null if none
         * @param oArgs.newNode {YAHOO.widget.Node} Node that receives the focus or null if none
         */

        this.createEvent('focusChanged',this);

    /**
         * Fires when the label in a TextNode or MenuNode or content in an HTMLNode receives a double Click
         * @event dblClickEvent
         * @type CustomEvent
         * @param oArgs.event  {HTMLEvent} The event object
         * @param oArgs.node {YAHOO.widget.Node} node the node that was clicked
         */
        var self = this;
        this.createEvent("dblClickEvent", {
            scope:this,
            onSubscribeCallback: function() {
                self._hasDblClickSubscriber = true;
            }
        });

    /**
         * Custom event that is fired when the text node label is clicked.
         *  The node clicked is  provided as an argument
         *
         * @event labelClick
         * @type CustomEvent
         * @param {YAHOO.widget.Node} node the node clicked
    * @deprecated use clickEvent or dblClickEvent
         */
        this.createEvent("labelClick", this);

    /**
     * Custom event fired when the highlight of a node changes.
     * The node that triggered the change is provided as an argument:
     * The status of the highlight can be checked in
     * <a href="YAHOO.widget.Node.html#property_highlightState">nodeRef.highlightState</a>.
     * Depending on <a href="YAHOO.widget.Node.html#property_propagateHighlight">nodeRef.propagateHighlight</a>, other nodes might have changed
     * @event highlightEvent
     * @type CustomEvent
     * @param node {YAHOO.widget.Node} the node that started the change in highlighting state
    */
        this.createEvent("highlightEvent",this);


        this._nodes = [];

        // store a global reference
        TV.trees[this.id] = this;

        // Set up the root node
        this.root = new Widget.RootNode(this);

        var LW = Widget.LogWriter;

        this.logger = (LW) ? new LW(this.toString()) : YAHOO;

        this.logger.log("tree init: " + this.id);

        if (this._initEditor) {
            this._initEditor();
        }

        // YAHOO.util.Event.onContentReady(this.id, this.handleAvailable, this, true);
        // YAHOO.util.Event.on(this.id, "click", this.handleClick, this, true);
    },

    //handleAvailable: function() {
        //var Event = YAHOO.util.Event;
        //Event.on(this.id,
    //},
 /**
     * Builds the TreeView from an object.
     * This is the method called by the constructor to build the tree when it has a second argument.
     *  A tree can be described by an array of objects, each object corresponding to a node.
     *  Node descriptions may contain values for any property of a node plus the following extra properties: <ul>
     * <li>type:  can be one of the following:<ul>
     *    <li> A shortname for a node type (<code>'text','menu','html'</code>) </li>
     *    <li>The name of a Node class under YAHOO.widget (<code>'TextNode', 'MenuNode', 'DateNode'</code>, etc) </li>
     *    <li>a reference to an actual class: <code>YAHOO.widget.DateNode</code></li>
     * </ul></li>
     * <li>children: an array containing further node definitions</li></ul>
     * A string instead of an object will produce a node of type 'text' with the given string as its label.
     * @method buildTreeFromObject
     * @param  oConfig {Array|Object|String}  array containing a full description of the tree.
     *        An object or a string will be turned into an array with the given object or string as its only element.
     *
     */
    buildTreeFromObject: function (oConfig) {
        var logger = this.logger;
        logger.log('Building tree from object');
        var build = function (parent, oConfig) {
            var i, item, node, children, type, NodeType, ThisType;
            for (i = 0; i < oConfig.length; i++) {
                item = oConfig[i];
                if (Lang.isString(item)) {
                    node = new Widget.TextNode(item, parent);
                } else if (Lang.isObject(item)) {
                    children = item.children;
                    delete item.children;
                    type = item.type || 'text';
                    delete item.type;
                    switch (Lang.isString(type) && type.toLowerCase()) {
                        case 'text':
                            node = new Widget.TextNode(item, parent);
                            break;
                        case 'menu':
                            node = new Widget.MenuNode(item, parent);
                            break;
                        case 'html':
                            node = new Widget.HTMLNode(item, parent);
                            break;
                        default:
                            if (Lang.isString(type)) {
                                NodeType = Widget[type];
                            } else {
                                NodeType = type;
                            }
                            if (Lang.isObject(NodeType)) {
                                for (ThisType = NodeType; ThisType && ThisType !== Widget.Node; ThisType = ThisType.superclass.constructor) {}
                                if (ThisType) {
                                    node = new NodeType(item, parent);
                                } else {
                                    logger.log('Invalid type in node definition: ' + type,'error');
                                }
                            } else {
                                logger.log('Invalid type in node definition: ' + type,'error');
                            }
                    }
                    if (children) {
                        build(node,children);
                    }
                } else {
                    logger.log('Invalid node definition','error');
                }
            }
        };
        if (!Lang.isArray(oConfig)) {
            oConfig = [oConfig];
        }


        build(this.root,oConfig);
    },
/**
     * Builds the TreeView from existing markup.   Markup should consist of &lt;UL&gt; or &lt;OL&gt; elements containing &lt;LI&gt; elements.
     * Each &lt;LI&gt; can have one element used as label and a second optional element which is to be a &lt;UL&gt; or &lt;OL&gt;
     * containing nested nodes.
     * Depending on what the first element of the &lt;LI&gt; element is, the following Nodes will be created: <ul>
     *           <li>plain text:  a regular TextNode</li>
     *           <li>anchor &lt;A&gt;: a TextNode with its <code>href</code> and <code>target</code> taken from the anchor</li>
     *           <li>anything else: an HTMLNode</li></ul>
     * Only the first  outermost (un-)ordered list in the markup and its children will be parsed.
     * Nodes will be collapsed unless  an  &lt;LI&gt;  tag has a className called 'expanded'.
     * All other className attributes will be copied over to the Node className property.
     * If the &lt;LI&gt; element contains an attribute called <code>yuiConfig</code>, its contents should be a JSON-encoded object
     * as the one used in method <a href="#method_buildTreeFromObject">buildTreeFromObject</a>.
     * @method buildTreeFromMarkup
     * @param  id {string|HTMLElement} The id of the element that contains the markup or a reference to it.
     */
    buildTreeFromMarkup: function (id) {
        this.logger.log('Building tree from existing markup');
        var build = function (markup) {
            var el, child, branch = [], config = {}, label, yuiConfig;
            // Dom's getFirstChild and getNextSibling skip over text elements
            for (el = Dom.getFirstChild(markup); el; el = Dom.getNextSibling(el)) {
                switch (el.tagName.toUpperCase()) {
                    case 'LI':
                        label = '';
                        config = {
                            expanded: Dom.hasClass(el,'expanded'),
                            title: el.title || el.alt || null,
                            className: Lang.trim(el.className.replace(/\bexpanded\b/,'')) || null
                        };
                        // I cannot skip over text elements here because I want them for labels
                        child = el.firstChild;
                        if (child.nodeType == 3) {
                            // nodes with only whitespace, tabs and new lines don't count, they are probably just formatting.
                            label = Lang.trim(child.nodeValue.replace(/[\n\t\r]*/g,''));
                            if (label) {
                                config.type = 'text';
                                config.label = label;
                            } else {
                                child = Dom.getNextSibling(child);
                            }
                        }
                        if (!label) {
                            if (child.tagName.toUpperCase() == 'A') {
                                config.type = 'text';
                                config.label = child.innerHTML;
                                config.href = child.href;
                                config.target = child.target;
                                config.title = child.title || child.alt || config.title;
                            } else {
                                config.type = 'html';
                                var d = document.createElement('div');
                                d.appendChild(child.cloneNode(true));
                                config.html = d.innerHTML;
                                config.hasIcon = true;
                            }
                        }
                        // see if after the label it has a further list which will become children of this node.
                        child = Dom.getNextSibling(child);
                        switch (child && child.tagName.toUpperCase()) {
                            case 'UL':
                            case 'OL':
                                config.children = build(child);
                                break;
                        }
                        // if there are further elements or text, it will be ignored.

                        if (YAHOO.lang.JSON) {
                            yuiConfig = el.getAttribute('yuiConfig');
                            if (yuiConfig) {
                                yuiConfig = YAHOO.lang.JSON.parse(yuiConfig);
                                config = YAHOO.lang.merge(config,yuiConfig);
                            }
                        }

                        branch.push(config);
                        break;
                    case 'UL':
                    case 'OL':
                        this.logger.log('ULs or OLs can only contain LI elements, not other UL or OL.  This will not work in some browsers','error');
                        config = {
                            type: 'text',
                            label: '',
                            children: build(child)
                        };
                        branch.push(config);
                        break;
                }
            }
            return branch;
        };

        var markup = Dom.getChildrenBy(Dom.get(id),function (el) {
            var tag = el.tagName.toUpperCase();
            return  tag == 'UL' || tag == 'OL';
        });
        if (markup.length) {
            this.buildTreeFromObject(build(markup[0]));
        } else {
            this.logger.log('Markup contains no UL or OL elements','warn');
        }
    },
  /**
     * Returns the TD element where the event has occurred
     * @method _getEventTargetTdEl
     * @private
     */
    _getEventTargetTdEl: function (ev) {
        var target = Event.getTarget(ev);
        // go up looking for a TD with a className with a ygtv prefix
        while (target && !(target.tagName.toUpperCase() == 'TD' && Dom.hasClass(target.parentNode,'ygtvrow'))) {
            target = Dom.getAncestorByTagName(target,'td');
        }
        if (Lang.isNull(target)) { return null; }
        // If it is a spacer cell, do nothing
        if (/\bygtv(blank)?depthcell/.test(target.className)) { return null;}
        // If it has an id, search for the node number and see if it belongs to a node in this tree.
        if (target.id) {
            var m = target.id.match(/\bygtv([^\d]*)(.*)/);
            if (m && m[2] && this._nodes[m[2]]) {
                return target;
            }
        }
        return null;
    },
  /**
     * Event listener for click events
     * @method _onClickEvent
     * @private
     */
    _onClickEvent: function (ev) {
        var self = this,
            td = this._getEventTargetTdEl(ev),
            node,
            target,
            toggle = function (force) {
                node.focus();
                if (force || !node.href) {
                    node.toggle();
                    try {
                        Event.preventDefault(ev);
                    } catch (e) {
                        // @TODO
                        // For some reason IE8 is providing an event object with
                        // most of the fields missing, but only when clicking on
                        // the node's label, and only when working with inline
                        // editing.  This generates a "Member not found" error
                        // in that browser.  Determine if this is a browser
                        // bug, or a problem with this code.  Already checked to
                        // see if the problem has to do with access the event
                        // in the outer scope, and that isn't the problem.
                        // Maybe the markup for inline editing is broken.
                    }
                }
            };

        if (!td) {
            return;
        }

        node = this.getNodeByElement(td);
        if (!node) {
            return;
        }

        // exception to handle deprecated event labelClick
        // @TODO take another look at this deprecation.  It is common for people to
        // only be interested in the label click, so why make them have to test
        // the node type to figure out whether the click was on the label?
        target = Event.getTarget(ev);
        if (Dom.hasClass(target, node.labelStyle) || Dom.getAncestorByClassName(target,node.labelStyle)) {
            this.logger.log("onLabelClick " + node.label);
            this.fireEvent('labelClick',node);
        }
        // http://yuilibrary.com/projects/yui2/ticket/2528946
        // Ensures that any open editor is closed.
        // Since the editor is in a separate source which might not be included,
        // we first need to ensure we have the _closeEditor method available
        if (this._closeEditor) { this._closeEditor(false); }

        //  If it is a toggle cell, toggle
        if (/\bygtv[tl][mp]h?h?/.test(td.className)) {
            toggle(true);
        } else {
            if (this._dblClickTimer) {
                window.clearTimeout(this._dblClickTimer);
                this._dblClickTimer = null;
            } else {
                if (this._hasDblClickSubscriber) {
                    this._dblClickTimer = window.setTimeout(function () {
                        self._dblClickTimer = null;
                        if (self.fireEvent('clickEvent', {event:ev,node:node}) !== false) {
                            toggle();
                        }
                    }, 200);
                } else {
                    if (self.fireEvent('clickEvent', {event:ev,node:node}) !== false) {
                        toggle();
                    }
                }
            }
        }
    },

  /**
     * Event listener for double-click events
     * @method _onDblClickEvent
     * @private
     */
    _onDblClickEvent: function (ev) {
        if (!this._hasDblClickSubscriber) { return; }
        var td = this._getEventTargetTdEl(ev);
        if (!td) {return;}

        if (!(/\bygtv[tl][mp]h?h?/.test(td.className))) {
            this.fireEvent('dblClickEvent', {event:ev, node:this.getNodeByElement(td)});
            if (this._dblClickTimer) {
                window.clearTimeout(this._dblClickTimer);
                this._dblClickTimer = null;
            }
        }
    },
  /**
     * Event listener for mouse over events
     * @method _onMouseOverEvent
     * @private
     */
    _onMouseOverEvent:function (ev) {
        var target;
        if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) {
            target.className = target.className.replace(/\bygtv([lt])([mp])\b/gi,'ygtv$1$2h');
        }
    },
  /**
     * Event listener for mouse out events
     * @method _onMouseOutEvent
     * @private
     */
    _onMouseOutEvent: function (ev) {
        var target;
        if ((target = this._getEventTargetTdEl(ev)) && (target = this.getNodeByElement(target)) && (target = target.getToggleEl())) {
            target.className = target.className.replace(/\bygtv([lt])([mp])h\b/gi,'ygtv$1$2');
        }
    },
  /**
     * Event listener for key down events
     * @method _onKeyDownEvent
     * @private
     */
    _onKeyDownEvent: function (ev) {
        var target = Event.getTarget(ev),
            node = this.getNodeByElement(target),
            newNode = node,
            KEY = YAHOO.util.KeyListener.KEY;

        switch(ev.keyCode) {
            case KEY.UP:
                this.logger.log('UP');
                do {
                    if (newNode.previousSibling) {
                        newNode = newNode.previousSibling;
                    } else {
                        newNode = newNode.parent;
                    }
                } while (newNode && !newNode._canHaveFocus());
                if (newNode) { newNode.focus(); }
                Event.preventDefault(ev);
                break;
            case KEY.DOWN:
                this.logger.log('DOWN');
                do {
                    if (newNode.nextSibling) {
                        newNode = newNode.nextSibling;
                    } else {
                        newNode.expand();
                        newNode = (newNode.children.length || null) && newNode.children[0];
                    }
                } while (newNode && !newNode._canHaveFocus);
                if (newNode) { newNode.focus();}
                Event.preventDefault(ev);
                break;
            case KEY.LEFT:
                this.logger.log('LEFT');
                do {
                    if (newNode.parent) {
                        newNode = newNode.parent;
                    } else {
                        newNode = newNode.previousSibling;
                    }
                } while (newNode && !newNode._canHaveFocus());
                if (newNode) { newNode.focus();}
                Event.preventDefault(ev);
                break;
            case KEY.RIGHT:
                this.logger.log('RIGHT');
                var self = this,
                    moveFocusRight,
                    focusOnExpand = function (newNode) {
                        self.unsubscribe('expandComplete',focusOnExpand);
                        moveFocusRight(newNode);
                    };
                moveFocusRight = function (newNode) {
                    do {
                        if (newNode.isDynamic() && !newNode.childrenRendered) {
                            self.subscribe('expandComplete',focusOnExpand);
                            newNode.expand();
                            newNode = null;
                            break;
                        } else {
                            newNode.expand();
                            if (newNode.children.length) {
                                newNode = newNode.children[0];
                            } else {
                                newNode = newNode.nextSibling;
                            }
                        }
                    } while (newNode && !newNode._canHaveFocus());
                    if (newNode) { newNode.focus();}
                };

                moveFocusRight(newNode);
                Event.preventDefault(ev);
                break;
            case KEY.ENTER:
                this.logger.log('ENTER: ' + newNode.href);
                if (node.href) {
                    if (node.target) {
                        window.open(node.href,node.target);
                    } else {
                        window.location(node.href);
                    }
                } else {
                    node.toggle();
                }
                this.fireEvent('enterKeyPressed',node);
                Event.preventDefault(ev);
                break;
            case KEY.HOME:
                this.logger.log('HOME');
                newNode = this.getRoot();
                if (newNode.children.length) {newNode = newNode.children[0];}
                if (newNode._canHaveFocus()) { newNode.focus(); }
                Event.preventDefault(ev);
                break;
            case KEY.END:
                this.logger.log('END');
                newNode = newNode.parent.children;
                newNode = newNode[newNode.length -1];
                if (newNode._canHaveFocus()) { newNode.focus(); }
                Event.preventDefault(ev);
                break;
            // case KEY.PAGE_UP:
                // this.logger.log('PAGE_UP');
                // break;
            // case KEY.PAGE_DOWN:
                // this.logger.log('PAGE_DOWN');
                // break;
            case 107:  // plus key
            case 187:  // plus key
                if (ev.shiftKey) {
                    this.logger.log('Shift-PLUS');
                    node.parent.expandAll();
                } else {
                    this.logger.log('PLUS');
                    node.expand();
                }
                break;
            case 109: // minus key
            case 189: // minus key
                if (ev.shiftKey) {
                    this.logger.log('Shift-MINUS');
                    node.parent.collapseAll();
                } else {
                    this.logger.log('MINUS');
                    node.collapse();
                }
                break;
            default:
                break;
        }
    },
    /**
     * Renders the tree boilerplate and visible nodes
     * @method render
     */
    render: function() {
        var html = this.root.getHtml(),
            el = this.getEl();
        el.innerHTML = html;
        if (!this._hasEvents) {
            Event.on(el, 'click', this._onClickEvent, this, true);
            Event.on(el, 'dblclick', this._onDblClickEvent, this, true);
            Event.on(el, 'mouseover', this._onMouseOverEvent, this, true);
            Event.on(el, 'mouseout', this._onMouseOutEvent, this, true);
            Event.on(el, 'keydown', this._onKeyDownEvent, this, true);
        }
        this._hasEvents = true;
    },

  /**
     * Returns the tree's host element
     * @method getEl
     * @return {HTMLElement} the host element
     */
    getEl: function() {
        if (! this._el) {
            this._el = Dom.get(this.id);
        }
        return this._el;
    },

    /**
     * Nodes register themselves with the tree instance when they are created.
     * @method regNode
     * @param node {Node} the node to register
     * @private
     */
    regNode: function(node) {
        this._nodes[node.index] = node;
    },

    /**
     * Returns the root node of this tree
     * @method getRoot
     * @return {Node} the root node
     */
    getRoot: function() {
        return this.root;
    },

    /**
     * Configures this tree to dynamically load all child data
     * @method setDynamicLoad
     * @param {function} fnDataLoader the function that will be called to get the data
     * @param iconMode {int} configures the icon that is displayed when a dynamic
     * load node is expanded the first time without children.  By default, the
     * "collapse" icon will be used.  If set to 1, the leaf node icon will be
     * displayed.
     */
    setDynamicLoad: function(fnDataLoader, iconMode) {
        this.root.setDynamicLoad(fnDataLoader, iconMode);
    },

    /**
     * Expands all child nodes.  Note: this conflicts with the "multiExpand"
     * node property.  If expand all is called in a tree with nodes that
     * do not allow multiple siblings to be displayed, only the last sibling
     * will be expanded.
     * @method expandAll
     */
    expandAll: function() {
        if (!this.locked) {
            this.root.expandAll();
        }
    },

    /**
     * Collapses all expanded child nodes in the entire tree.
     * @method collapseAll
     */
    collapseAll: function() {
        if (!this.locked) {
            this.root.collapseAll();
        }
    },

    /**
     * Returns a node in the tree that has the specified index (this index
     * is created internally, so this function probably will only be used
     * in html generated for a given node.)
     * @method getNodeByIndex
     * @param {int} nodeIndex the index of the node wanted
     * @return {Node} the node with index=nodeIndex, null if no match
     */
    getNodeByIndex: function(nodeIndex) {
        var n = this._nodes[nodeIndex];
        return (n) ? n : null;
    },

    /**
     * Returns a node that has a matching property and value in the data
     * object that was passed into its constructor.
     * @method getNodeByProperty
     * @param {object} property the property to search (usually a string)
     * @param {object} value the value we want to find (usuall an int or string)
     * @return {Node} the matching node, null if no match
     */
    getNodeByProperty: function(property, value) {
        for (var i in this._nodes) {
            if (this._nodes.hasOwnProperty(i)) {
                var n = this._nodes[i];
                if ((property in n && n[property] == value) || (n.data && value == n.data[property])) {
                    return n;
                }
            }
        }

        return null;
    },

    /**
     * Returns a collection of nodes that have a matching property
     * and value in the data object that was passed into its constructor.
     * @method getNodesByProperty
     * @param {object} property the property to search (usually a string)
     * @param {object} value the value we want to find (usuall an int or string)
     * @return {Array} the matching collection of nodes, null if no match
     */
    getNodesByProperty: function(property, value) {
        var values = [];
        for (var i in this._nodes) {
            if (this._nodes.hasOwnProperty(i)) {
                var n = this._nodes[i];
                if ((property in n && n[property] == value) || (n.data && value == n.data[property])) {
                    values.push(n);
                }
            }
        }

        return (values.length) ? values : null;
    },


    /**
     * Returns a collection of nodes that have passed the test function
     * passed as its only argument.
     * The function will receive a reference to each node to be tested.
     * @method getNodesBy
     * @param {function} a boolean function that receives a Node instance and returns true to add the node to the results list
     * @return {Array} the matching collection of nodes, null if no match
     */
    getNodesBy: function(fn) {
        var values = [];
        for (var i in this._nodes) {
            if (this._nodes.hasOwnProperty(i)) {
                var n = this._nodes[i];
                if (fn(n)) {
                    values.push(n);
                }
            }
        }
        return (values.length) ? values : null;
    },
    /**
     * Returns the treeview node reference for an ancestor element
     * of the node, or null if it is not contained within any node
     * in this tree.
     * @method getNodeByElement
     * @param el {HTMLElement} the element to test
     * @return {YAHOO.widget.Node} a node reference or null
     */
    getNodeByElement: function(el) {

        var p=el, m, re=/ygtv([^\d]*)(.*)/;

        do {

            if (p && p.id) {
                m = p.id.match(re);
                if (m && m[2]) {
                    return this.getNodeByIndex(m[2]);
                }
            }

            p = p.parentNode;

            if (!p || !p.tagName) {
                break;
            }

        }
        while (p.id !== this.id && p.tagName.toLowerCase() !== "body");

        return null;
    },

    /**
     * When in singleNodeHighlight it returns the node highlighted
     * or null if none.  Returns null if singleNodeHighlight is false.
     * @method getHighlightedNode
     * @return {YAHOO.widget.Node} a node reference or null
     */
    getHighlightedNode: function() {
        return this._currentlyHighlighted;
    },


    /**
     * Removes the node and its children, and optionally refreshes the
     * branch of the tree that was affected.
     * @method removeNode
     * @param {Node} node to remove
     * @param {boolean} autoRefresh automatically refreshes branch if true
     * @return {boolean} False is there was a problem, true otherwise.
     */
    removeNode: function(node, autoRefresh) {

        // Don't delete the root node
        if (node.isRoot()) {
            return false;
        }

        // Get the branch that we may need to refresh
        var p = node.parent;
        if (p.parent) {
            p = p.parent;
        }

        // Delete the node and its children
        this._deleteNode(node);

        // Refresh the parent of the parent
        if (autoRefresh && p && p.childrenRendered) {
            p.refresh();
        }

        return true;
    },

    /**
     * wait until the animation is complete before deleting
     * to avoid javascript errors
     * @method _removeChildren_animComplete
     * @param o the custom event payload
     * @private
     */
    _removeChildren_animComplete: function(o) {
        this.unsubscribe(this._removeChildren_animComplete);
        this.removeChildren(o.node);
    },

    /**
     * Deletes this nodes child collection, recursively.  Also collapses
     * the node, and resets the dynamic load flag.  The primary use for
     * this method is to purge a node and allow it to fetch its data
     * dynamically again.
     * @method removeChildren
     * @param {Node} node the node to purge
     */
    removeChildren: function(node) {

        if (node.expanded) {
            // wait until the animation is complete before deleting to
            // avoid javascript errors
            if (this._collapseAnim) {
                this.subscribe("animComplete",
                        this._removeChildren_animComplete, this, true);
                Widget.Node.prototype.collapse.call(node);
                return;
            }

            node.collapse();
        }

        this.logger.log("Removing children for " + node);
        while (node.children.length) {
            this._deleteNode(node.children[0]);
        }

        if (node.isRoot()) {
            Widget.Node.prototype.expand.call(node);
        }

        node.childrenRendered = false;
        node.dynamicLoadComplete = false;

        node.updateIcon();
    },

    /**
     * Deletes the node and recurses children
     * @method _deleteNode
     * @private
     */
    _deleteNode: function(node) {
        // Remove all the child nodes first
        this.removeChildren(node);

        // Remove the node from the tree
        this.popNode(node);
    },

    /**
     * Removes the node from the tree, preserving the child collection
     * to make it possible to insert the branch into another part of the
     * tree, or another tree.
     * @method popNode
     * @param {Node} node to remove
     */
    popNode: function(node) {
        var p = node.parent;

        // Update the parent's collection of children
        var a = [];

        for (var i=0, len=p.children.length;i<len;++i) {
            if (p.children[i] != node) {
                a[a.length] = p.children[i];
            }
        }

        p.children = a;

        // reset the childrenRendered flag for the parent
        p.childrenRendered = false;

         // Update the sibling relationship
        if (node.previousSibling) {
            node.previousSibling.nextSibling = node.nextSibling;
        }

        if (node.nextSibling) {
            node.nextSibling.previousSibling = node.previousSibling;
        }

        if (this.currentFocus == node) {
            this.currentFocus = null;
        }
        if (this._currentlyHighlighted == node) {
            this._currentlyHighlighted = null;
        }

        node.parent = null;
        node.previousSibling = null;
        node.nextSibling = null;
        node.tree = null;

        // Update the tree's node collection
        delete this._nodes[node.index];
    },

    /**
    * Nulls out the entire TreeView instance and related objects, removes attached
    * event listeners, and clears out DOM elements inside the container. After
    * calling this method, the instance reference should be expliclitly nulled by
    * implementer, as in myDataTable = null. Use with caution!
    *
    * @method destroy
    */
    destroy : function() {
        // Since the label editor can be separated from the main TreeView control
        // the destroy method for it might not be there.
        if (this._destroyEditor) { this._destroyEditor(); }
        var el = this.getEl();
        Event.removeListener(el,'click');
        Event.removeListener(el,'dblclick');
        Event.removeListener(el,'mouseover');
        Event.removeListener(el,'mouseout');
        Event.removeListener(el,'keydown');
        for (var i = 0 ; i < this._nodes.length; i++) {
            var node = this._nodes[i];
            if (node && node.destroy) {node.destroy(); }
        }
        el.innerHTML = '';
        this._hasEvents = false;
    },




    /**
     * TreeView instance toString
     * @method toString
     * @return {string} string representation of the tree
     */
    toString: function() {
        return "TreeView " + this.id;
    },

    /**
     * Count of nodes in tree
     * @method getNodeCount
     * @return {int} number of nodes in the tree
     */
    getNodeCount: function() {
        return this.getRoot().getNodeCount();
    },

    /**
     * Returns an object which could be used to rebuild the tree.
     * It can be passed to the tree constructor to reproduce the same tree.
     * It will return false if any node loads dynamically, regardless of whether it is loaded or not.
     * @method getTreeDefinition
     * @return {Object | false}  definition of the tree or false if any node is defined as dynamic
     */
    getTreeDefinition: function() {
        return this.getRoot().getNodeDefinition();
    },

    /**
     * Abstract method that is executed when a node is expanded
     * @method onExpand
     * @param node {Node} the node that was expanded
     * @deprecated use treeobj.subscribe("expand") instead
     */
    onExpand: function(node) { },

    /**
     * Abstract method that is executed when a node is collapsed.
     * @method onCollapse
     * @param node {Node} the node that was collapsed.
     * @deprecated use treeobj.subscribe("collapse") instead
     */
    onCollapse: function(node) { },

    /**
    * Sets the value of a property for all loaded nodes in the tree.
    * @method setNodesProperty
    * @param name {string} Name of the property to be set
    * @param value {any} value to be set
    * @param refresh {boolean} if present and true, it does a refresh
    */
    setNodesProperty: function(name, value, refresh) {
        this.root.setNodesProperty(name,value);
        if (refresh) {
            this.root.refresh();
        }
    },
    /**
    * Event listener to toggle node highlight.
    * Can be assigned as listener to clickEvent, dblClickEvent and enterKeyPressed.
    * It returns false to prevent the default action.
    * @method onEventToggleHighlight
    * @param oArgs {any} it takes the arguments of any of the events mentioned above
    * @return {false} Always cancels the default action for the event
    */
    onEventToggleHighlight: function (oArgs) {
        var node;
        if ('node' in oArgs && oArgs.node instanceof Widget.Node) {
            node = oArgs.node;
        } else if (oArgs instanceof Widget.Node) {
            node = oArgs;
        } else {
            return false;
        }
        node.toggleHighlight();
        return false;
    }


};

/* Backwards compatibility aliases */
var PROT = TV.prototype;
 /**
     * Renders the tree boilerplate and visible nodes.
     *  Alias for render
     * @method draw
     * @deprecated Use render instead
     */
PROT.draw = PROT.render;

/* end backwards compatibility aliases */

YAHOO.augment(TV, YAHOO.util.EventProvider);

/**
 * Running count of all nodes created in all trees.  This is
 * used to provide unique identifies for all nodes.  Deleting
 * nodes does not change the nodeCount.
 * @property YAHOO.widget.TreeView.nodeCount
 * @type int
 * @static
 */
TV.nodeCount = 0;

/**
 * Global cache of tree instances
 * @property YAHOO.widget.TreeView.trees
 * @type Array
 * @static
 * @private
 */
TV.trees = [];

/**
 * Global method for getting a tree by its id.  Used in the generated
 * tree html.
 * @method YAHOO.widget.TreeView.getTree
 * @param treeId {String} the id of the tree instance
 * @return {TreeView} the tree instance requested, null if not found.
 * @static
 */
TV.getTree = function(treeId) {
    var t = TV.trees[treeId];
    return (t) ? t : null;
};


/**
 * Global method for getting a node by its id.  Used in the generated
 * tree html.
 * @method YAHOO.widget.TreeView.getNode
 * @param treeId {String} the id of the tree instance
 * @param nodeIndex {String} the index of the node to return
 * @return {Node} the node instance requested, null if not found
 * @static
 */
TV.getNode = function(treeId, nodeIndex) {
    var t = TV.getTree(treeId);
    return (t) ? t.getNodeByIndex(nodeIndex) : null;
};


/**
     * Class name assigned to elements that have the focus
     *
     * @property TreeView.FOCUS_CLASS_NAME
     * @type String
     * @static
     * @final
     * @default "ygtvfocus"

    */
TV.FOCUS_CLASS_NAME = 'ygtvfocus';



})();
(function () {
    var Dom = YAHOO.util.Dom,
        Lang = YAHOO.lang,
        Event = YAHOO.util.Event;
/**
 * The base class for all tree nodes.  The node's presentation and behavior in
 * response to mouse events is handled in Node subclasses.
 * @namespace YAHOO.widget
 * @class Node
 * @uses YAHOO.util.EventProvider
 * @param oData {object} a string or object containing the data that will
 * be used to render this node, and any custom attributes that should be
 * stored with the node (which is available in noderef.data).
 * All values in oData will be used to set equally named properties in the node
 * as long as the node does have such properties, they are not undefined, private or functions,
 * the rest of the values will be stored in noderef.data
 * @param oParent {Node} this node's parent node
 * @param expanded {boolean} the initial expanded/collapsed state (deprecated, use oData.expanded)
 * @constructor
 */
YAHOO.widget.Node = function(oData, oParent, expanded) {
    if (oData) { this.init(oData, oParent, expanded); }
};

YAHOO.widget.Node.prototype = {

    /**
     * The index for this instance obtained from global counter in YAHOO.widget.TreeView.
     * @property index
     * @type int
     */
    index: 0,

    /**
     * This node's child node collection.
     * @property children
     * @type Node[]
     */
    children: null,

    /**
     * Tree instance this node is part of
     * @property tree
     * @type TreeView
     */
    tree: null,

    /**
     * The data linked to this node.  This can be any object or primitive
     * value, and the data can be used in getNodeHtml().
     * @property data
     * @type object
     */
    data: null,

    /**
     * Parent node
     * @property parent
     * @type Node
     */
    parent: null,

    /**
     * The depth of this node.  We start at -1 for the root node.
     * @property depth
     * @type int
     */
    depth: -1,

    /**
     * The node's expanded/collapsed state
     * @property expanded
     * @type boolean
     */
    expanded: false,

    /**
     * Can multiple children be expanded at once?
     * @property multiExpand
     * @type boolean
     */
    multiExpand: true,

    /**
     * Should we render children for a collapsed node?  It is possible that the
     * implementer will want to render the hidden data...  @todo verify that we
     * need this, and implement it if we do.
     * @property renderHidden
     * @type boolean
     */
    renderHidden: false,

    /**
     * This flag is set to true when the html is generated for this node's
     * children, and set to false when new children are added.
     * @property childrenRendered
     * @type boolean
     */
    childrenRendered: false,

    /**
     * Dynamically loaded nodes only fetch the data the first time they are
     * expanded.  This flag is set to true once the data has been fetched.
     * @property dynamicLoadComplete
     * @type boolean
     */
    dynamicLoadComplete: false,

    /**
     * This node's previous sibling
     * @property previousSibling
     * @type Node
     */
    previousSibling: null,

    /**
     * This node's next sibling
     * @property nextSibling
     * @type Node
     */
    nextSibling: null,

    /**
     * We can set the node up to call an external method to get the child
     * data dynamically.
     * @property _dynLoad
     * @type boolean
     * @private
     */
    _dynLoad: false,

    /**
     * Function to execute when we need to get this node's child data.
     * @property dataLoader
     * @type function
     */
    dataLoader: null,

    /**
     * This is true for dynamically loading nodes while waiting for the
     * callback to return.
     * @property isLoading
     * @type boolean
     */
    isLoading: false,

    /**
     * The toggle/branch icon will not show if this is set to false.  This
     * could be useful if the implementer wants to have the child contain
     * extra info about the parent, rather than an actual node.
     * @property hasIcon
     * @type boolean
     */
    hasIcon: true,

    /**
     * Used to configure what happens when a dynamic load node is expanded
     * and we discover that it does not have children.  By default, it is
     * treated as if it still could have children (plus/minus icon).  Set
     * iconMode to have it display like a leaf node instead.
     * @property iconMode
     * @type int
     */
    iconMode: 0,

    /**
     * Specifies whether or not the content area of the node should be allowed
     * to wrap.
     * @property nowrap
     * @type boolean
     * @default false
     */
    nowrap: false,

 /**
     * If true, the node will alway be rendered as a leaf node.  This can be
     * used to override the presentation when dynamically loading the entire
     * tree.  Setting this to true also disables the dynamic load call for the
     * node.
     * @property isLeaf
     * @type boolean
     * @default false
     */
    isLeaf: false,

/**
     * The CSS class for the html content container.  Defaults to ygtvhtml, but
     * can be overridden to provide a custom presentation for a specific node.
     * @property contentStyle
     * @type string
     */
    contentStyle: "",


    /**
     * The generated id that will contain the data passed in by the implementer.
     * @property contentElId
     * @type string
     */
    contentElId: null,

/**
 * Enables node highlighting.  If true, the node can be highlighted and/or propagate highlighting
 * @property enableHighlight
 * @type boolean
 * @default true
 */
    enableHighlight: true,

/**
 * Stores the highlight state.  Can be any of:
 * <ul>
 * <li>0 - not highlighted</li>
 * <li>1 - highlighted</li>
 * <li>2 - some children highlighted</li>
 * </ul>
 * @property highlightState
 * @type integer
 * @default 0
 */

 highlightState: 0,

 /**
 * Tells whether highlighting will be propagated up to the parents of the clicked node
 * @property propagateHighlightUp
 * @type boolean
 * @default false
 */

 propagateHighlightUp: false,

 /**
 * Tells whether highlighting will be propagated down to the children of the clicked node
 * @property propagateHighlightDown
 * @type boolean
 * @default false
 */

 propagateHighlightDown: false,

 /**
  * User-defined className to be added to the Node
  * @property className
  * @type string
  * @default null
  */

 className: null,

 /**
     * The node type
     * @property _type
     * @private
     * @type string
     * @default "Node"
*/
    _type: "Node",

    /*
    spacerPath: "http://l.yimg.com/a/i/space.gif",
    expandedText: "Expanded",
    collapsedText: "Collapsed",
    loadingText: "Loading",
    */

    /**
     * Initializes this node, gets some of the properties from the parent
     * @method init
     * @param oData {object} a string or object containing the data that will
     * be used to render this node
     * @param oParent {Node} this node's parent node
     * @param expanded {boolean} the initial expanded/collapsed state
     */
    init: function(oData, oParent, expanded) {

        this.data = {};
        this.children   = [];
        this.index      = YAHOO.widget.TreeView.nodeCount;
        ++YAHOO.widget.TreeView.nodeCount;
        this.contentElId = "ygtvcontentel" + this.index;

        if (Lang.isObject(oData)) {
            for (var property in oData) {
                if (oData.hasOwnProperty(property)) {
                    if (property.charAt(0) != '_'  && !Lang.isUndefined(this[property]) && !Lang.isFunction(this[property]) ) {
                        this[property] = oData[property];
                    } else {
                        this.data[property] = oData[property];
                    }
                }
            }
        }
        if (!Lang.isUndefined(expanded) ) { this.expanded  = expanded;  }

        this.logger     = new YAHOO.widget.LogWriter(this.toString());

        /**
         * The parentChange event is fired when a parent element is applied
         * to the node.  This is useful if you need to apply tree-level
         * properties to a tree that need to happen if a node is moved from
         * one tree to another.
         *
         * @event parentChange
         * @type CustomEvent
         */
        this.createEvent("parentChange", this);

        // oParent should never be null except when we create the root node.
        if (oParent) {
            oParent.appendChild(this);
        }
    },

    /**
     * Certain properties for the node cannot be set until the parent
     * is known. This is called after the node is inserted into a tree.
     * the parent is also applied to this node's children in order to
     * make it possible to move a branch from one tree to another.
     * @method applyParent
     * @param {Node} parentNode this node's parent node
     * @return {boolean} true if the application was successful
     */
    applyParent: function(parentNode) {
        if (!parentNode) {
            return false;
        }

        this.tree   = parentNode.tree;
        this.parent = parentNode;
        this.depth  = parentNode.depth + 1;

        // @todo why was this put here.  This causes new nodes added at the
        // root level to lose the menu behavior.
        // if (! this.multiExpand) {
            // this.multiExpand = parentNode.multiExpand;
        // }

        this.tree.regNode(this);
        parentNode.childrenRendered = false;

        // cascade update existing children
        for (var i=0, len=this.children.length;i<len;++i) {
            this.children[i].applyParent(this);
        }

        this.fireEvent("parentChange");

        return true;
    },

    /**
     * Appends a node to the child collection.
     * @method appendChild
     * @param childNode {Node} the new node
     * @return {Node} the child node
     * @private
     */
    appendChild: function(childNode) {
        if (this.hasChildren()) {
            var sib = this.children[this.children.length - 1];
            sib.nextSibling = childNode;
            childNode.previousSibling = sib;
        }
        this.children[this.children.length] = childNode;
        childNode.applyParent(this);

        // part of the IE display issue workaround. If child nodes
        // are added after the initial render, and the node was
        // instantiated with expanded = true, we need to show the
        // children div now that the node has a child.
        if (this.childrenRendered && this.expanded) {
            this.getChildrenEl().style.display = "";
        }

        return childNode;
    },

    /**
     * Appends this node to the supplied node's child collection
     * @method appendTo
     * @param parentNode {Node} the node to append to.
     * @return {Node} The appended node
     */
    appendTo: function(parentNode) {
        return parentNode.appendChild(this);
    },

    /**
    * Inserts this node before this supplied node
    * @method insertBefore
    * @param node {Node} the node to insert this node before
    * @return {Node} the inserted node
    */
    insertBefore: function(node) {
        this.logger.log("insertBefore: " + node);
        var p = node.parent;
        if (p) {

            if (this.tree) {
                this.tree.popNode(this);
            }

            var refIndex = node.isChildOf(p);
            //this.logger.log(refIndex);
            p.children.splice(refIndex, 0, this);
            if (node.previousSibling) {
                node.previousSibling.nextSibling = this;
            }
            this.previousSibling = node.previousSibling;
            this.nextSibling = node;
            node.previousSibling = this;

            this.applyParent(p);
        }

        return this;
    },

    /**
    * Inserts this node after the supplied node
    * @method insertAfter
    * @param node {Node} the node to insert after
    * @return {Node} the inserted node
    */
    insertAfter: function(node) {
        this.logger.log("insertAfter: " + node);
        var p = node.parent;
        if (p) {

            if (this.tree) {
                this.tree.popNode(this);
            }

            var refIndex = node.isChildOf(p);
            this.logger.log(refIndex);

            if (!node.nextSibling) {
                this.nextSibling = null;
                return this.appendTo(p);
            }

            p.children.splice(refIndex + 1, 0, this);

            node.nextSibling.previousSibling = this;
            this.previousSibling = node;
            this.nextSibling = node.nextSibling;
            node.nextSibling = this;

            this.applyParent(p);
        }

        return this;
    },

    /**
    * Returns true if the Node is a child of supplied Node
    * @method isChildOf
    * @param parentNode {Node} the Node to check
    * @return {boolean} The node index if this Node is a child of
    *                   supplied Node, else -1.
    * @private
    */
    isChildOf: function(parentNode) {
        if (parentNode && parentNode.children) {
            for (var i=0, len=parentNode.children.length; i<len ; ++i) {
                if (parentNode.children[i] === this) {
                    return i;
                }
            }
        }

        return -1;
    },

    /**
     * Returns a node array of this node's siblings, null if none.
     * @method getSiblings
     * @return Node[]
     */
    getSiblings: function() {
        var sib =  this.parent.children.slice(0);
        for (var i=0;i < sib.length && sib[i] != this;i++) {}
        sib.splice(i,1);
        if (sib.length) { return sib; }
        return null;
    },

    /**
     * Shows this node's children
     * @method showChildren
     */
    showChildren: function() {
        if (!this.tree.animateExpand(this.getChildrenEl(), this)) {
            if (this.hasChildren()) {
                this.getChildrenEl().style.display = "";
            }
        }
    },

    /**
     * Hides this node's children
     * @method hideChildren
     */
    hideChildren: function() {
        this.logger.log("hiding " + this.index);

        if (!this.tree.animateCollapse(this.getChildrenEl(), this)) {
            this.getChildrenEl().style.display = "none";
        }
    },

    /**
     * Returns the id for this node's container div
     * @method getElId
     * @return {string} the element id
     */
    getElId: function() {
        return "ygtv" + this.index;
    },

    /**
     * Returns the id for this node's children div
     * @method getChildrenElId
     * @return {string} the element id for this node's children div
     */
    getChildrenElId: function() {
        return "ygtvc" + this.index;
    },

    /**
     * Returns the id for this node's toggle element
     * @method getToggleElId
     * @return {string} the toggel element id
     */
    getToggleElId: function() {
        return "ygtvt" + this.index;
    },


    /*
     * Returns the id for this node's spacer image.  The spacer is positioned
     * over the toggle and provides feedback for screen readers.
     * @method getSpacerId
     * @return {string} the id for the spacer image
     */
    /*
    getSpacerId: function() {
        return "ygtvspacer" + this.index;
    },
    */

    /**
     * Returns this node's container html element
     * @method getEl
     * @return {HTMLElement} the container html element
     */
    getEl: function() {
        return Dom.get(this.getElId());
    },

    /**
     * Returns the div that was generated for this node's children
     * @method getChildrenEl
     * @return {HTMLElement} this node's children div
     */
    getChildrenEl: function() {
        return Dom.get(this.getChildrenElId());
    },

    /**
     * Returns the element that is being used for this node's toggle.
     * @method getToggleEl
     * @return {HTMLElement} this node's toggle html element
     */
    getToggleEl: function() {
        return Dom.get(this.getToggleElId());
    },
    /**
    * Returns the outer html element for this node's content
    * @method getContentEl
    * @return {HTMLElement} the element
    */
    getContentEl: function() {
        return Dom.get(this.contentElId);
    },


    /*
     * Returns the element that is being used for this node's spacer.
     * @method getSpacer
     * @return {HTMLElement} this node's spacer html element
     */
    /*
    getSpacer: function() {
        return document.getElementById( this.getSpacerId() ) || {};
    },
    */

    /*
    getStateText: function() {
        if (this.isLoading) {
            return this.loadingText;
        } else if (this.hasChildren(true)) {
            if (this.expanded) {
                return this.expandedText;
            } else {
                return this.collapsedText;
            }
        } else {
            return "";
        }
    },
    */

  /**
     * Hides this nodes children (creating them if necessary), changes the toggle style.
     * @method collapse
     */
    collapse: function() {
        // Only collapse if currently expanded
        if (!this.expanded) { return; }

        // fire the collapse event handler
        var ret = this.tree.onCollapse(this);

        if (false === ret) {
            this.logger.log("Collapse was stopped by the abstract onCollapse");
            return;
        }

        ret = this.tree.fireEvent("collapse", this);

        if (false === ret) {
            this.logger.log("Collapse was stopped by a custom event handler");
            return;
        }


        if (!this.getEl()) {
            this.expanded = false;
        } else {
            // hide the child div
            this.hideChildren();
            this.expanded = false;

            this.updateIcon();
        }

        // this.getSpacer().title = this.getStateText();

        ret = this.tree.fireEvent("collapseComplete", this);

    },

    /**
     * Shows this nodes children (creating them if necessary), changes the
     * toggle style, and collapses its siblings if multiExpand is not set.
     * @method expand
     */
    expand: function(lazySource) {
        // Only expand if currently collapsed.
        if (this.isLoading || (this.expanded && !lazySource)) {
            return;
        }

        var ret = true;

        // When returning from the lazy load handler, expand is called again
        // in order to render the new children.  The "expand" event already
        // fired before fething the new data, so we need to skip it now.
        if (!lazySource) {
            // fire the expand event handler
            ret = this.tree.onExpand(this);

            if (false === ret) {
                this.logger.log("Expand was stopped by the abstract onExpand");
                return;
            }

            ret = this.tree.fireEvent("expand", this);
        }

        if (false === ret) {
            this.logger.log("Expand was stopped by the custom event handler");
            return;
        }

        if (!this.getEl()) {
            this.expanded = true;
            return;
        }

        if (!this.childrenRendered) {
            this.logger.log("children not rendered yet");
            this.getChildrenEl().innerHTML = this.renderChildren();
        } else {
            this.logger.log("children already rendered");
        }

        this.expanded = true;

        this.updateIcon();

        // this.getSpacer().title = this.getStateText();

        // We do an extra check for children here because the lazy
        // load feature can expose nodes that have no children.

        // if (!this.hasChildren()) {
        if (this.isLoading) {
            this.expanded = false;
            return;
        }

        if (! this.multiExpand) {
            var sibs = this.getSiblings();
            for (var i=0; sibs && i<sibs.length; ++i) {
                if (sibs[i] != this && sibs[i].expanded) {
                    sibs[i].collapse();
                }
            }
        }

        this.showChildren();

        ret = this.tree.fireEvent("expandComplete", this);
    },

    updateIcon: function() {
        if (this.hasIcon) {
            var el = this.getToggleEl();
            if (el) {
                el.className = el.className.replace(/\bygtv(([tl][pmn]h?)|(loading))\b/gi,this.getStyle());
            }
        }
        el = Dom.get('ygtvtableel' + this.index);
        if (el) {
            if (this.expanded) {
                Dom.replaceClass(el,'ygtv-collapsed','ygtv-expanded');
            } else {
                Dom.replaceClass(el,'ygtv-expanded','ygtv-collapsed');
            }
        }
    },

    /**
     * Returns the css style name for the toggle
     * @method getStyle
     * @return {string} the css class for this node's toggle
     */
    getStyle: function() {
        // this.logger.log("No children, " + " isDyanmic: " + this.isDynamic() + " expanded: " + this.expanded);
        if (this.isLoading) {
            this.logger.log("returning the loading icon");
            return "ygtvloading";
        } else {
            // location top or bottom, middle nodes also get the top style
            var loc = (this.nextSibling) ? "t" : "l";

            // type p=plus(expand), m=minus(collapase), n=none(no children)
            var type = "n";
            if (this.hasChildren(true) || (this.isDynamic() && !this.getIconMode())) {
            // if (this.hasChildren(true)) {
                type = (this.expanded) ? "m" : "p";
            }

            // this.logger.log("ygtv" + loc + type);
            return "ygtv" + loc + type;
        }
    },

    /**
     * Returns the hover style for the icon
     * @return {string} the css class hover state
     * @method getHoverStyle
     */
    getHoverStyle: function() {
        var s = this.getStyle();
        if (this.hasChildren(true) && !this.isLoading) {
            s += "h";
        }
        return s;
    },

    /**
     * Recursively expands all of this node's children.
     * @method expandAll
     */
    expandAll: function() {
        var l = this.children.length;
        for (var i=0;i<l;++i) {
            var c = this.children[i];
            if (c.isDynamic()) {
                this.logger.log("Not supported (lazy load + expand all)");
                break;
            } else if (! c.multiExpand) {
                this.logger.log("Not supported (no multi-expand + expand all)");
                break;
            } else {
                c.expand();
                c.expandAll();
            }
        }
    },

    /**
     * Recursively collapses all of this node's children.
     * @method collapseAll
     */
    collapseAll: function() {
        for (var i=0;i<this.children.length;++i) {
            this.children[i].collapse();
            this.children[i].collapseAll();
        }
    },

    /**
     * Configures this node for dynamically obtaining the child data
     * when the node is first expanded.  Calling it without the callback
     * will turn off dynamic load for the node.
     * @method setDynamicLoad
     * @param fmDataLoader {function} the function that will be used to get the data.
     * @param iconMode {int} configures the icon that is displayed when a dynamic
     * load node is expanded the first time without children.  By default, the
     * "collapse" icon will be used.  If set to 1, the leaf node icon will be
     * displayed.
     */
    setDynamicLoad: function(fnDataLoader, iconMode) {
        if (fnDataLoader) {
            this.dataLoader = fnDataLoader;
            this._dynLoad = true;
        } else {
            this.dataLoader = null;
            this._dynLoad = false;
        }

        if (iconMode) {
            this.iconMode = iconMode;
        }
    },

    /**
     * Evaluates if this node is the root node of the tree
     * @method isRoot
     * @return {boolean} true if this is the root node
     */
    isRoot: function() {
        return (this == this.tree.root);
    },

    /**
     * Evaluates if this node's children should be loaded dynamically.  Looks for
     * the property both in this instance and the root node.  If the tree is
     * defined to load all children dynamically, the data callback function is
     * defined in the root node
     * @method isDynamic
     * @return {boolean} true if this node's children are to be loaded dynamically
     */
    isDynamic: function() {
        if (this.isLeaf) {
            return false;
        } else {
            return (!this.isRoot() && (this._dynLoad || this.tree.root._dynLoad));
            // this.logger.log("isDynamic: " + lazy);
            // return lazy;
        }
    },

    /**
     * Returns the current icon mode.  This refers to the way childless dynamic
     * load nodes appear (this comes into play only after the initial dynamic
     * load request produced no children).
     * @method getIconMode
     * @return {int} 0 for collapse style, 1 for leaf node style
     */
    getIconMode: function() {
        return (this.iconMode || this.tree.root.iconMode);
    },

    /**
     * Checks if this node has children.  If this node is lazy-loading and the
     * children have not been rendered, we do not know whether or not there
     * are actual children.  In most cases, we need to assume that there are
     * children (for instance, the toggle needs to show the expandable
     * presentation state).  In other times we want to know if there are rendered
     * children.  For the latter, "checkForLazyLoad" should be false.
     * @method hasChildren
     * @param checkForLazyLoad {boolean} should we check for unloaded children?
     * @return {boolean} true if this has children or if it might and we are
     * checking for this condition.
     */
    hasChildren: function(checkForLazyLoad) {
        if (this.isLeaf) {
            return false;
        } else {
            return ( this.children.length > 0 ||
                (checkForLazyLoad && this.isDynamic() && !this.dynamicLoadComplete)
            );
        }
    },

    /**
     * Expands if node is collapsed, collapses otherwise.
     * @method toggle
     */
    toggle: function() {
        if (!this.tree.locked && ( this.hasChildren(true) || this.isDynamic()) ) {
            if (this.expanded) { this.collapse(); } else { this.expand(); }
        }
    },

    /**
     * Returns the markup for this node and its children.
     * @method getHtml
     * @return {string} the markup for this node and its expanded children.
     */
    getHtml: function() {

        this.childrenRendered = false;

        return ['<div class="ygtvitem" id="' , this.getElId() , '">' ,this.getNodeHtml() , this.getChildrenHtml() ,'</div>'].join("");
    },

    /**
     * Called when first rendering the tree.  We always build the div that will
     * contain this nodes children, but we don't render the children themselves
     * unless this node is expanded.
     * @method getChildrenHtml
     * @return {string} the children container div html and any expanded children
     * @private
     */
    getChildrenHtml: function() {


        var sb = [];
        sb[sb.length] = '<div class="ygtvchildren" id="' + this.getChildrenElId() + '"';

        // This is a workaround for an IE rendering issue, the child div has layout
        // in IE, creating extra space if a leaf node is created with the expanded
        // property set to true.
        if (!this.expanded || !this.hasChildren()) {
            sb[sb.length] = ' style="display:none;"';
        }
        sb[sb.length] = '>';

        // this.logger.log(["index", this.index,
                         // "hasChildren", this.hasChildren(true),
                         // "expanded", this.expanded,
                         // "renderHidden", this.renderHidden,
                         // "isDynamic", this.isDynamic()]);

        // Don't render the actual child node HTML unless this node is expanded.
        if ( (this.hasChildren(true) && this.expanded) ||
                (this.renderHidden && !this.isDynamic()) ) {
            sb[sb.length] = this.renderChildren();
        }

        sb[sb.length] = '</div>';

        return sb.join("");
    },

    /**
     * Generates the markup for the child nodes.  This is not done until the node
     * is expanded.
     * @method renderChildren
     * @return {string} the html for this node's children
     * @private
     */
    renderChildren: function() {

        this.logger.log("rendering children for " + this.index);

        var node = this;

        if (this.isDynamic() && !this.dynamicLoadComplete) {
            this.isLoading = true;
            this.tree.locked = true;

            if (this.dataLoader) {
                this.logger.log("Using dynamic loader defined for this node");

                setTimeout(
                    function() {
                        node.dataLoader(node,
                            function() {
                                node.loadComplete();
                            });
                    }, 10);

            } else if (this.tree.root.dataLoader) {
                this.logger.log("Using the tree-level dynamic loader");

                setTimeout(
                    function() {
                        node.tree.root.dataLoader(node,
                            function() {
                                node.loadComplete();
                            });
                    }, 10);

            } else {
                this.logger.log("no loader found");
                return "Error: data loader not found or not specified.";
            }

            return "";

        } else {
            return this.completeRender();
        }
    },

    /**
     * Called when we know we have all the child data.
     * @method completeRender
     * @return {string} children html
     */
    completeRender: function() {
        this.logger.log("completeRender: " + this.index + ", # of children: " + this.children.length);
        var sb = [];

        for (var i=0; i < this.children.length; ++i) {
            // this.children[i].childrenRendered = false;
            sb[sb.length] = this.children[i].getHtml();
        }

        this.childrenRendered = true;

        return sb.join("");
    },

    /**
     * Load complete is the callback function we pass to the data provider
     * in dynamic load situations.
     * @method loadComplete
     */
    loadComplete: function() {
        this.logger.log(this.index + " loadComplete, children: " + this.children.length);
        this.getChildrenEl().innerHTML = this.completeRender();
        if (this.propagateHighlightDown) {
            if (this.highlightState === 1 && !this.tree.singleNodeHighlight) {
                for (var i = 0; i < this.children.length; i++) {
                this.children[i].highlight(true);
            }
            } else if (this.highlightState === 0 || this.tree.singleNodeHighlight) {
                for (i = 0; i < this.children.length; i++) {
                    this.children[i].unhighlight(true);
                }
            } // if (highlighState == 2) leave child nodes with whichever highlight state they are set
        }

        this.dynamicLoadComplete = true;
        this.isLoading = false;
        this.expand(true);
        this.tree.locked = false;
    },

    /**
     * Returns this node's ancestor at the specified depth.
     * @method getAncestor
     * @param {int} depth the depth of the ancestor.
     * @return {Node} the ancestor
     */
    getAncestor: function(depth) {
        if (depth >= this.depth || depth < 0)  {
            this.logger.log("illegal getAncestor depth: " + depth);
            return null;
        }

        var p = this.parent;

        while (p.depth > depth) {
            p = p.parent;
        }

        return p;
    },

    /**
     * Returns the css class for the spacer at the specified depth for
     * this node.  If this node's ancestor at the specified depth
     * has a next sibling the presentation is different than if it
     * does not have a next sibling
     * @method getDepthStyle
     * @param {int} depth the depth of the ancestor.
     * @return {string} the css class for the spacer
     */
    getDepthStyle: function(depth) {
        return (this.getAncestor(depth).nextSibling) ?
            "ygtvdepthcell" : "ygtvblankdepthcell";
    },

    /**
     * Get the markup for the node.  This may be overrided so that we can
     * support different types of nodes.
     * @method getNodeHtml
     * @return {string} The HTML that will render this node.
     */
    getNodeHtml: function() {
        this.logger.log("Generating html");
        var sb = [];

        sb[sb.length] = '<table id="ygtvtableel' + this.index + '" border="0" cellpadding="0" cellspacing="0" class="ygtvtable ygtvdepth' + this.depth;
        sb[sb.length] = ' ygtv-' + (this.expanded?'expanded':'collapsed');
        if (this.enableHighlight) {
            sb[sb.length] = ' ygtv-highlight' + this.highlightState;
        }
        if (this.className) {
            sb[sb.length] = ' ' + this.className;
        }
        sb[sb.length] = '"><tr class="ygtvrow">';

        for (var i=0;i<this.depth;++i) {
            sb[sb.length] = '<td class="ygtvcell ' + this.getDepthStyle(i) + '"><div class="ygtvspacer"></div></td>';
        }

        if (this.hasIcon) {
            sb[sb.length] = '<td id="' + this.getToggleElId();
            sb[sb.length] = '" class="ygtvcell ';
            sb[sb.length] = this.getStyle() ;
            sb[sb.length] = '"><a href="#" class="ygtvspacer">&#160;</a></td>';
        }

        sb[sb.length] = '<td id="' + this.contentElId;
        sb[sb.length] = '" class="ygtvcell ';
        sb[sb.length] = this.contentStyle  + ' ygtvcontent" ';
        sb[sb.length] = (this.nowrap) ? ' nowrap="nowrap" ' : '';
        sb[sb.length] = ' >';
        sb[sb.length] = this.getContentHtml();
        sb[sb.length] = '</td></tr></table>';

        return sb.join("");

    },
    /**
     * Get the markup for the contents of the node.  This is designed to be overrided so that we can
     * support different types of nodes.
     * @method getContentHtml
     * @return {string} The HTML that will render the content of this node.
     */
    getContentHtml: function () {
        return "";
    },

    /**
     * Regenerates the html for this node and its children.  To be used when the
     * node is expanded and new children have been added.
     * @method refresh
     */
    refresh: function() {
        // this.loadComplete();
        this.getChildrenEl().innerHTML = this.completeRender();

        if (this.hasIcon) {
            var el = this.getToggleEl();
            if (el) {
                el.className = el.className.replace(/\bygtv[lt][nmp]h*\b/gi,this.getStyle());
            }
        }
    },

    /**
     * Node toString
     * @method toString
     * @return {string} string representation of the node
     */
    toString: function() {
        return this._type + " (" + this.index + ")";
    },
    /**
    * array of items that had the focus set on them
    * so that they can be cleaned when focus is lost
    * @property _focusHighlightedItems
    * @type Array of DOM elements
    * @private
    */
    _focusHighlightedItems: [],
    /**
    * DOM element that actually got the browser focus
    * @property _focusedItem
    * @type DOM element
    * @private
    */
    _focusedItem: null,

    /**
    * Returns true if there are any elements in the node that can
    * accept the real actual browser focus
    * @method _canHaveFocus
    * @return {boolean} success
    * @private
    */
    _canHaveFocus: function() {
        return this.getEl().getElementsByTagName('a').length > 0;
    },
    /**
    * Removes the focus of previously selected Node
    * @method _removeFocus
    * @private
    */
    _removeFocus:function () {
        if (this._focusedItem) {
            Event.removeListener(this._focusedItem,'blur');
            this._focusedItem = null;
        }
        var el;
        while ((el = this._focusHighlightedItems.shift())) {  // yes, it is meant as an assignment, really
            Dom.removeClass(el,YAHOO.widget.TreeView.FOCUS_CLASS_NAME );
        }
    },
    /**
    * Sets the focus on the node element.
    * It will only be able to set the focus on nodes that have anchor elements in it.
    * Toggle or branch icons have anchors and can be focused on.
    * If will fail in nodes that have no anchor
    * @method focus
    * @return {boolean} success
    */
    focus: function () {
        var focused = false, self = this;

        if (this.tree.currentFocus) {
            this.tree.currentFocus._removeFocus();
        }

        var  expandParent = function (node) {
            if (node.parent) {
                expandParent(node.parent);
                node.parent.expand();
            }
        };
        expandParent(this);

        Dom.getElementsBy  (
            function (el) {
                return (/ygtv(([tl][pmn]h?)|(content))/).test(el.className);
            } ,
            'td' ,
            self.getEl().firstChild ,
            function (el) {
                Dom.addClass(el, YAHOO.widget.TreeView.FOCUS_CLASS_NAME );
                if (!focused) {
                    var aEl = el.getElementsByTagName('a');
                    if (aEl.length) {
                        aEl = aEl[0];
                        aEl.focus();
                        self._focusedItem = aEl;
                        Event.on(aEl,'blur',function () {
                            self.tree.fireEvent('focusChanged',{oldNode:self.tree.currentFocus,newNode:null});
                            self.tree.currentFocus = null;
                            self._removeFocus();
                        });
                        focused = true;
                    }
                }
                self._focusHighlightedItems.push(el);
            }
        );
        if (focused) {
            this.tree.fireEvent('focusChanged',{oldNode:this.tree.currentFocus,newNode:this});
            this.tree.currentFocus = this;
        } else {
            this.tree.fireEvent('focusChanged',{oldNode:self.tree.currentFocus,newNode:null});
            this.tree.currentFocus = null;
            this._removeFocus();
        }
        return focused;
    },

  /**
     * Count of nodes in a branch
     * @method getNodeCount
     * @return {int} number of nodes in the branch
     */
    getNodeCount: function() {
        for (var i = 0, count = 0;i< this.children.length;i++) {
            count += this.children[i].getNodeCount();
        }
        return count + 1;
    },

      /**
     * Returns an object which could be used to build a tree out of this node and its children.
     * It can be passed to the tree constructor to reproduce this node as a tree.
     * It will return false if the node or any children loads dynamically, regardless of whether it is loaded or not.
     * @method getNodeDefinition
     * @return {Object | false}  definition of the tree or false if the node or any children is defined as dynamic
     */
    getNodeDefinition: function() {

        if (this.isDynamic()) { return false; }

        var def, defs = Lang.merge(this.data), children = [];



        if (this.expanded) {defs.expanded = this.expanded; }
        if (!this.multiExpand) { defs.multiExpand = this.multiExpand; }
        if (this.renderHidden) { defs.renderHidden = this.renderHidden; }
        if (!this.hasIcon) { defs.hasIcon = this.hasIcon; }
        if (this.nowrap) { defs.nowrap = this.nowrap; }
        if (this.className) { defs.className = this.className; }
        if (this.editable) { defs.editable = this.editable; }
        if (!this.enableHighlight) { defs.enableHighlight = this.enableHighlight; }
        if (this.highlightState) { defs.highlightState = this.highlightState; }
        if (this.propagateHighlightUp) { defs.propagateHighlightUp = this.propagateHighlightUp; }
        if (this.propagateHighlightDown) { defs.propagateHighlightDown = this.propagateHighlightDown; }
        defs.type = this._type;



        for (var i = 0; i < this.children.length;i++) {
            def = this.children[i].getNodeDefinition();
            if (def === false) { return false;}
            children.push(def);
        }
        if (children.length) { defs.children = children; }
        return defs;
    },


    /**
     * Generates the link that will invoke this node's toggle method
     * @method getToggleLink
     * @return {string} the javascript url for toggling this node
     */
    getToggleLink: function() {
        return 'return false;';
    },

    /**
    * Sets the value of property for this node and all loaded descendants.
    * Only public and defined properties can be set, not methods.
    * Values for unknown properties will be assigned to the refNode.data object
    * @method setNodesProperty
    * @param name {string} Name of the property to be set
    * @param value {any} value to be set
    * @param refresh {boolean} if present and true, it does a refresh
    */
    setNodesProperty: function(name, value, refresh) {
        if (name.charAt(0) != '_'  && !Lang.isUndefined(this[name]) && !Lang.isFunction(this[name]) ) {
            this[name] = value;
        } else {
            this.data[name] = value;
        }
        for (var i = 0; i < this.children.length;i++) {
            this.children[i].setNodesProperty(name,value);
        }
        if (refresh) {
            this.refresh();
        }
    },
    /**
    * Toggles the highlighted state of a Node
    * @method toggleHighlight
    */
    toggleHighlight: function() {
        if (this.enableHighlight) {
            // unhighlights only if fully highligthed.  For not or partially highlighted it will highlight
            if (this.highlightState == 1) {
                this.unhighlight();
            } else {
                this.highlight();
            }
        }
    },

    /**
    * Turns highlighting on node.
    * @method highlight
    * @param _silent {boolean} optional, don't fire the highlightEvent
    */
    highlight: function(_silent) {
        if (this.enableHighlight) {
            if (this.tree.singleNodeHighlight) {
                if (this.tree._currentlyHighlighted) {
                    this.tree._currentlyHighlighted.unhighlight(_silent);
                }
                this.tree._currentlyHighlighted = this;
            }
            this.highlightState = 1;
            this._setHighlightClassName();
            if (!this.tree.singleNodeHighlight) {
                if (this.propagateHighlightDown) {
                    for (var i = 0;i < this.children.length;i++) {
                        this.children[i].highlight(true);
                    }
                }
                if (this.propagateHighlightUp) {
                    if (this.parent) {
                        this.parent._childrenHighlighted();
                    }
                }
            }
            if (!_silent) {
                this.tree.fireEvent('highlightEvent',this);
            }
        }
    },
    /**
    * Turns highlighting off a node.
    * @method unhighlight
    * @param _silent {boolean} optional, don't fire the highlightEvent
    */
    unhighlight: function(_silent) {
        if (this.enableHighlight) {
            // might have checked singleNodeHighlight but it wouldn't really matter either way
            this.tree._currentlyHighlighted = null;
            this.highlightState = 0;
            this._setHighlightClassName();
            if (!this.tree.singleNodeHighlight) {
                if (this.propagateHighlightDown) {
                    for (var i = 0;i < this.children.length;i++) {
                        this.children[i].unhighlight(true);
                    }
                }
                if (this.propagateHighlightUp) {
                    if (this.parent) {
                        this.parent._childrenHighlighted();
                    }
                }
            }
            if (!_silent) {
                this.tree.fireEvent('highlightEvent',this);
            }
        }
    },
    /**
    * Checks whether all or part of the children of a node are highlighted and
    * sets the node highlight to full, none or partial highlight.
    * If set to propagate it will further call the parent
    * @method _childrenHighlighted
    * @private
    */
    _childrenHighlighted: function() {
        var yes = false, no = false;
        if (this.enableHighlight) {
            for (var i = 0;i < this.children.length;i++) {
                switch(this.children[i].highlightState) {
                    case 0:
                        no = true;
                        break;
                    case 1:
                        yes = true;
                        break;
                    case 2:
                        yes = no = true;
                        break;
                }
            }
            if (yes && no) {
                this.highlightState = 2;
            } else if (yes) {
                this.highlightState = 1;
            } else {
                this.highlightState = 0;
            }
            this._setHighlightClassName();
            if (this.propagateHighlightUp) {
                if (this.parent) {
                    this.parent._childrenHighlighted();
                }
            }
        }
    },

    /**
    * Changes the classNames on the toggle and content containers to reflect the current highlighting
    * @method _setHighlightClassName
    * @private
    */
    _setHighlightClassName: function() {
        var el = Dom.get('ygtvtableel' + this.index);
        if (el) {
            el.className = el.className.replace(/\bygtv-highlight\d\b/gi,'ygtv-highlight' + this.highlightState);
        }
    }

};

YAHOO.augment(YAHOO.widget.Node, YAHOO.util.EventProvider);
})();
/**
 * A custom YAHOO.widget.Node that handles the unique nature of
 * the virtual, presentationless root node.
 * @namespace YAHOO.widget
 * @class RootNode
 * @extends YAHOO.widget.Node
 * @param oTree {YAHOO.widget.TreeView} The tree instance this node belongs to
 * @constructor
 */
YAHOO.widget.RootNode = function(oTree) {
    // Initialize the node with null params.  The root node is a
    // special case where the node has no presentation.  So we have
    // to alter the standard properties a bit.
    this.init(null, null, true);

    /*
     * For the root node, we get the tree reference from as a param
     * to the constructor instead of from the parent element.
     */
    this.tree = oTree;
};

YAHOO.extend(YAHOO.widget.RootNode, YAHOO.widget.Node, {

   /**
     * The node type
     * @property _type
      * @type string
     * @private
     * @default "RootNode"
     */
    _type: "RootNode",

    // overrides YAHOO.widget.Node
    getNodeHtml: function() {
        return "";
    },

    toString: function() {
        return this._type;
    },

    loadComplete: function() {
        this.tree.draw();
    },

   /**
     * Count of nodes in tree.
    * It overrides Nodes.getNodeCount because the root node should not be counted.
     * @method getNodeCount
     * @return {int} number of nodes in the tree
     */
    getNodeCount: function() {
        for (var i = 0, count = 0;i< this.children.length;i++) {
            count += this.children[i].getNodeCount();
        }
        return count;
    },

  /**
     * Returns an object which could be used to build a tree out of this node and its children.
     * It can be passed to the tree constructor to reproduce this node as a tree.
     * Since the RootNode is automatically created by treeView,
     * its own definition is excluded from the returned node definition
     * which only contains its children.
     * @method getNodeDefinition
     * @return {Object | false}  definition of the tree or false if any child node is defined as dynamic
     */
    getNodeDefinition: function() {

        for (var def, defs = [], i = 0; i < this.children.length;i++) {
            def = this.children[i].getNodeDefinition();
            if (def === false) { return false;}
            defs.push(def);
        }
        return defs;
    },

    collapse: function() {},
    expand: function() {},
    getSiblings: function() { return null; },
    focus: function () {}

});
(function () {
    var Dom = YAHOO.util.Dom,
        Lang = YAHOO.lang,
        Event = YAHOO.util.Event;
/**
 * The default node presentation.  The first parameter should be
 * either a string that will be used as the node's label, or an object
 * that has at least a string property called label.  By default,  clicking the
 * label will toggle the expanded/collapsed state of the node.  By
 * setting the href property of the instance, this behavior can be
 * changed so that the label will go to the specified href.
 * @namespace YAHOO.widget
 * @class TextNode
 * @extends YAHOO.widget.Node
 * @constructor
 * @param oData {object} a string or object containing the data that will
 * be used to render this node.
 * Providing a string is the same as providing an object with a single property named label.
 * All values in the oData will be used to set equally named properties in the node
 * as long as the node does have such properties, they are not undefined, private or functions.
 * All attributes are made available in noderef.data, which
 * can be used to store custom attributes.  TreeView.getNode(s)ByProperty
 * can be used to retrieve a node by one of the attributes.
 * @param oParent {YAHOO.widget.Node} this node's parent node
 * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded)
 */
YAHOO.widget.TextNode = function(oData, oParent, expanded) {

    if (oData) {
        if (Lang.isString(oData)) {
            oData = { label: oData };
        }
        this.init(oData, oParent, expanded);
        this.setUpLabel(oData);
    }

    this.logger     = new YAHOO.widget.LogWriter(this.toString());
};

YAHOO.extend(YAHOO.widget.TextNode, YAHOO.widget.Node, {

    /**
     * The CSS class for the label href.  Defaults to ygtvlabel, but can be
     * overridden to provide a custom presentation for a specific node.
     * @property labelStyle
     * @type string
     */
    labelStyle: "ygtvlabel",

    /**
     * The derived element id of the label for this node
     * @property labelElId
     * @type string
     */
    labelElId: null,

    /**
     * The text for the label.  It is assumed that the oData parameter will
     * either be a string that will be used as the label, or an object that
     * has a property called "label" that we will use.
     * @property label
     * @type string
     */
    label: null,

    /**
     * The text for the title (tooltip) for the label element
     * @property title
     * @type string
     */
    title: null,

    /**
     * The href for the node's label.  If one is not specified, the href will
     * be set so that it toggles the node.
     * @property href
     * @type string
     */
    href: null,

    /**
     * The label href target, defaults to current window
     * @property target
     * @type string
     */
    target: "_self",

    /**
     * The node type
     * @property _type
     * @private
     * @type string
     * @default "TextNode"
     */
    _type: "TextNode",


    /**
     * Sets up the node label
     * @method setUpLabel
     * @param oData string containing the label, or an object with a label property
     */
    setUpLabel: function(oData) {

        if (Lang.isString(oData)) {
            oData = {
                label: oData
            };
        } else {
            if (oData.style) {
                this.labelStyle = oData.style;
            }
        }

        this.label = oData.label;

        this.labelElId = "ygtvlabelel" + this.index;

    },

    /**
     * Returns the label element
     * @for YAHOO.widget.TextNode
     * @method getLabelEl
     * @return {object} the element
     */
    getLabelEl: function() {
        return Dom.get(this.labelElId);
    },

    // overrides YAHOO.widget.Node
    getContentHtml: function() {
        var sb = [];
        sb[sb.length] = this.href ? '<a' : '<span';
        sb[sb.length] = ' id="' + Lang.escapeHTML(this.labelElId) + '"';
        sb[sb.length] = ' class="' + Lang.escapeHTML(this.labelStyle)  + '"';
        if (this.href) {
            sb[sb.length] = ' href="' + Lang.escapeHTML(this.href) + '"';
            sb[sb.length] = ' target="' + Lang.escapeHTML(this.target) + '"';
        }
        if (this.title) {
            sb[sb.length] = ' title="' + Lang.escapeHTML(this.title) + '"';
        }
        sb[sb.length] = ' >';
        sb[sb.length] = Lang.escapeHTML(this.label);
        sb[sb.length] = this.href?'</a>':'</span>';
        return sb.join("");
    },



  /**
     * Returns an object which could be used to build a tree out of this node and its children.
     * It can be passed to the tree constructor to reproduce this node as a tree.
     * It will return false if the node or any descendant loads dynamically, regardless of whether it is loaded or not.
     * @method getNodeDefinition
     * @return {Object | false}  definition of the tree or false if this node or any descendant is defined as dynamic
     */
    getNodeDefinition: function() {
        var def = YAHOO.widget.TextNode.superclass.getNodeDefinition.call(this);
        if (def === false) { return false; }

        // Node specific properties
        def.label = this.label;
        if (this.labelStyle != 'ygtvlabel') { def.style = this.labelStyle; }
        if (this.title) { def.title = this.title; }
        if (this.href) { def.href = this.href; }
        if (this.target != '_self') { def.target = this.target; }

        return def;

    },

    toString: function() {
        return YAHOO.widget.TextNode.superclass.toString.call(this) + ": " + this.label;
    },

    // deprecated
    onLabelClick: function() {
        return false;
    },
    refresh: function() {
        YAHOO.widget.TextNode.superclass.refresh.call(this);
        var label = this.getLabelEl();
        label.innerHTML = this.label;
        if (label.tagName.toUpperCase() == 'A') {
            label.href = this.href;
            label.target = this.target;
        }
    }




});
})();
/**
 * A menu-specific implementation that differs from TextNode in that only
 * one sibling can be expanded at a time.
 * @namespace YAHOO.widget
 * @class MenuNode
 * @extends YAHOO.widget.TextNode
 * @param oData {object} a string or object containing the data that will
 * be used to render this node.
 * Providing a string is the same as providing an object with a single property named label.
 * All values in the oData will be used to set equally named properties in the node
 * as long as the node does have such properties, they are not undefined, private or functions.
 * All attributes are made available in noderef.data, which
 * can be used to store custom attributes.  TreeView.getNode(s)ByProperty
 * can be used to retrieve a node by one of the attributes.
 * @param oParent {YAHOO.widget.Node} this node's parent node
 * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded)
 * @constructor
 */
YAHOO.widget.MenuNode = function(oData, oParent, expanded) {
    YAHOO.widget.MenuNode.superclass.constructor.call(this,oData,oParent,expanded);

   /*
     * Menus usually allow only one branch to be open at a time.
     */
    this.multiExpand = false;

};

YAHOO.extend(YAHOO.widget.MenuNode, YAHOO.widget.TextNode, {

    /**
     * The node type
     * @property _type
     * @private
    * @default "MenuNode"
     */
    _type: "MenuNode"

});
(function () {
    var Dom = YAHOO.util.Dom,
        Lang = YAHOO.lang,
        Event = YAHOO.util.Event;

/**
 * This implementation takes either a string or object for the
 * oData argument.  If is it a string, it will use it for the display
 * of this node (and it can contain any html code).  If the parameter
 * is an object,it looks for a parameter called "html" that will be
 * used for this node's display.
 * @namespace YAHOO.widget
 * @class HTMLNode
 * @extends YAHOO.widget.Node
 * @constructor
 * @param oData {object} a string or object containing the data that will
 * be used to render this node.
 * Providing a string is the same as providing an object with a single property named html.
 * All values in the oData will be used to set equally named properties in the node
 * as long as the node does have such properties, they are not undefined, private or functions.
 * All other attributes are made available in noderef.data, which
 * can be used to store custom attributes.  TreeView.getNode(s)ByProperty
 * can be used to retrieve a node by one of the attributes.
 * @param oParent {YAHOO.widget.Node} this node's parent node
 * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded)
 * @param hasIcon {boolean} specifies whether or not leaf nodes should
 * be rendered with or without a horizontal line and/or toggle icon. If the icon
 * is not displayed, the content fills the space it would have occupied.
 * This option operates independently of the leaf node presentation logic
 * for dynamic nodes.
 * (deprecated; use oData.hasIcon)
 */
var HN =  function(oData, oParent, expanded, hasIcon) {
    if (oData) {
        this.init(oData, oParent, expanded);
        this.initContent(oData, hasIcon);
    }
};


YAHOO.widget.HTMLNode = HN;
YAHOO.extend(HN, YAHOO.widget.Node, {

    /**
     * The CSS class for the html content container.  Defaults to ygtvhtml, but
     * can be overridden to provide a custom presentation for a specific node.
     * @property contentStyle
     * @type string
     */
    contentStyle: "ygtvhtml",


    /**
     * The HTML content to use for this node's display
     * @property html
     * @type string
     */
    html: null,

/**
     * The node type
     * @property _type
     * @private
     * @type string
     * @default "HTMLNode"
     */
    _type: "HTMLNode",

    /**
     * Sets up the node label
     * @method initContent
     * @param oData {object} An html string or object containing an html property
     * @param hasIcon {boolean} determines if the node will be rendered with an
     * icon or not
     */
    initContent: function(oData, hasIcon) {
        this.setHtml(oData);
        this.contentElId = "ygtvcontentel" + this.index;
        if (!Lang.isUndefined(hasIcon)) { this.hasIcon  = hasIcon; }

        this.logger = new YAHOO.widget.LogWriter(this.toString());
    },

    /**
     * Synchronizes the node.html, and the node's content
     * @method setHtml
     * @param o {object |string | HTMLElement } An html string, an object containing an html property or an HTML element
     */
    setHtml: function(o) {
        this.html = (Lang.isObject(o) && 'html' in o) ? o.html : o;

        var el = this.getContentEl();
        if (el) {
            if (o.nodeType && o.nodeType == 1 && o.tagName) {
                el.innerHTML = "";
            } else {
                el.innerHTML = this.html;
            }
        }

    },

    // overrides YAHOO.widget.Node
    // If property html is a string, it sets the innerHTML for the node
    // If it is an HTMLElement, it defers appending it to the tree until the HTML basic structure is built
    getContentHtml: function() {
        if (typeof this.html === "string") {
            return this.html;
        } else {

            HN._deferredNodes.push(this);
            if (!HN._timer) {
                HN._timer = window.setTimeout(function () {
                    var n;
                    while((n = HN._deferredNodes.pop())) {
                        n.getContentEl().appendChild(n.html);
                    }
                    HN._timer = null;
                },0);
            }
            return "";
        }
    },

      /**
     * Returns an object which could be used to build a tree out of this node and its children.
     * It can be passed to the tree constructor to reproduce this node as a tree.
     * It will return false if any node loads dynamically, regardless of whether it is loaded or not.
     * @method getNodeDefinition
     * @return {Object | false}  definition of the tree or false if any node is defined as dynamic
     */
    getNodeDefinition: function() {
        var def = HN.superclass.getNodeDefinition.call(this);
        if (def === false) { return false; }
        def.html = this.html;
        return def;

    }
});

    /**
    * An array of HTMLNodes created with HTML Elements that had their rendering
    * deferred until the basic tree structure is rendered.
    * @property _deferredNodes
    * @type YAHOO.widget.HTMLNode[]
    * @default []
    * @private
    * @static
    */
HN._deferredNodes = [];
    /**
    * A system timer value used to mark whether a deferred operation is pending.
    * @property _timer
    * @type System Timer
    * @default null
    * @private
    * @static
    */
HN._timer = null;
})();
(function () {
    var Dom = YAHOO.util.Dom,
        Lang = YAHOO.lang,
        Event = YAHOO.util.Event,
        Calendar = YAHOO.widget.Calendar;

/**
 * A Date-specific implementation that differs from TextNode in that it uses
 * YAHOO.widget.Calendar as an in-line editor, if available
 * If Calendar is not available, it behaves as a plain TextNode.
 * @namespace YAHOO.widget
 * @class DateNode
 * @extends YAHOO.widget.TextNode
 * @param oData {object} a string or object containing the data that will
 * be used to render this node.
 * Providing a string is the same as providing an object with a single property named label.
 * All values in the oData will be used to set equally named properties in the node
 * as long as the node does have such properties, they are not undefined, private nor functions.
 * All attributes are made available in noderef.data, which
 * can be used to store custom attributes.  TreeView.getNode(s)ByProperty
 * can be used to retrieve a node by one of the attributes.
 * @param oParent {YAHOO.widget.Node} this node's parent node
 * @param expanded {boolean} the initial expanded/collapsed state (deprecated; use oData.expanded)
 * @constructor
 */
YAHOO.widget.DateNode = function(oData, oParent, expanded) {
    YAHOO.widget.DateNode.superclass.constructor.call(this,oData, oParent, expanded);
};

YAHOO.extend(YAHOO.widget.DateNode, YAHOO.widget.TextNode, {

    /**
     * The node type
     * @property _type
     * @type string
     * @private
     * @default  "DateNode"
     */
    _type: "DateNode",

    /**
    * Configuration object for the Calendar editor, if used.
    * See <a href="http://developer.yahoo.com/yui/calendar/#internationalization">http://developer.yahoo.com/yui/calendar/#internationalization</a>
    * @property calendarConfig
    */
    calendarConfig: null,



    /**
     *  If YAHOO.widget.Calendar is available, it will pop up a Calendar to enter a new date.  Otherwise, it falls back to a plain &lt;input&gt;  textbox
     * @method fillEditorContainer
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @return void
     */
    fillEditorContainer: function (editorData) {

        var cal, container = editorData.inputContainer;

        if (Lang.isUndefined(Calendar)) {
            Dom.replaceClass(editorData.editorPanel,'ygtv-edit-DateNode','ygtv-edit-TextNode');
            YAHOO.widget.DateNode.superclass.fillEditorContainer.call(this, editorData);
            return;
        }

        if (editorData.nodeType != this._type) {
            editorData.nodeType = this._type;
            editorData.saveOnEnter = false;

            editorData.node.destroyEditorContents(editorData);

            editorData.inputObject = cal = new Calendar(container.appendChild(document.createElement('div')));
            if (this.calendarConfig) {
                cal.cfg.applyConfig(this.calendarConfig,true);
                cal.cfg.fireQueue();
            }
            cal.selectEvent.subscribe(function () {
                this.tree._closeEditor(true);
            },this,true);
        } else {
            cal = editorData.inputObject;
        }

        editorData.oldValue = this.label;
        cal.cfg.setProperty("selected",this.label, false);

        var delim = cal.cfg.getProperty('DATE_FIELD_DELIMITER');
        var pageDate = this.label.split(delim);
        cal.cfg.setProperty('pagedate',pageDate[cal.cfg.getProperty('MDY_MONTH_POSITION') -1] + delim + pageDate[cal.cfg.getProperty('MDY_YEAR_POSITION') -1]);
        cal.cfg.fireQueue();

        cal.render();
        cal.oDomContainer.focus();
    },
     /**
    * Returns the value from the input element.
    * Overrides Node.getEditorValue.
    * @method getEditorValue
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @return {string} date entered
     */

    getEditorValue: function (editorData) {
        if (Lang.isUndefined(Calendar)) {
            return editorData.inputElement.value;
        } else {
            var cal = editorData.inputObject,
                date = cal.getSelectedDates()[0],
                dd = [];

            dd[cal.cfg.getProperty('MDY_DAY_POSITION') -1] = date.getDate();
            dd[cal.cfg.getProperty('MDY_MONTH_POSITION') -1] = date.getMonth() + 1;
            dd[cal.cfg.getProperty('MDY_YEAR_POSITION') -1] = date.getFullYear();
            return dd.join(cal.cfg.getProperty('DATE_FIELD_DELIMITER'));
        }
    },

    /**
     * Finally displays the newly entered date in the tree.
     * Overrides Node.displayEditedValue.
     * @method displayEditedValue
     * @param value {HTML} date to be displayed and stored in the node.
     * This data is added to the node unescaped via the innerHTML property.
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     */
    displayEditedValue: function (value,editorData) {
        var node = editorData.node;
        node.label = value;
        node.getLabelEl().innerHTML = value;
    },

   /**
     * Returns an object which could be used to build a tree out of this node and its children.
     * It can be passed to the tree constructor to reproduce this node as a tree.
     * It will return false if the node or any descendant loads dynamically, regardless of whether it is loaded or not.
     * @method getNodeDefinition
     * @return {Object | false}  definition of the node or false if this node or any descendant is defined as dynamic
     */
    getNodeDefinition: function() {
        var def = YAHOO.widget.DateNode.superclass.getNodeDefinition.call(this);
        if (def === false) { return false; }
        if (this.calendarConfig) { def.calendarConfig = this.calendarConfig; }
        return def;
    }


});
})();
(function () {
    var Dom = YAHOO.util.Dom,
        Lang = YAHOO.lang,
        Event = YAHOO.util.Event,
        TV = YAHOO.widget.TreeView,
        TVproto = TV.prototype;

    /**
     * An object to store information used for in-line editing
     * for all Nodes of all TreeViews. It contains:
     * <ul>
    * <li>active {boolean}, whether there is an active cell editor </li>
    * <li>whoHasIt {YAHOO.widget.TreeView} TreeView instance that is currently using the editor</li>
    * <li>nodeType {string} value of static Node._type property, allows reuse of input element if node is of the same type.</li>
    * <li>editorPanel {HTMLelement (&lt;div&gt;)} element holding the in-line editor</li>
    * <li>inputContainer {HTMLelement (&lt;div&gt;)} element which will hold the type-specific input element(s) to be filled by the fillEditorContainer method</li>
    * <li>buttonsContainer {HTMLelement (&lt;div&gt;)} element which holds the &lt;button&gt; elements for Ok/Cancel.  If you don't want any of the buttons, hide it via CSS styles, don't destroy it</li>
    * <li>node {YAHOO.widget.Node} reference to the Node being edited</li>
    * <li>saveOnEnter {boolean}, whether the Enter key should be accepted as a Save command (Esc. is always taken as Cancel), disable for multi-line input elements </li>
    * <li>oldValue {any}  value before editing</li>
    * </ul>
    *  Editors are free to use this object to store additional data.
     * @property editorData
     * @static
     * @for YAHOO.widget.TreeView
     */
    TV.editorData = {
        active:false,
        whoHasIt:null, // which TreeView has it
        nodeType:null,
        editorPanel:null,
        inputContainer:null,
        buttonsContainer:null,
        node:null, // which Node is being edited
        saveOnEnter:true,
        oldValue:undefined
        // Each node type is free to add its own properties to this as it sees fit.
    };

    /**
    * Validator function for edited data, called from the TreeView instance scope,
    * receives the arguments (newValue, oldValue, nodeInstance)
    * and returns either the validated (or type-converted) value or undefined.
    * An undefined return will prevent the editor from closing
    * @property validator
    * @type function
    * @default null
     * @for YAHOO.widget.TreeView
     */
    TVproto.validator = null;

    /**
    * Entry point for initializing the editing plug-in.
    * TreeView will call this method on initializing if it exists
    * @method _initEditor
     * @for YAHOO.widget.TreeView
     * @private
    */

    TVproto._initEditor = function () {
        /**
        * Fires when the user clicks on the ok button of a node editor
        * @event editorSaveEvent
        * @type CustomEvent
        * @param oArgs.newValue {mixed} the new value just entered
        * @param oArgs.oldValue {mixed} the value originally in the tree
        * @param oArgs.node {YAHOO.widget.Node} the node that has the focus
            * @for YAHOO.widget.TreeView
        */
        this.createEvent("editorSaveEvent", this);

        /**
        * Fires when the user clicks on the cancel button of a node editor
        * @event editorCancelEvent
        * @type CustomEvent
        * @param {YAHOO.widget.Node} node the node that has the focus
            * @for YAHOO.widget.TreeView
        */
        this.createEvent("editorCancelEvent", this);

    };

    /**
    * Entry point of the editing plug-in.
    * TreeView will call this method if it exists when a node label is clicked
    * @method _nodeEditing
    * @param node {YAHOO.widget.Node} the node to be edited
    * @return {Boolean} true to indicate that the node is editable and prevent any further bubbling of the click.
     * @for YAHOO.widget.TreeView
     * @private
    */



    TVproto._nodeEditing = function (node) {
        if (node.fillEditorContainer && node.editable) {
            var ed, topLeft, buttons, button, editorData = TV.editorData;
            editorData.active = true;
            editorData.whoHasIt = this;
            if (!editorData.nodeType) {
                // Fixes: http://yuilibrary.com/projects/yui2/ticket/2528945
                editorData.editorPanel = ed = this.getEl().appendChild(document.createElement('div'));
                Dom.addClass(ed,'ygtv-label-editor');
                ed.tabIndex = 0;

                buttons = editorData.buttonsContainer = ed.appendChild(document.createElement('div'));
                Dom.addClass(buttons,'ygtv-button-container');
                button = buttons.appendChild(document.createElement('button'));
                Dom.addClass(button,'ygtvok');
                button.innerHTML = ' ';
                button = buttons.appendChild(document.createElement('button'));
                Dom.addClass(button,'ygtvcancel');
                button.innerHTML = ' ';
                Event.on(buttons, 'click', function (ev) {
                    var target = Event.getTarget(ev),
                        editorData = TV.editorData,
                        node = editorData.node,
                        self = editorData.whoHasIt;
                    self.logger.log('click on editor');
                    if (Dom.hasClass(target,'ygtvok')) {
                        node.logger.log('ygtvok');
                        Event.stopEvent(ev);
                        self._closeEditor(true);
                    }
                    if (Dom.hasClass(target,'ygtvcancel')) {
                        node.logger.log('ygtvcancel');
                        Event.stopEvent(ev);
                        self._closeEditor(false);
                    }
                });

                editorData.inputContainer = ed.appendChild(document.createElement('div'));
                Dom.addClass(editorData.inputContainer,'ygtv-input');

                Event.on(ed,'keydown',function (ev) {
                    var editorData = TV.editorData,
                        KEY = YAHOO.util.KeyListener.KEY,
                        self = editorData.whoHasIt;
                    switch (ev.keyCode) {
                        case KEY.ENTER:
                            self.logger.log('ENTER');
                            Event.stopEvent(ev);
                            if (editorData.saveOnEnter) {
                                self._closeEditor(true);
                            }
                            break;
                        case KEY.ESCAPE:
                            self.logger.log('ESC');
                            Event.stopEvent(ev);
                            self._closeEditor(false);
                            break;
                    }
                });



            } else {
                ed = editorData.editorPanel;
            }
            editorData.node = node;
            if (editorData.nodeType) {
                Dom.removeClass(ed,'ygtv-edit-' + editorData.nodeType);
            }
            Dom.addClass(ed,' ygtv-edit-' + node._type);
            // Fixes: http://yuilibrary.com/projects/yui2/ticket/2528945
            Dom.setStyle(ed,'display','block');
            Dom.setXY(ed,Dom.getXY(node.getContentEl()));
            // up to here
            ed.focus();
            node.fillEditorContainer(editorData);

            return true;  // If inline editor available, don't do anything else.
        }
    };

    /**
    * Method to be associated with an event (clickEvent, dblClickEvent or enterKeyPressed) to pop up the contents editor
    *  It calls the corresponding node editNode method.
    * @method onEventEditNode
    * @param oArgs {object} Object passed as arguments to TreeView event listeners
    * @for YAHOO.widget.TreeView
    */

    TVproto.onEventEditNode = function (oArgs) {
        if (oArgs instanceof YAHOO.widget.Node) {
            oArgs.editNode();
        } else if (oArgs.node instanceof YAHOO.widget.Node) {
            oArgs.node.editNode();
        }
        return false;
    };

    /**
    * Method to be called when the inline editing is finished and the editor is to be closed
    * @method _closeEditor
    * @param save {Boolean} true if the edited value is to be saved, false if discarded
    * @private
     * @for YAHOO.widget.TreeView
    */

    TVproto._closeEditor = function (save) {
        var ed = TV.editorData,
            node = ed.node,
            close = true;
        // http://yuilibrary.com/projects/yui2/ticket/2528946
        // _closeEditor might now be called at any time, even when there is no label editor open
        // so we need to ensure there is one.
        if (!node || !ed.active) { return; }
        if (save) {
            close = ed.node.saveEditorValue(ed) !== false;
        } else {
            this.fireEvent( 'editorCancelEvent', node);
        }

        if (close) {
            Dom.setStyle(ed.editorPanel,'display','none');
            ed.active = false;
            node.focus();
        }
    };

    /**
    *  Entry point for TreeView's destroy method to destroy whatever the editing plug-in has created
    * @method _destroyEditor
    * @private
     * @for YAHOO.widget.TreeView
    */
    TVproto._destroyEditor = function() {
        var ed = TV.editorData;
        if (ed && ed.nodeType && (!ed.active || ed.whoHasIt === this)) {
            Event.removeListener(ed.editorPanel,'keydown');
            Event.removeListener(ed.buttonContainer,'click');
            ed.node.destroyEditorContents(ed);
            document.body.removeChild(ed.editorPanel);
            ed.nodeType = ed.editorPanel = ed.inputContainer = ed.buttonsContainer = ed.whoHasIt = ed.node = null;
            ed.active = false;
        }
    };

    var Nproto = YAHOO.widget.Node.prototype;

    /**
    * Signals if the label is editable.  (Ignored on TextNodes with href set.)
    * @property editable
    * @type boolean
         * @for YAHOO.widget.Node
    */
    Nproto.editable = false;

    /**
    * pops up the contents editor, if there is one and the node is declared editable
    * @method editNode
     * @for YAHOO.widget.Node
    */

    Nproto.editNode = function () {
        this.tree._nodeEditing(this);
    };


    /** Placeholder for a function that should provide the inline node label editor.
     *   Leaving it set to null will indicate that this node type is not editable.
     * It should be overridden by nodes that provide inline editing.
     *  The Node-specific editing element (input box, textarea or whatever) should be inserted into editorData.inputContainer.
     * @method fillEditorContainer
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @return void
     * @for YAHOO.widget.Node
     */
    Nproto.fillEditorContainer = null;


    /**
    * Node-specific destroy function to empty the contents of the inline editor panel.
    * This function is the worst case alternative that will purge all possible events and remove the editor contents.
    * Method Event.purgeElement is somewhat costly so if it can be replaced by specifc Event.removeListeners, it is better to do so.
    * @method destroyEditorContents
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @for YAHOO.widget.Node
     */
    Nproto.destroyEditorContents = function (editorData) {
        // In the worst case, if the input editor (such as the Calendar) has no destroy method
        // we can only try to remove all possible events on it.
        Event.purgeElement(editorData.inputContainer,true);
        editorData.inputContainer.innerHTML = '';
    };

    /**
    * Saves the value entered into the editor.
    * @method saveEditorValue
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @return {false or none} a return of exactly false will prevent the editor from closing
     * @for YAHOO.widget.Node
     */
    Nproto.saveEditorValue = function (editorData) {
        var node = editorData.node,
            value,
            validator = node.tree.validator;

        value = this.getEditorValue(editorData);

        if (Lang.isFunction(validator)) {
            value = validator(value,editorData.oldValue,node);
            if (Lang.isUndefined(value)) {
                return false;
            }
        }

        if (this.tree.fireEvent( 'editorSaveEvent', {
            newValue:value,
            oldValue:editorData.oldValue,
            node:node
        }) !== false) {
            this.displayEditedValue(value,editorData);
        }
    };


    /**
     * Returns the value(s) from the input element(s) .
     * Should be overridden by each node type.
     * @method getEditorValue
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @return {any} value entered
     * @for YAHOO.widget.Node
     */

     Nproto.getEditorValue = function (editorData) {
    };

    /**
     * Finally displays the newly edited value(s) in the tree.
     * Should be overridden by each node type.
     * @method displayEditedValue
     * @param value {HTML} value to be displayed and stored in the node
     * This data is added to the node unescaped via the innerHTML property.
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @for YAHOO.widget.Node
     */
    Nproto.displayEditedValue = function (value,editorData) {
    };

    var TNproto = YAHOO.widget.TextNode.prototype;



    /**
     *  Places an &lt;input&gt;  textbox in the input container and loads the label text into it.
     * @method fillEditorContainer
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @return void
     * @for YAHOO.widget.TextNode
     */
    TNproto.fillEditorContainer = function (editorData) {

        var input;
        // If last node edited is not of the same type as this one, delete it and fill it with our editor
        if (editorData.nodeType != this._type) {
            editorData.nodeType = this._type;
            editorData.saveOnEnter = true;
            editorData.node.destroyEditorContents(editorData);

            editorData.inputElement = input = editorData.inputContainer.appendChild(document.createElement('input'));

        } else {
            // if the last node edited was of the same time, reuse the input element.
            input = editorData.inputElement;
        }
        editorData.oldValue = this.label;
        input.value = this.label;
        input.focus();
        input.select();
    };

    /**
     * Returns the value from the input element.
     * Overrides Node.getEditorValue.
     * @method getEditorValue
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @return {string} value entered
     * @for YAHOO.widget.TextNode
     */

    TNproto.getEditorValue = function (editorData) {
        return editorData.inputElement.value;
    };

    /**
     * Finally displays the newly edited value in the tree.
     * Overrides Node.displayEditedValue.
     * @method displayEditedValue
     * @param value {string} value to be displayed and stored in the node
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @for YAHOO.widget.TextNode
     */
    TNproto.displayEditedValue = function (value,editorData) {
        var node = editorData.node;
        node.label = value;
        node.getLabelEl().innerHTML = value;
    };

    /**
    * Destroys the contents of the inline editor panel.
    * Overrides Node.destroyEditorContent.
    * Since we didn't set any event listeners on this inline editor, it is more efficient to avoid the generic method in Node.
    * @method destroyEditorContents
     * @param editorData {YAHOO.widget.TreeView.editorData}  a shortcut to the static object holding editing information
     * @for YAHOO.widget.TextNode
     */
    TNproto.destroyEditorContents = function (editorData) {
        editorData.inputContainer.innerHTML = '';
    };
})();
/**
 * A static factory class for tree view expand/collapse animations
 * @class TVAnim
 * @static
 */
YAHOO.widget.TVAnim = function() {
    return {
        /**
         * Constant for the fade in animation
         * @property FADE_IN
         * @type string
         * @static
         */
        FADE_IN: "TVFadeIn",

        /**
         * Constant for the fade out animation
         * @property FADE_OUT
         * @type string
         * @static
         */
        FADE_OUT: "TVFadeOut",

        /**
         * Returns a ygAnim instance of the given type
         * @method getAnim
         * @param type {string} the type of animation
         * @param el {HTMLElement} the element to element (probably the children div)
         * @param callback {function} function to invoke when the animation is done.
         * @return {YAHOO.util.Animation} the animation instance
         * @static
         */
        getAnim: function(type, el, callback) {
            if (YAHOO.widget[type]) {
                return new YAHOO.widget[type](el, callback);
            } else {
                return null;
            }
        },

        /**
         * Returns true if the specified animation class is available
         * @method isValid
         * @param type {string} the type of animation
         * @return {boolean} true if valid, false if not
         * @static
         */
        isValid: function(type) {
            return (YAHOO.widget[type]);
        }
    };
} ();
/**
 * A 1/2 second fade-in animation.
 * @class TVFadeIn
 * @constructor
 * @param el {HTMLElement} the element to animate
 * @param callback {function} function to invoke when the animation is finished
 */
YAHOO.widget.TVFadeIn = function(el, callback) {
    /**
     * The element to animate
     * @property el
     * @type HTMLElement
     */
    this.el = el;

    /**
     * the callback to invoke when the animation is complete
     * @property callback
     * @type function
     */
    this.callback = callback;

    this.logger = new YAHOO.widget.LogWriter(this.toString());
};

YAHOO.widget.TVFadeIn.prototype = {
    /**
     * Performs the animation
     * @method animate
     */
    animate: function() {
        var tvanim = this;

        var s = this.el.style;
        s.opacity = 0.1;
        s.filter = "alpha(opacity=10)";
        s.display = "";

        var dur = 0.4; 
        var a = new YAHOO.util.Anim(this.el, {opacity: {from: 0.1, to: 1, unit:""}}, dur);
        a.onComplete.subscribe( function() { tvanim.onComplete(); } );
        a.animate();
    },

    /**
     * Clean up and invoke callback
     * @method onComplete
     */
    onComplete: function() {
        this.callback();
    },

    /**
     * toString
     * @method toString
     * @return {string} the string representation of the instance
     */
    toString: function() {
        return "TVFadeIn";
    }
};
/**
 * A 1/2 second fade out animation.
 * @class TVFadeOut
 * @constructor
 * @param el {HTMLElement} the element to animate
 * @param callback {Function} function to invoke when the animation is finished
 */
YAHOO.widget.TVFadeOut = function(el, callback) {
    /**
     * The element to animate
     * @property el
     * @type HTMLElement
     */
    this.el = el;

    /**
     * the callback to invoke when the animation is complete
     * @property callback
     * @type function
     */
    this.callback = callback;

    this.logger = new YAHOO.widget.LogWriter(this.toString());
};

YAHOO.widget.TVFadeOut.prototype = {
    /**
     * Performs the animation
     * @method animate
     */
    animate: function() {
        var tvanim = this;
        var dur = 0.4;
        var a = new YAHOO.util.Anim(this.el, {opacity: {from: 1, to: 0.1, unit:""}}, dur);
        a.onComplete.subscribe( function() { tvanim.onComplete(); } );
        a.animate();
    },

    /**
     * Clean up and invoke callback
     * @method onComplete
     */
    onComplete: function() {
        var s = this.el.style;
        s.display = "none";
        s.opacity = 1;
        s.filter = "alpha(opacity=100)";
        this.callback();
    },

    /**
     * toString
     * @method toString
     * @return {string} the string representation of the instance
     */
    toString: function() {
        return "TVFadeOut";
    }
};
YAHOO.register("treeview", YAHOO.widget.TreeView, {version: "2.9.0", build: "2800"});
}())
Back to Directory File Manager