/**
* XSLDataGrid
*** Copyright (c) 2006, Lindsey Simon <lsimon@commoner.com>
* All rights reserved.
* 
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
* 
* *       Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* *       Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* 
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**/

/**
* @class constructor for XSLDataGrid
* @constructor
* @param {string} container_id to place the grid inside of
* @param {object} options
*
* <code>
* new XSLDataGrid( 'renderDiv', { url: 'XSLDataGridTestTransform.php', extra_parameters: 'module=heaven&uniqueid=6', width: 400, height: 250, transformer: 'client' } );
* </code>
* 
**/
var XSLDataGrid = Base.extend({
      
      /**
      * @constructor
      * @param {string} container_id to place the grid inside of
      * @param {object} options
      */
      constructor: function( container_id, options ) {

         console.debug("XSLDataGrid.initialize container_id: " + container_id );
         this._container_id = container_id;
         
         options = Object.extend({
               url: '',
               extra_parameters: '',
               width: 300,
               height: 150,
               transformer: 'client',
               prefetch: true,
               xdgPopupDivId: 'xdgPopupDiv',
               rowReloadLimitOnRearrange: 200,
               hideColContextMenuDelay: 1000,
               scrollerWidth: 20,
               debugging: false
      }, arguments[1] || {});
         this.options = options;
         
         // sets the debugging function
         this.setDebug( this.options.debugging );
         
         // possibly initialize xslt
         this.setTransformer( this.options.transformer );
         
         // set up some private vars
         this._selectedRows = [];
         this._colContextMenuFilters = [];
         
         // load data from url?
         if ( this.options.prefetch && this.options.url ) {
            this.load();
         }
         
         this.debug("XSLDataGrid.initialize complete");
      },
      
      /**
      * set debug capability
      * if true, use console.log from Firebug extension
      * @param {bool}
      */
      setDebug: function( bool ) 
      {
         if ( bool ) {
            this.debug = console.debug;
         }
         else {
            this.debug = Prototype.emptyFunction;
         }
      },
      
      /**
      * Where xslt will take place - either client or server
      * Optionally initialize clientside xslt once and pass a doXSLT along
      * @param {string} transformer
      **/
      setTransformer: function( transformer ) {
         this._transformer = transformer;
         
         // set sortMechanism if not set in options
         if ( this._transformer == 'client' && typeof this.options.sortMechanism == 'undefined' ) {
            this.options.sortMechanism = 'client';
         }
         else if ( this._transformer == 'server' && typeof this.options.sortMechanism == 'undefined' ) {
            this.options.sortMechanism = 'server';
         }
         
         var doXSLT = ( this.options.url || this.options.prefetch === false ) ? false : true;
         
         this.debug( "XSLDataGrid.setTransformer transformer: " + transformer + ", options.url: " + this.options.url + ", options.prefetch: " + this.options.prefetch + ", sortMechanism: " + this.options.sortMechanism + ", doXSLT: " + doXSLT );
         
         if ( this._transformer == "client"  && !this._XSLTProcessor ) {
            this.initClientSideXSLT( doXSLT );
         }
      },
      
      /**
      * All parameters to pass onto Ajax requests
      * @return {string} parameters
      */
      getParameters: function() {
         var parameters = "";
         
         // get the lay of the land
         parameters += "&group="+ this._group;
         parameters += "&sort=" + this._sort;
         parameters += "&order=" + this._order;
         
         // add in any extra parameters if there are some
         parameters += this.options.extra_parameters ? "&" + this.options.extra_parameters : "";
         return parameters;
      },
      
      /**
      * Store the elements into the memory
      * this should help us from making unnecessary calls to $ aka getElementById
      * @param container_id container_id
      */
      loadElementsIntoMemory: function() {
         this.debug("XSLDataGrid.loadElementsIntoMemory container_id:" + this._container_id);
         Utility.tick( 'XSLDataGrid.loadElementsIntoMemory' );
         
         // initialize to ensure these are clean after any DOM replacements in client
         this._activeRowId = '';
         this._selectedRows = [];
         
         // store DOM references to all the containers for functionality speed later
         this.xdgFrame = $( 'xdgFrame_' + this._container_id );
         this.xdgSuperContainer = this.xdgFrame.firstChild;
         this.xdgContainer = this.xdgSuperContainer.firstChild;
         
         this.xdgOverlay = $( 'xdgOverlay_' + this._container_id );
         this.xdgLoading = $( 'xdgLoading_' + this._container_id );
         
         this.xdgHeaderTable = $( 'xdgHeaderTable_' + this._container_id );
         this.xdgHeaderRow = $( 'xdgHeaderRow_' + this._container_id );
         this.gridLastHeaderCell = $( 'xdgHeaderCell_' + this._container_id + '_LAST' );
         
         this.xdgDataContainer = $( 'xdgDataContainer_' + this._container_id );
         this.xdgDataTable = this.xdgDataContainer.firstChild;
         this.xdgDataColgroup = $( 'xdgDataColgroup_' + this._container_id );
         this.gridLastDataCol = $( 'xdgDataCol_' + this._container_id + '_LAST' );
         
         // the scroller pieces
         this.xdgScrollContainer = $( 'xdgScrollContainer_' + this._container_id );
         this.xdgScroller = $( 'xdgScroller_' + this._container_id );
         this.xdgScrollHeight = $( 'xdgScrollHeight_' + this._container_id );
         
         // init group 
         var groupCell = $A( this.xdgHeaderRow.cells ).find( function( cell ) { return $( cell ).hasClassName( 'grouped' ); } );
         if ( groupCell ) {
            this._group = this._group_previous = groupCell.getAttribute( 'col_id' );
         }
         else {
            this._group = this._group_previous = false;
         }
         
         // init sort based on class settings in xhtml
         var sortAscCell = $A( this.xdgHeaderRow.cells ).find( function( cell ) { return $( cell ).hasClassName( 'sorted-ascending' ); } );
         var sortDescCell = $A( this.xdgHeaderRow.cells ).find( function( cell ) { return $( cell ).hasClassName( 'sorted-descending' ); } );
         
         if ( sortAscCell ) {
            this._sort = this._sort_previous = sortAscCell.getAttribute( 'col_id' );
            this._order = 'ascending';
         }
         else if ( sortDescCell ) {
            this._sort = sortDescCell.getAttribute( 'col_id' );
            this._order = this._order_previous = 'descending';
         }
         this.debug( "XSLDataGrid.loadElementsIntoMemory _group: " + this._group + ", _sort: " + this._sort + ", _order: " + this._order );
         
         // add data elements
         if ( this.xdgDataTable ) {
            
            // resize guide - look for global one, else take unique one
            this.xdgResizeGuide = $( 'xdgResizeGuide' ) ? $( 'xdgResizeGuide' ) : $( 'xdgResizeGuide_' + this._container_id );
         }
         
         // register Event listeners
         this.registerEventListeners();
         
         Utility.tock( 'XSLDataGrid.loadElementsIntoMemory' );
      },
      
      
      /**
      * registerEventListeners
      */
      registerEventListeners: function() {

         this.debug( "XSLDataGrid.registerEventListeners" );
         
         // turn off context menu for grid so that we can use right click menus
         $( this._container_id ).oncontextmenu = function() { return false; }
         
         // make sure we're ready to register
         if ( !this._bindEventListenersComplete ) {
            this.bindEventListeners();
         }
         
         // scrolling the data
         Event.observe( this.xdgScroller, "scroll", this._eventScrollDataTable );
         
         if (window.addEventListener) {
            /** DOMMouseScroll is for mozilla. */
            Event.observe( this.xdgDataTable, "DOMMouseScroll", this._eventDataTableMouseWheel );
         }
         else {
            /** IE/Opera. */
            Event.observe( this.xdgDataTable, "mousewheel", this._eventDataTableMouseWheel );
         }
         
         // for cell/row selection/activation / keypress
         if ( this.xdgDataTable ) {
            
            // for key press on datatable
            Event.observe( this.xdgDataTable, "mouseover", this._eventDataTableMouseOver );
            Event.observe( this.xdgDataTable, "mouseout", this._eventDataTableMouseOut );
            
            // row selection and activation
            Event.observe( this.xdgDataTable, "mousedown", this._eventDataTableMouseDown );
            Event.observe( this.xdgDataTable, "mouseup", this._eventDataTableMouseUp );
            
            // doubleclick
            Event.observe( this.xdgDataTable, "dblclick", this._eventDataTableDoubleClick );
         }
         
         // sorting, resizing, context menu
         Event.observe( this.xdgHeaderRow, "mousedown", this._eventHeaderMouseDown );
         Event.observe( this.xdgHeaderRow, "mouseup", this._eventHeaderMouseUp );
      },
      
      
      /**
      * bindEventListeners
      * Creates properties bound to this so we can start and stop observing
      */
      bindEventListeners: function() {

         
         this.debug( "XSLDataGrid.bindEventListeners" );
         this._eventScrollDataTable = this.scrollDataTable.bindAsEventListener( this );
         this._eventDataTableMouseWheel = this.dataTableMouseWheel.bindAsEventListener( this );
         this._eventDataTableKeypress = this.dataTableKeypress.bindAsEventListener( this );
         this._eventDataTableMouseOver =  this.dataTableMouseOver.bindAsEventListener( this );
         this._eventDataTableMouseOut =  this.dataTableMouseOut.bindAsEventListener( this );
         this._eventDataTableMouseDown = this.dataTableMouseDown.bindAsEventListener( this );
         this._eventDataTableMouseUp = this.dataTableMouseUp.bindAsEventListener( this );
         this._eventDataTableDoubleClick = this.dataTableDoubleClick.bindAsEventListener( this );
         this._eventHeaderMouseDown = this.headerMouseDown.bindAsEventListener( this );
         this._eventHeaderMouseUp = this.headerMouseUp.bindAsEventListener( this );
         this._eventDocumentMouseMove = this.colResizing.bindAsEventListener( this );
         this._eventDocumentMouseUp = this.stopColResize.bindAsEventListener( this );
         
         // done
         this._bindEventListenersComplete = true;
      },
      
      /**
      * destroyEventListeners
      */
      destroyEventListeners: function() {

         this.debug( "XSLDataGrid.destroyEventListeners" );
         // scrolling the data
         Event.stopObserving( this.xdgScroller, "scroll", this._eventScrollDataTable );
         Event.stopObserving( this.xdgDataTable, "mousewheel", this._eventDataTableMouseWheel);
         Event.stopObserving( this.xdgDataTable, "DOMMouseScroll", this._eventDataTableMouseWheel );
         
         // for cell/row selection/activation / keypress
         if ( this.xdgDataTable ) {
            
            // for key press on datatable
            Event.stopObserving( this.xdgDataTable, "mouseover", this._eventDataTableMouseOver );
            Event.stopObserving( this.xdgDataTable, "mouseout", this._eventDataTableMouseOut );
            
            // row selection and activation
            Event.stopObserving( this.xdgDataTable, "mousedown", this._eventDataTableMouseDown );
            Event.stopObserving( this.xdgDataTable, "mouseup", this._eventDataTableMouseUp );
            
            // doubleclick
            Event.stopObserving( this.xdgDataTable, "dblclick", this._eventDataTableDoubleClick );
         }
         
         // sorting, resizing, context menu
         Event.stopObserving( this.xdgHeaderRow, "mousedown", this._eventHeaderMouseDown );
         Event.stopObserving( this.xdgHeaderRow, "mouseup", this._eventHeaderMouseUp );
      },
      
      
      /**
      * removeDataTableFromDOM
      *
      * Speed up DOM operations on the xdgDataTable by temporarily
      * removing it from the live DOM
      */
      removeDataTableFromDOM: function() 
      {
         this.debug( "XSLDataGrid.removeDataTableFromDOM " + this.xdgDataContainer.id + ", " + this.xdgDataTable.id + ", same?: " + (this.xdgDataContainer.firstChild == this.xdgDataTable ) );
         this.xdgDataContainer.removeChild( this.xdgDataTable );
      },
      
      /**
      * restoreDataTableToDOM
      *
      * Get the xdgDataTable back into the live DOM
      */
      restoreDataTableToDOM: function() 
      {
         this.xdgDataContainer.insertBefore( this.xdgDataTable, null );
      },
      
      /**
      * Has the Grid loaded itself yet?
      * When loaded, we want to get dom elements into memory if x is true
      * @param {bool} x optional true || false to set.
      **/
      loaded: function( x ) {

         if ( x !== undefined ) { 
            this._loaded = x; 
         }
         if ( x ) {
            this.loadElementsIntoMemory();
         }
         return this._loaded;
      },
      
      /**
      * Positions the loading animation div based on current container size
      * @return {void}
      */
      buildLoadingAnimation: function( message ) {

         this.debug( "XSLDataGrid.buildLoadingAnimation" );
         
         // prevents errors on early calls to load
         if ( !this.xdgHeaderRow ) { return; }
         if ( !this._height ) { return; }
         if ( !this.xdgOverlay ) { return; }
         
         // base our position on the x,y of the header row
         Position.prepare();
         var pos = Position.page( this.xdgHeaderRow );
         var x = pos[0] + Position.deltaX;
         var y = pos[1] + Position.deltaY;
         
         
         // turn on the overlay and loading
         this.xdgOverlay.style.top = this.xdgLoading.style.top = y + "px";
         this.xdgOverlay.style.left = this.xdgLoading.style.left = x + "px";
         this.xdgOverlay.style.width = this.xdgLoading.style.width = this._width + "px";
         this.xdgOverlay.style.height = this.xdgLoading.style.height = this._height + "px";
         this.xdgOverlay.show();
         this.xdgLoading.show();
         
         return;
      },
      
      /**
      * Turn off the loading animation
      * @return {void}
      */
      stopLoadingAnimation: function() 
      {
         // turn off the overlay and loading
         this.xdgOverlay.hide(); 
         this.xdgLoading.hide();
         return true;
      },
      
      
      /**
      * Resize the grid to the specified width and height.
      * @param {int} width in pixels
      * @param {int} height in pixels
      *
      */
      resize: function( width, height ) {

         Utility.tick( 'XSLDataGrid.resize' );
         this.debug('XSLDataGrid.resize width:' + width + ', height: ' + height ); 
         if ( width === 0 || height === 0 ) { return; }
         
         this._width = width ? width : this.options.width;
         this._height = height ? height : this.options.height;
         
         this.debug('XSLDataGrid.resize calc width:' + this._width + ', height: ' + this._height );
         
         // only resize if we're loaded
         if ( this.loaded() ) {
            if ( this.xdgFrame.style.width != ( this._width + "px") ) {
               this.resizeWidth();
            }
            if ( this.xdgFrame.style.height != ( this._height + "px" ) ) {
               this.resizeHeight();
            }
         }
         
         // custom event
         this.fireEvent( 'resize' );
         
         Utility.tock( 'XSLDataGrid.resize' );
      },
      
      /**
      * resizeWidth
      */
      resizeWidth: function() {
         
         this.debug( "XSLDataGrid.resizeWidth" );
         Utility.tick( 'XSLDataGrid.resizeWidth' );
         
         // get "actual" container width set in template
         var containerwidth = parseInt( this.xdgContainer.getAttribute( 'containerwidth' ) );
         
         // set total with on superduper
         this.xdgFrame.style.width = this._width + "px";
         
         // remove width of a scrollerContainer
         var superWidth = this._width - this.options.scrollerWidth;
         if ( superWidth > 0 ) {
            this.xdgSuperContainer.style.width = superWidth + "px";
         }
         
         // test to see if we need to extend our grid last column
         var isBiggerBy = superWidth - containerwidth;
         
         //this.debug(this._container_id + ' super bigger by:'+isBiggerBy+', cw:'+containerwidth + ", superWidth: " + superWidth);
         if (isBiggerBy > 0) {
            this.xdgContainer.style.width = this.xdgHeaderTable.style.width = superWidth + "px";
            this.gridLastHeaderCell.style.width = isBiggerBy + "px";
            
            if ( this.xdgDataContainer ) {
               this.xdgDataContainer.style.width = this.xdgDataTable.style.width = superWidth + "px";
               this.gridLastDataCol.style.width = isBiggerBy + "px";
            }
            
         }
         else {
            this.xdgContainer.style.width = this.xdgHeaderTable.style.width = containerwidth + "px";
            this.gridLastHeaderCell.style.width = "0px";
            
            if ( this.xdgDataTable ) {
               this.xdgDataTable.style.width = this.xdgDataContainer.style.width = containerwidth + "px";
               this.gridLastDataCol.style.width = "0px";
            }
         }
         
         Utility.tock( 'XSLDataGrid.resizeWidth' );
      },
      
      /**
      * resizeHeight
      */
      resizeHeight: function() {
         
         this.debug( "XSLDataGrid.resizeHeight" );
         Utility.tick( 'XSLDataGrid.resizeHeight' );

         // set a bunch of heights based on total height which necessarily includes headers
         this.xdgFrame.style.height = this.xdgSuperContainer.style.height = this.xdgContainer.style.height =  this._height + "px";
         
         // calc the header height and make sure scroller is there
         var headerHeight = this.xdgHeaderTable.offsetHeight;
         this.xdgScroller.style.top = headerHeight + "px";
         
         // set vscroller height
         this.xdgScrollContainer.style.height = this.xdgScroller.style.height = this._height - headerHeight + "px";
         
         // set the data container to the full height
         var containerHeight = this._height - headerHeight - this.options.scrollerWidth;
         
         
         if ( containerHeight > 0 && this.xdgDataContainer ) {
            this.xdgDataContainer.style.height = containerHeight + "px";
         }
         
         // assume the existence of a horiz. scroll
         if ( this.xdgDataTable ) {
            this._dataheight = this.xdgDataTable.offsetHeight;
         }
         
         this.xdgScrollHeight.style.height = this._dataheight + this.options.scrollerWidth + "px";
         
         Utility.tock( 'XSLDataGrid.resizeHeight' );
         
      },
      
      
      /**
      * ScrollDataTable Handler
      * @param {obj} the xdgScroller element
      */
      scrollDataTable: function( e ) {
         //this.debug( "XSLDataGrid.scrollDataTable element.scrollTop:" + this.xdgScroller.scrollTop ); 
         this.xdgDataContainer.scrollTop = this.xdgScroller.scrollTop;
      },
      
      
      
      /**
      * dataTableMouseWheel
      * Event handler for mouse wheel event.
      * @param {obj} e event object
      */
      dataTableMouseWheel: function ( e ) {
         //console.debug( "XSLDataGrid.dataTableMouseWheel" );
         var delta = 0;
         
         /* IE/Opera. */
         if ( e.wheelDelta ) {
            delta = e.wheelDelta/120;
            // In Opera 9, delta differs in sign as compared to IE.
            if ( window.opera ) {
               delta = -delta;
            }
         }  
         /** Mozilla case. */
         else if ( e.detail ) {
            /** In < 2.0 versions of Mozilla, sign of delta is negative
            * and delta is multiple of 3.
            */
            delta = -e.detail/3;
         }
         
         /** If delta is nonzero, handle it.
         * Basically, delta is now positive if wheel was scrolled up,
         * and negative, if wheel was scrolled down.
         */
         if ( delta ) {
            delta *= 20;
            this.xdgScroller.scrollTop -= delta;
            this.scrollDataTable();
         }
         
         /** Prevent default actions caused by mouse wheel.
         * That might be ugly, but we handle scrolls somehow
         * anyway, so don't bother here..
         */
         Event.stop( e );
      },
      
      
      /**
      * Load the Grid and set the Sort and Group MenuBar Menus to the proper actions.
      **/
      load: function() 
      {
         this.debug( "XSLDataGrid.load container_id:" + this._container_id + ", transformer: " + this.transformer );
         
         // loading
         this.buildLoadingAnimation();
         
         // if clientside xslt, don't automatically fill in the container_id with
         // the result of the Ajax response
         var container_id = this._transformer == "client" ? '' : this._container_id;
         
         Utility.AjaxUpdater( container_id, {
               method: "get",
               url: this.options.url,
               parameters: this.getParameters(),
               onComplete: this.onLoad.bind( this )
         });
      },
      
      /**
      * onLoad after AjaxUpdater completes
      * @param {obj} request
      */
      onLoad: function( transport ) {
         this.debug("XSLDataGrid.onLoad transport:" + transport + ", transformer: " + this._transformer );
         // if clientside xslt, store our dual DOM XMLDOC first and then call transform
         if ( this._transformer == "client" ) {
            this._XMLDOMDoc = this._DOMParser.parseFromString( transport.responseText, "application/xhtml+xml" );
            this.doClientSideXSLT();
         }
         // load in DOM elements to memory now and set loaded flag
         else {
            $( this._container_id ).innerHTML = transport.responseText;
            this.loaded( true );
            
            // turn off loading
            this.stopLoadingAnimation();
            
            // resize for niceness
            this.resize();
            
         }
         
         // let everyone know that it's all cool...
         if ( typeof this.onXSLDataGridLoad == "function" ) {
            this.onXSLDataGridLoad();            
         }
         
         // custom load
         this.fireEvent( 'load' );
      },
      
      /**
      * Sort the grid
      * @param {string} col_id
      * @param {string} order
      **/
      sort: function( col_id, order ) {

         this.debug( "XSLDataGrid.sort col_id: " + col_id + ", order: " + order + ", sortMechanism: " + this.options.sortMechanism + ", url: " + this.options.url );
         order = (order !== undefined) ? order : 'asc';
         
         // store previous value
         this._sort_previous = this._sort;
         this._order_previous = this._order;
         
         // set new value
         this._sort = col_id;
         this._order = order;
         
         // sort on server
         if ( this.options.url && this.options.sortMechanism == 'server' ) {
            this.load();
         }
         // sort in client + we need an interval to give the spinner time to get seen
         else {
            this.buildLoadingAnimation();
            this._sortOrGroupInClientInterval = window.setTimeout( this.sortOrGroupInClient.bind( this, 10 ) );
         }
      },
      
      
      
      /**
      * Group the grid
      **/
      group: function( group ) {

         this.debug( "XSLDataGrid.group group:" + group );
         
         
         // store previous value
         // store previous value
         this._sort_previous = this._sort;
         this._order_previous = this._order;
         this._group_previous = this._group;
         
         // set new value
         if ( !group ) { group = ''; }
         this._group = group;
         
         // group on server
         if ( this.options.url && this.options.sortMechanism == 'server' ) {
            this.load();
         }
         // group in client + we need an interval to give the spinner time to get seen
         else {
            this.buildLoadingAnimation();
            this._sortOrGroupInClientInterval = window.setTimeout( this.sortOrGroupInClient.bind( this, 10 ) );
         }
      },
      
      /**
      * Hides the TBODY for this subset.
      * Triggered by the grouping top TRs.
      * @param {HTMLTableRowElement} row TR element
      * @param {MouseEvent} e mouse event
      **/
      rowSetClick: function( row, e ) {

         var row_parts = row.id.split('_');
         var tbody_id  = 'xdgRowSet_' + row_parts[1] + '_' + row_parts[2];
         var tbody = $( tbody_id );
         
         if ( tbody.isActive === false ) {

            tbody.isActive = true;
            // turn it on...
            tbody.style.display = '';
         }
         else
         {
            tbody.isActive = false;
            // turn it off...
            tbody.style.display = 'none';
         }
         
      },
      
      /**
      * Can we extrapolate a header cell from this event?
      *
      */
      getHeaderCellFromEvent: function( e ) {

         var th = false;
         if ( Event.element( e ).tagName == "TH" ) {
            th = Event.element( e );
         }
         else if ( Event.element( e ).parentNode.tagName == "TH" ) {  
            th = Event.element( e ).parentNode;
         }
         else if ( Event.element( e ).parentNode.parentNode.tagName == "TH" ) {  
            th = Event.element( e ).parentNode.parentNode;
         }
         return th;
      },
      
      /**
      * headerMouseDown
      * check for right click context menu
      * @param {obj} e
      */
      headerMouseDown: function( e ) {

         var element = $( Event.element( e ) );
         this.debug( "XSLDataGrid.headerMouseDown element:" + element.id + ", tagName: " + element.tagName + ", className: " + element.className );
         // only catch left clicks on mouse down
         if ( Event.isLeftClick( e ) ) {
            // clicked on sort text
            if ( element.hasClassName( 'xdgHeaderLabelText' ) ) {
               this.sortMouseDown( e );
            }
            // clicked on resizer
            else if ( element.hasClassName( 'xdgColResizer' ) ) {
               this.startColResize( e );
            }
            // drag it
            else {
               var th = this.getHeaderCellFromEvent( e );
               // clicked on the column - bootstrap dragging it
               if ( th && $( th ).hasClassName( 'rearrangeable' ) ) { 
                  this.toggleColsDraggable( true );
                  var draggable = Draggables.drags.find( function( d ) { return ( th.id == d.element.id ); } );
                  draggable.initDrag( e );
                  Draggables.updateDrag( e );
               }
            }
         }
      },
      
      /**
      * headerMouseUp
      * check for right click context menu
      * @param {obj} e
      */
      headerMouseUp: function( e ) {

         this.debug( "XSLDataGrid.headerMouseUp element:" + Event.element( e ).id );
         
         var element = $( Event.element( e ) );
         
         // most likely a sort
         if ( Event.isLeftClick( e ) ) {
            // from the text label
            if ( element.tagName == "DIV" && element.hasClassName( 'sortable' ) ) {
               this.sortMouseUp( e );
            }
         }
         // openColContextMenu on certain elements
         else if ( Event.isRightClick( e ) ) {
            // open context menu if we can get to a th
            var th = this.getHeaderCellFromEvent( e );
            if ( th ) {
               this.openColContextMenu( e, th.getAttribute( 'col_id' ), th.getAttribute( 'col_label' ) );
            }
         }
      },
      
      /**
      * dataTableKeypress
      * When over the data table, allow activating up and down the list
      * @param {obj} e
      */
      dataTableKeypress: function( e ) {

         this.debug( "XSLDataGrid.dataTableKeypress this.container_id: " + this._container_id + " keyCode: " + e.keyCode + ", ctrl? " + e.ctrlKey );
         Event.stop( e );
         return;
         /*
         // #TODO - fix this for grouping
         if ( e.keyCode == Event.KEY_UP )
            this.activatePreviousRow();
         else if ( e.keyCode == Event.KEY_DOWN )
            this.activateNextRow();
         */
      },
      
      /**
      * dataTableMouseOver
      * @param {obj} e
      */
      dataTableMouseOver: function( e ) {

         //this.debug( "XSLDataGrid.dataTableMouseOver element:" + Event.element( e ).id );
         Event.observe( document, "keypress", this._eventDataTableKeypress );
      },
      
      /**
      * dataTableMouseOut
      * @param {obj} e
      */
      dataTableMouseOut: function( e ) {

         //this.debug( "XSLDataGrid.dataTableMouseOut element:" + Event.element( e ).id );
         Event.stopObserving( document, "keypress", this._eventDataTableKeypress );
      },
      
      
      /**
      * dataTableMouseDown
      * in case we ever want to do more than call row-centric events
      * @param {obj} e
      */
      dataTableMouseDown: function( e ) {
         var row;
         var element = Event.element( e );
         this.debug( "XSLDataGrid.dataTableMouseDown element:" + element.id + ", element.tagName: " + element.tagName );
         
         // parentNode is row
         if ( element.tagName == "B") {
            row = element.parentNode.parentNode;
         }
         else {
            row = element.parentNode;
         }
         this.rowMouseDown( row, e );
      },
      
      /**
      * dataTableMouseUp
      * in case we ever want to do more than call row-centric events
      * @param {obj} e
      */
      dataTableMouseUp: function( e ) {

         var element = Event.element( e );
         //this.debug( "XSLDataGrid.dataTableMouseUp element:" + element.id );
         // parentNode is row
         var row = element.parentNode;
         this.rowMouseUp( row, e );
      },
      
      /**
      * dataTableDoubleClick
      * in case we ever want to do more than call row-centric events
      * @param {obj} e
      */
      dataTableDoubleClick: function( e ) {

         var element = Event.element( e );
         var row = element.parentNode;
         this.rowDblClick( row, e );
      },
      
      
      /**
      * rowMouseDown
      * Event Handler.
      * Just passes the event on to the callback.
      * @param {HTMLElement} row the TR that received the event
      * @param {MouseEvent} e
      **/
      rowMouseDown: function( row, e ) {

         // call Left or Right click functions
         if ( Event.isLeftClick( e ) ) {
            this.rowLeftClick( row, e );
         }
         else if ( Event.isRightClick( e ) ) {
            this.rowRightClick( row, e );
         }
         
         Event.stop( e );
      },
      
      /**
      * rowMouseUp
      * Event Handler.
      * Just passes the event on to the callback.
      * @param {HTMLElement} row the TR that received the event
      * @param {MouseEvent} e
      **/
      rowMouseUp: function( row, e ) {

      },
      
      /**
      * rowLeftClick
      * Dispatcher method called when a left click has been fired.
      * figures out if the user wants to select or activate a row.
      * @param {HTMLElement} row the TR that has been clicked
      * @param {MouseEvent} e
      **/
      rowLeftClick: function( row, e ) {

         this.debug( "XSLDataGrid.rowLeftClick row.id: " + row.id  + ", row.className: " + row.className + ", row.tagName: " + row.tagName );
         
         // prevent event bubbling
         Event.stop( e );
         
         // see if we're clicking a row group
         if ( row.className.match( 'xdgGrouped' ) ) {
            
            // if they click a bottom do nothing
            if ( row.className.match( 'Bottom' ) ) { return; }
            
            // get a reference to the tbody for the top grouper
            var tbody;
            if ( row.tagName == "DIV" ) {
               tbody = row.parentNode.parentNode.parentNode;
            }
            else if ( row.tagName == "TD" ) {
               tbody = row.parentNode.parentNode;
            }
            
            // the one to open/close is its nextSibling
            $( tbody.nextSibling ).toggle();
            
         }
         // if no active row, only activate
         else if ( !this.hasActiveRowId() ) {
            this.activateRow( row.id );        
         }
         else if ( e.shiftKey ) {
            this.rangeSelectRow( row.id );
         }
         else {
            this.activateRow( row.id );
         }
         
      },
      
      
      /**
      * rowRightClick
      * @param {HTMLElement} row the TR that has been clicked
      * @param {MouseEvent} e
      **/
      rowRightClick: function( row, e ) { 
         // custom event
         this.fireEvent( 'rowRightClick', { row: row, event: e } );
      },
      
      
      /**
      * rowDblClick
      * @param {HTMLElement} row the TR that received the event
      * @param {MouseEvent} e
      **/
      rowDblClick: function( row, e ) {

         Event.stop( e );
         
         // custom event
         this.fireEvent( 'rowDblClick', { row: row, event: e } );
      },
      
      
      
      /**
      * rowMouseOver
      * @param {HTMLElement} row the TR that received the event
      * @param {MouseEvent} e
      **/
      rowMouseOver: function( row, e ) {

         Event.stop( e );
         
         // custom event
         this.fireEvent( 'rowMouseOver', { row: row, event: e } );
      },
      
      /**
      * rowMouseOut
      * @param {HTMLElement} row the TR that received the event
      * @param {MouseEvent} e
      **/
      rowMouseOut: function( row, e ) {

         Event.stop( e );
         
         // custom event
         this.fireEvent( 'rowMouseOut', { row: row, event: e } );
      },
      
      /**
      * activateRow
      * Only one row may be active at a time.
      * @param {string} row_id DOM id of the target row.
      **/
      activateRow: function( row_id ) {

         this.debug( "XSLDataGrid.activateRow row_id: " + row_id );
         
         if ( !row_id ) { return; } // in case they click a link in a cell in the row or something
         
         // clear out selected... and set the row being activated as the first new row selected.
         this.clearSelectedRows();
         this.addSelectedRow( row_id );
         
         // set the activated row's classname to be active
         $( row_id ).addClassName( 'active' );
         
         // log the new row as the active one
         this._activeRowId = row_id;
         
         // custom event
         this.fireEvent( 'activateRow', { row_id: row_id, datagrid: this } );
      },
      
      
      /**
      * deactivateRow
      */
      deactivateRow: function( ) {

         this.clearSelectedRows();
         this._activeRowId = null;
      },
      
      /**
      * activateNextRow
      */
      activateNextRow: function() {
         // if no row active, set to first row
         if ( !this.hasActiveRowId() ) {
            this.activateRow( this.xdgDataTable.rows[0].id );
         }
         else {
            var activeRowIndex = $( this._activeRowId ).rowIndex;
            // make sure we're not already on the last row
            if ( this.xdgDataTable.rows.length != activeRowIndex-1 ) {
               this.activateRow( this.xdgDataTable.rows[activeRowIndex+1].id );
            }
         }
      },
      
      /**
      * activatePreviousRow
      */
      activatePreviousRow: function() {
         // if no row active, set to first row
         if ( !this.hasActiveRowId() ) {
            this.activateRow( this.xdgDataTable.rows[this.xdgDataTable.rows.length-1].id );
         }
         else {
            var activeRowIndex = $( this._activeRowId ).rowIndex;
            // make sure we're not already on the first row
            if ( activeRowIndex !== 0 ) {
               this.activateRow( this.xdgDataTable.rows[activeRowIndex-1].id );
            }
         }
      },
      
      /**
      * rangeSelectRow
      * Select a range of rows from the active_id, to the row_id
      * @param {string} row_id DOM id of the target row.
      **/
      rangeSelectRow: function( row_id ) 
      {
         // is the active row after the one clicked?... if so, then we'll loop ascending..
         var n_start = $( row_id ).rowIndex;
         var n_end   = $( this._activeRowId ).rowIndex;
         var n_asc   = (n_end > n_start);
         
         var rows = this.getRows();
         for ( var i = n_start; i != n_end; (n_asc ? i++ : i--) ) {

            if ( !this.isSelectedRow( rows[i].id ) ) { this.selectRow( rows[i].id ); }
         }
      },
      
      /**
      * selectRow
      * @param {string} row_id DOM id of the target row.
      **/
      selectRow: function( row_id ) {

         $( row_id ).addClassName( 'selected' );
         this.addSelectedRow( row_id );
      },
      
      /**
      * getRows
      * Get all the rows in the grid.
      * Makes sure to only return real rows and not include the rows that
      *   make up the grouping top and bottom.
      * @return An array of HTMLTableRowElements
      * @type array
      **/
      getRows: function() {

         var rows = [];
         var table = this.xdgDataTable;
         if ( table && table.tBodies ) {
            for ( var i = 0, n = table.tBodies.length; i < n; i++ ) {
               if ( table.tBodies[i].className == "xdgRowSet" ) {
                  for ( var ii = 0, nn = table.tBodies[i].rows.length; ii < nn; ii++ ) {
                     rows.push( table.tBodies[i].rows[ii] );
                  }
               }
            }
         }
         return rows;
      },
      
      /**
      * selectAllRows
      **/
      selectAllRows: function() {

         var rows = this.getRows();
         for ( var i = 0, n = rows.length; i < n; i++ ) {

            if ( !this.isSelectedRow( rows[i].id ) ) { this.selectRow( rows[i].id ) };
         }
      },
      
      /**
      * deleteSelectedRows
      * Remove rows from the grid.
      * @type void
      **/
      deleteSelectedRows: function() {

         var rows  = this._selectedRows;
         var table = this.xdgDataTable;
         
         if ( !table ) { return; }
         
         for ( var i = 0, n = rows.length; i < n; i++ ) {
            var row = $( rows[i] );
            if ( row ) { table.removeChild( row ); }
         }
         
         this.clearSelectedRows();
         this._activeRowId = null;
      },
      
      /**
      * hasActiveRowId
      * @return {bool}
      **/
      hasActiveRowId: function() 
      { 
         if ( this._activeRowId ) { return true; }
      },
      
      /**
      * getActiveRowAttribute
      * @param {string} attribute name
      * @return {string} attribute value
      **/
      getActiveRowAttribute: function ( attribute ) {

         var activeRow = $( this._activeRowId );
         return activeRow.getAttribute( attribute );
      },
      
      /**
      * getSelectedRowsAttribute
      * @return {array}
      **/
      getSelectedRowsAttribute: function ( attribute ) {

         var selectedRowsAttrs = [];
         var selectedRows = this._selectedRows;
         //this.debug('XSLDataGrid.sel.length:'+selectedRows.length);
         for ( var i = 0, n = selectedRows.length; i < n; i++ ) {
            var row = $( selectedRows[i] );
            selectedRowsAttrs.push( row.getAttribute( attribute ) );
         }
         return selectedRowsAttrs;
      },
      
      /**
      * getRowIdByAttribute
      * Find a row by an attribute and value
      * @param {string} attribute name
      * @return {string} attribute value
      */
      getRowIdByAttribute: function( attribute, value ) {

         var rows = this.getRows();
         for ( var i=0, n=rows.length; i<n; i++ ) {
            var row = $(rows[i]);
            if ( row.getAttribute( attribute ) == value ) {
               return row.id;
            }
         }
         return false;
      },
      
      /**
      * Add a single row ID to the set.
      * @param {string} row_id row ID to add
      **/
      addSelectedRow: function( row_id ) 
      {
         this.debug( "XSLDataGrid.addSelectedRow row_id: '" + row_id + "'" );
         if ( !row_id ) { return; } // when clicking a link in a cell or something
         this._selectedRows.push( row_id );
      },
      
      /**
      * Is this row ID in the set?
      * @type bool
      * @param {string} row_id row ID to query
      **/
      isSelectedRow: function( row_id ) {
         var isSelectedRow = this._selectedRows.find( function( selected_row_id ) { return row_id == selected_row_id;  } );
         return isSelectedRow;
      },
      
      /**
      * Remove a all row IDs from the set and reset their CSS className.
      **/
      clearSelectedRows: function() {

         var rows = this._selectedRows;
         var i = rows.length - 1;
         this.debug( "XSLDataGrid.clearSelectedRows rows.length:" + i );
         if ( rows.length ) {
            do {
               $( rows[i] ).removeClassName( 'active' );
               $( rows[i] ).removeClassName( 'selected' );
            }
            while (i--);
         }
         this._selectedRows = [];
      },
      
      /**
      * When someone mousedowns on the sort col text, set col clicked 
      * @param {obj} event
      **/
      sortMouseDown: function( e ) {

         var element = Event.element( e );
         var col = element.id.replace( 'xdgHeaderLabelText_' + this._container_id + '_', '' );
         this.debug("XSLDataGrid.sortMouseDown: " + col ); 
         this._sortClickedCol = col; 
      },
      
      /**
      * When someone mouseups on the sort col header, test for right or left click
      * @param {MouseEvent} e
      **/
      sortMouseUp: function( e ) 
      {
         var element = Event.element( e );
         var label = element.innerHTML;
         var col_id = element.parentNode.parentNode.getAttribute( 'col_id' );
         var order = $( 'xdgHeaderSortIcon_' + this._container_id + '_' + col_id ).hasClassName( 'xdgHeaderSortIcon_ascending' ) ? "descending" : "ascending";
         
         this.debug("XSLDataGrid.sortMouseUp col_id:" + col_id + ", order: " + order + ", left: " + Event.isLeftClick( e ) + ", this.cid:" + this._container_id ); 
         
         
         // make sure they really want to sort THIS column
         // i.e. they clicked and let up on the same col_id anchor
         if ( this._sortClickedCol != col_id ) { return; }
         this._sortClickedCol = false; // reset
         
         // Left-click == sorting
         if ( Event.isLeftClick( e ) ) {
            this.sort( col_id, order );
         }
         
         return false;
      },
      
      /**
      * Open the column-oriented context menu
      * @param {MouseEvent} e
      * @param {string} col_id
      * @param {string} col_label
      */
      openColContextMenu: function( e, col_id, col_label ) {

         this.debug( "XSLDataGrid.openColContextMenu col_id: " + col_id + ", col_label: " + col_label );
         
         // init
         var menuItem, imgNode, labelNode;
         
         // start off afresh
         this.clearColContextMenuTimer();
         
         // get the holder div and reset, turn on, and position
         var x = Event.pointerX( e ) - 2;
         var y = Event.pointerY( e ) - 2;
         
         var xdgColContextMenu = $( this.options.xdgPopupDivId );
         xdgColContextMenu.innerHTML = '';
         xdgColContextMenu.className = 'xdgColContextMenu';
         xdgColContextMenu.show();
         xdgColContextMenu.style.left = x + 'px';
         xdgColContextMenu.style.top = y + 'px';
         xdgColContextMenu.onmouseover = this.clearColContextMenuTimer.bind( this );
         xdgColContextMenu.onmouseout = this.startColContextMenuTimer.bind( this );
         
         var xdgColContextMenuList = xdgColContextMenu.appendChild( document.createElement( 'div' ) );
         xdgColContextMenuList.className = 'xdgColContextMenuList';
         xdgColContextMenuList.onmouseover = this.clearColContextMenuTimer.bind( this );
         xdgColContextMenuList.onmouseout = this.startColContextMenuTimer.bind( this );
         
         // Sort & Group By
         if ( $( 'xdgHeaderLabelText_' + this._container_id + '_' + col_id ).className.match( 'sortable' ) ) {
            
            // sort ASC
            menuItem = $( 'xdgColContextMenuItemTemplate' ).cloneNode( true );
            menuItem.id = "";
            menuItem.show();
            
            imgNode = menuItem.firstChild;
            $( imgNode ).addClassName( 'xdgHeaderSortIcon_ascending' );
            labelNode = imgNode.nextSibling;
            
            menuItem.datagrid = this; // store a reference
            menuItem.col_id = col_id; // store a reference
            menuItem.onclick = function() {
               this.datagrid.deactivateColContextMenu();
               this.datagrid.sort( this.col_id, 'ascending' );
            }
            labelNode.innerHTML = "Sort by " + col_label + " Ascending";
            
            // add to DOM
            xdgColContextMenuList.appendChild( menuItem );
            
            // sort DESC
            menuItem = $( 'xdgColContextMenuItemTemplate' ).cloneNode( true );
            menuItem.id = "";
            menuItem.show();
            
            imgNode = menuItem.firstChild;
            $( imgNode ).addClassName( 'xdgHeaderSortIcon_descending' );
            labelNode = imgNode.nextSibling;
            
            menuItem.datagrid = this; // store a reference
            menuItem.col_id = col_id; // store a reference
            menuItem.onclick = function() {
               this.datagrid.deactivateColContextMenu();
               this.datagrid.sort( this.col_id, 'descending' );
            }
            labelNode.innerHTML = "Sort by " + col_label + " Descending";
            
            // add to DOM
            xdgColContextMenuList.appendChild( menuItem );
            
            // group by
            menuItem = $( 'xdgColContextMenuItemTemplate' ).cloneNode( true );
            menuItem.id = "";
            menuItem.show();
            
            imgNode = menuItem.firstChild;
            $( imgNode ).addClassName( 'groupByColumnIcon' );
            labelNode = imgNode.nextSibling;
            
            menuItem.datagrid = this; // store a reference
            menuItem.col_id = col_id; // store a reference
            
            if ( this._group == col_id ) {
               menuItem.onclick = function() {
                  this.datagrid.deactivateColContextMenu();
                  this.datagrid.group( null );
               }
               labelNode.innerHTML = "Ungroup by " + col_label;
            }
            else {
               menuItem.onclick = function() 
               {
                  this.datagrid.deactivateColContextMenu();
                  this.datagrid.group( this.col_id );
               }
               labelNode.innerHTML = "Group by " + col_label;
            }
            
            // add to DOM
            xdgColContextMenuList.appendChild( menuItem );
            
            // offer an ungroup by to all others
            if ( this._group && this._group != col_id ) {
               menuItem = $( 'xdgColContextMenuItemTemplate' ).cloneNode( true );
               menuItem.id = "";
               menuItem.show();
               
               imgNode = menuItem.firstChild;
               $( imgNode ).addClassName( 'ungroupByColumnIcon' );
               labelNode = imgNode.nextSibling;
               
               menuItem.datagrid = this; // store a reference
               menuItem.col_id = col_id; // store a reference
               
               menuItem.onclick = function() 
               {
                  this.datagrid.deactivateColContextMenu();
                  this.datagrid.group( null );
               }
               labelNode.innerHTML = "Ungroup";
               
               
               // add to DOM
               xdgColContextMenuList.appendChild( menuItem );
            }
         }
         
         // Remove Column only works via AJAX
         if ( this._sortMechanism == 'server' && $( 'xdgHeaderCell_' + this._container_id + '_' + col_id ).hasClassName( 'rearrangeable' ) ) {
            menuItem = $( 'xdgColContextMenuItemTemplate' ).cloneNode( true );
            menuItem.id = "";
            menuItem.show();
            
            imgNode = menuItem.firstChild;
            $( imgNode ).addClassName( 'removeColumnIcon' );
            labelNode = imgNode.nextSibling;
            labelNode.innerHTML = "Remove " + col_label;
            
            menuItem.datagrid = this; // store a reference
            menuItem.col_id = col_id; // store a reference
            menuItem.col_label = col_label; // store a reference
            menuItem.onclick = function() 
            {
               this.datagrid.deactivateColContextMenu();
               this.datagrid.removeColumn( this.col_id, this.col_label );
            }
            
            // add to DOM
            xdgColContextMenuList.appendChild( menuItem );
         }
         
         // Quicksearch
         if ( $( 'xdgHeaderCell_' + this._container_id + '_' + col_id ).className.match( 'filterable' ) ) {
            menuItem = $( 'xdgColContextMenuItemFilterTemplate' ).cloneNode( true );
            menuItem.id = "";
            menuItem.show();
            
            var filterInput = menuItem.firstChild.nextSibling;
            // already set?
            if ( this._colContextMenuFilters && this._colContextMenuFilters[col_id] ) {
               filterInput.value = this._colContextMenuFilters[col_id];
            }
            
            filterInput.col_id = col_id; // store a reference
            
            Event.observe( filterInput, "blur", this.onFilterBlur.bindAsEventListener( this ) );
            Event.observe( filterInput, "keyup", this.onFilterKeyUp.bindAsEventListener( this ) );
            
            // add to DOM
            xdgColContextMenuList.appendChild( menuItem );
         }
         
         
         
         // If the menu is hanging off the screen, move it back in to the left
         Position.prepare();
         var pos = Position.page( this.xdgContainer );
         var xtotal = pos[0] + Position.deltaX + this.xdgContainer.offsetWidth;
         // and add 15 for padding
         var xtest = parseInt( xdgColContextMenu.style.left ) + xdgColContextMenuList.offsetWidth + 15;
         var diff = xtest - xtotal;
         if ( diff > 0 ) {
            xdgColContextMenu.style.left = parseInt( xdgColContextMenu.style.left ) - diff + "px";
         }
      },
      
      /**
      * deactivateColContextMenu
      */
      deactivateColContextMenu: function() 
      {
         $( this.options.xdgPopupDivId ).style.display = "none";
      },
      
      onFilterBlur: function( e ) {

         this.debug("XSLDataGrid.onFilterBlur");
      },
      
      onFilterKeyUp: function( e ) {

         this.debug("XSLDataGrid.onFilterKeyUp el:" + Event.element( e ) + ", val: " + Event.element( e ).value );
         
         // hold off on closing the context menu
         this.clearColContextMenuTimer();
         
         if ( this.filterObserver ) {
            window.clearTimeout( this.filterObserver );
         }
         this.filterObserver = window.setTimeout( this.filterRowsByCol.bind( this, Event.element( e ).col_id, Event.element( e ).value ), 1000 );
      },
      
      /**
      * Filter xdgDataTable rows 
      * param {string} col 
      * param {string} value to search for in col
      */
      filterRowsByCol: function( col, value ) {

         this.debug("XSLDataGrid.filterRowsByCol col:" + col + ", value: " + value );
         
         // hold off on closing the context menu
         this.clearColContextMenuTimer();
         
         // remember the value
         this._colContextMenuFilters[col] = value;
         
         // get the colIndex
         var colIndex = this.getDataColIndex( col );
         
         // lose the table in DOM for speed
         this.removeDataTableFromDOM();
         
         // loop through the data rows and turn on / off display
         var reg = new RegExp( value, "i" );
         var rows = this.xdgDataTable.rows;
         var i = rows.length-1;
         do {
            var row = rows[i];
            if ( value === '' || row.cells[colIndex].innerHTML.match( reg ) ) {
               row.style.display = "";	
            }
            else {
               row.style.display = "none";
            }
         }
         while (i--);
         
         // put it back
         this.restoreDataTableToDOM();
         
         this.resize();
         this.startColContextMenuTimer();
      },
      
      /**
      * Called by menu's mouseout handler.
      **/
      startColContextMenuTimer: function() {

         this.clearColContextMenuTimer();
         $( this.options.xdgPopupDivId )._hideTimer = window.setTimeout( this.deactivateColContextMenu.bind( this ), this.options.hideColContextMenuDelay );
      },
      
      /**
      * Called by menu's mouseover handler.
      **/
      clearColContextMenuTimer: function() {

         if ( $( this.options.xdgPopupDivId )._hideTimer ) {
            $( this.options.xdgPopupDivId )._hideTimer = window.clearTimeout( $( this.options.xdgPopupDivId )._hideTimer );
         }
      },
      
      /**
      * Turn on/off visibility of draggable div
      */
      toggleColsResizableVisibility: function( bool ) {

         var visibility = bool ? "visible" : "hidden";
         Element.childrenWithClassName( this.xdgHeaderRow, 'xdgColResizer' ).each( function(remover) { 
               remover.style.visibility = visibility; 
         });
         
         return true;
      },
      
      /**
      * Turn on/off Scriptaculous header draggability for reordering
      * @param bool
      */
      toggleColsDraggable: function( bool ) {

         this.debug( "XSLDataGrid.toggleColsDraggable bool: " + bool );
         if ( bool ) {
            Sortable.create( this.xdgHeaderRow, { tag: 'th', constraint: 'horizontal', handle: 'xdgHeaderCell', only: 'rearrangeable', ghosting: false, scroll: this.xdgSuperContainer.id, overlap: 'horizontal', ignoreHeight: true } );
            this.sortableObserver = new SortableObserver( this.xdgHeaderRow, this);
            
            // inline functions from observer to datagrid
            this.sortableObserver.datagrid = this; // store a reference
            
            this.sortableObserver.onEnd = function( eventName, draggable, event ) {
               // we only want to run this once, and Draggables notify all observers
               // ie once you have multiple grids on the same page
               var container_id = this.element.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.id;
               
               if ( draggable.element.id.match( container_id ) ) {
                  this.datagrid.onColDragEnd( eventName, draggable, event );
                  
                  // rekill the draggable altogether
                  this.datagrid.toggleColsDraggable( false );
               }
            }
            Draggables.addObserver( this.sortableObserver );
         }
         else { 
            Sortable.destroy( this.xdgHeaderRow );
            Draggables.removeObserver( this.sortableObserver );
            this.sortableObserver = {};
            
            // undoPositioned on HeaderCells
            $A( this.xdgHeaderRow.cells ).each( function( th ) {
                  $( th ).undoPositioned();      
            });
         }
      },
      
      /**
      * Callback from Scriptaculous when draggable has stopped
      * @param {string} eventName
      * @param {obj} draggable
      * @param {obj} event
      */	
      onColDragEnd: function( eventName, draggable, event ) {

         
         // get the col_id string
         var col_id = draggable.element.getAttribute( 'col_id' );
         
         // default
         var reload = false;
         
         // Figure out the new position
         var insertBeforeCol;
         var colStates = this.getColStates();
         for (var i=0, n=colStates.length, lastTest=colStates.length-1; i<n; i++) {
            if ( col_id == colStates[i].split( '|' )[0] ) {
               if ( i == lastTest ) {
                  insertBeforeCol = "LAST";
               }
               else { 
                  insertBeforeCol = colStates[i+1].split( '|' )[0];
               }
               break;
            }
         }
         var colIndex = this.getDataColIndex( col_id );
         var insertBeforeColIndex = this.getDataColIndex( insertBeforeCol );
         
         this.debug( "XSLDataGrid.onColDragEnd col_id: " + col_id + ", colIndex: " + colIndex + ", insertBeforeCol: " + insertBeforeCol + ", insertBeforeColIndex:" + insertBeforeColIndex ); 
         
         // if we didn't move anything, well, then we can stop
         if ( colIndex == ( insertBeforeColIndex - 1 ) ) { return; }
         
         // If we have data rows that are affected
         if ( this.xdgDataTable ) {
            
            // if not many rows, we can update prefs and just use DOM for visual
            var rowlength = this.xdgDataTable.rows.length;
            if ( rowlength < this.options.rowReloadLimitOnRearrange ) {
               
               // spinner
               this.buildLoadingAnimation();
               
               // Move the column - we'll turn off the animation in there
               this.moveColumnTo( col_id, colIndex, insertBeforeCol, insertBeforeColIndex );
            }
            
            // update prefs, and reload the whole grid - too much data to reorg in DOM
            else {
               reload = true;
            }
         }
         
         // now update prefs and reload only for the long data situation
         colStates = this.getColStates();
         this.updateColStatePrefs( colStates, reload );
      },
      
      
      /**
      * Take care of swapping order of the columns in DOM for the COLGROUP in xdgDataTable
      * as well as in the xdgDataTable.rows as well
      * @param int colIndex1 from index
      * @param int colIndex2 to index
      */
      moveColumnTo: function( col, colIndex, insertBeforeCol, insertBeforeColIndex ) {

         
         this.debug('XSLDataGrid.moveColumnTo col:' + col + '('+colIndex+'), to before:' + insertBeforeCol + '('+insertBeforeColIndex+') for container_id:' + this._container_id );
         
         // Fix the xdgDataTable COLGROUP first
         // looks like we can't indes into the childNodes with COLGROUP
         this.xdgDataColgroup.insertBefore( $( 'xdgDataCol_' + this._container_id + '_' + col ), $( 'xdgDataCol_' + this._container_id + '_' + insertBeforeCol ) );
         
         // lose the table in DOM for speed
         this.removeDataTableFromDOM();
         
         // Fix the xdgDataTable rows (that aren't groupers)
         var i = this.xdgDataTable.rows.length-1;
         var rows = this.xdgDataTable.rows;
         do {
            var row = $( rows[i] );
            if ( !row.className.match( 'xdgGroupedSet' ) ) {
               row.insertBefore( row.cells[colIndex], row.cells[insertBeforeColIndex] );
            }
         }
         while (i--);
         
         // now back in since we're done
         this.restoreDataTableToDOM();
         
         // set an interval to fix the Dual-DOM
         this.moveDualDomInterval = window.setTimeout( this.moveDualDomColumnTo.bind( this, colIndex, insertBeforeColIndex ) );
         
         // stop the loading animation in here after we've moved the column
         this.stopLoadingAnimation();
      },
      
      /**
      * Take care of swapping order of the columns in DOM for the COLGROUP in xdgDataTable
      * as well as in the xdgDataTable.rows as well
      * @param {int} colIndex from index
      * @param {int} insertBeforeColIndex to index
      */
      moveDualDomColumnTo: function( colIndex, insertBeforeColIndex ) {
         
         this.debug( "XSLDataGrid.moveDualDomColumnTo colIndex: " + colIndex + ", insertBeforeColIndex: " + insertBeforeColIndex );
         
         // clear interval
         this.moveDualDomInterval = window.clearTimeout( this.moveDualDomInterval ); 
         
         // Fix the rows
         // What's kinda cool is that IE has no extra text nodes, and firefox supports table
         // DOM in the XML DOM, so we can do this easily, though differently
         var rows = this._XMLDOMDoc.selectNodes( "//tr" );
         var i = rows.length - 1;
         
         // the Firefox way
         if ( rows[0].cells ) {
            do {
               rows[i].insertBefore( rows[i].cells[colIndex], rows[i].cells[insertBeforeColIndex] );
            }
            while (i--);
         }
         // IE
         else {
            do {
               rows[i].insertBefore( rows[i].childNodes[colIndex], rows[i].childNodes[insertBeforeColIndex] );
            }
            while (i--);
         }
      },
      
      /**
      * Called when user mousedowns on the column resizer
      * @param {MouseEvent} e event object
      */
      startColResize: function( e ) {
         
         var element = Event.element( e );
         var col = element.id.replace( 'xdgColResizer_' + this._container_id + '_', '' );
         this.debug("XSLDataGrid.startColResize col: " + col ); 
         
         // start watching the mouse and set current position
         Event.observe( document, "mouseup", this._eventDocumentMouseUp );
         Event.observe( document, "mousemove", this._eventDocumentMouseMove );
         
         // store start position
         this._dragStartPos = Event.pointerX(e);
         
         // store some DOM in memory since we have container_id and col here
         this._resizeGridHeaderCell = $( 'xdgHeaderCell_' + this._container_id + '_' + col );
         this._resizeGridDataCol = $( 'xdgDataCol_' + this._container_id + '_' + col );
         this._newColWidth = this._origColWidth = parseInt( this._resizeGridHeaderCell.style.width );
         
         // calc a minwidth based on field header label div which includes potential sort icon +10forfun
         this._resizeMinWidth = $( 'xdgHeaderLabelText_' + this._container_id + '_' + col ).offsetWidth + $( 'xdgHeaderSortIcon_' + this._container_id + '_' + col ).offsetWidth + 10;
         
         // turn on resize guides 
         if ( this.xdgDataContainer ) {
            // give them height
            var h = this.xdgDataContainer.offsetHeight + this._resizeGridHeaderCell.offsetHeight;
            this.xdgResizeGuide.show();
            this.xdgResizeGuide.style.height = h + "px";
            
            // position them
            Position.prepare();
            var pos = Position.page( this._resizeGridHeaderCell );
            var x = pos[0] + Position.deltaX + 1; // +1 for border
            var y = pos[1] + Position.deltaY; 
            this.xdgResizeGuide.style.left = x + 'px';
            this.xdgResizeGuide.style.top = y + 'px';
            this.xdgResizeGuide.style.width = this._resizeGridHeaderCell.offsetWidth + 'px';
         }
         
         // disable LAST cell while dragging
         var gridLastHeaderCellWidth = parseInt( this.gridLastHeaderCell.style.width );
         if ( gridLastHeaderCellWidth > 0 ) {
            this.gridLastHeaderCell.style.width = "0px";
            this.xdgHeaderTable.style.width = parseInt( this.xdgHeaderTable.style.width ) - gridLastHeaderCellWidth + 'px';
            
         }
         
         // make it clear to user that this col box grows/shrinks
         this._resizeGridHeaderCell.addClassName( 'xdgCellResizing' );
         
         return false;
      },
      
      
      /**
      * Called on mousedrag to resize the column and perhaps its data
      * @param e event object
      */
      colResizing: function( e ) {
         
         // calc new size
         var dragEndPos = Event.pointerX( e );
         var dragDiff = parseInt(dragEndPos - this._dragStartPos);
         
         // compare orig width to dragdiff to see if we should set stuff
         var newColWidth = dragDiff + this._newColWidth;
         
         // test against headerLabel text size
         if (newColWidth >= this._resizeMinWidth) {
            
            // set em here for endColResizing
            this._newColWidth = newColWidth;
            this._dragEndPos = dragEndPos;
            
            // resize data stuff and move the right guide
            if ( this.xdgDataContainer ) {
               this.xdgResizeGuide.style.width = dragDiff + parseInt( this.xdgResizeGuide.style.width ) + 'px';
            }
            // show the resize in the header in real time if no data table
            else {
               this.resizeColHeader();   
            }
            
            // store this position
            this._dragStartPos = this._dragEndPos;
            
         }
         return false;
      },
      
      /**
      * This way we could call this function optionally
      */
      resizeColHeader: function() {
         
         // resize the header rowcell
         this._resizeGridHeaderCell.style.width = this._newColWidth + "px";
         
         // subtract size of resizer+1=5 - for some reason this keeps the resizer from dropping out in IE
         var newInnerWidth = this._newColWidth-5;
         
         // calc new width
         this.newContainerWidth = ( this._newColWidth - this._origColWidth ) + parseInt( this.xdgContainer.getAttribute( 'containerwidth' ) );
         
         // and set the attribute for future window resizes
         this.xdgContainer.setAttribute( 'containerwidth', this.newContainerWidth );
         
         // resize grid container
         this.xdgContainer.style.width = this.newContainerWidth + "px";
         
         // reset the header container and its table
         this.xdgHeaderTable.style.width = this.newContainerWidth + "px";
      },
      
      /**
      * When the user mouseups after starting a column resize
      * also makes a call to update preferences
      * @param e event object
      * @return {bool} false
      */
      stopColResize: function( e ) {
         this.debug( "XSLDataGrid.stopColResize: col: " + this._resizeGridHeaderCell.getAttribute( 'col_id' ) + ", newColWidth: " + this._newColWidth );
         
         // stop observing the mouse
         Event.stopObserving( document, "mouseup", this._eventDocumentMouseUp );
         Event.stopObserving( document, "mousemove", this._eventDocumentMouseMove );
         
         // resize the column header
         this.resizeColHeader();
         
         
         // dataContainer
         if ( this.xdgDataContainer ) {
            this.xdgResizeGuide.hide();
            
            // only change widths if we truly dragged
            if ( this._dragEndPos ) {
               
               // lose the table in DOM for speed
               this.removeDataTableFromDOM();
               
               this.gridLastDataCol.style.width = "0px";
               // we need to change the value of the style attribute too for any
               // clientside sort group to be aware 
               this._resizeGridDataCol.setAttribute( 'style', 'width:' + this._newColWidth + "px;" );
               this._resizeGridDataCol.style.width = this._newColWidth + "px";
               
               // set in the Dual-DOM
               this._XMLDOMDoc.selectSingleNode( "/div/table/thead/tr/th[@id='" + this._resizeGridHeaderCell.getAttribute( 'col_id' ) + "']" ).setAttribute( 'width', this._newColWidth );
               
               this.xdgDataContainer.style.width = this.newContainerWidth + "px";
               this.xdgDataTable.style.width = this.newContainerWidth + "px";
               
               // put it back
               this.restoreDataTableToDOM();
            }
         }
         
         // style back
         this._resizeGridHeaderCell.removeClassName( 'xdgCellResizing' );
         
         // update colstate prefs
         this.updateColStatePrefs( this.getColStates() );
         
         // resize the left column to fix when 
         // container width ends smaller than super width
         this.resize();
         
         return false;
      },
      
      /**
      * Roll up the col order and sizing from the Grid
      * @return {array} colStates
      */
      getColStates: function() {

         var cn = this.xdgHeaderRow.cells;
         var colStates = [];
         for (var i=0, n=cn.length; i<n; ++i) {
            var colname = cn[i].id.replace( 'xdgHeaderCell_' + this._container_id + '_', '' );
            
            // ignore last spacer
            if ( colname != "LAST" ) {
               var colwidth = parseInt( cn[i].style.width );
               colStates.push( colname + '|' + colwidth );
            }
         }
         return colStates;
      },
      
      /**
      * Returns an array of col indexes ( i.e. names, whatever ) {
array} colIndexes
      */
      getColIndexes: function() {

         var cn = this.xdgHeaderRow.cells;
         var colIndexes = [];
         for (var i=0, n=cn.length; i<n; ++i) {
            var colkey = cn[i].id.replace( 'xdgHeaderCell_' + this._container_id + '_', '' );
            colIndexes.push( colkey );
         }
         return colIndexes;
      },
      
      /**
      * Returns an array of col indexes ( i.e. names, whatever ) {
int}
      */
      getColIndex: function( col ) {

         var colIndexes = this.getColIndexes();
         for (var i=0, n=colIndexes.length; i<n; ++i) {
            if ( colIndexes[i] == col ) {
               return i;
            }
         }
      },
      
      /**
      * Returns an array of col data indexes ( i.e. names, whatever ) {
array} colIndexes
      */
      getDataColIndexes: function() {

         var cn = $( 'xdgDataColgroup_' + this._container_id ).getElementsByTagName( 'col' );
         var colIndexes = [];
         for (var i=0, n=cn.length; i<n; ++i) {
            var colkey = cn[i].id.replace( 'xdgDataCol_' + this._container_id + '_', '' );
            colIndexes.push( colkey );
         }
         return colIndexes;
      },
      
      /**
      * Returns an array of col indexes ( i.e. names, whatever )
      */
      getDataColIndex: function( col ) {

         var colIndexes = this.getDataColIndexes();
         for (var i=0, n=colIndexes.length; i<n; ++i) {
            if ( colIndexes[i] == col ) {
               return i;
            }
         }
      },
      
      
      /**
      * Send an update with colStates set to blank string
      */
      resetColumnDefaults: function() {

         this.updateColStatePrefs( null, true );
      },
      
      /**
      * Add a column
      * @param {string} foramtted like field|fieldlabel|fieldwidth from a select most likely
      */
      addColumn: function( data ) {

         if (!data) { return; }
         var goods = data.split('|');
         var field = goods[0];
         var fieldlabel = goods[1];
         var fieldwidth = parseInt(goods[2]);
         
         this.buildLoadingAnimation();
         
         var colStates = this.getColStates();
         colStates.push( field + '|' + fieldwidth );
         //this.debug("XSLDataGrid.addColumn colstates:" + colStates.join(",")); return;
         
         // update and reload
         this.updateColStatePrefs( colStates, true );
         
      },
      
      /**
      * Remove a column from the grid
      * @param {string} col
      * @param {string} label
      */
      removeColumn: function( col, label ) {

         this.debug( "XSLDataGrid.removeColumn col: " + col + ", label: " + label );
         var confirmtest = window.confirm( "Remove the column " + label + "?" );
         if ( confirmtest ) {
            // proceed - get colStates and then wipe
            var colStates = this.getColStates();
            this.buildLoadingAnimation();
            for (var i=0, n=colStates.length; i<n; i++) {
               var colinfo = colStates[i].split('|');
               if ( colinfo[0] == col ) {
                  colStates.splice( i, 1 );
                  break;
               }
            }
            
            // AJAX update
            this.updateColStatePrefs( colStates, true );
         }
         
      },
      
      /**
      * update colStates prefs
      * @param {array} colStates return from getColStates
      * @param {bool} reload afterwards
      */
      updateColStatePrefs: function( colStates, reload ) {

         // if no url then no ajax desired
         if ( !( this.pref_url || this.options.url ) ) { return; }
         
         // start workin it
         if ( reload ) {
            this.buildLoadingAnimation();
         }
         
         // if null, then we're resetting
         if ( !colStates ) {
            colStates = '';
         }
         // otherwise join the colStates array
         else {
            colStates = colStates.join( "," );
         }
         this.debug( "XSLDataGrid.updateColStatePrefs " + colStates + ", reload:" + reload );
         
         // AJAX
         Utility.AjaxRequest({
               url: this.pref_url ? this.pref_url : this.options.url,
               parameters: this.getParameters() + '&colstates=' + colStates,
               onComplete: this.onUpdateColStatePrefs.bind( this, reload )
         });
         
      },
      
      /**
      * onUpdateColStatePrefs - when Ajax.Request onCompletes
      * @param {bool} reload
      * @param {obj} transport
      * @param {obj} json results
      */
      onUpdateColStatePrefs: function( reload, transport, json ) {

         if ( reload ) { 
            this.buildLoadingAnimation();
            this.load();
         }
         this.notifyObservers( 'onUpdateColStates' );
      },
      
      /**
      * notifyObservers
      * #TODO
      */
      notifyObservers: function( eventName ) {
         if ( this._observers ) {
            if(o[eventName]) { o[eventName](); }
         } 
      },
      
      
      /*****************************************************************************
      BEGIN CLIENTSIDE-ONLY FUNCTIONS
      *****************************************************************************/
      
      /*
      * initClientSideXSLT
      * @param {bool} doXSLT after init
      */
      initClientSideXSLT: function( doXSLT ) 
      {
         this.debug( "XSLDataGrid.initClientSideXSLT" );
         
         // using Sarissa
         this._XSLTProcessor = new XSLTProcessor();
         this._XMLSerializer = new XMLSerializer();
         this._DOMParser = new DOMParser();
         
         // figure out the path to the xsl file
         var script = $A(document.getElementsByTagName("script")).find( function(s) {
               return (s.src && s.src.match(/XSLDataGrid\.js(\?.*)?$/));
         });
         var path = script.src.replace(/XSLDataGrid\.js(\?.*)?$/,'');
         
         
         // go get the xsl
         Utility.AjaxUpdater( '', {
               url: path + 'XSLDataGrid.xsl',
               parameters: '',
               onComplete: this.initClientSideXSLTonComplete.bind( this, doXSLT )
         });
      },
      
      /**
      * initClientSideXSLTonComplete
      * @param {bool} doXSLT
      * @param {obj} transport
      */
      initClientSideXSLTonComplete: function( doXSLT, transport ) {
         this.debug( "XSLDataGrid.initClientSideXSLT onComplete doXSLT: " + doXSLT );
         this._XSLDOM = transport.responseXML;
         
         try {
            this._XSLTProcessor.importStylesheet( this._XSLDOM );
         }
         catch( exception ) {
            var error_details = exception.error_details ? exception.error_details : exception.description ? exception.description : exception.message +", on line " + exception.lineNumber + " in file " + exception.fileName;
            
            alert( "XSLDataGrid fatal error in initClientSideXSLTonComplete. " + error_details );
            return;
         }
         this._XSLTProcessorReady = true;
         
         if ( doXSLT ) {
            this.doClientSideXSLT();
         }
      },
      
      /**
      * doClientSideXSLT
      *
      * @throws exception when creating XML DOM Doc fails 
      * @throws exception when XSLT fails
      */
      doClientSideXSLT: function() {
         this.debug( "XSLDataGrid.doClientSideXSLT XSLTProcessorReady: "  + this._XSLTProcessorReady );
         Utility.tick( 'XSLDataGrid.doClientSideXSLT' );
         
         // if we haven't finished initializing XSLT, then start an interval
         if ( !this._XSLTProcessorReady ) {
            window.setTimeout( this.doClientSideXSLT.bind( this ), 20 ); 
            return;
         }
         
         // make sure we have our XMLDOMDoc in memory
         this.getXMLDOMDoc();
         
         // do the XSLT and grab the html
         var xhtml = this.getXHTMLFromXSLT();
         
         // fill it in
         $( this._container_id ).innerHTML = xhtml;
         
         // run loaded from here
         this.loaded( true );
         
         // turn off loading
         this.stopLoadingAnimation();
         
         // resize for niceness
         this.resize();
         
         Utility.tock( 'XSLDataGrid.doClientSideXSLT' );
      },
      
      /**
      * getXMLDOMDoc
      * make sure we have this._XMLDOMDoc in memory
      * @throws exception 
      */
      getXMLDOMDoc: function() {
         this.debug( "XSLDataGrid.getXMLDOMDoc this._XMLDOMDoc: " + this._XMLDOMDoc );
         
         // Now, we want to make sure we have our dual DOM XML Doc of the semantic table
         if ( !this._XMLDOMDoc ) {
            
            // try this for Firefox and any who support it
            try {
               this._XMLDOMDoc = Sarissa.getDomDocument();
               var clone = this._XMLDOMDoc.importNode( $( this._container_id ), true );
               this._XMLDOMDoc.appendChild( clone );
            }
            // If that didn't work, we're probably in IE
            // IE won't allow importNode from MSHTML -> MSXML
            // We get a "Type mismatch" exception - alas HTML is not XML
            // So the first time we do this in IE it will be slow and scale poorly
            // We will have to walk down the HTML DOM tree and reconstruct a well-formed, paresable string
            catch ( exception ) {
               //alert("Exception here");
               var xhtml = '<div id="' + this._container_id + '">' + Utility.tagNamesToLowerCase( Utility.serializeInnerToString( $( this._container_id ) ) ) + '</div>';
               this._XMLDOMDoc = this._DOMParser.parseFromString( xhtml, "application/xhtml+xml" );
               
               // test for parse error
               if ( Sarissa.getParseErrorText( this._XMLDOMDoc ) != Sarissa.PARSED_OK ) {
                  alert( "XSLDataGrid fatal Sarissa parse error in doClientSideXSLT:" + Sarissa.getParseErrorText( this._XMLDOMDoc ) );
                  throw exception;
               }
            }
         }
      },
      
      
      /**
      * getXHTMLFromXSLT
      * do the XSLT step a little differently for Mozilla and IE
      * @returns html string
      */
      getXHTMLFromXSLT: function() {
         Utility.tick( 'XSLDataGrid.getXHTMLFromXSLT' );
         var xhtml;
         
         // for Mozillae
         if( document.implementation.createDocument ) {
            
            // get an html node fragment
            var fragment = this._XSLTProcessor.transformToFragment( this._XMLDOMDoc, document );
            
            // create a tmpDiv in memory to get at innerHTML
            var tmpDiv = document.createElement( 'div' );
            tmpDiv.appendChild( fragment );
            xhtml = tmpDiv.innerHTML;
            
         } 
         // for IE
         else {
            xhtml = this._XMLDOMDoc.transformNode( this._XSLTProcessor.template.stylesheet );
            xhtml = this.fixStringForIE( xhtml );
         }
         
         Utility.tock( 'XSLDataGrid.getXHTMLFromXSLT' );
         
         // return
         return xhtml;
      },
      
      /**
      * sortOrGroupInClient
      * change some simple properties in our Dual DOM and then call doClientSideXSLT 
      */
      sortOrGroupInClient: function() {
         
         // clear interval
         this._sortOrGroupInClientInterval = window.clearTimeout( this._sortOrGroupInClientInterval );
         
         this.debug( "XSLDataGrid.sortOrGroupInClient sort:" + this._sort + ", sort_previous: " + this._sort_previous + ", order: " + this._order + ", group: " + this._group );
         
         
         // remove old sort and set new one
         if ( this._sort_previous ) {
            var sortPreviousNode = this._XMLDOMDoc.selectSingleNode( "/div/table/thead/tr/th[@id='" + this._sort_previous + "']" );
            sortPreviousNode.setAttribute( 'class', sortPreviousNode.getAttribute( 'class' ).replace( 'sorted-' + this._order_previous, '' ) );
            sortPreviousNode.setAttribute( 'sort', '' );
         }
         if ( this._sort ) {
            var sortNode = this._XMLDOMDoc.selectSingleNode( "/div/table/thead/tr/th[@id='" + this._sort + "']" );
            sortNode.setAttribute( 'class', sortNode.getAttribute( 'class' ) + ' sorted-' + this._order );
            sortNode.setAttribute( 'sort', this._order );
            //this.debug( "sortNode: " + this._XMLSerializer.serializeToString( sortNode ) );
         }
         
         // remove old group and set new one
         if ( this._group_previous ) {
            var groupPreviousNode = this._XMLDOMDoc.selectSingleNode( "/div/table/thead/tr/th[@id='" + this._group_previous + "']" );
            groupPreviousNode.setAttribute( 'class', groupPreviousNode.getAttribute( 'class' ).replace( 'grouped', '' ) );
         }
         if ( this._group ) {
            var groupNode = this._XMLDOMDoc.selectSingleNode( "/div/table/thead/tr/th[@id='" + this._group + "']" );
            groupNode.setAttribute( 'class', groupNode.getAttribute( 'class' ) + ' grouped' );
         }
         
         // nicely remove the current data table from DOM
         this.removeDataTableFromDOM();
         
         // redo XSLT
         this.doClientSideXSLT();
         
      },
      
      /**
      * fixStringForIE
      * Removes some annoying attributes from a string
      * @param {string} xhtml
      */
      fixStringForIE: function( xhtml ) {
         $A( [ 'disabled="true"', 'disabled="false"' ] ).each( function( stringToReplace ) { 
               var r = new RegExp( stringToReplace, 'gi' );
               xhtml = xhtml.replace( r, '' );
         });
         return xhtml;
      },
      
      fireEvent: function( msg ) {
         //alert( "FIRE: " + msg );  
      }
      
});

// Add on custom EventPublisher
//Object.extend( XSLDataGrid.prototype, EventPublisher.prototype );

// EOF XSLDataGrid.js
