//FUNCTIONS FOR SORTING, SELECTING, ETC. TABLE ROWS
//
//Felipe M. L. Gasper

//UNBEKNOWNST TO MANY WEB AUTHORS, HTML'S class ATTRIBUTE IS ACTUALLY A
//SPACE-SEPARATED LIST OF CLASSES. THIS JAVASCRIPT LIBRARY TAKES ADVANTAGE OF
//THIS, SO I'M STATING IT UP-FRONT.

//TO ENABLE...
//
//STRIPING: GIVE A TABLE OR TABLE BODY 'striped' IN ITS HTML class ATTRIBUTE.
//	TABLE BODIES WITH 'notStriped' IN THEIR class WON'T BE STRIPED.
//ROW SELECTING: GIVE A TABLE OR TABLE BODY 'rowsSelectable' IN THEIR class.
//	TABLE BODIES WITH 'rowsNotSelectable' WILL NOT INHERIT SELECTABILITY.
//	SPECIFY INDIVIDUAL ROW SELECTABILITY WITH 'selectable'/'notSelectable'.
//	PRIORITY GIVEN TO ROWS, THEN BODIES, THEN TABLES
//SORTING: GIVE A TABLE 'sortable' IN ITS HTML CLASS FOR COLUMN-AT-A-TIME
//	SORTING. PUTTING 'multiSortable' WITH IT ALLOWS MULTI-COLUMN SORTS.
//(SORTING REQUIRES <thead> AND <tbody> TAGS)
//BUTTONS FOR DEFAULT SORT, SELECT ALL, AND UNSELECT ALL: ADD 'showButtons'
//	TO TABLE'S CLASS

//CSS STUFF:
//STRIPING: APPLIES 'even'/'odd' TO ROW class ATTRIBUTES AND 'striped'
//	TO TABLE BODIES AND TABLES THAT ARE STRIPED
//ROW SELECTING: APPLIES 'selected' TO ROW class ATTRIBUTES AND 'selectable'
//	TO ROWS THAT CAN BE SELECTED
//SORTING: APPLIES 'unsorted'/'sortedAsc'/'sortedDesc' TO CELLS IN TABLE HEADS
//	AND BODIES AND 'sortable' TO THE APPROPRIATE TABLE HEAD CELLS. NOTE THAT
//	ALL HEADERS AND CELLS IN THEAD AND SORTABLE TBODY ELEMENTS WILL GET
//	'unsorted' ADDED TO THEIR CLASS, REGARDLESS OF WHETHER THEY'RE 'sortable'.
//	THIS ALLOWS CSS TO DISTINGUISH BETWEEN SORTABLE UNSORTED
//	AND NON-SORTABLE UNSORTED.
//	ALSO APPLIES 'sorting' TO THE TABLE DURING SORTS.
//BUTTONS: HAVE 'defaultSortButton', 'selectAllButton', & 'unselectAllButton'
//	IN THEIR RESPECTIVE CLASSES. BUTTONS ARE ADDED IMMEDIATELY BEFORE
//	THE TABLE IN THE DOCUMENT TREE. USE CSS TO STYLE THEIR APPEARANCE.

//JAVASCRIPT STUFF:
//STRIPING: SETS 'striped' TO BOOLEAN true FOR STRIPED TABLE BODIES AND TABLES
//   CREATES A stripe METHOD ON ALL TBODIES WITH striped SET TO true
//ROW SELECTING: SETS 'selected' AND 'selectable' PROPERTIES FOR ROWS, AND
//	'anyRowsSelectable' FOR TABLES AND TBODIES THAT ARE CSS 'rowsSelectable'
//SORTING: SETS 'sortable', 'multiSortable', 'sorts', AND 'sortTbody' ON
//	TABLES AND 'sortable' ON HEADER CELLS. 'sorts' IS AN ARRAY OF Sort
//	OBJECTS, EACH OF WHICH HAS 'index', 'order', AND 'asc' PROPERTIES.

//TO SPECIFY SORT CRITERIA FOR A COLUMN:
//IF tableObj REPRESENTS THE TABLE ON WHICH YOU'RE ADDING THE SORT:
//	DEFINE tableObj.sorterFuncs ARRAY, WITH INDICES CORRESPONDING TO FUNCTIONS
//		THAT GENERATE SORT VALUES (parseInt, parseFloat, ETC.)
//	YOU MAY DEFINE A tableObj.defaultSorterFunc FUNCTION.
//	ADDITIONALLY, SET A COLUMN'S sorterFuncs ENTRY TO false TO MAKE IT
//		NON-SORTABLE.
//	FOR EXAMPLE, [parseInt, String, false] WOULD BE USED FOR A TABLE
//		WHOSE COLUMNS ARE INTEGERS, STRINGS, AND NON-SORTABLE
//THESE VALUES SHOULD BE SET BEFORE THIS SCRIPT EXECUTES.

//EITHER THE LAST TABLE BODY WITH 'sortable' IN ITS class ATTRIBUTE OR THE
//FIRST TABLE BODY (IF NONE IS SPECIFIED AS 'sortable') WILL BE USED AS THE
//TABLE BODY ON WHICH TO SORT. BY THIS METHOD YOU CAN SEPARATE A 'TOTALS' ROW
//(OR SIMILAR THINGIE) NOT TO BE SORTED.

//YOU MAY SPECIFY tableObj.onrowselect/tableObj.onrowunselect FUNCTIONS.
//THESE FUNCTIONS RECEIVE THE ROW SELECTED/UNSELECTED AS THE 1ST PARAMETER.

//YOU CAN CALL superTable.sortDefault(table), superTable.selectAll(table),
//AND superTable.unselectAll(table)

//YOU CAN HAVE OTHER BUTTONS APPEAR WITH THEM BY ADDING THEM TO THE
//buttons ARRAY IN THE TABLE OBJECT. NOTE THAT push, pop, shift, AND unshift
//ARE OVERRIDDEN FOR THIS ARRAY SUCH THAT BUTTONS AUTOMATICALLY APPEAR IN THE
//DOCUMENT.

//ROW SELECTING ALLOWS CLICK-DRAG SELECTING OF MULTIPLE (ADJACENT) ROWS
//AND INTELLIGENTLY "SKIPS OVER" NON-SELECTABLE ROWS.

//CLICKS THAT GO TO LINKS, FORM ELEMENTS, ETC. IN TABLES WON'T SORT OR SELECT.

//BROWSER BUGS:
//SAFARI: -allRows, allCells, realCellIndex USED TO SIDESTEP BUGS IN SAFARI'S
//           DOM IMPLEMENTATION.
//        -SAFARI THINKS TEXT NODES CAN RECEIVE CLICKS. MAYBE THEY CAN, BUT
//           THIS LIBRARY IGNORES SUCH FOOLISHNESS.
//IE: -SINCE :hover ISN'T SUPPORTED, THERE'S NO (PALATABLE) WAY OF GETTING
//       FLOAT HIGHLIGHTS. DEAN EDWARDS'S IE7 LIBRARY MIGHT HELP HERE.

//----------------------------------------------------------------------
//BEGIN CODE

//FINDS OUT IF THE BROWSER SCREWS UP TABLE ELEMENT COUNTS (SAFARI)
function domCheck() {
	var testTable = document.createElement('table');
	var testThead = document.createElement('thead');
	var testTfoot = document.createElement('tfoot');
	var testTbody = document.createElement('tbody');

	testTable.appendChild(testThead);
	testTable.appendChild(testTfoot);
	testTable.appendChild(testTbody);

	for (var r=0; r < 3; r++) {
		var newRow = document.createElement('tr');
		for (var c=0; c < 3; c++) {
			newRow.appendChild(document.createElement('th'));
			newRow.appendChild(document.createElement('td'));
		}
		testTbody.appendChild(newRow);
	}

	window.rowsOk = (testTbody.rows.length == testTable.rows.length)
		&& (testTbody.rows.length == 3)
		&& (testTbody.rows[0].parentNode === testTbody)
	;

	window.cellsOk = (testTbody.rows[0].cells.length == 6);
	window.cellIndexOk = testTbody.rows[0].cells[3]
	   && (testTbody.rows[0].cells[3].cellIndex == 3);
}

//RETURNS AN ARRAY OF ALL ROWS (tr) IN AN OBJECT
//NECESSARY FOR BROWSERS THAT SCREW UP TABLE ROWS
function allRows(object) {
	var rowsCollection = object.getElementsByTagName('tr');

	return makeArray(rowsCollection);
}

//RETURNS AN ARRAY OF ALL CELLS (td, th) IN AN OBJECT
//NECESSARY FOR BROWSERS THAT SCREW UP CELL PROPERTIES
function allCells(object) {
	var tds = object.getElementsByTagName('td');
	var ths = object.getElementsByTagName('th');

	return makeArray(tds).concat(makeArray(ths));
}

//APPLY STRIPING
//USING sectionRowIndex IS REALLY, REALLY SLOW HERE....AT LEAST ON MOZ
superTable = {};
superTable.stripeTbody = function(theTbody) {
	var theRows = window.rowsOk ? theTbody.rows : allRows(theTbody);
	for (var r=0; r < theRows.length; r++) {
		var curRow = theRows[r];
		classRemove(curRow,myClasses.odd,'even');
		classAdd(curRow, (r % 2) ? 'odd' : 'even');
	}
}


//FIND THE FIRST PARENT OF elem tagName MATCHING ONE TAG NAME GIVEN
//(INCLUDE elem ITSELF)
//EX.: theCell = bubbleUpFind(e.target,'td','th');
function bubbleUpFind(elem) {
	var curElem = elem;

	//FIRST ARGUMENT ISN'T A TAG NAME, AND WE WANT CASE-INSENSITIVE MATCHING
	var tagNames = makeArray(arguments).slice(1).process(function(tagName) {
		return tagName.toLowerCase();
	});

	while (curElem && (curElem.nodeType == 1)
	   && (!tagNames.has(curElem.tagName.toLowerCase()))) {
		curElem = curElem.parentNode || undefined;
	}

	//THIS IS WEIRD BECAUSE OF IE
	return curElem && (curElem.nodeType == 1) && curElem;
}

superTable.findSortIndex = function(hdrCell,cacheyn) {
	var thead = bubbleUpFind(hdrCell,'thead');
	if (cacheyn) {
		var indexCache = [];
	}
	var theadRows = window.rowsOk ? thead.rows : allRows(thead);
	var sortIndex = false;

	//WHICH COLUMNS FOR EACH HDRROW ARE TAKEN BY A PRECEDING ROW'S ELEMENTS.
	//ACCOMODATES FOR rowSpan > 1
	var nonColumns = [[]];
	nonColumns.add = function(cellObject,startCol) {
		var overbiteHeight = cellObject.rowSpan - 1;
		var overbiteWidth = cellObject.colSpan - 1;

		//FOR EACH "OVERBITE" ROW....
		for (var rr=curRow+1; rr <= curRow+overbiteHeight; rr++) {
			//FOR EACH COLUMN OF "OVERBITE"...
			for (var hh=startCol; hh <= startCol+overbiteWidth; hh++) {
				if (isUndefined(this[rr]))
					this[rr] = [];
				this[rr].push(hh);
			}
		}
	}

	//GO THROUGH AND COMPUTE SORT INDICES UNTIL WE'VE FOUND THE RIGHT <th>
	for (var r=0; (r < theadRows.length) && (sortIndex === false); r++) {
		var curRow = theadRows[r];
		var rowCells = cellsOk ? curRow.cells : allCells(curRow);

		//THIS IS THE SORT INDEX, SPECIAL FOR OUR CODE
		var curColIndex = 0;

		//c IS THE SAME AS DOM'S colIndex
		for (var c=0; (c < rowCells.length) && (sortIndex === false); c++) {
			var curCell = rowCells[c];

			//INCREMENT curColIndex IF ITS CURRENT VALUE IS A NONCOLUMN
			while (nonColumns[r].has(curColIndex))
				curColIndex++;

			sortIndex = (curCell === hdrCell) && curColIndex;
			if (cacheyn) {
				if (indexCache[curColIndex]) {
					indexCache[curColIndex].push(curCell);
				} else {
					indexCache[curColIndex] = [curCell];
				}
			}

			//SAFARI STUPIDISM
			if (curCell.colSpan == 0) curColIndex++;
			else curColIndex += curCell.colSpan;

			if (curCell.rowSpan > 1) nonColumns.add(curCell,curColIndex);
		}
	}

	return (cacheyn ? indexCache : sortIndex);
}
superTable.sortIndexCache = function(tableObj) {
	var lastTheadCell = allCells(allRows(tableObj.tHead).last()).last();
	return superTable.findSortIndex(lastTheadCell, true);
}

superTable.sortTbody = function(tableObj) {
	var sortTbody = tableObj.tBodies[0];
	for (var b=1; b < tableObj.tBodies.length; b++) {
		var curTbody = tableObj.tBodies[b];
		if (classHas(curTbody,'sortable')) {
			sortTbody = curTbody;
		}
	}

	return sortTbody;
}

superTable.sortDefault = function(tableObj) {
	var oldSorts = tableObj.sorts;
	var originalRows = oldSorts && oldSorts.originalRows;

	if (originalRows) {
		this.reinsertRows(originalRows);

		tableObj.sorts = this.newSortList(tableObj);
		oldSorts.process(function(s) { s.updateCss() });
	}
}

superTable.isStriped = function(elem) {
	return classHas(elem,'striped') || classHas(elem.parentNode,'striped');
}
superTable.isSortable = function(elem) {
	return classHas(elem,'sortable') || classHas(elem.parentNode,'sortable');
}
superTable.isSelectable = function(row) {
	return (row.parentNode.tagName.toLowerCase() != 'thead') && (

	//PURE AND SIMPLE
	classHas(row,'selectable') ||

	//INHERITED
	!classHas(row,'notSelectable') && (
	   (classHas(row.parentNode.parentNode,'rowsSelectable') &&
	      !classHas(row.parentNode,'rowsNotSelectable')) ||
	   classHas(row.parentNode,'rowsSelectable')
	)

     );
}

superTable.Sort = function(index) {
	this.index = index;
	this.asc = true;

	return this;
}

superTable.Sort.prototype.reverse = function() {
	this.asc = !this.asc;

	if (this.list.length == 1) {
		//THIS IS FASTER THAN DOING THE ENTIRE SORT AGAIN
		var sortTbody = superTable.sortTbody(this.list.table);
		var bodyRows = allRows(sortTbody);
		bodyRows.reverse();
		superTable.reinsertRows(bodyRows);
	} else {
		this.list.sync();
	}

	this.updateCss();
}

superTable.Sort.prototype.updateCss = function() {
	//A SORT IS ACTIVE IFF IT IS PRESENT IN THE TABLE'S sorts ARRAY
	var active = this.list.table.sorts.has(this);
	var newClass = active ? myClasses.sort[this.asc] : myClasses.unsorted;
	var otherClasses = values(myClasses.sort)
	   .concat([myClasses.unsorted])
	   .except(newClass);

	var classFixer = function(elem) {
		classAdd(elem, newClass);
		classRemove.apply(this, [elem].concat(otherClasses));
	}

	//CELLS FIRST
	var sortTbody = superTable.sortTbody(this.list.table);
	var tbodyRows = rowsOk ? sortTbody.rows : allRows(sortTbody);
	for (var r=0; r < tbodyRows.length; r++) {
		var curRow = tbodyRows[r];
		var rowCells = window.cellsOk ? curRow.cells : allCells(curRow);
		var cellToFix = rowCells[this.index];

		classFixer(cellToFix);
	}

	//NOW HEADERS
	var indexCache = superTable.sortIndexCache(this.list.table);
	for (var h=0; h < indexCache[this.index].length; h++) {
		var hdr = indexCache[this.index][h];
		classFixer(hdr);
	}

	if (superTable.isStriped(sortTbody)) {
		superTable.stripeTbody(sortTbody);
	}
}

superTable.reinsertRows = function(theRows) {
	var sortTbody = theRows[0].parentNode;

	//SUPPOSEDLY FASTER IN SOME BROWSERS.
	superTable.removeRows(theRows);

	for (var r=theRows.length-1; r >= 0; r--) {
		sortTbody.insertBefore(theRows[r],sortTbody.firstChild);
	}
}
superTable.removeRows = function(theRows) {
	var sortTbody = theRows[0].parentNode;
	for (var r=theRows.length-1; r >= 0; r--) {
		sortTbody.removeChild(theRows[r]);
	}
}

//SINCE IE CAN'T PROPERLY HAVE SUBCLASSES INHERIT FROM Array...
superTable.newSortList = function(tableObj) {
	var sortList = [];
	sortList.table = tableObj;
	sortList.originalRows = tableObj.sorts
	   ? tableObj.sorts.originalRows
	   : allRows(superTable.sortTbody(tableObj));
	var _push = sortList.push;
	sortList.push = function(newSort) {
		newSort.order = this.length;
		newSort.list = this;
		return _push.call(this,newSort);
	}

	for (var a=1; a < arguments.length; a++) {
		sortList.push(arguments[a]);
	}

	sortList.sync = function() {
		var sorterFuncs = this.table.sorterFuncs;
		var rowSortFuncs = this.process( function(s) {
			var transformer =
			   sorterFuncs[s.index] || function(v) { return v; };

			var rowLevelTransformer = function(row) {
				return transformer(XBinnerText(
					(window.cellsOk ? row.cells : allCells(row))[s.index]
				));
			};
			rowLevelTransformer.reverse = !s.asc;

			return rowLevelTransformer;
		});

		var sortTbody = superTable.sortTbody(this.table);
		var theRows = rowsOk ? makeArray(sortTbody.rows) : allRows(sortTbody);

		theRows.sortBy.apply(theRows,rowSortFuncs);

		superTable.reinsertRows(theRows);
	}

	return sortList;
}


superTable.addTableSort = function(tableObj,newSortIndex) {
	var newSort = new this.Sort(newSortIndex);
	var multiSortable = classHas(tableObj,'multiSortable');
	var sortTbody = this.sortTbody(tableObj);

	var formerSort = tableObj.sorts && tableObj.sorts[0];

	if (multiSortable && tableObj.sorts) {
		tableObj.sorts.push(newSort);
	} else {
		tableObj.sorts = this.newSortList(tableObj,newSort);
	}

	tableObj.sorts.sync();

	newSort.updateCss();

	if (formerSort) formerSort.updateCss();
}

superTable.rowsSelectable = function(elem) {
	return classHas(elem,'rowsSelectable')
	   || classHas(elem.parentNode,'rowsSelectable');
}
superTable.rowNotSelectable = function(row) {
	return classHas(row,'notSelectable');
}
//ROW SELECT (TOGGLE) FUNCTION FOR A SINGLE ROW
superTable.rowSelect = function(theRow,force) {
	var makeSelected = force || (isUndefined(force) && !theRow.selected);

	if (makeSelected) {
		classAdd(theRow,myClasses.selected);
		if (superTable.onrowselect) {
			superTable.onrowselect(theRow);
		}
	} else {
		classRemove(theRow,myClasses.selected);
		if (superTable.onrowunselect) {
			superTable.onrowunselect(theRow);
		}
	}
	theRow.selected = makeSelected;
}
superTable.selectAll = function(obj, force) {
	var selectYN = (!isUndefined(force) ? force : true);
	var rows = obj.getElementsByTagName('tr');
	for (var r=0; r < rows.length; r++) {
		var curRow = rows[r];
		if (this.isSelectable(curRow)) this.rowSelect(curRow,selectYN);
	}
}
//THIS IS A WRAPPER TO selectAll TO PREVENT CODE DUPLICATION
superTable.unselectAll = function(obj) {
	this.selectAll(obj,false);
}

superTable.nextRow = function(row,previous) {
	var prevNext = previous ? 'previousSibling' : 'nextSibling';
	//EITHER THE NEXT/PREVIOUS SIBLING OR THE PARENT'S NEXT/PREVIOUS SIBLING'S
	//FIRST/LAST CHILD
	var theSibling = row[prevNext]
	   || (row.parentNode[prevNext]
	      && row.parentNode[prevNext][previous ? 'lastChild' : 'firstChild']);

	//(A CHILD OF A TBODY HAS TO BE A ROW IF IT'S OF nodeType 1)
	var rowSibling = theSibling && ((theSibling.nodeType == 1)
	   ? theSibling
	   : superTable.nextRow(theSibling,previous)
	);

	return rowSibling;
}
superTable.previousRow = function(row) {
	return superTable.nextRow(row, true);
}

//TAKEN FROM http://www.quirksmode.org/js/findpos.html
superTable.findPosY = function(obj) {
	var curtop = 0;
	if (obj.offsetParent) {
		while (obj.offsetParent) {
			curtop += obj.offsetTop
			obj = obj.offsetParent;
		}
	//MIGHT AS WELL SUPPORT NETSCAPE 4, THOUGH AGAINST MY BETTER JUDGEMENT
	} else if (obj.y) {
		curtop += obj.y;
	}

	return curtop;
}

//THIS MECHANISM USES offsetTop IN BROWSERS THAT SUPPORT IT (VIRTUALLY ALL),
//THOUGH IT CAN USE PURE W3C DOM IF NEEDS BE (WHICH IS SLOWER)
superTable.DragRows = function(newRow) {
	this.places = [];

	//I HAD THESE DIRECTLY SET AS PROPERTIES, WITH THIS CLASS INHERITING FROM
	//Array, BUT IE MAKES push/pop IGNORE length, WHICH IT MAKES READ-ONLY.
	//GOTTA LOVE THAT!!
	this.rows = [];

	var usePureDom = isUndefined(newRow.offsetTop);

	if (usePureDom) {
		var theTable = bubbleUpFind(newRow,'table');
		var tableRows = rowsOk ? theTable.rows : allRows(theTable);
	}

	this.findRowPlace = usePureDom
	   ? function(row) {
		//THIS COULD DO makeArray().find, BUT THAT'D BE SLOWER
		var r=0;
		while ((r < tableRows.length) && (row !== tableRows[r])) {
			r++;
		}

		return (r != tableRows.length) && r;
	   }
	   : superTable.findPosY
	;

	this.rows.push(newRow);
	this.places.push(this.findRowPlace(newRow));

	return this;
}

//THIS DOESN'T ADD THE places ENTRY BECAUSE IT SAVES WORK TO DO THAT OUTSIDE,
//BECAUSE update NEEDS THAT INFORMATION
superTable.DragRows.prototype.push = function(newRow) {
	if (!this.rows.has(newRow)) {
		classAdd(newRow, 'dragOver');
		this.rows.push(newRow);
	}

	return this.rows.length;
}
superTable.DragRows.prototype.pop = function() {
	var retVal;
	if (this.rows.length > 1) {
		retVal = this.rows.pop();
		classRemove(retVal,'dragOver');
		this.places.pop();
	}

	return retVal;
}

superTable.DragRows.prototype.update = function(newRow) {
	var newRowPlace = this.findRowPlace(newRow);

	//DELETE ROWS THAT AREN'T BETWEEN THE mousedown ROW AND newRow
	while (!parseInt(this.places.last()).between(this.places[0],newRowPlace)) {
		this.pop();
	}

	//IF WE ALREADY CONTAIN THE "NEW" ROW, THEN WE'RE DONE
	if (!this.rows.has(newRow)) {
		//NOW ADD ROWS TO THE ARRAY UNTIL WE MEET THE mouseoverED ONE
		var goBackwards = (parseInt(newRowPlace) < parseInt(this.places[0]));
		var stepper = superTable[goBackwards ? 'previousRow' : 'nextRow'];

		var curRow = stepper(this.rows.last());
		while (curRow && (curRow !== newRow)) {
			var curRowPlace = this.findRowPlace(curRow);
			this.places.push(curRowPlace);
			this.rows.push(curRow);
			curRow = stepper(curRow);
		}

		//FINALLY, ADD THE NEW ROW
		this.places.push(newRowPlace);
		this.rows.push(newRow);
	}
}

superTable.dragMouseoverHandler = function(e) {
	var dragRow = bubbleUpFind(e.target,'tr');
	if (dragRow) {
		superTable.dragRows.update(dragRow);
	}
};

superTable.listeners = {};
superTable.listeners.mousedown = function(e) {
	var theRow = bubbleUpFind(e.target,'tr');
	if (theRow && !e.target.tagName.match(/^(a|input|button)$/i)) {
		var theTable = bubbleUpFind(theRow,'table');
//		if (theTable && superTable.isSortable(theTable)) {
		if (theTable) {
			var tableSectionName = theRow.parentNode.tagName.toLowerCase();
			if (tableSectionName == 'tbody') {
				superTable.dragTable = theTable;
				XBaddEventListener(superTable.dragTable,'mouseover',
				   superTable.dragMouseoverHandler);
				superTable.dragRows = new superTable.DragRows(theRow);
			} else if (tableSectionName == 'thead') {
			}
		}
	}
}
superTable.listeners.mouseup = function(e) {
	if (superTable.dragTable) {
	//ROW SELECTING
		XBremoveEventListener(
		   superTable.dragTable,'mouseover',superTable.dragMouseoverHandler
		);

		var theRow = bubbleUpFind(e.target,'tr');
		if (theRow
		   && (bubbleUpFind(theRow,'table') === superTable.dragTable)) {
			superTable.dragRows.update(theRow);
		}

		superTable.dragRows.rows.process(function(row) {
			if (superTable.isSelectable(row)) {
				superTable.rowSelect(row);
			}
			classRemove(row,'dragOver');
		});

		delete superTable.dragTable;
		delete superTable.dragRows;
	} else if (bubbleUpFind(e.target,'thead')) {
		//SORTING
		var theTable = bubbleUpFind(e.target,'table');
		var clickCell = bubbleUpFind(e.target,'th','td');
		var sortIndex = superTable.findSortIndex(clickCell);

		//THERE CAN ONLY BE ONE OF THESE
		var existingMatchingSorts = theTable.sorts && theTable.sorts.filter(
			function(s) { return (s.index == sortIndex) }
		);

		if (existingMatchingSorts && existingMatchingSorts[0]) {
			existingMatchingSorts[0].reverse();
		} else {
			superTable.addTableSort(theTable,sortIndex);
		}
	}
}
for (var htype in superTable.listeners) {
	XBaddEventListener(document, htype, superTable.listeners[htype]);
}
domCheck();

//APPLY PROPERTIES THAT CAN'T BE DEALT WITH DYNAMICALLY, LIKE STRIPING
//AND ADDING CSS PROPERTIES
superTable.fixTables = function() {
	//DETECT IF WE GOT CALLED AS AN EVENT LISTENER
	var listener = (arguments.length == 1) && !arguments.nodeType;

	var tables = ((arguments.length > 0) && !listener)
	   ? arguments
	   : document.getElementsByTagName('table');

	for (var t=0; t < tables.length; t++) {
		var curTable = tables[t];
		var buttons = curTable.buttons || [];

		var _push = buttons.push;
		var _pop = buttons.pop;
		var _shift = buttons.shift;
		var _unshift = buttons.unshift;

		buttons.push = function() {
			for (var b=0; b < arguments.length; b++) {
				var button = arguments[b];
				curTable.parentNode.insertBefore(button,curTable);
			}

			return _push.apply(this,makeArray(arguments));
		}
		buttons.unshift = function() {
			for (var b=arguments.length-1; b >= 0; b--) {
				var button = arguments[b];
				curTable.parentNode.insertBefore(
				   button, buttons[0] || curTable
				);
			}

			return _unshift.apply(this,makeArray(arguments));
		}
		buttons.pop = function() {
			curTable.parentNode.removeChild(this.last());
			return _pop.apply(this);
		}
		buttons.shift = function() {
			curTable.parentNode.removeChild(this[0]);
			return _shift.apply(this);
		}

		var newButton = function(buttonText, func, htmlClass) {
			//THIS WILL BE OF TYPE 'submit'. IT HAS TO BE A <button>
			//BECAUSE OTHERWISE SAFARI REQUIRES THE BUTTON TO BE IN A FORM.
			//UNFORTUNATELY, W3C DOM DOESN'T LET US CHANGE THE BUTTON'S type
			//(AND NEITHER DOES OPERA).
			var theButton = document.createElement('button');

			buttonText = buttonText || '';

			if (htmlClass) {
				classAdd(theButton, htmlClass);
			}
			theButton.appendChild(document.createTextNode(buttonText));
			theButton.table = curTable;
			if (func) XBaddEventListener(theButton,'click',function(e) {
				func(e);
				e.preventDefault();
			});
			buttons.push(theButton);

			return theButton;
		}

		//ADD 'sortable' CSS CLASS TO APPLICABLE thead ELEMENTS
		if (superTable.isSortable(curTable)) {
			var indexCache = superTable.sortIndexCache(curTable);
			curTable.sorterFuncs = curTable.sorterFuncs || [];
			for (var i=0; i < indexCache.length; i++) {
				var columnSortable = isUndefined(curTable.sorterFuncs[i])
				   || (curTable.sorterFuncs[i] !== false);
				if (indexCache[i] && columnSortable) {
					for (var h=0; h < indexCache[i].length; h++) {
						var curHeader = indexCache[i][h];
						classAdd(curHeader,'sortable');
					}
				}
			}

			if (classHas(curTable,'showButtons')) {
				newButton('Default Sort', function(e) {
					superTable.sortDefault(e.currentTarget.table);
				});
			}
		}

		if (classHas(curTable,'rowsSelectable')) {
			newButton('Select All', function(e) {
				superTable.selectAll(e.currentTarget.table);
			});
			newButton('Unselect All', function(e) {
				superTable.unselectAll(e.currentTarget.table);
			});
		}

		//DO STRIPING AND CSS COMPENSATION FOR IE
		for (var b=0; b < curTable.tBodies.length; b++) {
			var curTbody = curTable.tBodies[b];
			if (superTable.isStriped(curTbody)) {
				superTable.stripeTbody(curTbody);
			}
		}
	}
}

//UNFORTUNATELY, THIS INVOLVES SOME BROWSER DETECTION,
//BUT ALL WE CARE ABOUT IS IDENTIFYING IE, REALLY, SO IT'S NOT *THAT* BAD.
//SOME CODE TAKEN FROM CHRIS NOTT'S "Browser Detect" v2.1.6
var ua = navigator.userAgent.toLowerCase();
superTable.usingIE = (ua.indexOf('msie') != -1)
   && (ua.indexOf('opera') == -1)
   && (ua.indexOf('webtv') == -1)
;
delete ua;

superTable.cssAddIEHover = function(e, removeYN) {
	var addRemove = removeYN ? classRemove : classAdd;
	addRemove(e.currentTarget,'IEhover');

	//SIMULATE EVENT BUBBLING FOR ADDING 'hover' CLASS
	if (e.target) {
		var curElem = e.target;
		while (curElem !== e.currentTarget) {
			addRemove(curElem,'IEhover');
		}
	}
};
superTable.cssRemoveIEHover = function(e) {
	return this.cssAddHover(e, true);
};
superTable.cssHover = function(elem) {
	XBaddEventListener(elem,'mouseover',this.cssAddHover);
	XBaddEventListener(elem,'mouseout',this.cssRemoveHover);
};

//ACCOMODATES FOR IE'S LACK OF :hover
//AS WELL AS LEGACY EVENT HANDLING (WHERE NEEDED)
superTable.compensate = function() {
	var cssListeners = {
		mouseover: function(e, removeYN) {
			var addRemove = removeYN ? classRemove : classAdd;
			var finished = false;

			//LEGACY DOM CAN'T HAVE e.target; IN THIS CASE, WE JUST GO
			//THROUGH THE ELEMENT AT HAND
			var curElem = e.target || e.currentTarget;

			while (!finished) {
				switch (curElem.tagName.toLowerCase()) {
				//HOVER OVER SORT-ENABLED TABLE HEADERS
				case 'th':
					var tableSection = curElem.parentNode.parentNode;
				   	if (tableSection.tagName.toLowerCase() == 'thead') {
						var theTable = tableSection.parentNode;
						var colIndex = superTable.findSortIndex(curElem);
						if ((colIndex !== false)
						   && ((theTable.sorterFuncs[colIndex] !== false)
						      || isUndefined(
						         theTable.sorterFuncs[colIndex]
						      )
						   )
						) {
							addRemove(curElem,'IEhover');
						}
					}
					break;
				//HOVER OVER SELECTABLE ROWS
				case 'tr':
					if (superTable.isSelectable(curElem)) {
						addRemove(curElem,'IEhover');
					}
					break;
				}

				finished = (curElem === e.currentTarget)
				   || !curElem.parentNode.tagName;
				if (!finished) {
					curElem = curElem.parentNode;
				}
			}
		},
		mouseout: function(e) {
			cssListeners.mouseover(e, true);
		}
	};

	var doCompensation = function (elem) {
		for (var evType in cssListeners) {
			XBaddEventListener(elem, evType, cssListeners[evType]);
		}
	};

	//IF WE CAN APPLY THIS TO THE WHOLE DOCUMENT, THAT'S BETTER BECAUSE IT
	//SAVES TIME. OTHERWISE (IE5/MAC), WE CHUG THROUGH EACH ELEMENT
	var ieDom = !!document.attachEvent;
	if (ieDom) {
		doCompensation(document);
	} else {
		//DO HEADER CELLS
		makeArray(document.getElementsByTagName('thead'))
		.process( function(th) {
			allCells(th).process(doCompensation);
		});

		makeArray(document.getElementsByTagName('tr'))
		.process(doCompensation);
	}
}

XBaddEventListener(window,'load', superTable.fixTables);
if (superTable.usingIE) {
	XBaddEventListener(window,'load',superTable.compensate);
}

var myClasses = {
	sortable: 'sortable',
	unsorted: 'unsorted',
	selected: 'selected',
	selectable: 'selectable',
	notSelectable: 'notSelectable',
	rowsSelectable: 'rowsSelectable',
	rowsNotSelectable: 'rowsNotSelectable',
	striped: 'striped',
	notStriped: 'notStriped',
	even: 'even',
	odd: 'odd',
	showButtons: 'showButtons',
	sorting: 'sorting'
};
myClasses.sort = new Object;
myClasses.sort[true] = 'sortedAsc';
myClasses.sort[false] = 'sortedDesc';

var myTitles = {
	unsorted: "Click to sort by this column.",
	sorted: "Click to reverse the sort on this column."
};

//superTableOnload();
