( function(){
    NIM.ui = NIM.ui || {};
    NIM.util = NIM.util || {};
    YAHOO.util.DDM.mode = YAHOO.util.DDM.POINT;
    YAHOO.util.DDM.useCache = true;
    /**
     * ScrollingDataTable
     *
     * @class ScrollingDataTable
     * @constructor
     * @extends YAHOO.widget.DataTable
     * @private
     */
    var ScrollingDataTable = function( elContainer, columnDefs, dataSource, configs){
        ScrollingDataTable.superclass.constructor.call( this, elContainer, columnDefs, dataSource, configs );
    };
    var SDT         = ScrollingDataTable,
        Dom         = YAHOO.util.Dom,
        Event       = YAHOO.util.Event,
        Selector    = YAHOO.util.Selector,
        Lang        = YAHOO.lang,
        CustomEvent = YAHOO.util.CustomEvent,
        DT          = YAHOO.widget.DataTable,
        UA          = YAHOO.env.ua;
    
    YAHOO.extend( SDT, DT, {
        
        // The amount of space to reserve for the vertical scrollbar
        scrollOffset: 19,
        
        /**
         * Returns DOM reference to the DataTable's Heading TABLE element.
         *
         * @method getHeaderTableEl
         * @return {HTMLElement} Reference to TABLE element.
         */
        getHeaderTableEl: function(){
            return this._headerTable;
        },
        /**
         * Sets given Column to given pixel 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 ){
            var columnIndex = oColumn.getKeyIndex();
            var elColGroupHeader = this._elHeaderColgroup.childNodes[ columnIndex ];
            var elColGroupBody = this._elColgroup.childNodes[ columnIndex ];
            var borderWidth = 0;
            if ( UA.ie ){
                borderWidth = ( parseInt( YAHOO.util.Dom.getStyle( oColumn.getThEl(), "borderRightWidth" ), 10 ) | 0 ) + ( parseInt( YAHOO.util.Dom.getStyle( oColumn.getThEl(), "borderLeftWidth" ), 10 ) | 0 );
            }
            
            elColGroupHeader.prevWidth = nWidth;// - borderWidth;
            elColGroupHeader.style.width = nWidth - borderWidth + 'px';
            elColGroupBody.style.width = nWidth - borderWidth + 'px';
            
            oColumn.width = nWidth - borderWidth;
        },
        
        /**
         * Sets the Spacer Column width 
         */
        setSpacerColumnWidth: function( spacerColEl, bodySpacerColEl, spacerThEl ){
            var borderWidth = 0;
            if ( UA.ie ){
                borderWidth = ( parseInt( YAHOO.util.Dom.getStyle( spacerThEl, "borderRightWidth" ), 10 ) | 0 ) + ( parseInt( YAHOO.util.Dom.getStyle( spacerThEl, "borderLeftWidth" ), 10 ) | 0 );
            }
            
            // spacerThEl.style.width = spacerColEl.prevWidth - borderWidth + 'px';
            // spacerColEl.style.width = spacerThEl.offsetWidth + 'px';
            // bodySpacerColEl.style.width = spacerThEl.offsetWidth + 'px';
            spacerColEl.style.width = spacerColEl.prevWidth - borderWidth + 'px';
            bodySpacerColEl.style.width = spacerColEl.prevWidth - borderWidth + 'px';
        },
        
        
        /////////////////////////////////////////////////////////////////////////////
        //
        // Overriden superclass methods
        //
        /////////////////////////////////////////////////////////////////////////////
        
        /**
         * Initializes COLGROUP and COL elements for managing column width.
         *
         * ***************************************************************************
         * Overrides the original YUI implementation by adding custom
         * className attributes to the <col> tags. The className is generated
         * by using the column's key value.
         * ***************************************************************************
         *
         * @method _initColgroupEl
         * @param elTable {HTMLElement} TABLE element into which to create COLGROUP and COL.
         * @private
         */
        _initColgroupEl: function( elTable ){
            if(elTable) {
                // Destroy previous
                this._destroyColgroupEl();
                // Add COLs to DOCUMENT FRAGMENT
                var allCols = this._aColIds || [],
                    allKeys = this._oColumnSet.keys,
                    i = 0, len = allCols.length,
                    elCol, oColumn,
                    elFragment = document.createDocumentFragment(),
                    elColTemplate = document.createElement("col");
                    
                this._fixedCols = [];
                for(i=0,len=allKeys.length; i<len; i++) {
                    oColumn = allKeys[i];
                    // (start) custom code
                    elCol = elColTemplate.cloneNode(false);
                    elCol.className = "col-" + ( allKeys[i].key || allKeys[i].field );
                    elCol = elFragment.appendChild( elCol );
                    if ( ! oColumn.resizeable ){
                        this._fixedCols.push( elCol );
                    }
                }
                // Create COLGROUP
                var elColgroup = elTable.insertBefore(document.createElement("colgroup"), elTable.firstChild);
                elColgroup.appendChild(elFragment);
                this._elColgroup = elColgroup;
            }
        },
        
        /**
         * Renders the view with 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.fireEvent("beforeRenderEvent");
            
            var i, j, k, len, allRecords;
            
            allRecords = this._oRecordSet.getRecords();
            
            var elTbody        = this._elTbody,
                nRecordsLength = allRecords.length,
                strTbody       = [];
            
            // clear all selections
            this.unselectAllRows();
            
            if ( nRecordsLength > 0 ){
                var allKeys = this._oColumnSet.keys,
                    keysLen = allKeys.length,
                    oColumn,
                    oRecord,
                    rows = this.getTbodyEl().rows,
                    row,
                    clonedRow,
                    cells,
                    cell;
                
                for ( var i = 0; i < nRecordsLength; i++ ){
                    oRecord = allRecords[i];
                    row = rows[i];
                    cells = row.cells;
                    
                    row.id = oRecord.getId(); // Needed for Record association and tracking of FIRST/LAST
                    
                    // Call custom row formatter if one is specified in the configs
                    if ( this.get("formatRow") ){
                        this.get("formatRow").call( this, row, oRecord );
                    }
                    
                    for( var x = 0, y = cells.length; x < y; x++ ){
                        oColumn = allKeys[x]; 
                        cell = cells[x];
                        if(oRecord && oColumn) {
                            var sField = oColumn.field, 
                                oData  = oRecord.getData(sField);
                            var fnFormatter = typeof oColumn.formatter === 'function' ? 
                            oColumn.formatter : 
                            DT.Formatter[oColumn.formatter+''] || 
                            DT.Formatter.defaultFormatter;
                            // Apply special formatter
                            if(fnFormatter) {
                                fnFormatter.call(this, cell.firstChild, oRecord, oColumn, oData);
                            } else {
                                cell.fistChild.innerHTML = oData;
                            }
                            // this.fireEvent("cellFormatEvent", { record:oRecord, column:oColumn, key:oColumn.key } );
                        }
                        
                    }
                }
                
                // remove extra rows from the table if there aren't as many rows to render 
                // from the record set
                var extra = rows.length - nRecordsLength;
                for( var i = rows.length-1; i >= nRecordsLength; i-- ){
                    rows[i].parentNode.removeChild( rows[i] );
                }
                
            } else {
                // Show empty message
                this.showTableMessage( this.get("MSG_EMPTY"), DT.CLASS_EMPTY );
                return;
            }
            this._setSelections();
            
            // Hide loading message
            this.hideTableMessage();
        },
        
        /**
         * Hook to update oPayload before consumption.
         *  
         * @method handleDataReturnPayload
         * @param oRequest {MIXED} Original generated request.
         * @param oResponse {Object} Response object.
         * @param oPayload {MIXED} State values.
         * @return oPayload {MIXED} State values.
         */
        _handleDataReturnPayload : function (oRequest, oResponse, oPayload) {
            oPayload = this.handleDataReturnPayload(oRequest, oResponse, oPayload);
            if(oPayload) {
                // Update pagination
                var oPaginator = this.get('paginator');
                if (oPaginator) {
                    // Update totalRecords
                    if(this.get("dynamicData")) {
                        if (widget.Paginator.isNumeric(oPayload.totalRecords)) {
                            oPaginator.set('totalRecords',oPayload.totalRecords);
                        }
                    }
                    else {
                        oPaginator.set('totalRecords',this._oRecordSet.getLength());
                    }
                    // Update other paginator values
                    if (Lang.isObject(oPayload.pagination)) {
                        oPaginator.set('rowsPerPage',oPayload.pagination.rowsPerPage);
                        oPaginator.set('recordOffset',oPayload.pagination.recordOffset);
                    }
                }
                // Update sorting
                if (oPayload.sortedBy) {
                    // Set the sorting values in preparation for refresh
                    this.set('sortedBy', oPayload.sortedBy);
                }
                // Backwards compatibility for sorting
                else if (oPayload.sorting) {
                    // Set the sorting values in preparation for refresh
                    this.set('sortedBy', oPayload.sorting);
                }
            }
        },
        
        /**
         * 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);
            this.setAttributeConfig("sortedBy", {
                value: null,
                // TODO: accepted array for nested sorts
                validator: function(oNewSortedBy) {
                    if(oNewSortedBy) {
                        return (Lang.isObject(oNewSortedBy) && oNewSortedBy.key);
                    }
                    else {
                        return (oNewSortedBy === null);
                    }
                },
                method: function(oNewSortedBy) {
                    // Stash the previous value
                    var oOldSortedBy = this.get("sortedBy");
                    // Workaround for bug 1827195
                    this._configs.sortedBy.value = oNewSortedBy;
                    // Remove ASC/DESC from TH
                    var oOldColumn,
                    nOldColumnKeyIndex,
                    oNewColumn,
                    nNewColumnKeyIndex;
                    if(this._elThead) {
                        if(oOldSortedBy && oOldSortedBy.key && oOldSortedBy.dir) {
                            oOldColumn = this._oColumnSet.getColumn(oOldSortedBy.key);
                            nOldColumnKeyIndex = oOldColumn.getKeyIndex();
                            // Remove previous UI from THEAD
                            var elOldTh = oOldColumn.getThEl();
                            Dom.removeClass(elOldTh, oOldSortedBy.dir);
                            this.formatTheadCell(oOldColumn.getThLinerEl().firstChild, oOldColumn, oNewSortedBy);
                        }
                        if(oNewSortedBy) {
                            oNewColumn = (oNewSortedBy.column) ? oNewSortedBy.column : this._oColumnSet.getColumn(oNewSortedBy.key);
                            nNewColumnKeyIndex = oNewColumn.getKeyIndex();
                            // Update THEAD with new UI
                            var elNewTh = oNewColumn.getThEl();
                            // Backward compatibility
                            if(oNewSortedBy.dir && ((oNewSortedBy.dir == "asc") ||  (oNewSortedBy.dir == "desc"))) {
                                var newClass = (oNewSortedBy.dir == "desc") ?
                                DT.CLASS_DESC :
                                DT.CLASS_ASC;
                                Dom.addClass(elNewTh, newClass);
                            }
                            else {
                                var sortClass = oNewSortedBy.dir || DT.CLASS_ASC;
                                Dom.addClass(elNewTh, sortClass);
                            }
                            this.formatTheadCell(oNewColumn.getThLinerEl().firstChild, oNewColumn, oNewSortedBy);
                        }
                    }
                    // if(this._elTbody) {
                    //     // Update TBODY UI
                    //     this._elTbody.style.display = "none";
                    //     var allRows = this._elTbody.rows,
                    //     allCells;
                    //     for(var i=allRows.length-1; i>-1; i--) {
                    //         allCells = allRows[i].childNodes;
                    //         if(allCells[nOldColumnKeyIndex]) {
                    //             Dom.removeClass(allCells[nOldColumnKeyIndex], oOldSortedBy.dir);
                    //         }
                    //         if(allCells[nNewColumnKeyIndex]) {
                    //             Dom.addClass(allCells[nNewColumnKeyIndex], oNewSortedBy.dir);
                    //         }
                    //     }
                    //     this._elTbody.style.display = "";
                    // }
                    // 
                    // this._clearTrTemplateEl();
                    
                }
            });
            
        }
        
    });
    /****************************************************************************/
    /****************************************************************************/
    /****************************************************************************/
    /////////////////////////////////////////////////////////////////////////////
    //
    // Overriden methods on the DataTable's ColumnResizer class
    // TODO: Extend the ColumnResizer class and override its superclass methods
    //
    /////////////////////////////////////////////////////////////////////////////
    /**
     * Handles mousedown events on the Column resizer.
     *
     * @method onMouseDown
     * @param e {string} The mousedown event
     */
    YAHOO.util.ColumnResizer.prototype.super_onMouseDown = YAHOO.util.ColumnResizer.prototype.onMouseDown;
    YAHOO.util.ColumnResizer.prototype.onMouseDown = function(e) {
        this.useShim = true;
        this.super_onMouseDown( e );
    };    
    
    YAHOO.util.ColumnResizer.prototype.startDrag = function( x, y ){
        this.clearConstraints();
        this.resetConstraints();
        YAHOO.util.DDM.refreshCache( this.groups );
        
        var datatableContainer = YAHOO.util.Dom.getAncestorByClassName( this.datatable.getTableEl(), "infs-outer-wrap" );
        var dragEl = this.getDragEl();
        var column = this.column;
        var lastColumn = this.datatable._elHeaderColgroup.childNodes.item( this.datatable._elHeaderColgroup.childNodes.length - 1 );
        // lock the Y-axis constraints
        this.setYConstraint( 0, 0 );
        
        // the X "left" constraint is determined by subtracting the column's min-width from its width
        var xLeft = column.getThEl().offsetWidth - column.minWidth;
        if ( xLeft < 0 ){
            xLeft = 0;
        }
        
        // the X "right" constraint is determined by subtracting the last column's min-width from its width
        var xRight = lastColumn.offsetWidth - this.datatable.scrollOffset;
        xRight = lastColumn.prevWidth - this.datatable.scrollOffset;
        if ( xRight < 0 ){
            xRight = 0;
        }
        this.setXConstraint( xLeft, xRight );
        var headCellRegion = Dom.getRegion( this.headCell );
        // this.setDragElPos( headCellRegion.left + headCellRegion.width - dragEl.offsetWidth, headCellRegion.top  );
        this.setDragElPos( headCellRegion.left + headCellRegion.width - dragEl.offsetWidth, headCellRegion.top  );
        Dom.setStyle( dragEl, "opacity", 0.67 );
        Dom.setStyle( dragEl, "height", this.headCell.offsetHeight + datatableContainer.offsetHeight + 'px' );
        Dom.setStyle( dragEl, "width", '1px' );
    };
    YAHOO.util.ColumnResizer.prototype.onDrag = function(e){
        // YAHOO.util.DDM.stopEvent( e );
        /** 
         * do nothing but simply override the default implementation, as
         * it resizes columns during the drag (an undesired side-effect) 
         */
    };
    YAHOO.util.ColumnResizer.prototype.endDrag = function(e){
        YAHOO.util.Event.stopPropagation( e );
        YAHOO.util.Event.preventDefault( e );
        var newX = YAHOO.util.Event.getXY(e)[0];
        if ( newX > YAHOO.util.Dom.getX( this.headCellLiner ) ){
            // dragging left
            if ( this.startX > newX ){
                // console.log( "going left" );
                if ( newX < this.minX ){
                    // console.warn( "max'd out" );
                    newX = this.minX;
                }
            } 
            // dragging right
            else {
                // console.log( "going right" );
                if ( newX > this.maxX ){
                    // console.warn( "max'd out" );
                    newX = this.maxX;
                }
            }
            // console.log( 'startX: ', this.startX );
            // console.log( "newX: ", newX );
            var offsetX = newX - this.startX;
            var newWidth = this.startWidth + offsetX - this.nLinerPadding;
            newWidth = this.startWidth + offsetX;
            
            // console.log( "new width: ", newWidth );
            // console.log( "difference in width: ", newWidth - this.startWidth );
            
            var lastColHeader = this.datatable._elHeaderColgroup.childNodes.item( this.datatable._elHeaderColgroup.childNodes.length - 1 );
            var lastColBody = this.datatable._elColgroup.childNodes.item( this.datatable._elColgroup.childNodes.length - 1 );
            var lastColWidth = lastColHeader.prevWidth; //lastColHeader.offsetWidth;
            this.datatable.setColumnWidth( this.column, newWidth );
            
            // set width of last column
            
            var borderWidth = 0;
            // To deal with the IE box model (we need to account for the border width)
            if ( UA.ie ){
                borderWidth = ( parseInt( YAHOO.util.Dom.getStyle( this.column.getThEl(), "borderRightWidth" ), 10 ) | 0 ) + ( parseInt( YAHOO.util.Dom.getStyle( this.column.getThEl(), "borderLeftWidth" ), 10 ) | 0 );
            }
            
            if ( lastColWidth - offsetX < this.scrollOffset ){
                lastColHeader.style.width = this.lastScrollOffset + 'px';
                lastColBody.style.width = this.lastScrollOffset + 'px';
                lastColHeader.prevWidth = this.lastScrollOffset;
            } else {
                lastColBody.style.width = lastColWidth - offsetX - borderWidth + 'px';
                lastColHeader.style.width = lastColWidth - offsetX - borderWidth + 'px';
                lastColHeader.prevWidth = lastColWidth - offsetX;// - borderWidth;
            }
        }
        this.clearConstraints();
        this.resetConstraints();
    };
        
    /****************************************************************************/
    /****************************************************************************/
    /****************************************************************************/
    /**
     * BaseDDProxy subclasses DDProxy to support draggable rows.
     *
     * @namespace NIM.util
     * @class BaseDDProxy
     * @extends YAHOO.util.DDProxy
     * @constructor
     * @param {String} id the id of the linked html element
     * @param {String} sGroup the group of related DragDrop objects
     * @param {object} config an object containing configurable attributes
     * Valid properties for DDProxy in addition to those in DragDrop: 
     * resizeFrame, centerFrame, dragElId */
    NIM.util.BaseDDProxy = function( id, sGroup, config ){
        NIM.util.BaseDDProxy.superclass.constructor.apply( this, arguments );
        YAHOO.util.Dom.addClass( this.getDragEl(), "infs-dt-dragproxy" );
    };
    
    YAHOO.extend( NIM.util.BaseDDProxy, YAHOO.util.DDProxy, {
        /**
         * @method createFrame
         * @private
         */
        createFrame: function() {
            var self=this, body=document.body;
            if (!body || !body.firstChild) {
                setTimeout( function() { self.createFrame(); }, 50 );
                return;
            }
            var div=this.getDragEl(), Dom=YAHOO.util.Dom;
            if (!div) {
                div    = document.createElement("div");
                div.id = this.dragElId;
                var s  = div.style;
                s.position   = "absolute";
                s.visibility = "hidden";
                s.cursor     = "move";
                s.zIndex     = 999;
                var _data = document.createElement('div');
                Dom.setStyle(_data, 'height', '100%');
                Dom.setStyle(_data, 'width', '100%');
                /**
                * If the proxy element has no background-color, then it is considered to the "transparent" by Internet Explorer.
                * Since it is "transparent" then the events pass through it to the iframe below.
                * So creating a "fake" div inside the proxy element and giving it a background-color, then setting it to an
                * opacity of 0, it appears to not be there, however IE still thinks that it is so the events never pass through.
                */
                Dom.setStyle(_data, 'background-color', '#ccc');
                Dom.setStyle(_data, 'opacity', '0');
                div.appendChild(_data);
                // appendChild can blow up IE if invoked prior to the window load event
                // while rendering a table.  It is possible there are other scenarios 
                // that would cause this to happen as well.
                body.insertBefore(div, body.firstChild);
            }
        },
        
        /**
         * @method setDragElPos
         * @private
         */
        setDragElPos: function( x, y ){
            if ( ! this.dragEl ) { this.dragEl = this.getDragEl(); };
            this.dragEl.style.top =  y + ( this.dragEl.offsetHeight / 4 ) + "px";
            this.dragEl.style.left = x + 5 + "px";
        },
        
        /**
         * @method applyConfig
         * @private
         */
        applyConfig: function(){
            NIM.util.BaseDDProxy.superclass.applyConfig.call(this);
            this.grid = this.config.grid || false;
        },
        
        /**
         * Abstract method called after a drag/drop object is clicked
         * and the drag or mousedown time thresholds have beeen met.
         * @method startDrag
         * @param {int} X click location
         * @param {int} Y click location
         */
        startDrag: function( x, y ){
            /* override this */
        },
        
        /**
         * Fired when we are done dragging the object
         * @method endDrag
         * @param {Event} e the mouseup event
         */
        endDrag: function( e ){
            /* override this */
        },
        
        /**
         * Abstract method called when this element is hovering over another 
         * DragDrop obj
         * @method onDragOver
         * @param {Event} e the mousemove event
         * @param {String|DragDrop[]} id In POINT mode, the element
         * id this is hovering over.
         */
        onDragOver: function( e, id ){
            /* override this */
        },
        /**
         * Abstract method called when we are no longer hovering over an element
         * @method onDragOut
         * @param {Event} e the mousemove event
         * @param {String|DragDrop[]} id In POINT mode, the element
         * id this was hovering over.
         */
        onDragOut: function( e, id ){
            /* override this */
        },
        
        /**
         * Abstract method called when this item is dropped on another DragDrop 
         * obj
         * @method onDragDrop
         * @param {Event} e the mouseup event
         * @param {String|DragDrop[]} id In POINT mode, the element
         * id this was dropped on.
         */
        onDragDrop: function( e, id ){
            /* override this */
        },
        /**
         * Abstract method called when this item is dropped on an area with no
         * drop target
         * @method onInvalidDrop
         * @param {Event} e the mouseup event
         */
        onInvalidDrop: function( e ){
            /* override this */
        }
    } );
    /****************************************************************************/
    /****************************************************************************/
    /****************************************************************************/
    /**
     * The InfiniteScroller control provides a fluid, multi-page control for displaying tabular 
     * data across A-grade browsers.  The Infinite Scroller widget handles datasets of any
     * size, and provides a desktop-like experience by allowing the user to scroll to view all
     * the data available in the data set.
     *
     * @module infinitescroller
     * @requires yahoo, dom, event, element, datasource, cachedmailcontroller
     * @optional dragdrop
     * @title InfiniteScroller Control
     */
    /**
     * InfiniteScroller
     *
     * @namespace NIM.ui
     * @class InfiniteScroller
     * @constructor
     * @param configs {Object} Object literal of configuration values.
     */
    function InfiniteScroller( configs ){
        this.initialLoad = true;        // set when first initialized
        
        this.configs = configs || {};
        this.id = InfiniteScroller.ID_PREFIX + Dom.generateId( null, "dt-" );
        this.metadata = new InfiniteScroller.MetaData( this.configs.totalRows || 0 );
        this.parentNode = Dom.get( this.configs.container );
        this.columnDefs = this.configs.columns;   // DataTable column definitions
        
        this.dataSource = new YAHOO.util.DataSource( this._getRowData, { scope: this } );
        this.dataSource.responseType = this.configs.dataSource.responseType;
        this.dataSource.responseSchema = this.configs.dataSource.responseSchema;
        this.dataSourcePK = this.configs.dataSource.responseSchema.pk;
        // Timeout (in milliseconds) before showing the "loading" mask
        // when firing the dataChange event (for loading new records in the grid)        
        this.loadMaskTimeout = this.configs.loadMaskTimeout || 250;
                                        
        this.loadMask_tId = null;       // timeout id for load mask
        this.rowHeight = null;          // The height (in px) of a single row in the table.
        this.lastPixelOffset = 0;       // The position of scroller in relation to its content
        this.lastStartOffset = 0;       // Index of the first row in the grid
        this.lastEndOffset = 0;         // Index of the last row in the grid
        
        this._checkedRows = [];         // List of checked rows (element value is the row's index)
        this._exceptionRows = [];       // List of unchecked rows (used for rows that are )
        this.wrapperWidth = 0;
        this.fixedColWidth = 0;
        this._createInfrastructure();   // Creates the table and all its associated elements
        this._initEvents();
        if ( this.configs.prefetch ){
            this.lastStartOffset = 0;
            this.lastEndOffset = parseInt( this.wrapper.offsetHeight / this.rowHeight );
            this.configs.prefetch.handler.call( this.configs.prefetch.scope, { start: 0, end: parseInt( this.wrapper.offsetHeight / this.rowHeight ) } );
        }
        
        this.ddproxy = this.configs.dragdrop || null;   // Drag and drop proxy (visitor)
    }
    
    /**
    * @attribute container
    * @description The parent DOM container in which to render the InfiniteScroller
    * @type String | HTMLElement
    */
    /**
    * @attribute columns
    * @description A list of column objects to be rendered in the InfiniteScroller (see Column Definitions in the 
    * <a href="http://docs.nitido.com/infinitescroller/InfiniteScroller-HowTo.pdf">InfiniteScroller How To</a> document)
    * @type Object
    */
    /**
    * @attribute dataSource
    * @description A NIM.util.DataSource instance (see Data Source Definition in the 
    * <a href="http://docs.nitido.com/infinitescroller/InfiniteScroller-HowTo.pdf">InfiniteScroller How To</a> document)
    * @type NIM.util.DataSource
    */
    /**
    * @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 NIM.ui.InfiniteScroller.SORT_ASC or NIM.ui.InfiniteScroller.SORT_DESC</dd>
    * </dl>
    * @type Object | null
    */
    /**
    * @attribute MSG_LOADING
    * @description The string to display when messages are loading in the InfiniteScroller
    * @type String | null
    * @default "Loading..."
    */
    /**
    * @attribute MSG_EMPTY
    * @description The string to display when there are no records in the InfiniteScroller
    * @type String | null
    * @default "No records found."
    */
    /**
    * @attribute prefetch
    * @description An object to indicate the table should be preloaded with data.  The object contains the 
    * following properties:
    * <dl>
    *     <dt>prefetch.handler</dt>
    *     <dd>{HTMLFunction} The callback function to call in order to indicate which records should be loaded. The function should accept the following
              parameters:
              <dl>
                <dt>handler</dt>
                <dd>The callback function to call in order to indicate which records should be loaded.</dd>
                <dt>scope</dt>
                <dd>The object to set as the execution scope of the handler function</dd>
             </dl>
          </dd>
    * </dl>
    * @type Object | null
    */
    
    /**
    * @attribute formatRow
    * @description A function to call to perform any custom formatting for the table row.
    * @type HTMLFunction | null
    */
    /**
    * @attribute dragdrop
    * @description  A drag and drop proxy object to support drag and drop in the InfiniteScroller
    * @type NIM.util.BaseDDProxy | null
    */
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Static properties
    //
    /////////////////////////////////////////////////////////////////////////////
    /**
     * ID prefix for InfiniteScroller widgets
     *
     * @property InfiniteScroller.ID_PREFIX
     * @type String
     * @static
     * @final
     * @default "nv-infs-"
     */
    InfiniteScroller.ID_PREFIX = "nv-infs-";
    /**
     * Default formatter for Columns containing checkbox elements
     *
     * @property InfiniteScroller.COLUMN_CHECKBOX
     * @type String
     * @static
     * @final
     * @default "checkbox"
     */
    InfiniteScroller.COLUMN_CHECKBOX = "checkbox";
    /**
     * ID assigned to the "Select All" checkbox for a Column defined as "selectAll"
     *
     * @property InfiniteScroller.COLUMN_CHECKBOX_ID
     * @type String
     * @static
     * @final
     * @default "nv-dt-cb"
     */
    InfiniteScroller.COLUMN_CHECKBOX_ID = "nv-dt-cb";
    /**
     * Ascending sort order
     *
     * @property nfiniteScroller.SORT_ASC
     * @type String
     * @static
     * @final
     * @default "ASC"
     */
    InfiniteScroller.SORT_ASC = "ASC";
    /**
     * Descending sort order.
     *
     * @property InfiniteScroller.SORT_DESC
     * @type String
     * @static
     * @final
     * @default "DESC"
     */
    InfiniteScroller.SORT_DESC = "DESC";
    
    InfiniteScroller.prototype = {
        
        
        /////////////////////////////////////////////////////////////////////////////
        //
        // Custom Events
        //
        /////////////////////////////////////////////////////////////////////////////
        /**
         * Fired when there has been a change to the data being displayed in the control 
         * (ie: user has scrolled, and new records need to be loaded)
         *
         * @event dataChangeEvent
         * @param oArgs.start {Number} The start offset/index of the viewable segment of rows to load
         * @param oArgs.end {Number} The end offset/index of the viewable segment of rows to load
         */
        /**
         * Fired when a column is sorted
         *
         * @event columnSortEvent
         * @param oArgs.column {String} The Column's key
         * @param oArgs.order {String} The sort order (NIM.ui.InfiniteScroller.SORT_ASC or NIM.ui.InfiniteScroller.SORT_DESC)
         * @param oArgs.start {Number} The start offset/index of the viewable segment of rows to load
         * @param oArgs.end {Number} The end offset/index of the viewable segment of rows to load
         */
        /**
         * Fired when a column defined as a NIM.ui.InfiniteScroller.COLUMN_CHECKBOX with selectAll behaviour is clicked
         *
         * @event selectAllClickEvent
         * @param oArgs.checked {Boolean} True if select all is checked
         */
        /**
         * Fired when a CHECKBOX element is clicked.
         *
         * @event checkboxClickEvent
         * @param oArgs.checked {Boolean} True if the item was checked
         * @param oArgs.element {HTMLElement} The DOM element reference of the checkbox element
         * @param oArgs.recordId {Record}: The record ID corresponding to the checked row
         */
        /**
         * Fired when a row has a click.
         *
         * @event rowClickEvent
         * @param oArgs.recordId {Number} The record ID corresponding to the clicked row
         */
         
        /**
         * Fired when a row has a mouseover
         *
         * @event rowMouseoverEvent
         * @param oArgs.recordId {Number} The record ID corresponding to the clicked row
         */
         
        /**
         * Fired when a row has a mouseout
         *
         * @event rowMouseoutEvent
         * @param oArgs.recordId {Number} The record ID corresponding to the clicked row
         */
        /**
         *  Fired when a row drag has started.
         *
         * @event rowDragStartEvent
         * @param oArgs.recordId {Number} The record ID corresponding to the clicked row
         */
         
        /**
         *  Fired when a row drag has ended.
         *
         * @event rowDragEndEvent
         * @param oArgs.recordId {Number} The record ID corresponding to the clicked row
         */
        /////////////////////////////////////////////////////////////////////////////
        //
        // Private methods
        //
        /////////////////////////////////////////////////////////////////////////////
        /**
         * Initialize Custom Events.
         *
         * @method _initEvents
         * @private   
         */
        _initEvents : function() {
            this.createEvent( "dataChangeEvent" );
            this.createEvent( "columnSortEvent", this );
            this.createEvent( "selectAllClickEvent", this );
            this.createEvent( "checkboxClickEvent", this );
            this.createEvent( "rowClickEvent", this );
            this.createEvent( "rowMouseoverEvent", this );
            this.createEvent( "rowMouseoutEvent", this );
            this.createEvent( "rowDragStartEvent", this );
            this.createEvent( "rowDragEndEvent", this );
        },
        /**
         * Returns the raw data represented by the rows in tha table
         *
         * @method _getRowData
         * @private
         * @return {Object[]} Array of raw row data
         */
        _getRowData: function(){
            return this._rowData;
        },
        
        /**
         * Returns the raw data represented by the rows in tha table
         *
         * @method _getRecordIdByPrimaryKey
         * @private
         * @return {String} Raw list of row data
         */
        _getRecordIdByPrimaryKey: function( record ){
            return record.getData( this.dataSourcePK );
        },
        
        /**
         * Augments a sortable Column's sortOptions by adding a sort delegate, as well
         * as adding a checkbox to the Column if its formatter has been specified as type
         * InfiniteScoller.COLUMN_CHECKBOX and has its selectAll option set to true
         *
         * @method _augmentColumnDefinitions
         * @private
         */
        _augmentColumnDefinitions: function(){
            var column;
            for( var i = 0, j = this.columnDefs.length; i < j; i++ ){
                column = this.columnDefs[i];
                if ( column.sortable === true ){
                    column.sortOptions = {
                        sortFunction: this._sortDelegate
                    };
                }
                
                if ( column.formatter && column.formatter == InfiniteScroller.COLUMN_CHECKBOX && column.selectAll === true ){
                    column.label = '<input id="' + InfiniteScroller.COLUMN_CHECKBOX_ID + this.id + '" class="yui-dt-checkbox" type="checkbox">';
                    this._hasCheckboxSelectAll = true;
                }
            }
        },
        
        /**
         * Delegate method for handling Column sorting. Fires a columnSortEvent to notify
         * of a sort action
         *
         * @method _sortDelegate
         * @private
         */
        _sortDelegate: function(){
            var data = arguments[0],
                sortOrder,
                sortColumn;
            
            sortOrder = ( data.dir == "yui-dt-asc" ) ? InfiniteScroller.SORT_ASC : InfiniteScroller.SORT_DESC;
            sortColumn = data.column.key;
            
            this._prevState = this.ytable.getState();
            var _that = this;
            this.loadMask_tId = window.setTimeout( function(){
                _that.showLoadMask( true );
            }, this.loadMaskTimeout );
            
            this.fireEvent( "columnSortEvent", { order: sortOrder, column: sortColumn, start: this.lastStartOffset, end: this.lastEndOffset } );
        },
        
        /**
         * 
         *
         * @method _handleRowMouseoverEvent
         * @private
         */
        _handleRowMouseoverEvent: function(){
            var record = this.ytable.getRecord( arguments[0].target );
            if ( record ){
                this.fireEvent( "rowMouseoverEvent" , { recordId: this._getRecordIdByPrimaryKey( record ) } );
            }
        },
        /**
         * 
         *
         * @method _handleRowMouseoutEvent
         * @private
         */
        _handleRowMouseoutEvent: function(){
            var record = this.ytable.getRecord( arguments[0].target );
            if ( record ){
                this.fireEvent( "rowMouseoutEvent" , { recordId: this._getRecordIdByPrimaryKey( record ) } );
            }
        },
        
        /**
         * 
         *
         * @method _handleRowMousedownEvent
         * @private
         */
        _handleRowMousedownEvent: function(){
            if ( this.ddproxy && typeof this.ddproxy == 'function' ){
                var dragProxy = new this.ddproxy( arguments[0].target, "dd-dragdrop", { grid: this, resizeFrame: false, maintainOffset: false } );
                dragProxy.handleMouseDown.call( dragProxy, arguments[0].event );
                var record = this.ytable.getRecord( arguments[0].target );
                if ( record ){
                    this.fireEvent( "rowDragStartEvent" , { recordId: this._getRecordIdByPrimaryKey( record ) } );
                }
                this._dragProxy = dragProxy;
            }
            YAHOO.util.Event.preventDefault( arguments[0].event );
        },
        
        /**
         * 
         *
         * @method _handleRowMouseupEvent
         * @private
         */
        _handleRowMouseupEvent: function(){
            if ( this._dragProxy ){
                this._dragProxy.onMouseUp.call( this._dragProxy, arguments[0].event );
                var record = this.ytable.getRecord( arguments[0].target );
                if ( record ){
                    this.fireEvent( "rowDragEndEvent" , { recordId: this._getRecordIdByPrimaryKey( record ) } );
                }
            }
            
            YAHOO.util.Event.preventDefault( arguments[0].event );
        },
        
        /**
         * 
         *
         * @method _handleTbodyKeyEvent
         * @private
         */
        _handleTbodyKeyEvent: function(){
            console.log( "Key Event: ", arguments );
        },
        
        /**
         * 
         *
         * @method _handleSelectAllClick
         * @private
         */
        _handleSelectAllClick: function(e){
            var checkbox = Event.getTarget( e ), 
                column   = this.ytable.getColumn( checkbox ),
                recordSet = null,
                record    = null,
                rData     = null,
                i, j;
            if ( column.formatter && column.formatter == InfiniteScroller.COLUMN_CHECKBOX && column.selectAll === true ){
                if ( this._hasCheckboxSelectAll === true ){
                    if ( checkbox.checked ){
                        this.checkboxSelectAll();
                    } else {
                        this.checkboxUnselectAll();
                    }
                }
            }
            
            this.fireEvent( "selectAllClickEvent", { checked: checkbox.checked } );
        },
        
        /**
         * Marks all rows in the table as selected.
         *
         * @method checkboxSelectAll
         */
        checkboxSelectAll: function(){
            this.isSelectAllChecked = true;
            recordSet = this.ytable.getRecordSet();
            for ( i = 0, j = recordSet.getLength(); i < j; i++ ){
                record = recordSet.getRecord( i );
                if ( this._exceptionRows.indexOf( this._getRecordIdByPrimaryKey( record ) ) == -1 ){
                    this.checkRow( record );
                }
            }
            // clear list row selection state
            this._checkedRows = [];
            this._exceptionRows = [];
        },
        
        /**
         * Marks all rows in the table as unselected.
         *
         * @method checkboxUnselectAll
         */
        checkboxUnselectAll: function(){
            this.isSelectAllChecked = false;
            recordSet = this.ytable.getRecordSet();
            for ( i = 0, j = recordSet.getLength(); i < j; i++ ){
                record = recordSet.getRecord( i );
                this.uncheckRow( record );
            }
            // clear list row selection state
            this._checkedRows = [];
            this._exceptionRows = [];
        },
        
        /**
         *
         *
         * @method _handleRowCheckboxClick
         * @private
         */
        _handleRowCheckboxClick: function(e){
            var checkbox      = Event.getTarget( e ),
                selectedIndex = null,
                record        = null,
                recordId      = null,
                index         = -1;
            // We add the lastStartOffset since the DataTable's record index is only for the
            // currently rendered items (not the entire set)
            selectedIndex = this.ytable.getRecordIndex( checkbox ) + this.lastStartOffset;
            record = this.ytable.getRecord( checkbox );
            recordId = this._getRecordIdByPrimaryKey( record );
            
            // If "select all" is enabled, we need to manage an "exception" list of
            // rows that are to be excluded from the "select all" state
            if ( this.isSelectAllChecked ){
                // checking a row when in "select all" removes it from the exception
                // list
                if ( checkbox.checked ){
                    index = this._exceptionRows.indexOf( recordId );
                    if ( index > -1 ){
                        var tmp;
                        tmp = this._exceptionRows.splice( index, 1 );
                        this.ytable.selectRow( this.ytable.getRecordIndex( checkbox ) );
                    }
                // We can add the row to the exception list
                } else {
                    this.ytable.unselectRow( this.ytable.getRecordIndex( checkbox ) );
                    this._exceptionRows.push( recordId );
                }
            // We're not in a "select all" state
            } else {
                if ( checkbox.checked ){
                    this.checkRow( record );
                } else {
                    this.uncheckRow( record );
                }
            }
            
            this.fireEvent( "checkboxClickEvent", { recordId: this._getRecordIdByPrimaryKey( record ), element: checkbox, checked: checkbox.checked } );
            
            return false;   // prevent the event from bubbling
        },
        
        /**
         * Sets given row to the selected state
         *
         * @method checkRow
         * @param record {HTMLElement | String | YAHOO.widget.Record | Number} HTML element
         * reference or ID string, Record instance, or RecordSet position index.
         */
        checkRow: function( record ){
            // debugger;
            var recordId,
                checkbox;
                
            // If rows have been selected (highlighted) and this is the first row to 
            // be checked, we clear all the selected rows before checking the specified
            // row
            if ( this._checkedRows.length == 0 && this.ytable.getSelectedRows().length > 0 ){
                this.ytable.unselectAllRows();
            }
            
            recordId = this._getRecordIdByPrimaryKey( record );
            this._checkedRows.push( recordId );
            this.ytable.selectRow( record );
            // recordSet = this.ytable.getRecordSet();
            // rData = record.getData();
            // rData.checked = true;
            // recordSet.updateRecord( record, rData );
            // this.ytable.updateRow( record, rData );
            checkbox = Selector.query( "input[type=checkbox]", record.getId(), true );
            checkbox.checked = true;
        },
        
        /**
         * Sets given row to the unselected state
         *
         * @method uncheckRow
         * @param record {HTMLElement | String | YAHOO.widget.Record | Number} HTML element
         * reference or ID string, Record instance, or RecordSet position index.
         */
        uncheckRow: function( record ){
            var recordId,
                recordIndexPosition,
                checkbox;
            
            recordId = this._getRecordIdByPrimaryKey( record );
            recordIndexPosition = this._checkedRows.indexOf( recordId );
            if ( recordIndexPosition > -1 ){
                var tmp;
                tmp = this._checkedRows.splice( recordIndexPosition, 1 );
            }
            checkbox = Selector.query( "input[type=checkbox]", record.getId(), true );
            checkbox.checked = false;
            this.ytable.unselectRow( record );
        },
        
        /**
         * Sets all rows to the unselected state
         *
         * @method uncheckAllRows
         */
        uncheckAllRows: function(){
            var recordSet,
                record;
            
            recordSet = this.ytable.getRecordSet();
            for ( var i = 0, j = recordSet.getLength(); i < j; i++ ){
                record = recordSet.getRecord( i );
                this.uncheckRow( record );
            }
        },
        
        /**
         * 
         *
         * @method _handleRowSelectEvent
         * @private
         */
        _handleRowSelectEvent: function(args){
            var selectedRows      = null,
                targetRecord      = null,
                targetRecordId    = null,
                targetRecordIndex = null,
                i, j;
            if ( this.isSelectAllChecked ){
                this.uncheckAllRows();
                Dom.get( InfiniteScroller.COLUMN_CHECKBOX_ID + this.id ).checked = false;
                this.isSelectAllChecked = false;
            }
            this.ytable.onEventSelectRow( arguments[0], arguments[1] );
            
            // Check to see if a single row has already been selected.  If a row
            // has been selected, and the newly selected row is a different row
            // from the currently selected row, we can safely add both rows to 
            // the _checkedRows list and activate their checkboxes.
            selectedRows = this.ytable.getSelectedRows();
            if ( selectedRows.length > 1 ){
                this.uncheckAllRows();
                for ( i = 0, j = selectedRows.length; i < j; i++ ){
                    targetRecord = this.ytable.getRecord( selectedRows[i] );
                    this.checkRow( targetRecord );
                }
            } else if ( selectedRows.length == 1 ){
                // uncheck all other rows before checking this one
                targetRecordIndex = this.ytable.getRecordIndex( selectedRows[0] );
                this.uncheckAllRows();
                targetRecord = this.ytable.getRecord( targetRecordIndex );
                this.ytable.selectRow( targetRecord );
            }
            
            this.fireEvent( "rowClickEvent", { recordId: this._getRecordIdByPrimaryKey( targetRecord ) } );
        },
        
         /**
          * After redrawing the grid with it's new list of rows, we scan through
          * to re-select any items that may have been previously selected before
          * scrolling to a new window
          *
          * @method _updateCheckedRows
          * @private
          */
        _updateCheckedRows: function(){
            var recordSet = this.ytable.getRecordSet(),
                record,
                i, j;
                
            if ( this.isSelectAllChecked === true ){
                for ( i = 0, j = recordSet.getLength(); i < j; i++ ){
                    record = recordSet.getRecord( i );
                    if ( this._exceptionRows.indexOf( this._getRecordIdByPrimaryKey( record ) ) == -1 ){
                        this.checkRow( record );
                    }
                }
                this._checkedRows = [];
                
            } else {
                for ( i = 0, j = recordSet.getLength(); i < j; i++ ){
                    record = recordSet.getRecord( i );
                    if ( this._checkedRows.indexOf( this._getRecordIdByPrimaryKey( record ) ) > -1 ){
                        this.checkRow( record );
                    }
                }
            }
        },
        
        /**
         * @method _createInfrastructure
         * @private
         */
        _createInfrastructure: function(){
            this.outerWrap = document.createElement( 'div' );
            this.outerWrap.className = "infs-outer-wrap";
            this.wrapper = document.createElement( 'div' );
            this.wrapper.className = "infs-wrap";
            this.outerWrap.appendChild( this.wrapper );
            this.loadMask = document.createElement( 'div' );
            this.loadMask.className = "infs-load-mask";
            this.loadMaskMsg = document.createElement( 'div' );
            this.loadMaskMsg.className = "infs-load-mask-msg";
            this.loadMaskMsg.defaultMsg = this.configs.MSG_LOADING || "Loading...";
            this.loadMaskMsg.innerHTML = this.loadMaskMsg.defaultMsg;
            this.container = document.createElement( 'div' );
            this.container.className = "infs-container";
            this.container.id = this.id;
            this.container.appendChild( this.loadMask );
            this.container.appendChild( this.loadMaskMsg );
            this.container.appendChild( this.outerWrap );
            this.parentNode.appendChild( this.container );
            this._augmentColumnDefinitions();
            this._createContainer();
            this._createTable();
            this._setDimensions();
            this._createScrollbar();
            Event.on( window, 'resize', function(e){
                this._setDimensions(e);
                this._handleScroll(e);
                this.reset = true;
            }, this, true );
        },
        
        /**
         * @method _createTable
         * @private
         */
        _createTable: function(){
            var configs = this.configs;
            configs.dynamicData = true; // set to true to prevent the YUI DataTable from sorting on the client side
            
            // alter sort keys to comply with YUI.widget.DataTable's constants
            if ( configs.sortedBy && configs.sortedBy.dir ){
                switch( configs.sortedBy.dir ){
                    case InfiniteScroller.SORT_ASC:
                        configs.sortedBy.dir = YAHOO.widget.DataTable.CLASS_ASC;
                        break;
                        
                    case InfiniteScroller.SORT_DESC:
                        configs.sortedBy.dir = YAHOO.widget.DataTable.CLASS_DESC;
                }
            }
            
            this.ytable = new ScrollingDataTable( this.gridWrap, this.columnDefs, this.dataSource, configs );
            
            this.myCallback = {
                success: function( oRequest, oResponse, oPayload ){
                    if ( this._prevState ){
                        oPayload.sortedBy = this._prevState.sortedBy || oPayload.sortedBy;
                        oPayload.selectedCells = this._prevState.selectedCells || oPayload.selectedCells;
                        oPayload.selectedRows = this._prevState.selectedRows || oPayload.selectedRows;
                    }
                    var elTbody = this.ytable.getTbodyEl();
                    
                    if ( this._rowData.length > elTbody.rows.length ){
                        var delta = Math.abs( this._rowData.length - elTbody.rows.length );
                        for ( var i = 0; i < delta; i++ ){
                            this._addRowPlaceholder();
                        }
                    }
                    
                    this.ytable.onDataReturnSetRows.call( this.ytable,oRequest, oResponse, oPayload );
                    this._updateCheckedRows();
                    
                    if ( this.initialLoad ){
                        this.gridWrap.style.visibility = "visible";
                        this.initialLoad = false;
                    }
                    
                    // if ( elTbody.scrollHeight <= this.outerWrap.scrollHeight && (  ) ){
                    //     this.scroller.style.display = "none";
                    // } else {
                    //     this.scroller.style.display = "";
                    // }
                },
                failure: function(){
                    // console.log( "FAIL" );
                },
                scope: this,
                argument: this.ytable.getState()
            };
            
            this.ytable.handleDataReturnPayload = function( oRequest, oResponse, oPayload ){
                return oPayload;
            };
            
            // subscribe to datatable custom events
            this.ytable.subscribe( "columnSortEvent", this._sortDelegate, this, true );
            this.ytable.subscribe( "checkboxClickEvent", this._handleRowCheckboxClick, this, true );
            this.ytable.subscribe( "rowClickEvent", this._handleRowSelectEvent, this, true );
            this.ytable.subscribe( "rowMousedownEvent", this._handleRowMousedownEvent, this, true );
            this.ytable.subscribe( "rowMouseupEvent", this._handleRowMouseupEvent, this, true );
            this.ytable.subscribe( "rowMouseoverEvent", this._handleRowMouseoverEvent, this, true );
            this.ytable.subscribe( "rowMouseoutEvent", this._handleRowMouseoutEvent, this, true );
            this.ytable.subscribe( "columnResizeEvent", function(){ this.spacerCol.prevWidth = this.spacerTh.offsetWidth; }, this, true );
            this.ytable.subscribe( "tbodyKeyEvent", this._handleTbodyKeyEvent, this, true );
            
            // this.ytable.subscribe( "theadCellClickEvent", this._headerClickDelegate, this, true );
            if ( this._hasCheckboxSelectAll ){
                Event.onAvailable( InfiniteScroller.COLUMN_CHECKBOX_ID + this.id, function(){
                    Event.on( InfiniteScroller.COLUMN_CHECKBOX_ID + this.id, 'click', this._handleSelectAllClick, this, true );
                }, this, true );
            }
            // move header outside the scroll area
            this.thead = this.ytable.getTheadEl();
            this.headerWrap = this.ytable.getContainerEl().cloneNode( false );
            Dom.removeClass( this.headerWrap, "infs-grid-wrap" );
            Dom.addClass( this.headerWrap, "infs-grid-hd" );
            // get colgroup
            var container = this.ytable.getContainerEl();
            var colGroup = Selector.query( 'colgroup', container, true );
            // Add a spacer column (the last column in the table)
            var spacerCol = document.createElement( "col" );
            spacerCol.className = "col-spacer";
            colGroup.appendChild( spacerCol );
            this.bodySpacerCol = spacerCol;
            
            var headerColGroup = colGroup.cloneNode( true );
            this.headerTable = this.ytable.getTableEl().cloneNode( false );
            this.headerTable.appendChild( headerColGroup );
            this.headerTable.appendChild( this.thead );
            this.headerWrap.appendChild( this.headerTable );
            this.outerWrap.parentNode.insertBefore( this.headerWrap, this.outerWrap );
            this.ytableEl = this.ytable.getTableEl();
            
            // append spacer <th> to header
            var headerTr = Selector.query( "tr", this.thead, true );
            var thEl = headerTr.appendChild( document.createElement( "th" ) );
            thEl.className = "col-spacer";
            this.spacerTh = thEl;
            
            this.spacerCol = Selector.query( 'col.col-spacer', headerColGroup, true );
            this.spacerCol.prevWidth = thEl.offsetWidth;
            
            this.ytable._elHeaderColgroup = headerColGroup;
            this.ytable._headerTable = this.headerTable;
            
            // set starting widths
            var columns = this.ytable._elHeaderColgroup.childNodes;
            var columnObj;
            var columnEl;
            var thEl;
            for ( var i = 0, j = columns.length; i < j; i++ ){
                columnEl = columns.item( i );
                columnObj = this.ytable.getColumn( columnEl.className.substr( 4 ) );
                if ( columnObj ){
                    thEl = columnObj.getThEl();
                    columnEl.prevWidth = thEl.offsetWidth;
                }
            }
            // create a row template and set its height as the rowHeight value
            if ( ! this.rowTemplate ){
                var tBodyEl = this.ytable.getTbodyEl();
                this.rowTemplate = tBodyEl.insertRow( 0 );//document.createElement( "tr" );
                Dom.addClass( this.rowTemplate, DT.CLASS_ODD + " " + DT.CLASS_FIRST );
                var allKeys = this.ytable._oColumnSet.keys;
                var keysLen = allKeys.length;
                var cell;
                var oColumn;
                var oColumnSet = this._oColumnSet;
                var aAddClasses = [];
                var divEl;
                for( var i = 0; i < keysLen; i++ ){
                    aAddClasses = [];
                    oColumn = allKeys[i]; 
                    cell = this.rowTemplate.insertCell( i );
                    if( i === 0) {
                        aAddClasses[aAddClasses.length] = DT.CLASS_FIRST;
                    }
                    if( i == keysLen - 1 ) {
                        aAddClasses[aAddClasses.length] = DT.CLASS_LAST;
                    }
                    
                    cell.className = this.ytable._getColumnClassNames( oColumn, aAddClasses );
                    
                    divEl = [];
                    divEl.push( '<div class="' + DT.CLASS_LINER );
                    if ( oColumn.width && DT._bDynStylesFallback ) {
                        // Validate minWidth
                        // alert("minWidth");
                        var nWidth = ( oColumn.minWidth && ( oColumn.width < oColumn.minWidth ) ) ? oColumn.minWidth : oColumn.width;
                        divEl.push( '" style="');
                        divEl.push( 'overflow: hidden; width:' + nWidth );
                    }
                    divEl.push( '"> ' );
                    divEl.push( '</div>' );
                    cell.innerHTML = divEl.join( '' );
                    
                }
                
                var spacerCell = this.rowTemplate.insertCell( i );
                spacerCell.className = "dt-col-spacer";
            }
            this.ytableEl.style.borderCollapse = "separate";
            this.rowHeight = parseInt( this.ytable.getMsgTdEl().offsetHeight, 10 );
            this.ytableEl.style.borderCollapse = "collapse";
            this.ytable.hideTableMessage();
        },
        /**
         * @method _createContainer
         * @private
         */
        _createContainer: function(){
            this.gridWrap = document.createElement( 'div' );
            this.gridWrap.className = "infs-grid-wrap";
            this.wrapper.appendChild( this.gridWrap );
            this.outerWrap.appendChild( this.wrapper );
            if ( UA.ie ){
                Event.on( this.container, 'mousewheel', this._handleMousewheel, this, true );
            } else {
                Event.on( this.container, 'DOMMouseScroll', this._handleMousewheel, this, true);
                Event.on( this.container, 'mousewheel', this._handleMousewheel, this, true );
            }
        },
        /**
         * @method _createScrollbar
         * @private
         */
        _createScrollbar: function(){
            var _that       = this,
                innerScroll = null;
            this.scroller = document.createElement( 'div' );
            this.scroller.className = "infs-scroller";
            // create inner div that sets the scroll height
            this.innerScroll = document.createElement( 'div' );
            this.innerScroll.style.width = '1px';
            this._setInnerScrollHeight();
            this.scroller.appendChild( this.innerScroll );
            this.outerWrap.appendChild( this.scroller );
            // reset scrollbar to top
            this.scroller.scrollTop = 0;
            setTimeout( function(){
                Event.on( _that.scroller, 'scroll', _that._handleScroll, _that, true );
            }, 500 );
        },
        
        /**
         * @method _setInnerScrollHeight
         * @private
         */
        _setInnerScrollHeight: function(){
            if ( this.metadata.totalRows == 0 ){
                this.innerScroll.style.height = this.scroller.offsetHeight + 'px';
            } else {
                this.innerScroll.style.height = parseInt( this.metadata.totalRows * this.rowHeight, 10 ) + 'px';
            }
        },
        
        /**
         * @method _generateRowPlaceholders
         * @private
         */
        _generateRowPlaceholders: function(){
            var totalRowsInView = Math.ceil( ( this.parentNode.offsetHeight - this.headerWrap.offsetHeight ) / this.rowHeight ),
                tBodyEl = this.ytable.getTbodyEl(),
                delta,
                rowLen = tBodyEl.rows.length;
            if ( rowLen != totalRowsInView ){
                
                if ( rowLen > totalRowsInView ){
                    delta = Math.abs( totalRowsInView - rowLen );
                    i = 0;
                    while( i < delta ){
                        tBodyEl.deleteRow( tBodyEl.rows.length - 1 );
                        i++;
                    }
                } else {
                    delta = Math.abs( totalRowsInView - rowLen );
                    i = 0;
                    var isLastRowOdd = Dom.hasClass( tBodyEl.rows[ tBodyEl.rows.length - 1], DT.CLASS_ODD );
                    var isFirst = tBodyEl.rows.length === 0;
                    var classes;
                    while( i < delta ){
                        // tBodyEl.insertRow( tBodyEl.rows.length );
                        var row = this.rowTemplate.cloneNode( true );
                        Dom.removeClass( row, DT.CLASS_EVEN );
                        Dom.removeClass( row, DT.CLASS_ODD );
                        Dom.removeClass( row, DT.CLASS_FIRST );
                        Dom.removeClass( row, DT.CLASS_LAST );
                        classes = [];
                        if ( isLastRowOdd ){
                            classes.push( DT.CLASS_EVEN );
                        } else {
                            classes.push( DT.CLASS_ODD );
                        }
                        isLastRowOdd = !isLastRowOdd;
                        
                        if ( i == delta - 1 ){
                            Dom.replaceClass( row, DT.CLASS_ODD, DT.CLASS_EVEN );
                            classes.push( DT.CLASS_LAST );
                        }
                        
                        if ( isFirst ) {
                            classes.push( DT.CLASS_FIRST );
                            isFirst = false;
                        }
                        row.className = classes.join( ' ' );
                        tBodyEl.appendChild( row );
                        i++;
                    }
                    
                }
            }
            
        },
        /**
         * @method _addRowPlaceholder
         * @private
         */
        _addRowPlaceholder: function(){
            var tBodyEl = this.ytable.getTbodyEl();
            var row = tBodyEl.rows[ tBodyEl.rows.length - 1 ].cloneNode( true );
            var isLastRowOdd = Dom.hasClass( tBodyEl.rows[ tBodyEl.rows.length - 1], DT.CLASS_ODD );
            var isFirst = tBodyEl.rows.length === 0;
            var classes  = [];
            Dom.removeClass( row, DT.CLASS_EVEN );
            Dom.removeClass( row, DT.CLASS_ODD );
            Dom.removeClass( row, DT.CLASS_FIRST );
            Dom.removeClass( row, DT.CLASS_LAST );
            if ( ! isLastRowOdd ){
                classes.push( DT.CLASS_ODD );
            } else {
                classes.push( DT.CLASS_EVEN );
            }
            isLastRowOdd = !isLastRowOdd;
            // remove CLASS_LAST from previous row
            Dom.removeClass( tBodyEl.rows[ tBodyEl.rows.length - 1], DT.CLASS_LAST );
            classes.push( DT.CLASS_LAST );
            if ( isFirst ) {
                classes.push( DT.CLASS_FIRST );
                isFirst = false;
            }
            row.className = classes.join( ' ' );
            tBodyEl.appendChild( row );
        },
        /**
         * @method _setDimensions
         * @private
         */
        _setDimensions: function(e){
            var prevContainerWidth = this.wrapperWidth,
                curContainerWidth = this.gridWrap.offsetWidth,
                headerCols = this.ytable._elHeaderColgroup.childNodes,
                fixedColWidth,
                resizeRatio = 1,
                columnObj,
                elCol;
            
            this.outerWrap.style.height = ( this.parentNode.offsetHeight - this.headerWrap.offsetHeight ) + 'px';
            // We hide the grid lines on the initial load to prevent "empty" rows
            // from appearing (this is more of an aesthetic issue than anything else)
            if ( this.initialLoad ){
                this.showLoadMask( true );
                this.gridWrap.style.visibility = "hidden";
            }
            
            // Generate any needed row placeholders
            this._generateRowPlaceholders();
            if ( ! this.fixedColWidth ){
                for ( var i = 0, j = this.ytable._fixedCols.length; i < j; i++ ){
                    columnObj = this.ytable.getColumn( this.ytable._fixedCols[i].className.substr( 4 ) );
                    this.fixedColWidth += columnObj.getThEl().offsetWidth;
                    // console.log( "Column width: ", this.ytable._fixedCols[i].offsetWidth );
                    // console.log( "Style should be: ", $D.getStyle( this.ytable._fixedCols[i], "width" ) );
                    // console.log( "column parent: ", this.ytable._fixedCols[i].parentNode.parentNode.parentNode );
                }
            }
            fixedColWidth = this.fixedColWidth;
            // We add the width of the spacer column to the "fixed column" width in order
            // to properly calculate the resize ratio for resizeable columns vs. the total
            // width of the containing table
            fixedColWidth += this.spacerCol.prevWidth;
                
            // console.log( "Fixed Column Width: ", fixedColWidth );    
                
            if ( prevContainerWidth > 0 ){
                resizeRatio = ( ( curContainerWidth - fixedColWidth ) / ( prevContainerWidth - fixedColWidth ) ); //+( (curContainerWidth / prevContainerWidth).toPrecision(3) );
            }
            for( var i = 0, j = headerCols.length; i < j; i++ ){
                elCol = headerCols.item( i );
                columnObj = this.ytable.getColumn( elCol.className.substr( 4 ) );    // obtain column key by stripping 'col-' from CSS class
                
                if ( columnObj && columnObj.resizeable ){
                    this.ytable.setColumnWidth( columnObj, Math.round( elCol.prevWidth * resizeRatio ) );
                    // console.log( "resizing column: %o. previous width: %d, new width should be: %d", columnObj, elCol.prevWidth, Math.round( elCol.prevWidth * resizeRatio ) );
                } else {
                    if ( columnObj ){
                        this.ytable.setColumnWidth( columnObj, elCol.prevWidth );
                    }
                }
            }
            // this.ytable.setSpacerColumnWidth( this.spacerCol, this.bodySpacerCol, this.spacerTh );
            this.wrapperWidth = this.gridWrap.offsetWidth;
        },
        /**
         * @method _handleMousewheel
         * @private
         */
        _handleMousewheel: function(e){
            var delta;
            if ( e.detail ){ /* Mozilla */
                delta = -e.detail/3;
            } else { /* IE/Opera/Safari */
                delta = e.wheelDelta / 120;
            }
            if ( delta > 0 ){   // scrolling up
                this.scroller.scrollTop += -( delta * this.rowHeight );
            } else { // scrolling down
                this.scroller.scrollTop += -( delta * this.rowHeight );
            }
            Event.stopEvent( e );
        },
        /**
         * @method _handleScroll
         * @private
         */
        _handleScroll: function( e ){
            if ( this.metadata.totalRows == 0 ){
                this.lastStartOffset = 0;
                this.lastEndOffset = parseInt( this.wrapper.offsetHeight / this.rowHeight );
                this.fireEvent( "dataChangeEvent", { start: 0, end: parseInt( this.wrapper.offsetHeight / this.rowHeight ) } );
                this._scrollTo( this.scroller.scrollTop );
                return;
            }
            
            var _that = this;
            window.clearTimeout( this.scrollThreshold_tId );
            this.scrollThreshold_tId = window.setTimeout( function(){
                // document.getElementById("console").innerHTML += "<br />scrolling...<br />";
                var scrollTop = _that.scroller.scrollTop;
                
                var startOffset = parseInt( scrollTop / _that.rowHeight, 10 ),
                endOffset       = parseInt( _that.wrapper.offsetHeight / _that.rowHeight, 10 ) + startOffset;// + 1;
                if ( endOffset > ( _that.metadata.totalRows - 1 ) ){
                    endOffset = _that.metadata.totalRows - 1;
                }
                
                // console.log( "lastStartOffset: %d, startOffset: %d", _that.lastStartOffset, startOffset );
                // console.log( "lastEndOffset: %d, endOffset: %d", _that.lastEndOffset, endOffset );
                if ( ( startOffset < _that.lastStartOffset || endOffset > _that.lastEndOffset ) || _that.reset === true ){
                    _that.lastStartOffset = startOffset;
                    _that.lastEndOffset = endOffset;
                    _that.reset = false;
                    // _that.scrollTo( scrollTop );
                    _that.loadMask_tId = window.setTimeout( function(){
                        _that.showLoadMask( true );
                    }, _that.loadMaskTimeout );
                    
                    _that.fireEvent( "dataChangeEvent", { start: startOffset, end: endOffset } );
                }
                // console.log( "Scroll Top: ", scrollTop );
                // console.log( "Adjustment should be: ", (scrollTop % _that.rowHeight ) );
                _that._scrollTo( scrollTop );
                
                // if ( scrollTop != _that.lastPixelOffset ){
                //     _that.gridWrap.style.top = -( scrollTop % _that.rowHeight ) + "px";
                //     _that.lastPixelOffset = scrollTop;
                // }
            }, 100 );
        },
        /**
         * Scrolls the table to the given offset
         *
         * @method _scrollTo
         * @private
         * @param pixelOffset {Number} Pixel offset to scroll the table to
         */
        _scrollTo: function( pixelOffset ){
            if ( this.lastPixelOffset == pixelOffset ){
                return;
            }
            // used to scroll container when extra rows are loaded
            this.wrapper.scrollTop = pixelOffset % this.rowHeight;
            this.lastPixelOffset = pixelOffset;
        },
        /////////////////////////////////////////////////////////////////////////////
        //
        // Public methods
        //
        /////////////////////////////////////////////////////////////////////////////
        /**
         * Updates the table's RecordSet with the given rows, as well as
         * updating the UI with the new rows
         *
         * @method updateData
         * @param rows {Array} Array of rows to render
         */
        updateData: function( rows ){
            if ( rows.length < 1 ){
                return;
            }
            window.clearTimeout( this.loadMask_tId );
            this._rowData = rows;
            this.dataSource.makeConnection( null, this.myCallback );
            
            this.showLoadMask( false );
        },
        
        /**
         * Sets the total number of rows in the table to the given number
         *
         * @method setTotalRows
         * @param n {Number} Total number of rows
         */
        setTotalRows: function( n ){
            this.metadata.setTotalRows( n );
            this._setInnerScrollHeight();
            
            if ( n < 1 ){
                this.ytable.showTableMessage( this.ytable.get("MSG_EMPTY"), DT.CLASS_EMPTY );
                this.showLoadMask( false );
            } else {
                this.ytable.hideTableMessage();
            }
        },
        
        /**
         * Returns the total number of rows in the table
         *
         * @method getTotalRows
         * @return {Number} Total number of rows in the table
         */
        getTotalRows: function(){
            return this.metadata.getTotalRows();
        },
        /**
         * Returns selected rows as an array of Primary Keys (specified by the DataSource).
         *
         * @method getSelectedRows
         * @return {String[]} Array of selected rows by Primary Key.
         */
        getSelectedRows: function(){
            var records = [];
            var returnObj = {};
            
            // if we're in a "select all" state, we simply return the total rows minus
            // the exception rows
            if ( this.isSelectAllChecked ){
                returnObj.allSelected = true;
                records = records.concat( this._exceptionRows );
            } else {
                returnObj.allSelected = false;
                // union of selected and checked rows
                var rows = this.ytable.getSelectedRows();
                for ( var i = 0, j = rows.length; i < j; i++ ){
                    records.push( this._getRecordIdByPrimaryKey( this.ytable.getRecord( rows[i] ) ) );
                }
                records = records.concat( this._checkedRows );
                // remove duplicate records due to the merge of arrays
                records = unique( records );
            }
            
            returnObj.msgs = records;
            
            return returnObj;
        },
        
        /**
         * Displays the table's modal/loading overlay
         *
         * @method showLoadMask
         * @param show {Boolean} True to display the load mask, false to hide it
         */
        showLoadMask: function( show ){
            var sLoadMask = this.loadMask.style;
            var sLoadMaskMsg = this.loadMaskMsg.style;
            
            if ( show ){
                sLoadMask.width = ( this.container.clientWidth || this.container.offsetWidth ) + 'px';
                sLoadMask.height = ( this.container.clientHeight || this.container.offsetHeight ) + 'px';
                sLoadMask.display = "block";
                
                sLoadMaskMsg.top = ( this.container.offsetHeight / 2 ) - ( this.loadMaskMsg.offsetHeight / 2 ) + 'px';
                sLoadMaskMsg.left = ( this.container.offsetWidth / 2 ) - ( this.loadMaskMsg.offsetWidth / 2 ) + 'px';
                sLoadMaskMsg.visibility = "visible";
            } else {
                sLoadMask.display = "none";
                sLoadMaskMsg.visibility = "hidden";
            }
        },
        
        /**
         * Set's the text of the table's modal/loading overlay
         *
         * @method setLoadMaskMessage
         * @param show {String|null} The message to display; if NULL is passed, the
         * message reverts to the default "Loading..." specified when the table
         * was instantiated
         */
        setLoadMaskMessage: function( msg ){
            if ( msg ){
                this.loadMaskMsg.innerHTML = msg;
            } else {
                this.loadMaskMsg.innerHTML = this.loadMaskMsg.defaultMsg;
            }
        },
        
        /**
         * 
         */
        setMessage: function( msg, cssClass ){
            if ( ! msg || typeof msg == 'undefined' ){
                // hide message
                this.ytable.hideTableMessage();
            } else {
                this.ytable.showTableMessage( msg, cssClass );
            }
        },
        
        /**
         * 
         */
        getRecord: function( id ){
            return this.ytable.getRecord( id );
        }
    };
    YAHOO.augment( InfiniteScroller, YAHOO.util.EventProvider );
    
    // window.InfiniteScroller = InfiniteScroller;
    NIM.ui.InfiniteScroller = InfiniteScroller;
    /////////////////////////////////////////////////////////////////////////////
    //
    // Generic helper methods
    //
    /////////////////////////////////////////////////////////////////////////////
    /**
    * InfiniteScroller.MetaData
    * @property DataTable.Formatter
    * @type Object
    * @static
    * @private
    */
    InfiniteScroller.MetaData = function( totalRows ){
        this.totalRows = totalRows;
    };
    InfiniteScroller.MetaData.prototype = {
        getPageSize: function(){
            return this.totalRows < this.pageSize ? this.totalRows : this.pageSize;
        },
        getTotalRows: function(){
            return this.totalRows;
        },
        setTotalRows: function( t ){
            this.totalRows = t;
        }
    };
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Generic helper methods
    //
    /////////////////////////////////////////////////////////////////////////////
    /**
     * DataSource provides a common configurable interface for which other components can 
     * fetch tabular data from. It is a required dependency of the InfiniteScroller control.
     * @namespace NIM.util
     * @class DataSource
     * @constructor
     */
    NIM.util.DataSource = function(){
        this.responseType = YAHOO.util.DataSource.TYPE_JSARRAY;
    };
    
    NIM.util.DataSource.prototype = {
        /**
         * Response schema object literal take the following properties:
         *
         * <dl>
         * <dt>fields</dt> <dd>Array of field names (aka keys), or array of object literals
         * such as: {key:"fieldname",parser:myCustomDateParser}</dd>
         * </dl>
         *
         * @property responseSchema
         * @type Object
         */
        responseSchema : null
    };
    /////////////////////////////////////////////////////////////////////////////
    //
    // Generic helper methods
    //
    /////////////////////////////////////////////////////////////////////////////
    // src: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript
    function unique(arr) {
        var hash = {}, result = [];
        for ( var i = 0, l = arr.length; i < l; ++i ) {
            if ( !hash.hasOwnProperty(arr[i]) ) { //it works with objects! in FF, at least
                hash[ arr[i] ] = true;
                result.push(arr[i]);
            }
        }
        return result;
    }   
    
} )();