/**
* 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 = Class.create();
XSLDataGrid.prototype = {
   
   /**
   * initialize
   * @param {string} container_id to place the grid inside of
   * @param {object} options
   */
   initialize: function( container_id, options )
   {
      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 = [];
      
      // 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();
      }
      
      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 )
   {
      var element = Event.element( e );
      this.xdgDataContainer.scrollTop = element.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 Mozilla, sign of delta is different than in IE.
           * Also, 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 )
         this.xdgScroller.scrollTop = this.xdgDataContainer.scrollTop -= delta*20;
    
      /** 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;
      
      new 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();            
      
   },
   
   /**
   * 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 ) );
      }
      this.notifyObservers( 'onXSLDataGridSort' );
      this.onSort();
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onSort: function() 
   {},
   
  
   
   /**
   * Group the grid
   **/
   group: function( group )
   {
      this.debug( "XSLDataGrid.group group:" + group );
      
      // store previous value
      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 ) );
      }

      this.notifyObservers( 'onXSLDataGridGroup' );
      this.onGroup();
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onGroup: function() 
   {},
   
   /**
   * 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 element = Event.element( e );
      this.debug( "XSLDataGrid.dataTableMouseDown element:" + element.id + ", element.tagName: " + element.tagName );
      // parentNode is row
      var 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 );
      this.onRowMouseDown( row, e );
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onRowMouseDown: function( row, 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 )
   {
      this.onRowMouseUp( row, e );
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onRowMouseUp: 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 );
      }
      
      this.onRowLeftClick( row, e );
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onRowLeftClick: function( row, e ) {},
   
   
   /**
   * rowRightClick
   * Dispatcher method called when a right 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
   **/
   rowRightClick: function( row, e ) { 
      //this.debug( 'XSLDataGrid.rowRightClick' ); 
      
      this.onRowRightClick( row, e );
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onRowRightClick: function( row, e ) {},
   
   
   
   /**
   * rowDblClick
   * @param {HTMLElement} row the TR that received the event
   * @param {MouseEvent} e
   **/
   rowDblClick: function( row, e )
   {
      Event.stop( e );
      this.onRowDblClick( row, e );
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onRowDblClick: function( row, e ) {
      this.notifyObservers( 'onRowDblClick' );
   },
   
   
   
   /**
   * rowMouseOver
   * @param {HTMLElement} row the TR that received the event
   * @param {MouseEvent} e
   **/
   rowMouseOver: function( row, e )
   {
      Event.stop( e );
      this.onRowMouseOver( row, e );
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onRowMouseOver: function( row, e ) {},
   
   /**
   * rowMouseOut
   * @param {HTMLElement} row the TR that received the event
   * @param {MouseEvent} e
   **/
   rowMouseOut: function( row, e )
   {
      Event.stop( e );
      this.onRowMouseOut( row, e );
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onRowMouseOut: function( row, 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;
      
      // run the callback
      this.onActivateRow( row_id );
   },
   
   /**
   * Abstract method to be used by subclasses.
   **/
   onActivateRow: function( row_id ) {},
   
   /**
   * 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 );
      }
      this.onRangeSelectRow( row_id );
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onRangeSelectRow: function( row_id ) {},
   
   
   
   
   
   /**
   * selectRow
   * @param {string} row_id DOM id of the target row.
   **/
   selectRow: function( row_id )
   {
      $( row_id ).addClassName( 'selected' );
      this.addSelectedRow( row_id );
      this.onSelectRow( row_id );
   },
   /**
   * Abstract method to be used by subclasses.
   **/
   onSelectRow: function( 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";
      
   },
   
   /**
   * Hide the Column Context Menu
   */
   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', ignorePositionY: 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 )
   * @return {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 )
   * @return {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 )
   * @return {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 )
   * @return {int}
   */
   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
      new 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 )
         this._observers.each( function(o) {
           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
      new 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
         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 ) {
            
            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', groupPreviousNode.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;
   }
}
// End of XSLDataGrid.js
