//GLOBAL FUNCTIONS
//
//DEFINE WITH ASSIGNMENT SO THAT IF THIS GETS CALLED IN AN ASP/JAVASCRIPT
//FUNCTION, IT'LL ALL WORK.

var clone = function(what) {
	var newThing;

	if (what.constructor == Array) {
		newThing = what.map(clone);
	} else if (what.constructor == Date) {
		newThing = new Date(what.getTime());
	} else if (what.constructor == Object) {
		newThing = {};
		for (var i in what) {
			newThing[i] = what[i];
		}
	} else {
		newThing = what;
	}

	return newThing;
}

var cmp = function(first, second) {
	return ((first > second) ? 1 : ((first < second) ? -1 : 0));
}

var keys = function(assocArray) {
	var keys = [];
	for (var i in assocArray) keys.push(i);

	return keys;
}
var values = function(assocArray) {
	var values = [];
	for (var i in assocArray) values.push(assocArray[i]);

	return values;
}

//"MAPS" A FUNCTION ONTO AN ARRAY (JUST A WRAPPER TO Array.prototype.map)
Function.prototype.map = function(theArray) {
	return Array.prototype.map.call(this, theArray);
}

//COMPARABLE TO SQL SORTS: EACH ARGUMENT SPECIFIES A FUNCTION THAT GENERATES
//A COMPARISON VALUE FOR EACH MEMBER OF THE ARRAY.
//
//FOR EXAMPLE, SORT DATES BY MONTH, BACKWARDS BY YEAR, AND SOMETHING ELSE
// BY DOING THIS:
//datesArray.sortBy('getMonth','!getYear',function (d) { ... })
//
//AS AN ADDED BONUS, YOU CAN WRITE A PROPERTY OR METHOD NAME AS A STRING.
//INDICATE NEGATION WITH A '!' AS THE FIRST CHARACTER.
//
//FOR GENERIC REVERSAL OF A SORT FUNCTION, DEFINE A reverse PROPERTY ON IT AS
//true.
Array.prototype.sortBy = function() {
	var copy = Array.prototype.makeEmpty.call(this);
	var sorters = Array.prototype.map.call(arguments, function(s) {
		var retVal;
		var reverse = false;

		if (s.constructor == Function) {
			retVal = s;
			reverse = s.reverse || false;
		} else {
			reverse = (s.charAt(0) == '!');
			if (reverse) s = s.substring(1);

			var referenced = copy[0][s];
			var isFunc = (referenced.constructor == Function);
			retVal = function(i) {
				return isFunc ? i[s]() : i[s];
			};
		}

		retVal.reverseVal = reverse ? -1 : 1;

		return retVal;
	});

	//FIGURE OUT VALUES TO BE COMPARED
	var compValues = copy.map(function(i) {
		var curValues = sorters.map(function(s) { return s(i); } );
		curValues.item = i;
		return curValues;
	});

	var jsSorter = function(first, second) {
		var retVal = 0;
		var index = 0;
		while (!retVal && (index < first.length)) {
			retVal = ((first[index] > second[index])
			   ? 1
			   : ((first[index] < second[index])
			      ? -1
			      : 0)
			) * sorters[index].reverseVal;
			index++;
		}

		return retVal;
	};

	compValues.sort(jsSorter);

	for (var cv=0; cv < compValues.length; cv++)
		this[cv] = compValues[cv].item;

	return this;
}

//RETURNS AN ARRAY OF INDICES OF OCCURRENCES OF val IN THE ARRAY.
Array.prototype.indicesOf = function(val, func) {
	var indices = [];
	var foundAt = false;
	do {
		foundAt = this.indexOf(val, foundAt || 0, func);
		if (foundAt !== false) {
			indices.push(foundAt);
			foundAt++;
		}
	} while (foundAt !== false);

	return indices;
}

if (!Array.prototype.indexOf) {
	Array.prototype.indexOf = function(val, startFrom) {
		//ITERATE THROUGH AND FIND WHEN val OCCURS
		var i = startFrom || 0;
		var foundAt = -1;
		while ((foundAt == -1) && (i < this.length)) {
			if (this[i] === val) {
				foundAt = i;
			}
			i++;
		}

		return foundAt;
	}
}

if (!Array.prototype.lastIndexOf) {
	Array.prototype.lastIndexOf = function(val, startFrom) {
		var reverseStartFrom = startFrom && ((this.length - 1) - startFrom);
		var reversed = this;
		reversed.reverse();
		var reverseIndexOf = reversed.indexOf(val,reverseStartFrom);

		return (reverseIndexOf > -1)
		   ? ((this.length - 1) - reverseIndexOf)
		   : -1
		;
	}
}

//EMPTIES AN ARRAY, RETURNING ALL THE OLD VALUES (BASICALLY SWAPPING OUT
//"THIS" ARRAY FOR AN EMPTY ONE)
Array.prototype.makeEmpty = function() {
	var values = [];
	while (this.length > 0) values.push(Array.prototype.shift.apply(this));

	return values;
}

//REMOVES ONE OR MORE VALUES, GIVEN BY INDEX, FROM AN ARRAY AND
//RETURNS THE SHORTENED ARRAY.
//TO COMBINE WITH ANOTHER FUNCTION, LOOK AT THE apply METHOD OF Function;
//FOR EXAMPLE, someArray.remove.apply(someArray,someArray.indicesOf('foo'))
Array.prototype.removeByIndex = function() {
	var copyOfThis = this.makeEmpty();

	for (var i=0; i < copyOfThis.length; i++)
		if (Array.prototype.indexOf.call(arguments,i) == -1)
			this.push(copyOfThis[i]);

	return this;
}
Array.prototype.remove = Array.prototype.removeByIndex;

Array.prototype.removeByValue = function(val, all) {
	if (all) {
		this.removeByIndex.apply(this, this.indicesOf(val));
	} else {
		this.removeByIndex(this.indexOf(val));
	}

	return this;
}

Array.prototype.last = function() {
	return this[this.length - 1];
}

//CURRENT ARRAY BECOMES KEYS AND otherArray BECOMES VALUES OF A NEW HASH
Array.prototype.combine = function(otherArray) {
	var newObject = false;

	if (this.length == otherArray.length) {
		newObject = {};
		for (var i=0; i < this.length; i++)
			newObject[this[i]] = otherArray[i];
	}

	return newObject;
}

//SET THEORY STUFF
Array.prototype.union = function(otherArray) {
	var thisDiffOther = this.map(function(i) {
		return (otherArray.indexOf(i) != -1);
	});

	return this.concat(thisDiffOther);
}
Array.prototype.intersection = function(otherArray) {
	var newArray = [];

	this.map(function(i) {
		if (otherArray.indexOf(i) != -1) newArray.push(i);
	});

	return newArray;
}

//SHOULD TAKE IN AN ARRAY ELEMENT AND RETURN A BOOLEAN.
//THIS WORKS FOR OR BECAUSE (a || b) <=> !(!a && !b).
Array.__utils_logical = function(theArray, toAnd, func, thisObj) {
	var i=0;
	var retVal = true;
	while (retVal && (i < theArray.length)) {
		var newAnd = func
		   ? (thisObj
		      ? func.apply(thisObj, theArray[i], i, theArray)
		      : func(theArray[i], i, theArray)
		   )
		   : theArray[i]
		;

		retVal = retVal && (toAnd ? newAnd : !newAnd);
		i++;
	}

	return (toAnd ? retVal : !retVal);
}

if (!Array.prototype.every) {
	Array.prototype.every = function(func, thisObj) {
		return Array.__utils_logical(this, true, func, thisObj);
	};
}
if (!Array.prototype.some) {
	Array.prototype.some = function(func, thisObj) {
		return Array.__utils_logical(this, false, func, thisObj);
	};
}

//RETURNS ALL VALUES THAT, RUN THROUGH THE OPTIONAL func,
//EVALUATE TO BOOLEAN TRUE
if (!Array.prototype.filter) {
	Array.prototype.filter = function(func, thisObj) {
		var returnArray = [];
		for (var i=0; i < this.length; i++) {
			if (func
			   ? (thisObj
			      ? func.apply(thisObj, this[i], i, this)
			      : func(this[i], i, this)
			   )
			   : this[i]
			) {
				returnArray.push(this[i]);
			}
		}

		return returnArray;
	}
}

//RETURNS AN ARRAY OF OUTPUT VALUES FROM THE SUPPLIED FUNCTION, WHICH CAN HAVE
//2 ARGUMENTS: A "CURRENT VALUE", AND A "CURRENT INDEX".
//DUPLICATES THE ARRAY IF NO FUNCTION SUPPLIED
if (!Array.prototype.map) {
	Array.prototype.map = function(func, thisObj) {
		var returnedArray = [];
		for (var i=0; i < this.length; i++) {
			returnedArray.push(thisObj
			   ? func.apply(thisObj,this[i],i,this)
			   : func(this[i],i,this)
			);
		}

		return returnedArray;
	}
}
Array.prototype.process = Array.prototype.map;

//MAY NOT WORK PERFECTLY.....HOPE FOR THE BEST
if (!Array.prototype.forEach) {
	Array.prototype.forEach = function(func, thisObj) {
		for (var i=0; i < this.length; i++) {
			if (thisObj) {
				func.apply(thisObj, this[i], i, this);
			} else {
				func(this[i],i,this);
			}
		}
	}
}

//FOR COMPATIBILITY WITH IE5/MAC, AMONG OTHERS
if (!Array.prototype.push)
	Array.prototype.push = function() {
		for (var a=0; a < arguments.length; a++)
			this[this.length] = arguments[a];
		return this.length;
	}
if (!Array.prototype.pop)
	Array.prototype.pop = function() {
		delete this[--this.length];
	}

String.prototype.toArray = function() {
	return this.match(/./g);
}
String.prototype.lpad = function(totalWidth) {
	var padding = ((arguments.length > 1) ? arguments[1].toString() : ' ');
	var thisCopy = this;

	while (thisCopy.length < totalWidth) thisCopy = padding + thisCopy;

	return thisCopy;
};
String.prototype.reverse = function() {
	return this.toArray().reverse().join('');
};

String.prototype.sprintf = function() {
	var theCopy = this;
	var valid = true;

	var argCounter = 0;

	while (valid) {
		var c = theCopy.slice(c || 0).indexOf('%');
		var pattern = theCopy.slice(c).match(
		   /%([+0 #-]*)((?:\d+|[*])?)(?:(?:\.(-?\d*|[*])?)?)([hlL]?)([%cCsSdiouxXfeEgGnp])/
		);

		//0: FLAGS
		//1: MINIMUM WIDTH
		//2: PRECISION
		//3: SIZE - IGNORED, SINCE JAVASCRIPT DOESN'T DISTINGUISH
		//4: TYPE (NO SUPPORT FOR g,G,n,p)

		if (pattern) {
			var replaceValue;
			if (pattern[5] == '%') {
				replaceValue = '%';
			} else {
				var flags = [];
				for (var f=0; f < pattern[0].length; f++) {
					if (pattern[1].charAt(f) == '#') {
						f++;
						flags.push('#'+pattern[1].charAt(f));
					} else {
						flags.push(pattern[1].charAt(f));
					}
				}

				var minWidth = (pattern[2] == '*')
				   ? arguments[argCounter++]
				   : pattern[2]
				;
				var precision = (pattern[3] == '*')
				   ? arguments[argCounter++]
				   : pattern[3]
				;

				var type = pattern[5];

				//FLAG HANDLING IS SET ASIDE BECAUSE BOTH FLOAT AND INTEGER
				//ROUTINES NEED ACCESS TO IT. PASS IT A STRING VALUE.
				var handleFlags = function(myValue) {
					if ((type == 'x') || (type == 'X')) {
						if (flags.indexOf('#') != -1) {
							myValue =
							   ((type == 'x') ? '0x' : '0X') + myValue;
						}
					} else if (myValue.charAt(0) != '-') {
						if (flags.indexOf('+') != -1) {
							myValue = '+'+myValue;
						} else if (flags.indexOf(' ') != -1) {
							myValue = ' '+myValue;
						}
					}

					return myValue;
				};

				var rawValue = arguments[argCounter++];

				switch(type) {
				case 'c':
				case 'C':
					rawValue = String.fromCharCode(rawValue);
					break;
				case 's':
				case 'S':
					rawValue = precision
					   ? rawValue.toString().slice(0,precision)
					   : rawValue.toString()
					;
					break;

				//INTEGER VALUES
				case 'd':
				case 'i':
				case 'o':
				case 'u':
				case 'x':
				case 'X':
					rawValue = Math.floor(Number(rawValue));

					//THIS MAKES HANDLING PADDING ETC. EASIER;
					//WE ADD IN THE '-' FOR TYPES d,i,o AT THE END
					var isNegative = rawValue < 0;
					rawValue = Math.abs(rawValue);

					//STRINGIFY, WHICH IN JS INCLUDES BASE CONVERSION
					if (type == 'x') {
						rawValue = rawValue.toString(16).toLowerCase();
					} else if (type == 'X') {
						rawValue = rawValue.toString(16).toUpperCase();
					} else if (type == 'o') {
						rawValue = rawValue.toString(8);
					} else {
						rawValue = rawValue.toString();
					}

					//ADD DIGITS TO MATCH THE GIVEN PRECISION
					if (precision) {
						while (rawValue.length < precision) {
							rawValue = '0'+rawValue;
						}
					}

					rawValue = handleFlags(
					   (isNegative ? '-' : '') + rawValue
					);

					break;

				//FLOATING-POINT NUMBERS
				case 'f':
				case 'e':
				case 'E':
					if (type == 'f') {
						rawValue = precision
						   ? rawValue.toFixed(precision)
						   : rawValue.toString()
						;
					} else if ((type == 'e') || (type == 'E')) {
						rawValue = precision
						   ? rawValue.toExponential(precision)
						   : rawValue.toExponential()
						;

						if (type == 'E') {
							rawValue = rawValue.toUpperCase();
						}
					}

					rawValue = handleFlags(rawValue);
					break;
				default:
					rawValue = false;
				}

				if (rawValue) {
					if (minWidth) {
						var padder = (flags.indexOf('0') != -1)
						   ? '0'
						   : ' '
						;
						var rpad = (flags.indexOf('-') != -1);

						while (rawValue.length < minWidth) {
							rawValue = rpad
							   ? rawValue+padder
							   : padder+rawValue
							;
						}
					}

					replaceValue = rawValue;
				} else {
					replaceValue = pattern;
				}
			}

			theCopy = (theCopy.slice(0,c))
			   +replaceValue
			   +(theCopy.slice(c+pattern[0].length) || '')
			;

			c += pattern[0].length;
		} else {
			valid = false;
		}
	}

	return theCopy;
}

Number.prototype.ordinalSuffix = function() {
	var suffix = false;

	if (parseInt(this) == this) {
		var thisStr = this.toString();
		var tens = thisStr.charAt(thisStr.length - 2);

		if (tens != '1') {
			var lastDigit = thisStr.charAt(thisStr.length - 1);
			if ((lastDigit >= 1) && (lastDigit <= 3))
				suffix = ['st','nd','rd'][parseInt(lastDigit)-1];
		}

		if (!suffix) suffix = 'th';
	}

	return suffix;
}
Number.prototype.toOrdinalString = function() {
	var ordinalSuffix = this.ordinalSuffix();

	return ordinalSuffix && (this.toString()+ordinalSuffix);
}
Number.prototype.between = function(first,second) {
	return (Math.min(first,second) <= this)
	   && (this <= Math.max(first,second));
}

Date.monthNames = [
   'January','February','March','April','May','June',
   'July','August','September','October','November','December'
];
Date.dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday',
   'Friday','Saturday'
];
Date.prototype.getMonthName = function() {
	return Date.monthNames[this.getMonth()];
}
Date.prototype.getDayName = function() {
	return Date.dayNames[this.getDay()];
}

Date.prototype.format = function(inString) {
	var formatted = '';
	var pad;

	for (var c=0; c < inString.length; c++) {
		var curChar = inString.charAt(c);
		if (curChar == '%') {
			var nextChar = inString.charAt(c+1);
			var ctrlChar;

			if (nextChar == '-') {
				pad = false;
				ctrlChar = inString.charAt(c+2);
				c++;
			} else if (nextChar == '_') {
				pad = ' ';
				ctrlChar = inString.charAt(c+2);
				c++;
			} else {
				pad = '0';
				ctrlChar = nextChar;
			}

			function numFormat(number,digits) {
				var negative = (number < 0);
				return pad
				   ? (negative ? '-' : '')
				      + Math.abs(number).toString().lpad(digits,pad)
				   : number;
			}

			c++;

			switch (ctrlChar) {
			case 'a':
				formatted += this.getDayName().slice(0,3);
				break;
			case 'A':
				formatted += this.getDayName();
				break;
			case 'b':
			case 'h':
				formatted += this.getMonthName().slice(0,3);
				break;
			case 'B':
				formatted += this.getMonthName();
				break;
			case 'c':
				formatted += this.toString();
				break;
			case 'C':
				formatted += numFormat(parseInt(this.getFullYear()/100),2);
				break;
			case 'd':
				formatted += numFormat(this.getDate(),2);
				break;
			case 'D':
			case 'x':
				formatted += this.format("%m/%d/%y");
				break;
			case 'e':
				formatted += this.format('%_d');
				break;
			case 'F':
				formatted += this.format('%Y-%m-%d');
				break;
//			case 'g':
//			case 'G':
			case 'H':
				formatted += numFormat(this.getHour(),2);
				break;
			case 'I':
				var simpleHour = this.getHour();
				var modHour;
				if (simpleHour == 0) {
					modHour = 12;
				} else if (simpleHour > 12) {
					modHour -= 12;
				} else {
					modHour = simpleHour;
				}

				formatted += numFormat(modHour,2);
				break;
			case 'j':
				var yearDay = this.getDate();
				var monthNum = this.getMonth();

				//ADD IN DAYS FROM MONTHS PAST
				for (var m=0; m < monthNum; m++) {
					yearDay += ([1,3,5,8,10].indexOf(monthNum) != 1)
					   ? (monthNum == 1)
					      ? ((this.getFullYear % 2) ? 28 : 29)
					      : 30
					   : 31
					;
				}

				formatted += numFormat(yearDay,3);
				break;
			case 'k':
				formatted += this.format("%_H");
				break;
			case 'l':
				formatted += this.format("%_I");
				break;
			case 'm':
				formatted += numFormat(this.getMonth() + 1,2);
				break;
			case 'M':
				formatted += numFormat(this.getMinutes(),2);
				break;
			case 'n':
				formatted += "\n";
				break;
			case 'N':
				formatted += numFormat(this.getMilliseconds(),3)
				   + '000000';
				break;
			case 'p':
				formatted += (this.getHours() > 11) ? 'PM' : 'AM';
				break;
			case 'P':
				formatted += this.format("%p").toLowerCase();
				break;
			case 'r':
				formatted += this.format("%I:%M:%S %p");
				break;
			case 'R':
				formatted += this.format("%H:%M");
				break;
			case 's':
				formatted += parseInt(this.getTime() / 1000);
				break;
			case 'S':
				formatted += numFormat(this.getSeconds(),2);
				break;
			case 't':
				formatted += "\t";
				break;
			case 'T':
			case 'X':
				formatted += this.format("%H:%M:%S");
				break;
			case 'u':
				var plainDay = this.getDay();
				formatted += (plainDay == 0) ? 7 : plainDay;
				break;
//			case 'U':
//			case 'V':
			case 'w':
				formatted += this.getDay();
				break;
//			case 'W':
			case 'y':
				formatted += this.getFullYear().toString().substr(-2);
				break;
			case 'Y':
				formatted += this.getFullYear();
				break;
			case 'z':
//				formatted += this.toString().match(/GMT(.*)\s/)[1];
				var minutesOff = this.getTimezoneOffset();
				formatted += numFormat(-1*minutesOff/60, 2)+'00';
				break;
			case 'Z':
				break;
			default:
				formatted += "%"+ctrlChar;
			}
		} else {
			formatted += curChar;
		}
	}

	return formatted;
}

Date.prototype.addInterval = function(multiplier, intervalType) {
	var copy = clone(this);

	var funcs = {
		year: 'FullYear',
		month: 'Month',
		day: 'Date',
		hour: 'Hours',
		minute: 'Minutes',
		second: 'Seconds',
		millisecond: 'Milliseconds'
	};

	var funcSuffix = funcs[intervalType];
	copy['set'+funcSuffix](copy['get'+funcSuffix]() + parseInt(multiplier));

	return copy;
}

//STUFF IN HERE IS USED FOR CLIENT-SIDE JAVASCRIPT (AS OPPOSED TO ASP ETC.)
try {
	var XBaddEventListener;
	var XBremoveEventListener;
	if (document.addEventListener) {
		XBaddEventListener = function(elem, eType, func, capture) {
			elem.addEventListener(eType, func, capture);
		}
		XBremoveEventListener = function(elem, eType, func, capture) {
			elem.removeEventListener(eType, func, capture);
		}
	} else {

		//AN ARRAY OF {elem: ..., listeners: []}
		//THIS FAILS RIGHT AWAY IF THIS ISN'T A CLIENT-SIDE SESSION
		window.XBlisteners = [];

		//THANKS TO CAIO CHASSOT FOR THIS WONDERFUL TRICK
		var fauxEvents = {
			IE: function (element) {
				this.type = window.event.type;
				this.bubbles = true;

				//AN ATTEMPT AT AMELIORATING THE AWFUL STATE OF MOUSE
				//BUTTON DETECTION - AT LEAST PARTLY.
				if ((this.type == 'mousedown') ||(this.type == 'mouseup')) {
					this.button = [null,0,1,null,2][window.event.button];
				}

				this.currentTarget = element;
				this.target = window.event.srcElement;
				this.relatedTarget = window.event.fromElement
				   || window.event.toElement;
				this.preventDefault =
				   function() {window.event.returnValue = false}
				this.stopPropagation =
				   function() {window.event.cancelBubble = true}
				this.altKey = event.altKey;
				this.ctrlKey = event.ctrlKey;
				this.shiftKey = event.shiftKey;
				this.clientX = event.clientX;
				this.clientY = event.clientY;

				return this;
			},
			legacy: function (element) {
				this.currentTarget = element;
			}
		};

		var XBdomType = document.attachEvent ? 'IE' : 'legacy';

		XBaddEventListener = function(elem, eType, func, capture, force) {
			//W3C DOM SAYS NO DUPLICATE EVENTS. (IF YOU REALLY MEAN IT, WRAP
			//DUPLICATES LIKE SO: function(e) { ... })
			var alreadyDone = elem.XBlisteners && elem.XBlisteners[eType]
			   && (elem.XBlisteners[eType].indexOf(func) != -1);

			if (!alreadyDone) {
				var Event = fauxEvents[XBdomType];

				if (!elem.XBlisteners) {
					elem.XBlisteners = {};
				}

				if (!elem.XBlisteners[eType]) {
					elem.XBlisteners[eType] = [];

					var wrapperFunc = function() {
						elem.XBlisteners[eType].map(
							function(lstr) { lstr( new Event(elem) ); }
						);
					};

					var legacyEventType = 'on'+eType;
					if (elem.attachEvent) {
						elem.attachEvent(legacyEventType, wrapperFunc);
					} else {
						elem[legacyEventType] = wrapperFunc;
					}
				}

				elem.XBlisteners[eType].push(func);
			}
		}

		XBremoveEventListener = function(elem, eType, func, capture) {
			elem.XBlisteners[eType].removeByValue(func);
		}
	}

	//MAKE MULTIPLE SELECTS WORK MORE INTUITIVELY, WITH CLICK AND DRAG
	function fixMultipleSelect(theSelect) {
		Array.prototype.forEach.call(theSelect.options, function(opt) {
			XBaddEventListener(opt,'click',arguments.callee.optionListener);
		});
	}
	fixMultipleSelect.optionListener = function(e) {
		if (!e.currentTarget.disabled) {
			if (e.shiftKey) {
				var indices = [
				   e.currentTarget.parentNode.lastSelectedIndex
				      || e.currentTarget.index,
				   e.currentTarget.index
				].sort();
				for (var i=indices[0]; i <= indices[1]; i++) {
					e.currentTarget.options[i].selected
					   = !newSelect.options[i].disabled;
				}
			} else {
				e.currentTarget.selected = !e.currentTarget.selected;
			}

			e.currentTarget.parentNode.lastSelectedIndex
			   = e.currentTarget.index;
		}

		e.stopPropagation();
	};

} catch(e) {}


//PLATFORM-NEUTRAL FUNCTION TO GET TEXT INSIDE AN ELEMENT
var XBinnerText = function(elem) {
	var returnVal = '';

	if (typeof elem == 'string') {
		returnVal = elem;
	} else if (typeof elem == 'undefined') {
		returnVal = elem;
	} else if (elem.innerText) {
		returnVal = elem.innerText;
	} else {
		var cs = elem.childNodes;
		for (var i=0; i < cs.length; i++)
			switch (cs[i].nodeType) {
				case 1: //ELEMENT NODE
					returnVal += XBinnerText(cs[i]);
					break;
				case 3: //TEXT NODE
					returnVal += cs[i].nodeValue;
					break;
			}
	}

	return returnVal;
}

//SINCE IE5 DOESN'T SUPPORT THE undefined KEYWORD
var isUndefined = function(v) {
	var undef;
	return (v === undef);
}

//METHODS TO ABSTRACT AWAY HTML CLASS HANDLING
//splitClasses FIRST TESTS TO SEE IF classSplit EXISTS, SO IT DOUBLES AS A
//CHECKER TO SEE IF CLASSES ARE ALREADY SPLIT. BY THIS METHOD ALL ELEMENTS
//CAN USE THESE METHODS, EVEN DYNAMICALLY GENERATED ONES.
var splitClasses = function(elem) {
	if (!elem.classSplit) {
		var hasClass = !isUndefined(elem.className);
		if (hasClass) {
			elem.classSplit = (elem.className === '')
			   ? []
			   : elem.className.split(/\s+/);
//			elem.classSplit.watch('length', function(prop, oldVal, newVal) {
//				elem.className = this.join(' ');
//				return newVal;
//			});
		}
	}

	return elem.classSplit || false;
};
var classHas = function(elem,theClass) {
	return splitClasses(elem) && (elem.classSplit.indexOf(theClass) != -1);
}
//ADDS ANY NUMBER OF CLASSES TO AN ELEMENT (GIVEN AFTER THE FIRST ARGUMENT)
var classAdd = function(elem) {
	if (splitClasses(elem)) {
		for (var a=1; a < arguments.length; a++) {
			if (!classHas(elem,arguments[a])) {
				elem.classSplit.push(arguments[a]);
			}
		}
		elem.className = elem.classSplit.join(' ');
	}

	return elem.classSplit || false;
}
//REMOVES ANY NUMBER OF CLASSES FROM AN ELEMENT, LIKE classAdd
var classRemove = function(elem) {
	if (splitClasses(elem)) {
		for (var a = 1; a < arguments.length; a++)
			elem.classSplit.removeByValue.apply(
			   elem.classSplit,[arguments[a]]
			);
		elem.className = elem.classSplit.join(' ');
	}

	return elem.classSplit || false;
}

//FIND THE FIRST PARENT OF elem tagName MATCHING ONE TAG NAME GIVEN
//(INCLUDE elem ITSELF)
//EX.: theCell = bubbleUpFind(e.target,'td','th');
var bubbleUpFind = function(elem) {
	var curElem = elem;

	//FIRST ARGUMENT ISN'T A TAG NAME, AND WE WANT CASE-INSENSITIVE MATCHING
	var tagNames = Array.prototype.slice.call(arguments,1)
	.map(function(tagName) {
		return tagName.toLowerCase();
	});

	while (curElem && (curElem.nodeType == 1)
	   && (tagNames.indexOf(curElem.tagName.toLowerCase())) == -1) {
		curElem = curElem.parentNode || undefined;
	}

	//THIS IS WEIRD BECAUSE OF IE
	return curElem && (curElem.nodeType == 1) && curElem;
}

//var clearChildren = function(elem) {
//	while (elem.childNodes.length > 0) {
//		elem.removeChild(elem.firstChild);
//	}
//}

