/*
  DESCRIPTION
  
  SortTable
  
  Version 1.1.0
  4th August 2007
  Original Version:
  Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
  Modified by:
  Stuart Breingan, smb17 'at' cs.st-andrews.ac.uk
 
  Instructions:
  Download this file
  Add <script src="sorttable.js"></script> to your HTML
  Add class="sorttable" to any table you'd like to make sortable
  Click on the headers to sort
  
  For further documentation please see:
  http://www-old.cs.st-andrews.ac.uk/wiki/index.php?title=How_to_edit_school_web_pages#Tables  
  
  Thanks to many, many people for contributions and suggestions.
  Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
  This basically means: do what you want with it.
  
   
*/

/*HISTORY

 * version 4.1.0 SM Breingan
 * Changed the date sorting so that ISO dates are supported (e.g 'Jan' rather than 01).
 *
 * version 4.0.0   SM Breingan
 * Added the ability to define "reverse_load" class to the leftmost column
 *
 * version 3.0.0   SM Breingan
 * Fixed a bug whereby reversing a column now no longer also reverses elements which are equal, hence adjacent columns now remain ordered correctly
 * 
 * version 2.0.0   SM Breingan
 * Modified the alpha sort to convert first to lower case
 * 
 * version 1.0.0    SM Breingan
 * Original version for use on CS Website, created by smb17
 * 
 */



/*global variable - check if browser is IE (conditional code in comment )*/ 
var stIsIE = /*@cc_on!@*/false;



/* ******************************************************************
 * 
 * CLASS:  Sorttable
 * 
 * Defines a class of functions for transforming a standard XHTML table
 * into one with the ability to sort by header
 * 
 * 
 * ****************************************************************** */

sorttable = {
		
	
		
/* *******************************************
 * 
 * DOM MANIPULATING FUNCTIONS
 * Change the table objects in document model
 * 	
 * ******************************************* */
   
  /*
   *  Initialising function for defining class variables and obtaining table
   * */
   init: function() {
  	
    // quit if this function has already been called
    if (arguments.callee.done) return;
    // flag this function so we don't do the same thing twice
    arguments.callee.done = true;
    // kill the timer, stop previous methods calls that are still loading
    if (_timer) clearInterval(_timer);
    //check the browser and document is valid
    if (!document.createElement || !document.getElementsByTagName) return;
    
   
   	sorttable.DATE_ISO = /^(\d\d)[\/\.-](\w\w\w)[\/\.-](\d\d\d\d)$/;
	sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
    //store unicode for up and down triangle
    sorttable.uptriangle = '&nbsp;&#x25b2;';
    sorttable.downtriangle = '&nbsp;&#x25bC;';
    //store a reference to the first column (to be sorted) in table, and an array of sorted elements
    sorttable.firstColArray;
    sorttable.firstColumn;
    
    forEach(document.getElementsByTagName('table'), function(table) {
      if (table.className.search(/\bsorttable\b/) != -1) {
        sorttable.makeSortable(table);
      }
    });
    
  },
  
  
  /*
   *  Function for making tables sortable by adding clickable headers, and creating the unicode arrows 
   * */
makeSortable: function(table) {
	
	
    if (table.getElementsByTagName('thead').length == 0) {
      // table doesn't have a tHead. Since it should have, create one and
      // put the first table row in it.
      the = document.createElement('thead');
      the.appendChild(table.rows[0]);
      table.insertBefore(the,table.firstChild);
    }
    // Safari doesn't support table.tHead
    if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
    if (table.tHead.rows.length != 1) return; // can't cope with two header rows
    
    
    // work through each column and calculate its type
    // and store a variable for the first column in the table that is sortable 
    headrow = table.tHead.rows[0].cells;
  	isFirstCol = true;
    firstrowNum = -1;
    sortfirstNum = -1;
  	
    for (var i=0; i<headrow.length; i++) {
      // manually override the type with a sorttable_type attribute  
      if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col
      
      // for each header create a unicode arrow, but of visibility hidden
      // hence when arrows are added the table does not change size
      
      arrowEl  = document.createElement('span');
      arrowEl.innerHTML = this.uptriangle;
      arrowEl.style.visibility = 'hidden';
      arrowEl.id = "arrow";
      
      //store the first column and add mouseover events to headers
      if(isFirstCol){
	  	isFirstCol = false;
	  	firstrowNum = i;
                sortfirstNum = i;
      }
      if(headrow[i].className.match(/\bsort_first\b/)){
        sortfirstNum = i;
      }
    	headrow[i].appendChild(arrowEl);
    	 	dean_addEvent(headrow[i], "mouseover", function(e){
      		this.style.cursor = 'pointer';
      });
      
      //add the type of sort to each header and add the click events
    	firstRow = firstrowNum;
        reverseLoad = false;
        mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
        mtch2 = headrow[firstrowNum].className.match(/\breverse_load\b/);
        if(mtch2) {reverseLoad = true;}
        if (mtch) { override = mtch[1]; }
	      if (mtch && typeof sorttable["sort_"+override] == 'function') {
	        headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
	      } else {
	        headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);
	      }
	      
	      headrow[i].sorttable_columnindex = i;
	      headrow[i].sorttable_tbody = table.tBodies[0];
	      dean_addEvent(headrow[i],"click", this.eventHandler );
	    }
    }
    
    //automatically sort this table by the first column on load
    firstColumn = headrow[sortfirstNum];
    leftmostColumn = headrow[firstrowNum];
    sortColArray = this.sortFirstRow(firstColumn);
    this.sortColumn(leftmostColumn, false);
    if(reverseLoad){
      this.sortColumn(leftmostColumn, false);
    }
    variable = true;
  },
  
  /*
   * Called by the click event on a header, this calls sortColumn where 'this' refers to the 
   * column header that was clicked
   */
	eventHandler: function(){
	sorttable.sortColumn(this, false);
	},

  /*
   * Stores a sorted array of the first sortable column. This is used to order elements
   * of equal value
   */
	sortFirstRow : function(column){
	
	first_array = [];        
	        first_col = firstColumn.sorttable_columnindex;
	        first_rows = firstColumn.sorttable_tbody.rows;
	        for (var j=0; j<first_rows.length; j++) {
	          first_array[first_array.length] = [sorttable.getInnerText(first_rows[j].cells[first_col]), first_rows[j]];
	        }
    first_array.sort(firstColumn.sorttable_sortfunction);
	return first_array;
	
	},
  
 /*
  * Method for sorting a column of the table, called on load and on mouse click.
  * Boolean isFirst determines if this is the first call on page load and prevents the first
  * column being sorted twice
  */ 
 	sortColumn:  function(column, isFirst) {
 		
 		//if we're already sorted by this column, just 
        //reverse the table, which is quicker*/
 		if (column.className.search(/\bsorttable_sorted\b/) != -1) {
		  sorttable.reverse(column);
            column.className = column.className.replace('sorttable_sorted',
                                                    'sorttable_sorted_reverse');
			//currArrow refers to the arrow element of the currently sorted column                                                    
            oldArrow = document.getElementById('currArrow');
            
        //update arrow visibility, display new arrow 
	    if(undefined != oldArrow) {
	    oldArrow.style.visibility = 'hidden';
	    oldArrow.id = 'arrow';
	    }
	    arrowEl = column.getElementsByTagName('span')[0];
	    arrowEl.style.visibility = 'visible';
	    arrowEl.innerHTML = this.downtriangle;
	    arrowEl.id = "currArrow";
	     return;
        }
        
        // if we're already sorted by this column in reverse, just 
        // re-reverse the table, which is quicker, and update arrows
          if (column.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
            sorttable.reverse(column);
            column.className = column.className.replace('sorttable_sorted_reverse',
                                                    'sorttable_sorted');
            oldArrow = document.getElementById('currArrow');
	    if(undefined != oldArrow) {
	    oldArrow.style.visibility = 'hidden';
	    oldArrow.id = 'arrow';
	    }
	    arrowEl = column.getElementsByTagName('span')[0];
	    arrowEl.style.visibility = 'visible';
	    arrowEl.innerHTML = this.uptriangle;
	    arrowEl.id = "currArrow";
	       return;
       }
          
       //if column hasn't been sorted, remove other sorted classes and arrows
	   theadrow = column.parentNode;
       forEach(theadrow.childNodes, function(cell) {
            if (cell.nodeType == 1) { // an element
              cell.className = cell.className.replace('sorttable_sorted_reverse','');
              cell.className = cell.className.replace('sorttable_sorted','');
            }
          });
        sortfwdind = document.getElementById('sorttable_sortfwdind');
        oldArrow = document.getElementById('currArrow');
	
	    if(undefined != oldArrow) {
	    oldArrow.style.visibility = 'hidden';
	    oldArrow.id = 'arrow';
	    }
        
        //view the arrow
         column.className += ' sorttable_sorted';
		 arrowEl = column.getElementsByTagName('span')[0];
	     arrowEl.style.visibility = 'visible';
	     arrowEl.innerHTML = this.uptriangle;
	     arrowEl.id = 'currArrow';
	     
	     
	     	//get the sorted array of first column, and sort accordingly
	      	tb = firstColumn.sorttable_tbody;
	        for (var j=0; j<sortColArray.length; j++) {
		 	 currFirstRow = sortColArray[j][1];
		  	 tb.appendChild(currFirstRow);	
	        }
 		        
	        //build an array for sorting, as this only calls getInnerText once     
	        row_array = [];	        	                
	        col = column.sorttable_columnindex;
	        rows = column.sorttable_tbody.rows;
	        
	        for (var j=0; j<rows.length; j++) {
	          row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
	        }
	      
	  
	        //Perform shaker sort (preserve ordering) 	        
	          sorttable.shaker_sort(row_array, column.sorttable_sortfunction);	                
	        /*Uncoment this row for ordinary sort (no preservation)*/
	        //row_array.sort(column.sorttable_sortfunction);
	        
	        tb = column.sorttable_tbody;
			for (var j=0; j<row_array.length; j++) {
		 	 currRow = row_array[j][1];
		 	 
		  //Change the classes of the rows when appending to table, so that alternate rows are coloured
		  if(j % 2 == 0) {
		    currRow.className = 'row_data_0';
		  } else {
		    currRow.className = 'row_data_1'; 
		  }
           tb.appendChild(currRow);
	      }    
	        delete row_array;
      },
  
    
  /*
   * Function for identifying the type of content in column, based on first non-blank row
   * and updating the class of the column accordingly 
   */
  guessType: function(table, column) {
    
    sortfn = sorttable.sort_alpha;
    for (var i=0; i<table.tBodies[0].rows.length; i++) {
      text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
      if (text != '') {
        if (text.match(/^-?[?$?]?[\d,.]+%?$/)) {
          return sorttable.sort_numeric;
        }
        if(text.match(sorttable.DATE_ISO)){
        	return sorttable.sort_iso;
        }
        
        // check for a date: dd/mm/yyyy or dd/mm/yy 
        // can have / or . or - as separator
        // can be mm/dd as well
        possdate = text.match(sorttable.DATE_RE)
		
        if (possdate) {
          // looks like a date
          first = parseInt(possdate[1]);
          second = parseInt(possdate[2]);
          if (first > 12) {
            // definitely dd/mm
            return sorttable.sort_ddmm;
          } else if (second > 12) {
            return sorttable.sort_mmdd;
          } else {
            // looks like a date, but we can't tell which, so assume
            // that it's dd/mm (English imperialism!) and keep looking
            sortfn = sorttable.sort_ddmm;
          }
        }
      }
    }
    return sortfn;
  },
  
  
   /* 
    * Gets the text we want to use for sorting for a cell.
    * strips leading and trailing whitespace.
    * this is *not* a generic getInnerText function; it's special to sorttable.
    * for example, you can override the cell text with a customkey attribute.
    * it also gets .value for <input> fields.
    */
  getInnerText: function(node) {
   


    hasInputs = (typeof node.getElementsByTagName == 'function') &&
                 node.getElementsByTagName('input').length;
    
    if (node.getAttribute("sorttable_customkey") != null) {
      return node.getAttribute("sorttable_customkey");
    }
    else if (typeof node.textContent != 'undefined' && !hasInputs) {
    	 return node.textContent.replace(/^\s+|\s+$/g, '');
    }
    else if (typeof node.innerText != 'undefined' && !hasInputs) {
      return node.innerText.replace(/^\s+|\s+$/g, '');
    }
    else if (typeof node.text != 'undefined' && !hasInputs) {
      return node.text.replace(/^\s+|\s+$/g, '');
    }
    else {
      switch (node.nodeType) {
        case 3:
          if (node.nodeName.toLowerCase() == 'input') {
            return node.value.replace(/^\s+|\s+$/g, '');
          }
        case 4:
          return node.nodeValue.replace(/^\s+|\s+$/g, '');
          break;
        case 1:
        case 11:
          
          var innerText = '';
          var bool = true;
          for (var i = 0; i < node.childNodes.length; i++) {
          
           	  innerText += sorttable.getInnerText(node.childNodes[i]);
                  	
          }
          return innerText.replace(/^\s+|\s+$/g, '');
          
          break;
        default:
          return '';
      }
    }
  },
  /*
   *  Reverses the rows in a tbody 
   */
  reverse: function(column) {
    tbody = column.sorttable_tbody;
    colindex = column.sorttable_columnindex;
    newrows = [];
    multiple = new Array(tbody.rows.length);
    //reverse the equal elements
    var count = 0;
    var store =  sorttable.getInnerText(tbody.rows[0].cells[colindex]);
    for(var i=1; i<tbody.rows.length; i++){
      var content = sorttable.getInnerText(tbody.rows[i].cells[colindex]);
      if(content != store){
	multiple[count] = new Array(store, i-1);
        count++;	
      }
      store = content;
    }
    
    multiple[count] = new Array(store, i-1);
    multiple = multiple.slice(0, count+1);
  
    var count = 0;
    for (var i=0; i<tbody.rows.length; i++) { 
      var store =  sorttable.getInnerText(tbody.rows[i].cells[colindex]);
      if(store == multiple[count][0] && multiple[count][1] != i){
	var j = multiple[count][1];	      
        while(j >= i) {
	    newrows[newrows.length] = tbody.rows[j];
            if(i == 225){
	    }
            j--
	    }
        i = multiple[count][1];
    
        count++;
	}
     	else{
      newrows[newrows.length] = tbody.rows[i];
      count++;
  
      }
    
    }

    for (var i=newrows.length-1; i>=0; i--) {
         
      if(newrows[i] == undefined){
	window.alert("at number "+i);
      }  
      if (i % 2 == 0) { newrows[i].className = 'row_data_0'}
      else { newrows[i].className = 'row_data_1'};
        tbody.appendChild(newrows[i]);
    
    }
    delete newrows;
  },
  
  
/* *******************************************
 * 
 * SORTING FUNCTIONS
 * Functions that operate on arrays, purely for sorting purposes
 * 	
 * ******************************************* */
 
  /* 
   * specific sort functions:
   * each sort function takes two parameters, a and b
   * you are comparing a[0] and b[0] 
   */
  sort_numeric: function(a,b) {
    aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
    if (isNaN(aa)) aa = 0;
    bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); 
    if (isNaN(bb)) bb = 0;
    return aa-bb;
  },
  sort_alpha: function(a,b) {
    if (a[0].toLowerCase()==b[0].toLowerCase()) return 0;
    if (a[0].toLowerCase() < b[0].toLowerCase()) return -1;
    return 1;
  },
  sort_ddmm: function(a,b) {
    mtch = a[0].match(sorttable.DATE_RE);
    y = mtch[3]; m = mtch[2]; d = mtch[1];
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt1 = y+m+d;
    mtch = b[0].match(sorttable.DATE_RE);
    y = mtch[3]; m = mtch[2]; d = mtch[1];
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt2 = y+m+d;
    if (dt1==dt2) return 0;
    if (dt1<dt2) return -1;
    return 1;
  },
  sort_mmdd: function(a,b) {
    mtch = a[0].match(sorttable.DATE_RE);
    y = mtch[3]; d = mtch[2]; m = mtch[1];
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt1 = y+m+d;
    mtch = b[0].match(sorttable.DATE_RE);
    y = mtch[3]; d = mtch[2]; m = mtch[1];
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt2 = y+m+d;
    if (dt1==dt2) return 0;
    if (dt1<dt2) return -1;
    return 1;
  },
  
  sort_iso: function(a,b) {
    mtch = a[0].match(sorttable.DATE_ISO);
    y = mtch[3]; m1 = mtch[2]; d = mtch[1];
    m = sorttable.numeric_month(m1);
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt1 = y+m+d;
    mtch = b[0].match(sorttable.DATE_ISO);
    y = mtch[3]; m2 = mtch[2]; d = mtch[1];
    m = sorttable.numeric_month(m2);
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt2 = y+m+d;
    if (dt1==dt2) return 0;
    if (dt1<dt2) return -1;
    return 1;
  },
  
    numeric_month: function(month){
  	
  	if(month == "Jan") return "1";
  	if(month == "Feb") return "2";
  	if(month == "Mar") return "3";
  	if(month == "Apr") return "4";
  	if(month == "May") return "5";
  	if(month == "Jun") return "6";
  	if(month == "Jul") return "7";
  	if(month == "Aug") return "8";
  	if(month == "Sep") return "9";
  	if(month == "Oct") return "10";
  	if(month == "Nov") return "11";
  	if(month == "Dec") return "12";
  	
  	
  
  },

  
   /* A stable sort function to allow multi-level sorting of data
    * see: http://en.wikipedia.org/wiki/Cocktail_sort
    * Joseph Nahmias
    */ 
  shaker_sort: function(list, comp_func) {
   
    var b = 0;
    var t = list.length - 1;
    var swap = true;

    while(swap) {
        swap = false;
        for(var i = b; i < t; ++i) { 
            if ( comp_func(list[i], list[i+1]) > 0 ) {
                var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
                swap = true;
            }
        } // for
        t--;

        if (!swap) break;

        for(var i = t; i > b; --i) {
            if ( comp_func(list[i], list[i-1]) < 0 ) {
                var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
                swap = true;
            }
        } // for
        b++;

    } // while(swap)
  }  
}

/* ******************************************************************
 * 
 * SUPPORTING FUNCTIONS
 * 
 * Browser specific code and functions for  dynamically adding event handlers
 * as the method call addEventListener does not work in IE
 * 
 ****************************************************************** */

// Dean Edwards/Matthias Miller/John Resig

/* for Mozilla/Opera9 */
if (document.addEventListener) {
    document.addEventListener("DOMContentLoaded", sorttable.init, false);
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
    document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
    var script = document.getElementById("__ie_onload");
    script.onreadystatechange = function() {
        if (this.readyState == "complete") {
            sorttable.init(); // call the onload handler
        }
    };
/*@end @*/

/* for Safari */
if (/WebKit/i.test(navigator.userAgent)) { // sniff
    var _timer = setInterval(function() {
        if (/loaded|complete/.test(document.readyState)) {
            sorttable.init(); // call the onload handler
        }
    }, 10);
}

/* for other browsers */
window.onload = sorttable.init;

// written by Dean Edwards, 2005
// with input from Tino Zijdel, Matthias Miller, Diego Perini

// http://dean.edwards.name/weblog/2005/10/add-event/

function dean_addEvent(element, type, handler) {
	if (element.addEventListener) {
		element.addEventListener(type, handler, false);
	} else {
		// assign each event handler a unique ID
		if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
		// create a hash table of event types for the element
		if (!element.events) element.events = {};
		// create a hash table of event handlers for each element/event pair
		var handlers = element.events[type];
		if (!handlers) {
			handlers = element.events[type] = {};
			// store the existing event handler (if there is one)
			if (element["on" + type]) {
				handlers[0] = element["on" + type];
			}
		}
		// store the event handler in the hash table
		handlers[handler.$$guid] = handler;
		// assign a global event handler to do all the work
		element["on" + type] = handleEvent;
	}
};
// a counter used to create unique IDs
dean_addEvent.guid = 1;

function removeEvent(element, type, handler) {
	if (element.removeEventListener) {
		element.removeEventListener(type, handler, false);
	} else {
		// delete the event handler from the hash table
		if (element.events && element.events[type]) {
			delete element.events[type][handler.$$guid];
		}
	}
};

function handleEvent(event) {
	var returnValue = true;
	// grab the event object (IE uses a global event object)
	event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
	// get a reference to the hash table of event handlers
	var handlers = this.events[event.type];
	// execute each event handler
	for (var i in handlers) {
		this.$$handleEvent = handlers[i];
		if (this.$$handleEvent(event) === false) {
			returnValue = false;
		}
	}
	return returnValue;
};

function fixEvent(event) {
	// add W3C standard event methods
	event.preventDefault = fixEvent.preventDefault;
	event.stopPropagation = fixEvent.stopPropagation;
	return event;
};
fixEvent.preventDefault = function() {
	this.returnValue = false;
};
fixEvent.stopPropagation = function() {
  this.cancelBubble = true;
}

// Dean's forEach: http://dean.edwards.name/base/forEach.js
/*
	forEach, version 1.0
	Copyright 2006, Dean Edwards
	License: http://www.opensource.org/licenses/mit-license.php
*/

// array-like enumeration
if (!Array.forEach) { // mozilla already supports this
	Array.forEach = function(array, block, context) {
		for (var i = 0; i < array.length; i++) {
			block.call(context, array[i], i, array);
		}
	};
}

// generic enumeration
Function.prototype.forEach = function(object, block, context) {
	for (var key in object) {
		if (typeof this.prototype[key] == "undefined") {
			block.call(context, object[key], key, object);
		}
	}
};

// character enumeration
String.forEach = function(string, block, context) {
	Array.forEach(string.split(""), function(chr, index) {
		block.call(context, chr, index, string);
	});
};

// globally resolve forEach enumeration
var forEach = function(object, block, context) {
	if (object) {
		var resolve = Object; // default
		if (object instanceof Function) {
			// functions have a "length" property
			resolve = Function;
		} else if (object.forEach instanceof Function) {
			// the object implements a custom forEach method so use that
			object.forEach(block, context);
			return;
		} else if (typeof object == "string") {
			// the object is a string
			resolve = String;
		} else if (typeof object.length == "number") {
			// the object is array-like
			resolve = Array;
		}
		resolve.forEach(object, block, context);
	}
};


