Technical information about the CD lists

Introduction

For a long time I've kept a list of CDs I owned so that I could avoid buying duplicates, but as my collection grew managing the data became a larger task. I thought it would be an interesting exercise to put the list on my web page which was where I first discovered the problem that HTML doesn't provide for tables that allow the data to scroll while the headers and footer remain fixed.

So although I don't buy many CDs anymore (who does?!) I have still kept on working on my solution to this problem. My JavaScript routine is now quite advanced in that it allows:

I've also incorporated it into an AJAX page to show how the routine can be called multiple times and the sorting, filtering options reinstated.

Data collection - FileMaker

HandClick on each picture for a larger image.

Originally I collected the data in a set of Microsoft Foxpro tables and had a program that would merge the data with a text file to output HTML web pages. I know Foxpro really well and could have written a set of programs to maintain the tables on nicely validated screens, but I decided I should use the opportunity to learn something new.

So I moved to FileMaker Pro and created a small application to manage the various tables: products, artists and so on within the various categories (Pop, Male, Female etc). Classical CDs added an extra layer as for those I store details of individual works on each CD. Since FileMaker is pretty useless at outputting text files I use the BaseElements plugin to create the PHP files with the table data.

The PHP files

I could have used static HTML files but PHP is much easier for coordinating the page headers, menus and so on. In general I'm not using PHP for anything too special, except for the option to search across all tables from the main screen which runs a PHP search routine to create the results tables. For the main lists the PHP files created in FileMaker as HTML are just INCLUDED in the appropriate place on the page, and in the AJAX page they are called from the XMLHTTP POST request.

The HTML layout

The key to the routine is how the tables are defined in the HTML. For any one table there are actually three tables in the HTML. The first is for the header, the second for the data and the third is the footer. This allows the data to scroll while the header and footer stay fixed. If the table was a fixed size with fixed column widths then that is really all that would be required and JavaScript wouldn't be needed at all. The problem comes when the columns automatically size themselves as the header table needs to be adjusted to match the data.

To allow the elements of the tables to be accessed I use element IDs to identify them, with the structure being:

<div id="anything">

<div id="CDLJAZ:headCon" class="tbCon">
<div id="CDLJAZ:headDiv" class="tbDiv">
<table id="CDLJAZ:headTab" class="tbTab">
	<colgroup><col><col><col><col><col><col></colgroup>
	[..header rows..]
</table>
</div>
</div>

<div id="CDLJAZ:dataDiv" class="tbDat">
<table id="CDLJAZ:dataTab" class="tbTab">
	<colgroup><col><col><col><col><col><col></colgroup>
	<tbody>
	[..header rows..]
	[..data rows..]
	[..footer rows..]
	</tbody>
</table>
</div>

<div id="CDLJAZ:footCon" class="tbCon">
<div id="CDLJAZ:footDiv" class="tbDiv">
<table id="CDLJAZ:footTab" class="tbTab">
	[..footer rows..]
</table>
</div>
</div>

</div>

So all IDs for the table share a common prefix ("CDLJAZ" in this case). The CSS formatting of the tables is all done using the classes, while the JavaScript processing only uses the IDs. The header and footer rows are repeated in the main tables (the dataTab ID) so that the table will still display properly if JavaScript is not enabled.

The ID I've called "anything" above is the main container for the table. It can be any size and position, and the tables will size themselves to fit inside it.

Note that I use <colgroup> elements to define the column widths in JavaScript. This means that this script will not work on older browsers such as IE7. I could get it to check on browser versions and use the widths of the <td> elements if necessary but I'm not really bothered about old browsers.

CSS styling

Most of the CSS is just there to set the look of the table and some default values that are then overridden by the JavaScript. For example the table has a default height of 400px but this is reset in the resizeTab function. Also the tbCon items, used by the header and footer, are set as display:none; to keep them hidden until JavaScript has removed the header and footer from the dataTab element.

The JavaScript

Each table on the page needs to be created as an object using a statement like:

var cdTab = new gonTab();

In order to keep the memory use down, the gonTab object only defines the object properties. All methods are added as prototypes to the gonTab definition, which avoids each table having its own copy of all the methods.

The table can be manipulated using the functions cdTab.init, cdTab.resizeTab, cdTab.sortTab and so on. In the HTML for the table there are references to the object (for sorting and positioning the rows) which means the object must be created before that HTML. The <head> section is a good place to do this. But the cdTab.init call must go after the HTML for the tables has been defined so this can go in either the body onload function or just before the </body> tag. I prefer the latter as it avoids the screen showing the uninitialised table first.

The init function has several parameters:

init = function (tid, subSortColumn, reloadRow, filterField, filterCol)

"tid": The ID prefix for the various sections ("CDLJAZ" in the example above).
"subSortColumn": Specifies a secondary column to be used when sorting the rows. Use -1 to ignore this feature.
"reloadRow": Specifies a header row that will cause the page to reload. Use -1 to ignore this feature.
"filterField": The ID of the input field to be used when filtering the table. Leave blank if not required.
"filterCol": Which column to apply the filter field to. -1 will apply it to the whole row.

On my pages I adjust the size of the container (called "anything" in the example above) when the page is loaded and then call resizeTab to fit the table inside it. I also have a window.onresize function that changes the container size and again calls resizeTab. In this way the table changes size when the browser window is changed.

Although it's complicated by the processing for changing tabs and the XMLHTTP requests, the AJAX page is the best example of how the table's properties and methods can be accessed from the calling page.

I should probably explain a bit about setting the column widths, as it isn't obvious what this bit of code does:

for (var i=0, len=dcol.length; i<len; i++) {
	dcol[i].width='';
}

for (var i=0, len=dcol.length; i<len; i++) {
	dcol[i].width=d[i].offsetWidth + 'px';
	hcol[i].width=dcol[i].width;
}

for (var j=0, len=hcol.length; j<len; j++) {
	var AOK = true;
	for (var i=0; i<len; i++) {
		if (h[i].offsetWidth == d[i].offsetWidth) continue;
		AOK = false;
		if (h[i].offsetWidth > d[i].offsetWidth) {
			dcol[i].width=h[i].offsetWidth + 'px';
			hcol[i].width=dcol[i].width;
		} else {
			hcol[i].width=d[i].offsetWidth + 'px';
			dcol[i].width=hcol[i].width;
		}
	}
	if (AOK) break;
}

this.ht.style.tableLayout = 'fixed';
for (var i=0, len=hcol.length; i<len; i++) {
	hcol[i].width=d[i].offsetWidth + 'px';
}
this.dt.style.tableLayout = 'fixed';
for (var i=0, len=dcol.length; i<len; i++) {
	dcol[i].width=hcol[i].width;
}

First it removes any data table column widths set by the previous call to resizeTab. Since the tables were previously set to style.tableLayout auto that leaves the browser free to determine the best column sizes based on the cell contents. Then it sets the columns in the header table to be the same as the data by using width on the colgroup elements. But here's the problem: browsers take column widths as a preference rather than a fixed size, and resize the columns if they think it necessary. So if the header title is too big for the new size (e.g. because the data column is empty) then it will make that column in the header bigger, regardless of the width set in the colgroup. Just setting the column widths doesn't guarantee that the headers and data will match.

My routine tries to use the browser's automatic resizing of the headers and data to adjust the column widths. If any columns are found to have different header and data widths then the preferred width is changed to the larger one, at which point the browser adjusts all the other columns slightly. By looping through this process a few times the columns become closer to the required sizes without having any overflows, but unfortunately might still not be exactly identical. So next it swaps the header table into fixed layout mode, which stops the browser automatically resizing, and sets the final column sizes. Since the CSS for the headers is set to overflow:hidden there is no bleeding of one header into the next if the cell is truncated too far. Then it swaps the data table to be fixed too and sets its columns to the same values. In fixed layout the columns are shown exactly as set in the colgroup so now the two tables match correctly with minimum truncation.