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 = ' ∇';
rows.reverse();
removeClass(span, 'sortDesc');
addClass(span, 'sortAsc');
} else {
ARROW = ' Δ';
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