/*===================================================================
 Author: Matt Kruse
 
 View documentation, examples, and source code at:
     http://www.JavascriptToolbox.com/

 NOTICE: You may use this code for any purpose, commercial or
 private, without any further permission from the author. You may
 remove this notice from your final code if you wish, however it is
 appreciated by the author if at least the web site address is kept.

 This code may NOT be distributed for download from script sites, 
 open source CDs or sites, or any other distribution method. If you
 wish you share this code with others, please direct them to the 
 web site above.
 
 Pleae do not link directly to the .js files on the server above. Copy
 the files to your own server for use with your site or webapp.
 ===================================================================*/
/* ******************************************************************* */
/*   UTIL FUNCTIONS                                                    */
/* ******************************************************************* */
var Util = {'$VERSION':1.06};

// Util functions - these are GLOBAL so they
// look like built-in functions.

// Determine if an object is an array
function isArray(o) {
	return (o!=null && typeof(o)=="object" && typeof(o.length)=="number" && (o.length==0 || defined(o[0])));
};

// Determine if an object is an Object
function isObject(o) {
	return (o!=null && typeof(o)=="object" && defined(o.constructor) && o.constructor==Object && !defined(o.nodeName));
};

// Determine if a reference is defined
function defined(o) {
	return (typeof(o)!="undefined");
};

// Iterate over an array, object, or list of items and run code against each item
// Similar functionality to Perl's map() function
function map(func) {
	var i,j,o;
	var results = [];
	if (typeof(func)=="string") {
		func = new Function('$_',func);
	}
	for (i=1; i<arguments.length; i++) {
		o = arguments[i];
		if (isArray(o)) {
			for (j=0; j<o.length; j++) {
				results[results.length] = func(o[j]);
			}
		}
		else if (isObject(o)) {
			for (j in o) {
				results[results.length] = func(o[j]);
			}
		}
		else {
			results[results.length] = func(o);
		}
	}
	return results;
};

// Set default values in an object if they are undefined
function setDefaultValues(o,values) {
	if (!defined(o) || o==null) {
		o = {};
	}
	if (!defined(values) || values==null) {
		return o;
	}
	for (var val in values) {
		if (!defined(o[val])) {
			o[val] = values[val];
		}
	}
	return o;
};

/* ******************************************************************* */
/*   DEFAULT OBJECT PROTOTYPE ENHANCEMENTS                             */
/* ******************************************************************* */
// These functions add useful functionality to built-in objects
Array.prototype.contains = function(o) {
	var i,l;
	if (!(l = this.length)) { return false; }
	for (i=0; i<l; i++) {
		if (o==this[i]) {
			return true;
		}
	}
}

/* ******************************************************************* */
/*   DOM FUNCTIONS                                                     */
/* ******************************************************************* */
var DOM = (function() { 
	var dom = {};
	
	// Get a parent tag with a given nodename
	dom.getParentByTagName = function(o,tagNames) {
		if(o==null) { return null; }
		if (isArray(tagNames)) {
			tagNames = map("return $_.toUpperCase()",tagNames);
			while (o=o.parentNode) {
				if (o.nodeName && tagNames.contains(o.nodeName)) {
					return o;
				}
			}
		}
		else {
			tagNames = tagNames.toUpperCase();
			while (o=o.parentNode) {
				if (o.nodeName && tagNames==o.nodeName) {
					return o;
				}
			}
		}
		return null;
	};
	
	// Remove a node from its parent
	dom.removeNode = function(o) {
		if (o!=null && o.parentNode && o.parentNode.removeChild) {
			// First remove all attributes which are func references, to avoid memory leaks
			for (var i in o) {
				if (typeof(o[i])=="function") {
					o[i] = null;
				}
			}
			o.parentNode.removeChild(o);
			return true;
		}
		return false;
	};

	// Get the outer width in pixels of an object, including borders, padding, and margin
	dom.getOuterWidth = function(o) {
		if (defined(o.offsetWidth)) {
			return o.offsetWidth;
		}
		return null;
	};

	// Get the outer height in pixels of an object, including borders, padding, and margin
	dom.getOuterHeight = function(o) {
		if (defined(o.offsetHeight)) {
			return o.offsetHeight;
		}
		return null;
	};

	// Resolve an item, an array of items, or an object of items
	dom.resolve = function() {
		var results = new Array();
		var i,j,o;
		for (var i=0; i<arguments.length; i++) {
			var o = arguments[i];
			if (o==null) {
				if (arguments.length==1) {
					return null;
				}
				results[results.length] = null;
			}
			else if (typeof(o)=='string') {
				if (document.getElementById) {
					o = document.getElementById(o);
				}
				else if (document.all) {
					o = document.all[o];
				}
				if (arguments.length==1) {
					return o;
				}
				results[results.length] = o;
			}
			else if (isArray(o)) {
				for (j=0; j<o.length; j++) {
					results[results.length] = o[j];
				}
			}
			else if (isObject(o)) {
				for (j in o) {
					results[results.length] = o[j];
				}
			}
			else if (arguments.length==1) {
				return o;
			}
			else {
				results[results.length] = o;
			}
	  }
	  return results;
	};
	dom.$ = dom.resolve;
	
	return dom;
})();

/* ******************************************************************* */
/*   CSS FUNCTIONS                                                     */
/* ******************************************************************* */
var CSS = (function(){
	var css = {};

	// Convert an RGB string in the form "rgb (255, 255, 255)" to "#ffffff"
	css.rgb2hex = function(rgbString) {
		if (typeof(rgbString)!="string" || !defined(rgbString.match)) { return null; }
		var result = rgbString.match(/^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*/);
		if (result==null) { return rgbString; }
		var rgb = +result[1] << 16 | +result[2] << 8 | +result[3]
		var hex = "";
		var digits = "0123456789abcdef";
		while(rgb!=0) { 
			hex = digits.charAt(rgb&0xf)+hex; 
			rgb>>>=4; 
		} 
		while(hex.length<6) { hex='0'+hex; }
		return "#" + hex;
	};

	// Convert hyphen style names like border-width to camel case like borderWidth
	css.hyphen2camel = function(property) {
		if (!defined(property) || property==null) { return null; }
		if (property.indexOf("-")<0) { return property; }
		var str = "";
		var c = null;
		var l = property.length;
		for (var i=0; i<l; i++) {
			c = property.charAt(i);
			str += (c!="-")?c:property.charAt(++i).toUpperCase();
		}
		return str;
	};
	
	// Determine if an object or class string contains a given class.
	css.hasClass = function(obj,className) {
		if (!defined(obj) || obj==null || !RegExp) { return false; }
		var re = new RegExp("(^|\\s)" + className + "(\\s|$)");
		if (typeof(obj)=="string") {
			return re.test(obj);
		}
		else if (typeof(obj)=="object" && obj.className) {
			return re.test(obj.className);
		}
		return false;
	};
	
	// Add a class to an object
	css.addClass = function(obj,className) {
		if (typeof(obj)!="object" || obj==null || !defined(obj.className)) { return false; }
		if (obj.className==null || obj.className=='') { 
			obj.className = className; 
			return true; 
		}
		if (css.hasClass(obj,className)) { return true; }
		obj.className = obj.className + " " + className;
		return true;
	};
	
	// Remove a class from an object
	css.removeClass = function(obj,className) {
		if (typeof(obj)!="object" || obj==null || !defined(obj.className) || obj.className==null) { return false; }
		if (!css.hasClass(obj,className)) { return false; }
		var re = new RegExp("(^|\\s+)" + className + "(\\s+|$)");
		obj.className = obj.className.replace(re,' ');
		return true;
	};
	
	// Fully replace a class with a new one
	css.replaceClass = function(obj,className,newClassName) {
		if (typeof(obj)!="object" || obj==null || !defined(obj.className) || obj.className==null) { return false; }
		css.removeClass(obj,className);
		css.addClass(obj,newClassName);
		return true;
	};
	
	// Get the currently-applied style of an object
	css.getStyle = function(o, property) {
		if (o==null) { return null; }
		var val = null;
		var camelProperty = css.hyphen2camel(property);
		// Handle "float" property as a special case
		if (property=="float") {
			val = css.getStyle(o,"cssFloat");
			if (val==null) { 
				val = css.getStyle(o,"styleFloat"); 
			}
		}
		else if (o.currentStyle && defined(o.currentStyle[camelProperty])) {
			val = o.currentStyle[camelProperty];
		}
		else if (window.getComputedStyle) {
			val = window.getComputedStyle(o,null).getPropertyValue(property);
		}
		else if (o.style && defined(o.style[camelProperty])) {
			val = o.style[camelProperty];
		}
		// For color values, make the value consistent across browsers
		// Convert rgb() colors back to hex for consistency
		if (/^\s*rgb\s*\(/.test(val)) {
			val = css.rgb2hex(val);
		}
		// Lowercase all #hex values
		if (/^#/.test(val)) {
			val = val.toLowerCase();
		}
		return val;
	};
	css.get = css.getStyle;

	// Set a style on an object
	css.setStyle = function(o, property, value) {
		if (o==null || !defined(o.style) || !defined(property) || property==null || !defined(value)) { return false; }
		if (property=="float") {
			o.style["cssFloat"] = value;
			o.style["styleFloat"] = value;
		}
		else if (property=="opacity") {
			o.style['-moz-opacity'] = value;
			o.style['-khtml-opacity'] = value;
			o.style.opacity = value;
			if (defined(o.style.filter)) {
				o.style.filter = "alpha(opacity=" + value*100 + ")";
			}
		}
		else {
			o.style[css.hyphen2camel(property)] = value;
		}
		return true;
	};
	css.set = css.setStyle;
	
	// Get a unique ID which doesn't already exist on the page
	css.uniqueIdNumber=1000;
	css.createId = function(o) {
		if (defined(o) && o!=null && defined(o.id) && o.id!=null && o.id!="") { 
			return o.id;
		}
		var id = null;
		while (id==null || document.getElementById(id)!=null) {
			id = "ID_"+(css.uniqueIdNumber++);
		}
		if (defined(o) && o!=null && (!defined(o.id)||o.id=="")) {
			o.id = id;
		}
		return id;
	};
	
	return css;
})();

/* ******************************************************************* */
/*   EVENT FUNCTIONS                                                   */
/* ******************************************************************* */

var Event = (function(){
	var ev = {};
	
	// Resolve an event using IE's window.event if necessary
	// --------------------------------------------------------------------
	ev.resolve = function(e) {
		if (!defined(e) && defined(window.event)) {
			e = window.event;
		}
		return e;
	};
	
	// Add an event handler to a function
	// Note: Don't use 'this' within functions added using this method, since
	// the attachEvent and addEventListener models differ.
	// --------------------------------------------------------------------
	ev.add = function( obj, type, fn, capture ) {
		if (obj.addEventListener) {
			obj.addEventListener( type, fn, capture );
			return true;
		}
		else if (obj.attachEvent) {
			obj.attachEvent( "on"+type, fn );
			return true;
		}
		return false;
	};

	// Get the mouse position of an event
	// --------------------------------------------------------------------
	// PageX/Y, where they exist, are more reliable than ClientX/Y because 
	// of some browser bugs in Opera/Safari
	ev.getMouseX = function(e) {
		e = ev.resolve(e);
		if (defined(e.pageX)) {
			return e.pageX;
		}
		if (defined(e.clientX)) {
			return e.clientX+Screen.getScrollLeft();
		}
		return null;
	};
	ev.getMouseY = function(e) {
		e = ev.resolve(e);
		if (defined(e.pageY)) {
			return e.pageY;
		}
		if (defined(e.clientY)) {
			return e.clientY+Screen.getScrollTop();
		}
		return null;
	};

	// Stop the event from bubbling up to parent elements.
	// Two method names map to the same function
	// --------------------------------------------------------------------
	ev.cancelBubble = function(e) {
		e = ev.resolve(e);
		if (typeof(e.stopPropagation)=="function") { e.stopPropagation(); } 
		if (defined(e.cancelBubble)) { e.cancelBubble = true; }
	};
	ev.stopPropagation = ev.cancelBubble;

	// Prevent the default handling of the event to occur
	// --------------------------------------------------------------------
	ev.preventDefault = function(e) {
		e = ev.resolve(e);
		if (typeof(e.preventDefault)=="function") { e.preventDefault(); } 
		if (defined(e.returnValue)) { e.returnValue = false; }
	};
	
	return ev;
})();

/* ******************************************************************* */
/*   SCREEN FUNCTIONS                                                  */
/* ******************************************************************* */
var Screen = (function() {
	var screen = {};

	// Get a reference to the body
	// --------------------------------------------------------------------
	screen.getBody = function() {
		if (document.body) {
			return document.body;
		}
		if (document.getElementsByTagName) {
			var bodies = document.getElementsByTagName("BODY");
			if (bodies!=null && bodies.length>0) {
				return bodies[0];
			}
		}
		return null;
	};

	// Get the amount that the main document has scrolled from top
	// --------------------------------------------------------------------
	screen.getScrollTop = function() {
		if (document.documentElement && defined(document.documentElement.scrollTop) && document.documentElement.scrollTop>0) {
			return document.documentElement.scrollTop;
		}
		if (document.body && defined(document.body.scrollTop)) {
			return document.body.scrollTop;
		}
		return null;
	};
	
	// Get the amount that the main document has scrolled from left
	// --------------------------------------------------------------------
	screen.getScrollLeft = function() {
		if (document.documentElement && defined(document.documentElement.scrollLeft) && document.documentElement.scrollLeft>0) {
			return document.documentElement.scrollLeft;
		}
		if (document.body && defined(document.body.scrollLeft)) {
			return document.body.scrollLeft;
		}
		return null;
	};
	
	// Util function to default a bad number to 0
	// --------------------------------------------------------------------
	screen.zero = function(n) {
		return (!defined(n) || isNaN(n))?0:n;
	};

	// Get the width of the entire document
	// --------------------------------------------------------------------
	screen.getDocumentWidth = function() {
		var width = 0;
		var body = screen.getBody();
		if (document.documentElement && (!document.compatMode || document.compatMode=="CSS1Compat")) {
		    var rightMargin = parseInt(CSS.get(body,'marginRight'),10) || 0;
		    var leftMargin = parseInt(CSS.get(body,'marginLeft'), 10) || 0;
			width = Math.max(body.offsetWidth + leftMargin + rightMargin, document.documentElement.clientWidth);
		}
		else {
			width =  Math.max(body.clientWidth, body.scrollWidth);
		}
		if (isNaN(width) || width==0) {
			width = screen.zero(self.innerWidth);
		}
		return width;
	};
	
	// Get the height of the entire document
	// --------------------------------------------------------------------
	screen.getDocumentHeight = function() {
		var body = screen.getBody();
		var innerHeight = (defined(self.innerHeight)&&!isNaN(self.innerHeight))?self.innerHeight:0;
		if (document.documentElement && (!document.compatMode || document.compatMode=="CSS1Compat")) {
		    var topMargin = parseInt(CSS.get(body,'marginTop'),10) || 0;
		    var bottomMargin = parseInt(CSS.get(body,'marginBottom'), 10) || 0;
			return Math.max(body.offsetHeight + topMargin + bottomMargin, document.documentElement.clientHeight, document.documentElement.scrollHeight, screen.zero(self.innerHeight));
		}
		return Math.max(body.scrollHeight, body.clientHeight, screen.zero(self.innerHeight));
	};
	
	// Get the width of the viewport (viewable area) in the browser window
	// --------------------------------------------------------------------
	screen.getViewportWidth = function() {
		if (document.documentElement && (!document.compatMode || document.compatMode=="CSS1Compat")) {
			return document.documentElement.clientWidth;
		}
		else if (document.compatMode && document.body) {
			return document.body.clientWidth;
		}
		return screen.zero(self.innerWidth);
	};
	
	// Get the height of the viewport (viewable area) in the browser window
	// --------------------------------------------------------------------
	screen.getViewportHeight = function() {
		if (!window.opera && document.documentElement && (!document.compatMode || document.compatMode=="CSS1Compat")) {
			return document.documentElement.clientHeight;
		}
		else if (document.compatMode && !window.opera && document.body) {
			return document.body.clientHeight;
		}
		return screen.zero(self.innerHeight);
	};

	return screen;
})();var Sort = (function(){
	var sort = {};
	sort.AlphaNumeric = function(a,b) {
		if (a==b) { return 0; }
		if (a<b) { return -1; }
		return 1;
	};

	sort.Default = sort.AlphaNumeric;
	
	sort.NumericConversion = function(val) {
		if (typeof(val)!="number") {
			if (typeof(val)=="string") {
				val = parseFloat(val.replace(/,/g,''));
				if (isNaN(val) || val==null) { val=0; }
			}
			else {
				val = 0;
			}
		}
		return val;
	};
	
	sort.Numeric = function(a,b) {
		return sort.NumericConversion(a)-sort.NumericConversion(b);
	};

	sort.IgnoreCaseConversion = function(val) {
		if (val==null) { val=""; }
		return (""+val).toLowerCase();
	};

	sort.IgnoreCase = function(a,b) {
		return sort.AlphaNumeric(sort.IgnoreCaseConversion(a),sort.IgnoreCaseConversion(b));
	};

	sort.CurrencyConversion = function(val) {
		if (typeof(val)=="string") {
			val = val.replace(/^[^\d\.]/,'');
		}
		return sort.NumericConversion(val);
	};
	
	sort.Currency = function(a,b) {
		return sort.Numeric(sort.CurrencyConversion(a),sort.CurrencyConversion(b));
	};
	
//	sort.DateConversion = function(val) {
//		// inner util function to parse date formats
//		function getdate(str) {
//			// inner util function to convert 2-digit years to 4
//			function fixYear(yr) {
//				yr = +yr;
//				if (yr<50) { yr += 2000; }
//				else if (yr<100) { yr += 1900; }
//				return yr;
//			};
//			var ret;
//			// YYYY-MM-DD
//			if (ret=str.match(/(\d{2,4})-(\d{1,2})-(\d{1,2})/)) {
//				return (fixYear(ret[1])*10000) + (ret[2]*100) + (+ret[3]);
//			}
//			// MM/DD/YY[YY] or MM-DD-YY[YY]
//			if (ret=str.match(/(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})/)) {
//				return (fixYear(ret[3])*10000) + (ret[1]*100) + (+ret[2]);
//			}
//			return 99999999; // So non-parsed dates will be last, not first
//		};
//		return getdate(val);
//	};

	sort.DateConversion = function(val) {
			// inner util function to parse date formats
			function getdate(str) {
				// inner util function to convert 2-digit years to 4
				var d = Date.parse(str); 
				if (d == NaN) {
					return Date.parse("01/01/1800");
				} else {
					return d;
				}
			};
			return getdate(val);
		};
	sort.Date = function(a,b) {
		return sort.Numeric(sort.DateConversion(a),sort.DateConversion(b));
	};

	return sort;
})();// Functions for interacting with Tables
// =====================================
var Table = {};

Table.VERSION = .955;
	
// Get a parent TABLE object reference from any element within it
// --------------------------------------------------------------
Table.getTable = function(o) {
	if (o==null) { 
		return o; 
	}
	return DOM.getParentByTagName(o,"TABLE");
};

// Resolve a table given an element reference, and make sure it has an ID
// ----------------------------------------------------------------------
Table.resolve = function(o) {
	if (o==null) { return null; }
	if (o.nodeName && o.nodeName!="TABLE") {
		o = this.getTable(o);
	}
	CSS.createId(o);
	return o;
};

// Find the table cell containing any passed-in element
// ----------------------------------------------------
Table.getCell = function(o) {
	if (o==null) { return null; }
	if (o.nodeName && o.nodeName!="TH" && o.nodeName!="TD") {
		o = DOM.getParentByTagName(o,["TD","TH"]);
	}
	return o;
};

// Expand all hidden TBODY tags	
// ----------------------------
Table.expandBodies = function(t) {
	var bodies = this.getBodies(t);
	if (bodies==null) { 
		return bodies; 
	}
	for (var i=0; i<bodies.length; i++) {
		if (CSS.get(bodies[i],"display")=="none") {
			CSS.set(bodies[i],"display","block");
		}
	}
};

// Get all the tbody elements in a table
// -------------------------------------
Table.getBodies = function(t) {
	if (t==null) { 
		return t; 
	}
	if (t.getElementsByTagName) {
		return t.getElementsByTagName("TBODY");
	}
	return null;
};

// Get all the thead elements in a table
// -------------------------------------
Table.getHeads = function(t) {
	if (t==null) { 
		return t; 
	}
	if (t.getElementsByTagName) {
		return t.getElementsByTagName("THEAD");
	}
	return null;
};

// Expand all table rows and hide the row containing the onclick action
// --------------------------------------------------------------------
Table.expandRowClicked = function(o) {
	var t, tr;
	if ((tr = DOM.getParentByTagName(o,"TR"))==null) { 
		return; 
	}
	if ((t = this.getTable(tr))==null) { 
		return; 
	}
	this.expandBodies(t);
	CSS.setStyle(tr,"display","none");
};

// Run a function against each cell in a table header, usually to add
// or remove css classes based on sorting, filtering, etc.
// ------------------------------------------------------------------
Table.processHeaderCells = function(t, func) {
	t = this.resolve(t);
	if (t==null) { return; }
	var theads = this.getHeads(t);
	for (var i=0; i<theads.length; i++) {
		var th = theads[i];
		if (th.rows && th.rows.length && th.rows.length>0) { 
			var rows = th.rows;
			var len = rows.length;
			for (var j=0; j<len; j++) { 
				var row = rows[j];
				if (row.cells && row.cells.length && row.cells.length>0) {
					var cells = row.cells;
					var len2 = cells.length;
					for (var k=0; k<len2; k++) {
						var cellsK = cells[k];
						func(cellsK);
					}
				}
			}
		}
	}
};

// Get the cellIndex value for a cell. This is only needed because of a Safari
// bug that causes cellIndex to exist but always be 0.
// Rather than feature-detecting each time it is called, the function will
// re-write itself the first time it is called.
// ---------------------------------------------------------------------------
Table.getCellIndex = function(td) {
	var tr = td.parentNode;
	var cells = tr.cells;
	if (cells && cells.length) {
		if (cells.length>1 && cells[cells.length-1].cellIndex && cells[cells.length-1].cellIndex>0) {
			this.getCellIndex = function(td) {
				return td.cellIndex;
			};
			return td.cellIndex;
		}
		for (var i=0; i<cells.length; i++) {
			if (tr.cells[i]==td) {
				return i;
			}
		}
	}
	return 0;
};

// Get the text value of a cell. Only use innerText if explicitly told to, because 
// otherwise we want to be able to handle sorting on inputs and other types
// -------------------------------------------------------------------------------
Table.getCellValue = function(td,useInnerText) {
	if (td==null) { 
		return null; 
	}
	if (useInnerText && defined(td.innerText)) {
		return td.innerText;
	}
	if (!td.childNodes) { 
		return ""; 
	}
	var childNodes = td.childNodes;
	var childNodesLength = childNodes.length;
	var node = null;
	var ret = "";
	for (var i=0; i<childNodesLength; i++) {
		node = childNodes[i];
		if (node.nodeType && node.nodeType==1) {
			if (node.nodeName=="INPUT" && defined(node.value)) {
				if (node.type && (node.type!="checkbox" || node.checked)) {
					ret += node.value;
				}
			}
			else if (node.nodeName=="SELECT" && node.selectedIndex>=0 && node.options) {
				// Sort select elements by the visible text
				ret += node.options[node.selectedIndex].text;
			}
			else if (node.nodeName=="IMG" && node.name) {
				// Sort image elements by their NAME attribute
				ret += node.name;
			}
			else {
				ret += this.getCellValue(node);
			}
		}
		else {
			if (node.nodeType && node.nodeType==3) {
				if (defined(node.innerText)) {
					ret += node.innerText;
				}
				else if (defined(node.nodeValue)) {
					ret += node.nodeValue;
				}
			}
		}
	}
	return ret;
};

// Consider colspan and rowspan values in table header cells to calculate the actual cellIndex
// of a given cell
// -------------------------------------------------------------------------------------------
Table.tableHeaderIndexes = {};
Table.getActualCellIndex = function(tableCellObj) {
	var tableObj = this.getTable(tableCellObj);
	var cellCoordinates = tableCellObj.parentNode.rowIndex+"-"+this.getCellIndex(tableCellObj);

	// If it has already been computed, return the answer from the lookup table
	if (typeof(this.tableHeaderIndexes[tableObj.id])!='undefined') {
		return this.tableHeaderIndexes[tableObj.id][cellCoordinates];			
	} 

	var matrix = [];
	this.tableHeaderIndexes[tableObj.id] = {};
	var thead = tableObj.getElementsByTagName('THEAD')[0];
	var trs = thead.getElementsByTagName('TR');

	// Loop thru every tr and every cell in the tr, building up a 2-d array "grid" that gets
	// populated with an "x" for each space that a cell takes up. If the first cell is colspan
	// 2, it will fill in values [0] and [1] in the first array, so that the second cell will
	// find the first empty cell in the first row (which will be [2]) and know that this is
	// where it sits, rather than its internal .cellIndex value of [1].
	for (var i=0; i<trs.length; i++) {
		var cells = trs[i].cells;
		for (var j=0; j<cells.length; j++) {
			var c = cells[j];
			var rowIndex = c.parentNode.rowIndex;
			var cellId = rowIndex+"-"+this.getCellIndex(c);
			var rowSpan = c.rowSpan || 1;
			var colSpan = c.colSpan || 1
			var firstAvailCol;
			if(typeof(matrix[rowIndex])=="undefined") { 
				matrix[rowIndex] = []; 
			}
			var m = matrix[rowIndex];
			// Find first available column in the first row
			for (var k=0; k<m.length+1; k++) {
				if (typeof(m[k])=="undefined") {
					firstAvailCol = k;
					break;
				}
			}
			this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol;
			for (var k=rowIndex; k<rowIndex+rowSpan; k++) {
				if(typeof(matrix[k])=="undefined") { 
					matrix[k] = []; 
				}
				var matrixrow = matrix[k];
				for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) {
					matrixrow[l] = "x";
				}
			}
		}
	}
	// Store the map so future lookups are fast.
	return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
};


// Sort all rows in each TBODY (tbodies are sorted independent of each other)
// --------------------------------------------------------------------------
Table.lastSortedColumn = {};
Table.SortedAscendingClassName = "TableSortedAscending";
Table.SortedDescendingClassName = "TableSortedDescending";
Table.SortableClassName = "sortable";

Table.sort = function(t,args) {
	var colIndex, sortType, descending, rowShade, ignoreHiddenRows;
	if (!defined(args)) { 
		args = {}; 
	}

	// Save a ref to the original object passed in
	var origT = t;

	if (t==null) { 
		return; 
	}
	// Resolve the table
	t = this.resolve(t);

	// Resolve actual colIndex
	if (defined(args['colIndex'])) {
		colIndex = args['colIndex'];
	} else if (defined(origT) && defined(origT.cellIndex)) {
		colIndex = this.getActualCellIndex(origT);
	} else {
		colIndex = 0;
	}

	// Resolve sortType
	sortType = ((!defined(args['sortType'])) || (typeof(args['sortType'])!="function")) ? Sort.Default : args['sortType'];

	// Resolve descending
	if (defined(this.lastSortedColumn[t.id]) && this.lastSortedColumn[t.id]['index']==colIndex) {
		descending = !(this.lastSortedColumn[t.id]['descending']);
	} else if (defined(args['descending']) && typeof(args['descending'])=="boolean") { 
		descending = args['descending'];
	} else {
		descending = false;
	}

	// Resolve whether or not to consider hidden rows when shading alternate rows
	ignoreHiddenRows = (defined(args['ignoreHiddenRows'])) ? args['ignoreHiddenRows'] : false;

	// Class name corresponding to the sort order
	var sortedAscendingClassName = this.SortedAscendingClassName;
	var sortedDescendingClassName = this.SortedDescendingClassName;
	var sortableClassName = this.SortableClassName;
	var sortedClassName = descending?sortedDescendingClassName:sortedAscendingClassName;

	// If standard sorting functions are used, convert each cell value in advance using a conversion
	// function, then sort by alphanumeric so sorting is much faster
	var sortConversion = false;
	if (sortType==Sort.Default || sortType==Sort.AlphaNumeric) {
		sortType=Sort.AlphaNumeric;
	} else if (sortType==Sort.IgnoreCase) {
		sortConversion = Sort.IgnoreCaseConversion;
		sortType=Sort.AlphaNumeric;
	} else if (sortType==Sort.Numeric) {
		sortConversion = Sort.NumericConversion;
		sortType=Sort.AlphaNumeric;
	} else if (sortType==Sort.Currency) {
		sortConversion = Sort.CurrencyConversion;
		sortType=Sort.AlphaNumeric;
	} else if (sortType==Sort.Date) {
		sortConversion = Sort.DateConversion;
		sortType=Sort.AlphaNumeric;
	}

	// Store the last sorted column so clicking again will reverse the sort order
	this.lastSortedColumn[t.id] = {'index':colIndex, 'descending':descending};

	// Loop through all THEADs and remove sorted class names, then re-add them for the colIndex
	// that is being sorted
	var self = this;
	this.processHeaderCells(t,
		function(cell) {
			if (CSS.hasClass(cell,sortableClassName)) {
				CSS.removeClass(cell,sortedAscendingClassName);
				CSS.removeClass(cell,sortedDescendingClassName);
				// If the computed colIndex of the cell equals the sorted colIndex, flag it as sorted
				if (colIndex==self.getActualCellIndex(cell)) {
					CSS.addClass(cell,sortedClassName);
				}
			}
		}
	);

	// Sort each tbody independently
	var bodies = this.getBodies(t);
	if (bodies==null || bodies.length==0) { return; }
	for (var i=0; i<bodies.length; i++) {
		var tb = bodies[i];
		var tbrows = tb.rows;
		var tbrowslength = tbrows.length;
		var rows = [];

		// If there are no inputs in the tbody, feel free to use innerText which is faster
		var useInnerText = (tb.getElementsByTagName('INPUT').length==0);

		// Create a separate array which will store the converted values and refs to the
		// actual tows. This is the array that will be sorted.
		var cRow;
		var cRowIndex=0;
		if (cRow=tbrows[cRowIndex]){
			// Funky loop style because it's faster in IE
			do {
				if (rowCells = cRow.cells) {
					var cellValue = (rowCells&&colIndex<rowCells.length)?this.getCellValue(rowCells[colIndex],useInnerText):null;
					if (sortConversion) cellValue = sortConversion(cellValue);
					rows[cRowIndex] = [cellValue,tbrows[cRowIndex]];
				}
			} while (cRow=tbrows[++cRowIndex])
		}

		// Do the actual sorting
		var newSortFunc = function(a,b) {
			return (descending)?sortType(b[0],a[0]):sortType(a[0],b[0]);
		};
		rows.sort(newSortFunc);

		// Move the rows to the correctly sorted order. Appending an existing DOM object
		// just moves it!
		var cRow;
		var cRowIndex=0;
		if (cRow=rows[cRowIndex]){
			do { tb.appendChild(cRow[1]); } while (cRow=rows[++cRowIndex])
		}
	}

	// Re-shade alternate rows if a class name was supplied
	if (defined(args['rowShade'])) {
		this.shadeOddRows(t,args['rowShade'],ignoreHiddenRows);
	}
};

// Filter all rows in each TBODY
// -----------------------------
// TODO: Finish up and Test table filtering!

Table.FilteredClassName = "TableFiltered";
Table.FilterableClassName = "filterable";
Table.Filters = {};

Table.filter = function(t,filters,args) {
	var colIndex, rowShade, filter, allFilters;
	var reset = false; // If null is passed in, reset the whole table
	if (!defined(args)) { args = {}; }
	// Filters can either be sent in one at a time or as a group
	if (!defined(filters)) { return; }
	// Allow for passing a select list in as the filter, since this is common design
	if (filters && filters.nodeName && filters.nodeName=="SELECT" && filters.type) {
		if (filters.type=="select-one" && defined(filters.selectedIndex) && filters.selectedIndex>-1) {
			var sel = filters;
			filters = {};
			filters.filter = sel.options[sel.selectedIndex].value;
		}
	}
	if (filters && filters.nodeName && filters.nodeName=="INPUT" && filters.type) {
		var sel = filters;
		filters = {};
		filters.filter = '/' + sel.value + '/';
	}
	if (isObject(filters) && !isArray(filters)) {
		filters = [filters];
	}
	else if (filters==null) {
		reset = true;
		filters = [];
	}
	else { return; }

	// Find the cell
	t = this.getCell(t);
	
	// Resolve colIndex for each filter
	for (var i=0; i<filters.length; i++) {
		colIndex = filters[i].colIndex;
		if (!defined(colIndex) && defined(t) && defined(t.cellIndex)) {
			filters[i].colIndex = this.getCellIndex(t);
		}
	}

	if (t==null) { return; }
	// Resolve the table
	t = this.resolve(t);

	// Update the list of all active filters for this table
	if (!defined(this.Filters[t.id]) || reset) {
		this.Filters[t.id] = {};
	}
	var allFilters = this.Filters[t.id];
	for (var i=0; i<filters.length; i++) {
		filter = filters[i];
		if (filter.filter==null || filter.filter=="") {
			delete allFilters[filter.colIndex];
		}
		else {
			allFilters[filter.colIndex] = filter.filter;
		}
	}

	// Filter all tbodies of the table
	var bodies = this.getBodies(t);
	if (bodies==null || bodies.length==0) { return; }
	for (var i=0; i<bodies.length; i++) {
		var tb = bodies[i];
		for (var j=0; j<tb.rows.length; j++) {
			var row = tb.rows[j];
			if (reset) {
				row.style.display="";
			}
			else if (row.cells) {
				var cells = row.cells;
				var cellsLength = cells.length;
				// Test each filter
				var hide = false;
				for (colIndex in allFilters) {
					if (!hide) {
						filter = allFilters[colIndex];
						if (colIndex < cellsLength) {
							var val = this.getCellValue(cells[colIndex]);
							if (filter.charAt(0)=="/" && val.search) {
								hide = (val.search(new RegExp(filter.substring(1,filter.length-1), "i"))<0);
							}
							else if (val!=filter) {
								hide = true;
							}
						}
					}
				}
				if (hide) {
					row.style.display = "none";
				}
				else {
					row.style.display="";
				}
			}
		}
	}

	// Loop through all THEADs and add filtered class names
	var self = this;
	this.processHeaderCells(t,
		function(cell) {
			if (defined(allFilters[self.getCellIndex(cell)]) && CSS.hasClass(cell,self.FilterableClassName)) {
				CSS.addClass(cell,self.FilteredClassName);
			}
			else {
				CSS.removeClass(cell,self.FilteredClassName);
			}
		}
	);

	// Shade rows if a class name was supplied
	if (defined(args['rowShade'])) {
		this.shadeOddRows(t,args['rowShade']);
	}
};

// Shade alternate rows
// --------------------
Table.shadeOddRows = function(t,className,ignoreHiddenRows) { 
	if (t==null) { 
		return;
	}
	ignoreHiddenRows = (defined(ignoreHiddenRows) && typeof(ignoreHiddenRows)=="boolean") ? ignoreHiddenRows : false;
	t = this.resolve(t);
	var bodies = this.getBodies(t);
	if (bodies==null || bodies.length==0) { 
		return; 
	}
	for (var i=0; i<bodies.length; i++) {
		var tb = bodies[i];
		var tbrows = tb.rows;
		var cRowIndex=0;
		var cRow;
		var displayedCount=0;
		if (cRow=tbrows[cRowIndex]){
			do {
				if (ignoreHiddenRows || CSS.getStyle(cRow,"display")!="none") {
					if (displayedCount++%2==0) { 
						CSS.removeClass(cRow,className); 
					}
					else { 
						CSS.addClass(cRow,className); 
					}
				}
			} while (cRow=tbrows[++cRowIndex])
		}
	}
};

// "Page" a table by showing only a subset of the rows
// ---------------------------------------------------
Table.pages = {};
Table.page = function(t,pageIndex,pageSize,args) {
	if (!defined(args)) { args = {}; }
	if (!defined(pageSize) || typeof(pageSize)!="number" || pageSize==0) {
		pageSize = 25; // arbitrary default
	}
	if (!defined(pageIndex) || typeof(pageIndex)!="number") {
		pageIndex = 0;
	}

	var startRow = pageIndex*pageSize;
	var endRow = startRow + pageSize - 1;
	
	if (t==null) { return; }
	// Resolve the table
	t = this.resolve(t);

	// Assumption: only one tbody!
	var bodies = this.getBodies(t);
	if (bodies==null || bodies.length==0) { return; }
	var tb = bodies[0];

	// Don't let the page go past the beginning
	if (startRow<0) {
		pageIndex = 0;
		startRow = 0;
		endRow = startRow + pageSize - 1;
	}
	// Don't let the page go past the end
	if (startRow > tb.rows.length) {
		pageIndex = Math.floor(tb.rows.length/pageSize);
		if (pageIndex==tb.rows.length/pageSize) {
			pageIndex--;
		}
		startRow = pageIndex * pageSize;
		endRow = startRow + pageSize;
	}

	// Store the table's current state	
	this.pages[t.id] = { 'pageIndex':pageIndex, 'pageSize':pageSize };

	for (var i=0; i<tb.rows.length; i++) {
		var row = tb.rows[i];
		if (i<startRow || i>endRow) {
			row.style.display="none";
		}
		else {
			row.style.display="";
		}
	}

	// Shade rows if a class name was supplied
	if (defined(args['rowShade'])) {
		this.shadeOddRows(t,args['rowShade']);
	}
}

Table.pageNext = function(t,pageSize,args) {
	t = this.resolve(t);
	if (defined(Table.pages[t.id])) {
		var pages = Table.pages[t.id];
		var newPage = pages.pageIndex+1;
		this.page(t,newPage,pageSize || pages.pageSize,args);
		return newPage;
	}
	else {
		this.page(t,1,pageSize,args);
		return 1;
	}
	return -1;
};

Table.pagePrevious = function(t,pageSize,args) {
	t = this.resolve(t);
	if (defined(Table.pages[t.id])) {
		var pages = Table.pages[t.id];
		var newPage = pages.pageIndex-1;
		this.page(t,newPage,pageSize || pages.pageSize,args);
		return newPage;
	}
	else {
		this.page(t,0,pageSize,args);
		return 0;
	}
	return -1;
};