Yahoo! UI Library

DataTable Widget  2.5.2

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 message containers.
     *
     * @property DataTable.CLASS_MSG
     * @type String
     * @static
     * @final
     * @default "yui-dt-msg"
     */
    CLASS_MSG : "yui-dt-msg",

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

    /**
     * Color assigned to header filler on scrollable tables when columnFiller
     * is set to true.
     *
     * @property DataTable.CLASS_COLUMN_FILLER_COLOR
     * @type String
     * @static
     * @final
     * @default "#F2F2F2"
     */
    COLOR_COLUMNFILLER : "#F2F2F2",

    /**
     * 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 {DataTable} 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 Paginator 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 Paginator 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 {DataTable} 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 {DataTable} 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 {DataTable} 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 =