HTML tables are supposed to support a scrollable table body, but certain shoddy browsers only pay lip service to specifications.
At work users want, nay demand their data table to be displayed in a useful fashion, and who can blame them?
No one wants to scroll down 50 columns to find that value you’re looking for, only to have to scroll back up 50 columns to find out what’s in the next row.
I’ve tried a couple of different approaches to creating scrollable tables, and previously none worked, or worked well.
This “pure CSS” approach is pretty good for standard’s compliant browsers…but that’s not really the problem. This version relies on a rendering bug in Internet Explorer to fix the table header in place. If that table is integrated with other DHTML (say a script which resizes the table based on the layout of the page, or size of the browser window) it has a tendency to go “bonk” in a most amusing fashion.
Other techniques are overblown monolithic piles of JavaScript which functionally recreate a table-like structure from other elements. Again that seems like disaster waiting to happen, and is not likely to work across browsers.
(I’m guilty of this. I wrote a horrible nest of javascript which actually duplicates the entire table, wraps it in a DIV, then clips the div to show just the header at the right spot. *shudder*)
Most examples of scrolling HTML tables also feature skinny little 4 or 5 column tables. What happens when you need to display all 35 columns of sales data from the legacy database in your new corporate web app and the mainframe users are screaming for something, anything, to make their miserable lives better? (short version: most scrolling table solutions don’t work well for truly large tables)
The most obvious javascript solution to this problem is to clone or copy the header (and footer) of the table to be scrolled, and superimpose them over the table in the right position, and dropping the whole mess into a DIV styled to scroll.
Easier said than done.
The challenge is matching up the widths of the table cells between your “data” table and your “header” table. It is possible to programmatically capture the widths of each table cell in a row, then apply those widths to your header. Often the columns widths of the header won’t match up with the columns in the table, especially if there’s any padding applied via CSS or the cellpadding attribute of the table tag.
While I was working on this I had a dim recollection from the early dark days of web design. Back when cavemen used Macromedia Fireworks to hack up a print designer’s layout into a million tiny GIFs and export into an awful, slow loading, HTML table nightmare.
Those messes of table cells and images were held together by pre-stretching the table cells with 1px by 1px transparent GIF images laid out into little skinny rows at the tops of these layouts to enforce the width of the table.
HTML table cells widths are determined primarily by their content. If you assign a width to a table cell, it’s merely a suggestion. If you stick a 600px wide picture in that table cell, it’s going to stretch to accommodate it. This is on purpose. You’d be pissed if the web site you were reading cut off the last word in every sentence just because some designer wanted the column to be 60px wide.
Taking a cue from my ancient ancestors, I worked up a solution that first determines the current display widths of all the table cells in the first row of the table body.
Those widths are then applied to dynamically generated DIV elements, placed into table cells in dynamically generated table rows.
The content of the added rows are all styled to be 0px tall so the don’t interfere with the display of the table.
The table is your standard HTML table with a THEAD, TBODY, and TFOOT. The TFOOT is optional.
The table is placed in a DIV element, which becomes the scrolling frame.
<div id="scroller">
<table cellspacing="0" cellpadding="0" class="formdata">
<thead>
<tr>
<th>foo</th>
<!-- more columns -->
</tr>
</thead>
<tfoot>
<tr>
<td>foo-ter</td>
<!-- more columns-->
</tr>
</tfoot>
<tbody>
<tr>
<!-- ... -->
</tbody>
</table>
</div>
I use the following CSS for my example:
body {
font-family: Arial, Helvetica, sans-serif;
font-size: .9em;
margin: 0;
padding: 1em;
}
table.formdata,
table.formdata td,
table.formdata th {
border: 1px solid #bbbbbc;
border-collapse: collapse;
}
table.formdata th,
table.formdata td {
padding: .25em;
margin: 0px;
}
table.formdata thead th,
table.formdata tfoot td {
background: #ddd;
}
#scroller {
border: 1px sold #bbbbbc;
}
The scrolling table is instantiated on window load like so:
<script type="text/javascript" language="javascript">
var scroller;
function setScroll() {
scroller = new ScrollTable("scroller",300);
}
window.onload = function() {
setScroll();
}
</script>
The constructor requires an id or object reference of the div (or block) containing the table, and an optional numeric height (in pixels). The default height is 400px.
The ScrollingTable object requires the Prototype javascript library (the latest version, 1.4.1, can be found here)
var ScrollTable = Class.create();
ScrollTable.prototype = {
initialize: function(tbox,theight) {
// default height
this.setHeight = 400; if (theight && !isNaN(theight)) {this.setHeight = theight;}
// get containing box, set style
this.tableBox = $(tbox);
// clean house
this.resetScroll();
//set container styles
Element.setStyle(this.tableBox,{position: "relative",overflow: "auto", height: (this.setHeight+"px"),width: "100%"});
// set up table
this.table = this.tableBox.getElementsByTagName("TABLE")[0];
this.setWidths();
Element.setStyle(this.table,{position: "absolute", top: this.tableBox.scrollTop+"px", left: "0px"});
// create table head and foot (if needed)
this.tableHead = null; this.createThead();
this.tableFoot = null; this.createTfoot();
//reposition head and foot onscroll
this.tableBox.tableHead = this.tableHead;
this.tableBox.tableFoot = this.tableFoot;
this.tableBox.onscroll = function() {
Element.setStyle(this.tableHead,{top:this.scrollTop+"px"});
if (this.tableFoot!=null) {
var delta = Position.realOffset(this.tableFoot);
var newTop = (this.clientHeight-this.tableFoot.offsetHeight)+delta[1];
Element.setStyle(this.tableFoot,{top:newTop+"px"});
}
}
},
// return to original state. called on initialize -- used for house cleaning if needed.
resetScroll: function() {
Element.setStyle(this.tableBox,{position: "static",overflow: "visible", width: "auto",height: "auto"});
var tbl = this.tableBox.getElementsByTagName("TABLE")
if (tbl.length>1) {
while(tbl.length>1) {
this.tableBox.removeChild(tbl[1]); // remove added header and fooer
}
//remove spacers
if ($("_scTbodySpacer")) {Element.remove("_scTbodySpacer");}
if ($("_scTheadSpacer")) {Element.remove("_scTheadSpacer");}
if ($("_scTfootSpacer")) {Element.remove("_scTfootSpacer");}
}
},
resize: function(theight) {
if (isNaN(theight)) {return;}
this.setHeight = theight;
Element.setStyle(this.tableBox,{height:this.setHeight+"px"});
var delta = Position.realOffset(this.tableBox);
Element.setStyle(this.tableHead,{top:delta[1]+"px"});
if (this.tableFoot!=null) {
var delta = Position.realOffset(this.tableFoot);
var newTop = (this.tableBox.clientHeight-this.tableFoot.offsetHeight)+delta[1];
Element.setStyle(this.tableFoot,{top:newTop+"px"});
}
return;
},
//manually fix width of table and cell widths
setWidths: function() {
Element.setStyle(this.table,{width:this.table.offsetWidth+"px"});
var td = this.table.tBodies[0].rows[0].cells;
var tdl = td.length; //number of cells in first row in tbody
var len = new Array();
for(var i=0; i<tdl; i++) {
len.push(td[i].offsetWidth);
}
// create spacer "gif" - Party like it's 1997
var div = document.createElement("DIV")
div.appendChild(document.createTextNode("\u00A0"));
Element.setStyle(div,{height: "0px",margin: "0px",padding:"0px", overflow:"hidden"});
var tr = document.createElement("TR");
for(var j=0;j<tdl;j++) {
var td = document.createElement("TD");
var insertSpacer = div.cloneNode(true);
Element.setStyle(insertSpacer,{width: len[j]+"px"});
Element.setStyle(td,{height:"0px",overflow:"hidden",padding:"0px",margin:"0px"});
td.appendChild(insertSpacer);
tr.appendChild(td);
}
//append spacer row to first tbody
var tbody = this.table.tBodies[0];
var tbtr = tr.cloneNode(true); tbtr.id = "_scTbodySpacer";
tbody.appendChild(tbtr);
//append spacer row to thead
var tHead = this.table.tHead;
var thtr = tr.cloneNode(true); thtr.id = "_scTheadSpacer";
tHead.appendChild(thtr);
// append spacer row to tfoot if available
if (this.table.tFoot) {
var tFoot = this.table.tFoot;
var tftr = tr.cloneNode(true); tftr.id = "_scTfootSpacer"
tFoot.appendChild(tftr);
}
},
// clone THEAD or first row into new table to create header
createThead: function() {
this.tableHead = this.table.cloneNode(false);
var append = (this.table.tHead) ?
this.table.tHead.cloneNode(true) :
this.table.tBodies[0].rows[0].cloneNode(true);
if (append.nodeName=="THEAD") {
this.tableHead.appendChild(append);
} else {
var tb = document.createElement("TBODY")
tb.appendChild(append);
this.tableHead.appendChild(tb);
}
this.tableBox.appendChild(this.tableHead);
Element.setStyle(this.tableHead,{position:"relative",left:"0px",zIndex:9});
},
// create footer from TFOOT if it exists
createTfoot: function() {
if (!this.table.tFoot || this.table.tFoot.rows.length==0) {
this.tableFoot = null;
return;
}
this.tableFoot = this.table.cloneNode(false);
var append = this.table.tFoot.cloneNode(true);
this.tableFoot.appendChild(append);
this.tableBox.appendChild(this.tableFoot);
var scrollBottom =(this.tableBox.clientHeight-this.tableFoot.offsetHeight);
Element.setStyle(this.tableFoot,{position: "absolute",top:scrollBottom+"px", zIndex:9});
}
}
The scrolling table can be resized by calling the ‘resize’ method, and supplying a numeric height (in pixels). Like so:
scroller.resize(150);
This technique is used on the “small, medium, large” links on the example page.
To do:
There’s an odd bug that appears if the ScrollingTable is larger than the srolling height of the window…the scroll height of the frame increaes so the table can be scrolled out of view. This bug is consistant on Internet Explorer, Mozilla/Firefox, and Safari
Works with:
- Internet Explorer 6+ (Windows)
- Safari (Mac OS X)
- Firefox (all platforms)
