
// 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 --)
//		...
//		(/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)
//			...
//		(/tbody)
//		(/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 show the sort column in bold. But if you HAVE defined them, 
//		they will override the default styles. Again, /library/examples/SortableTableExample.html 
//		illustrates this. 
//
//										Steve Seaquist
//										11/24/2007
//
// Revision History:	04/27/2010, SRS:	Allowed SortSortableTable to call an optional 
//											completion function. Since most calls to SortSortableTable 
//											are generated by GetSortableTables, had to make it 
//											externally defined, "DoThisOnSortCompletion(pId)". 
//											Reindented to make function header lines more visible. 
//											Set /library/css/sba.css defaults for sortkey and sortdata. 
//						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;
				}
			}
		}
	if	(window.DoThisOnSortCompletion)
		window.DoThisOnSortCompletion(pId);
	}

// End SortableTables.js

