/*
  sorttable
  VERSION 2
  7TH APRIL 2007
  STUART LANGRIDGE, http://www.kryogenix.org/code/browser/sorttable/
  
  INSTRUCTIONS:
  DOWNLOAD THIS FILE
  ADD <SCRIPT Src="sorttable.js"></SCRIPT> TO YOUR HTML
  ADD Class="sortable" TO ANY TABLE YOU'D LIKE TO MAKE SORTABLE
  CLICK ON THE HEADERS TO SORT
  
  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.
*/

var stIsIE = /*@cc_on!@*/false;

sorttable = {
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
    if (_timer)
    {
	clearInterval(_timer);
    }

    if (!document.createElement || !document.getElementsByTagName)
    {
	return;
    }

    sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;

    forEach(document.getElementsByTagName('table'), function(table) 
    {
	if (table.className.search(/\bsortable\b/) != -1) 
	{
	    sorttable.makeSortable(table);
	}
    });

},
  
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, SIGH
    if (table.tHead == null) 
    {
	table.tHead = table.getElementsByTagName('thead')[0];
    }

    if (table.tHead.rows.length != 1)
    {
	return; // CAN'T COPE WITH TWO HEADER ROWS
    }

    // SORTTABLE V1 PUT ROWS WITH A CLASS OF "SORTBOTTOM" AT THE BOTTOM (AS
    // "TOTAL" ROWS, FOR EXAMPLE). THIS IS B&R, SINCE WHAT YOU'RE SUPPOSED
    // TO DO IS PUT THEM IN A tfoot. SO, IF THERE ARE SORTBOTTOM ROWS,
    // FOR BACKWARDS COMPATIBILITY, MOVE THEM TO tfoot (CREATING IT IF NEEDED).
    sortbottomrows = [];
    for (var i=0; i<table.rows.length; i++) 
    {
      if (table.rows[i].className.search(/\bsortbottom\b/) != -1) 
      {
	sortbottomrows[sortbottomrows.length] = table.rows[i];
      }
    }
    if (sortbottomrows) 
    {
      if (table.tFoot == null) 
      {
	// TABLE DOESN'T HAVE A tfoot. CREATE ONE.
	tfo = document.createElement('tfoot');
	table.appendChild(tfo);
      }
      for (var i=0; i<sortbottomrows.length; i++) 
      {
	tfo.appendChild(sortbottomrows[i]);
      }
      delete sortbottomrows;
    }

    // WORK THROUGH EACH COLUMN AND CALCULATE ITS TYPE
    headrow = table.tHead.rows[0].cells;
    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
	mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
	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);
	      }
	      // MAKE IT CLICKABLE TO SORT
	      headrow[i].sorttable_columnindex = i;
	      headrow[i].sorttable_tbody = table.tBodies[0];
	dean_addEvent(headrow[i],"click", function(e) 
	{

	  if (this.className.search(/\bsorttable_sorted\b/) != -1) 
	  {
	    // IF WE'RE ALREADY SORTED BY THIS COLUMN, JUST 
	    // REVERSE THE TABLE, WHICH IS QUICKER
	    sorttable.reverse(this.sorttable_tbody);
	    this.className = this.className.replace('sorttable_sorted',
						    'sorttable_sorted_reverse');
	    this.removeChild(document.getElementById('sorttable_sortfwdind'));
	    sortrevind = document.createElement('span');
	    sortrevind.id = "sorttable_sortrevind";
	    sortrevind.innerHTML = stIsIE ? '&nbsp<font face="webdings">5</font>' : '&nbsp;&#x25B4;';
	    this.appendChild(sortrevind);
	    return;
	  }
	  if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) 
	  {
	    // IF WE'RE ALREADY SORTED BY THIS COLUMN IN REVERSE, JUST 
	    // RE-REVERSE THE TABLE, WHICH IS QUICKER
	    sorttable.reverse(this.sorttable_tbody);
	    this.className = this.className.replace('sorttable_sorted_reverse',
						    'sorttable_sorted');
	    this.removeChild(document.getElementById('sorttable_sortrevind'));
	    sortfwdind = document.createElement('span');
	    sortfwdind.id = "sorttable_sortfwdind";
	    sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
	    this.appendChild(sortfwdind);
	    return;
	  }
	  
	  // REMOVE SORTTABLE_SORTED CLASSES
	  theadrow = this.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');
	  if (sortfwdind) 
	  {
	  sortfwdind.parentNode.removeChild(sortfwdind); 
	  }
	      sortrevind = document.getElementById('sorttable_sortrevind');
	  if (sortrevind) 
	  { 
	      sortrevind.parentNode.removeChild(sortrevind); 
	  }
	  
	  this.className += ' sorttable_sorted';
	  sortfwdind = document.createElement('span');
	  sortfwdind.id = "sorttable_sortfwdind";
	  sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
	  this.appendChild(sortfwdind);

	    // BUILD AN ARRAY TO SORT. THIS IS A SCHWARTZIAN TRANSFORM THING,
	    // I.E., WE "DECORATE" EACH ROW WITH THE ACTUAL SORT KEY,
	    // SORT BASED ON THE SORT KEYS, AND THEN PUT THE ROWS BACK IN ORDER
	    // WHICH IS A LOT FASTER BECAUSE YOU ONLY DO getInnerText ONCE PER ROW
	    row_array = [];
	    col = this.sorttable_columnindex;
	    rows = this.sorttable_tbody.rows;
	    for (var j=0; j<rows.length; j++) 
	    {
	      row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
	    }
	    /* If YOU WANT A STABLE SORT, UNCOMMENT THE FOLLOWING LINE */
	    //sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
	    /* AND COMMENT OUT THIS ONE */
	    row_array.sort(this.sorttable_sortfunction);
	    
	    tb = this.sorttable_tbody;
	    for (var j=0; j<row_array.length; j++) 
	    {
	      tb.appendChild(row_array[j][1]);
	    }
	    
	    delete row_array;
	  });
	}
    }
},
  
guessType: function(table, column) 
{
    // GUESS THE TYPE OF A COLUMN BASED ON ITS FIRST NON-BLANK ROW
    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;
	}
	// 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;
},
  
getInnerText: function(node) 
{
    // 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.

    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 = '';
	  for (var i = 0; i < node.childNodes.length; i++) 
	  {
	    innerText += sorttable.getInnerText(node.childNodes[i]);
	  }
	  return innerText.replace(/^\s+|\s+$/g, '');
	  break;
	default:
	  return '';
      }
    }
},
  
reverse: function(tbody) 
{
    // REVERSE THE ROWS IN A TBODY
    newrows = [];
    for (var i=0; i<tbody.rows.length; i++) 
    {
      newrows[newrows.length] = tbody.rows[i];
    }
    for (var i=newrows.length-1; i>=0; i--) 
    {
       tbody.appendChild(newrows[i]);
    }
    delete newrows;
},
  
  /* 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]==b[0]) return 0;
    if (a[0]<b[0]) 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;
},
  
shaker_sort: function(list, comp_func) 
{
    // A STABLE SORT FUNCTION TO ALLOW MULTI-LEVEL SORTING OF DATA
    // SEE: http://en.wikipedia.org/wiki/Cocktail_sort
    // THANKS TO JOSEPH NAHMIAS
    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: BUNDLED HERE TO AVOID DEPENDING ON A LIBRARY
   ****************************************************************** */

// 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);
	}
};
