DirectedInsight
NEWEST
Different ways to show the user they're wrong.
Helpful
Some simple queries that can save you some research time
NEWER
Complete AJAX example.
COOL
Dynamic file upload fields

small DI oval 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:

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.


small DI oval 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 = "&nbsp;&nbsp;&uArr;";
        newRows.reverse();
        span.setAttribute("sortdir","up");
    } else {
        sortArrow = "&nbsp;&nbsp;&dArr;";
        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

small DI oval