Monday, November 22, 2010

[CLIENT GRIDVIEW II]:Matt Besereth : Creating a GridView with Resizable Column Headers

http://mattberseth.com/blog/2007/08/creating_a_gridview_with_resiz.html


Creating a GridView with Resizable Column Headers

I was recently reading Dan Wahlin's excellent 'How To' for creating a GridView extender control.  While I was going through his code samples, I thought it might be interesting to create another GridView extender that allows you to resize the column widths of the GridView by clicking and dragging the header cell borders.  Before I went down the path of actually implementing the extender control, I wanted to get a feel for what the client side code for handling this might look like.  So I quick created a sample application where I added this functionality to the Google Analytics GridView I blogged about in an earlier post.
** Because this sample is a proof of concept, I only tested in IE7.  If I move the code to an extender control I will go back and test it with other browsers. 
Live Demo | Download
Before I get into the implementation details, here is a quick screen shot of the grid.  Unfortunately the mouse cursor didn't come across in the screen shot, but when it is placed over the cell borders in the header row, it displays the east/west pointing arrow.

Adding this behavior to the existing GridView required implementing a handful of JavaScript functions.  My approach for implementing this features was to do the following:
  1. Handle each of the header cells's mousemove events to determine when the user has the cursor placed roughly over the cell's right hand border.  If a resize is already in processes then I use this event to determine what the new width of the column should be
  2. Handle each of the header cell's mousedown events to determine when the resizing begins
  3. Handle the document's mouseup event.  When this occurs I make sure that the resize has stopped
  4. Handle the document's selectstart event.  I cancel this event if a resize is currently in executing.  Doing this make's sure that the header cell's text isn't highlighted when I am resizing the cell.  I learned this trick from the AjaxControlToolkit's ResizeableControlExtender.  Here is a screen shot of what this would look like if you don't cancel this event:





pageLoad Event Handler

First, I added the pageLoad JavaScript function to my page.  The ASP.NET AJAX framework will call this method for you so you will not have to worry about wiring it up.  I use this method to add event handlers to the mousemove and mousedown events for the table's header cells.  Additionally I add handlers for the document level mouseup and selectstart events as follows ...
//  true when a header is currently being resized
var _isResizing;
//  a reference to the header column that is being resized
var _element;
//  an array of all of the tables header cells
var _ths;

function pageLoad(args){
    //  get all of the th elements from the gridview
    _ths = $get('gvCustomers').getElementsByTagName('TH');
    
    //  if the grid has at least one th element
    if(_ths.length > 1){
    
        for(i = 0; i < _ths.length; i++){
            //  determine the widths
            _ths[i].style.width = Sys.UI.DomElement.getBounds(_ths[i]).width + 'px';
        
            //  attach the mousemove and mousedown events
            if(i < _ths.length - 1){
                $addHandler(_ths[i], 'mousemove', _onMouseMove);
                $addHandler(_ths[i], 'mousedown', _onMouseDown);
            }
        }

        //  add a global mouseup handler            
        $addHandler(document, 'mouseup', _onMouseUp);
        //  add a global selectstart handler
        $addHandler(document, 'selectstart', _onSelectStart);
    }       
}

TH's mousemove Event

Next, I add logic to the mousemove event handler.  This event will fire everytime the mouse position changes while the cursor is over any of the header cells.  The code in this handler first checks to see if we are currently resizing a cell.  If we are, we do a little math to figure out what the new width of the column being resized needs to be.  If a resize is not currently taking place, then I check to see how close the mouse cursor is to the cells right hand border.  If it is within 2 pixels I display the east/west cursor so the user knows they can resize the cell.  Here is the JavaScript code for this handler ...
function _onMouseMove(args){    
    if(_isResizing){
        
        //  determine the new width of the header
        var bounds = Sys.UI.DomElement.getBounds(_element); 
        var width = args.clientX - bounds.x;
        
        //  we set the minimum width to 1 px, so make
        //  sure it is at least this before bothering to
        //  calculate the new width
        if(width > 1){
        
            //  get the next th element so we can adjust its size as well
            var nextColumn = _element.nextSibling;
            var nextColumnWidth;
            if(width < _toNumber(_element.style.width)){
                //  make the next column bigger
                nextColumnWidth = _toNumber(nextColumn.style.width) + _toNumber(_element.style.width) - width;
            }
            else if(width > _toNumber(_element.style.width)){
                //  make the next column smaller
                nextColumnWidth = _toNumber(nextColumn.style.width) - (width - _toNumber(_element.style.width));
            }   
            
            //  we also don't want to shrink this width to less than one pixel,
            //  so make sure of this before resizing ...
            if(nextColumnWidth > 1){
                _element.style.width = width + 'px';
                nextColumn.style.width = nextColumnWidth + 'px';
            }
        }
    }   
    else{
        //  get the bounds of the element.  If the mouse cursor is within
        //  2px of the border, display the e-cursor -> cursor:e-resize
        var bounds = Sys.UI.DomElement.getBounds(args.target);
        if(Math.abs((bounds.x + bounds.width) - (args.clientX)) <= 2) {
            args.target.style.cursor = 'e-resize';
        }  
        else{
            args.target.style.cursor = '';
        }          
    }         
}

TH's mousedown Event

The mousedown event handler if very simple.  If checks if the cursor is style is 'e-resize'.  If so it sets the _isResizing bit to true so the other handlers know that a resize is currently taking place.  It also grabs the header cell that is being resized.  Here is the code ...
function _onMouseDown(args){
    //  if the user clicks the mouse button while
    //  the cursor is in the resize position, it means
    //  they want to start resizing.  Set _isResizing to true
    //  and grab the th element that is being resized
    if(args.target.style.cursor == 'e-resize') {
        _isResizing = true;
        _element = args.target;               
    }                    
} 

document's mouseup Event

Next, I added code for the mouseup event.  This is a handler is setup at the document level so no matter where the user lets go of the button this logic will be executed.  This handler does 2 things: resets the _isResizing and _element values back to there unitialized state and resets the header cells cursor style back to its initial state.  Here is the code for this ...
function _onMouseUp(args){
    //  the user let go of the mouse - so
    //  they are done resizing the header.  Reset
    //  everything back
    if(_isResizing){
        
        //  set back to default values
        _isResizing = false;
        _element = null;
        
        //  make sure the cursor is set back to default
        for(i = 0; i < _ths.length; i++){   
            _ths[i].style.cursor = '';
        }
    }
} 

document's selectstart Event

Finally, the last handler.  Like I mentioned earlier, I added this one to prevent the header cell's text from highlighting as I am resizing the cells.  All this function does is cancel's the event if a resize is currently executing.  Here is the code for this ...
function _onSelectStart(args){
    // Don't allow selection during drag
    if(_isResizing){
        args.preventDefault();
        return false;
    }
}    
That's it. Enjoy!

No comments:

Post a Comment