///////////////////////////////////////////////////////////////////////////////
//
//  SuperTable is an Object-Oriented Javascript API for dynamically 
//  manipulating HTML tables. 
//
//  Copyright (C) 2003  John Aguinaldo
//
//
//
//  This library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//  
//  This library 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
//  Lesser General Public License for more details.
//  
//  You should have received a copy of the GNU Lesser General Public
//  License along with this library; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
//
//
//  IF YOU ARE NOT FAMILIAR with the terms of this license, please take
//  the time to read it at the URL below before utilizing it:
//      www.gnu.org/licenses/lgpl.txt 
//
//  If you feel that this license is too restrictive for your use, then 
//  please contact me to discuss what you are trying to accomplish.
//  
//  
//  PROJECT AND CONTACT information are posted at: 
//      http://supertable.sourceforge.net
//
//
//  THIS HEADER INCLUDING COPYRIGHT MUST REMAIN INTACT.
//
///////////////////////////////////////////////////////////////////////////////
//
//  Release 0.5  $Revision: 1.1 $
//



//=============================================================================
//
//  TABLE CLASS
//
//=============================================================================
function Table( psTbodyId )
{
    // INSTANCE PROPERTIES
    this.tbody_array = []; 
    this.lastSortDirection = 0;  // Track last sort direction
    this.tbody_objref = document.getElementById( psTbodyId );
    this.columns = [];

    // DEFINE GETTERS & SETTERS
    Table.prototype.getSortDirection = function() {
        return this.lastSortDirection;
    }
    Table.prototype.setSortDirection = function(pnDir) {
        this.lastSortDirection = pnDir;
    }

    // INSTANCE METHODS
    this.setAlphaSortLimit = function(limit) { Array.prototype.limit = limit; }

    // SHARED METHODS
    Table.prototype.defineColumn = defineColumn;
    Table.prototype.array2table = array2table;
    Table.prototype.table2array = table2array;
    Table.prototype.sortTable = sortTable;
    Table.prototype.filterTable = filterTable;
    Table.prototype.deleteRows = deleteRows;
    Table.prototype.autoNumberSeq = autoNumberSeq;
    Table.prototype.hyphenLongWords = hyphenLongWords;
    Table.prototype.cellRowProcessor = cellRowProcessor;
    Table.prototype.removeStyle = removeStyle;

    // MAIN
    this.setAlphaSortLimit(15);  // Set default character limit for alpha sorts
}


//=====================================
//  TABLE CLASS METHODS
//=====================================
//

///////////////////////////////////////
//
// Remove styles from all tbody rows/cells (useful for printing)
//
function removeStyle()
{
    // If IE, then use 'className' to reference style class attribute
    var classSyntax = (document.all) ? 'className' : 'class';

    var oTrows = this.tbody_objref.rows;
    for(cRow=0; cRow<oTrows.length; cRow++)
    {
        // Clear TR attribute 'class'
        oTrows.item(cRow).setAttribute(classSyntax,'');

        var oTcells = oTrows.item(cRow).cells;
        for(cCell=0; cCell<oTcells.length; cCell++)
        {
            // Clear TD attribute 'class'
            oTcells.item(cCell).setAttribute(classSyntax,'');
        }
    }
}


///////////////////////////////////////
//
// Parse tbody to an array
//
function table2array()
{
    var trows = this.tbody_objref.rows;
    for(cRows=0; cRows<trows.length; cRows++)
    {
        var tcells = trows[cRows].cells;
        this.tbody_array[cRows] = new Array();
        for(cCells=0; cCells<tcells.length; cCells++)
        {
            // Load contents of HTML table cell into Array
            this.tbody_array[cRows][cCells] = tcells[cCells].innerHTML;
        }
    }
}


///////////////////////////////////////
//
// Define a column in the columns[] array of Table
//
function defineColumn( index,poSortFunction )
{
    this.columns[index] = new Column( poSortFunction );
}


///////////////////////////////////////
//
// Convert tbody to array, sort array, delete tbody rows,
// then load tbody with sorted array.
//
function sortTable(index)
{
    // If tbody array already exists, then don't recreate it.
    if( !this.tbody_array.length ) { this.table2array() }

    // Detect sort function...
    switch ( this.columns[index].getSortFunction() )
    {
        case 'numeric':
            this.tbody_array.mergeSort( index, 'numeric' );
            break;
        case 'alpha':
            this.tbody_array.mergeSort( index, 'alpha' );
            break;
        case null:
            return;
        default:
            this.tbody_array.sort( this.columns[index].getSortFunction() );
    }
    this.deleteRows();
    this.array2table();
}


///////////////////////////////////////
//
// Delete tbody rows, then rebuild from table array.
// New table is automatically filtered upon rebuild.
//
function filterTable()
{
    // If tbody array already exists, then don't recreate it.
    if( !this.tbody_array.length ) { this.table2array() }

    this.deleteRows();
    this.array2table();
}


///////////////////////////////////////
//
// Delete all rows of tbody
//
function deleteRows()
{
    // delete all rows of table
    while (this.tbody_objref.rows.length > 0)
    {
        this.tbody_objref.deleteRow(0);
    }
}


///////////////////////////////////////
//
// Fill each row of column pnIndex with numeric series from 1 to n.
// This provides automatic sequence numbering.
//
function autoNumberSeq( pnIndex )
{
    pnIndex = (pnIndex) ? pnIndex : 0;
    var aTrows = this.tbody_objref.rows;
    for(cRow=0; cRow<aTrows.length; cRow++)
    {
        aTrows[cRow].cells[pnIndex].innerHTML = cRow+1;
    }
}


///////////////////////////////////////
//
// Break strings matching pattern defined by hyphenRegex using delimiter 
// defined by hyphenStr.
// This prevents long continuous strings from stretching a table cell
// (i.e. long URLs and long directory paths...)
//
function hyphenLongWords( sCell_text,nRow,nCell )
{
    //var sCell_text = this.tbody_array[nRow][nCell];
    if( this.columns[nCell].getHyphenLimit() )
    {
        var oRegex = this.columns[nCell].getHyphenRegex();
        var sHyphenStr = this.columns[nCell].getHyphenStr();
        sCell_text = sCell_text.replace( oRegex, "$1" + sHyphenStr );
        return sCell_text;
    }
    else
    {
        return sCell_text;
    }
}


///////////////////////////////////////
//
// User-defined method gives user access to internal variables of array2table
// allowing the user to intercept, test, and modify cell contents prior to 
// display.
//
// Global variables oNew_row & oNew_cell are also available...
// ...we were not able to pass them in explicitly otherwise
// we would overwrite them.  They can be used to set HTML
// attributes and apply CSS styles using example:
//     oNew_row.setAttribute('class','class-definition');
//
function cellRowProcessor( sCell_text,nTbody_array_row,nTbody_array_cell )
{
    return sCell_text;
}


///////////////////////////////////////
//
// Convert 2-dimensional array into HTML <tbody>
//
function array2table(psTbody_id,paTable)
{
    // TODO: createTextNode() below is causing embedded html tags
    //       to be encoded as &xxx characters..  How to stop this?
    //       Maybe use createDocumentFragment??


    // Set regex
    for( i in this.columns )
    {
        if( !this.columns[i].filter ) { continue }

        this.columns[i].filter.setRegex( this.columns[i].filter.getFormFldVal() );
    }

    //Iterate through each array element - row, then cell
    rowLoop:
    for( cRow=0; cRow<this.tbody_array.length; cRow++ )
    {
        // Filter criteria block..  If filter terms are detected, 
        // then skip block
        for( i in this.columns )
        {
            if( this.columns[i].filter )
            {
                if( this.tbody_array[cRow][i].search( this.columns[i].filter.getRegex() ) < 0 )
                { continue rowLoop; }
            }
        }

        // Creates an element whose tag name is TR
        //    Must be Global variable...  used by Style.apply()
        oNew_row=document.createElement("TR");

        cellLoop:
        for(cCell=0; cCell<this.tbody_array[cRow].length; cCell++)
        {
            // Get cell contents from array
            sCell_text = this.tbody_array[cRow][cCell];


            // Creates a TD element
            //    Must be Global variable...  used by Style.apply()
            oNew_cell=document.createElement("TD");

            // Creates a Text Node using array contents
            var sNew_text=document.createTextNode( '' );

            // Color rows based on priority...
            this.columns[cCell].style.apply( sCell_text );

            // SPLIT LONG WORDS
            sCell_text = this.hyphenLongWords( sCell_text,cRow,cCell );

            // PROCESS USER DEFINED FUNCTIONS (IF DEFINED)
            sCell_text = this.cellRowProcessor( sCell_text,cRow,cCell );

            // Load array data into cell via innerHTML (instead of previous method createTextNode)
            oNew_cell.innerHTML = sCell_text;

            // APPENDS the Text Node we created into the cell TD
            oNew_cell.appendChild(sNew_text);

            // APPENDS the cell TD into the row TR
            oNew_row.appendChild(oNew_cell);

        }
        // Appends the row TR into TBODY
        this.tbody_objref.appendChild(oNew_row);
    }
}




//=============================================================================
//
//  COLUMN CLASS
//
//=============================================================================
function Column( poSortFunction )
{
    // INSTANCE PROPERTIES
    this.hyphenLimit = 0;
    this.hyphenStr = '- ';
    this.hyphenRegex = null;
    this.filter = null;
    this.sortFunction = poSortFunction;
    this.style = new Style(); // Instantiate style object to contain "rules"

    

    // DEFINE GETTERS & SETTERS
    Column.prototype.getHyphenLimit = function() {
        return this.hyphenLimit;
    }
    Column.prototype.setHyphenLimit = function(length) {
        this.hyphenLimit = length;
        this.hyphenRegex = eval('/([\\w:\\/.?&-_]{' + length + '})/');
    }
    Column.prototype.getHyphenStr = function() {
        return this.hyphenStr;
    }
    Column.prototype.setHyphenStr = function(psHyphenStr) {
        this.hyphenStr = psHyphenStr;
    }
    Column.prototype.getHyphenRegex = function() {
        return this.hyphenRegex;
    }
    Column.prototype.setHyphenRegex = function(re) {
        this.hyphenRegex = re;
    }
    Column.prototype.getSortFunction = function(){
        return this.sortFunction;
    }
    Column.prototype.setSortFunction = function(poFunction){
        this.sortFunction = poFunction;
    }

    // SHARED METHODS
    Column.prototype.defineFilter = function( p_sFormName,p_sFormFieldName ) {
        this.filter = new Filter( p_sFormName,p_sFormFieldName );
    }
    Column.prototype.hideColumn = function(index){
        //To be written 
    }
}




//=============================================================================
//
//  STYLE CLASS
//
//=============================================================================
function Style()
{
    // INSTANCE PROPERTIES
    this.rules = [];

    // SHARED METHODS
    Style.prototype.defineRule = function( regex,css,applyTo ) {
        this.rules[this.rules.length] = new Rule( regex,css,applyTo );
    }
    Style.prototype.apply = apply;
}


//=====================================
//  STYLE CLASS METHODS
//=====================================
//

///////////////////////////////////////
//
// Iterate through defined style rules for column and apply CSS
// if contents of column cell match regex pattern.
//
function apply( sCell_text )
{
    // If IE, then use 'className' to reference style class attribute
    var classSyntax = (document.all) ? 'className' : 'class';

    for( i=0; i<this.rules.length; i++ )
    {
        if( sCell_text.search( this.rules[i].getRegex() ) != -1 )
        {
            switch( this.rules[i].getApplyTo() )
            {
                case 'row':
                    oNew_row.setAttribute( classSyntax,this.rules[i].getCss() );
                    return;
                case 'cell':
                    oNew_cell.setAttribute( classSyntax,this.rules[i].getCss() );
                    return;
                default:
                    return;
            }
        }
    }
}




//=============================================================================
//
//  RULE CLASS
//
//=============================================================================
function Rule( regex,css,applyTo )
{
    // INSTANCE PROPERTIES
    this.regex = regex;
    this.css = css;
    this.applyTo = applyTo;

    // DEFINE GETTERS & SETTERS
    Rule.prototype.getRegex = function() {
        return this.regex;
    }
    Rule.prototype.setRegex = function(poRe) {
        this.regex = poRe;
    }
    Rule.prototype.getCss = function() {
        return this.css;
    }
    Rule.prototype.setCss = function(psCss) {
        this.css = psCss;
    }
    Rule.prototype.getApplyTo = function() {
        return this.applyTo;
    }
    Rule.prototype.setApplyTo = function(psTarget) {
        this.applyTo = psTarget;
    }
}




//=============================================================================
//
//  FILTER CLASS
//
//=============================================================================
function Filter( psFormName,psFormFieldName )
{
    // INSTANCE PROPERTIES
    this.form = document.forms[psFormName];
    this.formField = this.form[psFormFieldName];
    this.regex = '';

    // DEFINE GETTERS & SETTERS
    Filter.prototype.getRegex = function() {
        return this.regex;
    }
    Filter.prototype.setRegex = function(psRe) {
        this.regex = new RegExp( psRe,"gi" );
    }
    Filter.prototype.getFormFldVal = function() {
        // Set regex to .* if form field is null
        if( this.formField.value == '' ) {
            return '.*';
        }
        else {
            return this.formField.value;
        }
    }
}




//=============================================================================
//
//  MISCELLANEOUS FUNCTIONS
//
//=============================================================================

//
// This function is helpful when sorting an array of varied size
// text strings.  The function trims off the first few characters
// (as defined by 'pnLimit') of the text string 'psText' and then
// converts each character into a three-digit numeric code.
// This allows the string text to be accurately compared and
// sorted.
//
function text2Number(psText,pnLimit,psType)
{
    // If psType = 'numeric' then parseInt and return
    if( psType == 'numeric' )
    {
        return parseInt(psText);
    }

    // Strip leading space and convert to lower case
    psText = psText.replace(/^(\s|&nbsp;)*/,'');
    psText.toLowerCase();

    // Instantiate as string to accumulate number
    var sCodeString = '';
    // Capture first [pnLimit] characters of psText
    for(i=0; i<pnLimit; i++)
    {
        // Convert character to number
        var nThis_code = ( psText.charCodeAt(i) )? '' + psText.charCodeAt(i) : '000';
        // Append leading zero if only two digits
        if ( nThis_code.length == 2 ){ nThis_code = '0' + nThis_code }
        // Append current code to accumulated string
        sCodeString += nThis_code;
    }
    // Convert to number and get absolute value
    var nCodeString = 0 - sCodeString;
    (nCodeString < 0) ? nCodeString = -nCodeString : nCodeString = nCodeString;
    return nCodeString;
}




//
//  CUSTOM ARRAY OBJECT METHOD FOR SORTING NUMBERS AND STRINGS
//
//  REQUIRES: 
//
//      text2Number()
//      Array.limit
//      
Array.prototype.mergeSort = function(col,type,start,end)
{
    // If this is the first call (not a recursion)
    if( arguments.length == 2 )
    {
        start = 0;
        end = this.length;
    }

    if( end-start > 1 )
    {
      var mid=parseInt( (start+end)/2 );
      this.mergeSort(col,type,start,mid);
      this.mergeSort(col,type,mid,end);

      var tmp=[];
      var start2=start;
      var mid2=mid;
      for(var i=0; i<end-start; ++i)
      {
        if     ( start2>=mid ) { tmp[i] = this[mid2++] }
        else if( mid2>=end ) { tmp[i] = this[start2++] }
        else if( text2Number( this[start2][col],this.limit,type ) <= 
                    text2Number( this[mid2][col],this.limit,type ) )
                           { tmp[i] = this[start2++] }
        else               { tmp[i] = this[mid2++] }
      }
      for(i=0; i<end-start; ++i)
      {
        this[i+start]=tmp[i];
      }
    }
}


//__END__
