YUI recommends YUI 3.

YUI 2 has been deprecated since 2011. This site acts as an archive for files and documentation.

Yahoo! UI Library

DataTable Widget  2.5.1

Yahoo! UI Library > datatable > DataTable.js (source view)

Show Private Show Protected
/**
 * 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 connection, dragdrop
 * @title DataTable Widget
 * @beta
 */

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

/**
 * DataTable class for the YUI DataTable widget.
 *
 * @namespace YAHOO.widget
 * @class DataTable
 * @uses YAHOO.util.EventProvider
 * @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 = YAHOO.widget.DataTable,
        DS = YAHOO.util.DataSource;

    // Internal vars
    this._nIndex = DT._nCount;
    this._sId = "yui-dt"+this._nIndex;
    this._oChainRender = new YAHOO.util.Chain();
    this._oChainSync = new YAHOO.util.Chain();
    this._oChainRender.subscribe("end",this._sync, 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) {
        YAHOO.log("Could not instantiate DataTable due to an invalid RecordSet", "error", this.toString());
        return;
    }

    // Initialize node templates
    this._initNodeTemplates();

    // Initialize container element
    this._initContainerEl(elContainer);
    if(!this._elContainer) {
        YAHOO.log("Could not instantiate DataTable due to an invalid container element", "error", this.toString());
        return;
    }

    // Initialize the rest of the DOM elements
    this._initTableEl();
    if(!this._elContainer || !this._elThead || !this._elTbody) {
        YAHOO.log("Could not instantiate DataTable due to an invalid DOM elements", "error", this.toString());
        return;
    }

    // Call Element's constructor after DOM elements are created
    // but *before* table is populated with data
    DT.superclass.constructor.call(this, this._elContainer, this._oConfigs);

    // HACK: Set sortedBy values for 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;
        }
    }

    //HACK: Set the paginator values.  Attribute doesn't afford for merging
    // obj value's keys.  It's all or nothing.  Merge in provided keys.
    if(this._oConfigs.paginator && !(this._oConfigs.paginator instanceof YAHOO.widget.Paginator)) {
        // Backward compatibility
        this.updatePaginator(this._oConfigs.paginator);
    }

    // Initialize inline Cell editing
    this._initCellEditorEl();
    
    // Initialize Column sort
    this._initColumnSort();

    // Once per instance
    YAHOO.util.Event.addListener(document, "click", this._onDocumentClick, this);

    DT._nCount++;
    DT._nCurrentCount++;
    
    // Send a simple initial request
    var oCallback = {
        success : this.onDataReturnSetRows,
        failure : this.onDataReturnSetRows,
        scope   : this,
        argument: {}
    };
    if(this.get("initialLoad") === true) {
        this._oDataSource.sendRequest(this.get("initialRequest"), oCallback);
    }
    // Do not send an initial request at all
    else if(this.get("initialLoad") === false) {
        this.showTableMessage(DT.MSG_EMPTY, DT.CLASS_EMPTY);
        this._oChainRender.add({
            method: function() {
                if((this instanceof DT) && this._sId && this._bInit) {
                    this._bInit = false;
                    this.fireEvent("initEvent");
                    YAHOO.log("DataTable initialized with no rows", "info", this.toString());
                }
            },
            scope: this
        });
        this._oChainRender.run();
    }
    // Send an initial request with a custom payload
    else {
        var oCustom = this.get("initialLoad");
        oCallback.argument = oCustom.argument;
        this._oDataSource.sendRequest(oCustom.request, oCallback);
    }
};

/////////////////////////////////////////////////////////////////////////////
//
// Public constants
//
/////////////////////////////////////////////////////////////////////////////
(function () {

var lang   = YAHOO.lang,
    util   = YAHOO.util,
    widget = YAHOO.widget,
    ua     = YAHOO.env.ua,
    
    Dom    = util.Dom,
    Ev     = util.Event,
    DS     = util.DataSource,
    DT     = widget.DataTable,
    Pag    = widget.Paginator;
    

    

lang.augmentObject(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 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 proxy elements.
     *
     * @property DataTable.CLASS_RESIZERPROXY
     * @type String
     * @static
     * @final
     * @default "yui-dt-resizerproxy"
     */
    CLASS_RESIZERPROXY : "yui-dt-resizerproxy",

    /**
     * Class name assigned to Editor container elements.
     *
     * @property DataTable.CLASS_EDITOR
     * @type String
     * @static
     * @final
     * @default "yui-dt-editor"
     */
    CLASS_EDITOR : "yui-dt-editor",

    /**
     * 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 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",

    /**
     * Message to display if DataTable has no data.
     *
     * @property DataTable.MSG_EMPTY
     * @type String
     * @static
     * @final
     * @default "No records found."
     */
    MSG_EMPTY : "No records found.",

    /**
     * Message to display while DataTable is loading data.
     *
     * @property DataTable.MSG_LOADING
     * @type String
     * @static
     * @final
     * @default "Loading data..."
     */
    MSG_LOADING : "Loading data...",

    /**
     * Message to display while DataTable has data error.
     *
     * @property DataTable.MSG_ERROR
     * @type String
     * @static
     * @final
     * @default "Data error."
     */
    MSG_ERROR : "Data error.",

    /////////////////////////////////////////////////////////////////////////
    //
    // Private static variables
    //
    /////////////////////////////////////////////////////////////////////////

    /**
     * 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 STYLE node that is dynamically created and written to
     * in order to manage Column widths.
     *
     * @property DataTable._elStylesheet
     * @type HTMLElement
     * @private
     * @static     
     */
    _elStylesheet : null,

    /**
     * Set to true if _elStylesheet cannot be populated due to browser incompatibility.
     *
     * @property DataTable._bStylesheetFallback
     * @type boolean
     * @private
     * @static     
     */
    _bStylesheetFallback : (ua.ie && (ua.ie<7)) ? true : false,

    /**
     * Object literal hash of Columns and their dynamically create style rules.
     *
     * @property DataTable._oStylesheetRules
     * @type Object
     * @private
     * @static     
     */
    _oStylesheetRules : {},

    /**
     * Element reference to shared Column drag target.
     *
     * @property DataTable._elColumnDragTarget
     * @type HTMLElement
     * @private
     * @static 
     */
    _elColumnDragTarget : null,

    /**
     * Element reference to shared Column resizer proxy.
     *
     * @property DataTable._elColumnResizerProxy
     * @type HTMLElement
     * @private
     * @static 
     */
    _elColumnResizerProxy : null,

    /**
     * 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(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(o.constructor && (o.constructor == Object)) { 
            for (var x in o){
                if(lang.hasOwnProperty(o, x)) {
                    if(lang.isValue(o[x]) && (o[x].constructor == Object) || lang.isArray(o[x])) {
                        copy[x] = DT._cloneObject(o[x]);
                    }
                    else {
                        copy[x] = o[x];
                    }
                }
            }
        }
        else {
            copy = o;
        }
    
        return copy;
    },

    /**
     * Creates HTML markup for shared Column drag target.
     *
     * @method DataTable._initColumnDragTargetEl
     * @return {HTMLElement} Reference to Column drag target. 
     * @private
     * @static 
     */
    _initColumnDragTargetEl : function() {
        if(!DT._elColumnDragTarget) {
            // Attach Column drag target element as first child of body
            var elColumnDragTarget = document.createElement('div');
            elColumnDragTarget.id = "yui-dt-coltarget";
            elColumnDragTarget.className = DT.CLASS_COLTARGET;
            elColumnDragTarget.style.display = "none";
            document.body.insertBefore(elColumnDragTarget, document.body.firstChild);

            // Internal tracker of Column drag target
            DT._elColumnDragTarget = elColumnDragTarget;
            
        }
        return DT._elColumnDragTarget;
    },

    /**
     * Creates HTML markup for shared Column resizer proxy.
     *
     * @method DataTable._initColumnResizerProxyEl
     * @return {HTMLElement} Reference to Column resizer proxy.
     * @private 
     * @static 
     */
    _initColumnResizerProxyEl : function() {
        if(!DT._elColumnResizerProxy) {

            // Attach Column resizer element as first child of body
            var elColumnResizerProxy = document.createElement("div");
            elColumnResizerProxy.id = "yui-dt-colresizerproxy";
            Dom.addClass(elColumnResizerProxy, DT.CLASS_RESIZERPROXY);
            document.body.insertBefore(elColumnResizerProxy, document.body.firstChild);

            // Internal tracker of Column resizer proxy
            DT._elColumnResizerProxy = elColumnResizerProxy;
        }
        return DT._elColumnResizerProxy;
    },

    /**
     * Outputs markup into the given TH based on given Column.
     *
     * @method DataTable.formatTheadCell
     * @param elCellLabel {HTMLElement} The label DIV element within the TH liner.
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param oSelf {DT} DataTable instance.
     * @static
     */
    formatTheadCell : function(elCellLabel, oColumn, oSelf) {
        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 = oSelf.getColumnSortDir(oColumn);
            var sSortDir = (sSortClass === DT.CLASS_DESC) ? "descending" : "ascending";

            // Generate a unique HREF for visited status
            var sHref = oSelf.getId() + "-sort" + oColumn.getId() + "-" + sSortDir;
            
            // Generate a dynamic TITLE for sort status
            var sTitle = "Click to sort " + sSortDir;
            
            // 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;
        }
    },

    /**
     * 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 {Object | Boolean} Data value for the cell. By default, the value
     * is what gets written to the BUTTON.
     * @static
     */
    formatButton : function(el, oRecord, oColumn, oData) {
        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} 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}. Other forms of oData require a custom
     * formatter.
     * @static
     */
    formatCheckbox : function(el, oRecord, oColumn, oData) {
        var bChecked = oData;
        bChecked = (bChecked) ? " 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.
     * @static
     */
    formatCurrency : function(el, oRecord, oColumn, oData) {
        el.innerHTML = util.Number.format(oData, {
                prefix:"$",
                decimalPlaces:2,
                decimalSeparator:".",
                thousandsSeparator:","
            });
    },

    /**
     * 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.
     * @static
     */
    formatDate : function(el, oRecord, oColumn, oData) {
        el.innerHTML = util.Date.format(oData, {format:"MM/DD/YYYY"});
    },

    /**
     * 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.
     * @static
     */
    formatDropdown : function(el, oRecord, oColumn, oData) {
        var selectedValue = (lang.isValue(oData)) ? oData : oRecord.getData(oColumn.key);
        var options = (lang.isArray(oColumn.dropdownOptions)) ?
                oColumn.dropdownOptions : null;

        var selectEl;
        var 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");
            Dom.addClass(selectEl, DT.CLASS_DROPDOWN);
            selectEl = el.appendChild(selectEl);

            // Add event listener
            Ev.addListener(selectEl,"change",this._onDropdownChange,this);
        }

        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;
                    optionEl.innerHTML = (lang.isValue(option.text)) ?
                            option.text : 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 {Object} Data value for the cell, or null.
     * @static
     */
    formatEmail : function(el, oRecord, oColumn, oData) {
        if(lang.isString(oData)) {
            el.innerHTML = "<a href=\"mailto:" + oData + "\">" + oData + "</a>";
        }
        else {
            el.innerHTML = lang.isValue(oData) ? oData : "";
        }
    },

    /**
     * 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 {Object} Data value for the cell, or null.
     * @static
     */
    formatLink : function(el, oRecord, oColumn, oData) {
        if(lang.isString(oData)) {
            el.innerHTML = "<a href=\"" + oData + "\">" + oData + "</a>";
        }
        else {
            el.innerHTML = lang.isValue(oData) ? oData : "";
        }
    },

    /**
     * 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.
     * @static
     */
    formatNumber : function(el, oRecord, oColumn, oData) {
        if(lang.isNumber(oData)) {
            el.innerHTML = oData;
        }
        else {
            el.innerHTML = lang.isValue(oData) ? oData : "";
        }
    },

    /**
     * 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.
     * @static
     */
    formatRadio : function(el, oRecord, oColumn, oData) {
        var bChecked = oData;
        bChecked = (bChecked) ? " checked" : "";
        el.innerHTML = "<input type=\"radio\"" + bChecked +
                " name=\"col" + oColumn.getId() + "-radio\"" +
                " 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 {Object} (Optional) Data value for the cell.
     * @static
     */
    formatText : function(el, oRecord, oColumn, oData) {
        var value = (lang.isValue(oRecord.getData(oColumn.key))) ?
                oRecord.getData(oColumn.key) : "";
        //TODO: move to util function
        el.innerHTML = value.toString().replace(/&/g, "&#38;").replace(/</g, "&#60;").replace(/>/g, "&#62;");
    },

    /**
     * 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.
     * @static
     */
    formatTextarea : function(el, oRecord, oColumn, oData) {
        var value = (lang.isValue(oRecord.getData(oColumn.key))) ?
                oRecord.getData(oColumn.key) : "";
        var 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.
     * @static
     */
    formatTextbox : function(el, oRecord, oColumn, oData) {
        var value = (lang.isValue(oRecord.getData(oColumn.key))) ?
                oRecord.getData(oColumn.key) : "";
        var markup = "<input type=\"text\" value=\"" + value + "\">";
        el.innerHTML = markup;
    },

    /**
     * Handles Pag changeRequest events for static DataSources
     * (i.e. DataSources that return all data immediately)
     * @method DataTable.handleSimplePagination
     * @param {object} the requested state of the pagination
     * @param {DataTable} the DataTable instance
     * @static     
     */
    handleSimplePagination : function (oState,self) {
        // Set the core pagination values silently (the second param)
        // to avoid looping back through the changeRequest mechanism
        oState.paginator.setTotalRecords(oState.totalRecords,true);
        oState.paginator.setStartIndex(oState.recordOffset,true);
        oState.paginator.setRowsPerPage(oState.rowsPerPage,true);

        self.render();
    },

    /**
     * Handles Pag changeRequest events for dynamic DataSources
     * such as DataSource.TYPE_XHR or DataSource.TYPE_JSFUNCTION.
     * @method DataTable.handleDataSourcePagination
     * @param {object} the requested state of the pagination
     * @param {DataTable} the DataTable instance
     * @static     
     */
    handleDataSourcePagination : function (oState,self) {
        var requestedRecords = oState.records[1] - oState.recordOffset;

        // Translate the proposed page state into a DataSource request param
        var generateRequest = self.get('generateRequest');
        var request = generateRequest({ pagination : oState }, self);

        var callback = {
            success : self.onDataReturnSetRows,
            failure : self.onDataReturnSetRows,
            argument : {
                startIndex : oState.recordOffset,
                pagination : oState
            },
            scope : self
        };

        self._oDataSource.sendRequest(request, callback);
    },

    /**
     * Enables CHECKBOX Editor.
     *
     * @method DataTable.editCheckbox
     * @param oEditor {Object} Object literal representation of Editor values.
     * @param oSelf {DT} Reference back to DataTable instance.
     * @static
     */
    //DT.editCheckbox = function(elContainer, oRecord, oColumn, oEditor, oSelf) 
    editCheckbox : function(oEditor, oSelf) {
        var elCell = oEditor.cell;
        var oRecord = oEditor.record;
        var oColumn = oEditor.column;
        var elContainer = oEditor.container;
        var aCheckedValues = oEditor.value;
        if(!lang.isArray(aCheckedValues)) {
            aCheckedValues = [aCheckedValues];
        }

        // Checkboxes
        if(oColumn.editorOptions && lang.isArray(oColumn.editorOptions.checkboxOptions)) {
            var checkboxOptions = oColumn.editorOptions.checkboxOptions;
            var checkboxValue, checkboxId, elLabel, j, k;
            // First create the checkbox buttons in an IE-friendly way
            for(j=0; j<checkboxOptions.length; j++) {
                checkboxValue = lang.isValue(checkboxOptions[j].label) ?
                        checkboxOptions[j].label : checkboxOptions[j];
                checkboxId =  oSelf.getId() + "-editor-checkbox" + j;
                elContainer.innerHTML += "<input type=\"checkbox\"" +
                        " name=\"" + oSelf.getId() + "-editor-checkbox\"" +
                        " value=\"" + checkboxValue + "\"" +
                        " id=\"" +  checkboxId + "\">";
                // Then create the labels in an IE-friendly way
                elLabel = elContainer.appendChild(document.createElement("label"));
                elLabel.htmlFor = checkboxId;
                elLabel.innerHTML = checkboxValue;
            }
            var aCheckboxEls = [];
            var checkboxEl;
            // Loop through checkboxes to check them
            for(j=0; j<checkboxOptions.length; j++) {
                checkboxEl = Dom.get(oSelf.getId() + "-editor-checkbox" + j);
                aCheckboxEls.push(checkboxEl);
                for(k=0; k<aCheckedValues.length; k++) {
                    if(checkboxEl.value === aCheckedValues[k]) {
                        checkboxEl.checked = true;
                    }
                }
                // Focus the first checkbox
                if(j===0) {
                    oSelf._focusEl(checkboxEl);
                }
            }
            // Loop through checkboxes to assign click handlers
            for(j=0; j<checkboxOptions.length; j++) {
                checkboxEl = Dom.get(oSelf.getId() + "-editor-checkbox" + j);
                Ev.addListener(checkboxEl, "click", function(){
                    var aNewValues = [];
                    for(var m=0; m<aCheckboxEls.length; m++) {
                        if(aCheckboxEls[m].checked) {
                            aNewValues.push(aCheckboxEls[m].value);
                        }
                    }
                    oSelf._oCellEditor.value = aNewValues;
                    oSelf.fireEvent("editorUpdateEvent",{editor:oSelf._oCellEditor});
                });
            }
        }
    },

    /**
     * Enables Date Editor.
     *
     * @method DataTable.editDate
     * @param oEditor {Object} Object literal representation of Editor values.
     * @param oSelf {DT} Reference back to DataTable instance.
     * @static
     */
    editDate : function(oEditor, oSelf) {
        var elCell = oEditor.cell;
        var oRecord = oEditor.record;
        var oColumn = oEditor.column;
        var elContainer = oEditor.container;
        var value = oEditor.value;
        
        // Set a default
        if(!(value instanceof Date)) {
            value = oEditor.defaultValue || new Date();
        }

        // Calendar widget
        if(YAHOO.widget.Calendar) {
            var selectedValue = (value.getMonth()+1)+"/"+value.getDate()+"/"+value.getFullYear();
            var calContainer = elContainer.appendChild(document.createElement("div"));
            var calPrefix = oColumn.getColEl();
            calContainer.id = calPrefix + "-dateContainer";
            var calendar =
                    new YAHOO.widget.Calendar(calPrefix + "-date",
                    calContainer.id,
                    {selected:selectedValue, pagedate:value});
            calendar.render();
            calContainer.style.cssFloat = "none";

            if(ua.ie) {
                var calFloatClearer = elContainer.appendChild(document.createElement("br"));
                calFloatClearer.style.clear = "both";
            }

            calendar.selectEvent.subscribe(function(type, args, obj) {
                oSelf._oCellEditor.value = new Date(args[0][0][0], args[0][0][1]-1, args[0][0][2]);
                oSelf.fireEvent("editorUpdateEvent",{editor:oSelf._oCellEditor});
            });
        }
        else {
            //TODO;
        }
    },

    /**
     * Enables SELECT Editor.
     *
     * @method DataTable.editDropdown
     * @param oEditor {Object} Object literal representation of Editor values.
     * @param oSelf {DT} Reference back to DataTable instance.
     * @static
     */
    editDropdown : function(oEditor, oSelf) {
        var elCell = oEditor.cell;
        var oRecord = oEditor.record;
        var oColumn = oEditor.column;
        var elContainer = oEditor.container;
        var value = oEditor.value;
        
        // Set a default
        if(!lang.isValue(value)) {
            value = oEditor.defaultValue;
        }


        // Textbox
        var elDropdown = elContainer.appendChild(document.createElement("select"));
        var dropdownOptions = (oColumn.editorOptions && lang.isArray(oColumn.editorOptions.dropdownOptions)) ?
                oColumn.editorOptions.dropdownOptions : [];
        for(var j=0; j<dropdownOptions.length; j++) {
            var dropdownOption = dropdownOptions[j];
            var elOption = document.createElement("option");
            elOption.value = (lang.isValue(dropdownOption.value)) ?
                    dropdownOption.value : dropdownOption;
            elOption.innerHTML = (lang.isValue(dropdownOption.text)) ?
                    dropdownOption.text : dropdownOption;
            elOption = elDropdown.appendChild(elOption);
            if(value === elDropdown.options[j].value) {
                elDropdown.options[j].selected = true;
            }
        }

        // Set up a listener on each check box to track the input value
        Ev.addListener(elDropdown, "change",
            function(){
                oSelf._oCellEditor.value = elDropdown[elDropdown.selectedIndex].value;
                oSelf.fireEvent("editorUpdateEvent",{editor:oSelf._oCellEditor});
        });

        // Focus the dropdown
        oSelf._focusEl(elDropdown);
    },

    /**
     * Enables INPUT TYPE=RADIO Editor.
     *
     * @method DataTable.editRadio
     * @param oEditor {Object} Object literal representation of Editor values.
     * @param oSelf {DT} Reference back to DataTable instance.
     * @static
     */
    editRadio : function(oEditor, oSelf) {
        var elCell = oEditor.cell;
        var oRecord = oEditor.record;
        var oColumn = oEditor.column;
        var elContainer = oEditor.container;
        var value = oEditor.value;

        // Set a default
        if(!lang.isValue(value)) {
            value = oEditor.defaultValue;
        }

        // Radios
        if(oColumn.editorOptions && lang.isArray(oColumn.editorOptions.radioOptions)) {
            var radioOptions = oColumn.editorOptions.radioOptions;
            var radioValue, radioId, elLabel, j;
            // First create the radio buttons in an IE-friendly way
            for(j=0; j<radioOptions.length; j++) {
                radioValue = lang.isValue(radioOptions[j].label) ?
                        radioOptions[j].label : radioOptions[j];
                radioId =  oSelf.getId() + "-col" + oColumn.getId() + "-radioeditor" + j;
                elContainer.innerHTML += "<input type=\"radio\"" +
                        " name=\"" + oSelf.getId() + "-editor-radio\"" +
                        " value=\"" + radioValue + "\"" +
                        " id=\"" +  radioId + "\">";
                // Then create the labels in an IE-friendly way
                elLabel = elContainer.appendChild(document.createElement("label"));
                elLabel.htmlFor = radioId;
                elLabel.innerHTML = radioValue;
            }
            // Then check one, and assign click handlers
            for(j=0; j<radioOptions.length; j++) {
                var radioEl = Dom.get(oSelf.getId() + "-col" + oColumn.getId() + "-radioeditor" + j);
                if(value === radioEl.value) {
                    radioEl.checked = true;
                    oSelf._focusEl(radioEl);
                }
                Ev.addListener(radioEl, "click",
                    function(){
                        oSelf._oCellEditor.value = this.value;
                        oSelf.fireEvent("editorUpdateEvent",{editor:oSelf._oCellEditor});
                });
            }
        }
    },

    /**
     * Enables TEXTAREA Editor.
     *
     * @method DataTable.editTextarea
     * @param oEditor {Object} Object literal representation of Editor values.
     * @param oSelf {DT} Reference back to DataTable instance.
     * @static
     */
    editTextarea : function(oEditor, oSelf) {
       var elCell = oEditor.cell;
       var oRecord = oEditor.record;
       var oColumn = oEditor.column;
       var elContainer = oEditor.container;
       var value = oEditor.value;

        // Set a default
        if(!lang.isValue(value)) {
            value = oEditor.defaultValue || "";
        }

        // Textarea
        var elTextarea = elContainer.appendChild(document.createElement("textarea"));
        elTextarea.style.width = elCell.offsetWidth + "px"; //(parseInt(elCell.offsetWidth,10)) + "px";
        elTextarea.style.height = "3em"; //(parseInt(elCell.offsetHeight,10)) + "px";
        elTextarea.value = value;

        // Set up a listener on each check box to track the input value
        Ev.addListener(elTextarea, "keyup", function(){
            //TODO: set on a timeout
            oSelf._oCellEditor.value = elTextarea.value;
            oSelf.fireEvent("editorUpdateEvent",{editor:oSelf._oCellEditor});
        });

        // Select the text
        elTextarea.focus();
        elTextarea.select();
    },

    /**
     * Enables INPUT TYPE=TEXT Editor.
     *
     * @method DataTable.editTextbox
     * @param oEditor {Object} Object literal representation of Editor values.
     * @param oSelf {DT} Reference back to DataTable instance.
     * @static
     */
    editTextbox : function(oEditor, oSelf) {
       var elCell = oEditor.cell;
       var oRecord = oEditor.record;
       var oColumn = oEditor.column;
       var elContainer = oEditor.container;
       var value = oEditor.value;

        // Set a default
        if(!lang.isValue(value)) {
            value = oEditor.defaultValue || "";
        }

        // Textbox
        var elTextbox;
        // Bug 1802582: SF3/Mac needs a form element wrapping the input
        if(ua.webkit>420) {
            elTextbox = elContainer.appendChild(document.createElement("form")).appendChild(document.createElement("input"));
        }
        else {
            elTextbox = elContainer.appendChild(document.createElement("input"));
        }
        elTextbox.type = "text";
        elTextbox.style.width = elCell.offsetWidth + "px"; //(parseInt(elCell.offsetWidth,10)) + "px";
        //elTextbox.style.height = "1em"; //(parseInt(elCell.offsetHeight,10)) + "px";
        elTextbox.value = value;

        // 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){
            // Prevent form submit
            // Save on "enter"
            if((v.keyCode === 13)) {
                YAHOO.util.Event.preventDefault(v);
                oSelf.saveCellEditor();
            }
        });

        // Set up a listener on each textbox to track the input value
        Ev.addListener(elTextbox, "keyup", function(v){
            // Update the tracker value
            oSelf._oCellEditor.value = elTextbox.value;
            oSelf.fireEvent("editorUpdateEvent",{editor:oSelf._oCellEditor});
        });

        // Select the text
        elTextbox.focus();
        elTextbox.select();
    },

    /**
     * Validates Editor input value to type Number, doing type conversion as
     * necessary. A valid Number value is return, else the previous value 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 null;
        }
    },

    /**
     * Translates (proposed) DataTable state data into a form consumable by
     * DataSource sendRequest as the request parameter.  Use
     * set('generateRequest', yourFunc) to use a custom function rather than this
     * one.
     * @method DataTable._generateRequest
     * @param oData {Object} Object literal defining the current or proposed state
     * @param oDataTable {DataTable} Reference to the DataTable instance
     * @returns {MIXED} Returns appropriate value based on DataSource type
     * @private
     * @static     
     */
    _generateRequest : function (oData, oDataTable) {
        var request = oData;

        if (oData.pagination) {
            if (oDataTable._oDataSource.dataType === DS.TYPE_XHR) {
                request = '?page=' +         oData.pagination.page +
                          '&recordOffset=' + oData.pagination.recordOffset +
                          '&rowsPerPage=' +  oData.pagination.rowsPerPage;
            }
        }
        
        return request;
    }
});

// Do in separate step so referenced properties are available
// TODO: editor shortcuts
/**
 * Cell formatting functions.
 * @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
};

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 Value for the SUMMARY attribute.
    * @type String
    */
    this.setAttributeConfig("summary", {
        value: null,
        validator: lang.isString,
        method: function(sSummary) {
            this._elThead.parentNode.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 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 generateRequest
     * @description A function used to translate proposed DataTable state info
     * into a value which is then passed to the DataSource's sendRequest method.
     * This function is called to get the DataTable's initial data as well as
     * any data changes or requests such as pagination or sorting.  The method
     * is passed two params, an object literal with the state data and a
     * reference to the DataTable.
     * @type function
     * @default DT._generateRequest
     */
    this.setAttributeConfig("generateRequest", {
        value: DT._generateRequest,
        validator: lang.isFunction
    });

    /**
    * @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 DT.CLASS_ASC or DT.CLASS_DESC</dd>
    * </dl>
    * @type Object | null
    */
    this.setAttributeConfig("sortedBy", {
        value: null,
        // TODO: accepted array for nested sorts
        validator: function(oNewSortedBy) {
            if(oNewSortedBy) {
                return ((oNewSortedBy.constructor == Object) && oNewSortedBy.key);
            }
            else {
                return (oNewSortedBy === null);
            }
        },
        method: function(oNewSortedBy) {
            // Remove ASC/DESC from TH
            var oOldSortedBy = this.get("sortedBy");
            if(oOldSortedBy && (oOldSortedBy.constructor == Object) && oOldSortedBy.key) {
                var oldColumn = this._oColumnSet.getColumn(oOldSortedBy.key);
                var oldThEl = this.getThEl(oldColumn);
                Dom.removeClass(oldThEl, DT.CLASS_ASC);
                Dom.removeClass(oldThEl, DT.CLASS_DESC);
            }

            // Set ASC/DESC on TH
            if(oNewSortedBy) {
                var column = (oNewSortedBy.column) ? oNewSortedBy.column : this._oColumnSet.getColumn(oNewSortedBy.key);
                if(column) {
                    // Backward compatibility
                    if(oNewSortedBy.dir && ((oNewSortedBy.dir == "asc") ||  (oNewSortedBy.dir == "desc"))) {
                        var newClass = (oNewSortedBy.dir == "desc") ?
                                DT.CLASS_DESC :
                                DT.CLASS_ASC;
                        Dom.addClass(column.getThEl(), newClass);
                    }
                    else {
                         var sortClass = oNewSortedBy.dir || DT.CLASS_ASC;
                         Dom.addClass(column.getThEl(), sortClass);
                    }
                }
            }
        }
    });
    
    /**
    * @attribute paginator
    * @description Stores an instance of Pag, or (for
    * backward compatibility), an object literal of pagination values in the
    * following form:<br>
    *   { containers:[], // UI container elements <br>
    *   rowsPerPage:500, // 500 rows <br>
    *   currentPage:1,  // page one <br>
    *   pageLinks:0,    // show all links <br>
    *   pageLinksStart:1, // first link is page 1 <br>
    *   dropdownOptions:null, // no dropdown <br>
    *   links: [], // links elements <br>
    *   dropdowns: [] } //dropdown elements
    *
    * @default null
    * @type {Object|YAHOO.widget.Paginator}
    */
    this.setAttributeConfig("paginator", {
        value : { // Backward compatibility
            rowsPerPage:500, // 500 rows per page
            currentPage:1,  // show page one
            startRecordIndex:0, // start with first Record
            totalRecords:0, // how many Records total
            totalPages:0, // how many pages total
            rowsThisPage:0, // how many rows this page
            pageLinks:0,    // show all links
            pageLinksStart:1, // first link is page 1
            dropdownOptions: null, //no dropdown
            containers:[], // Paginator container element references
            dropdowns: [], //dropdown element references,
            links: [] // links elements
        },
        validator : function (oNewPaginator) {
            if (typeof oNewPaginator === 'object' && oNewPaginator) {
                if (oNewPaginator instanceof Pag) {
                    return true;
                }
                else {
                    // Backward compatibility
                    if(oNewPaginator && (oNewPaginator.constructor == Object)) {
                        // Check for incomplete set of values
                        if((oNewPaginator.rowsPerPage !== undefined) &&
                                (oNewPaginator.currentPage !== undefined) &&
                                (oNewPaginator.startRecordIndex !== undefined) &&
                                (oNewPaginator.totalRecords !== undefined) &&
                                (oNewPaginator.totalPages !== undefined) &&
                                (oNewPaginator.rowsThisPage !== undefined) &&
                                (oNewPaginator.pageLinks !== undefined) &&
                                (oNewPaginator.pageLinksStart !== undefined) &&
                                (oNewPaginator.dropdownOptions !== undefined) &&
                                (oNewPaginator.containers !== undefined) &&
                                (oNewPaginator.dropdowns !== undefined) &&
                                (oNewPaginator.links !== undefined)) {

                            // Validate each value
                            if(lang.isNumber(oNewPaginator.rowsPerPage) &&
                                    lang.isNumber(oNewPaginator.currentPage) &&
                                    lang.isNumber(oNewPaginator.startRecordIndex) &&
                                    lang.isNumber(oNewPaginator.totalRecords) &&
                                    lang.isNumber(oNewPaginator.totalPages) &&
                                    lang.isNumber(oNewPaginator.rowsThisPage) &&
                                    lang.isNumber(oNewPaginator.pageLinks) &&
                                    lang.isNumber(oNewPaginator.pageLinksStart) &&
                                    (lang.isArray(oNewPaginator.dropdownOptions) || lang.isNull(oNewPaginator.dropdownOptions)) &&
                                    lang.isArray(oNewPaginator.containers) &&
                                    lang.isArray(oNewPaginator.dropdowns) &&
                                    lang.isArray(oNewPaginator.links)) {
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        },
        method : function (oNewPaginator) {
            // Hook into the pagintor's change event
            if (oNewPaginator instanceof Pag) {
                oNewPaginator.subscribe('changeRequest', this.onPaginatorChange, this, true);

                // If the paginator has no configured containers, add some
                var containers = oNewPaginator.getContainerNodes();
                if (!containers.length) {
                    // Build the container nodes
                    var c_above = document.createElement('div');
                    c_above.id = this._sId + "-paginator0";
                    this._elContainer.insertBefore(c_above,this._elContainer.firstChild);

                    // ...and one below the table
                    var c_below = document.createElement('div');
                    c_below.id = this._sId + "-paginator1";
                    this._elContainer.appendChild(c_below);

                    containers = [c_above, c_below];
                    Dom.addClass(containers,
                                DT.CLASS_PAGINATOR);

                    oNewPaginator.set('containers',containers);
                }
            }
        }
    });

    /**
    * @attribute paginated
    * @deprecated No longer used, as long as "paginator" value is an instance of
    * Paginator class.  
    */
    this.setAttributeConfig("paginated", {
        value: false,
        validator: lang.isBoolean,
        method : function (on) {
            var curVal = this.get('paginated');
            var i,len;
            if (on == curVal) {
                return;
            }

            var oPaginator  = this.get('paginator');
            if (!(oPaginator instanceof Pag)) {
                // Backward compatibility--pagination generated here
                oPaginator = oPaginator || {
                    rowsPerPage     : 500,  // 500 rows per page
                    currentPage     : 1,    // show page one
                    startRecordIndex: 0,    // start with first Record
                    totalRecords    : 0,    // how many Records total
                    totalPages      : 0,    // how many pages total
                    rowsThisPage    : 0,    // how many rows this page
                    pageLinks       : 0,    // show all links
                    pageLinksStart  : 1,    // first link is page 1
                    dropdownOptions : null, // no dropdown
                    containers      : [],   // Paginator container element references
                    dropdowns       : [],   // dropdown element references,
                    links           : []    // links elements
                };
                var aContainerEls = oPaginator.containers;

                // Paginator is enabled
                if(on) {
                    // No containers found, create two from scratch
                    if(aContainerEls.length === 0) {
                        // One before TABLE
                        var pag0 = document.createElement("span");
                        pag0.id = this._sId + "-paginator0";
                        Dom.addClass(pag0, DT.CLASS_PAGINATOR);
                        pag0 = this._elContainer.insertBefore(pag0, this._elContainer.firstChild);
                        aContainerEls.push(pag0);

                        // One after TABLE
                        var pag1 = document.createElement("span");
                        pag1.id = this._sId + "-paginator1";
                        Dom.addClass(pag1, DT.CLASS_PAGINATOR);
                        pag1 = this._elContainer.appendChild(pag1);
                        aContainerEls.push(pag1);

                        // (re)set the paginator value directly
                        oPaginator.containers = aContainerEls;
                        this._configs.paginator.value= oPaginator;
                    }
                    else {
                        // Show each container
                        for(i=0; i<aContainerEls.length; i++) {
                            aContainerEls[i].style.display = "";
                        }
                    }

                    // Links are enabled
                    if(oPaginator.pageLinks > -1) {
                        var aLinkEls = oPaginator.links;
                        // No links containers found, create from scratch
                        if(aLinkEls.length === 0) {
                            for(i=0; i<aContainerEls.length; i++) {
                                // Create one links container per Paginator container
                                var linkEl = document.createElement("span");
                                linkEl.id = "yui-dt-pagselect"+i;
                                linkEl = aContainerEls[i].appendChild(linkEl);

                                // Add event listener
                                //TODO: anon fnc
                                Ev.addListener(linkEl,"click",this._onPaginatorLinkClick,this);

                                 // Add directly to tracker
                                this._configs.paginator.value.links.push(linkEl);
                           }
                       }
                    }

                    for(i=0; i<aContainerEls.length; i++) {
                        // Create one SELECT element per Paginator container
                        var selectEl = document.createElement("select");
                        Dom.addClass(selectEl, DT.CLASS_DROPDOWN);
                        selectEl = aContainerEls[i].appendChild(selectEl);
                        selectEl.id = "yui-dt-pagselect"+i;

                        // Add event listener
                        //TODO: anon fnc
                        Ev.addListener(selectEl,"change",this._onPaginatorDropdownChange,this);

                        // Add DOM reference directly to tracker
                       this._configs.paginator.value.dropdowns.push(selectEl);

                        // Hide dropdown
                        if(!oPaginator.dropdownOptions) {
                            selectEl.style.display = "none";
                        }
                    }

                    //TODO: fire paginatorDisabledEvent & add to api doc
                    YAHOO.log("Paginator enabled", "info", this.toString());
                }
                // Pagination is disabled
                else {
                    // Containers found
                    if(aContainerEls.length > 0) {
                        // Destroy or just hide?

                        // Hide each container
                        for(i=0; i<aContainerEls.length; i++) {
                            aContainerEls[i].style.display = "none";
                        }

                        /*TODO?
                        // Destroy each container
                        for(i=0; i<aContainerEls.length; i++) {
                            Ev.purgeElement(aContainerEls[i], true);
                            aContainerEls.innerHTML = null;
                            //TODO: remove container?
                            // aContainerEls[i].parentNode.removeChild(aContainerEls[i]);
                        }
                        */
                    }
                    //TODO: fire paginatorDisabledEvent & add to api doc
                    YAHOO.log("Paginator disabled", "info", this.toString());
                }
            }
        }
    });

    /**
     * @attribute paginationEventHandler
     * @description For use with Pag pagination.  A
     * handler function that receives the requestChange event from the
     * configured paginator.  The handler method will be passed these
     * parameters:
     * <ol>
     * <li>oState {Object} - an object literal describing the requested
     * pagination state</li>
     * <li>oSelf {DataTable} - The DataTable instance.</li>
     * </ol>
     * 
     * For pagination through dynamic or server side data, assign
     * DT.handleDataSourcePagination or your own custom
     * handler.
     * @type {function|Object}
     * @default DT.handleSimplePagination
     */
    this.setAttributeConfig("paginationEventHandler", {
        value     : DT.handleSimplePagination,
        validator : lang.isObject
    });

    /**
    * @attribute caption
    * @description Value for the CAPTION element.
    * @type String
    */
    this.setAttributeConfig("caption", {
        value: null,
        validator: lang.isString,
        method: function(sCaption) {
            // Create CAPTION element
            if(!this._elCaption) {
                var bodyTable = this._elTbodyContainer.getElementsByTagName('table')[0];
                
                this._elCaption = bodyTable.createCaption();
            }
            // Set CAPTION value
            this._elCaption.innerHTML = sCaption;
        }
    });

    /**
    * @attribute scrollable
    * @description True if primary TBODY should scroll.
    * @default false
    * @type Boolean
    */
    this.setAttributeConfig("scrollable", {
        value: false,
        validator: function(oParam) {
            return (lang.isBoolean(oParam));
        },
        method: function(oParam) {
            var headTable = this._elTheadContainer.getElementsByTagName('table')[0],
                bodyTable = this._elTbodyContainer.getElementsByTagName('table')[0],
                headThead = headTable.getElementsByTagName('thead')[0],
                bodyThead = bodyTable.getElementsByTagName('thead')[0];

            if(oParam) {
                if (headThead) {
                    headTable.removeChild(headThead);
                }
                if (bodyThead) {
                    bodyTable.removeChild(bodyThead);
                }
                headTable.appendChild(this._elThead);
                bodyTable.insertBefore(this._elA11yThead,bodyTable.firstChild || null);

                // Move the caption from the body table to the head table
                // if there is a caption
                if (bodyTable.caption) {
                    headTable.insertBefore(bodyTable.caption,headTable.firstChild);
                }

                Dom.addClass(this._elContainer,DT.CLASS_SCROLLABLE);

                // Bug 1716354 - fix gap in Safari 2 and 3 (also seen in
                // other browsers)
                bodyTable.style.marginTop = "-"+this._elTbody.offsetTop+"px";

                this._syncColWidths();
                this._syncScrollPadding();
            }
            else {
                if (headThead) {
                    headTable.removeChild(headThead);
                }
                if (bodyThead) {
                    bodyTable.removeChild(bodyThead);
                }
                headTable.appendChild(this._elA11yThead);
                bodyTable.insertBefore(this._elThead,bodyTable.firstChild || null);
                bodyTable.style.marginTop = '';

                // Move the caption from the head table to the body table
                // if there is a caption
                if (headTable.caption) {
                    bodyTable.insertBefore(headTable.caption,bodyTable.firstChild);
                }

                Dom.removeClass(this._elContainer,DT.CLASS_SCROLLABLE);
            }
        }
    });

    /**
    * @attribute width
    * @description Table width for scrollable tables
    * @type String
    */
    this.setAttributeConfig("width", {
        value: null,
        validator: lang.isString,
        method: function(oParam) {
            if(this.get("scrollable")) {
                this._elTheadContainer.style.width = oParam;
                this._elTbodyContainer.style.width = oParam;            
            }
        }
    });

    /**
    * @attribute height
    * @description Table height for scrollable tables
    * @type String
    */
    this.setAttributeConfig("height", {
        value: null,
        validator: lang.isString,
        method: function(oParam) {
            if(this.get("scrollable")) {
                this._elTbodyContainer.style.height = oParam;  
            }          
        }
    });

    /**
    * @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,
        writeOnce: true
    });

    /**
     * @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 	 
     }); 	 
},

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

/**
 * True if instance is initialized, so as to fire the initEvent rather than
 * renderEvent.
 *
 * @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,

/**
 * Sync chain.
 *
 * @property _oChainSync
 * @type YAHOO.util.Chain
 * @private
 */
_oChainSync : 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 _aFallbackColResizer
 * @type Array
 * @private
 */
_aFallbackColResizer : [],

/**
 * 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 container element for the DataTable's primary THEAD.
 *
 * @property _elTheadContainer
 * @type HTMLElement
 * @private
 */
_elTheadContainer : null,

/**
 * DOM reference to the container element for the DataTable's primary TBODY.
 *
 * @property _elTbodyContainer
 * @type HTMLElement
 * @private
 */
_elTbodyContainer : null,

/**
 * DOM reference to the CAPTION element for the DataTable instance.
 *
 * @property _elCaption
 * @type HTMLElement
 * @private
 */
_elCaption : null,

/**
 * DOM reference to the primary 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 _elMsgTbodyRow
 * @type HTMLElement
 * @private
 */
_elMsgTbodyRow : null,

/**
 * DOM reference to the secondary TBODY element's single TD element used to display DataTable messages.
 *
 * @property _elMsgTbodyCell
 * @type HTMLElement
 * @private
 */
_elMsgTbodyCell : 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,

/**
 * 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 cell to create all new cells from.
 * @property _tdElTemplate
 * @type {HTMLElement}
 * @private 
 */
_tdElTemplate : null,

/**
 * Template row to create all new rows from.
 * @property _trElTemplate
 * @type {HTMLElement}
 * @private 
 */
_trElTemplate : null,

/**
 * True if x-scrollbar is currently visible.
 * @property _bScrollbarX
 * @type {Boolean}
 * @private 
 */
_bScrollbarX : null,



























/////////////////////////////////////////////////////////////////////////////
//
// 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);
},

/**
 * Post render syncing of Column widths and scroll padding
 *
 * @method _sync
 * @private
 */
_sync : function() {
    this._syncColWidths();
    this._forceGeckoRedraw();
},

/**
 * Syncs up widths of THs and TDs across all those Columns without width values.
 * Actual adjustment is to the liner DIVs so window resizing will not affect cells. 
 *
 * @method _syncColWidths
 * @private
 */
_syncColWidths : function() {
    if(!this.get('scrollable')) {
        return;
    }

    if(this._elTbody.rows.length > 0) {
        // Validate there is at least one row with cells and at least one Column
        var allKeys = this._oColumnSet.keys,
            elRow = this.getFirstTrEl();
    
        if(allKeys && elRow && (elRow.cells.length === allKeys.length)) {
            // Temporarily unsnap container since it causes inaccurate calculations
            var bUnsnap = false;
            if((YAHOO.env.ua.gecko || YAHOO.env.ua.opera) && this.get("scrollable")) {
                bUnsnap = true;
                if(this.get("width")) {
                    this._elTheadContainer.style.width = "";
                    this._elTbodyContainer.style.width = "";
                }
                else {
                    this._elContainer.style.width = "";
                }
            }
    
            var i,
                oColumn,
                cellsLen = elRow.cells.length;
            // First time through, reset the widths to get an accurate measure of the TD
            for(i=0; i<cellsLen; i++) {
                oColumn = allKeys[i];
                // Only for Columns without widths
                if(!oColumn.width) {
                    this._setColumnWidth(oColumn, "auto");
                }
            }
    
            // Calculate width for every Column
            for(i=0; i<cellsLen; i++) {
                oColumn = allKeys[i];
                var newWidth;
                
                // Columns without widths
                if(!oColumn.width) {
                    var elTh = oColumn.getThEl();
                    var elTd = elRow.cells[i];
                    
                    if(elTh.offsetWidth !== elTd.offsetWidth) {
                        var elWider = (elTh.offsetWidth > elTd.offsetWidth) ? elTh.firstChild : elTd.firstChild;               
                        // Calculate the final width by comparing liner widths
                        newWidth = elWider.offsetWidth -
                                (parseInt(Dom.getStyle(elWider,"paddingLeft"),10)|0) -
                                (parseInt(Dom.getStyle(elWider,"paddingRight"),10)|0);
                        
                        // Validate against minWidth        
                        newWidth = (oColumn.minWidth && (oColumn.minWidth > newWidth)) ?
                                oColumn.minWidth : newWidth;
                
                    }
                }
                // Columns with widths
                else {
                    newWidth = oColumn.width;
                }
                
                // Hidden Columns
                if(oColumn.hidden) {
                    oColumn._nLastWidth = newWidth;
                    newWidth = 1;
                }
                
                // Update to the new width
                if(newWidth) {
                    this._setColumnWidth(oColumn, newWidth+"px"); 
                }
            }
            
            // Resnap unsnapped containers
            if(bUnsnap) {
                var sWidth = this.get("width");
                this._elTheadContainer.style.width = sWidth;
                this._elTbodyContainer.style.width = sWidth;     
            } 
        }
    }

    this._syncScrollPadding();
},

/**
 * Syncs padding around scrollable tables, including Column header right-padding
 * and container width and height.
 *
 * @method _syncScrollPadding
 * @private
 */
_syncScrollPadding : function() {
    // Proceed only if scrollable is enabled
    if(this.get("scrollable")) {
        var elTbody = this._elTbody,
            elTbodyContainer = this._elTbodyContainer,
            aLastHeaders, len, prefix, i, elLiner;
        
        // IE 6 and 7 only when y-scrolling not enabled
        if(!this.get("height") && (ua.ie)) {
            // Snap outer container height to content
            // but account for x-scrollbar if it is visible
            if(elTbody.rows.length > 0) {
                elTbodyContainer.style.height = 
                        (elTbodyContainer.scrollWidth > elTbodyContainer.offsetWidth) ?
                        (elTbody.offsetHeight + 19) + "px" : 
                        elTbody.offsetHeight + "px";
            }
            else {
                elTbodyContainer.style.height = 
                        (elTbodyContainer.scrollWidth > elTbodyContainer.offsetWidth) ?
                        (this._elMsgTbody.offsetHeight + 19) + "px" : 
                        this._elMsgTbody.offsetHeight + "px";
            }
        }

        // X-scrolling not enabled
        if(!this.get("width")) {
            // Snap outer container width to content
            // but account for y-scrollbar if it is visible
            this._elContainer.style.width = 
                    (elTbodyContainer.scrollHeight > elTbodyContainer.offsetHeight) ?
                    (elTbody.parentNode.offsetWidth + 19) + "px" :
                    //TODO: Can we detect left and right border widths instead of hard coding?
                    (elTbody.parentNode.offsetWidth + 2) + "px";
        }
        // X-scrolling is enabled and x-scrollbar is visible
        else if((elTbodyContainer.scrollWidth > elTbodyContainer.offsetWidth) ||
            ((elTbodyContainer.scrollHeight > elTbodyContainer.offsetHeight) && (elTbodyContainer.scrollWidth > elTbodyContainer.offsetWidth-16))) {
            // Perform sync routine
            if(!this._bScrollbarX) {
                // Add Column header right-padding
                aLastHeaders = this._oColumnSet.headers[this._oColumnSet.headers.length-1];
                len = aLastHeaders.length;
                prefix = this._sId+"-th";
                for(i=0; i<len; i++) {
                    //TODO: A better way to get th cell
                    elLiner = Dom.get(prefix+aLastHeaders[i]).firstChild;
                    elLiner.style.marginRight = 
                            (parseInt(Dom.getStyle(elLiner,"marginRight"),10) + 
                            16) + "px";
                }
                
                // Save state   
                this._bScrollbarX = true;
            }
        }
        // X-scrollbar enabled but x-scrollbar is not visible
        else {
            // Perform sync routine
            if(this._bScrollbarX) {                 
                // Remove Column header right-padding                   
                aLastHeaders = this._oColumnSet.headers[this._oColumnSet.headers.length-1];
                len = aLastHeaders.length;
                prefix = this._sId+"-th";
                for(i=0; i<len; i++) {
                    //TODO: A better way to get th cell
                    elLiner = Dom.get(prefix+aLastHeaders[i]).firstChild;
                    Dom.setStyle(elLiner,"marginRight","");
                }
                                        
                // Save state
                this._bScrollbarX = false;
            }
        }
    
        // Sync message tbody
        if(this._elTbody.rows.length === 0) {
            this._elMsgTbody.parentNode.width = this.getTheadEl().parentNode.offsetWidth;
        }
        else {
            this._elMsgTbody.parentNode.width = "";
        }
    }
},

/**
 * Forces Gecko repaint by removing/adding the no-op class name
 *
 * @method _forceGeckoRedraw
 * @private
 */
_forceGeckoRedraw : function() {
    // Bug 1741322: Needed to force FF to redraw to fix squishy headers on wide tables when new content comes in
    if(ua.gecko) {
        var elContainer = this.getContainerEl();
        setTimeout(function() {
            Dom.removeClass(elContainer,"yui-dt-noop");
        },0);
        setTimeout(function() {
            Dom.addClass(elContainer,"yui-dt-noop");
        },0);
    }
},





















// INIT FUNCTIONS

/**
 * Initializes the HTMLElement templates used to create various table child
 * nodes.
 * @method _initNodeTemplates
 * @private
 */
_initNodeTemplates : function () {
    var d   = document,
        tr  = d.createElement('tr'),
        td  = d.createElement('td'),
        div = d.createElement('div');

    // Append the liner element
    td.appendChild(div);

    this._tdElTemplate = td;
    this._trElTemplate = tr;
},

/**
 * Initializes the DataTable container element.
 *
 * @method _initContainerEl
 * @param elContainer {HTMLElement | String} HTML DIV element by reference or ID.
 * @private
 */
_initContainerEl : function(elContainer) {
    // Clear any previous container
    if(this._elContainer) {
        Ev.purgeElement(this._elContainer, true);
        this._elContainer.innerHTML = "";
    }

    elContainer = Dom.get(elContainer);
    if(elContainer && elContainer.nodeName && (elContainer.nodeName.toLowerCase() == "div")) {
        // Esp for progressive enhancement
        Ev.purgeElement(elContainer, true);
        elContainer.innerHTML = "";

        Dom.addClass(elContainer,"yui-dt yui-dt-noop");
        
        // Container for header TABLE
        this._elTheadContainer = elContainer.appendChild(document.createElement("div"));
        Dom.addClass(this._elTheadContainer, "yui-dt-hd");

        // Container for body TABLE
        this._elTbodyContainer = elContainer.appendChild(document.createElement("div"));
        Dom.addClass(this._elTbodyContainer, "yui-dt-bd");

        this._elContainer = elContainer;
    }
},

/**
 * Initializes object literal of config values.
 *
 * @method _initConfigs
 * @param oConfig {Object} Object literal of config values.
 * @private
 */
_initConfigs : function(oConfigs) {
    if(oConfigs) {
        if(oConfigs.constructor != Object) {
            oConfigs = null;
            YAHOO.log("Invalid configs", "warn", this.toString());
        }
        // Backward compatibility
        else if(lang.isBoolean(oConfigs.paginator)) {
            YAHOO.log("DataTable's paginator model has been revised" +
            " -- please refer to the documentation for implementation" +
            " details", "warn", this.toString());
        }
        this._oConfigs = oConfigs;
    }
    else {
        this._oConfigs = {};
    }
},

/**
 * Initializes ColumnSet.
 *
 * @method _initColumnSet
 * @param aColumnDefs {Object[]} Array of object literal Column definitions.
 * @private
 */
_initColumnSet : function(aColumnDefs) {
    if(this._oColumnSet) {
        // First clear _oStylesheetRules for existing ColumnSet
        for(var i=0, l=this._oColumnSet.keys.length; i<l; i++) {
            DT._oStylesheetRules[".yui-dt-col-"+this._oColumnSet.keys[i].getId()] = undefined;
        }
        
        this._oColumnSet = null;
    }
    
    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());
    }


},

/**
 * Initializes DataSource.
 *
 * @method _initDataSource
 * @param oDataSource {YAHOO.util.DataSource} DataSource instance.
 * @private
 */
_initDataSource : function(oDataSource) {
    this._oDataSource = null;
    if(oDataSource && (oDataSource instanceof DS)) {
        this._oDataSource = oDataSource;
    }
    // Backward compatibility
    else {
        var tmpTable = null;
        var tmpContainer = this._elContainer;
        var i;
        // 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=0; 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();
    }
},

/**
 * Creates HTML markup for TABLE, THEAD and TBODY elements.
 *
 * @method _initTableEl
 * @private
 */
_initTableEl : function() {
    var elTable;

    // Destroy existing
    if(this._elThead) {
        var i;
        // Destroy ColumnDDs
        var aTree = this._oColumnSet.tree[0];
        for(i=0; i<aTree.length; i++) {
            if(aTree[i]._dd) {
                aTree[i]._dd = aTree[i]._dd.unreg();
            }
        }
    
        // Destroy ColumnResizers
        var aKeys = this._oColumnSet.keys;
        for(i=0; i<aKeys.length; i++) {
            if(aKeys[i]._ddResizer) {
                aKeys[i]._ddResizer = aKeys[i]._ddResizer.unreg();
            }
        }
        elTable = this._elThead.parentNode;
        Ev.purgeElement(elTable, true);
        elTable.parentNode.removeChild(elTable);
        this._elThead = null;
    }
    if(this._elTbody) {
        elTable = this._elTbody.parentNode;
        Ev.purgeElement(elTable, true);
        elTable.parentNode.removeChild(elTable);
        this._elTbody = null;
    }

    // Create elements for header
    // Create TABLE
    var elHeadTable = document.createElement("table");
    elHeadTable.id = this._sId + "-headtable";
    elHeadTable = this._elTheadContainer.appendChild(elHeadTable);

    // Create elements for body
    // Create TABLE
    var elBodyTable = document.createElement("table");
    elBodyTable.id = this._sId + "-bodytable";
    this._elTbodyContainer.appendChild(elBodyTable);

    // Create THEAD for display and for a11y
    this._initTheadEls();

    // Create TBODY for data
    this._elTbody = elBodyTable.appendChild(document.createElement("tbody"));
    this._elTbody.tabIndex = 0;
    Dom.addClass(this._elTbody,DT.CLASS_BODY);

    // Create TBODY for messages
    var elMsgTbody = document.createElement("tbody");
    var elMsgRow = elMsgTbody.appendChild(document.createElement("tr"));
    Dom.addClass(elMsgRow,DT.CLASS_FIRST);
    Dom.addClass(elMsgRow,DT.CLASS_LAST);
    this._elMsgRow = elMsgRow;
    var elMsgCell = elMsgRow.appendChild(document.createElement("td"));
    elMsgCell.colSpan = this._oColumnSet.keys.length;
    Dom.addClass(elMsgCell,DT.CLASS_FIRST);
    Dom.addClass(elMsgCell,DT.CLASS_LAST);
    this._elMsgTd = elMsgCell;
    this._elMsgTbody = elBodyTable.appendChild(elMsgTbody);
    var elMsgCellLiner = elMsgCell.appendChild(document.createElement("div"));
    Dom.addClass(elMsgCellLiner,DT.CLASS_LINER);
    this.showTableMessage(DT.MSG_LOADING, DT.CLASS_LOADING);

    var elContainer = this._elContainer;
    var elThead = this._elThead;
    var elTbody = this._elTbody;
    
    // IE puts focus outline in the wrong place
    if(ua.ie) {
        elTbody.hideFocus=true;
    }
    var elTbodyContainer = this._elTbodyContainer;

    // Set up DOM events
    Ev.addListener(elContainer, "focus", this._onTableFocus, this);
    Ev.addListener(elTbody, "focus", this._onTbodyFocus, this);

    Ev.addListener(elTbody, "mouseover", this._onTableMouseover, this);
    Ev.addListener(elTbody, "mouseout", this._onTableMouseout, this);
    Ev.addListener(elTbody, "mousedown", this._onTableMousedown, this);

    Ev.addListener(elTbody, "keydown", this._onTbodyKeydown, this);

    Ev.addListener(elTbody, "keypress", this._onTableKeypress, this);

    // Since we can't listen for click and dblclick on the same element...
    Ev.addListener(elTbody.parentNode, "dblclick", this._onTableDblclick, this);
    Ev.addListener(elTbody, "click", this._onTbodyClick, this);

    Ev.addListener(elTbodyContainer, "scroll", this._onScroll, this); // to sync horiz scroll headers
},

/**
 * Initializes THEAD elements for display and for screen readers.
 *
 * @method _initTheadEls
 * @private
 */
_initTheadEls : function() {
    var i,j, l, elThead, elA11yThead, aTheads;
    
    // First time through
    if(!this._elThead) {
        // Create THEADs
        elThead     = this._elThead     = document.createElement('thead');
        elA11yThead = this._elA11yThead = document.createElement('thead');
        
        aTheads = [elThead, elA11yThead];

        Ev.addListener(elThead, "focus", this._onTheadFocus, this);
        Ev.addListener(elThead, "keydown", this._onTheadKeydown, this);
        Ev.addListener(elThead, "mouseover", this._onTableMouseover, this);
        Ev.addListener(elThead, "mouseout", this._onTableMouseout, this);
        Ev.addListener(elThead, "mousedown", this._onTableMousedown, this);
        Ev.addListener(elThead, "mouseup", this._onTableMouseup, this);
        Ev.addListener(elThead, "click", this._onTheadClick, this);
        Ev.addListener(elThead.parentNode, "dblclick", this._onTableDblclick, this);
        
        // Add the accessibility-only thead to the header table by default.
        // The theads will be swapped for scrollable DataTables, the display
        // thead fixed in place, and the a11y thead hidden
        this._elTheadContainer.firstChild.appendChild(elA11yThead);
        this._elTbodyContainer.firstChild.appendChild(elThead);
    }
    // Reinitialization
    else {
        // Clear rows from THEADs
        elThead = this._elThead;
        elA11yThead = this._elA11yThead;
        aTheads = [elThead, elA11yThead];
            
        for(i=0; i<aTheads.length; i++) {
            for(j=aTheads[i].rows.length-1; j>-1; j--) {
                Ev.purgeElement(aTheads[i].rows[j], true);
                aTheads[i].removeChild(aTheads[i].rows[j]);
            }     
        }
    }

    
    var oColumn,
        oColumnSet = this._oColumnSet;

    // Add TRs to the THEADs
    var colTree = oColumnSet.tree;
    var elTheadCell, id;
    for(l=0; l<aTheads.length; l++) {
        for(i=0; i<colTree.length; i++) {
            var elTheadRow = aTheads[l].appendChild(document.createElement("tr"));
            id = (l===1) ? this._sId+"-hdrow" + i + "-a11y": this._sId+"-hdrow" + i;
            elTheadRow.id = id;
    
            // ...and create TH cells
            for(j=0; j<colTree[i].length; j++) {
                oColumn = colTree[i][j];
                elTheadCell = elTheadRow.appendChild(document.createElement("th"));
                if(l===0) {
                    oColumn._elTh = elTheadCell;
                }
                id = (l===1) ? this._sId+"-th" + oColumn.getId() + "-a11y": this._sId+"-th" + oColumn.getId();
                elTheadCell.id = id;
                elTheadCell.yuiCellIndex = j;
                this._initThEl(elTheadCell,oColumn,i,j, (l===1));
            }
    
            if(l===0) {
                // Set FIRST/LAST on THEAD rows
                if(i === 0) {
                    Dom.addClass(elTheadRow, DT.CLASS_FIRST);
                }
                if(i === (colTree.length-1)) {
                    Dom.addClass(elTheadRow, DT.CLASS_LAST);
                }
            }
        }

        if(l===0) {
            // Set FIRST/LAST on TH elements using the values in ColumnSet headers array
            var aFirstHeaders = oColumnSet.headers[0];
            var aLastHeaders = oColumnSet.headers[oColumnSet.headers.length-1];
            for(i=0; i<aFirstHeaders.length; i++) {
                //TODO: A better way to get th cell
                Dom.addClass(Dom.get(this._sId+"-th"+aFirstHeaders[i]), DT.CLASS_FIRST);
            }
            for(i=0; i<aLastHeaders.length; i++) {
                //TODO: A better way to get th cell
                Dom.addClass(Dom.get(this._sId+"-th"+aLastHeaders[i]), DT.CLASS_LAST);
            }
        
            // Add DD features only after DOM has been updated
            var foundDD = (util.DD) ? true : false;
            var needDD = false;
            // draggable
            // HACK: Not able to use attribute since this code is run before
            // attributes are initialized
            if(this._oConfigs.draggableColumns) {
                for(i=0; i<this._oColumnSet.tree[0].length; i++) {
                    oColumn = this._oColumnSet.tree[0][i];
                    if(foundDD) {
                        elTheadCell = oColumn.getThEl();
                        Dom.addClass(elTheadCell, DT.CLASS_DRAGGABLE);
                        var elDragTarget = DT._initColumnDragTargetEl();
                        oColumn._dd = new YAHOO.widget.ColumnDD(this, oColumn, elTheadCell, elDragTarget);
                    }
                    else {
                        needDD = true;
                    }    
                }
            }
            // resizeable
            for(i=0; i<this._oColumnSet.keys.length; i++) {
                oColumn = this._oColumnSet.keys[i];
                if(oColumn.resizeable) {
                    if(foundDD) {
                        elTheadCell = oColumn.getThEl();
                        Dom.addClass(elTheadCell, DT.CLASS_RESIZEABLE);
                        var elThLiner = elTheadCell.firstChild;
                        var elThResizer = elThLiner.appendChild(document.createElement("div"));
                        elThResizer.id = this._sId + "-colresizer" + oColumn.getId();
                        oColumn._elResizer = elThResizer;
                        Dom.addClass(elThResizer,DT.CLASS_RESIZER);
                        var elResizerProxy = DT._initColumnResizerProxyEl();
                        oColumn._ddResizer = new YAHOO.util.ColumnResizer(
                                this, oColumn, elTheadCell, elThResizer.id, elResizerProxy);
                        var cancelClick = function(e) {
                            Ev.stopPropagation(e);
                        };
                        Ev.addListener(elThResizer,"click",cancelClick);
                    }
                    else {
                        needDD = true;
                    }
                }
            }
            if(needDD) {
                YAHOO.log("Could not find DragDrop dependancy", "warn", this.toString());
            }
            
            YAHOO.log("TH cells for " + this._oColumnSet.keys.length + " keys created","info",this.toString());
        }
        else {
            YAHOO.log("Accessibility TH cells for " + this._oColumnSet.keys.length + " keys created","info",this.toString());
        }
    }


     // Set widths for hidden Columns
    for(var g=0, h=this._oColumnSet.keys.length; g<h; g++) {
        // Hidden Columns
        if(this._oColumnSet.keys[g].hidden) {
            var oHiddenColumn = this._oColumnSet.keys[g];
            var oHiddenThEl = oHiddenColumn.getThEl();
            oHiddenColumn._nLastWidth = oHiddenThEl.offsetWidth -
                        (parseInt(Dom.getStyle(oHiddenThEl,"paddingLeft"),10)|0) -
                        (parseInt(Dom.getStyle(oHiddenThEl,"paddingRight"),10)|0);
            this._setColumnWidth(oHiddenColumn, "1px"); 
        }
    }


    // Bug 1806891
    if(ua.webkit && ua.webkit < 420) {
        var oSelf = this;
        setTimeout(function() {
            oSelf._elThead.style.display = "";
        },0);
        this._elThead.style.display = 'none';
    }
},

/**
 * Populates TH cell as defined by Column.
 *
 * @method _initThEl
 * @param elTheadCell {HTMLElement} TH cell element reference.
 * @param oColumn {YAHOO.widget.Column} Column object.
 * @param row {Number} Row index.
 * @param col {Number} Column index.
 * @param bA11y {Boolean} True if TH is for accessibility, so as not to
 * initialize presentation elements.
 * @private
 */
_initThEl : function(elTheadCell,oColumn,row,col, bA11y) {
    // Clear out the cell of prior content
    // TODO: purgeListeners and other validation-related things
    var colKey = oColumn.getKey();
    var colId = oColumn.getId();
    elTheadCell.yuiColumnKey = colKey;
    elTheadCell.yuiColumnId = colId;
    elTheadCell.innerHTML = "";
    elTheadCell.rowSpan = oColumn.getRowspan();
    elTheadCell.colSpan = oColumn.getColspan();

    var elTheadCellLiner = elTheadCell.appendChild(document.createElement("div"));
    var elTheadCellLabel = elTheadCellLiner.appendChild(document.createElement("span"));

    // Keep it basic for screen readers
    if(bA11y) {
        //TODO: remove IDs and form elements from label
        if(oColumn.abbr) {
            elTheadCell.abbr = oColumn.abbr;
        }
        elTheadCellLabel.innerHTML = lang.isValue(oColumn.label) ? oColumn.label : colKey;
    }
    // Visually format the elements
    else {
        // Needed for resizer
        elTheadCellLiner.id = elTheadCell.id + "-liner";
        
        // Add classes on the liner
        var aClasses;
        if(lang.isString(oColumn.className)) {
            aClasses = [oColumn.className];
        }
        else if(lang.isArray(oColumn.className)) {
            aClasses = oColumn.className;
        }
        else {
            aClasses = [];
        }
        
        //TODO: document special keys will get stripped here
        aClasses[aClasses.length] = "yui-dt-col-"+colKey.replace(/[^\w\-.:]/g,"");
        
        aClasses[aClasses.length] = "yui-dt-col-"+oColumn.getId();
        
        aClasses[aClasses.length] = DT.CLASS_LINER;

        Dom.addClass(elTheadCellLiner,aClasses.join(" "));

        // Add classes on the label
        Dom.addClass(elTheadCellLabel,DT.CLASS_LABEL);
        
        // Add classes on the cell
        aClasses = [];
        if(oColumn.resizeable) {
            aClasses[aClasses.length] = DT.CLASS_RESIZEABLE;
        }
        if(oColumn.sortable) {
            aClasses[aClasses.length] = DT.CLASS_SORTABLE;
        }

        //Set Column hidden
        if(oColumn.hidden) {
            aClasses[aClasses.length] = DT.CLASS_HIDDEN;
        }
        
        // Set Column selection on TD
        if(oColumn.selected) {
            aClasses[aClasses.length] = DT.CLASS_SELECTED;
        }

        Dom.addClass(elTheadCell,aClasses.join(" "));

        DT.formatTheadCell(elTheadCellLabel, oColumn, this);
    }
},

/**
 * Creates HTML markup for Cell Editor.
 *
 * @method _initCellEditorEl
 * @private
 */
_initCellEditorEl : function() {
    // TODO: destroy previous instances

    // 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;
},

/** 	 
  * Initializes Column sorting. 	 
  * 	 
  * @method _initColumnSort 	 
  * @private 	 
  */ 	 
_initColumnSort : function() {
    this.subscribe("theadCellClickEvent", this.onEventSortColumn); 	 
}, 	 
 

































// DOM MUTATION FUNCTIONS



/**
 * Create a TR element for a given Record.
 * @method _createTrEl
 * @param oRecord {YAHOO.widget.Record} Record instance
 * @return {HTMLElement} The new TR element.  This must be added to the DOM.
 * @private 
 */
_createTrEl : function (oRecord) {
    // Clone the empty tr template.  We can't clone an existing row
    // because of the expandos and td ids that must be set in _addTdEl
    var elRow     = this._trElTemplate.cloneNode(true);

    elRow.id = this._sId+"-bdrow"+this._nTrCount;
    this._nTrCount++;

    // Call _updateTrEl to populate and align the row contents
    return this._updateTrEl(elRow,oRecord);
},

/**
 * Formats all TD elements of given TR element with data from the given Record.
 *
 * @method _updateTrEl
 * @param elRow {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(elRow, oRecord) {
    var oColumnSet = this._oColumnSet,
        sortKey,
        sortClass,
        isSortedBy = this.get("sortedBy"),
        i,j,len,jlen;

    if(isSortedBy) {
        sortKey = isSortedBy.key;
        sortClass = isSortedBy.dir;
    }

    // Hide the row to prevent constant reflows
    elRow.style.display = 'none';

    // Remove extra TD elements
    while(elRow.childNodes.length > oColumnSet.keys.length) {
        elRow.removeChild(elRow.firstChild);
    }
    // Add more TD elements as needed
    for (i=elRow.childNodes.length||0, len=oColumnSet.keys.length; i < len; ++i) {
        this._addTdEl(elRow,oColumnSet.keys[i],i);
    }

    // Update TD elements with new data
    for(i=0,len=oColumnSet.keys.length; i<len; ++i) {
        var oColumn     = oColumnSet.keys[i],
            elCell      = elRow.childNodes[i],
            elCellLiner = elCell.firstChild,
            cellHeaders = '',
            headerType  = this.get('scrollable') ? "-a11y " : " ";

        // Set the cell content
        this.formatCell(elCellLiner, oRecord, oColumn);

        // Set the cell's accessibility headers
        for(j=0,jlen=oColumnSet.headers[i].length; j < jlen; ++j) {
            cellHeaders += this._sId + "-th" + oColumnSet.headers[i][j] + headerType;
        }
        elCell.headers = cellHeaders;

        // Set ASC/DESC on TD
        if(oColumn.key === sortKey) {
            Dom.replaceClass(elCell, sortClass === DT.CLASS_ASC ?
                                     DT.CLASS_DESC : DT.CLASS_ASC, sortClass);
        } else {
            Dom.removeClass(elCell, DT.CLASS_ASC);
            Dom.removeClass(elCell, DT.CLASS_DESC);
        }
        
        // Set Column hidden if appropriate
        if(oColumn.hidden) {
            Dom.addClass(elCell, DT.CLASS_HIDDEN);
        }
        else {
            Dom.removeClass(elCell, DT.CLASS_HIDDEN);
        }

        // Set Column selection on TH
        if(oColumn.selected) {
            Dom.addClass(elCell, DT.CLASS_SELECTED);
        }
        else {
            Dom.removeClass(elCell, DT.CLASS_SELECTED);
        }
    }

    // Update Record ID
    elRow.yuiRecordId = oRecord.getId();
    
    // Redisplay the row for reflow
    elRow.style.display = '';

    return elRow;
},


/**
 * Creates a cell within the specified row and column.
 * @method _addTdEl
 * @param elRow {HTMLElement} The row to add the cell to
 * @param oColumn {Column} The column definition to use for the cell
 * @param index {number} (optional) the index to add the cell at (default null)
 * @return {HTMLElement} the new cell
 * @private
 */
_addTdEl : function (elRow,oColumn,index) {
    var elCell      = this._tdElTemplate.cloneNode(true),
        elCellLiner = elCell.firstChild;

    index = index || elRow.cells.length;

    elCell.id           = elRow.id+"-cell"+this._nTdCount;
    this._nTdCount++;
    elCell.yuiColumnKey = oColumn.getKey();
    elCell.yuiColumnId  = oColumn.getId();

    // For SF2 cellIndex bug: http://www.webreference.com/programming/javascript/ppk2/3.html
    elCell.yuiCellIndex = index;

    // Set FIRST/LAST on TD
    if (!(index % this._oColumnSet.keys.length - 1)) {
        elCell.className = index ? DT.CLASS_LAST : DT.CLASS_FIRST;
    }

    var insertBeforeCell = elRow.cells[index] || null;
    return elRow.insertBefore(elCell,insertBeforeCell);
},

/**
 * 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.getTrEl(row));
    }
    else {
        return null;
    }
},



























// CSS/STATE FUNCTIONS




/**
 * Assigns the class DT.CLASS_FIRST to the first TR element
 * of the DataTable page and updates internal tracker.
 *
 * @method _setFirstRow
 * @private
 */
_setFirstRow : function() {
    var rowEl = this.getFirstTrEl();
    if(rowEl) {
        // Remove FIRST
        if(this._sFirstTrId) {
            Dom.removeClass(this._sFirstTrId, DT.CLASS_FIRST);
        }
        // Set FIRST
        Dom.addClass(rowEl, DT.CLASS_FIRST);
        this._sFirstTrId = rowEl.id;
    }
    else {
        this._sFirstTrId = null;
    }
},

/**
 * Assigns the class DT.CLASS_LAST to the last TR element
 * of the DataTable page and updates internal tracker.
 *
 * @method _setLastRow
 * @private
 */
_setLastRow : function() {
    var rowEl = this.getLastTrEl();
    if(rowEl) {
        // Unassign previous class
        if(this._sLastTrId) {
            Dom.removeClass(this._sLastTrId, DT.CLASS_LAST);
        }
        // Assign class
        Dom.addClass(rowEl, DT.CLASS_LAST);
        this._sLastTrId = rowEl.id;
    }
    else {
        this._sLastTrId = null;
    }
},

/**
 * Assigns the classes DT.CLASS_EVEN and
 * DT.CLASS_ODD to alternating TR elements of the DataTable
 * page. For performance, a subset of rows may be specified.
 *
 * @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);
    }
},













































/////////////////////////////////////////////////////////////////////////////
//
// Private DOM Event Handlers
//
/////////////////////////////////////////////////////////////////////////////

/**
 * Syncs scrolltop and scrollleft of all TABLEs.
 *
 * @method _onScroll
 * @param e {HTMLEvent} The scroll event.
 * @param oSelf {DT} DataTable instance
 * @private
 */
_onScroll : function(e, oSelf) {
    oSelf._elTheadContainer.scrollLeft = oSelf._elTbodyContainer.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 click events on the DOCUMENT.
 *
 * @method _onDocumentClick
 * @param e {HTMLEvent} The click event.
 * @param oSelf {DT} 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 && 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 {DT} 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 {DT} 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 {DT} 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 oSelf {DT} DataTable instance.
 * @private
 */
_onTableMouseover : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
        var elTag = 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 oSelf {DT} DataTable instance.
 * @private
 */
_onTableMouseout : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    var elTag = 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 {DT} DataTable instance.
 * @private
 */
_onTableMousedown : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    var elTag = 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 dblclick events on the DataTable instance.
 *
 * @method _onTableDblclick
 * @param e {HTMLEvent} The dblclick event.
 * @param oSelf {DT} DataTable instance.
 * @private
 */
_onTableDblclick : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    var elTag = 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 {DT} DataTable 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 DT) && oSelf._sId) {
                oSelf._elTbodyContainer.scrollLeft = oSelf._elTheadContainer.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
                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 {DT} 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 && oSelf._oCellEditor.isActive) {
        oSelf.fireEvent("editorBlurEvent", {editor:oSelf._oCellEditor});
    }

    var elTarget = Ev.getTarget(e);
    var elTag = 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 keypress events on the TABLE. Mainly to support stopEvent on Mac.
 *
 * @method _onTableKeypress
 * @param e {HTMLEvent} The key event.
 * @param oSelf {DT} DataTable instance.
 * @private
 */
_onTableKeypress : function(e, oSelf) {
    if(ua.webkit) {
        var nKey = Ev.getCharCode(e);
        // arrow down
        if(nKey == 40) {
            Ev.stopEvent(e);
        }
        // arrow up
        else if(nKey == 38) {
            Ev.stopEvent(e);
        }
    }
},

/**
 * Handles click events on the THEAD element.
 *
 * @method _onTheadClick
 * @param e {HTMLEvent} The click event.
 * @param oSelf {DT} DataTable instance.
 * @private
 */
_onTheadClick : function(e, oSelf) {
    // Always blur the cell editor
    if(oSelf._oCellEditor && oSelf._oCellEditor.isActive) {
        oSelf.fireEvent("editorBlurEvent", {editor:oSelf._oCellEditor});
    }

    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "input":
                if(elTarget.type.toLowerCase() == "checkbox") {
                    bKeepBubbling = oSelf.fireEvent("theadCheckboxClickEvent",{target:elTarget,event:e});
                }
                else if(elTarget.type.toLowerCase() == "radio") {
                    bKeepBubbling = oSelf.fireEvent("theadRadioClickEvent",{target:elTarget,event:e});
                }
                break;
            case "a":
                bKeepBubbling = oSelf.fireEvent("theadLinkClickEvent",{target:elTarget,event:e});
                break;
            case "button":
                bKeepBubbling = oSelf.fireEvent("theadButtonClickEvent",{target:elTarget,event:e});
                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 {DT} DataTable instance.
 * @private
 */
_onTbodyClick : function(e, oSelf) {
    // Always blur the cell editor
    if(oSelf._oCellEditor && oSelf._oCellEditor.isActive) {
        oSelf.fireEvent("editorBlurEvent", {editor:oSelf._oCellEditor});
    }

    // Fire Custom Events
    var elTarget = Ev.getTarget(e);
    var elTag = elTarget.nodeName.toLowerCase();
    var bKeepBubbling = true;
    while(elTarget && (elTag != "table")) {
        switch(elTag) {
            case "body":
                return;
            case "input":
                if(elTarget.type.toLowerCase() == "checkbox") {
                    bKeepBubbling = oSelf.fireEvent("checkboxClickEvent",{target:elTarget,event:e});
                }
                else if(elTarget.type.toLowerCase() == "radio") {
                    bKeepBubbling = oSelf.fireEvent("radioClickEvent",{target:elTarget,event:e});
                }
                break;
            case "a":
                bKeepBubbling = oSelf.fireEvent("linkClickEvent",{target:elTarget,event:e});
                break;
            case "button":
                bKeepBubbling = oSelf.fireEvent("buttonClickEvent",{target:elTarget,event:e});
                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});
},

/*TODO undeprecate?
 * Handles change events on SELECT elements within DataTable.
 *
 * @method _onDropdownChange
 * @param e {HTMLEvent} The change event.
 * @param oSelf {DT} DataTable instance.
 * @private
 * @deprecated
 */
_onDropdownChange : function(e, oSelf) {
    var elTarget = Ev.getTarget(e);
    //TODO: pass what args?
    //var value = elTarget[elTarget.selectedIndex].value;
    oSelf.fireEvent("dropdownChangeEvent", {event:e, target:elTarget});
},
































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


/////////////////////////////////////////////////////////////////////////////
//
// 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 the DataTable instance's Cell Editor as an object literal with the
 * following properties:
 * <dl>
 * <dt>cell</dt>
 * <dd>{HTMLElement} Cell element being edited.</dd>
 *
 * <dt>column</dt>
 * <dd>{YAHOO.widget.Column} Associated Column instance.</dd>
 *
 * <dt>container</dt>
 * <dd>{HTMLElement} Reference to editor's container DIV element.</dd>
 *
 * <dt>isActive</dt>
 * <dd>{Boolean} True if cell is currently being edited.</dd>
 *
 * <dt>record</dt>
 * <dd>{YAHOO.widget.Record} Associated Record instance.</dd>
 *
 * <dt>validator</dt>
 * <dd>{HTMLFunction} Associated validator function called before new data is stored. Called
 * within the scope of the DataTable instance, the function receieves the
 * following arguments:
 *
 * <dl>
 *  <dt>oNewData</dt>
 *  <dd>{Object} New data to validate.</dd>
 *
 *  <dt>oOldData</dt>
 *  <dd>{Object} Original data in case of reversion.</dd>
 *
 *  <dt>oCellEditor</dt>
 *  <dd>{Object} Object literal representation of Editor values.</dd>
 * </dl>
 *
 *  </dd>
 *
 * <dt>defaultValue</dt>
 * <dd>Dynamically settable default value</dd>
 * </dl>
 *
 * <dt>value</dt>
 * <dd>Current input value</dd>
 * </dl>
 *
 *
 *
 *
 *
 * @method getCellEditor
 * @return {Object} Cell Editor object literal values.
 */
getCellEditor : function() {
    return this._oCellEditor;
},











































// 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 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
 * directly 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.
 *
 * @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) {
    var allRows = this._elTbody.rows;

    // By Record
    if(row instanceof YAHOO.widget.Record) {
        var nTrIndex = this.getTrIndex(row);
            if(nTrIndex !== null) {
                return allRows[nTrIndex];
            }
            // Not a valid Record
            else {
                return null;
            }
    }
    // By page row index
    else if(lang.isNumber(row) && (row > -1) && (row < allRows.length)) {
        return allRows[row];
    }
    // By ID string or element reference
    else {
        var elRow;
        var el = Dom.get(row);

        // Validate HTML element
        if(el && (el.ownerDocument == document)) {
            // Validate TR element
            if(el.nodeName.toLowerCase() != "tr") {
                // Traverse up the DOM to find the corresponding TR element
                elRow = Dom.getAncestorByTagName(el,"tr");
            }
            else {
                elRow = el;
            }

            // Make sure the TR is in this TBODY
            if(elRow && (elRow.parentNode == this._elTbody)) {
                // Now we can return the TR element
                return elRow;
            }
        }
    }

    return null;
},

/**
 * Returns DOM reference to the first TR element in the DataTable page, or null.
 *
 * @method getFirstTrEl
 * @return {HTMLElement} Reference to TR element.
 */
getFirstTrEl : function() {
    return this._elTbody.rows[0] || null;
},

/**
 * Returns DOM reference to the last TR element in the DataTable page, or null.
 *
 * @method getLastTrEl
 * @return {HTMLElement} Reference to last TR element.
 */
getLastTrEl : function() {
    var allRows = this._elTbody.rows;
        if(allRows.length > 0) {
            return allRows[allRows.length-1] || null;
        }
},

/**
 * Returns DOM reference to the next TR element from the given 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.
 * @return {HTMLElement} Reference to next TR element.
 */
getNextTrEl : function(row) {
    var nThisTrIndex = this.getTrIndex(row);
    if(nThisTrIndex !== null) {
        var allRows = this._elTbody.rows;
        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 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.
 * @return {HTMLElement} Reference to previous TR element.
 */
getPreviousTrEl : function(row) {
    var nThisTrIndex = this.getTrIndex(row);
    if(nThisTrIndex !== null) {
        var allRows = this._elTbody.rows;
        if(nThisTrIndex > 0) {
            return allRows[nThisTrIndex-1];
        }
    }

    YAHOO.log("Could not get previous TR element for row " + row, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to a TD liner element.
 *
 * @method getTdLinerEl
 * @param cell {HTMLElement | String | Object} DOM element reference or string ID, 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.
 *
 * @method getTdEl
 * @param cell {HTMLElement | String | Object} DOM element reference or string ID, 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
        if(elCell && (elCell.parentNode.parentNode == this._elTbody)) {
            // Now we can return the TD element
            return elCell;
        }
    }
    else if(cell) {
        var oRecord, nColKeyIndex;

        if(lang.isString(cell.columnId) && lang.isString(cell.recordId)) {
            oRecord = this.getRecord(cell.recordId);
            var oColumn = this.getColumnById(cell.columnId);
            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 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 = this.getTrEl(row) || this.getFirstTrEl();
    if(elRow && (elRow.cells.length > 0)) {
        return elRow.cells[0];
    }
    YAHOO.log("Could not get first TD element for row " + elRow, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to the last TD element in the DataTable page (by default),
 * the first TD element of the optionally given row, or null.
 *
 * @method getLastTdEl
 * @return {HTMLElement} Reference to last TD element.
 */
getLastTdEl : function(row) {
    var elRow = this.getTrEl(row) || this.getLastTrEl();
    if(elRow && (elRow.cells.length > 0)) {
        return elRow.cells[elRow.cells.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 = elCell.yuiCellIndex;
        var elRow = this.getTrEl(elCell);
        if(nThisTdIndex < elRow.cells.length-1) {
            return elRow.cells[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 = elCell.yuiCellIndex;
        var elRow = this.getTrEl(elCell);
        if(nThisTdIndex > 0) {
            return elRow.cells[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.
 * @return {HTMLElement} Reference to next TD element, or null.
 */
getAboveTdEl : function(cell) {
    var elCell = this.getTdEl(cell);
    if(elCell) {
        var elPreviousRow = this.getPreviousTrEl(elCell);
        if(elPreviousRow) {
            return elPreviousRow.cells[elCell.yuiCellIndex];
        }
    }
    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.
 * @return {HTMLElement} Reference to previous TD element, or null.
 */
getBelowTdEl : function(cell) {
    var elCell = this.getTdEl(cell);
    if(elCell) {
        var elNextRow = this.getNextTrEl(elCell);
        if(elNextRow) {
            return elNextRow.cells[elCell.yuiCellIndex];
        }
    }
    YAHOO.log("Could not get below TD element for cell " + cell, "info", this.toString());
    return null;
},

/**
 * Returns DOM reference to a TH liner 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 elCell = this.getThEl(theadCell);
    return elCell.firstChild || 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 elTheadCell;

    // Validate Column instance
    if(theadCell instanceof YAHOO.widget.Column) {
        var oColumn = theadCell;
        elTheadCell = oColumn.getThEl();
        if(elTheadCell) {
            return elTheadCell;
        }
    }
    // 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
                elTheadCell = Dom.getAncestorByTagName(el,"th");
            }
            else {
                elTheadCell = el;
            }

            // Make sure the TH is in this THEAD
            if(elTheadCell && (elTheadCell.parentNode.parentNode == this._elThead)) {
                // Now we can return the TD element
                return elTheadCell;
            }
        }
    }

    return null;
},

/**
 * Returns the page row index of given row. Returns null if the row is not on the
 * current DataTable page.
 *
 * @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 row does not exist or is not on current page.
 */
getTrIndex : function(row) {
    var nRecordIndex;

    // By Record
    if(row instanceof YAHOO.widget.Record) {
        nRecordIndex = this._oRecordSet.getRecordIndex(row);
        if(nRecordIndex === null) {
            // Not a valid Record
            return null;
        }
    }
    // Calculate page row index from Record index
    else if(lang.isNumber(row)) {
        nRecordIndex = row;
    }
    if(lang.isNumber(nRecordIndex)) {
        // Validate the number
        if((nRecordIndex > -1) && (nRecordIndex < this._oRecordSet.getLength())) {
            // DataTable is paginated
            var oPaginator = this.get('paginator');
            if(oPaginator instanceof Pag || this.get('paginated')) {
                // Get the first and last Record on current page
                var startRecordIndex = 0,
                    endRecordIndex   = 0;

                if (oPaginator instanceof Pag) {
                    var rng = oPaginator.getPageRecords();
                    startRecordIndex = rng[0];
                    endRecordIndex   = rng[1];
                } else {
                    startRecordIndex = oPaginator.startRecordIndex;
                    endRecordIndex = startRecordIndex + oPaginator.rowsPerPage - 1;
                }

                // This Record is on current page
                if((nRecordIndex >= startRecordIndex) && (nRecordIndex <= endRecordIndex)) {
                    return nRecordIndex - startRecordIndex;
                }
                // This Record is not on current page
                else {
                    return null;
                }
            }
            // Not paginated, just return the Record index
            else {
                return nRecordIndex;
            }
        }
        // RecordSet index is out of range
        else {
            return null;
        }
    }
    // By element reference or ID string
    else {
        // Validate TR element
        var elRow = this.getTrEl(row);
        if(elRow && (elRow.ownerDocument == document) &&
                (elRow.parentNode == this._elTbody)) {
            return elRow.sectionRowIndex;
        }
    }

    YAHOO.log("Could not get page row index for row " + row, "info", this.toString());
    return null;
},














































// TABLE FUNCTIONS

/**
 * 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. The render method should be called as a separate step in order
 * to update the UI. 
 *
 * @method initializeTable
 */
initializeTable : function() {
    // Reset init flag
    this._bInit = true;
    
    // Clear the RecordSet
    this._oRecordSet.reset();

    // Clear selections
    this._unselectAllTrEls();
    this._unselectAllTdEls();
    this._aSelections = null;
    this._oAnchorRecord = null;
    this._oAnchorCell = null;
    
    // Clear sort
    this.set("sortedBy", null);
},

/**
 * 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() {
    this._oChainRender.stop();
    this.showTableMessage(DT.MSG_LOADING, DT.CLASS_LOADING);
    YAHOO.log("DataTable rendering...", "info", this.toString());

    var i, j, k, l, len, allRecords;

    // Paginator is enabled, show a subset of Records and update Paginator UI
    var oPaginator = this.get('paginator');
    var bPaginated = oPaginator instanceof Pag || this.get('paginated');
    if(bPaginated) {
        if (oPaginator instanceof Pag) {
            allRecords = this._oRecordSet.getRecords(
                            oPaginator.getStartIndex(),
                            oPaginator.getRowsPerPage());
            oPaginator.render();
        }
        else {
            // Backward compatibility
            this.updatePaginator();
            var rowsPerPage = oPaginator.rowsPerPage;
            var startRecordIndex = (oPaginator.currentPage - 1) * rowsPerPage;
            allRecords = this._oRecordSet.getRecords(startRecordIndex, rowsPerPage);
            this.formatPaginators();
        }
    }
    // Show all records
    else {
        allRecords = this._oRecordSet.getRecords();
    }

    var elTbody = this._elTbody;
    var allRows = elTbody.rows;

    // Should have rows
    if(lang.isArray(allRecords) && (allRecords.length > 0)) {
        // Keep track of selected rows
        var allSelectedRows = this.getSelectedRows();
        // Keep track of selected cells
        var allSelectedCells = this.getSelectedCells();
        // Anything to reinstate?
        var bReselect = (allSelectedRows.length>0) || (allSelectedCells.length > 0);

        // Remove extra rows from the bottom so as to preserve ID order
        while(elTbody.hasChildNodes() && (allRows.length > allRecords.length)) {
            elTbody.deleteRow(-1);
        }

        // Unselect all TR and TD elements in the UI
        if(bReselect) {
            this._unselectAllTrEls();
            this._unselectAllTdEls();
        }

        this.hideTableMessage();

        // How many rows to work with each loop
        var loopN = this.get("renderLoopSize");
        var loopStart,
            loopEnd;
        
        // From the top, update in-place existing rows, so as to reuse DOM elements
        if(allRows.length > 0) {
            loopEnd = allRows.length; // End at last row
            this._oChainRender.add({
                method: function(oArg) {
                    if((this instanceof DT) && this._sId) {
                        var i = oArg.nCurrentRow,
                            // Must reference allRows.length instead of loopEnd
                            // here because loopEnd is reused below outside
                            // this closure.
                            len = loopN > 0 ? Math.min(i + loopN,allRows.length) : allRows.length;
                        for(; i < len; ++i) {
                            this._updateTrEl(allRows[i], allRecords[i]);
                        }
                        if (loopN > 0) {
                            this._syncColWidths();
                        }
                        oArg.nCurrentRow = i;
                    }
                },
                iterations: (loopN > 0) ? Math.ceil(loopEnd/loopN) : 1,
                argument: {nCurrentRow:0}, // Start at first row
                scope: this,
                timeout: (loopN > 0) ? 0 : -1
            });
        }

        // Add more TR elements as necessary
        loopStart = allRows.length; // where to start
        loopEnd = allRecords.length; // where to end
        var nRowsNeeded = (loopEnd - loopStart); // how many needed
        if(nRowsNeeded > 0) {
            this._oChainRender.add({
                method: function(oArg) {
                    if((this instanceof DT) && this._sId) {
                        var i = oArg.nCurrentRow,
                            len = loopN > 0 ? Math.min(i + loopN,loopEnd) : loopEnd,
                            df = document.createDocumentFragment(),
                            tr;
                        for(; i < len; ++i) {
                            tr = this._createTrEl(allRecords[i]);
                            tr.className = (i%2) ? DT.CLASS_ODD : DT.CLASS_EVEN;
                            df.appendChild(tr);
                        }
                        this._elTbody.appendChild(df);
                        if (loopN > 0) {
                            this._syncColWidths();
                        }
                        oArg.nCurrentRow = i;
                    }
                },
                iterations: (loopN > 0) ? Math.ceil(nRowsNeeded/loopN) : 1,
                argument: {nCurrentRow:loopStart}, // start at last row
                scope: this,
                timeout: (loopN > 0) ? 0 : -1
            });
        }

        this._oChainRender.add({
            method: function(oArg) {
                if((this instanceof DT) && this._sId) {
                    this._setFirstRow();
                    this._setLastRow();

                    // Reinstate selected and sorted classes
                    if(bReselect) {
                        // Loop over each row
                        for(j=0; j<allRows.length; j++) {
                            var thisRow = allRows[j];
                            var sMode = this.get("selectionMode");
                            if ((sMode == "standard") || (sMode == "single")) {
                                // Set SELECTED
                                for(k=0; k<allSelectedRows.length; k++) {
                                    if(allSelectedRows[k] === thisRow.yuiRecordId) {
                                        Dom.addClass(thisRow, DT.CLASS_SELECTED);
                                        if(j === allRows.length-1) {
                                            this._oAnchorRecord = this.getRecord(thisRow.yuiRecordId);
                                        }
                                    }
                                }
                            }
                            else {
                                // Loop over each cell
                                for(k=0; k<thisRow.cells.length; k++) {
                                    var thisCell = thisRow.cells[k];
                                    // Set SELECTED
                                    for(l=0; l<allSelectedCells.length; l++) {
                                        if((allSelectedCells[l].recordId === thisRow.yuiRecordId) &&
                                                (allSelectedCells[l].columnId === thisCell.yuiColumnId)) {
                                            Dom.addClass(thisCell, DT.CLASS_SELECTED);
                                            if(k === thisRow.cells.length-1) {
                                                this._oAnchorCell = {record:this.getRecord(thisRow.yuiRecordId), column:this.getColumnById(thisCell.yuiColumnId)};
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                
                if(this._bInit) {
                    this._forceGeckoRedraw();

                    this._oChainRender.add({
                        method: function() {
                            if((this instanceof DT) && this._sId && this._bInit) {
                                this._bInit = false;
                                this.fireEvent("initEvent");
                                YAHOO.log("DataTable initialized with " + allRecords.length + " of " + this._oRecordSet.getLength() + " rows", "info", this.toString());
                            }
                        },
                        scope: this
                    });
                    this._oChainRender.run();
                }
                else {
                    this.fireEvent("renderEvent");
                    // Backward compatibility
                    this.fireEvent("refreshEvent");
                    YAHOO.log("DataTable rendered " + allRecords.length + " of " + this._oRecordSet.getLength() + " rows", "info", this.toString());
                }
            
            },
            scope: this,
            timeout: (loopN > 0) ? 0 : -1
        }); 
        
        this._oChainRender.add({
            method: function() {
                if((this instanceof DT) && this._sId) {
                    this._syncColWidths();
                }
            },
            scope: this
        });
                    
        this._oChainRender.run();  
    }
    // Empty
    else {
        // Remove all rows
        while(elTbody.hasChildNodes()) {
            elTbody.deleteRow(-1);
        }

        this.showTableMessage(DT.MSG_EMPTY, DT.CLASS_EMPTY);
    }
},

/**
 * 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() {
    this._oChainRender.stop();
    
    //TODO: destroy static resizer proxy and column proxy?
    
    var i;
    // Destroy ColumnDDs
    var aTree = this._oColumnSet.tree[0];
    for(i=0; i<aTree.length; i++) {
        if(aTree[i]._dd) {
            aTree[i]._dd = aTree[i]._dd.unreg();
        }
    }

    // Destroy ColumnResizers
    var aKeys = this._oColumnSet.keys;
    for(i=0; i<aKeys.length; i++) {
        if(aKeys[i]._ddResizer) {
            aKeys[i]._ddResizer = aKeys[i]._ddResizer.unreg();
        }
    }
    
    // Destroy Cell Editor
    Ev.purgeElement(this._oCellEditor.container, true);
    document.body.removeChild(this._oCellEditor.container);

    var instanceName = this.toString();
    var elContainer = this._elContainer;

    // Unhook custom events
    this._oRecordSet.unsubscribeAll();
    this.unsubscribeAll();

    // Unhook DOM events
    Ev.purgeElement(elContainer, true);
    Ev.removeListener(document, "click", this._onDocumentClick);

    // Remove DOM elements
    elContainer.innerHTML = "";

    // 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._elStylesheet) {
            document.getElementsByTagName('head')[0].removeChild(DT._elStylesheet);
            DT._elStylesheet = null;
        }
    }

    YAHOO.log("DataTable instance destroyed: " + instanceName);
},

/**
 * 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);
    }

    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());
},

/**
 * 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);
},

































































// 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 instanceof Pag) {
            return oPaginator.get('recordOffset') + nTrIndex;
        }
        else if (this.get('paginated')) {
            return oPaginator.startRecordIndex + 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.yuiRecordId);
        }
    }

    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} 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.
 * @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.getColumnById(elCell.yuiColumnId);
        }
        // Validate TH element
        else {
            elCell = this.getThEl(column);
            if(elCell) {
                oColumn = this._oColumnSet.getColumnById(elCell.yuiColumnId);
            }
        }
    }
    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.
 * @return {String} DataTable.widget.CLASS_ASC or DataTable.widget.CLASS_DESC.
 */
getColumnSortDir : function(oColumn) {
    // Backward compatibility
    if(oColumn.sortOptions && oColumn.sortOptions.defaultOrder) {
        if(oColumn.sortOptions.defaultOrder == "asc") {
            oColumn.sortOptions.defaultDir = DT.CLASS_ASC;
        }
        else if (oColumn.sortOptions.defaultOrder == "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;

    // Already sorted?
    var bSorted = false;
    var 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;
},

/**
 * Sorts given Column.
 *
 * @method sortColumn
 * @param oColumn {YAHOO.widget.Column} Column instance.
 * @param sDir {String} (Optional) DT.CLASS_ASC or
 * DT.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 sortDir = sDir || this.getColumnSortDir(oColumn);

        // Do the actual sort
        var oSortedBy = this.get("sortedBy") || {};
        var bSorted = (oSortedBy.key === oColumn.key) ? true : false;
        if(!bSorted || sDir) {
            // Is there a custom sort handler function defined?
            var sortFnc = (oColumn.sortOptions && lang.isFunction(oColumn.sortOptions.sortFunction)) ?
                    // Custom sort function
                    oColumn.sortOptions.sortFunction :

                    // Default sort function
                    function(a, b, desc) {
                        var sorted = YAHOO.util.Sort.compare(a.getData(oColumn.key),b.getData(oColumn.key), desc);
                        if(sorted === 0) {
                            return YAHOO.util.Sort.compare(a.getId(),b.getId(), desc);
                        }
                        else {
                            return sorted;
                        }
                    };

            this._oRecordSet.sortRecords(sortFnc, ((sortDir == DT.CLASS_DESC) ? true : false));
        }
        else {
            this._oRecordSet.reverseRecords();
        }

        // Update sortedBy tracker
        this.set("sortedBy", {key:oColumn.key, dir:sortDir, column:oColumn});

        // Reset to first page
        //TODO: Keep selection in view
        var oPaginator = this.get('paginator');
        if (oPaginator instanceof Pag) {
            // TODO : is this server-side op safe?  Will fire changeRequest
            // event mechanism
            oPaginator.setPage(1,true);
        }
        else if (this.get('paginated')) {
            // Backward compatibility
            this.updatePaginator({currentPage:1});
        }

        // Update the UI
        DT.formatTheadCell(oColumn.getThEl().firstChild.firstChild, oColumn, this);
        this.render();

        this.fireEvent("columnSortEvent",{column:oColumn,dir:sortDir});
        YAHOO.log("Column \"" + oColumn.key + "\" sorted \"" + sortDir + "\"", "info", this.toString());
    }
    else {
        YAHOO.log("Could not sort Column \"" + oColumn.key + "\"", "warn", this.toString());
    }
},

/**
 * Sets DOM elements of given Column to given pixel width. 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.
 * @private
 */
_setColumnWidth : function(oColumn, sWidth) {
    oColumn = this.getColumn(oColumn);
    if(oColumn) {
        // Create STYLE node
        if(!DT._bStylesheetFallback) {
            var s;
            if(!DT._elStylesheet) {
                    s = document.createElement('style');
                    s.type = 'text/css';
                    DT._elStylesheet = document.getElementsByTagName('head').item(0).appendChild(s);
            }
                
            if(DT._elStylesheet) {
                s = DT._elStylesheet;
                
                var sClassname = ".yui-dt-col-" + oColumn.getId();
                
                // Create rule for the Column
                var rule = DT._oStylesheetRules[sClassname];
                if (!rule) {
                    if (s.styleSheet && s.styleSheet.addRule) {
                        s.styleSheet.addRule(sClassname,"overflow:hidden");
                        s.styleSheet.addRule(sClassname,"width:"+sWidth);
                        rule = s.styleSheet.rules[s.styleSheet.rules.length-1];
                    } else if (s.sheet && s.sheet.insertRule) {
                        s.sheet.insertRule(sClassname+" {overflow:hidden;width:"+sWidth+";}",s.sheet.cssRules.length);
                        rule = s.sheet.cssRules[s.sheet.cssRules.length-1];
                    } else {
                        DT._bStylesheetFallback = true;
                    }
                    DT._oStylesheetRules[sClassname] = rule;
                }
                
                // Update existing rule for the Column
                else {
                    rule.style.width = sWidth;
                } 
                return;
            }
            
            DT._bStylesheetFallback = true;
        }
        // Do it the old fashioned way
        if(DT._bStylesheetFallback) {           
            if(sWidth == "auto") {
                sWidth = ""; 
            }

            if (!this._aFallbackColResizer[this._elTbody.rows.length]) {
                /*
                Compile a custom function to do all the cell width
                assignments at the same time.  A new resizer function is created
                for each new unique number of rows in _elTbody.  This will
                result in a function declaration like:
                function (oColumn,sWidth) {
                    var colIdx = oColumn.getKeyIndex();
                    oColumn.getThEl().firstChild.style.width =
                    this._elTbody.rows[0].cells[colIdx].firstChild.style.width =
                    this._elTbody.rows[0].cells[colIdx].style.width =
                    this._elTbody.rows[1].cells[colIdx].firstChild.style.width =
                    this._elTbody.rows[1].cells[colIdx].style.width =
                    ... (for all row indices in this._elTbody.rows.length - 1)
                    this._elTbody.rows[99].cells[colIdx].firstChild.style.width =
                    this._elTbody.rows[99].cells[colIdx].style.width = sWidth;
                    ending with the sWidth as the initial assignment   ^
                }
                */
                var i,j,k;
                var resizerDef = [
                    'var colIdx=oColumn.getKeyIndex();',
                    'oColumn.getThEl().firstChild.style.width='
                ];
                for (i=this._elTbody.rows.length-1, j=2; i >= 0; --i) {
                    resizerDef[j++] = 'this._elTbody.rows[';
                    resizerDef[j++] = i;
                    resizerDef[j++] = '].cells[colIdx].firstChild.style.width=';
                    resizerDef[j++] = 'this._elTbody.rows[';
                    resizerDef[j++] = i;
                    resizerDef[j++] = '].cells[colIdx].style.width=';
                }
                resizerDef[j] = 'sWidth;';
                resizerDef[j+1] = 'oColumn.getThEl().firstChild.style.overflow=';
                for (i=this._elTbody.rows.length-1, k=j+2; i >= 0; --i) {
                    resizerDef[k++] = 'this._elTbody.rows[';
                    resizerDef[k++] = i;
                    resizerDef[k++] = '].cells[colIdx].firstChild.style.overflow=';
                    resizerDef[k++] = 'this._elTbody.rows[';
                    resizerDef[k++] = i;
                    resizerDef[k++] = '].cells[colIdx].style.overflow=';
                }
                resizerDef[k] = '"hidden";';
                this._aFallbackColResizer[this._elTbody.rows.length] =
                    new Function('oColumn','sWidth',resizerDef.join(''));
            }
            var resizerFn = this._aFallbackColResizer[this._elTbody.rows.length];

            if (resizerFn) {
                resizerFn.call(this,oColumn,sWidth);
                //this._syncScrollPadding();
                return;
            }
        }
    }
    else {
        YAHOO.log("Could not set width of Column " + oColumn + " to " + sWidth, "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.
 */
setColumnWidth : function(oColumn, nWidth) {
    oColumn = this.getColumn(oColumn);
    if(oColumn) {
        // Validate new width against minimum width
        if(lang.isNumber(nWidth)) {
            nWidth = (nWidth > oColumn.minWidth) ? nWidth : oColumn.minWidth;

            // Save state
            oColumn.width = nWidth;
            
            // Resize the DOM elements
            //this._oChainSync.stop();
            this._setColumnWidth(oColumn, nWidth+"px");
            this._syncScrollPadding();
            
            this.fireEvent("columnSetWidthEvent",{column:oColumn,width:nWidth});
            YAHOO.log("Set width of Column " + oColumn + " to " + nWidth + "px", "info", this.toString());
            return;
        }
    }
    YAHOO.log("Could not set width of Column " + oColumn + " to " + nWidth + "px", "warn", this.toString());
},


/**
 * 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} Column instance.
 */
hideColumn : function(oColumn) {
    oColumn = this.getColumn(oColumn);
    if(oColumn && !oColumn.hidden) {
        // Only top-level Columns can get hidden
        if(oColumn.getTreeIndex() !== null) {
            var allrows = this.getTbodyEl().rows;
            var l = allrows.length;
            var allDescendants = this._oColumnSet.getDescendants(oColumn);
            for(var i=0; i<allDescendants.length; i++) {
                var thisColumn = allDescendants[i];
                thisColumn.hidden = true;

                var elTheadCell = thisColumn.getThEl();
                var elTheadCellLiner = elTheadCell.firstChild;
                // Store to reinstate later
                thisColumn._nLastWidth = elTheadCellLiner.offsetWidth - 
                        (parseInt(Dom.getStyle(elTheadCellLiner,"paddingLeft"),10)|0) -
                        (parseInt(Dom.getStyle(elTheadCellLiner,"paddingRight"),10)|0);
                Dom.addClass(elTheadCell,DT.CLASS_HIDDEN);

                // Adjust body cells (if key Column)
                var thisKeyIndex = thisColumn.getKeyIndex();
                if(thisKeyIndex !== null) {
                    for(var j=0;j<l;j++) {
                        Dom.addClass(allrows[j].cells[thisKeyIndex],DT.CLASS_HIDDEN);
                    }

                    this._setColumnWidth(thisColumn, "1px");
                    
                    // Disable interactive features
                    if(thisColumn.resizeable) {
                        Dom.removeClass(thisColumn.getResizerEl(),DT.CLASS_RESIZER);
                    }
                    if(thisColumn.sortable) {
                        Dom.removeClass(thisColumn.getThEl(),DT.CLASS_SORTABLE);
                        thisColumn.getThEl().firstChild.firstChild.firstChild.style.display = "none";
                    }
                }
                // Just set thead cell width directly for parent Column
                else {
                    elTheadCell.firstChild.style.width = "1px";
                }
                
                this.fireEvent("columnHideEvent",{column:thisColumn});
                YAHOO.log("Column \"" + oColumn.key + "\" hidden", "info", this.toString());
            }
        }
        else {
            YAHOO.log("Could not hide Column \"" + oColumn.key + "\". 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} Column instance.
 */
showColumn : function(oColumn) {
    oColumn = this.getColumn(oColumn);
    if(oColumn && oColumn.hidden) {
        // Only top-level Columns can get hidden
        if(oColumn.getTreeIndex() !== null) {
            var allrows = this.getTbodyEl().rows;
            var l = allrows.length;
            var allDescendants = this._oColumnSet.getDescendants(oColumn);
            for(var i=0; i<allDescendants.length; i++) {
                var thisColumn = allDescendants[i];
                thisColumn.hidden = false;
                
                var elTheadCell = thisColumn.getThEl();
                Dom.removeClass(elTheadCell,DT.CLASS_HIDDEN);

                // Adjust body cells (if key Column)
                var thisKeyIndex = thisColumn.getKeyIndex();
                if(thisKeyIndex !== null) {
                    for(var j=0;j<l;j++) {
                        Dom.removeClass(allrows[j].cells[thisKeyIndex],DT.CLASS_HIDDEN);
                    }
                    
                    this.setColumnWidth(thisColumn, (thisColumn._nLastWidth || thisColumn.minWidth), true);

                    // Enable interactive features
                    if(thisColumn.sortable) {
                        thisColumn.getThEl().firstChild.firstChild.firstChild.style.display = "";
                        Dom.removeClass(thisColumn.getThEl(),DT.CLASS_SORTABLE);
                    }
                    if(thisColumn.resizeable) {
                        thisColumn._ddResizer.resetResizerEl();
                        Dom.addClass(thisColumn.getResizerEl(),DT.CLASS_RESIZER);
                    }
                }
                else {
                    elTheadCell.firstChild.style.width = "";
                }

                thisColumn._nLastWidth = null;
                this.fireEvent("columnShowEvent",{column:thisColumn});
                YAHOO.log("Column \"" + oColumn.key + "\" shown", "info", this.toString());
            }
        }
        else {
            YAHOO.log("Could not show Column \"" + oColumn.key + "\". 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) {
    var nColTreeIndex = oColumn.getTreeIndex();
    if(nColTreeIndex !== null) {
        this._oChainRender.stop();
        var aOrigColumnDefs = this._oColumnSet.getDefinitions();

        oColumn = aOrigColumnDefs.splice(nColTreeIndex,1)[0];
        this._initColumnSet(aOrigColumnDefs);
        this._initTheadEls();

        this.render();
        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) Column key index.
 */
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;
    }
    
    var oColumnSet = this._oColumnSet;

    // Validate index
    if(!lang.isValue(index) || !lang.isNumber(index)) {
        index = oColumnSet.tree[0].length;
    }
    
    this._oChainRender.stop();
    var aNewColumnDefs = this._oColumnSet.getDefinitions();
    aNewColumnDefs.splice(index, 0, oColumn);
    this._initColumnSet(aNewColumnDefs);
    this._initTheadEls();
    this.render();
    this.fireEvent("columnInsertEvent",{column:oColumn,index:index});
    YAHOO.log("Column \"" + oColumn.key + "\" inserted into index " + index, "info", 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()}
            });
            oChainRender.run();       
            
            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()}
            });
            oChainRender.run();       
            
            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 DT.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)) {
        /*// Make sure previous row is unhighlighted
        var sId = oColumn.getId();
        var sLastId = this._sLastHighlightedColumnId;
        if(sLastId && (sLastId !== sId)) {
            this.unhighlightColumn(this.getColumn(sLastId));
        }*/

        //this._sLastHighlightedColumnId = sId;
            
        // 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()}
        });
        oChainRender.run();       
            
        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 DT.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()}
        });
        oChainRender.run();       
            
        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 TR element at the given index and populates with given Record data.
 *
 * @method _addTrEl
 * @param oRecord {YAHOO.widget.Record} Record instance. 
 * @param index {Number} TR index.
 * @return {HTMLElement} Reference to new TR element. 
 * @private 
 */
_addTrEl : function(oRecord, index) {
    var elNewTr = this._createTrEl(oRecord);
    if(elNewTr) {
        if (index >= 0 && index < this._elTbody.rows.length) {
            this._elTbody.insertBefore(elNewTr,
                this._elTbody.rows[index]);
            if (!index) {
                this._setFirstRow();
            }
        } else {
            this._elTbody.appendChild(elNewTr);
            this._setLastRow();
            index = this._elTbody.rows.length - 1;
        }
        this._setRowStripes(index);
    }
    return elNewTr;
},

/**
 * 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(oData && (oData.constructor == Object)) {
        var oRecord = this._oRecordSet.addRecord(oData, index);
        if(oRecord) {
            var recIndex;
            var oPaginator = this.get('paginator');

            // Paginated
            if (oPaginator instanceof Pag || this.get('paginated')) {
                recIndex = this.getRecordIndex(oRecord);
                var endRecIndex;
                if (oPaginator instanceof Pag) {
                    // Update the paginator's totalRecords
                    var totalRecords = oPaginator.get('totalRecords');
                    if (totalRecords !== Pag.VALUE_UNLIMITED) {
                        oPaginator.set('totalRecords',totalRecords + 1);
                    }

                    endRecIndex = (oPaginator.getPageRecords())[1];
                }
                // Backward compatibility
                else {
                    endRecIndex = oPaginator.startRecordIndex +
                                  oPaginator.rowsPerPage - 1;
                    this.updatePaginator();
                }

                // 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.getTrIndex(oRecord);
                if(lang.isNumber(recIndex)) {
                    // Add the TR element
                    this._oChainRender.add({
                        method: function(oArg) {
                            if((this instanceof DT) && this._sId) {
                                var elNewTr = this._addTrEl(oRecord, recIndex);
                                if(elNewTr) {
                                    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());
                                }
                            }
                        },
                        scope: this,
                        timeout: (this.get("renderLoopSize") > 0) ? 0 : -1
                    });
                    this._oChainRender.run();
                    return;
                }
            }            
        }
    }
    YAHOO.log("Could not add row with " + lang.dump(oData), "error", 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.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 instanceof Pag ||
                this.get('paginated')) {
                var endRecIndex;
                if (oPaginator instanceof Pag) {
                    // Update the paginator's totalRecords
                    var totalRecords = oPaginator.get('totalRecords');
                    if (totalRecords !== Pag.VALUE_UNLIMITED) {
                        oPaginator.set('totalRecords',totalRecords + aRecords.length);
                    }

                    endRecIndex = (oPaginator.getPageRecords())[1];
                }
                // Backward compatibility
                else {
                    endRecIndex = oPaginator.startRecordIndex +
                                  oPaginator.rowsPerPage - 1;
                    this.updatePaginator();
                }

                // 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 " + recIndex +
                        " 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
                this._oChainRender.add({
                    method: function(oArg) {
                        if((this instanceof DT) && this._sId) {
                            var i = oArg.nCurrentRow,
                                j = oArg.nCurrentRecord,
                                len = loopN > 0 ? Math.min(i + loopN,loopEnd) : loopEnd;
                            for(; i < len; ++i,++j) {
                                this._addTrEl(aRecords[j], i);
                            }
                            oArg.nCurrentRow = i;
                            oArg.nCurrentRecord = j;
                        }
                    },
                    iterations: (loopN > 0) ? Math.ceil(loopEnd/loopN) : 1,
                    argument: {nCurrentRow:recIndex,nCurrentRecord:0},
                    scope: this,
                    timeout: (loopN > 0) ? 0 : -1
                });
                this._oChainRender.add({
                    method: function() {
                        this.fireEvent("rowsAddEvent", {records:aRecords});
                        YAHOO.log("Added " + aRecords.length + 
                                " rows at index " + recIndex +
                                " with data " + lang.dump(aData), "info", this.toString());
                    },
                    scope: this,
                    timeout: -1 // Needs to run immediately after the DOM insertions above
                });
                this._oChainRender.run();
                this.hideTableMessage();                
                return;
            }            
        }
    }
    YAHOO.log("Could not add rows with " + lang.dump(aData));    
},

/**
 * 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 oldRecord, oldData, updatedRecord, elRow;

    // Get the Record directly
    if((row instanceof YAHOO.widget.Record) || (lang.isNumber(row))) {
            // Get the Record directly
            oldRecord = this._oRecordSet.getRecord(row);

            // Is this row on current page?
            elRow = this.getTrEl(oldRecord);
    }
    // Get the Record by TR element
    else {
        elRow = this.getTrEl(row);
        if(elRow) {
            oldRecord = this.getRecord(elRow);
        }
    }

    // Update the Record
    if(oldRecord) {
        // Copy data from the Record for the event that gets fired later
        var oRecordData = oldRecord.getData();
        oldData = YAHOO.widget.DataTable._cloneObject(oRecordData);

        updatedRecord = this._oRecordSet.updateRecord(oldRecord, oData);
    }
    else {
        YAHOO.log("Could not update row " + row + " with the data : " +
                lang.dump(oData), "error", this.toString());
        return;

    }

    // Update the TR only if row is on current page
    if(elRow) {
        this._oChainRender.add({
            method: function() {
                if((this instanceof DT) && this._sId) {
                    this._updateTrEl(elRow, updatedRecord);
                    //this._oChainSync.run();
                    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._oChainRender.run();
    }
    else {
        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());   
    }
},

/**
 * 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 = 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.isNumber(tracker[j]) && (tracker[j] === sRecordId)) ||
                        ((tracker[j].constructor == Object) && (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 instanceof Pag ||
                    this.get('paginated')) {
        
                    var endRecIndex;
                    if (oPaginator instanceof Pag) {
                        // Update the paginator's totalRecords
                        var totalRecords = oPaginator.get('totalRecords');
                        if (totalRecords !== Pag.VALUE_UNLIMITED) {
                            oPaginator.set('totalRecords',totalRecords - 1);
                        }
        
                        endRecIndex = (oPaginator.getPageRecords())[1];
                    } else {
                        // Backward compatibility
                        endRecIndex = oPaginator.startRecordIndex +
                                      oPaginator.rowsPerPage - 1;
        
                        this.updatePaginator();
                    }
        
                    // If the deleted record was on this or a prior page, re-render
                    if (nRecordIndex <= endRecIndex) {
                        this.render();
                    }
                    return;
                }
                else {
                    if(lang.isNumber(nTrIndex)) {
                        this._oChainRender.add({
                            method: function() {
                                if((this instanceof DT) && this._sId) {
                                    var isLast = (nTrIndex == this.getLastTrEl().sectionRowIndex);
                                    this._deleteTrEl(nTrIndex);
                    
                                    // Empty body
                                    if(this._elTbody.rows.length === 0) {
                                        this.showTableMessage(DT.MSG_EMPTY, DT.CLASS_EMPTY);
                                    }
                                    // Update UI
                                    else {
                                        // 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._oChainRender.run();
                        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 = 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.isNumber(tracker[j]) && (tracker[j] === sRecordId)) ||
                        ((tracker[j].constructor == Object) && (tracker[j].recordId === sRecordId))) {
                    tracker.splice(j,1);
                }
            }
    
            // Delete Record from RecordSet
            var highIndex = nRecordIndex+1;
            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;
            }
            else {
                count = 1;
            }
            
            var aData = this._oRecordSet.deleteRecords(lowIndex, count);
    
            // Update the UI
            if(aData) {
                // If paginated and the deleted row was on this or a prior page, just
                // re-render
                var oPaginator = this.get('paginator');
                if (oPaginator instanceof Pag ||
                    this.get('paginated')) {
        
                    var endRecIndex;
                    if (oPaginator instanceof Pag) {
                        // Update the paginator's totalRecords
                        var totalRecords = oPaginator.get('totalRecords');
                        if (totalRecords !== Pag.VALUE_UNLIMITED) {
                            oPaginator.set('totalRecords',totalRecords - 1);
                        }
        
                        endRecIndex = (oPaginator.getPageRecords())[1];
                    } else {
                        // Backward compatibility
                        endRecIndex = oPaginator.startRecordIndex +
                                      oPaginator.rowsPerPage - 1;
        
                        this.updatePaginator();
                    }
        
                    // If the lowest deleted record was on this or a prior page, re-render
                    if (lowIndex <= endRecIndex) {
                        this.render();
                    }
                    return;
                }
                else {
                    if(lang.isNumber(nTrIndex)) {
                        // Delete the TR elements starting with highest index
                        var loopN = this.get("renderLoopSize");
                        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() {    
                                // Empty body
                                if(this._elTbody.rows.length === 0) {
                                    this.showTableMessage(DT.MSG_EMPTY, DT.CLASS_EMPTY);
                                }
                                else {
                                    this._setFirstRow();
                                    this._setLastRow();
                                    this._setRowStripes();
                                }
                                
                                this.fireEvent("rowsDeleteEvent", {recordIndex:count,
                                oldData:aData, count:nTrIndex});
                                YAHOO.log("DataTable row deleted: Record ID = " + sRecordId +
                                    ", Record index = " + nRecordIndex +
                                    ", page row index = " + nTrIndex, "info", this.toString());
                            },
                            scope: this,
                            timeout: -1 // Needs to run immediately after the DOM deletions above
                        });
                        this._oChainRender.run();
                        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 elCell {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(elCell, oRecord, oColumn) {
    if(!(oRecord instanceof YAHOO.widget.Record)) {
        oRecord = this.getRecord(elCell);
    }
    if(!(oColumn instanceof YAHOO.widget.Column)) {
        oColumn = this._oColumnSet.getColumn(elCell.parentNode.yuiColumnKey);
    }

    if(oRecord && oColumn) {
        var sKey = oColumn.key;
        var oData = oRecord.getData(sKey);

        // Add classNames
        var aClasses;
        if(lang.isString(oColumn.className)) {
            aClasses = [oColumn.className];
        }
        else if(lang.isArray(oColumn.className)) {
            aClasses = oColumn.className;
        }
        else {
            aClasses = [];
        }

        //TODO: document special keys will get stripped here
        aClasses[aClasses.length] = "yui-dt-col-"+sKey.replace(/[^\w\-.:]/g,"");
        
        aClasses[aClasses.length] = "yui-dt-col-"+oColumn.getId();
        
        aClasses[aClasses.length] = DT.CLASS_LINER;

        if(oColumn.sortable) {
            aClasses[aClasses.length] = DT.CLASS_SORTABLE;
        }
        if(oColumn.resizeable) {
            aClasses[aClasses.length] = DT.CLASS_RESIZEABLE;
        }
        if(oColumn.editor) {
            aClasses[aClasses.length] = DT.CLASS_EDITABLE;
        }

        elCell.className = "";
        Dom.addClass(elCell, aClasses.join(" "));


        var fnFormatter = typeof oColumn.formatter === 'function' ?
                          oColumn.formatter :
                          DT.Formatter[oColumn.formatter+''];

        // Apply special formatter
        if(fnFormatter) {
            fnFormatter.call(this, elCell, oRecord, oColumn, oData);
        }
        else {
            elCell.innerHTML = oData === undefined ||
                               oData === null ||
                               (typeof oData === 'number' && isNaN(oData)) ?
                                "" : oData.toString();
        }

        this.fireEvent("cellFormatEvent", {record:oRecord, column:oColumn, key:sKey, el:elCell});
    }
    else {
        YAHOO.log("Could not format cell " + elCell, "error", this.toString());
    }
},


















































// PAGINATION

/**
 * Delegates the Pag changeRequest events to the configured
 * handler.
 * @method onPaginatorChange
 * @param {Object} an object literal describing the proposed pagination state
 */
onPaginatorChange : function (oState) {
    var handler = this.get('paginationEventHandler');

    handler(oState,this);
},


















































// SELECTION/HIGHLIGHTING

/**
 * ID string of last highlighted cell element
 *
 * @property _sLastHighlightedTdElId
 * @type String
 * @private
 */
//_sLastHighlightedTdElId : 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, columnId:sColumnId})
 *
 * @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, columnId:sColumnId}.
 *
 * @property _oAnchorCell
 * @type Object
 * @private
 */
_oAnchorCell : null,

/**
 * Convenience method to remove the class DT.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.getColumnById(oTriggerCell.columnId);
                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