# Yahoo! UI Library

### DataTable Widget 2.6.0

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

Show Private Show Protected Show Deprecated

    (function () {

    var lang   = YAHOO.lang,
        util   = YAHOO.util,
        widget = YAHOO.widget,
        ua     = YAHOO.env.ua,

        Dom    = util.Dom,
        Ev     = util.Event,
        DS     = util.DataSourceBase,
        DT     = widget.DataTable,
        Pag    = widget.Paginator;

    /**
     * The ScrollingDataTable class extends the DataTable class to provide
     * functionality for x-scrolling, y-scrolling, and xy-scrolling.
     *
     * @namespace YAHOO.widget
     * @class ScrollingDataTable
     * @extends YAHOO.widget.DataTable
     * @constructor
     * @param elContainer {HTMLElement} Container element for the TABLE.
     * @param aColumnDefs {Object[]} Array of object literal Column definitions.
     * @param oDataSource {YAHOO.util.DataSource} DataSource instance.
     * @param oConfigs {object} (optional) Object literal of configuration values.
     */
    widget.ScrollingDataTable = function(elContainer,aColumnDefs,oDataSource,oConfigs) {
        oConfigs = oConfigs || {};

        // Prevent infinite loop
        if(oConfigs.scrollable) {
            oConfigs.scrollable = false;
        }

        widget.ScrollingDataTable.superclass.constructor.call(this, elContainer,aColumnDefs,oDataSource,oConfigs);

        // Once per instance
        this.subscribe("columnShowEvent", this._onColumnChange);
    };

    var SDT = widget.ScrollingDataTable;

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public constants
    //
    /////////////////////////////////////////////////////////////////////////////
    lang.augmentObject(SDT, {

        /**
         * Class name assigned to inner DataTable header container.
         *
         * @property DataTable.CLASS_HEADER
         * @type String
         * @static
         * @final
         * @default "yui-dt-hd"
         */
        CLASS_HEADER : "yui-dt-hd",

        /**
         * Class name assigned to inner DataTable body container.
         *
         * @property DataTable.CLASS_BODY
         * @type String
         * @static
         * @final
         * @default "yui-dt-bd"
         */
        CLASS_BODY : "yui-dt-bd"
    });

    lang.extend(SDT, DT, {

    /**
     * Container for fixed header TABLE element.
     *
     * @property _elHdContainer
     * @type HTMLElement
     * @private
     */
    _elHdContainer : null,

    /**
     * Fixed header TABLE element.
     *
     * @property _elHdTable
     * @type HTMLElement
     * @private
     */
    _elHdTable : null,

    /**
     * Container for scrolling body TABLE element.
     *
     * @property _elBdContainer
     * @type HTMLElement
     * @private
     */
    _elBdContainer : null,

    /**
     * Body THEAD element.
     *
     * @property _elBdThead
     * @type HTMLElement
     * @private
     */
    _elBdThead : null,

    /**
     * Offscreen container to temporarily clone SDT for auto-width calculation.
     *
     * @property _elTmpContainer
     * @type HTMLElement
     * @private
     */
    _elTmpContainer : null,

    /**
     * Offscreen TABLE element for auto-width calculation.
     *
     * @property _elTmpTable
     * @type HTMLElement
     * @private
     */
    _elTmpTable : null,

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

    /////////////////////////////////////////////////////////////////////////////
    //
    // Superclass methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Implementation of Element's abstract method. Sets up config values.
     *
     * @method initAttributes
     * @param oConfigs {Object} (Optional) Object literal definition of configuration values.
     * @private
     */

    initAttributes : function(oConfigs) {
        oConfigs = oConfigs || {};
        SDT.superclass.initAttributes.call(this, oConfigs);

        /**
        * @attribute width
        * @description Table width for scrollable tables. Note: When setting width
        * and height at runtime, please set height first.
        * @type String
        */
        this.setAttributeConfig("width", {
            value: null,
            validator: lang.isString,
            method: function(oParam) {
                if(this._elHdContainer && this._elBdContainer) {
                    this._elHdContainer.style.width = oParam;
                    this._elBdContainer.style.width = oParam;
                    this._syncScrollX();
                    this._syncScrollOverhang();
                }
            }
        });

        /**
        * @attribute height
        * @description Table body height for scrollable tables, not including headers.
        * @type String
        */
        this.setAttributeConfig("height", {
            value: null,
            validator: lang.isString,
            method: function(oParam) {
                if(this._elHdContainer && this._elBdContainer) {
                    this._elBdContainer.style.height = oParam;
                    this._syncScrollX();
                    this._syncScrollY();
                    this._syncScrollOverhang();
                }
            }
        });

        /**
        * @attribute COLOR_COLUMNFILLER
        * @description CSS color value assigned to header filler on scrollable tables.
        * @type String
        * @default "#F2F2F2"
        */
        this.setAttributeConfig("COLOR_COLUMNFILLER", {
            value: "#F2F2F2",
            validator: lang.isString,
            method: function(oParam) {
                this._elHdContainer.style.backgroundColor = oParam;
            }
        });
    },

    /**
     * Initializes DOM elements for a ScrollingDataTable, including creation of
     * two separate TABLE elements.
     *
     * @method _initDomElements
     * @param elContainer {HTMLElement | String} HTML DIV element by reference or ID.
     * return {Boolean} False in case of error, otherwise true
     * @private
     */
    _initDomElements : function(elContainer) {
        // Outer and inner containers
        this._initContainerEl(elContainer);
        if(this._elContainer && this._elHdContainer && this._elBdContainer) {
            // TABLEs
            this._initTableEl();

            if(this._elHdTable && this._elTable) {
                // COLGROUPs
                ///this._initColgroupEl(this._elHdTable, this._elTable);
                this._initColgroupEl(this._elHdTable);

                // THEADs
                this._initTheadEl(this._elHdTable, this._elTable);

                // Primary TBODY
                this._initTbodyEl(this._elTable);
                // Message TBODY
                this._initMsgTbodyEl(this._elTable);
            }
        }
        if(!this._elContainer || !this._elTable || !this._elColgroup ||  !this._elThead || !this._elTbody || !this._elMsgTbody ||
                !this._elHdTable || !this._elBdThead) {
            YAHOO.log("Could not instantiate DataTable due to an invalid DOM elements", "error", this.toString());
            return false;
        }
        else {
            return true;
        }
    },

    /**
     * Destroy's the DataTable outer and inner container elements, if available.
     *
     * @method _destroyContainerEl
     * @param elContainer {HTMLElement} Reference to the container element.
     * @private
     */
    _destroyContainerEl : function(elContainer) {
        Dom.removeClass(elContainer, DT.CLASS_SCROLLABLE);
        SDT.superclass._destroyContainerEl.call(this, elContainer);
        this._elHdContainer = null;
        this._elBdContainer = null;
    },

    /**
     * Initializes the DataTable outer container element and creates inner header
     * and body container elements.
     *
     * @method _initContainerEl
     * @param elContainer {HTMLElement | String} HTML DIV element by reference or ID.
     * @private
     */
    _initContainerEl : function(elContainer) {
        SDT.superclass._initContainerEl.call(this, elContainer);

        if(this._elContainer) {
            elContainer = this._elContainer; // was constructor input, now is DOM ref
            Dom.addClass(elContainer, DT.CLASS_SCROLLABLE);

            // Container for header TABLE
            var elHdContainer = document.createElement("div");
            elHdContainer.style.width = this.get("width") || "";
            elHdContainer.style.backgroundColor = this.get("COLOR_COLUMNFILLER");
            Dom.addClass(elHdContainer, SDT.CLASS_HEADER);
            this._elHdContainer = elHdContainer;
            elContainer.appendChild(elHdContainer);

            // Container for body TABLE
            var elBdContainer = document.createElement("div");
            elBdContainer.style.width = this.get("width") || "";
            elBdContainer.style.height = this.get("height") || "";
            Dom.addClass(elBdContainer, SDT.CLASS_BODY);
            Ev.addListener(elBdContainer, "scroll", this._onScroll, this); // to sync horiz scroll headers
            this._elBdContainer = elBdContainer;
            elContainer.appendChild(elBdContainer);
        }
    },

    /**
     * Creates HTML markup CAPTION element.
     *
     * @method _initCaptionEl
     * @param sCaption {String} Text for caption.
     * @private
     */
    _initCaptionEl : function(sCaption) {
        // Not yet supported
        /*if(this._elHdTable && sCaption) {
            // Create CAPTION element
            if(!this._elCaption) {
                this._elCaption = this._elHdTable.createCaption();
            }
            // Set CAPTION value
            this._elCaption.innerHTML = sCaption;
        }
        else if(this._elCaption) {
            this._elCaption.parentNode.removeChild(this._elCaption);
        }*/
    },

    /**
     * Destroy's the DataTable head TABLE element, if available.
     *
     * @method _destroyHdTableEl
     * @private
     */
    _destroyHdTableEl : function() {
        var elTable = this._elHdTable;
        if(elTable) {
            Ev.purgeElement(elTable, true);
            elTable.parentNode.removeChild(elTable);

            // A little out of place, but where else can we null out these extra elements?
            ///this._elBdColgroup = null;
            this._elBdThead = null;
        }
    },

    /**
     * Initializes ScrollingDataTable TABLE elements into the two inner containers.
     *
     * @method _initTableEl
     * @private
     */
    _initTableEl : function() {
        // Head TABLE
        if(this._elHdContainer) {
            this._destroyHdTableEl();

            // Create TABLE
            this._elHdTable = this._elHdContainer.appendChild(document.createElement("table"));
        }
        // Body TABLE
        SDT.superclass._initTableEl.call(this, this._elBdContainer);
    },

    /**
     * Initializes ScrollingDataTable THEAD elements into the two inner containers.
     *
     * @method _initTheadEl
     * @param elHdTable {HTMLElement} (optional) Fixed header TABLE element reference.
     * @param elTable {HTMLElement} (optional) TABLE element reference.
     * @private
     */
    _initTheadEl : function(elHdTable, elTable) {
        elHdTable = elHdTable || this._elHdTable;
        elTable = elTable || this._elTable;

        // Scrolling body's THEAD
        this._initBdTheadEl(elTable);
        // Standard fixed head THEAD
        SDT.superclass._initTheadEl.call(this, elHdTable);
    },

    /**
     * SDT changes ID so as not to duplicate the accessibility TH IDs.
     *
     * @method _initThEl
     * @param elTh {HTMLElement} TH element reference.
     * @param oColumn {YAHOO.widget.Column} Column object.
     * @private
     */
    _initThEl : function(elTh, oColumn) {
        SDT.superclass._initThEl.call(this, elTh, oColumn);
        elTh.id = this.getId() +"-fixedth-" + oColumn.getSanitizedKey(); // Needed for getColumn by TH and ColumnDD
    },

    /**
     * Destroy's the DataTable body THEAD element, if available.
     *
     * @method _destroyBdTheadEl
     * @private
     */
    _destroyBdTheadEl : function() {
        var elBdThead = this._elBdThead;
        if(elBdThead) {
            var elTable = elBdThead.parentNode;
            Ev.purgeElement(elBdThead, true);
            elTable.removeChild(elBdThead);
            this._elBdThead = null;

            this._destroyColumnHelpers();
        }
    },

    /**
     * Initializes body THEAD element.
     *
     * @method _initBdTheadEl
     * @param elTable {HTMLElement} TABLE element into which to create THEAD.
     * @return {HTMLElement} Initialized THEAD element.
     * @private
     */
    _initBdTheadEl : function(elTable) {
        if(elTable) {
            // Destroy previous
            this._destroyBdTheadEl();

            var elThead = elTable.insertBefore(document.createElement("thead"), elTable.firstChild);

            // Add TRs to the THEAD;
            var oColumnSet = this._oColumnSet,
                colTree = oColumnSet.tree,
                elTh, elTheadTr, oColumn, i, j, k, len;

            for(i=0, k=colTree.length; i<k; i++) {
                elTheadTr = elThead.appendChild(document.createElement("tr"));

                // ...and create TH cells
                for(j=0, len=colTree[i].length; j<len; j++) {
                    oColumn = colTree[i][j];
                    elTh = elTheadTr.appendChild(document.createElement("th"));
                    this._initBdThEl(elTh,oColumn,i,j);
                }
            }
            this._elBdThead = elThead;
            YAHOO.log("Accessibility TH cells for " + this._oColumnSet.keys.length + " keys created","info",this.toString());
        }
    },

    /**
     * Populates TH element for the body THEAD element.
     *
     * @method _initBdThEl
     * @param elTh {HTMLElement} TH element reference.
     * @param oColumn {YAHOO.widget.Column} Column object.
     * @private
     */
    _initBdThEl : function(elTh, oColumn) {
        elTh.id = this.getId()+"-th-" + oColumn.getSanitizedKey(); // Needed for accessibility
        elTh.rowSpan = oColumn.getRowspan();
        elTh.colSpan = oColumn.getColspan();
        // Assign abbr attribute
        if(oColumn.abbr) {
            elTh.abbr = oColumn.abbr;
        }

        // TODO: strip links and form elements
        var sKey = oColumn.getKey();
        var sLabel = lang.isValue(oColumn.label) ? oColumn.label : sKey;
        elTh.innerHTML = sLabel;
    },

    /**
     * Initializes ScrollingDataTable TBODY element for data
     *
     * @method _initTbodyEl
     * @param elTable {HTMLElement} TABLE element into which to create TBODY .
     * @private
     */
    _initTbodyEl : function(elTable) {
        SDT.superclass._initTbodyEl.call(this, elTable);

        // Bug 2105534 - Safari gap
        elTable.style.marginTop = "-"+this._elTbody.offsetTop+"px";
    },

    /**
     * Sets focus on the given element.
     *
     * @method _focusEl
     * @param el {HTMLElement} Element.
     * @private
     */
    _focusEl : function(el) {
        el = el || this._elTbody;
        var oSelf = this;
        this._storeScrollPositions();
        // http://developer.mozilla.org/en/docs/index.php?title=Key-navigable_custom_DHTML_widgets
        // The timeout is necessary in both IE and Firefox 1.5, to prevent scripts from doing
        // strange unexpected things as the user clicks on buttons and other controls.

        // Bug 1921135: Wrap the whole thing in a setTimeout
        setTimeout(function() {
            setTimeout(function() {
                try {
                    el.focus();
                    oSelf._restoreScrollPositions();
                }
                catch(e) {
                }
            },0);
        }, 0);
    },

    /**
     * Internal wrapper calls run() on render Chain instance.
     *
     * @method _runRenderChain
     * @private
     */
    _runRenderChain : function() {
        this._storeScrollPositions();
        this._oChainRender.run();
    },

    /**
     * Stores scroll positions so they can be restored after a render.
     *
     * @method _storeScrollPositions
     * @private
     */
     _storeScrollPositions : function() {
        this._nScrollTop = this._elBdContainer.scrollTop;
        this._nScrollLeft = this._elBdContainer.scrollLeft;
    },

    /**
     * Restores scroll positions to stored value.
     *
     * @method _retoreScrollPositions
     * @private
     */
     _restoreScrollPositions : function() {
        // Reset scroll positions
        if(this._nScrollTop) {
            this._elBdContainer.scrollTop = this._nScrollTop;
            this._nScrollTop = null;
        }
        if(this._nScrollLeft) {
            this._elBdContainer.scrollLeft = this._nScrollLeft;
            this._nScrollLeft = null;
        }
    },

    /**
     * Helper function calculates and sets a validated width for a Column in a ScrollingDataTable.
     *
     * @method _validateColumnWidth
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param elTd {HTMLElement} TD element to validate against.
     * @private
     */
    _validateColumnWidth : function(oColumn, elTd) {
        // Only Columns without widths that are not hidden
        if(!oColumn.width && !oColumn.hidden) {
            var elTh = oColumn.getThEl();
            // Unset a calculated auto-width
            if(oColumn._calculatedWidth) {
                this._setColumnWidth(oColumn, "auto", "visible");
            }
            // Compare auto-widths
            if(elTh.offsetWidth !== elTd.offsetWidth) {
                var elWider = (elTh.offsetWidth > elTd.offsetWidth) ?
                        oColumn.getThLinerEl() : elTd.firstChild;

                // Grab the wider liner width, unless the minWidth is wider
                var newWidth = Math.max(0,
                    (elWider.offsetWidth -(parseInt(Dom.getStyle(elWider,"paddingLeft"),10)|0) - (parseInt(Dom.getStyle(elWider,"paddingRight"),10)|0)),
                    oColumn.minWidth);

                var sOverflow = 'visible';

                // Now validate against maxAutoWidth
                if((oColumn.maxAutoWidth > 0) && (newWidth > oColumn.maxAutoWidth)) {
                    newWidth = oColumn.maxAutoWidth;
                    sOverflow = "hidden";
                }

                // Set to the wider auto-width
                this._elTbody.style.display = "none";
                this._setColumnWidth(oColumn, newWidth+'px', sOverflow);
                oColumn._calculatedWidth = newWidth;
                this._elTbody.style.display = "";
            }
        }
    },

    /**
     * For one or all Columns of a ScrollingDataTable, when Column is not hidden,
     * and width is not set, syncs widths of header and body cells and
     * validates that width against minWidth and/or maxAutoWidth as necessary.
     *
     * @method validateColumnWidths
     * @param oArg.column {YAHOO.widget.Column} (optional) One Column to validate. If null, all Columns' widths are validated.
     */
    validateColumnWidths : function(oColumn) {
        // Validate there is at least one TR with proper TDs
        var allKeys   = this._oColumnSet.keys,
            allKeysLength = allKeys.length,
            elRow     = this.getFirstTrEl();

        // Reset overhang for IE
        if(ua.ie) {
            this._setOverhangValue(1);
        }

        if(allKeys && elRow && (elRow.childNodes.length === allKeysLength)) {
            // Temporarily unsnap container since it causes inaccurate calculations
            var sWidth = this.get("width");
            if(sWidth) {
                this._elHdContainer.style.width = "";
                this._elBdContainer.style.width = "";
            }
            this._elContainer.style.width = "";

            //Validate just one Column
            if(oColumn && lang.isNumber(oColumn.getKeyIndex())) {
                this._validateColumnWidth(oColumn, elRow.childNodes[oColumn.getKeyIndex()]);
            }
            // Iterate through all Columns to unset calculated widths in one pass
            else {
                var elTd, todos = [], thisTodo, i, len;
                for(i=0; i<allKeysLength; i++) {
                    oColumn = allKeys[i];
                    // Only Columns without widths that are not hidden, unset a calculated auto-width
                    if(!oColumn.width && !oColumn.hidden && oColumn._calculatedWidth) {
                        todos[todos.length] = oColumn;
                    }
                }

                this._elTbody.style.display = "none";
                for(i=0, len=todos.length; i<len; i++) {
                    this._setColumnWidth(todos[i], "auto", "visible");
                }
                this._elTbody.style.display = "";

                todos = [];

                // Iterate through all Columns and make the store the adjustments to make in one pass
                for(i=0; i<allKeysLength; i++) {
                    oColumn = allKeys[i];
                    elTd = elRow.childNodes[i];
                    // Only Columns without widths that are not hidden
                    if(!oColumn.width && !oColumn.hidden) {
                        var elTh = oColumn.getThEl();

                        // Compare auto-widths
                        if(elTh.offsetWidth !== elTd.offsetWidth) {
                            var elWider = (elTh.offsetWidth > elTd.offsetWidth) ?
                                    oColumn.getThLinerEl() : elTd.firstChild;

                            // Grab the wider liner width, unless the minWidth is wider
                            var newWidth = Math.max(0,
                                (elWider.offsetWidth -(parseInt(Dom.getStyle(elWider,"paddingLeft"),10)|0) - (parseInt(Dom.getStyle(elWider,"paddingRight"),10)|0)),
                                oColumn.minWidth);

                            var sOverflow = 'visible';

                            // Now validate against maxAutoWidth
                            if((oColumn.maxAutoWidth > 0) && (newWidth > oColumn.maxAutoWidth)) {
                                newWidth = oColumn.maxAutoWidth;
                                sOverflow = "hidden";
                            }

                            todos[todos.length] = [oColumn, newWidth, sOverflow];
                        }
                    }
                }

                this._elTbody.style.display = "none";
                for(i=0, len=todos.length; i<len; i++) {
                    thisTodo = todos[i];
                    // Set to the wider auto-width
                    this._setColumnWidth(thisTodo[0], thisTodo[1]+"px", thisTodo[2]);
                    thisTodo[0]._calculatedWidth = thisTodo[1];
                }
                this._elTbody.style.display = "";
            }

            // Resnap unsnapped containers
            if(sWidth) {
                this._elHdContainer.style.width = sWidth;
                this._elBdContainer.style.width = sWidth;
            }
        }

        this._syncScroll();
        this._restoreScrollPositions();
    },

    /**
     * Syncs padding around scrollable tables, including Column header right-padding
     * and container width and height.
     *
     * @method _syncScroll
     * @private
     */
    _syncScroll : function() {
        this._syncScrollX();
        this._syncScrollY();
        this._syncScrollOverhang();
        if(ua.opera) {
            // Bug 1925874
            this._elHdContainer.scrollLeft = this._elBdContainer.scrollLeft;
            if(!this.get("width")) {
                // Bug 1926125
                document.body.style += '';
            }
        }
     },

    /**
     * Snaps container width for y-scrolling tables.
     *
     * @method _syncScrollY
     * @private
     */
    _syncScrollY : function() {
        var elTbody = this._elTbody,
            elBdContainer = this._elBdContainer;

        // X-scrolling not enabled
        if(!this.get("width")) {
            // Snap outer container width to content
            this._elContainer.style.width =
                    (elBdContainer.scrollHeight > elBdContainer.clientHeight) ?
                    // but account for y-scrollbar since it is visible
                    (elTbody.parentNode.clientWidth + 19) + "px" :
                    // no y-scrollbar, just borders
                    (elTbody.parentNode.clientWidth + 2) + "px";
        }
    },

    /**
     * Snaps container height for x-scrolling tables in IE. Syncs message TBODY width.
     *
     * @method _syncScrollX
     * @private
     */
    _syncScrollX : function() {
        var elTbody = this._elTbody,
            elBdContainer = this._elBdContainer;

        // IE 6 and 7 only when y-scrolling not enabled
        if(!this.get("height") && (ua.ie)) {
            // Snap outer container height to content
            elBdContainer.style.height =
                    // but account for x-scrollbar if it is visible
                    (elBdContainer.scrollWidth > elBdContainer.offsetWidth ) ?
                    (elTbody.parentNode.offsetHeight + 18) + "px" :
                    elTbody.parentNode.offsetHeight + "px";
        }

        // Sync message tbody
        if(this._elTbody.rows.length === 0) {
            this._elMsgTbody.parentNode.style.width = this.getTheadEl().parentNode.offsetWidth + "px";
        }
        else {
            this._elMsgTbody.parentNode.style.width = "";
        }
    },

    /**
     * Adds/removes Column header overhang as necesary.
     *
     * @method _syncScrollOverhang
     * @private
     */
    _syncScrollOverhang : function() {
        var elBdContainer = this._elBdContainer,
            // Overhang should be either 1 (default) or 18px, depending on the location of the right edge of the table
            nPadding = 1;

        // Y-scrollbar is visible, which is when the overhang needs to jut out
        if((elBdContainer.scrollHeight > elBdContainer.clientHeight) &&
            // X-scrollbar is also visible, which means the right is jagged, not flush with the Column
            (elBdContainer.scrollWidth > elBdContainer.clientWidth)) {
            nPadding = 18;
        }

        this._setOverhangValue(nPadding);

    },

    /**
     * Sets Column header overhang to given width.
     *
     * @method _setOverhangValue
     * @param nBorderWidth {Number} Value of new border for overhang.
     * @private
     */
    _setOverhangValue : function(nBorderWidth) {
        var aLastHeaders = this._oColumnSet.headers[this._oColumnSet.headers.length-1] || [],
            len = aLastHeaders.length,
            sPrefix = this._sId+"-fixedth-",
            sValue = nBorderWidth + "px solid " + this.get("COLOR_COLUMNFILLER");

        this._elThead.style.display = "none";
        for(var i=0; i<len; i++) {
            Dom.get(sPrefix+aLastHeaders[i]).style.borderRight = sValue;
        }
        this._elThead.style.display = "";
    },

    /**
     * Returns DOM reference to the DataTable's fixed header container element.
     *
     * @method getHdContainerEl
     * @return {HTMLElement} Reference to DIV element.
     */
    getHdContainerEl : function() {
        return this._elHdContainer;
    },

    /**
     * Returns DOM reference to the DataTable's scrolling body container element.
     *
     * @method getBdContainerEl
     * @return {HTMLElement} Reference to DIV element.
     */
    getBdContainerEl : function() {
        return this._elBdContainer;
    },

    /**
     * Returns DOM reference to the DataTable's fixed header TABLE element.
     *
     * @method getHdTableEl
     * @return {HTMLElement} Reference to TABLE element.
     */
    getHdTableEl : function() {
        return this._elHdTable;
    },

    /**
     * Returns DOM reference to the DataTable's scrolling body TABLE element.
     *
     * @method getBdTableEl
     * @return {HTMLElement} Reference to TABLE element.
     */
    getBdTableEl : function() {
        return this._elTable;
    },

    /**
     * Disables ScrollingDataTable UI.
     *
     * @method disable
     */
    disable : function() {
        var elMask = this._elMask;
        elMask.style.width = this._elBdContainer.offsetWidth + "px";
        elMask.style.height = this._elHdContainer.offsetHeight + this._elBdContainer.offsetHeight + "px";
        elMask.style.display = "";
        this.fireEvent("disableEvent");
    },

    /**
     * Removes given Column. NOTE: You cannot remove nested Columns. You can only remove
     * non-nested Columns, and top-level parent Columns (which will remove all
     * children Columns).
     *
     * @method removeColumn
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @return oColumn {YAHOO.widget.Column} Removed Column instance.
     */
    removeColumn : function(oColumn) {
        // Store scroll pos
        var hdPos = this._elHdContainer.scrollLeft;
        var bdPos = this._elBdContainer.scrollLeft;

        // Call superclass method
        oColumn = SDT.superclass.removeColumn.call(this, oColumn);

        // Restore scroll pos
        this._elHdContainer.scrollLeft = hdPos;
        this._elBdContainer.scrollLeft = bdPos;

        return oColumn;
    },

    /**
     * Inserts given Column at the index if given, otherwise at the end. NOTE: You
     * can only add non-nested Columns and top-level parent Columns. You cannot add
     * a nested Column to an existing parent.
     *
     * @method insertColumn
     * @param oColumn {Object | YAHOO.widget.Column} Object literal Column
     * definition or a Column instance.
     * @param index {Number} (optional) New tree index.
     * @return oColumn {YAHOO.widget.Column} Inserted Column instance.
     */
    insertColumn : function(oColumn, index) {
        // Store scroll pos
        var hdPos = this._elHdContainer.scrollLeft;
        var bdPos = this._elBdContainer.scrollLeft;

        // Call superclass method
        var oNewColumn = SDT.superclass.insertColumn.call(this, oColumn, index);

        // Restore scroll pos
        this._elHdContainer.scrollLeft = hdPos;
        this._elBdContainer.scrollLeft = bdPos;

        return oNewColumn;
    },

    /**
     * Removes given Column and inserts into given tree index. NOTE: You
     * can only reorder non-nested Columns and top-level parent Columns. You cannot
     * reorder a nested Column to an existing parent.
     *
     * @method reorderColumn
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param index {Number} New tree index.
     */
    reorderColumn : function(oColumn, index) {
        // Store scroll pos
        var hdPos = this._elHdContainer.scrollLeft;
        var bdPos = this._elBdContainer.scrollLeft;

        // Call superclass method
        var oNewColumn = SDT.superclass.reorderColumn.call(this, oColumn, index);

        // Restore scroll pos
        this._elHdContainer.scrollLeft = hdPos;
        this._elBdContainer.scrollLeft = bdPos;

        return oNewColumn;
    },

    /**
     * Sets given Column to given pixel width. If new width is less than minWidth
     * width, sets to minWidth. Updates oColumn.width value.
     *
     * @method setColumnWidth
     * @param oColumn {YAHOO.widget.Column} Column instance.
     * @param nWidth {Number} New width in pixels.
     */
    setColumnWidth : function(oColumn, nWidth) {
        oColumn = this.getColumn(oColumn);
        if(oColumn) {
            // Validate new width against minWidth
            if(lang.isNumber(nWidth)) {
                nWidth = (nWidth > oColumn.minWidth) ? nWidth : oColumn.minWidth;

                // Save state
                oColumn.width = nWidth;

                // Resize the DOM elements
                this._setColumnWidth(oColumn, nWidth+"px");
                this._syncScroll();

                this.fireEvent("columnSetWidthEvent",{column:oColumn,width:nWidth});
                YAHOO.log("Set width of Column " + oColumn + " to " + nWidth + "px", "info", this.toString());
                return;
            }
            // Unsets a width to auto-size
            else if(nWidth === null) {
                // Save state
                oColumn.width = nWidth;

                // Resize the DOM elements
                this._setColumnWidth(oColumn, "auto");
                this.validateColumnWidths(oColumn);
                this.fireEvent("columnUnsetWidthEvent",{column:oColumn});
                YAHOO.log("Column " + oColumn + " width unset", "info", this.toString());

                return;
            }
        }
        YAHOO.log("Could not set width of Column " + oColumn + " to " + nWidth + "px", "warn", this.toString());
    },

    /**
     * Displays message within secondary TBODY.
     *
     * @method showTableMessage
     * @param sHTML {String} (optional) Value for innerHTMlang.
     * @param sClassName {String} (optional) Classname.
     */
    showTableMessage : function(sHTML, sClassName) {
        var elCell = this._elMsgTd;
        if(lang.isString(sHTML)) {
            elCell.firstChild.innerHTML = sHTML;
        }
        if(lang.isString(sClassName)) {
            Dom.addClass(elCell.firstChild, sClassName);
        }

        // Needed for SDT only
        var elThead = this.getTheadEl();
        var elTable = elThead.parentNode;
        var newWidth = elTable.offsetWidth;
        this._elMsgTbody.parentNode.style.width = this.getTheadEl().parentNode.offsetWidth + "px";

        this._elMsgTbody.style.display = "";

        this.fireEvent("tableMsgShowEvent", {html:sHTML, className:sClassName});
        YAHOO.log("DataTable showing message: " + sHTML, "info", this.toString());
    },

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private Custom Event Handlers
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Handles Column mutations
     *
     * @method onColumnChange
     * @param oArgs {Object} Custom Event data.
     */
    _onColumnChange : function(oArg) {
        // Figure out which Column changed
        var oColumn = (oArg.column) ? oArg.column :
                (oArg.editor) ? oArg.editor.column : null;
        this._storeScrollPositions();
        this.validateColumnWidths(oColumn);
    },

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

    /**
     * Syncs scrolltop and scrollleft of all TABLEs.
     *
     * @method _onScroll
     * @param e {HTMLEvent} The scroll event.
     * @param oSelf {YAHOO.widget.ScrollingDataTable} ScrollingDataTable instance.
     * @private
     */
    _onScroll : function(e, oSelf) {
        oSelf._elHdContainer.scrollLeft = oSelf._elBdContainer.scrollLeft;

        if(oSelf._oCellEditor && oSelf._oCellEditor.isActive) {
            oSelf.fireEvent("editorBlurEvent", {editor:oSelf._oCellEditor});
            oSelf.cancelCellEditor();
        }

        var elTarget = Ev.getTarget(e);
        var elTag = elTarget.nodeName.toLowerCase();
        oSelf.fireEvent("tableScrollEvent", {event:e, target:elTarget});
    },

    /**
     * Handles keydown events on the THEAD element.
     *
     * @method _onTheadKeydown
     * @param e {HTMLEvent} The key event.
     * @param oSelf {YAHOO.widget.ScrollingDataTable} ScrollingDataTable instance.
     * @private
     */
    _onTheadKeydown : function(e, oSelf) {
        // If tabbing to next TH label link causes THEAD to scroll,
        // need to sync scrollLeft with TBODY
        if(Ev.getCharCode(e) === 9) {
            setTimeout(function() {
                if((oSelf instanceof SDT) && oSelf._sId) {
                    oSelf._elBdContainer.scrollLeft = oSelf._elHdContainer.scrollLeft;
                }
            },0);
        }

        var elTarget = Ev.getTarget(e);
        var elTag = elTarget.nodeName.toLowerCase();
        var bKeepBubbling = true;
        while(elTarget && (elTag != "table")) {
            switch(elTag) {
                case "body":
                    return;
                case "input":
                case "textarea":
                    // TODO: implement textareaKeyEvent
                    break;
                case "thead":
                    bKeepBubbling = oSelf.fireEvent("theadKeyEvent",{target:elTarget,event:e});
                    break;
                default:
                    break;
            }
            if(bKeepBubbling === false) {
                return;
            }
            else {
                elTarget = elTarget.parentNode;
                if(elTarget) {
                    elTag = elTarget.nodeName.toLowerCase();
                }
            }
        }
        oSelf.fireEvent("tableKeyEvent",{target:(elTarget || oSelf._elContainer),event:e});
    }

    /**
     * Fired when a fixed scrolling DataTable has a scroll.
     *
     * @event tableScrollEvent
     * @param oArgs.event {HTMLEvent} The event object.
     * @param oArgs.target {HTMLElement} The DataTable's CONTAINER element (in IE)
     * or the DataTable's TBODY element (everyone else).
     *
     */

    });

    })();

Copyright © 2008 Yahoo! Inc. All rights reserved.