//**
// Filename: SortableTable.js
//
// author Bob Matlin bmatlin_at_matlinsoftwareservices_dot_com (replace "_at_" with @, "_dot_" with .)
// rev 10
//
// 11/18/2005 Fixed bugs that resulted in error when a table cell was missing.
//
// Description: A JavaScript class that allows html tables to be sorted. The table can
// be sorted once (on body load, for instance) or if desired, the table can also be sorted 
// live by the user.
//
// Note: This has only been used with Netscape 7.2, IE 6.0, and Firefox 1.0.1. If you use other
// browsers or versions, test before incorporating this code into your web pages.
//
// Copyright (C) 2004 Bob Matlin
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//
// Alternatively, you may visit http://www.gnu.org. As of this writing (June 6, 2004), the
// GPL is at http://www.gnu.org/licenses/gpl.html#SEC1.
//
// Contact information:
// bmatlin_at_matlinsoftwareservices_dot_com
// (replace "_at_" with @, "_dot_" with .)
//
// Or visit http://matlinsoftwareservices.com and go to the Contact page.
//
// If you need to contact me by mail, please use email first.
//*

//**
// Constructor
//
// param id - the id of a table; to sort live, make this the same name as the associated variable. That is,
// make the constructor like this: var tbl = new SortableTable("tbl",...).
//
// param liveSort - boolean - indicates whether or not to allow the user to sort the table; requires that the table
// has a header; if not specified, default is false
//
// param numHeaderRows - (optional) int - the number of header rows; if not specified, will try to be determined using 
// the THEAD TABLE property 
//
// param numFooterRows - (optional) int - the number of footer rows; if not specified, will try to be determined using
// the TFOOT TABLE property
//*
function SortableTable(id,liveSort,numHeaderRows,numFooterRows) {
    this._id = id;

    this._numHeaderRows = numHeaderRows;
    if (isNaN(numHeaderRows+0)) this._numHeaderRows = 0;
    
    this._numFooterRows = numFooterRows;
    if (isNaN(numFooterRows+0)) this._numFooterRows = 0;

    table = document.getElementById(id);
    //some checking
    if (table == null) return;
    if (table.tagName != "TABLE") return;

    //if the header and footer rows were not specified, see if they exist anyway
    if (this._numHeaderRows < 1 && table.tHead != null) {
        if (table.tHead.rows.length > 0) this._numHeaderRows = table.tHead.rows.length;
    }
    if (this._numFooterRows < 1 && table.tFoot != null) {
        if (table.tFoot.rows.length > 0) this._numFooterRows = table.tFoot.rows.length;
    }

    this._table = table;

    //for now assume all rows are sorted
    this._startRow = this._numHeaderRows;
    this._stopRow = table.rows.length - this._numFooterRows - 1;
    
    //save the cell contents in an array; will be used to rewrite the table later
    var numRows = table.rows.length - this._numHeaderRows - this._numFooterRows;
    if (numRows <= 0) return; //no valid rows, so just return

    //contains the plain text in a cell; this is what's sorted
    var txtArray = new Array(numRows);
    //contains the full cell contents including html tags; this is used to rewrite the table
    var contentArray = new Array(numRows);
    var rowIdx = 0;
    var numCells = table.rows[this._startRow].cells.length;
    for (i = this._startRow; i <= this._stopRow; i++) {
        var curRow = new Array(numCells);
        var contentRow = new Array(numCells);
        for (j = 0; j < numCells; j++) {
            var c = table.rows[i].cells[j];
            if (c == null || !c) continue;

            //copy the exact cell contents
            contentRow[j] = c.innerHTML;
            //copy just the plain text
            txt = this.getCellText(c);
            curRow[j] = txt;
        }
        txtArray[rowIdx] = curRow;
        contentArray[rowIdx] = contentRow;
        rowIdx++;
    }
    this._txtArray = txtArray;
    this._contentArray = contentArray;

    if (liveSort) {
        //can only add buttons to the last header row, so a header has to exist
        if (this._numHeaderRows >= 1) this.addSortButtons();
    }
}

//use this to add anonymous functions as methods
var _sortableTable = SortableTable;

//**
//NOTE: assumes that the variable representing the table has the same name as the table id.
//
// Adds a sort indicator in the last header row. If the row has fewer cells than
// the data rows, the indicator will act on the left most column in the group.
//
// If there are no header rows, just return.
//*
_sortableTable.prototype.addSortButtons = function() {
    //add a "sort me" indicator in the header row
    var table = this._table;
    var lastHdrRow = this._numHeaderRows - 1;
    if (lastHdrRow < 0) return;

    var ncols = table.rows[lastHdrRow].cells.length;
    var id = table.id;
    var i;
    var dataColIdx = 0;
    for (i = 0; i < ncols; i++) {
        var content = table.rows[lastHdrRow].cells[i].innerHTML;
        var s = '<br><a name="' + id + '"></a><a href="#' + table.id + '" onClick="javascript:';
        s += id + '.sort(' + dataColIdx + ',\'d\',\'a\')" title="sort down">&lt;</a>&nbsp;';
        s += '<a href="#' + table.id + '" onClick="javascript:';
        s += id + '.sort(' + dataColIdx +  ',\'u\',\'a\')" title="sort up">&gt;</a>';
        table.rows[lastHdrRow].cells[i].innerHTML += s;
        //make sure that if a header cell spans columns that we keep in sync with the data rows
        dataColIdx += table.rows[lastHdrRow].cells[i].colSpan;
    }
}

// return just the text contained in a node; in other words, do not return any markup
_sortableTable.prototype.getCellText = function (node) {
    var cn = node.childNodes;
    var txt = "";
    if (cn.length > 0) {
        var i;
        for (i = 0; i < cn.length; i++) {
            if (cn[i].nodeType == 3) {
                txt += cn[i].nodeValue;
            } else {
                //txt += SortableTable_getText(cn[i]);
                txt += this.getCellText(cn[i]);
            }
        }
    } else {
        if (node.nodeType == 3) {
            //if (node.nodeValue == null) return("");
            return(node.nodeValue);
        } else {
            return("");
        }
    }
    return(txt);
}

//**
// Determine the column type by looking for the first non-empty cell in the column,
// then checking to see if the entry is a date first, then a number. If it is
// neither, it is assumed to be a string.
//
// param colNum - integer, 0 to max col num
//
// return character representing type; s = string, d = date, n = number. If
// colNum is out of the appropriate range, returns s.
//*
_sortableTable.prototype.getColumnType = function(colNum) {
    if (colNum < 0) return('s');

    var txtArray = this._txtArray;
    if (txtArray == null || txtArray.length <= 0) return('s');

    //all txtArray rows should be the same length
    if (colNum >= this._txtArray[0].length) return('s');

    //try to find the first non-empty cell in the column
    var i = 0;
    while (i < txtArray.length) {
        var txt = txtArray[i][colNum];
        if (!txt.match(/^[ \t]+$/)) break;
        i++;
    }
    if (i >= txtArray.length) i--;

    var txt = txtArray[i][colNum];
    if (this.isDate(txt)) return('d');
    if (!isNaN(txt)) return('n');

    return('s'); //assume it is a string
}

//**
// Sort the column based on colType. If dir is not specified,
// sort ascending. If colType is not specified, assume string.
// colNum is a 0 based index. If colNum < 0 or > number of columns,
// just return.
//
// param colNum - the number of the column with which the table is sorted
//
// param dir - either 'u' for ascending (up) or 'd' for descending
//
// param colType - string - one of 'a' for auto (the code
// tries to determine the column type based on the first non-empty cell
// in the column), 'd' for date, 'n' for number. Anything else
// results in the column being treated like string data.
// 
// return - an array with the new sort indeces
//*
_sortableTable.prototype.sort = function(colNum,dir,colType) {
    //make the array to sort
    var txtArray = this._txtArray;
    if (txtArray == null || txtArray.length <= 0) return(new Array());

    if (colNum < 0 || colNum >= txtArray[0].length) return;

    var arrayLen = txtArray.length;

    var sortArray = new Array(arrayLen);
    for (i = 0; i < arrayLen; i++) {
        elt = new Array(2);
        elt[0] = i;
        //get the text value of the cell
        elt[1] = txtArray[i][colNum];
        sortArray[i] = elt;
    }

    if (colType == 'a') {
        //automatic - figure it out
        colType = this.getColumnType(colNum);
    } 

    if (colType == 'n') {
        sortArray.sort(SortableTable_compareNumbers);
    } else if (colType == 'd') {
        sortArray.sort(SortableTable_compareDates);
    } else {
        //native sorting
        sortArray.sort(SortableTable_compare);
    }
    if (dir == "d") sortArray.reverse();
    
    var retArray = new Array(arrayLen);
    for (i = 0; i < arrayLen; i++) {
        var v = sortArray[i][0];
        retArray[i] = v;
    }

    //redraw the table
    var numColumns = txtArray[0].length;
    var contentArray = this._contentArray;
    for (i = this._startRow; i <= this._stopRow; i++) {
        for (j = 0; j < numColumns; j++) {
            var newRowNum = retArray[i-this._startRow];
            var v = contentArray[newRowNum][j];
            var c = this._table.rows[i].cells[j];
            if (c == null || !c) continue;

            c.innerHTML = v;
        }
    }

    return(retArray);
}

//**
// The default compare function.
// v1, v2 are arrays. The second items (i.e., v1[1] and v1[2]) determine sort order.
//*
function SortableTable_compare(v1,v2) {
    if (v1[1] > v2[1]) return(1);
    if (v1[1] < v2[1]) return(-1);
    return(0);
}

function SortableTable_compareNumbers(v1,v2) {
    var n1 = Number(v1[1]);
    var n2 = Number(v2[1]);
    if (isNaN(n1) && isNaN(n2)) return(0);
    if (isNaN(n2)) return(1);
    if (isNaN(n1)) return(-1);

    if (n1 > n2) return(1);
    if (n1 < n2) return(-1);
    return(0);
}

function SortableTable_compareDates(v1,v2) {
    var d = SortableTable_dateStrToJSDate(v1[1]);
    var n1 = Date.parse(d);

    d = SortableTable_dateStrToJSDate(v2[1]);
    var n2 = Date.parse(d);
    if (isNaN(n1) && isNaN(n2)) return(0);
    if (isNaN(n2)) return(1);
    if (isNaN(n1)) return(-1);

    if (n1 > n2) return(1);
    if (n1 < n2) return(-1);
    return(0);
}

//**
// Accepts dates in any format allowed by JavaScript. Dashes may be
// included but will be removed before processing.
//*
_sortableTable.prototype.isDate = function(v) {
    var d;
    d = SortableTable_dateStrToJSDate(v);

    d = Date.parse(d);
    if (isNaN(d)) {
        return(false);
    }

    return(true);
}

//**
// Do a little massaging to get things that are supposed to be
// dates into a form that JavaScript recognizes. Currently just
// removes dashes.
//*
function SortableTable_dateStrToJSDate(v) {
    var d;
    var s = v.toString();
    if (s.indexOf("-") >= 0) {
        var da = s.split('-'); //if the date contains a dash, just remove it
        d = da.join(' ');
    } else {
        d = v;
    }
    return(d);
}
