
// Begin SortableTables.js: 
//
// Author:	Steve Seaquist, Trusted Mission Solutions, Inc, for the US Small Business Administration. 
// Created:	11/24/2007. 
// Description:
//
// This file requires literally no JavaScript code at all on your part. Instead, all you have 
// to do is code your tables in a particular way, which just so happens to be the way that 
// everyone is SUPPOSED to code tables for Section 508 anyway. That is: THEADs, TRs and THs in the 
// THEADs, TBODYs, TRs and TDs in the TBODYs, id attributes on the rows and columns, and headers 
// attributes on the cells. In addition, you have to use 3 fake CSS class names to control how 
// the sorts are generated. I use the term "fake" because they don't exist in any style sheet. 
// So the browser ignores them. They exist just to tell this code what you want to do. 
//
// For a good example of a table that does everything you're supposed to do for Section 508, and 
// that also uses this file, have a look at /library/examples/SortableTableExample.html. 
//
// Usage overview (parentheses instead of angle brackets in case you're viewing with a browser): 
//
//		(html lang="en-US")
//		(head)
//		...
//		(script src="/library/javascripts/SortableTables.js")(/script)		(-- mandatory --)
//		(style)																(-- optional --)
//		.sortdata	[color: black;	font-weight: normal;}					(-- optional --)
//		.sortkey	{color: red;	font-weight: bold;}						(-- optional --)
//		(/style)															(-- optional --)
//		...
//		(/head)
//		(body)
//		...
//		(table class="sortable" ... )										(-- mandatory --)
//		(thead)
//			(tr)
//				(th class="sortcount")#(/th)								(-- optional --)
//				(th class="sortnumeric")LoanNmb(/th)						(-- optional --)
//				(th class="sorttext")Borrower(/th)							(-- optional --)
//				(th class="sorttext")Loan Amount(/th)						(-- optional --)
//				...
//			(/tr)
//		(/thead)
//		(tbody)
//			(tr)
//				(td)...(/td)
//				(td)...(/td)
//				(td)...(/td)
//			(/tr)
//			...
//		(/tbldy)
//		(/table)
//		...
//		(/body)
//		(/html)
//
// Comments:
//
//	(1)	GetSortableTables() processes TABLE tags only if they have class "sortable". 
//	(2)	GetSortableTables() processes TH tags only within THEAD. 
//	(3)	GetSortableTables() processes TD tags only within TBODY. 
//	(4)	You can toggle back and forth between THEAD and TBODY if you want. 
//	(5)	There are 4 fake CSS class names: 
//		(table class="sortable"):	Marks a table  as sortable. 
//		(th class="sortcount"):		Marks a column as unsorted, always containing row count. 
//		(th class="sortnumeric"):	Marks a column as sortable by numeric sort. 
//		(th class="sorttext"):		Marks a column as sortable by text sort. 
//	(6)	If you need to use another class name, give it in the same class attribute, 
//		separated with a space. Exampple: (table class="sortable normal" ... )
//	(7)	The THEAD rows are NOT sorted. 
//	(8)	The TBODY rows ARE sorted, but rowspan and colspan are not supported, and 
//		probably never will be. 
//	(9)	If you nest HTML in a td, THE HTML WILL BE SORTED, not the data it contains. 
//		(So it's generally not a good idea to nest HTML in sortable tables. Use CSS 
//		class names instead if you want to format the contents of a cell.) 
// (10)	Any td cell may contain a single occurrence of "&nbsp;". It will not affect the sort. 
//		It's the same as an empty cell as far as sorting is concerned. 
// (11)	The th tags in the thead with class names sortnumeric and sorttext will be turned into 
//		a hotlink. The first time a user clicks the hotlink, it sorts on that column, ascending. 
//		If the user clicks on that same hotlink a second time, it will be resorted, descending. 
// (12)	Users can sort on only one column at a time. No compound keys. 
// (13)	More than one table can be marked sortable, provided that they're completely separate
//		(not nested). They will sort independently of each other. 
// (14)	After a sort, every TD in the TBODY will have a new class added. If the TD is in the 
//		column that was last sorted, the [additional] class name will be "sortkey". Otherwise, 
//		it will be "sortdata". If you haven't defined CSS classes by those names, the addition 
//		of those class names will do nothing. But if you HAVE defined them, they will highlight 
//		the sort column (and the other columns that went along for the ride) with the classes 
//		you defined. Again, /library/examples/SortableTableExample.html illustrates this. 
//
//										Steve Seaquist
//										11/24/2007
//
// Revision History: 01/31/2008, SRS:	Corrected spelling of sSortableTablePrevOnLoad to gSortableTablePrevOnLoad. 
//										(Because it's global, it gets the g prefix.) Added Revision History. 
//					11/24/2007, SRS:	Original implementation. 

// Global variables: 

var	gSortableTableCol					= -1;
var	gSortableTableId					= -1;
var	gSortableTablePrevOnLoad			= (window.onload ? window.onload : null);
var	gSortableTables						= new Array();

// Utility function for use by RemoveTypicalSortCellGarbage (also in /library/javascripts/StringTrim.js):

String.prototype.trim						= function ()
{
var	sFirstNonBlankEncountered				= -1;
var	sLastNonBlankEncountered				= -1;
for	(var i = 0; i < this.length; i++)
	switch (this.charAt(i))
		{
		case " ":
		case "\n":
		case "\r":
		case "\t":
			break;
		default:
			if	(sFirstNonBlankEncountered == -1)
				{
				sFirstNonBlankEncountered	= i;
				sLastNonBlankEncountered	= i;
				}
			else
				sLastNonBlankEncountered	= i;
		}
if	(sFirstNonBlankEncountered == -1)
	return "";
return this.substring(sFirstNonBlankEncountered,sLastNonBlankEncountered+1);
}

// Functions, in alphabetical order: 

function DumpSortableTables			()		// Debug routine. 
{
var	sString								= "gSortableTables is null.\n";
if	(gSortableTables.length)
	{
	sString								= "There are "+gSortableTables.length+" elements in gSortableTables.\n\n";
	if	(gSortableTables.length > 0)
		{
		sString							+= "gSortableTables:\n\n";
		for	(var i = 0; i < gSortableTables.length; i++)
			{
			for	(var j = 0; j < gSortableTables[i].length; j++)
				{
				if	(j == 4)
					{
					sTable				= gSortableTables[i][j];
					sString				+= "gSortableTables["+i+"]["+j+"] = \n";
					for	(var r = 0; r < sTable.length; r++)
						{
						sString			+= "    {";
						for	(var c = 0; c < sTable[r].length; c++)
							sString		+= "'"+sTable[r][c]+"',";
						sString			+= "}\n";
						}
					}
				else
					sString				+= "gSortableTables["+i+"]["+j+"] = "+gSortableTables[i][j]+"\n";
				}
			}
		sString							+= "\nEnd of gSortableTables listing.\n";
		}
	}
alert(sString);
}

function GetSortableTables				()		// Builds the global variables used by SortSortableTable(). 
{
var	a									= 0;
var	b									= 0;
var	c									= 0;
var	i									= 0;
var	j									= 0;
var	k									= 0;
var	r									= 0;
var	t									= 0;
var	sLen								= 0;
var	sTables								= document.getElementsByTagName("table");
for	(t = 0; t < sTables.length; t++)
	{
	var	sFound							= false;
	var	sTable							= sTables[t];
	//DumpObject(sTable, "sTable", 20);
	var	sAttrs							= sTable.attributes;
	if ((sAttrs)
	&&	(sAttrs.length)
	&&	(sAttrs.length > 0))
		for	(a = 0; a < sAttrs.length; a++)
			{
			var	sAttr					= sAttrs[a];
			//DumpObject(sAttr, "sAttr", 20);
			if	(sAttr.nodeName.toLowerCase() == "class")
				{
				var	sClasses			= sAttr.nodeValue.toLowerCase().split(" ");
				for	(c = 0; c < sClasses.length; c++)
					{
					var	sClass			= sClasses[c];
					if	(sClass == "sortable")
						{
						sFound			= true;
						break;
						}
					}
				}
			}
	if	(sFound)
		{
	//	alert("Found.");
		var	sId							= ((sTable.id) ? sTable.id : ("SortableTable"+(sLen+1)));
		var	sRows						= 0;
		var	sCols						= 0;
		var	sColOfCount					= 0;
		var	sCells						= new Array();
		var	sSorters					= new Array();
		var	sSortersRows				= 0;
		var	sTBodies					= sTable.getElementsByTagName("tbody");
		var	sTHeads						= sTable.getElementsByTagName("thead");
		for	(b = 0; b < sTBodies.length; b++)
			{
			var	sTBody					= sTBodies[b];
			var	sTRs					= sTBody.getElementsByTagName("tr");
			for	(r = 0; r < sTRs.length; r++)
				{
				var	sTR					= sTRs[r];
				var	sTDs				= sTR.getElementsByTagName("td");
				sCells[sRows]			= new Array();
				sCells[sRows][0]		= sRows.toString();	// Allows putting array back in original order. 
				for	(c = 0; c < sTDs.length; c++)
					{
					var	sTD				= sTDs[c];
					sCells[sRows][c+1]	= ((sTD.innerHTML) ? sTD.innerHTML : "");
					}
				if	(sCols < (c+1))
					sCols				= (c+1);
				sRows++;
				}
			}
		for	(h = 0; h < sTHeads.length; h++)
			{
			var	sTHead					= sTHeads[h];
			var	sTRs					= sTHead.getElementsByTagName("tr");
			for	(r = 0; r < sTRs.length; r++)
				{
				var	sCtr				= 0;
				var	sTR					= sTRs[r];
				var	sTHs				= sTR.getElementsByTagName("th");
				for	(c = 0; c < sTHs.length; c++)
					{
					sCtr++;	// Will always be c+1, but without running the risk of string concatenation. 
					var	sTH				= sTHs[c];
					var	sAttrs			= sTH.attributes;
					var	sSortType		= "";
					var	sThisColIsCount	= false;
					if ((sAttrs)
					&&	(sAttrs.length)
					&&	(sAttrs.length > 0))
						for	(a = 0; a < sAttrs.length; a++)
							{
							var	sAttr					= sAttrs[a];
							//DumpObject(sAttr, "sAttr", 20);
							if	(sAttr.nodeName.toLowerCase() == "class")
								{
								var	sClasses			= sAttr.nodeValue.toLowerCase().split(" ");
								for	(i = 0; i < sClasses.length; i++)
									switch (sClasses[i])
										{
										case "sortcount":
											sThisColIsCount	= sCtr;
											break
										case "sortnumeric":
											sSortType		= "num";
											break;
										case "sorttext":
											sSortType		= "txt";
											break;
										}
								}
							}
					if	(sThisColIsCount)
						{
						sColOfCount		= sCtr;	// Keep the sortcount column from being sortable, 
						sSortType		= "";	// because it would be much too confusing to users. 
						}
					if	(sSortType.length > 0)
						sTH.innerHTML	= "<a href=\"javascript:SortSortableTable(\'"
										+ sId
										+ "\',"
										+ sCtr
										+ ",\'"
										+ sSortType
										+ "\');\" title=\"Sort table by this column.\">"
										+ sTH.innerHTML
										+ "</a>";
					}
				}
			}
		for	(var j = 0; j < sRows; j++)
			for	(var k = sCells[j].length + 1; k < sCols; k++)
				sCells[j][k]			= "";
		var	sArray						= new Array();
		sArray[0]						= sId;			// HTML reference
		sArray[1]						= sTable;		// HTML reference
		sArray[2]						= sRows;
		sArray[3]						= sCols;
		sArray[4]						= sCells;
		sArray[5]						= -1;			// column of current sort
		sArray[6]						= 0;			// -1 desc, 0 unsorted, +1 asc
		sArray[7]						= sColOfCount;	// If used, column is always a count. 
		gSortableTables[sLen]			= sArray;
		sLen++;
		//alert("Stored something. gSortableTables now = "+gSortableTables);
		}
	//else
	//	alert("Not Found.");
	}
if	(gSortableTablePrevOnLoad)
	gSortableTablePrevOnLoad();
}
window.onload							= GetSortableTables;

function RemoveTypicalSortCellGarbage	(pStr, pNumeric)	// Don't let outer whitespace and &nbsp; affect the 
{															// sort. Also, locase for case insensitive sorting. 
var	sStr								= pStr.toLowerCase().trim().replace(/&nbsp;/g, "");
if	(pNumeric)
	sStr								= sStr.replace(/\$/g, "").replace(/%/g, "").replace(/,/g, "");
return sStr;
}

function SortCellAlphabetically			(p1, p2)
{
var	sText1								= RemoveTypicalSortCellGarbage(p1[gSortableTableCol], false);
var	sText2								= RemoveTypicalSortCellGarbage(p2[gSortableTableCol], false);
if	(gSortableTableAscDesc < 0)
	return (sText2 > sText1) ? 1 : -1;
else// default to ascending
	return (sText1 > sText2) ? 1 : -1;
}

function SortCellNumerically			(p1, p2)
{
var	sText1								= RemoveTypicalSortCellGarbage(p1[gSortableTableCol], true);
var	sText2								= RemoveTypicalSortCellGarbage(p2[gSortableTableCol], true);
if	(gSortableTableAscDesc < 0)
	return parseFloat(sText2) - parseFloat(sText1);
else// default to ascending
	return parseFloat(sText1) - parseFloat(sText2);
}

function SortSortableTable				(pId, pCol, pType)
{
var	sFound								= -1;
if	(gSortableTables.length)
	for	(var t = 0; t < gSortableTables.length; t++)
		if	(gSortableTables[t][0] == pId)
			{
			sFound						= t;
			break
			}
if	(sFound < 0)
	{
	alert("Unable to sort the '"+pId+"' table. (Table not found.)");
	return;
	}
if ((gSortableTables[sFound][5] == pCol)
&&	(gSortableTables[sFound][6] == 1))
	gSortableTableAscDesc				= -1;
else// default to ascending
	gSortableTableAscDesc				= 1;
gSortableTableCol						= pCol;
if	(pType == "num")
	gSortableTables[sFound][4].sort		(SortCellNumerically);
else
	gSortableTables[sFound][4].sort		(SortCellAlphabetically);

// Now propagate the just-sorted memory table back to the HTML table: 
gSortableTables[sFound][5]				= pCol;
gSortableTables[sFound][6]				= gSortableTableAscDesc;
var	sTable								= gSortableTables[sFound][1];
var	sCells								= gSortableTables[sFound][4];
var	sColOfCount							= gSortableTables[sFound][7];
var	sTBodies							= sTable.getElementsByTagName("tbody");
for	(b = 0; b < sTBodies.length; b++)
	{
	var	sCtrRow							= 0;	// logical row, always 1 greater than r. 
	var	sTBody							= sTBodies[b];
	var	sTRs							= sTBody.getElementsByTagName("tr");
	for	(var r = 0; r < sTRs.length; r++)
		{
		sCtrRow++;
		var	sCtrCol						= 0;	// logical col, always 1 greater than c. 
		var	sTR							= sTRs[r];
		var	sTDs						= sTR.getElementsByTagName("td");
		for	(var c = 0; c < sTDs.length; c++)
			{
			sCtrCol++;
			var	sTD						= sTDs[c];
			sTD.innerHTML				= ((sCtrCol == sColOfCount)	? sCtrRow	: sCells[r][sCtrCol]);
			var	sAttrs					= sTD.attributes;
			var	sFound					= false;
			var	sNewAttrs				= "";
			var	sNewSortClassName		= ((sCtrCol == pCol)		? "sortkey"	: "sortdata");
			if ((sAttrs)
			&&	(sAttrs.length)
			&&	(sAttrs.length > 0))
				{
				for	(a = 0; a < sAttrs.length; a++)
					{
					var	sAttr			= sAttrs[a];
					//DumpObject(sAttr, "sAttr", 20);
					if	(sAttr.nodeName.toLowerCase() == "class")
						{
						var	sClasses	= sAttr.nodeValue.toLowerCase().split(" ");
						for	(i = 0; i < sClasses.length; i++)
							{
							var	sClass	= sClasses[i];
							switch (sClass)
								{
								case "sortdata":
								case "sortkey":
									sNewAttrs	+= " " + sNewSortClassName;
									sFound		= true;
									break;
								default:
									sNewAttrs	+= " " + sClass;
								}
							}
						}
					}
				}
			if	(!sFound)
				sNewAttrs				+= " " + sNewSortClassName;
			sTD.className				= sNewAttrs.substring(1,sNewAttrs.length);
			//if	(!confirm("Column "+c+".className = '" + sNewAttrs + "'."))
			//	return;
			}
		}
	}
}

// End SortableTables.js
