Dynamic Table Column Resizing in IE
I wrote about this very subject in the past. That old blog entry was showing the code from the first few drafts of getting this working right.
Since then, many people have read it, and many have asked questions, and I've been continually promising to give an update of the end result of how it came out. The credit doesn't go to me however, as much as I'd like to think so. Noonie (my boss) wrote the first drafts, and Rory refined it. However, since I posted Noonie's first draft, I get to post the update.
Note: Please remember, this only works in Internet Explorer. There's several good reasons why this doesn't work in Firefox (all to do with the client-side javascript) and these include window.attachEvent() and element.parentElement(). I haven't looked at making a Firefox version yet.
Extensibility options: This example works perfectly. On top of that, you can combine it with other tricks - like, say, TBODY scrolling :) The two don't interfere with each other.
So, there easiest way is to simply list all the code, and describe what's going on.
There's four important points to remember about it all.
- Based on how the code is written, the table that has resizable columns must be contained in some other element (in these examples I use a DIV) and that element must have a style set of "position: absolute;". You need this to size the black vertical bar that attaches to the mouse correctly when performing an actual resize.
- Based on how the code is written, you can only perform resize operations on table header cells (TH tags). This simply feels like the right way to do it - you could make it TD tags instead, and be able resize the column for the entire height of the table if you wanted to.
- The actual table must have a style set of "table-layout: fixed;" If you don't set this style, strange things happen (try it and see, it goes all loopy when you resize :)
- To save having to remember to put too many requirements in the implementation, the black vertical bar is created dynamically. It's important to leave the creation of this element until the page's onload event fires - if it runs before it finishes loading, you IE sometime throws an error trying to call document.body.appendChild() - it took us ages to debug this!
So long as you remember these three things, it should all work smoothly.
So, first lets start with some simple styles
.tablecontainer
{
position: absolute;
}
.mytable
{
table-layout: fixed;
}
.mytable TD, .mytable TH
{
border: solid 1px black;
width: 120px;
}
.mytable TH
{
background-color: #e0e0e0;
}
The tablecontainer class is set to absolute positioning (see point 1 above). The mytable class has a table-layout of fixed (see point 3 above). The rest is only there for prettiness.
Now, the HTML. The styles listed above are referenced in tabletest.css. Javascript functions are all contained in tableresize.js.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Table Test</title>
<link rel="stylesheet" type="text/css" href="tabletest.css" />
<script type="text/javascript" src="tableresize.js"></script>
</head>
<body>
<div class="tablecontainer">
<table border="0" cellspacing="0" cellpadding="0" class="mytable"
onmousemove="TableResize_OnMouseMove(this);"
onmouseup="TableResize_OnMouseUp(this);"
onmousedown="TableResize_OnMouseDown(this);">
<tr>
<th>Column 1</th><th>Column 2</th><th>Column 3</th><th>Column 4</th><th>Column 5</th>
</tr>
<tr>
<td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td>
</tr>
<tr>
<td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td>
</tr>
<tr>
<td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td>
</tr>
<tr>
<td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td>
</tr>
<tr>
<td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td>
</tr>
<tr>
<td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td>
</tr>
<tr>
<td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td>
</tr>
<tr>
<td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td><td>Some Data</td>
</tr>
</table>
</div>
</body>
</html>
As you can see, it's just a simple table contained in a DIV. It's really nothing special. The trick is in the javascript that follows. It's fairly well commented, so there shouldn't be too much to say about it.
/*
Table Resizing code
*/
/*
Global constants to store elements that may be resized but we
could probably place these into custom table attributes instead.
*/
var sResizableElement = "TH"; // This MUST be upper case
var iResizeThreshold = 8;
var iEdgeThreshold = 8;
var iSizeThreshold = 20;
var sVBarID = "VBar";
/*
Global variables to store position and distance moved but we
could probably place these into custom table attributes instead.
*/
var oResizeTarget = null;
var iStartX = null;
var iEndX = null;
var iSizeX = null;
/*
Helper Functions
*/
/*
Creates the VBar on document load
*/
function TableResize_CreateVBar()
{
// Returns a reference to the resizer VBar for the table
var objItem = document.getElementById(sVBarID);
// Check if the item doesn't yet exist
if (!objItem)
{
// and Create the item if necessary
objItem = document.createElement("SPAN");
// Setup the bar
objItem.id = sVBarID;
objItem.style.position = "absolute";
objItem.style.top = "0px";
objItem.style.left = "0px";
objItem.style.height = "0px";
objItem.style.width = "2px";
objItem.style.background = "silver";
objItem.style.borderLeft = "1px solid black";
objItem.style.display = "none";
// Add the bar to the document
document.body.appendChild(objItem);
}
}
window.attachEvent("onload", TableResize_CreateVBar);
/*
Returns a valid resizable element, even if it contains another element
which was actually clicked otherwise it returns the top body element.
*/
function TableResize_GetOwnerHeader(objReference)
{
var oElement = objReference;
while (oElement != null && oElement.tagName != null && oElement.tagName != "BODY")
{
if (oElement.tagName.toUpperCase() == sResizableElement)
{
return oElement;
}
oElement = oElement.parentElement;
}
// The TH wasn't found
return null;
}
/*
Find cell at column iCellIndex in the first row of the table
needed because you can only resize a column from the first row.
by using this, we can resize from any cell in the table if we want to.
*/
function TableResize_GetFirstColumnCell(objTable, iCellIndex)
{
var oHeaderCell = objTable.rows(0).cells(iCellIndex);
return oHeaderCell;
}
/*
Clean up - clears out the tracking information if we're not resizing.
*/
function TableResize_CleanUp()
{
// Void the Global variables and hide the resizer VBar.
var oVBar = document.getElementById(sVBarID);
if (oVBar)
{
oVBar.runtimeStyle.display = "none";
}
iEndX = null;
iSizeX = null;
iStartX = null;
oResizeTarget = null;
oAdjacentCell = null;
return true;
}
/*
Main Functions
*/
/*
MouseMove event.
On resizable table This checks if you are in an allowable 'resize start' position.
It also puts the vertical bar (visual feedback) directly under the mouse cursor.
The vertical bar may NOT be currently visible, that depnds on if you're resizing.
*/
function TableResize_OnMouseMove(objTable)
{
// Change cursor and store cursor position for resize indicator on column
var objTH = TableResize_GetOwnerHeader(event.srcElement);
if (!objTH)
return;
var oVBar = document.getElementById(sVBarID);
if (!oVBar)
return;
var oAdjacentCell = objTH.nextSibling;
// Show the resize cursor if we are within the edge threshold.
if ((event.offsetX >= (objTH.offsetWidth - iEdgeThreshold)) && (oAdjacentCell != null))
{
objTH.runtimeStyle.cursor = "e-resize";
}
else
{
if(objTH.style.cursor)
{
objTH.runtimeStyle.cursor = objTH.style.cursor;
}
else
{
objTH.runtimeStyle.cursor = "";
}
}
// We want to keep the right cursor if resizing and
// don't want resizing to select any text elements...
if (oVBar.runtimeStyle.display == "inline")
{
// We have to add the body.scrollLeft in case the table is wider than the view window
// where the table is entirely within the screen this value should be zero...
oVBar.runtimeStyle.left = window.event.clientX + document.body.scrollLeft;
document.selection.empty();
}
return true;
}
/*
MouseDown event.
This fills the globals with tracking information, and displays the
vertical bar. This is only done if you are allowed to start resizing.
*/
function TableResize_OnMouseDown(objTable)
{
// Record start point and show vertical bar resize indicator
var oTargetCell = event.srcElement;
if (!oTargetCell)
return;
var oVBar = document.getElementById(sVBarID);
if (!oVBar)
return;
if (oTargetCell.parentElement.tagName.toUpperCase() == sResizableElement)
{
oTargetCell = oTargetCell.parentElement;
}
var oHeaderCell = TableResize_GetFirstColumnCell(objTable, oTargetCell.cellIndex);
if ((oHeaderCell.tagName.toUpperCase() == sResizableElement) && (oTargetCell.runtimeStyle.cursor == "e-resize"))
{
iStartX = event.screenX;
oResizeTarget = oHeaderCell;
// Mark the table with the resize attribute and show the resizer VBar.
// We also capture all events on the table we are resizing because Internet
// Explorer sometimes forgets to bubble some events up.
// Now all events will be fired on the table we are resizing.
objTable.setAttribute("Resizing", "true");
objTable.setCapture();
// Set up the VBar for display
// We have to add the body.scrollLeft in case the table is wider than the view window
// where the table is entriely within the screen this value should be zero...
oVBar.runtimeStyle.left = window.event.clientX + document.body.scrollLeft;
oVBar.runtimeStyle.top = objTable.parentElement.offsetTop + objTable.offsetTop;;
oVBar.runtimeStyle.height = objTable.parentElement.clientHeight;
oVBar.runtimeStyle.display = "inline";
}
return true;
}
/*
MouseUp event.
This finishes the resize.
*/
function TableResize_OnMouseUp(objTable)
{
// Resize the column and its adjacent sibling if position and size are within threshold values
var oAdjacentCell = null;
var iAdjCellOldWidth = 0;
var iResizeOldWidth = 0;
if (iStartX != null && oResizeTarget != null)
{
iEndX = event.screenX;
iSizeX = iEndX - iStartX;
// Mark the table with the resize attribute for not resizing
objTable.setAttribute("Resizing", "false");
if ((oResizeTarget.offsetWidth + iSizeX) >= iSizeThreshold)
{
if (Math.abs(iSizeX) >= iResizeThreshold)
{
if (oResizeTarget.nextSibling != null)
{
oAdjacentCell = oResizeTarget.nextSibling;
iAdjCellOldWidth = (oAdjacentCell.offsetWidth);
}
else
{
oAdjacentCell = null;
}
iResizeOldWidth = (oResizeTarget.offsetWidth);
oResizeTarget.style.width = iResizeOldWidth + iSizeX;
if ((oAdjacentCell != null) && (oAdjacentCell.tagName.toUpperCase() == sResizableElement))
{
oAdjacentCell.style.width = (((iAdjCellOldWidth - iSizeX) >= iSizeThreshold)?(iAdjCellOldWidth - iSizeX):(oAdjacentCell.style.width = iSizeThreshold))
}
}
}
else
{
oResizeTarget.style.width = iSizeThreshold;
}
}
// Clean up the VBar and release event capture.
TableResize_CleanUp();
objTable.releaseCapture();
return true;
}
And that's all there is to it. There's on function called on mouse down, one on mouse move, and one on mouse up. The resizing happens in the mouse up event.
In my first post about doing this, I had resizable attributes marking which columns could and couldn't be resized. While you could still do this, we realised it was pretty silly - either you've attached the resize code to the table onmouse* events, or you haven't - surely all columns would be resizable. However, there's nothing to stop you putting extra checks in yourself. I also previously had double-click functionality to 'resize to fit' in there before - this is still pretty nasty, as we didn't bother getting it perfect, since we didn't need it. I'll leave that as an exercise for you to do :)