Welcome to CrankyGoblin.Com Sign in | Join | Help

Public Class GeoffAppleby

Inherits Microsoft.VisualBasic.MVP : Implements IBrainFart
Dynamic Table Column Resizing in HTML

Update: I've rewritten this post, and placed it in the articles section of my blog. That's right - the update I've been promising for months has finally arrived! While the theory hasn't changed much, it's got a much better working example, and the javascript is a lot nicer now. So don't bother reading this page - move on to the updated one! It's here: Dynamic Table Column Resizing in IE.

In the HTML version of the PropertyGrid that I'm writing, I thought it would be cool if the columns could be resized, just like how you can on the real WinForms one. Plus, we figured we could reuse this bit later, as we're trying to emulate a fair bit of the windows explorer look and feel, and our listview should really be resizable too.

A few people have done this before, although documentation on it isn't that great. You can also buy components that can do it for you - and don't get me wrong, there's nothing wrong with buying third party software so long as they are high quality, and you shouldn't spend your life reinventing the wheel over and over and over again - but we wanted to figure it out for ourselves, jsut for hte challange and the fun of it.

So, as a result, I thought I'd post how our current implementation of it works. My boss is about to finish a two week holiday, and this was his at-home project :) If you can see any glaring errors, we'd certainly appreciate any advice - other than, hopefully someone finds it useful. Do remember, however, that this is most likely an IE only thing. I haven't tested it on anything else.

To start with, there's 3 main things that need to be kept track of:

  • Mouse Down - this is the start of the resize.
  • Mouse Move - for visible feedback on the resize, if the resize has started.
  • Mouse Up - to do the actual resizing.

You also need to remember between mouse down and mouse up what column is being resized, so there's a few global variables to keep track of it all. There's an extra event for niceness - a double click is detected and the column is resized to autofit.

Side note: Globals are evil. EVIL! I shoot anyone who uses them. However, I consider myself a real programmer, and as such, baby script kiddy stuff like javascript doesn't really count (as far as i'm concerned) as real programming, so all bets are off.

This technique is also dependant on what we're doing is focussing on a specific TD (or TH) tag. You resize this element, and give (or take) width from the next one along in the row. There's not really much else to say - read the source, it's commented, if you don't understand, ask me a question.

I can't post a live example here, i'm not allowed to include any script in a blog entry. But you should be able to copy it out and run it and see it working.

     1: <script type="text/javascript">
     2:     // Global constants to store elements that may be resized
     3:     var sActiveElementType = "TD";
     4:     var sVbarId = "vBar";
     5:     var sActiveTableId = "propertygridtable";
     6:     var sActiveCobtainerId = "propertygridbox";
     7:     var iResizeThreshold = 3;
     8:     var iEdgeThreshold = 10;
     9:     var iSizeThreshold = 20;
    10:     // Global variables to store position and distance moved
    11:     var oResizeTarget = null;
    12:     var iStartX = null;
    13:     var iEndX = null;
    14:     var iSizeX = null;
    15:  
    16:     // MouseMove event on resizable table. This checks if you are in an allowable 
    17:     //    'resize start' position. It also puts the vertical bar (visual feedback) 
    18:     //    directly under the mouse cursor. The vertical bar might NOT be currently 
    19:     // visible, that depnds on if you're resizing or not.
    20:     function trackCursor() {
    21:         // Change cursor and store cursor position for resize indicator on column
    22:         var oTarget = event.srcElement;
    23:         var oVbar = document.getElementById(sVbarId);
    24:  
    25:         oVbar.style.top = document.getElementById(sActiveCobtainerId).offsetTop;
    26:         oVbar.style.height = document.getElementById(sActiveCobtainerId).offsetHeight;
    27:         // We have to add the body.scrollLeft in case the table is wider than the view
    28:         // window where it is entriely within the screen this value should be zero...
    29:         oVbar.style.left = window.event.clientX + document.body.scrollLeft;
    30:  
    31:         if (
    32:                (oTarget.tagName.toUpperCase() == sActiveElementType) 
    33:             && (event.offsetX >= (oTarget.offsetWidth - iEdgeThreshold)) 
    34:             && selectColhead(oTarget.cellIndex).getAttribute("resizable")
    35:             ) {
    36:             oTarget.style.cursor = "e-resize";
    37:         } else {
    38:             oTarget.style.cursor = "";
    39:         }
    40:         // We don't want resizing to select any text elements...
    41:         if (oVbar.style.display == "inline") {
    42:             document.selection.empty();
    43:         }
    44:         return true;
    45:     }
    46:  
    47:     // Find cell at column iCellIndex in the first row of the table 
    48:     //        - needed because you can only resize a column from the first row.
    49:     //          by using this, we can resize from any cell in the table
    50:     function selectColhead(iCellIndex) {
    51:         var oTable = document.getElementById(sActiveTableId);
    52:         var oHeaderCell = oTable.rows(0).cells(iCellIndex);
    53:         return oHeaderCell;
    54:     }
    55:  
    56:     // MouseDown event. This fills the globals with tracking information, and 
    57:     //    displays the vertical bar. This is only done if you are allowed to start 
    58:     //    resizing.
    59:     function selectCol() {
    60:         // Record start point and show vertical bar resize indicator
    61:         var oTargetCell = event.srcElement;
    62:         var oHeaderCell = selectColhead(event.srcElement.cellIndex);
    63:         if(oTargetCell.style.cursor == "e-resize") {
    64:             iStartX = event.screenX;
    65:             oResizeTarget = oHeaderCell;
    66:             var oVbar = document.getElementById(sVbarId);
    67:             //if the cell isn't marked as 'resizable' viz a custom attribute
    68:             // then we won't allow this column to resize.
    69:             if (oResizeTarget.getAttribute("reSizable")) {
    70:                 oVbar.style.display = "inline";
    71:             }
    72:         }
    73:         return true;
    74:     }
    75:  
    76:     // MouseUp event. This finishes the resize.
    77:     function resizeCol() {
    78:         // Resize the column and its adjacent sibling if position and size 
    79:         // are within threshold values
    80:         var oAdjacentCell = null;
    81:         if (iStartX != null && oResizeTarget != null) {
    82:             iEndX = event.screenX;
    83:             iSizeX = iEndX - iStartX;
    84:             if ((oResizeTarget.offsetWidth + iSizeX) >= iSizeThreshold) {
    85:                 if (Math.abs(iSizeX) >= iResizeThreshold) {
    86:                     var iAdjCellOldWidth;
    87:                     if (oResizeTarget.nextSibling != null) {
    88:                         oAdjacentCell = oResizeTarget.nextSibling;
    89:                         iAdjCellOldWidth = (oAdjacentCell.offsetWidth);
    90:                     } else {
    91:                         oAdjacentCell = null;
    92:                     }
    93:                     var iResizeOldWidth = (oResizeTarget.offsetWidth);
    94:                     oResizeTarget.style.width = iResizeOldWidth + iSizeX;
    95:                     if (
    96:                            (oAdjacentCell != null) 
    97:                         && (oAdjacentCell.tagName.toUpperCase() == sActiveElementType)
    98:                         ) {
    99:                         oAdjacentCell.style.width = (
   100:                                 ((iAdjCellOldWidth - iSizeX) >= iSizeThreshold) ? 
   101:                                 (iAdjCellOldWidth - iSizeX) : 
   102:                                 (oAdjacentCell.style.width = iSizeThreshold)
   103:                             )
   104:                     }
   105:                 }
   106:             } else {
   107:                 oResizeTarget.style.width = iSizeThreshold;
   108:             }
   109:         }
   110:         return true;
   111:     }
   112:  
   113:     // DoubleClick event. This pushes the current column out to a good size.
   114:     function setMaxWidth(){
   115:         // Try to emulate the column double-click behaviour from explorer
   116:         // this is not complete and only approximates the correct behaviour!!!
   117:         var oHeaderCell = selectColhead(event.srcElement.cellIndex)
   118:         if (oHeaderCell.tagName.toUpperCase() == sActiveElementType) {
   119:             var oAdjacentCell = (oHeaderCell.nextSibling != null) ? 
   120:                                     oHeaderCell.nextSibling : null;
   121:             var iOldWidth = null;
   122:             var iNewWidth = null;
   123:             var iAdjWidth = null;
   124:             var iWidthDiff = null;
   125:             var oTable = document.getElementById(sActiveTableId);
   126:             iOldWidth = oHeaderCell.offsetWidth;
   127:             iAdjWidth = oAdjacentCell !=null?oAdjacentCell.offsetWidth:0;
   128:             oTable.style.tableLayout = "auto";
   129:             iNewWidth = oHeaderCell.clientWidth;
   130:             iWidthDiff = iNewWidth - iOldWidth;
   131:             oTable.style.tableLayout = "fixed";
   132:             oHeaderCell.style.width = (iOldWidth + iWidthDiff) <= iSizeThreshold ?
   133:                                         iSizeThreshold : (iOldWidth + iWidthDiff);
   134:             if (oAdjacentCell != null) {
   135:                 oAdjacentCell.style.width = (iAdjWidth - iWidthDiff) <= iSizeThreshold ?
   136:                                             iSizeThreshold : (iAdjWidth - iWidthDiff);
   137:             }
   138:             // We don't want this particular double-click to select any header text elements...
   139:             document.selection.empty();
   140:         }
   141:         return true;
   142:     }
   143:  
   144:     // BODY MouseUp event - clears out the tracking information
   145:     //    if we're not resizing.
   146:     function cleanUp() {
   147:         // Void the Global variables and hide the vertical bar
   148:         var oVbar = document.getElementById(sVbarId);
   149:         oVbar.style.display = "none";
   150:         iEndX = null;
   151:         iSizeX = null;
   152:         iStartX = null;
   153:         oResizeTarget = null;
   154:         oAdjacentCell = null;
   155:         return true;
   156:     }
   157: </script>
   158: <BODY onMouseUp="cleanUp();">
   159:     <span id="vBar" class="vBar"></span>
   160:     <div id="container">
   161:         <table id="rTable" border="0" cellpadding="1" cellspacing="0" 
   162:             style="width: 100%; border-collapse: collapse; table-layout: fixed;" 
   163:             onMouseMove="trackCursor();" 
   164:             onMouseUp="resizeCol();" 
   165:             onMouseDown="selectCol();" 
   166:             onDblClick="setMaxWidth();">
   167:             <tr>
   168:                 <td>Heading 1</td>
   169:                 <td reSizable="true">Heading 2</td>
   170:                 <td reSizable="true">Heading 3</td>
   171:                 <td>Heading 4</td>
   172:             </tr>
   173:             <tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>
   174:             <tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>    
   175:             <tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>
   176:             <tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>
   177:             <tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>
   178:             <tr><td>1</td><td>2</td><td>3</td><td>4</td></tr>
   179:         </table>
   180:     </div>
   181: </body>

So once you have the script, there's not a heap to do in your HTML. There's a few things in there that could possibly done nicer, but it works, and it works fairly well. But I'd appreciate to know what you all think :)

Posted: Saturday, October 30, 2004 7:48 PM by Geoff Appleby

Comments

Scott Galloway said:

Great technique...but can I put in a vote against line numbers in posted code snippets - pretty but they make it a pain to cut 'n paste...
# October 30, 2004 4:13 PM

Geoff Appleby said:

Sorry Scott,
Yeah, i thought about this just after i posted.
It's saturday night and a few too many rums are under my belt to be bothered to take them out, but i'll certainly be remembering it for next time. :)
# October 30, 2004 4:49 PM

Dee said:

find and replace with regular expression [0-9]+:
# October 30, 2004 8:03 PM

Yama said:

Hi,

I keep on getting an error for line 26 telling me object required. Where is "propertygridbox" in the form?

Yama
# November 9, 2004 8:46 PM

Yama said:

Man where is your mind at? LOL

Change following lines:

5: var sActiveTableId = "propertygridtable";

6: var sActiveCobtainerId = "propertygridbox";

To

5: var sActiveTableId = "rTable";
6: var sActiveCobtainerId = "container";


Yama

# November 9, 2004 8:53 PM

Yama said:

Hi,

Why are you doing this?
<BODY onMouseUp="cleanUp();">

I'd rather do this:
<BODY onLoad="cleanUp();">

If using C# we might want to save the values in a SessionState or ViewState when the page reloads from a sort for example or from a click with autopostback set to true.

The onMouseUp event will cause the table to move everytime you click on the table where the cursor is an e-resize.

Did you test this before posting? Or are you like me... Saying to yourself that it should work what the hack let me check it in.

LOL

Yama Kamyar
# November 9, 2004 9:30 PM

Geoff Appleby said:

Oh for sure, there's typos in there. I took it from a working page, and cleaned it up a little to remove some of the extra stuff i was doing. I guess i missed some (propertygridXXX was what the table was called before i made the changes).

The onmouseup on the body is necessary however. That's not where the resize happens, it's where it clears the globals having finished resizing.

If you can do it better, then do it better, and post your example (send me the link to!! :) I was posting this as a starting point, not as gospel, and it was late at night :P
# November 9, 2004 11:52 PM

Ronaldo said:

Hi, trying to use your snippet but the table keep jumping up and down when I click it...

any idea whats going on?
# December 28, 2004 6:36 PM

Geoff Appleby said:

Hi Ronaldo,

Yeah, this version was the first version we got working, and i wasnt' 100% happy with it.

We have a much better version working now that performs a lot better - watch this space over the next week or so, i'll be posting an update with a version that behaves a lot nicer!
# December 28, 2004 11:47 PM

Paul said:

Where is the working example you got this from?
# January 18, 2005 8:17 PM

Lakshmi Bhetanbhotla said:

Hi,
Do you have the latest working version that you mentioned above.

--Lakshmi.
# January 25, 2005 2:15 AM

Geoff Appleby said:

Laksmi, yes, I do, I've just got to find time to post it. I'll a do a new post when I've updated it, I've just been flat out with work. Soon, i promise.
# January 25, 2005 2:30 AM

Rama said:

This is a great stuff. I am also in middle of creating such script. Is it possible for you to send me the modified script to pvrk_6@yahoo.com. THis will greatly expedite my work and really appreciate if you could send me the info. We have created two scripts for sorting of the table rows (pretty fast actually). If you are interested, let me know, i can send it to you.
-Thanks, Rama
# January 26, 2005 3:12 AM

Viola said:

Hi,
when I try to use this script I've an error notification because reSizable attribute is not supported in IE6.0: I'm alone to have this problem?

Thank you

# February 8, 2005 4:03 PM

TrackBack said:

# February 9, 2005 5:30 AM

TrackBack said:

# February 9, 2005 5:46 AM

abeelias said:

Copy Paste... If you're a "real" programmer you'd take a bit more pride in putting up code that works. And the line numbers was a nice touch for anyone trying to copy and paste it into a working example. If you want a solution look at ActiveWidgets its GPL and they have a working example.
# July 12, 2005 8:57 PM

Geoff Appleby said:

Hi abeelias.

Thanks for your tremendously useful comment. If you read the entire article (or even the first paragraph), you'd see a link to an updated post that does indeed contain code that works and doesn't have line numbers.

I left this one unmodified in it's poor state for several reasons, none of which I think I need to justify to you (not now that you've proved yourself to be such a gracious reasonable person).

Do you only try to find ways to put people down, or do you have nice things to say too?
# July 12, 2005 9:33 PM

Max said:

Hi Gappleby,
Thank you for your time and effort in helping others to save time in deploying great solutions.

I can't believe the comment of abeelias!!

Anyone such as yourself who takes any time to help others deserves praise! I don't expect free, useful code to be perfect, I am just grateful that people like you are happy to provide base code snippets to help people like me save a couple or three days in trial & error and research for each new extension we write.

Thanks, and don't let the pricks become thorns in your side.  

Cheers,

Max
# April 3, 2006 8:31 PM

vogel said:

Порожняк !!
# April 5, 2006 12:39 PM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

To submit your comment, click on these pictures:
  • Geoff's mother (normal)
  • Searching Geoff
  • Geoff's big sister's tongue
Gaptcha Image - No Peeking! Gaptcha Image - No Peeking! Gaptcha Image - No Peeking!
Gaptcha Image - No Peeking! Gaptcha Image - No Peeking! Gaptcha Image - No Peeking!
Gaptcha Image - No Peeking! Gaptcha Image - No Peeking! Gaptcha Image - No Peeking!
Can't recognise the people in these pictures? Look here for a quick introduction.
There's a time limit for you to get your comment submitted before this set of pictures expires. If you think it's been longer than 10 minutes, get some new pictures first (you won't lose what you've typed so far).
Get some new pictures 

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS