function SortableTable(oTable, oHeaderTable, oSortTypes, oSortColumns, sortColumn, descending) {

	this.reStrip = new RegExp();
	this.reStrip.compile("(<([^>]+)>)", "ig");

	this.element = oTable;
	this.tHead = oHeaderTable;
	if (this.tHead == null) {
		this.tHead = oTable.tHead;
	}
	this.tBody = oTable.tBodies[0];
	this.document = oTable.ownerDocument || oTable.document;
	
	this.sortColumn = sortColumn;
	this.descending = descending;
	
	var oThis = this;
	this._headerOnclick = function (e) {
		oThis.headerOnclick(e);
	};
	
	// only IE needs this
	var win = this.document.defaultView || this.document.parentWindow;
	this._onunload = function () {
		oThis.destroy();
	};
	if (win && typeof win.attachEvent != "undefined") {
		win.attachEvent("onunload", this._onunload);
	}
	
	if (this.tHead != null) {
		this.initHeader(oSortTypes || [], oSortColumns || []);
	}
}

SortableTable.gecko = navigator.product == "Gecko";
SortableTable.msie = /msie/i.test(navigator.userAgent);
// Mozilla is faster when doing the DOM manipulations on
// an orphaned element. MSIE is not
SortableTable.removeBeforeSort = SortableTable.gecko;

SortableTable.prototype.onsort = function () {};

// adds arrow containers and events
// also binds sort type to the header cells so that reordering columns does
// not break the sort types
SortableTable.prototype.initHeader = function (oSortTypes, oSortColumns) {
	var cells = this.tHead.rows[0].cells;
	var l = cells.length;
	var c;
	for (var i = 0; i < l; i++) {
		c = cells[i];
		if (oSortTypes[i] != null) {
			c._sortType = oSortTypes[i] == null ? 'NONE' : oSortTypes[i];
			c._sortColumn = oSortColumns[i] == null ? '' : oSortColumns[i];
			if (oSortTypes[i].toUpperCase() != 'NONE') {
				c.style.cursor = "hand";
				if (typeof c.addEventListener != "undefined")
					c.addEventListener("click", this._headerOnclick, false);
				else if (typeof c.attachEvent != "undefined")		
					c.attachEvent("onclick", this._headerOnclick);
			}
		}
	}
	this.updateHeaderArrows();
};

// remove arrows and events
SortableTable.prototype.uninitHeader = function () {
	var cells = this.tHead.rows[0].cells;
	var l = cells.length;
	var c;
	for (var i = 0; i < l; i++) {
		c = cells[i];
		if (c.lastChild != null)
			c.removeChild(c.lastChild);
		if (typeof c.removeEventListener != "undefined")
			c.removeEventListener("click", this._headerOnclick, false);
		else if (typeof c.detachEvent != "undefined")
			c.detachEvent("onclick", this._headerOnclick);
	}
};

SortableTable.prototype.updateHeaderArrows = function () {
	if (this.tHead != null) {
		var cells = this.tHead.rows[0].cells;
		var l = cells.length;
		var indicator;
		for (var i = 0; i < l; i++) {
			indicator = cells[i].lastChild;
			if (indicator.tagName != 'SPAN') {
				indicator = this.document.createElement("SPAN");
				indicator.style.fontFamily = "webdings";
				indicator.style.fontSize = "10px";
				indicator.style.padding = "0";
				indicator.className = "sort";
				cells[i].appendChild(indicator);
			}
			if (i == this.sortColumn)
				indicator.innerText = this.descending ? "5" : "6";
			else
				indicator.innerText = "";			
		}
	}
};

SortableTable.prototype.headerOnclick = function (e) {
	// reserve the last 6 pixels of the cell for column resizing, not sorting.
	var el = document.elementFromPoint(e.clientX, e.clientY);
	if ((el.tagName == "TD") && ((event.srcElement.offsetWidth - event.offsetX) < 7)) {
		return;
	}

	// find TD element
	el = e.target || e.srcElement;
	while (el.tagName != "TD")
		el = el.parentNode;
		
	this.sort(el.cellIndex);
	
	// Save sort preferences
	el = document.getElementById(this.element.id + "SortColumn");
	if (el != null) {
		el.setAttribute(this.element.id + "SortColumn", this.sortColumn);
		el.setAttribute(this.element.id + "SortOrder", this.descending ? 1 : 0);
		el.save(document.title + this.element.id);
	}
};

SortableTable.prototype.getSortType = function (nColumn) {
	if (this.tHead != null) {
		var cell = this.tHead.rows[0].cells[nColumn];
		var val = cell._sortType;
		if (val != "")
			return val;
	}
	return "String";
};

SortableTable.prototype.getSortColumn = function (nColumn) {
	if (this.tHead != null) {
		var cell = this.tHead.rows[0].cells[nColumn];
		var val = cell._sortColumn;
		if (val != "")
			return val;
	}
	if (this.element.rows[0] == undefined)
	   return SortableTable.getDataFld(null);
	else
	  return SortableTable.getDataFld(this.element.rows[0].cells[nColumn]);
};

// only nColumn is required
// if sSortType is left out the sort type is found from the sortTypes array
// if bDescending is left out the old value is taken into account

SortableTable.prototype.sort = function (nColumn, sSortType, bDescending) {
	if (sSortType == null)
		sSortType = this.getSortType(nColumn);

	// exit if None	
	if (sSortType.toUpperCase() == "NONE")
		return;
	
	if (bDescending == null) {
		if (this.sortColumn != nColumn)
			this.descending = false;
		else
			this.descending = !this.descending;
	}
	else
		this.descending = bDescending;
	
	this.sortColumn = nColumn;
	
	if (typeof this.onbeforesort == "function")
		this.onbeforesort();
		
	var a = [];
	var sSortColumn = this.getSortColumn(nColumn);
	var f = this.getSortFunction(sSortType, nColumn);
	var dsObject = this.document.getElementById(this.element.dataSrc.substr(1, this.element.dataSrc.length));
	
	if (dsObject == null)
		a = this.getCache(sSortType, nColumn);
	else if ((dsObject.getAttribute("remote") == null) || (dsObject.getAttribute("remote") == 0))
		a = this.getDataSrcCache(sSortType, sSortColumn);
	else {
		// Server-side Sort
		var start = this.element.getAttribute("startRow");
		if (start == null) {
			start = 1;
		}
		var limit = this.element.getAttribute("dataPageSize");
		var dir = this.descending ? "DESC" : "ASC";
		eval(dsObject.id + "_AsyncLoader(start, limit, sSortColumn, dir)");
	}
		
	var l = a.length;

	if (l > 0) {
		// Client-side Sort
		var tBody = this.tBody;
		
		a.sort(f);
		
		if (this.descending)
			a.reverse();
		
		if (SortableTable.removeBeforeSort) {
			// remove from doc
			var nextSibling = tBody.nextSibling;
			var p = tBody.parentNode;
			p.removeChild(tBody);
		}
	
		// insert in the new order
		if (dsObject == null) {
			for (var i = 0; i < l; i++) 
				tBody.appendChild(a[i].element);
		}
		else {
			// Create new XML document sorted in correct order, then load the DataSrc
			if (dsObject.recordset.state != 0) {
				dsObject.recordset.MoveFirst();
				if ((dsObject.XMLDocument.firstChild.hasChildNodes) && (dsObject.XMLDocument.firstChild.getAttribute("Error") != 1)) {
					var domObj = new ActiveXObject("Microsoft.XMLDOM");
					domObj.preserveWhiteSpace = true;
					var objRoot = domObj.createElement("records");
					domObj.appendChild(objRoot);
					var vntArray = dsObject.recordset.GetRows(l);
					for (var i = 0; i < l; i++) {
						objRecord = domObj.createElement("record");
						for (var j = 0; j < dsObject.recordset.Fields.Count - 1; j++) {
							objField = domObj.createElement(dsObject.recordset.Fields.Item(j).Name);
							objField.text = vntArray.getItem(j, a[i].element) == null ? "" : vntArray.getItem(j, a[i].element);
							objRecord.appendChild(objField);
						}
						objRoot.appendChild(objRecord);
					}
					var oldEvent = dsObject.getAttribute("ondatasetcomplete");
					dsObject.ondatasetcomplete = "";
					dsObject.loadXML(domObj.xml);
					if (oldEvent != null) {
						dsObject.ondatasetcomplete = oldEvent;
					}
				}
			}
		}
		if (SortableTable.removeBeforeSort) {	
			// insert into doc
			p.insertBefore(tBody, nextSibling);
		}
	}
	
	this.updateHeaderArrows();
	
	this.destroyCache(a);
	
	// update hidden elements, if they're there
	var tID = this.element.id;
	var sc = this.document.getElementById(tID + "SortColumn");
	if (sc != null) {
		sc.value = this.sortColumn;
	}
	var so = this.document.getElementById(tID + "SortOrder");
	if (so != null) {
		so.value = this.descending == true ? 1 : 0;
	}
	
	if (typeof this.onsort == "function")
		this.onsort();
};

SortableTable.prototype.asyncSort = function (nColumn, sSortType, bDescending) {
	var oThis = this;
	this._asyncsort = function () {
		oThis.sort(nColumn, sSortType, bDescending);
	};
	window.setTimeout(this._asyncsort, 1);	
};

SortableTable.prototype.getCache = function (sType, nColumn) {
	var rows = this.tBody.rows;
	var l = rows.length;
	var a = new Array(l);
	var r;
	for (var i = 0; i < l; i++) {
		r = rows[i];
		a[i] = {
			value:		this.getRowValue(r, sType, nColumn),
			element:	r
		};
	};
	return a;
};

SortableTable.prototype.getDataSrcCache = function (sType, df) {
	var dsObject = this.document.getElementById(this.element.dataSrc.substr(1, this.element.dataSrc.length));
	var a = new Array(0);

	if ((dsObject != null) && (dsObject.recordset != undefined) && (dsObject.recordset.state != 0) && (dsObject.XMLDocument.firstChild.hasChildNodes) && (dsObject.XMLDocument.firstChild.getAttribute("Error") != 1)) {
		var l = dsObject.recordset.recordCount;
		if ((l <= 1) || (this.element.rows.length == 0)) {
			return a;
		}
		if (df.length == 0) {
			return a;
		}
		var a = new Array(l), s;
		dsObject.recordset.MoveFirst();
		for (var i = 0; i < l; i++) {
			s = this.stripHTML(dsObject.recordset.fields.item(df).value);
			a[i] = {
				value:		this.getValueFromString(s, sType),
				element:	i
			};
			dsObject.recordset.MoveNext();
		};
	};
	return a;
};

SortableTable.prototype.stripHTML = function (oldString) {
	if (oldString == null) {
		return "";
	}
	return oldString.replace(this.reStrip, "");
}

SortableTable.prototype.destroyCache = function (oArray) {
	var l = oArray.length;
	for (var i = 0; i < l; i++) {
		oArray[i].value = null;
		oArray[i].element = null;
		oArray[i] = null;
	}
}

SortableTable.prototype.getRowValue = function (oRow, sType, nColumn) {
	var s;
	var c = oRow.cells[nColumn];
	if (c == null)
		return "";
	if (typeof c.innerText != "undefined")
		s = c.innerText;
	else
		s = SortableTable.getInnerText(c);
	return this.getValueFromString(s, sType);
};

SortableTable.getInnerText = function (oNode) {
	var s = "";	
	var cs = oNode.childNodes;
	var l = cs.length;
	for (var i = 0; i < l; i++) {
		switch (cs[i].nodeType) {
			case 1: //ELEMENT_NODE
				s += SortableTable.getInnerText(cs[i]);
				break;
			case 3:	//TEXT_NODE
				s += cs[i].nodeValue;
				break;
		}
	}
	return s;
}

SortableTable.getDataFld = function (oNode) {
	var s = "";
	if (oNode == null) {
		return s;
	}
	var cs = oNode.getElementsByTagName("INPUT");
	var l = cs.length;
	for (var i = l - 1; i >= 0; i--) {
		if (s == "") {
			s = cs[i].dataFld;
		}
	}
	if (s == "") {
		var cs = oNode.getElementsByTagName("SPAN");
		var l = cs.length;
		for (var i = l - 1; i >= 0; i--) {
			if (s == "") {
				s = cs[i].dataFld;
			}
		}
		if (s == "") {
			cs = oNode.getElementsByTagName("LABEL");
			l = cs.length;
			for (i = l - 1; i >= 0; i--) {
				if (s == "") {
					s = cs[i].dataFld;
				}
			}
		}
	}
	return s;
}

SortableTable.prototype.toNumber = function (s) {
    return Number(s.replace(/[^0-9\.]/g, ""));
}

SortableTable.prototype.parseDate = function (s) {
	if (isNaN(Date.parse(s.replace(/\-/g, '/'))))
		return 0;
	return Date.parse(s.replace(/\-/g, '/'));
}

SortableTable.prototype.getValueFromString = function (sText, sType) {
	switch (sType) {
		case "Number":
			return this.toNumber(sText);
		case "CaseInsensitiveString":
			return sText.toUpperCase();
		case "Date":
			return this.parseDate(sText);
	}
	return sText;
};

SortableTable.prototype.getSortFunction = function (sType, nColumn) {
	return function compare(o1, o2) {
		var n1 = o1.value;
		var n2 = o2.value;
		
		if (isNaN(n1) && !isNaN(n2))
			return 1;
		if (!isNaN(n1) && isNaN(n2))
			return -1;
		if (!isNaN(n1) && !isNaN(n2)) {
			n1 = parseFloat(n1);
			n2 = parseFloat(n2);
		}
		if (n1 < n2)
			return -1;
		if (n2 < n1)
			return 1;
		return 0;
	};
};

SortableTable.prototype.destroy = function () {
	if (this.tHead != null) {
		this.uninitHeader();
	}
	var win = this.document.parentWindow;
	if (win && typeof win.detachEvent != "undefined") {	// only IE needs this
		win.detachEvent("onunload", this._onunload);
	}	
	this._onunload = null;
	this.element = null;
	this.tHead = null;
	this.tBody = null;
	this.document = null;
	this._headerOnclick = null;
	this.sortTypes = null;
	this._asyncsort = null;
	this.onsort = null;
};
