Sortable Tables
Correction: I found and corrected an error in the date sorting function that was making it sort by year then day then month.
The following is a revision/update/complete muck-up of a script pointed out to me by a co-worker. The original article and script were written by Stuart Langridge. I took his original and made modifications (hopefully for the better) and we have used it at work on numerous occasions.
The basic principle is to be able to create an HTML table and allow the user to sort it without having to make a round trip to the server to do the sorting. The script that does the sorting can be included in your pages with no modifications. To set up a table to be sorted all you need to do is:
- give the table a unique ID
- assign the table a class of 'sortable'
- Within the headers of the table create a link that calls the javascript function and create a span with 3 empty spaces in it for the sort arrow to be placed.
- You make a row that isn't sorted by giving the row a class of summary
In the call to the function to do the sorting you can, optionally, specify the type of compare to perform for that column. Otherwise, the script will make a reasonable attempt to determine the type of data and compare it accordingly.
Try it
Click a column header to sort the table
| Order # | User ID | Order Date | Status | Order $ |
|---|---|---|---|---|
| 12345 | Wendel's Widgets | 11/30/04 | Open | $12.25 |
| 26359 | Roger's Robots | 05/20/01 | Shipped | $11.32 |
| 99568 | SuperMart | 11/29/04 | Open | $100.26 |
| 456887 | Acme Drapes | 1/01/04 | Lost | $0.02 |
| 11250 | Ran out of names | 12/30/08 | Shipped | $1.28 |
| Total $ value: | $125.13 | |||
You can find the HTML for the demo table by viewing the source and searching for 'demotablehtml'.
The javascript to do the actual sorting. It is fairly well commented, but it you have a question send us an email:
var browserName = navigator.appName;
var colNumber; // the column number of the selected header
function sortTable(theHdr, compareType){
var selCell = theHdr.parentNode; // the cell
colNumber = selCell.cellIndex;
var theTable = getParent(selCell,"table"); // the table the cell is in
if (theTable.rows.length <= 1)
return;
// find the data type of the selected column and set the comparator
comparator = compareType;
if (comparator == undefined){
var colData = getInnerText(theTable.rows[1].cells[colNumber]);
comparator = textCompare;
if (colData.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/))
comparator = dateCompare;
if (colData.match(/^\d\d[\/-]\d\d[\/-]\d\d$/))
comparator = dateCompare;
if (colData.match(/^[£$]/))
comparator = currencyCompare;
if (colData.match(/^[\d\.]+$/))
comparator = numberCompare;
}
// copy the data rows in the table into a temp array and sort them
// exclude summary rows by checking the class name of the row
var newRows = new Array();
for (j = 1; j < theTable.rows.length; j++){
if (theTable.rows[j].className.indexOf("summary") == -1){
newRows[j-1] = theTable.rows[j];
}
}
newRows.sort(comparator);
// determine number of summary rows and save them in a temp array
var sumRowsCount = theTable.rows.length - newRows.length - 1;
var sumStart = theTable.rows.length - sumRowsCount;
var sumArray = new Array();
for (i = sumStart; i < theTable.rows.length; i++){
sumArray[i - sumStart] = theTable.rows[i];
}
// check all spans in the same table and clear their arrows
var allspans = document.getElementsByTagName("span");
for (var indx = 0; indx < allspans.length; indx++) {
if (allspans[indx].className == "sortarrow") {
if (getParent(allspans[indx],"table") == getParent(theHdr,"table")) {
allspans[indx].innerHTML = " ";
}
}
}
// find the span in the selected header and set the arrow indicator
var span;
for (var indx = 0; indx < theHdr.childNodes.length; indx++) {
if (theHdr.childNodes[indx].tagName &&
theHdr.childNodes[indx].tagName.toLowerCase() == 'span'){
span = theHdr.childNodes[indx];
}
}
var sortArrow;
if (span.getAttribute("sortdir") == "down") {
sortArrow = " ⇑";
newRows.reverse();
span.setAttribute("sortdir","up");
} else {
sortArrow = " ⇓";
span.setAttribute("sortdir","down");
}
span.innerHTML = sortArrow;
// rebuild the table by inserting the rows back in. Exclude summary rows
// that are added on the bottom.
for (i=0; i<newrows.length; i++){
if (!newrows[i].classname || (newrows[i].classname &&
(newrows[i].classname.indexof("summary") == -1))){
thetable.tbodies[0].appendchild(newrows[i]);
}
}
// put the summary rows back on the bottom
for (i=0; i<sumArray.length; i++){
theTable.tBodies[0].appendChild(sumArray[i]);
}
}
Two helper functions:
/* function getParent(elem, parentToFind)
This function finds the parent of the passed in element that has the
name that is passed as the second argument.
Inputs:
elem - the element whos parent is being found
parentToFind - the name of the parent to find
Return Value:
The parent element that is found or null if one is not found
12-11-2004 DV
*/
function getParent(elem, parentToFind) {
if (elem == null){
return null;
} else if (elem.nodeType == 1 && elem.tagName.toLowerCase() ==
parentToFind.toLowerCase())
return elem;
else
return getParent(elem.parentNode, parentToFind);
}
/* function getInnerText(elem)
This function takes the passed in element and extracts its innerText. If the
passed in element has no innertext itself and has child nodes the
function will recursively extract the inner text from them as well
and append it.
Inputs:
elem - the element to extract the innerText from
Return Value:
A string containing the innerText of the passed in element.
12-11-2004 DV
*/
function getInnerText(elem) {
if (typeof elem == "string"){
return elem;
} else if (typeof elem == "undefined"){
return elem;
} else if (elem.innerText){
return elem.innerText;
}
// check any child nodes
var theText = "";
var theKids = elem.childNodes;
for (var i = 0; i < theKids.length; i++) {
switch (theKids[i].nodeType) {
case 1: //ELEMENT_NODE
theText += getInnerText(theKids[i]);
break;
case 3: //TEXT_NODE
theText += theKids[i].nodeValue;
break;
}
}
return theText;
}
The comparator functions used to sort the data in the cells:
/*
Return Value:
-1 - if the first date comes before the second
0 - if the two dates are the same
1 - if the second date comes before the first
*/
function dateCompare(arr1, arr2) {
// get the value from inside the cell
var value1 = getInnerText(arr1.cells[colNumber]);
var value2 = getInnerText(arr2.cells[colNumber]);
// determine if 4 digits years are used - parse out the date
if (value1.length == 10) {
dt1 = value1.substr(6,4) + value1.substr(0,2) + value1.substr(3,2);
} else {
yr = value1.substr(6,2);
if (parseInt(yr) < 50){
yr = '20'+yr;
} else {
yr = '19'+yr;
}
dt1 = yr + value1.substr(0,2) + value1.substr(3,2);
}
if (value2.length == 10) {
dt2 = value2.substr(6,4) + value2.substr(0,2) + value2.substr(3,2);
} else {
yr = value2.substr(6,2);
if (parseInt(yr) < 50){
yr = '20'+yr;
} else {
yr = '19'+yr;
}
dt2 = yr + value2.substr(0,2) + value2.substr(3,2);
}
if (dt1 == dt2)
return 0;
if (dt1 < dt2)
return -1;
return 1;
}
/*
Return Value:
< 0 - indicates the first value comes before the second
0 - if the two values are the same
> 0 - if the second value comes before the first
*/
function currencyCompare(arr1, arr2) {
var value1 = getInnerText(arr1.cells[colNumber]).replace(/[^0-9.]/g,'');
var value2 = getInnerText(arr2.cells[colNumber]).replace(/[^0-9.]/g,'');
return parseFloat(value1) - parseFloat(value2);
}
/*
Return Value:
< 0 - indicates the first value comes before the second
0 - if the two values are the same
> 0 - if the second value comes before the first
*/
function numberCompare(arr1, arr2) {
var value1 = parseFloat(getInnerText(arr1.cells[colNumber]));
if (isNaN(value1))
value1 = 0;
var value2 = parseFloat(getInnerText(arr2.cells[colNumber]));
if (isNaN(value2))
value2 = 0;
return value1 - value2;
}
/*
Return Value:
-1 - indicates the first value comes before the second
0 - if the two values are the same
1 - if the second value comes before the first
*/
function textCompare(arr1, arr2) {
var value1 = getInnerText(arr1.cells[colNumber]).toLowerCase();
var value2 = getInnerText(arr2.cells[colNumber]).toLowerCase();
if (value1 == value2)
return 0;
if (value1 < value2)
return -1;
return 1;
}
Be aware that in this version it is hard coded to look for an 8 or 10 character
date, so your dates should look like 08/27/2006 or 06/26/04. The separator
doesn't matter.
There are many other potential comparators that could be needed, if you use
this and need to create a new comparator, send it to us and we'll include it
here for everyone. The easiest way to include this would be to create an
include file, drop all the JavaScript into it, and make your table accordingly.
If you have a question, comment, bug fix, or addition let us know. We'll add it
to the demo with the proper credit. Just drop us an
email at comments@directedinsight.com
