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 :)