Active Tables

I have implemented in the past a number of server side routines to allow HTML tables to be sorted by clicking on the title bar. I decided to investigate whether this could be done client side using DHTML.

Initial investigations found a website, which showed a method of sorting tables in Javascript. This method involved guessing the field type by the contents of the first data row and sorting the fields based on that assumption. Although this worked, I would rather the field types were identified by a class reference. So I decided to have a go at making the code do what I wanted. The results are shown below (The table was borrowed from wikipedia just for use in this example).

Order of Death President Date of Death Order of Office Cause of Death
1 George Washington 14 December 1799 1 pneumonia
2 Thomas Jefferson 4 July 1826 3 most likely dehydration resulting from amoebic dysentery
3 John Adams 4 July 1826 2 most likely heart failure caused by arteriosclerosis
4 James Monroe 4 July 1831 5 most likely tuberculosis
5 James Madison 28 June 1836 4 unknown debility
6 William Henry Harrison 4 April 1841 9 pleurisy, pneumonia
7 Andrew Jackson 8 June 1845 7 comsumption, dropsy, tubercular hemorrhage
8 John Quincy Adams 23 February 1848 6 paralysis (stroke)
9 James K. Polk 15 June 1849 11 cholera morbus resulting in debilitating diarrhea
10 Zachary Taylor 9 July 1850 12 heat stroke, bringing on bilious fever, typhoid fever, and cholera morbus
11 John Tyler 18 January 1862 10 bilious fever, respiratory failure
12 Martin Van Buren 24 July 1862 8 asthmatic suffocation
13 Abraham Lincoln 15 April 1865 16 assassinated by gunshot
14 James Buchanan 1 June 1868 15 respiratory failure, rheumatic gout
15 Franklin Pierce 8 October 1869 14 inflammation of the stomach
16 Millard Fillmore 8 March 1874 13 paralysis
17 Andrew Johnson 31 July 1875 17 paralysis
18 James A. Garfield 19 September 1881 20 assassinated by gunshot
19 Ulysses S. Grant 23 July 1885 18 carcinoma (cancer) of the tongue and tonsils
20 Chester A. Arthur 18 November 1886 21 Bright's disease, apoplexy
21 Rutherford B. Hayes 17 January 1893 19 heart disease
22 Benjamin Harrison 13 March 1901 23 pneumonia
23 William McKinley 14 September 1901 25 assassinated by gunshot
24 Grover Cleveland 24 June 1908 22/24 coronary sclerosis, paralysis, or intestinal obstruction (it is disputed which)
25 Theodore Roosevelt 6 January 1919 26 coronary embolism (assumed), inflammatory rheumatism
26 Warren G. Harding 2 August 1923 29 apoplexy, pneumonia, and enlargement of the heart, all brought on by high blood pressure
27 Woodrow Wilson 3 February 1924 28 apoplexy, paralysis
28 William Howard Taft 8 March 1930 27 heart attack
29 Calvin Coolidge 5 January 1933 30 coronary thrombosis
30 Franklin Delano Roosevelt 12 April 1945 32 cerebral hemorrhage
31 John F. Kennedy 22 November 1963 35 assassinated by gunshot
32 Herbert Hoover 20 October 1964 31 internal hemorrhage, upper gastrointestinal bleed; strained vascular systems
33 Dwight D. Eisenhower 28 March 1969 34 coronary thrombosis
34 Harry S. Truman 26 December 1972 33 minor lung congestion; complexity of organic failures; collapse of cardiovascular system
35 Lyndon B. Johnson 22 January 1973 36 heart failure
36 Richard Nixon 22 April 1994 37 paralysis, swelling of the brain
37 Ronald Reagan 5 June 2004 40 pneumonia

 

Step 1 Identify The Tables to be "Activated"

The first step in making a table or tables on a page active is to identify them. The following stippet of code identifies all the tables in a page which have a class of "activetable" and then if they have at least one row, modifies the header by adding an onclick event and a <span> to hold the sort direction indicator.

addEvent(window, "load", sortables_init);

var SORT_COLUMN_INDEX;

function sortables_init() {
// Find all tables with class sortable and make them sortable
if (!document.getElementsByTagName) return;
tbls = document.getElementsByTagName("table");
for (ti=0;ti<tbls.length;ti++) {
thisTbl = tbls[ti];
if (hasClass(thisTbl, 'activetable')) {
ts_makeSortable(thisTbl);
}
}
}

function ts_makeSortable(table) {
if (table.rows && table.rows.length > 0) {
var firstRow = table.rows[0];
}
if (!firstRow) return;
// We have a first row: assume it's the header,
// and make its contents clickable links
for (var i=0;i<firstRow.cells.length;i++) {
var cell = firstRow.cells[i];
cell.innerHTML = cell.innerHTML + '<span></span>';
addEvent(cell,'click',ts_resortTable);
addClass(cell,'sortHeader');
}
}

The first line of the script adds an onload event to the body of the document, causing the table detection function to be initiated. Most of this code is taken straight from the kryogenix sorttable example. The old changes in this section is the last three lines, which I think is a tidier way of adding the sorting event than manually adding an onclick event by rewriting the innerHTML as done by the kryogenix example.

There are a number of helper functions which are used in this example, addEvent, which is a cross platform method of adding events to modern browsers by Scott Andrew and three functions of my own, designed to make class handling easier. These are addClass, hasClass and removeClass, which I think are fairly self explanitory.

function addEvent(elm, evType, fn, useCapture) {
// addEvent and removeEvent
// cross-browser event handling for IE5+, NS6 and Mozilla
// By Scott Andrew


if (elm.addEventListener){
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent){
var r = elm.attachEvent("on"+evType, fn);
return r;
} else {
alert("Handler could not be set");
}
}

function hasClass(el, fclass) {

var iClass = ' ' + el.className + ' ';
return (iClass.indexOf(fclass) != -1);
}

function removeClass(el, removeClass) {

var result = '';
var classNames = el.className.split(' ');
for (c=0; c< classNames.length; c++) {
if (classNames[c] != removeClass) {
result += ' ' + classNames[c];
}
}
if (result == '') {
el.className = '';
} else {
el.className = result.substr(1);
}
}

function addClass(el, addClass) {

if (!hasClass(el,addClass)) {
if (el.className == '') {
el.className = addClass;
} else {
el.className += (' ' + addClass);
}
}
}

Now all thats in place, It just requires the sorting code itself.

function addToArray(array, collection) {

for (i=0; i< collection.length; i++) {
array.push(collection[i]);
}
}

function ts_getInnerText(el) {
if (typeof el == "string") return el;
if (typeof el == "undefined") { return el };
if (el.innerText) return el.innerText; //Not needed but it is faster
var str = '';

var cs = el.childNodes;
var l = cs.length;
for (var i = 0; i < l; i++) {
switch (cs[i].nodeType) {
case 1: //ELEMENT_NODE
str += ts_getInnerText(cs[i]);
break;
case 3: //TEXT_NODE
str += cs[i].nodeValue;
break;
}
}
return str;
}

function ts_resortTable(e) {
// Get the clicked cell
if (!e) var e = window.event;
e.cancelBubble = true;
if (e.stopPropagation) {
e.stopPropagation();
}
var cell;
if (e.target) cell = e.target;
else if (e.srcElement) cell = e.srcElement;
// If the nodeName is not a TH, then it is a span or other element
// in the cell, so get the correct element
if (cell.nodeName != 'TH') {
cell = getParent(cell,'TH');
}
// Get the span identifying the sort direction
var spans = cell.getElementsByTagName('span');
var span = spans[spans.length -1];
var column = cell.cellIndex;
var table = getParent(cell,'TABLE');

// Determine the type of sort
if (table.rows.length <= 1) return;
var itm = ts_getInnerText(table.rows[1].cells[column]);
sortfn = ts_sort_caseinsensitive;
if (hasClass(cell,'date')) sortfn = ts_sort_date;
if (hasClass(cell,'currency')) sortfn = ts_sort_currency;
if (hasClass(cell,'numeric')) sortfn = ts_sort_numeric;
if (hasClass(cell,'decimal')) sortfn = ts_sort_numeric;
SORT_COLUMN_INDEX = column;
var firstRow = new Array();
var rows = new Array();

addToArray(rows,table.rows);
//remove the header row
rows.splice(0,1);

rows.sort(sortfn);

if (hasClass(span,'sortDesc')) {
ARROW = '&nbsp;&nabla;';
rows.reverse();
removeClass(span, 'sortDesc');
addClass(span, 'sortAsc');
} else {
ARROW = '&nbsp;&Delta;';
removeClass(span, 'sortAsc');
addClass(span, 'sortDesc');
}
span.innerHTML = ARROW;

for (i=0;i<rows.length;i++) {
table.tBodies[0].appendChild(rows[i]);
}

// Delete any other arrows there may be showing
var allspans = table.getElementsByTagName("span");
for (var ci=0;ci<allspans.length;ci++) {
if (hasClass(allspans[ci], 'sortAsc') || hasClass(allspans[ci], 'sortDesc')) {
if (allspans[ci] != span) {
allspans[ci].innerHTML = '';
allspans[ci].className = '';
}
}
}
}

function getParent(el, pTagName) {
if (el == null) return null;
else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase())
// Gecko bug, supposed to be uppercase
return el;
else
return getParent(el.parentNode, pTagName);
}

function ts_sort_date(a,b) {

dt1 = Date.parse(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
dt2 = Date.parse(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));
if (isNaN(dt2)) dt2 = 0;
if (isNaN(dt1)) dt1 = 0;
return dt1-dt2;
}

function ts_sort_currency(a,b) {
aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
return parseFloat(aa) - parseFloat(bb);
}

function ts_sort_numeric(a,b) {
aa = parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
if (isNaN(aa)) aa = 0;
bb = parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));
if (isNaN(bb)) bb = 0;
return aa-bb;
}

function ts_sort_caseinsensitive(a,b) {
aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
if (aa==bb) return 0;
if (aa<bb) return -1;
return 1;
}

function ts_sort_default(a,b) {
aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
if (aa==bb) return 0;
if (aa<bb) return -1;
return 1;
}

The only thing remaining is some CSS to make the table look better...

table.activetable {
border-collapse:collapse;
border: 1px solid grey;
font-size: 90%;
}

table.activetable th {
background-color:#CCCCCC;
cursor:pointer;
}
table.activetable th span {
color:#666666;
font-size:80%;
}

table.activetable td, table.activetable th {
border: 1px solid gray;
padding: 1px 4px;
}
table td, table th {
text-align: left;
}
table .numeric {
text-align:right;
}
table .decimal, table .date {
text-align: center;
white-space:nowrap;
}

And thats about it. you can download the source javascript file, activetable.js and the css file activetable.css if you wish.

Last updated: 12 Mar 2008 20:15:56