/*** @inca-include ../admin/assets/scripts/base/sugar.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/base/sugar.js
 * FUNCTION:		Contains various Utility functions.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-18
 ***************************************************************************************/

// Constants
var HTML_CHARS_FIND		= [String.fromCharCode(160), "¡", "¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "\"", "<", ">", "&"];
var HTML_CHARS_REPLACE	= ["&nbsp;", "&iexcl;", "&cent;", "&pound;", "&curren;", "&yen;", "&brvbar;", "&sect;", "&uml;", "&copy;", "&ordf;", "&laquo;", "&not;", "&shy;", "&reg;", "&macr;", "&deg;", "&plusmn;", "&sup2;", "&sup3;", "&acute;", "&micro;", "&para;", "&middot;", "&cedil;", "&sup1;", "&ordm;", "&raquo;", "&frac14;", "&frac12;", "&frac34;", "&iquest;", "&Agrave;", "&Aacute;", "&Acirc;", "&Atilde;", "&Auml;", "&Aring;", "&AElig;", "&Ccedil;", "&Egrave;", "&Eacute;", "&Ecirc;", "&Euml;", "&Igrave;", "&Iacute;", "&Icirc;", "&Iuml;", "&ETH;", "&Ntilde;", "&Ograve;", "&Oacute;", "&Ocirc;", "&Otilde;", "&Ouml;", "&times;", "&Oslash;", "&Ugrave;", "&Uacute;", "&Ucirc;", "&Uuml;", "&Yacute;", "&THORN;", "&szlig;", "&agrave;", "&aacute;", "&acirc;", "&atilde;", "&auml;", "&aring;", "&aelig;", "&ccedil;", "&egrave;", "&eacute;", "&ecirc;", "&euml;", "&igrave;", "&iacute;", "&icirc;", "&iuml;", "&eth;", "&ntilde;", "&ograve;", "&oacute;", "&ocirc;", "&otilde;", "&ouml;", "&divide;", "&oslash;", "&ugrave;", "&uacute;", "&ucirc;", "&uuml;", "&yacute;", "&thorn;", "&yuml;", "&quot;", "&lt;", "&gt;", "&amp;"];

var ENT_COMPAT		= 1;
var ENT_QUOTES		= 2;
var ENT_NOQUOTES	= 4;

function $E(element) {
	if (isDefined(element.id)) {
		alert("FOUND ELEMENT ID");
		var element_obj = $(element.id);
		
		if (element_obj) {
			alert("FOUND ELEMENT WITH MATCHING ID");
			return element_obj;
		}
	}
	
	return $(element);
}

/**
 * htmlentities() is a JS implementation of the PHP function.
 * 
 * @param str	{String}	The string to encode.
 * 
 * @return the string with HTML entitites encoded.
 * @type String
 */
function htmlentities(str) {
	
	// Check str
	if (isString(str) || isNumeric(str)) {
		
		// Convert to string, if not already
		str = String(str);
		
		// Replace chars
		for (var i = 0; i < HTML_CHARS_FIND.length; i++) {
			str = str.split(HTML_CHARS_FIND[i]).join(HTML_CHARS_REPLACE[i]);
		}
	}
	
	return str;
}

var Objects = {
	
	print:								function(obj, recursive, padding) {
		
		padding		= isIntegeric(padding) ? Number(padding) : 0;
		recursive	= isBoolean(recursive) ? recursive : true;
		
		// Create holder
		var str = "";
		
		// Line padding
		var lp = "";
		for (var i = 0; i < padding; i++) {
			lp += " ";
		}
		
		// Populate
		if (isObject(obj)) {
			for (var i in obj) {
				if (isObject(obj[i]) && !isArray(obj[i])) {
					if (recursive) {
						str += lp + i + ": {\n" + this.print(obj[i], recursive, padding + 4) + lp + "}\n";
					} else {
						str += lp + i + ": Object\n";
					}
					//str += lp + i + ": {Object or array}\n";
				} else {
					str += lp + i + ": " + obj[i] + "\n";
				}
			}
		}
		
		// Return
		return str;
	},
	
	getProperties:						function(obj) {
		
		// Get array to contain properties
		var properties = [];
		
		// Populate
		if (isObject(obj)) {
			for (var i in obj) {
				properties.push(i);
			}
		}
		
		// Return
		return properties;
	},
	
	getPropertyCount:					function(obj) {
		return this.getProperties(obj).length;
	},
	
	hasProperties:						function(obj) {
		return this.getPropertyCount(obj) > 0;
	},
	
	merge:								function() {
		
		// The resulting merged object
		var merged = {};
		
		// For each object passed
		for (var i = 0; i < arguments.length; i++) {
			if (isObject(arguments[i])) {
				
				// For each property
				for (var p in arguments[i]) {
					
					// If property is already set, and is an object in both cases, merge the two objects, otherwise overwrite
					if (isObject(arguments[i][p]) && isObject(merged[p])) {
						merged[p] = this.merge(merged[p], arguments[i][p]);
					} else {
						merged[p] = arguments[i][p];
					}
				}
			}
		}
		
		// Done
		return merged;
	},
	
	getFirstKey:						function(obj) {
		for (var i in obj) {
			return i;
		}
	},
	
	getLastKey:							function(obj) {
		var key;
		for (var i in obj) {
			key = i;
		}
		
		return key;
	},
	
	end: true
}

function printObject(obj) {
	var str = "";
	if (isObject(obj)) {
		for (var i in obj) {
			str += i + ": " + obj[i] + "\n";
		}
	} else {
		str = "Argument is not an object.";
	}
	
	return str;
}

/**
 * getPropertiesOfObject() gets an array of properties in an object.
 * 
 * @param obj	{Object}	An object to get the properties of.
 * 
 * @return an array of property IDs.
 * @type Array
 */
function getPropertiesOfObject(obj) {
	var properties = [];
	if (isObject(obj)) {
		for (var i in obj) {
			properties.push(i);
		}
	}
	
	// Return
	return properties;
}

function generateRandomID(prefix) {
	return String(prefix) + Math.round(Math.random() * 1000);
}

/**
 * makeArray() makes an item into an array if it is not already.
 * 
 * @param item	{Mixed}	A variable.
 * 
 * @return the variable if it is an array, or an array containing the variable otherwise.
 * @type Array
 */
function makeArray(item) {
	return isArray(item) ? item : [item];
}

function createTextNode(str) {
	var str = "";
	
	if (arguments.length >= 1) {
		for (var i = 0; i < arguments.length; i++) {
			if (isString(arguments[i])) {
				str += arguments[i];
			}
		}
	} else {
		str = "[No strings passed]";
	}
	
	return document.createTextNode(str);
}

/**
 * createElement() is a shortcut for creating elements. The first argument should be
 * a string containing the element type to create. Any subsequent arguments should be
 * one of:
 *		a) An array containing an attribute name/value pair
 *		b) Another element to append as a child
 *
 * @param 1		{String}					The element type ('div', 'p', etc).
 * @param 2 - n	{String/Array/DOMElement}	Elements to add to the created element.
 *
 * @return the created element.
 * @type DOMElement
*/
function createElement() {

	// Check args OK
	if (arguments.length >= 1) {
	
		// Create an element from the first arg
		var element = document.createElement(arguments[0]);
		
		// For each additional argument
		for (var i = 1; i < arguments.length; i++) {
		
			// If an array, assume a pair of attribute types and values
			if (isArray(arguments[i])) {
				if (arguments[i].length == 2) {
					element.setAttribute(arguments[i][0], arguments[i][1]);
				}
			
			// If object, assume a child node to append
			} else if (isObject(arguments[i])) {
				if (arguments[i].property && arguments[i].value) {
					element[arguments[i].property] = arguments[i].value;
				} else {
					try {
						element.appendChild(arguments[i]);
					} catch (error) {
						alert(error);
					}
				}
			} else if (isString(arguments[i]) || isNumeric(arguments[i])) {
				try {
					
					/**
					 * Want to use this, but it inserts all entities as literals (&amp; is inserted exactly as such, for example, so is < and >)
					 */
					//var text_node = document.createTextNode(String(arguments[i]));
					//element.appendChild(text_node);
					
					/**
					 * Instead, use this, which BREAKS ANY INSERTED OBJECT REFERENCES. For example:
					 * 
					 * var span = createElement("span");
					 * var div = createElement("div", span, "plain text");
					 * 
					 * span now NO LONGER REFERS TO THE ELEMENT IN DIV (though it still contains a Span DOM element).
					 * This occurs whenever innerHTML is used. Can be avoided by using:
					 * var div = createElement("div", span, document.createTextNode("plain text"))
					 * However this suffers the same entity problem as detailed above. :(
					 */
					element.innerHTML += String(arguments[i]);
					
				} catch (error) {
					alert(error);
				}
			}
		}
		
		// Done
		return $(element);
	}
	
	// Fail
	return false;
}


function createTextNode() {
	var elements = new Array();
	
	for (var i = 0; i < arguments.length; i++) {
		if (isString(arguments[i])) {
			elements.push(arguments[i]);
		} else if (isInteger(arguments[i]) || isFloat(arguments[i]) || isBoolean(arguments[i])) {
			elements.push(String(arguments[i]));
		}
	}
	
	elements = elements.join("");
	
	return document.createTextNode(elements);
}



Function.prototype.si = function (v) {
      try {
            return v instanceof this;
      } catch (e) {
            return false;
      }
};

Boolean.si = function (v) {
    return typeof v === 'boolean';
};

Number.si = function (v) {
    return typeof v === 'number' && isFinite(v);
};

String.si = function (v) {
    return typeof v === 'string';
};

Array.si = function (v) {
    return v && typeof v === 'object' && typeof v.length === 'number' &&
	          !(v.propertyIsEnumerable('length'));
};

function isEmpty(o) {
    var i, v;
    if (Object.si(o)) {
        for (i in o) {
            v = o[i];
            if (v !== undefined && !Function.si(v)) {
                return false;
            }
        }
    }
    return true;
}

String.prototype.entityify = function () {
	return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
};

String.prototype.quote = function () {
	var c, i, l = this.length, o = '"';
	for (i = 0; i < l; i += 1) {
		c = this.charAt(i);
		if (c >= ' ') {
			if (c == '\\' || c == '"') {
				o += '\\';
			}
			o += c;
		} else {
			switch (c) {
			case '\b':
				o += '\\b';
				break;
			case '\f':
				o += '\\f';
				break;
			case '\n':
				o += '\\n';
				break;
			case '\r':
				o += '\\r';
				break;
			case '\t':
				o += '\\t';
				break;
			default:
				c = c.charCodeAt();
				o += '\\u00' + Math.floor(c / 16).toString(16) +
					(c % 16).toString(16);
			}
		}
	}
	return o + '"';
};

String.prototype.supplant = function (o) {
	var i, j, s = this, v;
	for (;;) {
		i = s.lastIndexOf('{');
		if (i < 0) {
			break;
		}
		j = s.indexOf('}', i);
		if (i + 1 >= j) {
			break;
		}
		v = o[s.substring(i + 1, j)];
		if (!String.si(v) && !Number.si(v)) {
			break;
		}
		s = s.substring(0, i) + v + s.substring(j + 1);
	}
	return s;
};

String.prototype.trim = function () {
	return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
}

String.prototype.stripTags = function() {
	return this.replace(/\<[^\>]*\>/gi, "");
}

String.prototype.hasNoTags = function() {
	return this == this.stripTags();
}

function isAlien(a) {
   return isObject(a) && typeof a.constructor != 'function';
}

function isArray(a) {
    return isObject(a) && a.constructor == Array;
}

function isBoolean(a) {
    return typeof a == 'boolean';
}

function isEmpty(o) {
    var i, v;
    if (isObject(o)) {
        for (i in o) {
            v = o[i];
            if (isUndefined(v) && isFunction(v)) {
                return false;
            }
        }
    }
    return true;
}

function isFunction(a) {
    return typeof a == 'function';
}

function isNull(a) {
    return a === null;
}

function isNumber(a) {
    return typeof a == 'number' && isFinite(a);
}

function isObject(a) {
    return (a && typeof a == 'object') || isFunction(a);
}

function isString(a) {
    return typeof a == 'string';
}

function isUndefined(a) {
    return typeof a == 'undefined';
}

function isDefined(a) {
	return !isUndefined(a);
}

function isNumeric(x) {

	if (isString(x) || isNumber(x)) {
	
		// Create regular expression
		var reg_exp = /^[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$/;
	
		// Match
		var result = String(x).match(reg_exp);
		result = result == null ? false : result;
		
		// Done
		return result;
	}
	
	return false;
}

function isIntegeric(x) {
	
	if (isString(x) || isNumber(x)) {
	
		// Create regular expression
		var reg_exp = /^[0-9\-]+$/;
	
		// Match
		var result = String(x).match(reg_exp);
		result = result == null ? false : result;
		
		// Done
		return result;
	}
	
	return false;
}

function isInteger(x) {
	return isIntegeric(x) && typeof x == "number";
}

var isInt = isInteger;

/**
 * mergeArraysToObject() merges two Arrays into an object, with the first Array
 * containing the object's properties, and the second the values.
 * 
 * @param property_keys		{Array}	The property keys for the created object.
 * @param property_values	{Array} The property values for the created object.
 * 
 * @return an object with the properties.
 * @type Object
 */
function mergeArraysToObject(property_keys, property_values) {
	
	// Validate variables
	property_keys	= property_keys		|| [];
	property_values	= property_values	|| null;
	
	// Create the object to store the properties in
	var obj = {};
	
	// For each property key
	for (var i = 0; i < property_keys.length; i++) {
		
		// If a matching property value exists
		if (isArray(property_values)) {
			if (isDefined(property_values[i])) {
				obj[property_keys[i]] = property_values[i];
			}
			
		// Else if property values array was omitted, use the key as the value
		} else if (property_values === null) {
			obj[property_keys[i]] = property_keys[i];
		}
	}
	
	// Return our populated object
	return obj;
}

/**
 * arrayToObject() creates an object with properties from an array.
 * 
 * @param array	{Array}	The array whose values should be added to an object.
 * 
 * @return an object with properties for each index of the array.
 * @type Object
 */
function arrayToObject(array) {
	
	// Check it is an array
	if (isArray(array)) {
	
		// Create empty object
		var obj = {};
		
		// For each array element
		for (var i = 0; i < array.length; i++) {
			
			// Add a property to the object
			obj[i] = array[i];
		}
		
		// Return
		return obj;
		
	// If it's already an object, return as is
	} else if (isObject(array)) {
		return array;
		
	// Otherwise, fail
	} else {
		return {};
	}
}


/****************************************************************************************
PADDING
****************************************************************************************/

function Pad(value, character, length, direction) {
	
	// If passed an array of values, do them all
	if (isArray(value)) {
		value.each(function(item, index) {
			value[index] = Pad(item, character, length, direction);
		});
		
	} else {
	
		// Check values
		value			= String(value);
		character		= String(character);
		length			= length || 0;
	
		// Pad
		while (value.length < length) {
			if (direction == Pad.LEFT)
				value 	= character + value;
			else
				value	+= character;
		}
	}
	
	// Done
	return value;
}

Pad.LEFT = true;
Pad.RIGHT = false;

Pad.forDates = function(value) {	
	return Pad(value, "0", 2, Pad.LEFT);
}

Pad.toString = function(value) {
	return Pad(value, "", 0, Pad.LEFT);
}

/****************************************************************************************
 STRING-STRIPPING EXTENSIONS
 ***************************************************************************************/

function StringMod(str) {
	this.str = String(str);
}

StringMod.prototype.removeCharsExcept = function(chars) {
	
	// Use the removeChars method with the inversion argument
	return this.removeChars(chars, true);
}

StringMod.prototype.removeChars = function(chars, invert) {
	
	// Check whether inverting (ie removing everything EXCEPT the car list)
	invert = invert || false;
	
	// Create new string containing the valid chars
	var valid = "";
	
	// For each char in the current string
	for (var i = 0; i < this.str.length; i++) {
		
		// If valid, add to valid string
		if (invert) {
			if (chars.search(this.str[i]) !== false)
				valid += this.str[i];
		} else {
			if (chars.search(this.str[i]) === false)
				valid += this.str[i];
		}
	}
	
	// Replace current string
	this.str = valid;
	
	// Return for further manipulation
	return this;
}


function removeCharsFromStringExcept(string, valid_chars) {
	
	var valid = "";
	
	string = String(string);
	
	for (var i = 0; i < string.length; i++) {
		if (valid_chars.search(string[i]) !== false)
			valid += string[i];
	}
	
	return valid;
}


/****************************************************************************************
ARRAY EXTENSIONS
****************************************************************************************/

/**
 * getKeys() gets the keys of an array
 *
 * @return an array of keys
*/
Array.prototype.getKeys = function() {

	// Keys
	var keys = new Array();

	for (var i in this) {
		if (!isFunction(this[i])) {
			keys.push(i);
		}
	}

	return keys;
}

/**
 * getValues() gets the values of an array (minus the keys)
 *
 * @param include_functions is whether to include functions
 *
 * @return an array of values
*/
Array.prototype.getValues = function(include_functions) {

	// Check include functions
	include_functions = include_functions || false;

	// Values
	var values = new Array();

	for (var i in this) {
		if (!isFunction(this[i]) || include_functions) {
			values.push(this[i]);
		}
	}

	return values;
}

Array.prototype.removeDuplicateValues = function() {

	// Remove
	this.remove(this.getDuplicateValues());
	
	// Done
	return this;
}

Array.prototype.getDuplicateValues = function() {
	
	var indices = [];
	var checked = [];
	
	for (var i = 0; i < this.length; i++) {
		if (isDefined(this[i])) {
			if (checked.search(this[i]) === false) {
				checked.push(this[i]);
			} else {
				indices.push(i);
			}
		}
	}
	
	return indices;
}

Array.prototype.removeEmptyValues = function() {
	
	// Remove
	this.remove(this.getEmptyValues());
	
	// Done
	return this;
}

Array.prototype.getEmptyValues = function() {
	
	var indices = [];
	
	for (var i = 0; i < this.length; i++) {
		if (isDefined(this[i])) {
			if (this[i] == false || this[i] == "" || this[i].length == 0)
				indices.push(i);
		}
	}
	
	return indices;
}

Array.prototype.remove = function(index) {
	
	if (isArray(index)) {
		
		var offset = 0;

		// Remove them
		for (var i = 0; i < index.length; i++) {
			this.remove(index[i] - offset);
			offset++;
		}
		
	} else {
		this.splice(index, 1);
	}
}

Array.prototype.searchAndRemove = function(items) {
	
	items = isArray(items) ? items : [items];
	
	for (var i = 0; i < items.length; i++) {
		this.remove(this.searchAll(items[i]));
	}
	
	return this;
}

Array.prototype.search = function(value) {
	
	// Search each element
	for (var i = 0; i < this.length; i++) {
		
		// If matches, return index
		if (this[i] === value || String(this[i]) === String(value)) {
			return i;
		}
	}
	
	// Fail
	return false;
}

Array.prototype.has = function(value) {
	return this.search(value) !== false;
}

/**
 * searchAll() returns all occurances of a value in this array.
 * 
 * @param value	{Mixed}	The value to search for.
 * 
 * @return an array of indices where the value was found.
 * @type Array
 */
Array.prototype.searchAll = function(value) {
	
	// Indices
	var indices = [];
	
	// Search each element
	for (var i = 0; i < this.length; i++) {
		
		// If matches, add to indices list
		if (this[i] === value || String(this[i]) === String(value)) {
			indices.push(i);
		}
	}
	
	// Return results
	return indices;
}

Array.prototype.pushAndReturn = function(value) {
	this.push(value);
	return this;
}

/****************************************************************************************
 DATE EXTENSIONS
 ***************************************************************************************/

/**********************************************************
 STATIC METHODS
 *********************************************************/

// Constants
Date.AS_STRING			= true;
Date.PADDED				= true;
Date.SECONDS_IN_DAY		= 86400;
Date.SECONDS_IN_WEEK	= 604800;
Date.SECONDS_IN_HOUR	= 3600;
Date.SECONDS_IN_MINUTE	= 60;

/**
 * Date.getAllMonthNames() gets an array of all full month names
 *
 * @return an array of months.
 * @type Array
*/
Date.getAllMonthNames			= function() {
	return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
}

/**
 * Date.getAllShortMonthNames() gets an array of all short month names
 *
 * @return an array of months.
 * @type Array
*/
Date.getAllShortMonthNames		= function() {
	return ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
}

/**
 * Date.getAllDayNames() gets an array of full day names
 * 
 * @return an array of days.
 * @type Array
*/
Date.getAllDayNames				= function() {
	return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
}

/**
 * Date.getAllDayShortNames() gets an array of short day names
 * 
 * @return an array of days.
 * @type Array
*/
Date.getAllDayShortNames		= function() {
	return ["Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"];
}

/**
 * Date.getNumberRange() gets an array of a range of numbers. Facilitates the various
 * number-returning date methods.
 *
 * @param start is the first number in the range
 * @param end is the last number in the range
 * @param as_string is whether to return the numbers as strings
 * @param padded is whether to pad the numbers with leading zeroes if they are only
 *	one character long.
 *
 * @return an array of numbers.
 * @type Array
*/
Date.getNumberRange				= function(start, end, as_string, padded) {
	
	// Create a number range
	var range = $A($R(start, end));
	
	// Return in appropriate form
	if (as_string) {
		return padded ? Pad.forDates(range) : Pad.toString(range);
	} else {
		return range;
	}
}

/**
 * Date.getAllMonthNumbers() gets all possible month numerical identifiers (1 - 12)
 *
 * @param as_string is whether the numbers should be strings
 * @param padded is whether to pad the numbers with zeroes.
 *
 * @return an array of numbers.
 * @type Array
*/
Date.getAllMonthNumbers			= function(as_string, padded) {
	return Date.getNumberRange(1, 12, as_string, padded);
}

/**
 * Date.getYearRange() gets an array of years in a passed range
 *
 * @param start is the start year
 * @param end is the end year
 * @param as_string is whether the numbers should be strings
 * @param padded is whether to pad the numbers with zeroes.
 *
 * @return an array of numbers.
 * @type Array
*/
Date.getYearRange				= function(start, end, as_string) {
	return Date.getNumberRange(start, end, as_string);
}

/**
 * Date.getAllDateNumbers() gets all possible date numerical identifiers (1 - 31). To get
 * a range of date numbers for a specific month (ie one with < 31 days), use the
 * getDateRange() method on a Date object.
 *
 * @param as_string is whether the numbers should be strings
 * @param padded is whether to pad the numbers with zeroes.
 *
 * @return an array of numbers.
 * @type Array
*/
Date.getAllDateNumbers			= function(as_string, padded) {
	return Date.getNumberRange(1, 31, as_string, padded);
}

/**
 * Date.getAllHourNumbers() gets all possible hour numerical identifiers (0 - 23)
 *
 * @param as_string is whether the numbers should be strings
 * @param padded is whether to pad the numbers with zeroes.
 *
 * @return an array of numbers.
 * @type Array
*/
Date.getAllHourNumbers			= function(as_string, padded) {
	return Date.getNumberRange(0, 23, as_string, padded);
}

/**
 * getAllHourNumbersPrintable() gets all possible hour numeric identifiers, but with a
 * 24 as the first number instead of a 0, for aesthetic purposes.
 * 
 * @param as_string is whether the numbers should be strings
 * @param padded is whether to pad the numbers with zeroes.
 *
 * @return an array of numbers.
 * @type Array
 */
Date.getAllHourNumbersPrintable	= function(as_string, padded) {
	
	// Get normal list
	var result = Date.getAllHourNumbers(as_string, padded);
	
	// Replace first item with a 24 instead
	result[0] = as_string ? "24" : 24;
	
	return result;
}

/**
 * Date.geAllMinuteNumbers() gets all possible minute numerical identifiers (0 - 59)
 *
 * @param as_string is whether the numbers should be strings
 * @param padded is whether to pad the numbers with zeroes.
 *
 * @return an array of numbers.
 * @type Array
*/
Date.getAllMinuteNumbers		= function(as_string, padded) {
	return Date.getNumberRange(0, 59, as_string, padded);
}

/**
 * Date.create() creates a new date based on a passed UNIX timestamp.
 *
 * @param seconds is the UNIX timestamp
 *
 * @return the new Date object.
 * @type Date
*/
Date.create						= function(seconds) {
	return new Date(parseInt(seconds) * 1000);
}

/**
 * Date.increment() facilitates increasing a value that can potentially loop - for
 * example a Date or Month.
 *
 * @param initial_value is the value to increment
 * @param amount is the amount to increment
 * @param max is the maximum value that can be obtained before looping
 * @param min is the minimum value that can be obtained before looping
 *
 * @return the new value.
 * @type Integer
*/
Date.increment					= function(initial_value, amount, max, min) {
	
	// Do initial increase
	initial_value += Math.abs(amount);
	
	// Decrease to fit in loop
	while (initial_value > max) {
		initial_value -= (max - min + 1);
	}
	
	// Done
	return initial_value;
}

/**
 * Date.decrement() facilitates decreasing a value that can potentially loop - for
 * example a Date or Month.
 *
 * @param initial_value is the value to decrement
 * @param amount is the amount to decrement
 * @param max is the maximum value that can be obtained before looping
 * @param min is the minimum value that can be obtained before looping
 *
 * @return the new value.
 * @type Integer
*/
Date.decrement					= function(initial_value, amount, max, min) {
	
	// Do initial decrease
	initial_value -= Math.abs(amount);
	
	// Increase to fit in loop
	while (initial_value < min) {
		initial_value += (max - min + 1);
	}
	
	// Done
	return initial_value;
}

/**
 * isValid() validates a series of parameters to see if they form a valid date.
 * 
 * @param y	{Integer}	The year value.
 * @param m	{Integer}	The month value.
 * @param d	{Integer}	The day (of the month) value.
 * @param h	{Integer}	The hours value.
 * @param i	{Integer}	The minutes value.
 * @param s	{Integer}	The seconds value.
 */
Date.isValid					= function(y, m, d, h, i, s) {
	
	// Validate variables
	y = isIntegeric(y) ? parseInt(y) : 2000;
	m = isIntegeric(m) ? parseInt(m) : 1;
	d = isIntegeric(d) ? parseInt(d) : 1;
	h = isIntegeric(h) ? parseInt(h) : 0;
	i = isIntegeric(i) ? parseInt(i) : 0;
	s = isIntegeric(s) ? parseInt(s) : 0;
	
	// Check 'em
	if (Date.isValidDate(d, m, y)) {
		if (Date.isValidHour(h)) {
			if (Date.isValidMinute(i)) {
				if (Date.isValidSecond(s)) {
					return true;
				}
			}
		}
	}
	
	// You lose
	return false;
}

Date.isValidYear				= function(y) {
	if (isIntegeric(y)) {
		y = parseInt(y);
		return (y >= 0 && y <= 4000);
	}
	
	return false;
}

Date.isValidMonth				= function(m) {
	if (isIntegeric(m)) {
		m = parseInt(m);
		return (m >= 1 && m <= 12);
	}
	
	return false;
}

Date.isValidDate				= function(d, m, y) {
	if (isIntegeric(d)) {
		d = parseInt(d);
		if (Date.isValidYear(y)) {
			if (Date.isValidMonth(m)) {
				return (d >= 1 && d <= Date.getMaxDate(m, y));
			}
		}
	}
	
	return false;
}

Date.isValidHour				= function(h) {
	if (isIntegeric(h)) {
		h = parseInt(h);
		return (h >= 0 && h <= 23);
	}
	
	return false;
}

Date.isValidMinute				= function(i) {
	if (isIntegeric(i)) {
		i = parseInt(i);
		return (i >= 0 && i <= 59);
	}
	
	return false;
}

Date.isValidSecond				= Date.isValidMinute;

/**
 * getMaxDate() gets the maximum date for passed month and year.
 * 
 * @param month	{Integer}	The month number, between 1 and 12.
 * @param year	{Integer}	The year.
 * 
 * @return the maximum date for that month, between 28 and 31.
 * @type Integer
 */
Date.getMaxDate					= function(month, year) {
	
	// Check month
	month = isIntegeric(month) ? Number(month) : false;
	
	// If month found OK
	if (month >= 1 && month <= 12) {
		if ([4, 6, 9, 11].search(month) !== false) {
			return 30;
		} else if (month == 2) {
			return Date.isLeapYear(year) ? 29 : 28;
		} else {
			return 31;
		}
	}
	
	// Fail
	return 0;
}

/**
 * isLeapYear() returns whether the passed year is a leap year.
 * 
 * @param year	{Integer}	The year to check.
 * 
 * @return true if the year is a leap year, false otherwise.
 * @type Boolean
 */
Date.isLeapYear					= function(year) {
	
	// Check year
	year = isIntegeric(year) ? Number(year) : false;
	
	// Verify
	if (year !== false) {
		return (year % 4 == 0 && !year % 100 == 0) || (year % 100 == 0 && year % 400 == 0);
	}
	
	// Fail
	return false;
}

Date.time						= function() {
	return (new Date()).time();
}

/**********************************************************
 'GET' METHODS
 *********************************************************/

/**
 * getMonthName() gets the name of the  month of this date.
 * 
 * @return the name of the month.
 * @type String
 */
Date.prototype.getMonthName			= function() {
	return Date.getAllMonthNames()[this.getMonth()];
}

/**
 * getShortMonthName() gets the 3-letter name of the month of this date.
 * 
 * @return the short name of the month.
 * @type String
 */
Date.prototype.getShortMonthName	= function() {
	return Date.getAllShortMonthNames()[this.getMonth()];
}

/**
 * getMonthNumber() gets the number of the month of this date. Note that this is
 * DIFFERENT from the built-in getMonth() call, which gets an index (0-11) - this method
 * returns the 'real' month number between 1 and 12.
 * 
 * @param as_string	{Boolean}	Whether the numbers should be strings
 * @param padded	{Boolean}	Whether to pad the numbers with zeroes.
 * 
 * @return the month number. 
 * @type String/Integer
 */
Date.prototype.getMonthNumber		= function(as_string, padded) {
	return Date.getAllMonthNumbers(as_string, padded)[this.getMonth()];
}

/**
 * isLeapYear() returns whether the year of the this date is a leap year.
 * 
 * @return true if this year is a leap year, false if not.
 * @type Boolean
 */
Date.prototype.isLeapYear			= function() {
	var year = this.getFullYear();
	return (year % 4 == 0 && !year % 100 == 0) || (year % 100 == 0 && year % 400 == 0);
}

/**
 * getMaxDate() gets the maximum date (day of the month) for the month of this date.
 * 
 * @param month	{Integer}	The month to use. Defaults to the month of this date.
 * 
 * @return the maximum date of this month.
 * @type Integer
 */
Date.prototype.getMaxDate			= function(month) {
	
	month = month || this.getMonth();
	
	if ([3, 5, 8, 10].search(month) !== false) {
		return 30;
	} else if (month == 1) {
		return this.isLeapYear() ? 29 : 28;
	} else {
		return 31;
	}
}

/**
 * getDateRange() gets the maximum range of numbers for dates in this month - ie numbers
 * between 1 and the highest date in this month.
 * 
 * @param as_string	{Boolean}	Whether the numbers should be strings
 * @param padded	{Boolean}	Whether to pad the numbers with zeroes.
 * @param month		{Integer}	The month to use. Defaults to the month of this date.
 */
Date.prototype.getDateRange			= function(as_string, padded, month) {
	return Date.getNumberRange(1, this.getMaxDate(month), as_string, padded);
}

/**
 * getDayName() gets the name of the day of this date.
 * 
 * @return the day name.
 * @type String
 */
Date.prototype.getDayName			= function() {
	return Date.getAllDayNames()[this.getDay()];
}

/**
 * getDayShortName() gets the short name of the day of this date.
 * 
 * @return the short day name.
 * @type String
 */
Date.prototype.getDayShortName		= function() {
	return Date.getAllDayShortNames()[this.getDay()];
}

/**
 * getPrintedDateSuffix() returns the suffix for the date number for printing this date.
 * For example, if the date was the 13/7/83, it would return 'th'.
 * 
 * @return the suffix.
 * @type String
 */
Date.prototype.getPrintedDateSuffix	= function() {
	
	// Characters to watch
	var full	= String(this.getDate());
	var last	= full.charAt(full.length - 1);
	
	if (last == "1" && full != "11") {
		return 	"st";
	} else if (last == "2" && full != "12") {
		return	"nd";
	} else if (last == "3" && full != "13") {
		return	"rd";
	} else {
		return	"th";
	}
}

/**
 * getHours12() gets the hours of this date in 12-hour format.
 * 
 * @return the hour in 12-hour form.
 * @type Integer
 */
Date.prototype.getHours12			= function() {

	// Get hours
	var hours = this.getHours();

	if (hours == 0) {
		return 12;
	} else if (hours > 12) {
		return hours - 12;
	} else {
		return hours;
	}
}

/**
 * getAMPM() returns whether this date is AM or PM.
 * 
 * @return AM or PM.
 * @type String
 */
Date.prototype.getAMPM				= function() {
	return this.getHours() > 11 ? "PM" : "AM";
}

Date.prototype.getTZFormatted		= function() {
	var hours = (this.getTimezoneOffset() * -1) / 60;
	
	var format = (hours < 0 ? "-" : "+") + Pad.forDates(hours) + (hours == parseInt(hours) ? "00" : "30");
	
	return format;
}

/**
 * format() allows construction of a format string to represent this date, similar to
 * PHP's date() method.
 * 
 * @param format	{String}	The format string to use.
 * 
 * @return the formatted string.
 * @type String
 */
Date.prototype.format				= function(format) {

	// Output
	var output	= "";
	var date	= this;
	
	$A(format).each(function(c) {
		if (c		== "a")										// a:	AM/PM, lowercase
			output	+= date.getAMPM.toLowerCase();
		else if (c	== "A")										// A:	AM/PM, uppercase
			output	+= date.getAMPM();
		else if (c	== "d")										// d:	Date, padded (see also 'j')
			output	+= Pad.forDates(date.getDate());
		else if (c	== "D")										// D:	Day name, short (see also 'l')
			output	+= date.getDayShortName();
		else if (c	== "F")										// F:	Month, full name (see also 'M')
			output	+= date.getMonthName();
		else if (c	== "g")										// g:	Hours, 12-hour time, unpadded
			output	+= Pad.toString(date.getHours12());
		else if (c	== "G")										// G:	Hours, 24-hour time, unpadded
			output	+= Pad.toString(date.getHours());
		else if (c	== "h")										// h:	Hours, 12-hour time, padded
			output	+= Pad.forDates(date.getHours12());
		else if (c	== "H")										// H:	Hours, 24-hour time, padded
			output	+= Pad.forDates(date.getHours());
		else if (c	== "i")										// i:	Minutes, padded
			output	+= Pad.forDates(date.getMinutes());
		else if (c	== "j")										// j:	Date, unpadded (see also 'd')
			output	+= Pad.toString(date.getDate());
		else if (c	== "l")										// l:	Day name, full (see also 'D')
			output	+= date.getDayName();
		else if (c	== "m")										// m:	Month, padded (see also 'n')
			output	+= date.getMonthNumber(Date.AS_STRING, Date.PADDED);
		else if (c	== "M")										// M:	Month, short name (see also 'F')
			output	+= date.getMonthShortName();
		else if (c	== "n")										// n:	Month, unpadded (see also 'm')
			output	+= date.getMonthNumber(Date.AS_STRING);
		else if (c	== "s")										// s:	Seconds, padded
			output	+= Pad.forDates(date.getSeconds());
		else if (c	== "S")										// S:	Date Suffix
			output	+= date.getPrintedDateSuffix();
		else if (c	== "U" || c == "u")							// u|U:	Number of seconds since UNIX epoch
			output	+= Pad.toString(Math.floor(date.getTime() / 1000));
		else if (c	== "y")										// y:	Year, short
				output	+= Pad.toString(date.getYear());
		else if (c	== "Y")										// Y:	Year, long
			output	+= Pad.toString(date.getFullYear());
		else if (c == "O")
			output	+= date.getTZFormatted();
		else
			output	+= c;	
	});

	return output;
}

/**
 * time() gets the UNIX timestamp of this date, like PHP's time() function.
 * 
 * @return the UNIX timestamp.
 * @type Integer
 */
Date.prototype.time					= function() {
	return parseInt(this.format("U"));
}

/**
 * getDayOf1st() gets the day of the week the first day of this date's month is on.
 * 
 * @return the day of the week (a number between 0 (Sun) and 6 (Sat)).
 * @type Integer
 */
Date.prototype.getDayOf1st			= function() {
	
	var date = new Date(this);
	date.setDate(1);
	return date.getDay();
}


/**********************************************************
 'SET' METHODS
 *********************************************************/

/**
 * incrementDate() increments a date by a passed number of days.
 *
 * @param days is the number of days. Defaults to 1
*/
Date.prototype.incrementDate	= function(days) {
	this.setTime((this.time() + Date.SECONDS_IN_DAY * (days || 1)) * 1000);
}

/**
 * incrementWeek() is a convenience method to increment a date by 7 days.
*/
Date.prototype.incrementWeek	= function() {
	this.incrementDate(7);
}

/**
 * incrementMonth() increments a date by a number of months.
 *
 * @param months is the number of months to increment. Defaults to 1
*/
Date.prototype.incrementMonth	= function(months) {
	
	// Store intial date
	var initial = new Date(this);
	
	// Reset date to 1 to avoid silliness
	this.setDate(1);
	
	// Loop for each month
	for (var i = 1; i <= (months || 1); i++) {
	
		// Increase month
		this.setMonth(Date.increment(this.getMonth(), 1, 11, 0));
	
		// Watch for a year increase as well
		if (initial.getMonth() > this.getMonth())
			this.setYear(this.getFullYear() + 1);
	}
	
	// Check date - if the original date doesn't exist in the new month (ie Feb 30th),
	// set to the maximum date.
	if (this.getMaxDate() < initial.getDate()) {
		this.setDate(this.getMaxDate());
	} else {
		this.setDate(initial.getDate());
	}
}

Date.prototype.incrementYear	= function(years) {
	
	this.setYear(this.getYear() + (years || 1));
}

/**
 * decrementDate() decrements a date by a passed number of days.
 *
 * @param days is the number of days to decrement by. Defaults to 1
*/
Date.prototype.decrementDate	= function(days) {
	this.setTime((this.time() - Date.SECONDS_IN_DAY * (days || 1)) * 1000);
}

/**
 * decrementWeek() is a convenience method to decrement a date by 7 days.
*/
Date.prototype.decrementWeek	= function() {
	this.decrementDate(7);
}

/**
 * decrementMonth() decrements a date by a number of months
 *
 * @param months is the number of months to decrement by. Defaults to 1
*/
Date.prototype.decrementMonth	= function(months) {
	
	// Store initial date
	var initial = new Date(this);
	
	// Reset date to 1 to avoid silliness
	this.setDate(1);
	
	// Loop for each month
	for (var i = 1; i <= (months || 1); i++) {
		
		// Decrease month
		this.setMonth(Date.decrement(this.getMonth(), 1, 11, 0));
	
		// If rolling back a year, set that too
		if (initial.getMonth() < this.getMonth())
			this.setYear(this.getFullYear() - 1);
	}
	
	// Check date - if the original date doesn't exist in the new month (ie Feb 30th),
	// set to the maximum date.
	if (this.getMaxDate() < initial.getDate()) {
		this.setDate(this.getMaxDate());
	} else {
		this.setDate(initial.getDate());
	}
}

Date.prototype.decrementYear	= function(years) {
	
	this.setYear(this.getYear() - (years || 1));
}

/*** @inca-include ../admin/assets/scripts/base/prototype-1.6.0.3.js ***/

/*  Prototype JavaScript framework, version 1.6.0.3
 *  (c) 2005-2008 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://www.prototypejs.org/
 *
 *--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.6.0.3',

  Browser: {
    IE:     !!(window.attachEvent &&
      navigator.userAgent.indexOf('Opera') === -1),
    Opera:  navigator.userAgent.indexOf('Opera') > -1,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 &&
      navigator.userAgent.indexOf('KHTML') === -1,
    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  },

  BrowserFeatures: {
    XPath: !!document.evaluate,
    SelectorsAPI: !!document.querySelector,
    ElementExtensions: !!window.HTMLElement,
    SpecificElementExtensions:
      document.createElement('div')['__proto__'] &&
      document.createElement('div')['__proto__'] !==
        document.createElement('form')['__proto__']
  },

  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,

  emptyFunction: function() { },
  K: function(x) { return x }
};

if (Prototype.Browser.MobileSafari)
  Prototype.BrowserFeatures.SpecificElementExtensions = false;


/* Based on Alex Arnell's inheritance implementation. */
var Class = {
  create: function() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

    function klass() {
      this.initialize.apply(this, arguments);
    }

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    if (parent) {
      var subclass = function() { };
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize)
      klass.prototype.initialize = Prototype.emptyFunction;

    klass.prototype.constructor = klass;

    return klass;
  }
};

Class.Methods = {
  addMethods: function(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = Object.keys(source);

    if (!Object.keys({ toString: true }).length)
      properties.push("toString", "valueOf");

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() == "$super") {
        var method = value;
        value = (function(m) {
          return function() { return ancestor[m].apply(this, arguments) };
        })(property).wrap(method);

        value.valueOf = method.valueOf.bind(method);
        value.toString = method.toString.bind(method);
      }
      this.prototype[property] = value;
    }

    return this;
  }
};

var Abstract = { };

Object.extend = function(destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
};

Object.extend(Object, {
  inspect: function(object) {
    try {
      if (Object.isUndefined(object)) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : String(object);
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  },

  toJSON: function(object) {
    var type = typeof object;
    switch (type) {
      case 'undefined':
      case 'function':
      case 'unknown': return;
      case 'boolean': return object.toString();
    }

    if (object === null) return 'null';
    if (object.toJSON) return object.toJSON();
    if (Object.isElement(object)) return;

    var results = [];
    for (var property in object) {
      var value = Object.toJSON(object[property]);
      if (!Object.isUndefined(value))
        results.push(property.toJSON() + ': ' + value);
    }

    return '{' + results.join(', ') + '}';
  },

  toQueryString: function(object) {
    return $H(object).toQueryString();
  },

  toHTML: function(object) {
    return object && object.toHTML ? object.toHTML() : String.interpret(object);
  },

  keys: function(object) {
    var keys = [];
    for (var property in object)
      keys.push(property);
    return keys;
  },

  values: function(object) {
    var values = [];
    for (var property in object)
      values.push(object[property]);
    return values;
  },

  clone: function(object) {
    return Object.extend({ }, object);
  },

  isElement: function(object) {
    return !!(object && object.nodeType == 1);
  },

  isArray: function(object) {
    return object != null && typeof object == "object" &&
      'splice' in object && 'join' in object;
  },

  isHash: function(object) {
    return object instanceof Hash;
  },

  isFunction: function(object) {
    return typeof object == "function";
  },

  isString: function(object) {
    return typeof object == "string";
  },

  isNumber: function(object) {
    return typeof object == "number";
  },

  isUndefined: function(object) {
    return typeof object == "undefined";
  }
});

Object.extend(Function.prototype, {
  argumentNames: function() {
    var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1]
      .replace(/\s+/g, '').split(',');
    return names.length == 1 && !names[0] ? [] : names;
  },

  bind: function() {
    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
    var __method = this, args = $A(arguments), object = args.shift();
    return function() {
      return __method.apply(object, args.concat($A(arguments)));
    }
  },

  bindAsEventListener: function() {
    var __method = this, args = $A(arguments), object = args.shift();
    return function(event) {
      return __method.apply(object, [event || window.event].concat(args));
    }
  },

  curry: function() {
    if (!arguments.length) return this;
    var __method = this, args = $A(arguments);
    return function() {
      return __method.apply(this, args.concat($A(arguments)));
    }
  },

  delay: function() {
    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  },

  defer: function() {
    var args = [0.01].concat($A(arguments));
    return this.delay.apply(this, args);
  },

  wrap: function(wrapper) {
    var __method = this;
    return function() {
      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
    }
  },

  methodize: function() {
    if (this._methodized) return this._methodized;
    var __method = this;
    return this._methodized = function() {
      return __method.apply(null, [this].concat($A(arguments)));
    };
  }
});

Date.prototype.toJSON = function() {
  return '"' + this.getUTCFullYear() + '-' +
    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
    this.getUTCDate().toPaddedString(2) + 'T' +
    this.getUTCHours().toPaddedString(2) + ':' +
    this.getUTCMinutes().toPaddedString(2) + ':' +
    this.getUTCSeconds().toPaddedString(2) + 'Z"';
};

var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) { }
    }

    return returnValue;
  }
};

RegExp.prototype.match = RegExp.prototype.test;

RegExp.escape = function(str) {
  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};

/*--------------------------------------------------------------------------*/

var PeriodicalExecuter = Class.create({
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

  registerCallback: function() {
    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  execute: function() {
    this.callback(this);
  },

  stop: function() {
    if (!this.timer) return;
    clearInterval(this.timer);
    this.timer = null;
  },

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.execute();
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
});
Object.extend(String, {
  interpret: function(value) {
    return value == null ? '' : String(value);
  },
  specialChar: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '\\': '\\\\'
  }
});

Object.extend(String.prototype, {
  gsub: function(pattern, replacement) {
    var result = '', source = this, match;
    replacement = arguments.callee.prepareReplacement(replacement);

    while (source.length > 0) {
      if (match = source.match(pattern)) {
        result += source.slice(0, match.index);
        result += String.interpret(replacement(match));
        source  = source.slice(match.index + match[0].length);
      } else {
        result += source, source = '';
      }
    }
    return result;
  },

  sub: function(pattern, replacement, count) {
    replacement = this.gsub.prepareReplacement(replacement);
    count = Object.isUndefined(count) ? 1 : count;

    return this.gsub(pattern, function(match) {
      if (--count < 0) return match[0];
      return replacement(match);
    });
  },

  scan: function(pattern, iterator) {
    this.gsub(pattern, iterator);
    return String(this);
  },

  truncate: function(length, truncation) {
    length = length || 30;
    truncation = Object.isUndefined(truncation) ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : String(this);
  },

  strip: function() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  },

  stripTags: function() {
    return this.replace(/<\/?[^>]+>/gi, '');
  },

  stripScripts: function() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  },

  extractScripts: function() {
    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
    return (this.match(matchAll) || []).map(function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  },

  evalScripts: function() {
    return this.extractScripts().map(function(script) { return eval(script) });
  },

  escapeHTML: function() {
    var self = arguments.callee;
    self.text.data = this;
    return self.div.innerHTML;
  },

  unescapeHTML: function() {
    var div = new Element('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? (div.childNodes.length > 1 ?
      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
      div.childNodes[0].nodeValue) : '';
  },

  toQueryParams: function(separator) {
    var match = this.strip().match(/([^?#]*)(#.*)?$/);
    if (!match) return { };

    return match[1].split(separator || '&').inject({ }, function(hash, pair) {
      if ((pair = pair.split('='))[0]) {
        var key = decodeURIComponent(pair.shift());
        var value = pair.length > 1 ? pair.join('=') : pair[0];
        if (value != undefined) value = decodeURIComponent(value);

        if (key in hash) {
          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
          hash[key].push(value);
        }
        else hash[key] = value;
      }
      return hash;
    });
  },

  toArray: function() {
    return this.split('');
  },

  succ: function() {
    return this.slice(0, this.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
  },

  times: function(count) {
    return count < 1 ? '' : new Array(count + 1).join(this);
  },

  camelize: function() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  },

  capitalize: function() {
    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
  },

  underscore: function() {
    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
  },

  dasherize: function() {
    return this.gsub(/_/,'-');
  },

  inspect: function(useDoubleQuotes) {
    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
      var character = String.specialChar[match[0]];
      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
    });
    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  },

  toJSON: function() {
    return this.inspect(true);
  },

  unfilterJSON: function(filter) {
    return this.sub(filter || Prototype.JSONFilter, '#{1}');
  },

  isJSON: function() {
    var str = this;
    if (str.blank()) return false;
    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
  },

  evalJSON: function(sanitize) {
    var json = this.unfilterJSON();
    try {
      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
    } catch (e) { }
    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
  },

  include: function(pattern) {
    return this.indexOf(pattern) > -1;
  },

  startsWith: function(pattern) {
    return this.indexOf(pattern) === 0;
  },

  endsWith: function(pattern) {
    var d = this.length - pattern.length;
    return d >= 0 && this.lastIndexOf(pattern) === d;
  },

  empty: function() {
    return this == '';
  },

  blank: function() {
    return /^\s*$/.test(this);
  },

  interpolate: function(object, pattern) {
    return new Template(this, pattern).evaluate(object);
  }
});

if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
  escapeHTML: function() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  },
  unescapeHTML: function() {
    return this.stripTags().replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
  }
});

String.prototype.gsub.prepareReplacement = function(replacement) {
  if (Object.isFunction(replacement)) return replacement;
  var template = new Template(replacement);
  return function(match) { return template.evaluate(match) };
};

String.prototype.parseQuery = String.prototype.toQueryParams;

Object.extend(String.prototype.escapeHTML, {
  div:  document.createElement('div'),
  text: document.createTextNode('')
});

String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);

var Template = Class.create({
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    if (Object.isFunction(object.toTemplateReplacements))
      object = object.toTemplateReplacements();

    return this.template.gsub(this.pattern, function(match) {
      if (object == null) return '';

      var before = match[1] || '';
      if (before == '\\') return match[2];

      var ctx = object, expr = match[3];
      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
      match = pattern.exec(expr);
      if (match == null) return before;

      while (match != null) {
        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
        ctx = ctx[comp];
        if (null == ctx || '' == match[3]) break;
        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
        match = pattern.exec(expr);
      }

      return before + String.interpret(ctx);
    });
  }
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

var $break = { };

var Enumerable = {
  each: function(iterator, context) {
    var index = 0;
    try {
      this._each(function(value) {
        iterator.call(context, value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  },

  eachSlice: function(number, iterator, context) {
    var index = -number, slices = [], array = this.toArray();
    if (number < 1) return array;
    while ((index += number) < array.length)
      slices.push(array.slice(index, index+number));
    return slices.collect(iterator, context);
  },

  all: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = true;
    this.each(function(value, index) {
      result = result && !!iterator.call(context, value, index);
      if (!result) throw $break;
    });
    return result;
  },

  any: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = false;
    this.each(function(value, index) {
      if (result = !!iterator.call(context, value, index))
        throw $break;
    });
    return result;
  },

  collect: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];
    this.each(function(value, index) {
      results.push(iterator.call(context, value, index));
    });
    return results;
  },

  detect: function(iterator, context) {
    var result;
    this.each(function(value, index) {
      if (iterator.call(context, value, index)) {
        result = value;
        throw $break;
      }
    });
    return result;
  },

  findAll: function(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  },

  grep: function(filter, iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];

    if (Object.isString(filter))
      filter = new RegExp(filter);

    this.each(function(value, index) {
      if (filter.match(value))
        results.push(iterator.call(context, value, index));
    });
    return results;
  },

  include: function(object) {
    if (Object.isFunction(this.indexOf))
      if (this.indexOf(object) != -1) return true;

    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  },

  inGroupsOf: function(number, fillWith) {
    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
    return this.eachSlice(number, function(slice) {
      while(slice.length < number) slice.push(fillWith);
      return slice;
    });
  },

  inject: function(memo, iterator, context) {
    this.each(function(value, index) {
      memo = iterator.call(context, memo, value, index);
    });
    return memo;
  },

  invoke: function(method) {
    var args = $A(arguments).slice(1);
    return this.map(function(value) {
      return value[method].apply(value, args);
    });
  },

  max: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value >= result)
        result = value;
    });
    return result;
  },

  min: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value < result)
        result = value;
    });
    return result;
  },

  partition: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var trues = [], falses = [];
    this.each(function(value, index) {
      (iterator.call(context, value, index) ?
        trues : falses).push(value);
    });
    return [trues, falses];
  },

  pluck: function(property) {
    var results = [];
    this.each(function(value) {
      results.push(value[property]);
    });
    return results;
  },

  reject: function(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (!iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  },

  sortBy: function(iterator, context) {
    return this.map(function(value, index) {
      return {
        value: value,
        criteria: iterator.call(context, value, index)
      };
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }).pluck('value');
  },

  toArray: function() {
    return this.map();
  },

  zip: function() {
    var iterator = Prototype.K, args = $A(arguments);
    if (Object.isFunction(args.last()))
      iterator = args.pop();

    var collections = [this].concat(args).map($A);
    return this.map(function(value, index) {
      return iterator(collections.pluck(index));
    });
  },

  size: function() {
    return this.toArray().length;
  },

  inspect: function() {
    return '#<Enumerable:' + this.toArray().inspect() + '>';
  }
};

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  filter:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray,
  every:   Enumerable.all,
  some:    Enumerable.any
});
function $A(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

if (Prototype.Browser.WebKit) {
  $A = function(iterable) {
    if (!iterable) return [];
    // In Safari, only use the `toArray` method if it's not a NodeList.
    // A NodeList is a function, has an function `item` property, and a numeric
    // `length` property. Adapted from Google Doctype.
    if (!(typeof iterable === 'function' && typeof iterable.length ===
        'number' && typeof iterable.item === 'function') && iterable.toArray)
      return iterable.toArray();
    var length = iterable.length || 0, results = new Array(length);
    while (length--) results[length] = iterable[length];
    return results;
  };
}

Array.from = $A;

Object.extend(Array.prototype, Enumerable);

if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  },

  clear: function() {
    this.length = 0;
    return this;
  },

  first: function() {
    return this[0];
  },

  last: function() {
    return this[this.length - 1];
  },

  compact: function() {
    return this.select(function(value) {
      return value != null;
    });
  },

  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(Object.isArray(value) ?
        value.flatten() : [value]);
    });
  },

  without: function() {
    var values = $A(arguments);
    return this.select(function(value) {
      return !values.include(value);
    });
  },

  reverse: function(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  },

  reduce: function() {
    return this.length > 1 ? this : this[0];
  },

  uniq: function(sorted) {
    return this.inject([], function(array, value, index) {
      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
        array.push(value);
      return array;
    });
  },

  intersect: function(array) {
    return this.uniq().findAll(function(item) {
      return array.detect(function(value) { return item === value });
    });
  },

  clone: function() {
    return [].concat(this);
  },

  size: function() {
    return this.length;
  },

  inspect: function() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  },

  toJSON: function() {
    var results = [];
    this.each(function(object) {
      var value = Object.toJSON(object);
      if (!Object.isUndefined(value)) results.push(value);
    });
    return '[' + results.join(', ') + ']';
  }
});

// use native browser JS 1.6 implementation if available
if (Object.isFunction(Array.prototype.forEach))
  Array.prototype._each = Array.prototype.forEach;

if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
  i || (i = 0);
  var length = this.length;
  if (i < 0) i = length + i;
  for (; i < length; i++)
    if (this[i] === item) return i;
  return -1;
};

if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
  var n = this.slice(0, i).reverse().indexOf(item);
  return (n < 0) ? n : i - n - 1;
};

Array.prototype.toArray = Array.prototype.clone;

function $w(string) {
  if (!Object.isString(string)) return [];
  string = string.strip();
  return string ? string.split(/\s+/) : [];
}

if (Prototype.Browser.Opera){
  Array.prototype.concat = function() {
    var array = [];
    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
    for (var i = 0, length = arguments.length; i < length; i++) {
      if (Object.isArray(arguments[i])) {
        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
          array.push(arguments[i][j]);
      } else {
        array.push(arguments[i]);
      }
    }
    return array;
  };
}
Object.extend(Number.prototype, {
  toColorPart: function() {
    return this.toPaddedString(2, 16);
  },

  succ: function() {
    return this + 1;
  },

  times: function(iterator, context) {
    $R(0, this, true).each(iterator, context);
    return this;
  },

  toPaddedString: function(length, radix) {
    var string = this.toString(radix || 10);
    return '0'.times(length - string.length) + string;
  },

  toJSON: function() {
    return isFinite(this) ? this.toString() : 'null';
  }
});

$w('abs round ceil floor').each(function(method){
  Number.prototype[method] = Math[method].methodize();
});
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {

  function toQueryPair(key, value) {
    if (Object.isUndefined(value)) return key;
    return key + '=' + encodeURIComponent(String.interpret(value));
  }

  return {
    initialize: function(object) {
      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
    },

    _each: function(iterator) {
      for (var key in this._object) {
        var value = this._object[key], pair = [key, value];
        pair.key = key;
        pair.value = value;
        iterator(pair);
      }
    },

    set: function(key, value) {
      return this._object[key] = value;
    },

    get: function(key) {
      // simulating poorly supported hasOwnProperty
      if (this._object[key] !== Object.prototype[key])
        return this._object[key];
    },

    unset: function(key) {
      var value = this._object[key];
      delete this._object[key];
      return value;
    },

    toObject: function() {
      return Object.clone(this._object);
    },

    keys: function() {
      return this.pluck('key');
    },

    values: function() {
      return this.pluck('value');
    },

    index: function(value) {
      var match = this.detect(function(pair) {
        return pair.value === value;
      });
      return match && match.key;
    },

    merge: function(object) {
      return this.clone().update(object);
    },

    update: function(object) {
      return new Hash(object).inject(this, function(result, pair) {
        result.set(pair.key, pair.value);
        return result;
      });
    },

    toQueryString: function() {
      return this.inject([], function(results, pair) {
        var key = encodeURIComponent(pair.key), values = pair.value;

        if (values && typeof values == 'object') {
          if (Object.isArray(values))
            return results.concat(values.map(toQueryPair.curry(key)));
        } else results.push(toQueryPair(key, values));
        return results;
      }).join('&');
    },

    inspect: function() {
      return '#<Hash:{' + this.map(function(pair) {
        return pair.map(Object.inspect).join(': ');
      }).join(', ') + '}>';
    },

    toJSON: function() {
      return Object.toJSON(this.toObject());
    },

    clone: function() {
      return new Hash(this);
    }
  }
})());

Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
Hash.from = $H;
var ObjectRange = Class.create(Enumerable, {
  initialize: function(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  },

  _each: function(iterator) {
    var value = this.start;
    while (this.include(value)) {
      iterator(value);
      value = value.succ();
    }
  },

  include: function(value) {
    if (value < this.start)
      return false;
    if (this.exclusive)
      return value < this.end;
    return value <= this.end;
  }
});

var $R = function(start, end, exclusive) {
  return new ObjectRange(start, end, exclusive);
};

var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (Object.isFunction(responder[callback])) {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) { }
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate:   function() { Ajax.activeRequestCount++ },
  onComplete: function() { Ajax.activeRequestCount-- }
});

Ajax.Base = Class.create({
  initialize: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   '',
      evalJSON:     true,
      evalJS:       true
    };
    Object.extend(this.options, options || { });

    this.options.method = this.options.method.toLowerCase();

    if (Object.isString(this.options.parameters))
      this.options.parameters = this.options.parameters.toQueryParams();
    else if (Object.isHash(this.options.parameters))
      this.options.parameters = this.options.parameters.toObject();
  }
});

Ajax.Request = Class.create(Ajax.Base, {
  _complete: false,

  initialize: function($super, url, options) {
    $super(options);
    this.transport = Ajax.getTransport();
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = Object.clone(this.options.parameters);

    if (!['get', 'post'].include(this.method)) {
      // simulate other verbs over post
      params['_method'] = this.method;
      this.method = 'post';
    }

    this.parameters = params;

    if (params = Object.toQueryString(params)) {
      // when GET, append parameters to URL
      if (this.method == 'get')
        this.url += (this.url.include('?') ? '&' : '?') + params;
      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
        params += '&_=';
    }

    try {
      var response = new Ajax.Response(this);
      if (this.options.onCreate) this.options.onCreate(response);
      Ajax.Responders.dispatch('onCreate', this, response);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
      this.transport.send(this.body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'X-Prototype-Version': Prototype.Version,
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    };

    if (this.method == 'post') {
      headers['Content-type'] = this.options.contentType +
        (this.options.encoding ? '; charset=' + this.options.encoding : '');

      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    // user-defined headers
    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (Object.isFunction(extras.push))
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    var status = this.getStatus();
    return !status || (status >= 200 && status < 300);
  },

  getStatus: function() {
    try {
      return this.transport.status || 0;
    } catch (e) { return 0 }
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && this.isSameOrigin() && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      // avoid memory leak in MSIE: clean up
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  isSameOrigin: function() {
    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
      protocol: location.protocol,
      domain: document.domain,
      port: location.port ? ':' + location.port : ''
    }));
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name) || null;
    } catch (e) { return null }
  },

  evalResponse: function() {
    try {
      return eval((this.transport.responseText || '').unfilterJSON());
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Response = Class.create({
  initialize: function(request){
    this.request = request;
    var transport  = this.transport  = request.transport,
        readyState = this.readyState = transport.readyState;

    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
      this.status       = this.getStatus();
      this.statusText   = this.getStatusText();
      this.responseText = String.interpret(transport.responseText);
      this.headerJSON   = this._getHeaderJSON();
    }

    if(readyState == 4) {
      var xml = transport.responseXML;
      this.responseXML  = Object.isUndefined(xml) ? null : xml;
      this.responseJSON = this._getResponseJSON();
    }
  },

  status:      0,
  statusText: '',

  getStatus: Ajax.Request.prototype.getStatus,

  getStatusText: function() {
    try {
      return this.transport.statusText || '';
    } catch (e) { return '' }
  },

  getHeader: Ajax.Request.prototype.getHeader,

  getAllHeaders: function() {
    try {
      return this.getAllResponseHeaders();
    } catch (e) { return null }
  },

  getResponseHeader: function(name) {
    return this.transport.getResponseHeader(name);
  },

  getAllResponseHeaders: function() {
    return this.transport.getAllResponseHeaders();
  },

  _getHeaderJSON: function() {
    var json = this.getHeader('X-JSON');
    if (!json) return null;
    json = decodeURIComponent(escape(json));
    try {
      return json.evalJSON(this.request.options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  },

  _getResponseJSON: function() {
    var options = this.request.options;
    if (!options.evalJSON || (options.evalJSON != 'force' &&
      !(this.getHeader('Content-type') || '').include('application/json')) ||
        this.responseText.blank())
          return null;
    try {
      return this.responseText.evalJSON(options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  }
});

Ajax.Updater = Class.create(Ajax.Request, {
  initialize: function($super, container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    };

    options = Object.clone(options);
    var onComplete = options.onComplete;
    options.onComplete = (function(response, json) {
      this.updateContent(response.responseText);
      if (Object.isFunction(onComplete)) onComplete(response, json);
    }).bind(this);

    $super(url, options);
  },

  updateContent: function(responseText) {
    var receiver = this.container[this.success() ? 'success' : 'failure'],
        options = this.options;

    if (!options.evalScripts) responseText = responseText.stripScripts();

    if (receiver = $(receiver)) {
      if (options.insertion) {
        if (Object.isString(options.insertion)) {
          var insertion = { }; insertion[options.insertion] = responseText;
          receiver.insert(insertion);
        }
        else options.insertion(receiver, responseText);
      }
      else receiver.update(responseText);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
  initialize: function($super, container, url, options) {
    $super(options);
    this.onComplete = this.options.onComplete;

    this.frequency = (this.options.frequency || 2);
    this.decay = (this.options.decay || 1);

    this.updater = { };
    this.container = container;
    this.url = url;

    this.start();
  },

  start: function() {
    this.options.onComplete = this.updateComplete.bind(this);
    this.onTimerEvent();
  },

  stop: function() {
    this.updater.options.onComplete = undefined;
    clearTimeout(this.timer);
    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
  },

  updateComplete: function(response) {
    if (this.options.decay) {
      this.decay = (response.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = response.responseText;
    }
    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});
function $(element) {
  if (arguments.length > 1) {
    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
      elements.push($(arguments[i]));
    return elements;
  }
  if (Object.isString(element))
    element = document.getElementById(element);
  return Element.extend(element);
}

if (Prototype.BrowserFeatures.XPath) {
  document._getElementsByXPath = function(expression, parentElement) {
    var results = [];
    var query = document.evaluate(expression, $(parentElement) || document,
      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, length = query.snapshotLength; i < length; i++)
      results.push(Element.extend(query.snapshotItem(i)));
    return results;
  };
}

/*--------------------------------------------------------------------------*/

if (!window.Node) var Node = { };

if (!Node.ELEMENT_NODE) {
  // DOM level 2 ECMAScript Language Binding
  Object.extend(Node, {
    ELEMENT_NODE: 1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE: 3,
    CDATA_SECTION_NODE: 4,
    ENTITY_REFERENCE_NODE: 5,
    ENTITY_NODE: 6,
    PROCESSING_INSTRUCTION_NODE: 7,
    COMMENT_NODE: 8,
    DOCUMENT_NODE: 9,
    DOCUMENT_TYPE_NODE: 10,
    DOCUMENT_FRAGMENT_NODE: 11,
    NOTATION_NODE: 12
  });
}

(function() {
  var element = this.Element;
  this.Element = function(tagName, attributes) {
    attributes = attributes || { };
    tagName = tagName.toLowerCase();
    var cache = Element.cache;
    if (Prototype.Browser.IE && attributes.name) {
      tagName = '<' + tagName + ' name="' + attributes.name + '">';
      delete attributes.name;
      return Element.writeAttribute(document.createElement(tagName), attributes);
    }
    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
  };
  Object.extend(this.Element, element || { });
  if (element) this.Element.prototype = element.prototype;
}).call(window);

Element.cache = { };

Element.Methods = {
  visible: function(element) {
    return $(element).style.display != 'none';
  },

  toggle: function(element) {
    element = $(element);
    Element[Element.visible(element) ? 'hide' : 'show'](element);
    return element;
  },

  hide: function(element) {
    element = $(element);
    element.style.display = 'none';
    return element;
  },

  show: function(element) {
    element = $(element);
    element.style.display = '';
    return element;
  },

  remove: function(element) {
    element = $(element);
    element.parentNode.removeChild(element);
    return element;
  },

  update: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);
    content = Object.toHTML(content);
    element.innerHTML = content.stripScripts();
    content.evalScripts.bind(content).defer();
    return element;
  },

  replace: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    else if (!Object.isElement(content)) {
      content = Object.toHTML(content);
      var range = element.ownerDocument.createRange();
      range.selectNode(element);
      content.evalScripts.bind(content).defer();
      content = range.createContextualFragment(content.stripScripts());
    }
    element.parentNode.replaceChild(content, element);
    return element;
  },

  insert: function(element, insertions) {
    element = $(element);

    if (Object.isString(insertions) || Object.isNumber(insertions) ||
        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
          insertions = {bottom:insertions};

    var content, insert, tagName, childNodes;

    for (var position in insertions) {
      content  = insertions[position];
      position = position.toLowerCase();
      insert = Element._insertionTranslations[position];

      if (content && content.toElement) content = content.toElement();
      if (Object.isElement(content)) {
        insert(element, content);
        continue;
      }

      content = Object.toHTML(content);

      tagName = ((position == 'before' || position == 'after')
        ? element.parentNode : element).tagName.toUpperCase();

      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

      if (position == 'top' || position == 'after') childNodes.reverse();
      childNodes.each(insert.curry(element));

      content.evalScripts.bind(content).defer();
    }

    return element;
  },

  wrap: function(element, wrapper, attributes) {
    element = $(element);
    if (Object.isElement(wrapper))
      $(wrapper).writeAttribute(attributes || { });
    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
    else wrapper = new Element('div', wrapper);
    if (element.parentNode)
      element.parentNode.replaceChild(wrapper, element);
    wrapper.appendChild(element);
    return wrapper;
  },

  inspect: function(element) {
    element = $(element);
    var result = '<' + element.tagName.toLowerCase();
    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
      var property = pair.first(), attribute = pair.last();
      var value = (element[property] || '').toString();
      if (value) result += ' ' + attribute + '=' + value.inspect(true);
    });
    return result + '>';
  },

  recursivelyCollect: function(element, property) {
    element = $(element);
    var elements = [];
    while (element = element[property])
      if (element.nodeType == 1)
        elements.push(Element.extend(element));
    return elements;
  },

  ancestors: function(element) {
    return $(element).recursivelyCollect('parentNode');
  },

  descendants: function(element) {
    return $(element).select("*");
  },

  firstDescendant: function(element) {
    element = $(element).firstChild;
    while (element && element.nodeType != 1) element = element.nextSibling;
    return $(element);
  },

  immediateDescendants: function(element) {
    if (!(element = $(element).firstChild)) return [];
    while (element && element.nodeType != 1) element = element.nextSibling;
    if (element) return [element].concat($(element).nextSiblings());
    return [];
  },

  previousSiblings: function(element) {
    return $(element).recursivelyCollect('previousSibling');
  },

  nextSiblings: function(element) {
    return $(element).recursivelyCollect('nextSibling');
  },

  siblings: function(element) {
    element = $(element);
    return element.previousSiblings().reverse().concat(element.nextSiblings());
  },

  match: function(element, selector) {
    if (Object.isString(selector))
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(element.parentNode);
    var ancestors = element.ancestors();
    return Object.isNumber(expression) ? ancestors[expression] :
      Selector.findElement(ancestors, expression, index);
  },

  down: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return element.firstDescendant();
    return Object.isNumber(expression) ? element.descendants()[expression] :
      Element.select(element, expression)[index || 0];
  },

  previous: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
    var previousSiblings = element.previousSiblings();
    return Object.isNumber(expression) ? previousSiblings[expression] :
      Selector.findElement(previousSiblings, expression, index);
  },

  next: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
    var nextSiblings = element.nextSiblings();
    return Object.isNumber(expression) ? nextSiblings[expression] :
      Selector.findElement(nextSiblings, expression, index);
  },

  select: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element, args);
  },

  adjacent: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element.parentNode, args).without(element);
  },

  identify: function(element) {
    element = $(element);
    var id = element.readAttribute('id'), self = arguments.callee;
    if (id) return id;
    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
    element.writeAttribute('id', id);
    return id;
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (Prototype.Browser.IE) {
      var t = Element._attributeTranslations.read;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name]) name = t.names[name];
      if (name.include(':')) {
        return (!element.attributes || !element.attributes[name]) ? null :
         element.attributes[name].value;
      }
    }
    return element.getAttribute(name);
  },

  writeAttribute: function(element, name, value) {
    element = $(element);
    var attributes = { }, t = Element._attributeTranslations.write;

    if (typeof name == 'object') attributes = name;
    else attributes[name] = Object.isUndefined(value) ? true : value;

    for (var attr in attributes) {
      name = t.names[attr] || attr;
      value = attributes[attr];
      if (t.values[attr]) name = t.values[attr](element, value);
      if (value === false || value === null)
        element.removeAttribute(name);
      else if (value === true)
        element.setAttribute(name, name);
      else element.setAttribute(name, value);
    }
    return element;
  },

  getHeight: function(element) {
    return $(element).getDimensions().height;
  },

  getWidth: function(element) {
    return $(element).getDimensions().width;
  },

  classNames: function(element) {
    return new Element.ClassNames(element);
  },

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    var elementClassName = element.className;
    return (elementClassName.length > 0 && (elementClassName == className ||
      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    if (!element.hasClassName(className))
      element.className += (element.className ? ' ' : '') + className;
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    element.className = element.className.replace(
      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
    return element;
  },

  toggleClassName: function(element, className) {
    if (!(element = $(element))) return;
    return element[element.hasClassName(className) ?
      'removeClassName' : 'addClassName'](className);
  },

  // removes whitespace-only text node children
  cleanWhitespace: function(element) {
    element = $(element);
    var node = element.firstChild;
    while (node) {
      var nextNode = node.nextSibling;
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        element.removeChild(node);
      node = nextNode;
    }
    return element;
  },

  empty: function(element) {
    return $(element).innerHTML.blank();
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);

    if (element.compareDocumentPosition)
      return (element.compareDocumentPosition(ancestor) & 8) === 8;

    if (ancestor.contains)
      return ancestor.contains(element) && ancestor !== element;

    while (element = element.parentNode)
      if (element == ancestor) return true;

    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = element.cumulativeOffset();
    window.scrollTo(pos[0], pos[1]);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    style = style == 'float' ? 'cssFloat' : style.camelize();
    var value = element.style[style];
    if (!value || value == 'auto') {
      var css = document.defaultView.getComputedStyle(element, null);
      value = css ? css[style] : null;
    }
    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
    return value == 'auto' ? null : value;
  },

  getOpacity: function(element) {
    return $(element).getStyle('opacity');
  },

  setStyle: function(element, styles) {
    element = $(element);
    var elementStyle = element.style, match;
    if (Object.isString(styles)) {
      element.style.cssText += ';' + styles;
      return styles.include('opacity') ?
        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
    }
    for (var property in styles)
      if (property == 'opacity') element.setOpacity(styles[property]);
      else
        elementStyle[(property == 'float' || property == 'cssFloat') ?
          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
            property] = styles[property];

    return element;
  },

  setOpacity: function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    var display = element.getStyle('display');
    if (display != 'none' && display != null) // Safari bug
      return {width: element.offsetWidth, height: element.offsetHeight};

    // All *Width and *Height properties give 0 on elements with display none,
    // so enable the element temporarily
    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};
  },

  makePositioned: function(element) {
    element = $(element);
    var pos = Element.getStyle(element, 'position');
    if (pos == 'static' || !pos) {
      element._madePositioned = true;
      element.style.position = 'relative';
      // Opera returns the offset relative to the positioning context, when an
      // element is position relative but top and left have not been defined
      if (Prototype.Browser.Opera) {
        element.style.top = 0;
        element.style.left = 0;
      }
    }
    return element;
  },

  undoPositioned: function(element) {
    element = $(element);
    if (element._madePositioned) {
      element._madePositioned = undefined;
      element.style.position =
        element.style.top =
        element.style.left =
        element.style.bottom =
        element.style.right = '';
    }
    return element;
  },

  makeClipping: function(element) {
    element = $(element);
    if (element._overflow) return element;
    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
    if (element._overflow !== 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

  undoClipping: function(element) {
    element = $(element);
    if (!element._overflow) return element;
    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
    element._overflow = null;
    return element;
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        if (element.tagName.toUpperCase() == 'BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p !== 'static') break;
      }
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  absolutize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'absolute') return element;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    var offsets = element.positionedOffset();
    var top     = offsets[1];
    var left    = offsets[0];
    var width   = element.clientWidth;
    var height  = element.clientHeight;

    element._originalLeft   = left - parseFloat(element.style.left  || 0);
    element._originalTop    = top  - parseFloat(element.style.top || 0);
    element._originalWidth  = element.style.width;
    element._originalHeight = element.style.height;

    element.style.position = 'absolute';
    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.width  = width + 'px';
    element.style.height = height + 'px';
    return element;
  },

  relativize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'relative') return element;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    element.style.position = 'relative';
    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.height = element._originalHeight;
    element.style.width  = element._originalWidth;
    return element;
  },

  cumulativeScrollOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  getOffsetParent: function(element) {
    if (element.offsetParent) return $(element.offsetParent);
    if (element == document.body) return $(element);

    while ((element = element.parentNode) && element != document.body)
      if (Element.getStyle(element, 'position') != 'static')
        return $(element);

    return $(document.body);
  },

  viewportOffset: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      // Safari fix
      if (element.offsetParent == document.body &&
        Element.getStyle(element, 'position') == 'absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return Element._returnOffset(valueL, valueT);
  },

  clonePosition: function(element, source) {
    var options = Object.extend({
      setLeft:    true,
      setTop:     true,
      setWidth:   true,
      setHeight:  true,
      offsetTop:  0,
      offsetLeft: 0
    }, arguments[2] || { });

    // find page position of source
    source = $(source);
    var p = source.viewportOffset();

    // find coordinate system to use
    element = $(element);
    var delta = [0, 0];
    var parent = null;
    // delta [0,0] will do fine with position: fixed elements,
    // position:absolute needs offsetParent deltas
    if (Element.getStyle(element, 'position') == 'absolute') {
      parent = element.getOffsetParent();
      delta = parent.viewportOffset();
    }

    // correct by body offsets (fixes Safari)
    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

    // set position
    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
    return element;
  }
};

Element.Methods.identify.counter = 1;

Object.extend(Element.Methods, {
  getElementsBySelector: Element.Methods.select,
  childElements: Element.Methods.immediateDescendants
});

Element._attributeTranslations = {
  write: {
    names: {
      className: 'class',
      htmlFor:   'for'
    },
    values: { }
  }
};

if (Prototype.Browser.Opera) {
  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
    function(proceed, element, style) {
      switch (style) {
        case 'left': case 'top': case 'right': case 'bottom':
          if (proceed(element, 'position') === 'static') return null;
        case 'height': case 'width':
          // returns '0px' for hidden elements; we want it to return null
          if (!Element.visible(element)) return null;

          // returns the border-box dimensions rather than the content-box
          // dimensions, so we subtract padding and borders from the value
          var dim = parseInt(proceed(element, style), 10);

          if (dim !== element['offset' + style.capitalize()])
            return dim + 'px';

          var properties;
          if (style === 'height') {
            properties = ['border-top-width', 'padding-top',
             'padding-bottom', 'border-bottom-width'];
          }
          else {
            properties = ['border-left-width', 'padding-left',
             'padding-right', 'border-right-width'];
          }
          return properties.inject(dim, function(memo, property) {
            var val = proceed(element, property);
            return val === null ? memo : memo - parseInt(val, 10);
          }) + 'px';
        default: return proceed(element, style);
      }
    }
  );

  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
    function(proceed, element, attribute) {
      if (attribute === 'title') return element.title;
      return proceed(element, attribute);
    }
  );
}

else if (Prototype.Browser.IE) {
  // IE doesn't report offsets correctly for static elements, so we change them
  // to "relative" to get the values, then change them back.
  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
    function(proceed, element) {
      element = $(element);
      // IE throws an error if element is not in document
      try { element.offsetParent }
      catch(e) { return $(document.body) }
      var position = element.getStyle('position');
      if (position !== 'static') return proceed(element);
      element.setStyle({ position: 'relative' });
      var value = proceed(element);
      element.setStyle({ position: position });
      return value;
    }
  );

  $w('positionedOffset viewportOffset').each(function(method) {
    Element.Methods[method] = Element.Methods[method].wrap(
      function(proceed, element) {
        element = $(element);
        try { element.offsetParent }
        catch(e) { return Element._returnOffset(0,0) }
        var position = element.getStyle('position');
        if (position !== 'static') return proceed(element);
        // Trigger hasLayout on the offset parent so that IE6 reports
        // accurate offsetTop and offsetLeft values for position: fixed.
        var offsetParent = element.getOffsetParent();
        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
          offsetParent.setStyle({ zoom: 1 });
        element.setStyle({ position: 'relative' });
        var value = proceed(element);
        element.setStyle({ position: position });
        return value;
      }
    );
  });

  Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
    function(proceed, element) {
      try { element.offsetParent }
      catch(e) { return Element._returnOffset(0,0) }
      return proceed(element);
    }
  );

  Element.Methods.getStyle = function(element, style) {
    element = $(element);
    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
    var value = element.style[style];
    if (!value && element.currentStyle) value = element.currentStyle[style];

    if (style == 'opacity') {
      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if (value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }

    if (value == 'auto') {
      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
        return element['offset' + style.capitalize()] + 'px';
      return null;
    }
    return value;
  };

  Element.Methods.setOpacity = function(element, value) {
    function stripAlpha(filter){
      return filter.replace(/alpha\([^\)]*\)/gi,'');
    }
    element = $(element);
    var currentStyle = element.currentStyle;
    if ((currentStyle && !currentStyle.hasLayout) ||
      (!currentStyle && element.style.zoom == 'normal'))
        element.style.zoom = 1;

    var filter = element.getStyle('filter'), style = element.style;
    if (value == 1 || value === '') {
      (filter = stripAlpha(filter)) ?
        style.filter = filter : style.removeAttribute('filter');
      return element;
    } else if (value < 0.00001) value = 0;
    style.filter = stripAlpha(filter) +
      'alpha(opacity=' + (value * 100) + ')';
    return element;
  };

  Element._attributeTranslations = {
    read: {
      names: {
        'class': 'className',
        'for':   'htmlFor'
      },
      values: {
        _getAttr: function(element, attribute) {
          return element.getAttribute(attribute, 2);
        },
        _getAttrNode: function(element, attribute) {
          var node = element.getAttributeNode(attribute);
          return node ? node.value : "";
        },
        _getEv: function(element, attribute) {
          attribute = element.getAttribute(attribute);
          return attribute ? attribute.toString().slice(23, -2) : null;
        },
        _flag: function(element, attribute) {
          return $(element).hasAttribute(attribute) ? attribute : null;
        },
        style: function(element) {
          return element.style.cssText.toLowerCase();
        },
        title: function(element) {
          return element.title;
        }
      }
    }
  };

  Element._attributeTranslations.write = {
    names: Object.extend({
      cellpadding: 'cellPadding',
      cellspacing: 'cellSpacing'
    }, Element._attributeTranslations.read.names),
    values: {
      checked: function(element, value) {
        element.checked = !!value;
      },

      style: function(element, value) {
        element.style.cssText = value ? value : '';
      }
    }
  };

  Element._attributeTranslations.has = {};

  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
      'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
  });

  (function(v) {
    Object.extend(v, {
      href:        v._getAttr,
      src:         v._getAttr,
      type:        v._getAttr,
      action:      v._getAttrNode,
      disabled:    v._flag,
      checked:     v._flag,
      readonly:    v._flag,
      multiple:    v._flag,
      onload:      v._getEv,
      onunload:    v._getEv,
      onclick:     v._getEv,
      ondblclick:  v._getEv,
      onmousedown: v._getEv,
      onmouseup:   v._getEv,
      onmouseover: v._getEv,
      onmousemove: v._getEv,
      onmouseout:  v._getEv,
      onfocus:     v._getEv,
      onblur:      v._getEv,
      onkeypress:  v._getEv,
      onkeydown:   v._getEv,
      onkeyup:     v._getEv,
      onsubmit:    v._getEv,
      onreset:     v._getEv,
      onselect:    v._getEv,
      onchange:    v._getEv
    });
  })(Element._attributeTranslations.read.values);
}

else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1) ? 0.999999 :
      (value === '') ? '' : (value < 0.00001) ? 0 : value;
    return element;
  };
}

else if (Prototype.Browser.WebKit) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;

    if (value == 1)
      if(element.tagName.toUpperCase() == 'IMG' && element.width) {
        element.width++; element.width--;
      } else try {
        var n = document.createTextNode(' ');
        element.appendChild(n);
        element.removeChild(n);
      } catch (e) { }

    return element;
  };

  // Safari returns margins on body which is incorrect if the child is absolutely
  // positioned.  For performance reasons, redefine Element#cumulativeOffset for
  // KHTML/WebKit only.
  Element.Methods.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return Element._returnOffset(valueL, valueT);
  };
}

if (Prototype.Browser.IE || Prototype.Browser.Opera) {
  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
  Element.Methods.update = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);

    content = Object.toHTML(content);
    var tagName = element.tagName.toUpperCase();

    if (tagName in Element._insertionTranslations.tags) {
      $A(element.childNodes).each(function(node) { element.removeChild(node) });
      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
        .each(function(node) { element.appendChild(node) });
    }
    else element.innerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

if ('outerHTML' in document.createElement('div')) {
  Element.Methods.replace = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) {
      element.parentNode.replaceChild(content, element);
      return element;
    }

    content = Object.toHTML(content);
    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

    if (Element._insertionTranslations.tags[tagName]) {
      var nextSibling = element.next();
      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
      parent.removeChild(element);
      if (nextSibling)
        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
      else
        fragments.each(function(node) { parent.appendChild(node) });
    }
    else element.outerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

Element._returnOffset = function(l, t) {
  var result = [l, t];
  result.left = l;
  result.top = t;
  return result;
};

Element._getContentFromAnonymousElement = function(tagName, html) {
  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
  if (t) {
    div.innerHTML = t[0] + html + t[1];
    t[2].times(function() { div = div.firstChild });
  } else div.innerHTML = html;
  return $A(div.childNodes);
};

Element._insertionTranslations = {
  before: function(element, node) {
    element.parentNode.insertBefore(node, element);
  },
  top: function(element, node) {
    element.insertBefore(node, element.firstChild);
  },
  bottom: function(element, node) {
    element.appendChild(node);
  },
  after: function(element, node) {
    element.parentNode.insertBefore(node, element.nextSibling);
  },
  tags: {
    TABLE:  ['<table>',                '</table>',                   1],
    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
    SELECT: ['<select>',               '</select>',                  1]
  }
};

(function() {
  Object.extend(this.tags, {
    THEAD: this.tags.TBODY,
    TFOOT: this.tags.TBODY,
    TH:    this.tags.TD
  });
}).call(Element._insertionTranslations);

Element.Methods.Simulated = {
  hasAttribute: function(element, attribute) {
    attribute = Element._attributeTranslations.has[attribute] || attribute;
    var node = $(element).getAttributeNode(attribute);
    return !!(node && node.specified);
  }
};

Element.Methods.ByTag = { };

Object.extend(Element, Element.Methods);

if (!Prototype.BrowserFeatures.ElementExtensions &&
    document.createElement('div')['__proto__']) {
  window.HTMLElement = { };
  window.HTMLElement.prototype = document.createElement('div')['__proto__'];
  Prototype.BrowserFeatures.ElementExtensions = true;
}

Element.extend = (function() {
  if (Prototype.BrowserFeatures.SpecificElementExtensions)
    return Prototype.K;

  var Methods = { }, ByTag = Element.Methods.ByTag;

  var extend = Object.extend(function(element) {
    if (!element || element._extendedByPrototype ||
        element.nodeType != 1 || element == window) return element;

    var methods = Object.clone(Methods),
      tagName = element.tagName.toUpperCase(), property, value;

    // extend methods for specific tags
    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

    for (property in methods) {
      value = methods[property];
      if (Object.isFunction(value) && !(property in element))
        element[property] = value.methodize();
    }

    element._extendedByPrototype = Prototype.emptyFunction;
    return element;

  }, {
    refresh: function() {
      // extend methods for all tags (Safari doesn't need this)
      if (!Prototype.BrowserFeatures.ElementExtensions) {
        Object.extend(Methods, Element.Methods);
        Object.extend(Methods, Element.Methods.Simulated);
      }
    }
  });

  extend.refresh();
  return extend;
})();

Element.hasAttribute = function(element, attribute) {
  if (element.hasAttribute) return element.hasAttribute(attribute);
  return Element.Methods.Simulated.hasAttribute(element, attribute);
};

Element.addMethods = function(methods) {
  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

  if (!methods) {
    Object.extend(Form, Form.Methods);
    Object.extend(Form.Element, Form.Element.Methods);
    Object.extend(Element.Methods.ByTag, {
      "FORM":     Object.clone(Form.Methods),
      "INPUT":    Object.clone(Form.Element.Methods),
      "SELECT":   Object.clone(Form.Element.Methods),
      "TEXTAREA": Object.clone(Form.Element.Methods)
    });
  }

  if (arguments.length == 2) {
    var tagName = methods;
    methods = arguments[1];
  }

  if (!tagName) Object.extend(Element.Methods, methods || { });
  else {
    if (Object.isArray(tagName)) tagName.each(extend);
    else extend(tagName);
  }

  function extend(tagName) {
    tagName = tagName.toUpperCase();
    if (!Element.Methods.ByTag[tagName])
      Element.Methods.ByTag[tagName] = { };
    Object.extend(Element.Methods.ByTag[tagName], methods);
  }

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    for (var property in methods) {
      var value = methods[property];
      if (!Object.isFunction(value)) continue;
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = value.methodize();
    }
  }

  function findDOMClass(tagName) {
    var klass;
    var trans = {
      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
      "FrameSet", "IFRAME": "IFrame"
    };
    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName.capitalize() + 'Element';
    if (window[klass]) return window[klass];

    window[klass] = { };
    window[klass].prototype = document.createElement(tagName)['__proto__'];
    return window[klass];
  }

  if (F.ElementExtensions) {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
  }

  if (F.SpecificElementExtensions) {
    for (var tag in Element.Methods.ByTag) {
      var klass = findDOMClass(tag);
      if (Object.isUndefined(klass)) continue;
      copy(T[tag], klass.prototype);
    }
  }

  Object.extend(Element, Element.Methods);
  delete Element.ByTag;

  if (Element.extend.refresh) Element.extend.refresh();
  Element.cache = { };
};

document.viewport = {
  getDimensions: function() {
    var dimensions = { }, B = Prototype.Browser;
    $w('width height').each(function(d) {
      var D = d.capitalize();
      if (B.WebKit && !document.evaluate) {
        // Safari <3.0 needs self.innerWidth/Height
        dimensions[d] = self['inner' + D];
      } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) {
        // Opera <9.5 needs document.body.clientWidth/Height
        dimensions[d] = document.body['client' + D]
      } else {
        dimensions[d] = document.documentElement['client' + D];
      }
    });
    return dimensions;
  },

  getWidth: function() {
    return this.getDimensions().width;
  },

  getHeight: function() {
    return this.getDimensions().height;
  },

  getScrollOffsets: function() {
    return Element._returnOffset(
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
  }
};
/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
 * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
 * license.  Please see http://www.yui-ext.com/ for more information. */

var Selector = Class.create({
  initialize: function(expression) {
    this.expression = expression.strip();

    if (this.shouldUseSelectorsAPI()) {
      this.mode = 'selectorsAPI';
    } else if (this.shouldUseXPath()) {
      this.mode = 'xpath';
      this.compileXPathMatcher();
    } else {
      this.mode = "normal";
      this.compileMatcher();
    }

  },

  shouldUseXPath: function() {
    if (!Prototype.BrowserFeatures.XPath) return false;

    var e = this.expression;

    // Safari 3 chokes on :*-of-type and :empty
    if (Prototype.Browser.WebKit &&
     (e.include("-of-type") || e.include(":empty")))
      return false;

    // XPath can't do namespaced attributes, nor can it read
    // the "checked" property from DOM nodes
    if ((/(\[[\w-]*?:|:checked)/).test(e))
      return false;

    return true;
  },

  shouldUseSelectorsAPI: function() {
    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;

    if (!Selector._div) Selector._div = new Element('div');

    // Make sure the browser treats the selector as valid. Test on an
    // isolated element to minimize cost of this check.
    try {
      Selector._div.querySelector(this.expression);
    } catch(e) {
      return false;
    }

    return true;
  },

  compileMatcher: function() {
    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
        c = Selector.criteria, le, p, m;

    if (Selector._cache[e]) {
      this.matcher = Selector._cache[e];
      return;
    }

    this.matcher = ["this.matcher = function(root) {",
                    "var r = root, h = Selector.handlers, c = false, n;"];

    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
            new Template(c[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.matcher.push("return h.unique(n);\n}");
    eval(this.matcher.join('\n'));
    Selector._cache[this.expression] = this.matcher;
  },

  compileXPathMatcher: function() {
    var e = this.expression, ps = Selector.patterns,
        x = Selector.xpath, le, m;

    if (Selector._cache[e]) {
      this.xpath = Selector._cache[e]; return;
    }

    this.matcher = ['.//*'];
    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        if (m = e.match(ps[i])) {
          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
            new Template(x[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.xpath = this.matcher.join('');
    Selector._cache[this.expression] = this.xpath;
  },

  findElements: function(root) {
    root = root || document;
    var e = this.expression, results;

    switch (this.mode) {
      case 'selectorsAPI':
        // querySelectorAll queries document-wide, then filters to descendants
        // of the context element. That's not what we want.
        // Add an explicit context to the selector if necessary.
        if (root !== document) {
          var oldId = root.id, id = $(root).identify();
          e = "#" + id + " " + e;
        }

        results = $A(root.querySelectorAll(e)).map(Element.extend);
        root.id = oldId;

        return results;
      case 'xpath':
        return document._getElementsByXPath(this.xpath, root);
      default:
       return this.matcher(root);
    }
  },

  match: function(element) {
    this.tokens = [];

    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
    var le, p, m;

    while (e && le !== e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          // use the Selector.assertions methods unless the selector
          // is too complex.
          if (as[i]) {
            this.tokens.push([i, Object.clone(m)]);
            e = e.replace(m[0], '');
          } else {
            // reluctantly do a document-wide search
            // and look for a match in the array
            return this.findElements(document).include(element);
          }
        }
      }
    }

    var match = true, name, matches;
    for (var i = 0, token; token = this.tokens[i]; i++) {
      name = token[0], matches = token[1];
      if (!Selector.assertions[name](element, matches)) {
        match = false; break;
      }
    }

    return match;
  },

  toString: function() {
    return this.expression;
  },

  inspect: function() {
    return "#<Selector:" + this.expression.inspect() + ">";
  }
});

Object.extend(Selector, {
  _cache: { },

  xpath: {
    descendant:   "//*",
    child:        "/*",
    adjacent:     "/following-sibling::*[1]",
    laterSibling: '/following-sibling::*',
    tagName:      function(m) {
      if (m[1] == '*') return '';
      return "[local-name()='" + m[1].toLowerCase() +
             "' or local-name()='" + m[1].toUpperCase() + "']";
    },
    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
    id:           "[@id='#{1}']",
    attrPresence: function(m) {
      m[1] = m[1].toLowerCase();
      return new Template("[@#{1}]").evaluate(m);
    },
    attr: function(m) {
      m[1] = m[1].toLowerCase();
      m[3] = m[5] || m[6];
      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
    },
    pseudo: function(m) {
      var h = Selector.xpath.pseudos[m[1]];
      if (!h) return '';
      if (Object.isFunction(h)) return h(m);
      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
    },
    operators: {
      '=':  "[@#{1}='#{3}']",
      '!=': "[@#{1}!='#{3}']",
      '^=': "[starts-with(@#{1}, '#{3}')]",
      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
      '*=': "[contains(@#{1}, '#{3}')]",
      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
    },
    pseudos: {
      'first-child': '[not(preceding-sibling::*)]',
      'last-child':  '[not(following-sibling::*)]',
      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
      'empty':       "[count(*) = 0 and (count(text()) = 0)]",
      'checked':     "[@checked]",
      'disabled':    "[(@disabled) and (@type!='hidden')]",
      'enabled':     "[not(@disabled) and (@type!='hidden')]",
      'not': function(m) {
        var e = m[6], p = Selector.patterns,
            x = Selector.xpath, le, v;

        var exclusion = [];
        while (e && le != e && (/\S/).test(e)) {
          le = e;
          for (var i in p) {
            if (m = e.match(p[i])) {
              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
              e = e.replace(m[0], '');
              break;
            }
          }
        }
        return "[not(" + exclusion.join(" and ") + ")]";
      },
      'nth-child':      function(m) {
        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
      },
      'nth-last-child': function(m) {
        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
      },
      'nth-of-type':    function(m) {
        return Selector.xpath.pseudos.nth("position() ", m);
      },
      'nth-last-of-type': function(m) {
        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
      },
      'first-of-type':  function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
      },
      'last-of-type':   function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
      },
      'only-of-type':   function(m) {
        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
      },
      nth: function(fragment, m) {
        var mm, formula = m[6], predicate;
        if (formula == 'even') formula = '2n+0';
        if (formula == 'odd')  formula = '2n+1';
        if (mm = formula.match(/^(\d+)$/)) // digit only
          return '[' + fragment + "= " + mm[1] + ']';
        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
          if (mm[1] == "-") mm[1] = -1;
          var a = mm[1] ? Number(mm[1]) : 1;
          var b = mm[2] ? Number(mm[2]) : 0;
          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
          "((#{fragment} - #{b}) div #{a} >= 0)]";
          return new Template(predicate).evaluate({
            fragment: fragment, a: a, b: b });
        }
      }
    }
  },

  criteria: {
    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
    attr: function(m) {
      m[3] = (m[5] || m[6]);
      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
    },
    pseudo: function(m) {
      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
    },
    descendant:   'c = "descendant";',
    child:        'c = "child";',
    adjacent:     'c = "adjacent";',
    laterSibling: 'c = "laterSibling";'
  },

  patterns: {
    // combinators must be listed first
    // (and descendant needs to be last combinator)
    laterSibling: /^\s*~\s*/,
    child:        /^\s*>\s*/,
    adjacent:     /^\s*\+\s*/,
    descendant:   /^\s/,

    // selectors follow
    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
    id:           /^#([\w\-\*]+)(\b|$)/,
    className:    /^\.([\w\-\*]+)(\b|$)/,
    pseudo:
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
    attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
  },

  // for Selector.match and Element#match
  assertions: {
    tagName: function(element, matches) {
      return matches[1].toUpperCase() == element.tagName.toUpperCase();
    },

    className: function(element, matches) {
      return Element.hasClassName(element, matches[1]);
    },

    id: function(element, matches) {
      return element.id === matches[1];
    },

    attrPresence: function(element, matches) {
      return Element.hasAttribute(element, matches[1]);
    },

    attr: function(element, matches) {
      var nodeValue = Element.readAttribute(element, matches[1]);
      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
    }
  },

  handlers: {
    // UTILITY FUNCTIONS
    // joins two collections
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        a.push(node);
      return a;
    },

    // marks an array of nodes for counting
    mark: function(nodes) {
      var _true = Prototype.emptyFunction;
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = _true;
      return nodes;
    },

    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = undefined;
      return nodes;
    },

    // mark each child node with its position (for nth calls)
    // "ofType" flag indicates whether we're indexing for nth-of-type
    // rather than nth-child
    index: function(parentNode, reverse, ofType) {
      parentNode._countedByPrototype = Prototype.emptyFunction;
      if (reverse) {
        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
          var node = nodes[i];
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
        }
      } else {
        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
      }
    },

    // filters out duplicates and extends all nodes
    unique: function(nodes) {
      if (nodes.length == 0) return nodes;
      var results = [], n;
      for (var i = 0, l = nodes.length; i < l; i++)
        if (!(n = nodes[i])._countedByPrototype) {
          n._countedByPrototype = Prototype.emptyFunction;
          results.push(Element.extend(n));
        }
      return Selector.handlers.unmark(results);
    },

    // COMBINATOR FUNCTIONS
    descendant: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, node.getElementsByTagName('*'));
      return results;
    },

    child: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        for (var j = 0, child; child = node.childNodes[j]; j++)
          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
      }
      return results;
    },

    adjacent: function(nodes) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        var next = this.nextElementSibling(node);
        if (next) results.push(next);
      }
      return results;
    },

    laterSibling: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, Element.nextSiblings(node));
      return results;
    },

    nextElementSibling: function(node) {
      while (node = node.nextSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    previousElementSibling: function(node) {
      while (node = node.previousSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    // TOKEN FUNCTIONS
    tagName: function(nodes, root, tagName, combinator) {
      var uTagName = tagName.toUpperCase();
      var results = [], h = Selector.handlers;
      if (nodes) {
        if (combinator) {
          // fastlane for ordinary descendant combinators
          if (combinator == "descendant") {
            for (var i = 0, node; node = nodes[i]; i++)
              h.concat(results, node.getElementsByTagName(tagName));
            return results;
          } else nodes = this[combinator](nodes);
          if (tagName == "*") return nodes;
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName.toUpperCase() === uTagName) results.push(node);
        return results;
      } else return root.getElementsByTagName(tagName);
    },

    id: function(nodes, root, id, combinator) {
      var targetNode = $(id), h = Selector.handlers;
      if (!targetNode) return [];
      if (!nodes && root == document) return [targetNode];
      if (nodes) {
        if (combinator) {
          if (combinator == 'child') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (targetNode.parentNode == node) return [targetNode];
          } else if (combinator == 'descendant') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Element.descendantOf(targetNode, node)) return [targetNode];
          } else if (combinator == 'adjacent') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Selector.handlers.previousElementSibling(targetNode) == node)
                return [targetNode];
          } else nodes = h[combinator](nodes);
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node == targetNode) return [targetNode];
        return [];
      }
      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
    },

    className: function(nodes, root, className, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      return Selector.handlers.byClassName(nodes, root, className);
    },

    byClassName: function(nodes, root, className) {
      if (!nodes) nodes = Selector.handlers.descendant([root]);
      var needle = ' ' + className + ' ';
      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
        nodeClassName = node.className;
        if (nodeClassName.length == 0) continue;
        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
          results.push(node);
      }
      return results;
    },

    attrPresence: function(nodes, root, attr, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var results = [];
      for (var i = 0, node; node = nodes[i]; i++)
        if (Element.hasAttribute(node, attr)) results.push(node);
      return results;
    },

    attr: function(nodes, root, attr, value, operator, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var handler = Selector.operators[operator], results = [];
      for (var i = 0, node; node = nodes[i]; i++) {
        var nodeValue = Element.readAttribute(node, attr);
        if (nodeValue === null) continue;
        if (handler(nodeValue, value)) results.push(node);
      }
      return results;
    },

    pseudo: function(nodes, name, value, root, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      if (!nodes) nodes = root.getElementsByTagName("*");
      return Selector.pseudos[name](nodes, value, root);
    }
  },

  pseudos: {
    'first-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.previousElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'last-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.nextElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'only-child': function(nodes, value, root) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
          results.push(node);
      return results;
    },
    'nth-child':        function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root);
    },
    'nth-last-child':   function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true);
    },
    'nth-of-type':      function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, false, true);
    },
    'nth-last-of-type': function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true, true);
    },
    'first-of-type':    function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, false, true);
    },
    'last-of-type':     function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, true, true);
    },
    'only-of-type':     function(nodes, formula, root) {
      var p = Selector.pseudos;
      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
    },

    // handles the an+b logic
    getIndices: function(a, b, total) {
      if (a == 0) return b > 0 ? [b] : [];
      return $R(1, total).inject([], function(memo, i) {
        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
        return memo;
      });
    },

    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
    nth: function(nodes, formula, root, reverse, ofType) {
      if (nodes.length == 0) return [];
      if (formula == 'even') formula = '2n+0';
      if (formula == 'odd')  formula = '2n+1';
      var h = Selector.handlers, results = [], indexed = [], m;
      h.mark(nodes);
      for (var i = 0, node; node = nodes[i]; i++) {
        if (!node.parentNode._countedByPrototype) {
          h.index(node.parentNode, reverse, ofType);
          indexed.push(node.parentNode);
        }
      }
      if (formula.match(/^\d+$/)) { // just a number
        formula = Number(formula);
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.nodeIndex == formula) results.push(node);
      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
        if (m[1] == "-") m[1] = -1;
        var a = m[1] ? Number(m[1]) : 1;
        var b = m[2] ? Number(m[2]) : 0;
        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
          for (var j = 0; j < l; j++)
            if (node.nodeIndex == indices[j]) results.push(node);
        }
      }
      h.unmark(nodes);
      h.unmark(indexed);
      return results;
    },

    'empty': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        // IE treats comments as element nodes
        if (node.tagName == '!' || node.firstChild) continue;
        results.push(node);
      }
      return results;
    },

    'not': function(nodes, selector, root) {
      var h = Selector.handlers, selectorType, m;
      var exclusions = new Selector(selector).findElements(root);
      h.mark(exclusions);
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node._countedByPrototype) results.push(node);
      h.unmark(exclusions);
      return results;
    },

    'enabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node.disabled && (!node.type || node.type !== 'hidden'))
          results.push(node);
      return results;
    },

    'disabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.disabled) results.push(node);
      return results;
    },

    'checked': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.checked) results.push(node);
      return results;
    }
  },

  operators: {
    '=':  function(nv, v) { return nv == v; },
    '!=': function(nv, v) { return nv != v; },
    '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
    '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
    '$=': function(nv, v) { return nv.endsWith(v); },
    '*=': function(nv, v) { return nv.include(v); },
    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
     '-').include('-' + (v || "").toUpperCase() + '-'); }
  },

  split: function(expression) {
    var expressions = [];
    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
      expressions.push(m[1].strip());
    });
    return expressions;
  },

  matchElements: function(elements, expression) {
    var matches = $$(expression), h = Selector.handlers;
    h.mark(matches);
    for (var i = 0, results = [], element; element = elements[i]; i++)
      if (element._countedByPrototype) results.push(element);
    h.unmark(matches);
    return results;
  },

  findElement: function(elements, expression, index) {
    if (Object.isNumber(expression)) {
      index = expression; expression = false;
    }
    return Selector.matchElements(elements, expression || '*')[index || 0];
  },

  findChildElements: function(element, expressions) {
    expressions = Selector.split(expressions.join(','));
    var results = [], h = Selector.handlers;
    for (var i = 0, l = expressions.length, selector; i < l; i++) {
      selector = new Selector(expressions[i].strip());
      h.concat(results, selector.findElements(element));
    }
    return (l > 1) ? h.unique(results) : results;
  }
});

if (Prototype.Browser.IE) {
  Object.extend(Selector.handlers, {
    // IE returns comment nodes on getElementsByTagName("*").
    // Filter them out.
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        if (node.tagName !== "!") a.push(node);
      return a;
    },

    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node.removeAttribute('_countedByPrototype');
      return nodes;
    }
  });
}

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}
var Form = {
  reset: function(form) {
    $(form).reset();
    return form;
  },

  serializeElements: function(elements, options) {
    if (typeof options != 'object') options = { hash: !!options };
    else if (Object.isUndefined(options.hash)) options.hash = true;
    var key, value, submitted = false, submit = options.submit;

    var data = elements.inject({ }, function(result, element) {
      if (!element.disabled && element.name) {
        key = element.name; value = $(element).getValue();
        if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted = true)))) {
          if (key in result) {
            // a key is already present; construct an array of values
            if (!Object.isArray(result[key])) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return options.hash ? data : Object.toQueryString(data);
  }
};

Form.Methods = {
  serialize: function(form, options) {
    return Form.serializeElements(Form.getElements(form), options);
  },

  getElements: function(form) {
    return $A($(form).getElementsByTagName('*')).inject([],
      function(elements, child) {
        if (Form.Element.Serializers[child.tagName.toLowerCase()])
          elements.push(Element.extend(child));
        return elements;
      }
    );
  },

  getInputs: function(form, typeName, name) {
    form = $(form);
    var inputs = form.getElementsByTagName('input');

    if (!typeName && !name) return $A(inputs).map(Element.extend);

    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
      var input = inputs[i];
      if ((typeName && input.type != typeName) || (name && input.name != name))
        continue;
      matchingInputs.push(Element.extend(input));
    }

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('disable');
    return form;
  },

  enable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('enable');
    return form;
  },

  findFirstElement: function(form) {
    var elements = $(form).getElements().findAll(function(element) {
      return 'hidden' != element.type && !element.disabled;
    });
    var firstByIndex = elements.findAll(function(element) {
      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
    }).sortBy(function(element) { return element.tabIndex }).first();

    return firstByIndex ? firstByIndex : elements.find(function(element) {
      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
    });
  },

  focusFirstElement: function(form) {
    form = $(form);
    form.findFirstElement().activate();
    return form;
  },

  request: function(form, options) {
    form = $(form), options = Object.clone(options || { });

    var params = options.parameters, action = form.readAttribute('action') || '';
    if (action.blank()) action = window.location.href;
    options.parameters = form.serialize(true);

    if (params) {
      if (Object.isString(params)) params = params.toQueryParams();
      Object.extend(options.parameters, params);
    }

    if (form.hasAttribute('method') && !options.method)
      options.method = form.method;

    return new Ajax.Request(action, options);
  }
};

/*--------------------------------------------------------------------------*/

Form.Element = {
  focus: function(element) {
    $(element).focus();
    return element;
  },

  select: function(element) {
    $(element).select();
    return element;
  }
};

Form.Element.Methods = {
  serialize: function(element) {
    element = $(element);
    if (!element.disabled && element.name) {
      var value = element.getValue();
      if (value != undefined) {
        var pair = { };
        pair[element.name] = value;
        return Object.toQueryString(pair);
      }
    }
    return '';
  },

  getValue: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    return Form.Element.Serializers[method](element);
  },

  setValue: function(element, value) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    Form.Element.Serializers[method](element, value);
    return element;
  },

  clear: function(element) {
    $(element).value = '';
    return element;
  },

  present: function(element) {
    return $(element).value != '';
  },

  activate: function(element) {
    element = $(element);
    try {
      element.focus();
      if (element.select && (element.tagName.toLowerCase() != 'input' ||
          !['button', 'reset', 'submit'].include(element.type)))
        element.select();
    } catch (e) { }
    return element;
  },

  disable: function(element) {
    element = $(element);
    element.disabled = true;
    return element;
  },

  enable: function(element) {
    element = $(element);
    element.disabled = false;
    return element;
  }
};

/*--------------------------------------------------------------------------*/

var Field = Form.Element;
var $F = Form.Element.Methods.getValue;

/*--------------------------------------------------------------------------*/

Form.Element.Serializers = {
  input: function(element, value) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        return Form.Element.Serializers.inputSelector(element, value);
      default:
        return Form.Element.Serializers.textarea(element, value);
    }
  },

  inputSelector: function(element, value) {
    if (Object.isUndefined(value)) return element.checked ? element.value : null;
    else element.checked = !!value;
  },

  textarea: function(element, value) {
    if (Object.isUndefined(value)) return element.value;
    else element.value = value;
  },

  select: function(element, value) {
    if (Object.isUndefined(value))
      return this[element.type == 'select-one' ?
        'selectOne' : 'selectMany'](element);
    else {
      var opt, currentValue, single = !Object.isArray(value);
      for (var i = 0, length = element.length; i < length; i++) {
        opt = element.options[i];
        currentValue = this.optionValue(opt);
        if (single) {
          if (currentValue == value) {
            opt.selected = true;
            return;
          }
        }
        else opt.selected = value.include(currentValue);
      }
    }
  },

  selectOne: function(element) {
    var index = element.selectedIndex;
    return index >= 0 ? this.optionValue(element.options[index]) : null;
  },

  selectMany: function(element) {
    var values, length = element.length;
    if (!length) return null;

    for (var i = 0, values = []; i < length; i++) {
      var opt = element.options[i];
      if (opt.selected) values.push(this.optionValue(opt));
    }
    return values;
  },

  optionValue: function(opt) {
    // extend element because hasAttribute may not be native
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
};

/*--------------------------------------------------------------------------*/

Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
  initialize: function($super, element, frequency, callback) {
    $super(callback, frequency);
    this.element   = $(element);
    this.lastValue = this.getValue();
  },

  execute: function() {
    var value = this.getValue();
    if (Object.isString(this.lastValue) && Object.isString(value) ?
        this.lastValue != value : String(this.lastValue) != String(value)) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
});

Form.Element.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});

/*--------------------------------------------------------------------------*/

Abstract.EventObserver = Class.create({
  initialize: function(element, callback) {
    this.element  = $(element);
    this.callback = callback;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback, this);
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          Event.observe(element, 'click', this.onElementEvent.bind(this));
          break;
        default:
          Event.observe(element, 'change', this.onElementEvent.bind(this));
          break;
      }
    }
  }
});

Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
if (!window.Event) var Event = { };

Object.extend(Event, {
  KEY_BACKSPACE: 8,
  KEY_TAB:       9,
  KEY_RETURN:   13,
  KEY_ESC:      27,
  KEY_LEFT:     37,
  KEY_UP:       38,
  KEY_RIGHT:    39,
  KEY_DOWN:     40,
  KEY_DELETE:   46,
  KEY_HOME:     36,
  KEY_END:      35,
  KEY_PAGEUP:   33,
  KEY_PAGEDOWN: 34,
  KEY_INSERT:   45,

  cache: { },

  relatedTarget: function(event) {
    var element;
    switch(event.type) {
      case 'mouseover': element = event.fromElement; break;
      case 'mouseout':  element = event.toElement;   break;
      default: return null;
    }
    return Element.extend(element);
  }
});

Event.Methods = (function() {
  var isButton;

  if (Prototype.Browser.IE) {
    var buttonMap = { 0: 1, 1: 4, 2: 2 };
    isButton = function(event, code) {
      return event.button == buttonMap[code];
    };

  } else if (Prototype.Browser.WebKit) {
    isButton = function(event, code) {
      switch (code) {
        case 0: return event.which == 1 && !event.metaKey;
        case 1: return event.which == 1 && event.metaKey;
        default: return false;
      }
    };

  } else {
    isButton = function(event, code) {
      return event.which ? (event.which === code + 1) : (event.button === code);
    };
  }

  return {
    isLeftClick:   function(event) { return isButton(event, 0) },
    isMiddleClick: function(event) { return isButton(event, 1) },
    isRightClick:  function(event) { return isButton(event, 2) },

    element: function(event) {
      event = Event.extend(event);

      var node          = event.target,
          type          = event.type,
          currentTarget = event.currentTarget;

      if (currentTarget && currentTarget.tagName) {
        // Firefox screws up the "click" event when moving between radio buttons
        // via arrow keys. It also screws up the "load" and "error" events on images,
        // reporting the document as the target instead of the original image.
        if (type === 'load' || type === 'error' ||
          (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
            && currentTarget.type === 'radio'))
              node = currentTarget;
      }
      if (node.nodeType == Node.TEXT_NODE) node = node.parentNode;
      return Element.extend(node);
    },

    findElement: function(event, expression) {
      var element = Event.element(event);
      if (!expression) return element;
      var elements = [element].concat(element.ancestors());
      return Selector.findElement(elements, expression, 0);
    },

    pointer: function(event) {
      var docElement = document.documentElement,
      body = document.body || { scrollLeft: 0, scrollTop: 0 };
      return {
        x: event.pageX || (event.clientX +
          (docElement.scrollLeft || body.scrollLeft) -
          (docElement.clientLeft || 0)),
        y: event.pageY || (event.clientY +
          (docElement.scrollTop || body.scrollTop) -
          (docElement.clientTop || 0))
      };
    },

    pointerX: function(event) { return Event.pointer(event).x },
    pointerY: function(event) { return Event.pointer(event).y },

    stop: function(event) {
      Event.extend(event);
      event.preventDefault();
      event.stopPropagation();
      event.stopped = true;
    }
  };
})();

Event.extend = (function() {
  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
    m[name] = Event.Methods[name].methodize();
    return m;
  });

  if (Prototype.Browser.IE) {
    Object.extend(methods, {
      stopPropagation: function() { this.cancelBubble = true },
      preventDefault:  function() { this.returnValue = false },
      inspect: function() { return "[object Event]" }
    });

    return function(event) {
      if (!event) return false;
      if (event._extendedByPrototype) return event;

      event._extendedByPrototype = Prototype.emptyFunction;
      var pointer = Event.pointer(event);
      Object.extend(event, {
        target: event.srcElement,
        relatedTarget: Event.relatedTarget(event),
        pageX:  pointer.x,
        pageY:  pointer.y
      });
      return Object.extend(event, methods);
    };

  } else {
    Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__'];
    Object.extend(Event.prototype, methods);
    return Prototype.K;
  }
})();

Object.extend(Event, (function() {
  var cache = Event.cache;

  function getEventID(element) {
    if (element._prototypeEventID) return element._prototypeEventID[0];
    arguments.callee.id = arguments.callee.id || 1;
    return element._prototypeEventID = [++arguments.callee.id];
  }

  function getDOMEventName(eventName) {
    if (eventName && eventName.include(':')) return "dataavailable";
    return eventName;
  }

  function getCacheForID(id) {
    return cache[id] = cache[id] || { };
  }

  function getWrappersForEventName(id, eventName) {
    var c = getCacheForID(id);
    return c[eventName] = c[eventName] || [];
  }

  function createWrapper(element, eventName, handler) {
    var id = getEventID(element);
    var c = getWrappersForEventName(id, eventName);
    if (c.pluck("handler").include(handler)) return false;

    var wrapper = function(event) {
      if (!Event || !Event.extend ||
        (event.eventName && event.eventName != eventName))
          return false;

      Event.extend(event);
      handler.call(element, event);
    };

    wrapper.handler = handler;
    c.push(wrapper);
    return wrapper;
  }

  function findWrapper(id, eventName, handler) {
    var c = getWrappersForEventName(id, eventName);
    return c.find(function(wrapper) { return wrapper.handler == handler });
  }

  function destroyWrapper(id, eventName, handler) {
    var c = getCacheForID(id);
    if (!c[eventName]) return false;
    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
  }

  function destroyCache() {
    for (var id in cache)
      for (var eventName in cache[id])
        cache[id][eventName] = null;
  }


  // Internet Explorer needs to remove event handlers on page unload
  // in order to avoid memory leaks.
  if (window.attachEvent) {
    window.attachEvent("onunload", destroyCache);
  }

  // Safari has a dummy event handler on page unload so that it won't
  // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
  // object when page is returned to via the back button using its bfcache.
  if (Prototype.Browser.WebKit) {
    window.addEventListener('unload', Prototype.emptyFunction, false);
  }

  return {
    observe: function(element, eventName, handler) {
      element = $(element);
      var name = getDOMEventName(eventName);

      var wrapper = createWrapper(element, eventName, handler);
      if (!wrapper) return element;

      if (element.addEventListener) {
        element.addEventListener(name, wrapper, false);
      } else {
        element.attachEvent("on" + name, wrapper);
      }

      return element;
    },

    stopObserving: function(element, eventName, handler) {
      element = $(element);
      var id = getEventID(element), name = getDOMEventName(eventName);

      if (!handler && eventName) {
        getWrappersForEventName(id, eventName).each(function(wrapper) {
          element.stopObserving(eventName, wrapper.handler);
        });
        return element;

      } else if (!eventName) {
        Object.keys(getCacheForID(id)).each(function(eventName) {
          element.stopObserving(eventName);
        });
        return element;
      }

      var wrapper = findWrapper(id, eventName, handler);
      if (!wrapper) return element;

      if (element.removeEventListener) {
        element.removeEventListener(name, wrapper, false);
      } else {
        element.detachEvent("on" + name, wrapper);
      }

      destroyWrapper(id, eventName, handler);

      return element;
    },

    fire: function(element, eventName, memo) {
      element = $(element);
      if (element == document && document.createEvent && !element.dispatchEvent)
        element = document.documentElement;

      var event;
      if (document.createEvent) {
        event = document.createEvent("HTMLEvents");
        event.initEvent("dataavailable", true, true);
      } else {
        event = document.createEventObject();
        event.eventType = "ondataavailable";
      }

      event.eventName = eventName;
      event.memo = memo || { };

      if (document.createEvent) {
        element.dispatchEvent(event);
      } else {
        element.fireEvent(event.eventType, event);
      }

      return Event.extend(event);
    }
  };
})());

Object.extend(Event, Event.Methods);

Element.addMethods({
  fire:          Event.fire,
  observe:       Event.observe,
  stopObserving: Event.stopObserving
});

Object.extend(document, {
  fire:          Element.Methods.fire.methodize(),
  observe:       Element.Methods.observe.methodize(),
  stopObserving: Element.Methods.stopObserving.methodize(),
  loaded:        false
});

(function() {
  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
     Matthias Miller, Dean Edwards and John Resig. */

  var timer;

  function fireContentLoadedEvent() {
    if (document.loaded) return;
    if (timer) window.clearInterval(timer);
    document.fire("dom:loaded");
    document.loaded = true;
  }

  if (document.addEventListener) {
    if (Prototype.Browser.WebKit) {
      timer = window.setInterval(function() {
        if (/loaded|complete/.test(document.readyState))
          fireContentLoadedEvent();
      }, 0);

      Event.observe(window, "load", fireContentLoadedEvent);

    } else {
      document.addEventListener("DOMContentLoaded",
        fireContentLoadedEvent, false);
    }

  } else {
    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
    $("__onDOMContentLoaded").onreadystatechange = function() {
      if (this.readyState == "complete") {
        this.onreadystatechange = null;
        fireContentLoadedEvent();
      }
    };
  }
})();
/*------------------------------- DEPRECATED -------------------------------*/

Hash.toQueryString = Object.toQueryString;

var Toggle = { display: Element.toggle };

Element.Methods.childOf = Element.Methods.descendantOf;

var Insertion = {
  Before: function(element, content) {
    return Element.insert(element, {before:content});
  },

  Top: function(element, content) {
    return Element.insert(element, {top:content});
  },

  Bottom: function(element, content) {
    return Element.insert(element, {bottom:content});
  },

  After: function(element, content) {
    return Element.insert(element, {after:content});
  }
};

var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

// This should be moved to script.aculo.us; notice the deprecated methods
// further below, that map to the newer Element methods.
var Position = {
  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,

  // must be called before calling withinIncludingScrolloffset, every time the
  // page is scrolled
  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  // caches x/y coordinate pair to use with overlap
  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = Element.cumulativeOffset(element);

    return (y >= this.offset[1] &&
            y <  this.offset[1] + element.offsetHeight &&
            x >= this.offset[0] &&
            x <  this.offset[0] + element.offsetWidth);
  },

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = Element.cumulativeScrollOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = Element.cumulativeOffset(element);

    return (this.ycomp >= this.offset[1] &&
            this.ycomp <  this.offset[1] + element.offsetHeight &&
            this.xcomp >= this.offset[0] &&
            this.xcomp <  this.offset[0] + element.offsetWidth);
  },

  // within must be called directly before
  overlap: function(mode, element) {
    if (!mode) return 0;
    if (mode == 'vertical')
      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
        element.offsetHeight;
    if (mode == 'horizontal')
      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
        element.offsetWidth;
  },

  // Deprecation layer -- use newer Element methods now (1.5.2).

  cumulativeOffset: Element.Methods.cumulativeOffset,

  positionedOffset: Element.Methods.positionedOffset,

  absolutize: function(element) {
    Position.prepare();
    return Element.absolutize(element);
  },

  relativize: function(element) {
    Position.prepare();
    return Element.relativize(element);
  },

  realOffset: Element.Methods.cumulativeScrollOffset,

  offsetParent: Element.Methods.getOffsetParent,

  page: Element.Methods.viewportOffset,

  clone: function(source, target, options) {
    options = options || { };
    return Element.clonePosition(target, source, options);
  }
};

/*--------------------------------------------------------------------------*/

if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
  function iter(name) {
    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
  }

  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
  function(element, className) {
    className = className.toString().strip();
    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
  } : function(element, className) {
    className = className.toString().strip();
    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
    if (!classNames && !className) return elements;

    var nodes = $(element).getElementsByTagName('*');
    className = ' ' + className + ' ';

    for (var i = 0, child, cn; child = nodes[i]; i++) {
      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
          (classNames && classNames.all(function(name) {
            return !name.toString().blank() && cn.include(' ' + name + ' ');
          }))))
        elements.push(Element.extend(child));
    }
    return elements;
  };

  return function(className, parentElement) {
    return $(parentElement || document.body).getElementsByClassName(className);
  };
}(Element.Methods);

/*--------------------------------------------------------------------------*/

Element.ClassNames = Class.create();
Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = $(element);
  },

  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },

  set: function(className) {
    this.element.className = className;
  },

  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set($A(this).concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set($A(this).without(classNameToRemove).join(' '));
  },

  toString: function() {
    return $A(this).join(' ');
  }
};

Object.extend(Element.ClassNames.prototype, Enumerable);

/*--------------------------------------------------------------------------*/

Element.addMethods();

/*** @inca-include ../admin/assets/scripts/base/prototype.extensions.js ***/

Object.extend(Element.Methods, {
	
	/**
	 * getPosition() gets the Position of the Element
	*/
	getPosition: function(element) {
	
		// Variables
		var x, y = 0;
	
		// Check Element
		element = $(element);
		
		// Do some checks
		var display = $(element).getStyle("display");
		if (display != "none" && display != null) // Safari bug
			return {x: element.offsetLeft, y: element.offsetTop};
			
		 // All *Width and *Height properties give 0 on elements with display none,
	    // so enable the element temporarily
	    var els = element.style;
	    var originalVisibility = els.visibility;
	    var originalPosition = els.position;
	    var originalDisplay = els.display;
	    els.visibility = "hidden";
	    els.position = "absolute";
	    els.display = "block";
		
		// Get X and Y pos
		if (element.offsetParent) {
			while (element.offsetParent) {
				y += parseInt(element.offsetTop);
				x += parseInt(element.offsetLeft);
				element = element.offsetParent;
			}
		} else if (element.y) {
			y += parseInt(element.y);
			x += parseInt(element.x);
		}

		// Restore original settings
	    els.display = originalDisplay;
	    els.position = originalPosition;
	    els.visibility = originalVisibility;
	
		// Done
		return {x: x, y: y}
	},
	
	/**
	 * getX gets the X-pos of the element
	*/
	getX: function(element) {
		return $(element).getPosition().x;
	},
	
	/**
	 * getY gets the Y-pos of the element
	*/
	getY: function(element) {
		return $(element).getPosition().y;
	},
	
	setPosition: function(element, x, y) {
	
		// Check Element
		element = $(element);
		
		return $(element).setY(y).setX(x);
	},
	
	setY: function(element, y) {
	
		// Check Element
		element = $(element);
		
		if (element) {
			try {
				element.style.top = parseInt(y) + "px";
			} catch (error) {
				alert("setY on: " + element.innerHTML + " to '" + y + "' fails");
			}
		}
		
		return element;
	},
	
	setX: function(element, x) {
	
		// Check Element
		element = $(element);
		
		if (element) {
			try {
				element.style.left = parseInt(x) + "px";
			} catch (error) {
				alert("setX on: " + element.innerHTML + " to '" + x + "' fails");
			}
		}
		
		return element;
	}
});

// Call this to reflect the new functions
Element.addMethods();

/*** @inca-include ../admin/assets/scripts/base/script.aculo.us/scriptaculous.js ***/

// script.aculo.us scriptaculous.js v1.8.1, Thu Jan 03 22:07:12 -0500 2008

// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// 
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// For details, see the script.aculo.us web site: http://script.aculo.us/

var Scriptaculous = {
  Version: '1.8.1',
  require: function(libraryName) {
    // inserting via DOM fails in Safari 2.0, so brute force approach
    document.write('<script type="text/javascript" src="'+libraryName+'"><\/script>');
  },
  REQUIRED_PROTOTYPE: '1.6.0',
  load: function() {
    function convertVersionString(versionString){
      var r = versionString.split('.');
      return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]);
    }
 
    if((typeof Prototype=='undefined') || 
       (typeof Element == 'undefined') || 
       (typeof Element.Methods=='undefined') ||
       (convertVersionString(Prototype.Version) < 
        convertVersionString(Scriptaculous.REQUIRED_PROTOTYPE)))
       throw("script.aculo.us requires the Prototype JavaScript framework >= " +
        Scriptaculous.REQUIRED_PROTOTYPE);
    
    $A(document.getElementsByTagName("script")).findAll( function(s) {
      return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/))
    }).each( function(s) {
      var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,'');
      var includes = s.src.match(/\?.*load=([a-z,]*)/);
      (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider,sound').split(',').each(
       function(include) { Scriptaculous.require(path+include+'.js') });
    });
  }
}

Scriptaculous.load();

/*** @inca-include ../admin/assets/scripts/base/md5.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/base/md5.js
 * FUNCTION:		Contains the MD5 object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-23
 ***************************************************************************************/

/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

/**
 * The MD5 object contains methods to encode strings to MD5 hashes.
 */
var MD5 = {
	
	// Version
	__VERSION:							"1.0",

	// Properties
	hex_case:							0,
	b64_pad:							"",
	chrs_z:								8,
	
	/************************************************************************************
	 * GENERAL PUBLIC FUNCTIONS
	************************************************************************************/
	
	hexMD5:								function(str) {
		str = String(str);
		return this._binl2Hex(this._coreMD5(this._str2Binl(str), str.length * this.chrs_z));
	},
	
	b64MD5:								function(str) {
		str = String(str);
		return this._binl2B64(this._coreMD5(this._str2Binl(str), str.length * this.chrs_z));
	},
	
	strMD5:								function(str) {
		str = String(str);
		return this._binl2Str(this._coreMD5(this._str2Binl(str), str.length * this.chrs_z));
	},
	
	/************************************************************************************
	 * GENERAL PUBLIC CONCAT FUNCTIONS
	************************************************************************************/
	
	hexHMACMD5:							function(key, data) {
		return this._binl2Hex(this._coreHMACMD5(key, data));
	},
	
	b64HMACMD5:							function(key, data) {
		return this._binl2B64(this._coreHMACMD5(key, data));
	},
	
	strHMACMD5:							function(key, data) {
		return this._binl2Str(this._coreHMACMD5(key, data));
	},
	
	/************************************************************************************
	 * CORE FUNCTIONALITY
	************************************************************************************/
	
	_md5VMTest:							function() {
		return this.hexMD5("abc") == "900150983cd24fb0d6963f7d28e17f72";
	},
	
	_coreMD5:							function(x, len) {
		
		x[len >> 5] |= 0x80 << ((len) % 32);
		x[(((len + 64) >>> 9) << 4) + 14] = len;

		var a =  1732584193;
		var b = -271733879;
		var c = -1732584194;
		var d =  271733878;

		for (var i = 0; i < x.length; i += 16) {
			var olda = a;
			var oldb = b;
			var oldc = c;
			var oldd = d;

			a = this._md5FF(a, b, c, d, x[i+ 0], 7 , -680876936);
			d = this._md5FF(d, a, b, c, x[i+ 1], 12, -389564586);
			c = this._md5FF(c, d, a, b, x[i+ 2], 17,  606105819);
			b = this._md5FF(b, c, d, a, x[i+ 3], 22, -1044525330);
			a = this._md5FF(a, b, c, d, x[i+ 4], 7 , -176418897);
			d = this._md5FF(d, a, b, c, x[i+ 5], 12,  1200080426);
			c = this._md5FF(c, d, a, b, x[i+ 6], 17, -1473231341);
			b = this._md5FF(b, c, d, a, x[i+ 7], 22, -45705983);
			a = this._md5FF(a, b, c, d, x[i+ 8], 7 ,  1770035416);
			d = this._md5FF(d, a, b, c, x[i+ 9], 12, -1958414417);
			c = this._md5FF(c, d, a, b, x[i+10], 17, -42063);
			b = this._md5FF(b, c, d, a, x[i+11], 22, -1990404162);
			a = this._md5FF(a, b, c, d, x[i+12], 7 ,  1804603682);
			d = this._md5FF(d, a, b, c, x[i+13], 12, -40341101);
			c = this._md5FF(c, d, a, b, x[i+14], 17, -1502002290);
			b = this._md5FF(b, c, d, a, x[i+15], 22,  1236535329);
			
			a = this._md5GG(a, b, c, d, x[i+ 1], 5 , -165796510);
			d = this._md5GG(d, a, b, c, x[i+ 6], 9 , -1069501632);
			c = this._md5GG(c, d, a, b, x[i+11], 14,  643717713);
			b = this._md5GG(b, c, d, a, x[i+ 0], 20, -373897302);
			a = this._md5GG(a, b, c, d, x[i+ 5], 5 , -701558691);
			d = this._md5GG(d, a, b, c, x[i+10], 9 ,  38016083);
			c = this._md5GG(c, d, a, b, x[i+15], 14, -660478335);
			b = this._md5GG(b, c, d, a, x[i+ 4], 20, -405537848);
			a = this._md5GG(a, b, c, d, x[i+ 9], 5 ,  568446438);
			d = this._md5GG(d, a, b, c, x[i+14], 9 , -1019803690);
			c = this._md5GG(c, d, a, b, x[i+ 3], 14, -187363961);
			b = this._md5GG(b, c, d, a, x[i+ 8], 20,  1163531501);
			a = this._md5GG(a, b, c, d, x[i+13], 5 , -1444681467);
			d = this._md5GG(d, a, b, c, x[i+ 2], 9 , -51403784);
			c = this._md5GG(c, d, a, b, x[i+ 7], 14,  1735328473);
			b = this._md5GG(b, c, d, a, x[i+12], 20, -1926607734);
			
			a = this._md5HH(a, b, c, d, x[i+ 5], 4 , -378558);
			d = this._md5HH(d, a, b, c, x[i+ 8], 11, -2022574463);
			c = this._md5HH(c, d, a, b, x[i+11], 16,  1839030562);
			b = this._md5HH(b, c, d, a, x[i+14], 23, -35309556);
			a = this._md5HH(a, b, c, d, x[i+ 1], 4 , -1530992060);
			d = this._md5HH(d, a, b, c, x[i+ 4], 11,  1272893353);
			c = this._md5HH(c, d, a, b, x[i+ 7], 16, -155497632);
			b = this._md5HH(b, c, d, a, x[i+10], 23, -1094730640);
			a = this._md5HH(a, b, c, d, x[i+13], 4 ,  681279174);
			d = this._md5HH(d, a, b, c, x[i+ 0], 11, -358537222);
			c = this._md5HH(c, d, a, b, x[i+ 3], 16, -722521979);
			b = this._md5HH(b, c, d, a, x[i+ 6], 23,  76029189);
			a = this._md5HH(a, b, c, d, x[i+ 9], 4 , -640364487);
			d = this._md5HH(d, a, b, c, x[i+12], 11, -421815835);
			c = this._md5HH(c, d, a, b, x[i+15], 16,  530742520);
			b = this._md5HH(b, c, d, a, x[i+ 2], 23, -995338651);
			
			a = this._md5II(a, b, c, d, x[i+ 0], 6 , -198630844);
			d = this._md5II(d, a, b, c, x[i+ 7], 10,  1126891415);
			c = this._md5II(c, d, a, b, x[i+14], 15, -1416354905);
			b = this._md5II(b, c, d, a, x[i+ 5], 21, -57434055);
			a = this._md5II(a, b, c, d, x[i+12], 6 ,  1700485571);
			d = this._md5II(d, a, b, c, x[i+ 3], 10, -1894986606);
			c = this._md5II(c, d, a, b, x[i+10], 15, -1051523);
			b = this._md5II(b, c, d, a, x[i+ 1], 21, -2054922799);
			a = this._md5II(a, b, c, d, x[i+ 8], 6 ,  1873313359);
			d = this._md5II(d, a, b, c, x[i+15], 10, -30611744);
			c = this._md5II(c, d, a, b, x[i+ 6], 15, -1560198380);
			b = this._md5II(b, c, d, a, x[i+13], 21,  1309151649);
			a = this._md5II(a, b, c, d, x[i+ 4], 6 , -145523070);
			d = this._md5II(d, a, b, c, x[i+11], 10, -1120210379);
			c = this._md5II(c, d, a, b, x[i+ 2], 15,  718787259);
			b = this._md5II(b, c, d, a, x[i+ 9], 21, -343485551);

			a = this._safeAdd(a, olda);
			b = this._safeAdd(b, oldb);
			c = this._safeAdd(c, oldc);
			d = this._safeAdd(d, oldd);
		}
		return new Array(a, b, c, d);	
	},
	
	_md5CMN:							function(q, a, b, x, s, t) {
		return this._safeAdd(this._bitRol(this._safeAdd(this._safeAdd(a, q), this._safeAdd(x, t)), s), b);
	},
	
	_md5FF:								function(a, b, c, d, x, s, t) {
		return this._md5CMN((b & c) | ((~b) & d), a, b, x, s, t);
	},
	
	_md5GG:								function(a, b, c, d, x, s, t) {
		return this._md5CMN((b & d) | (c & (~d)), a, b, x, s, t);
	},
	
	_md5HH:								function(a, b, c, d, x, s, t) {
		return this._md5CMN(b ^ c ^ d, a, b, x, s, t);
	},
	
	_md5II:								function(a, b, c, d, x, s, t) {
		return this._md5CMN(c ^ (b | (~d)), a, b, x, s, t);
	},
	
	/************************************************************************************
	 * UTILITY METHODS
	************************************************************************************/
	
	_coreHMACMD5:						function(key, data) {
		var bkey = this._str2Binl(key);
		if (bkey.length > 16) {
			bkey = this._coreMD5(bkey, key.length * this.chrs_z);
		}

		var ipad = new Array(16)
		var opad = new Array(16);
		
		for (var i = 0; i < 16; i++) {
			ipad[i] = bkey[i] ^ 0x36363636;
			opad[i] = bkey[i] ^ 0x5C5C5C5C;
		}

		var hash = this._coreMD5(ipad.concat(this._str2Binl(data)), 512 + data.length * this.chrs_z);
		
		return this._coreMD5(opad.concat(hash), 512 + 128);
	},
	
	_safeAdd:							function(x, y) {
		var lsw = (x & 0xFFFF) + (y & 0xFFFF);
		var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
		return (msw << 16) | (lsw & 0xFFFF);		
	},
	
	_bitRol:							function(num, cnt) {
		return (num << cnt) | (num >>> (32 - cnt));
	},
	
	_str2Binl:							function(str) {
		var bin = new Array();
		var mask = (1 << this.chrs_z) - 1;
		for (var i = 0; i < str.length * this.chrs_z; i += this.chrs_z) {
			bin[i >> 5] |= (str.charCodeAt(i / this.chrs_z) & mask) << (i % 32);
		}
		return bin;		
	},
	
	_binl2Str:							function(bin) {
		var str = "";
		var mask = (1 << this.chrs_z) - 1;
		for (var i = 0; i < bin.length * 32; i += this.chrs_z) {
			str += String.fromCharCode((bin[i >> 5] >>> (i % 32)) & mask);
		}
		return str;
	},
	
	_binl2Hex:							function(binarray) {
		var hex_tab = this.hex_case ? "0123456789ABCDEF" : "0123456789abcdef";
		var str = "";
		for (var i = 0; i < binarray.length * 4; i++) {
			str +=	hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xF) +
					hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8    )) & 0xF);
		}
		return str;
	},
	
	_binl2B64:							function(binarray) {
		var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
		var str = "";
		for (var i = 0; i < binarray.length * 4; i += 3) {
			var triplet = (((binarray[i     >> 2] >> 8 * ( i      % 4)) & 0xFF) << 16)
						| (((binarray[i + 1 >> 2] >> 8 * ((i + 1) % 4)) & 0xFF) << 8 )
						|  ((binarray[i + 2 >> 2] >> 8 * ((i + 2) % 4)) & 0xFF);
			for (var j = 0; j < 4; j++) {
				if (i * 8 + j * 6 > binarray.length * 32) {
					str += this.b64_pad;
				} else {
					str += tab.charAt((triplet >> 6*(3 - j)) & 0x3F);
				}
			}
		}
		return str;
	},
	
	end: true
}

/**
 * md5() is a shortcut to quickly create a URL-ready MD5 Hash from a string.
 * 
 * @param str	{String}	The string to Hash.
 * 
 * @return the MD5 Hash
 * @type String
 */
function md5(str) {
	return MD5.hexMD5(str);
}


/*** @inca-include ../admin/assets/scripts/objects/Admin.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/admin.js
 * FUNCTION:		Contains the Admin object, containing top level INCA Admin functions.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-18
 ***************************************************************************************/

var Admin = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Admin",
	
	// Global constants
	OBJECT_STATUS_UNINITIALISED:		0,
	OBJECT_STATUS_INITIALISED:			1,
	OBJECT_STATUS_PREPARED:				2,
	
	// Properties
	_properties:						[],
	_title_format:						"INCA2 - %project_id%@%site_id% - %message%",
	_init_messages:						[],
	
	/**
	 * init() initialises the Admin object.
	 */
	init:				function() {
		
		log("init", "Initialising Admin (v" + this._VERSION + ")");
		log("info", "Admin version: " + this.getProperty("INCA_VERSION"));
		log("info", "Admin URL: " + this.getProperty("SITE_ADMIN_URL"));
		
		this.updateTitle("Initialising, please wait...");
		
		// Initialise cookies
		Cookies.init();
		
		// Initialise browser
		Browser.init();
		
		// Do compatability check
		Browser.doAdminCompatibilityCheck();

		// Begin mouse tracking		
		Mouse.init();
		
		// Initialise request
		Request.init();
		
		// Initialise page
		Page.init();
		
		// Compare start date
		if (this.getProperty("PROJECT_START_DATE") != this.getProperty("PROJECT_ORIG_START_DATE")) {
			var start_date		= Date.create(this.getProperty("PROJECT_START_DATE"));
			var orig_start_date	= Date.create(this.getProperty("PROJECT_ORIG_START_DATE"));
			Alerts.warning("<p>A custom start date is currently set that is different from the project start date.</p>" +
							"<p>Any <strong>relative dates</strong> you set will be offset from this custom start date.</p>" +
							"<p>The <em>project start date</em> is " + orig_start_date.format("Y-m-d H:i O") + "</p>" +
							"<p>Your <em>current custom start date</em> is " + start_date.format("Y-m-d H:i O") + "</p>"
							, this, "init", true);
		}
		
		// Show init messages
		this.showInitMessages();
		
		// Make init request
		Administrator.doInitializationRequest();
	},
	
	/**
	 * setProperty() sets a property of the Admin.
	 * 
	 * @param id	{String}	The property ID.
	 * @param value	{Mixed}		The property value.
	 */
	setProperty:		function(id, value) {
		this._properties[id] = value;
	},
	
	/**
	 * getProperty() gets the value of a stored property.
	 * 
	 * @param id			{String}	The ID of the property to get.
	 * @param alt_value		{Mixed}		An alternative value to return if the requested
	 * 									ID was not found.
	 * 
	 * @return the value of the Property, or the alternative value.
	 * @type Mixed
	 */
	getProperty:		function(id, alt_value) {
		
		// Check arguments
		alt_value = alt_value || false;
		
		if (this._properties[id]) {
			return this._properties[id];
		} else {
			return alt_value;
		}
	},
	
	/**
	 * updateTitle() updates the window title.
	 * 
	 * @param message	{String}	The message to display in the title.
	 */
	updateTitle:		function(message) {
		
		// Check arguments
		message = message || "Loading...";
		
		try {
			document.title = this._title_format.split("%project_id%").join(this.getProperty("PROJECT_ID", "")).split("%site_id%").join(this.getProperty("SITE_ID", "")).split("%message%").join(message);
		} catch (error) {};
	},
	
	addInitMessage:		function(message) {
		this._init_messages.push(message);
	},
	
	showInitMessages:	function() {
		for (var i = 0; i < this._init_messages.length; i++) {
			Alerts.jsonMessage(this._init_messages[i]);
		}
	},
	
	onError:			function(e) {
		Alerts.error(e, this, "onError", true);
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/AdminInterface.js ***/

var AdminInterface = {
	
	TextButtonPair:					{
		
		_getElements:					function(element) {
			var id = element.id.split("-")[0];
			var text	= $(id + "-text");
			var button	= $(id + "-button");
			return [text, button];
		},
		
		over:							function(element) {
			var elements = this._getElements(element);
			elements[0].addClassName("hidden");
			elements[1].removeClassName("hidden");
		},
		
		out:							function(element) {
			var elements = this._getElements(element);
			elements[0].removeClassName("hidden");
			elements[1].addClassName("hidden");
		}
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Cookies.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/cookies.js
 * FUNCTION:		Contains the Cookies object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-26
 ***************************************************************************************/

/**
 * The Cookies object handles Admin cookies.
 * 
 * @creator Cameron Morrow
 */
var Cookies = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Cookies",
	SAFE_CHARS:							"0123456789" +
										"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
										"abcdefghijklmnopqrstuvwxyz" +
										"-_.!~*'()",
	HEX_CHARS:							"0123456789ABCDEFabcdef",
	
	
	// An object storing cookie keys and values
	cookies:							{},
	
	/**
	 * init() initialises the Cookies object.
	 */
	init:								function() {
		
		// Log
		log("init", "Initialising Cookies (v" + this._VERSION + ")");
		
		// Parse already-stored cookies into local object
		this._parse();
	},
	
	/************************************************************************************
	 * PUBLIC METHODS
	 ***********************************************************************************/
	
	/**
	 * set() sets a cookie variable.
	 * 
	 * @param key	{String}	The key (identifier) for the value.
	 * @param value	{String}	The value to store.
	 */
	set:								function(key, value) {
		this.cookies[String(key)] = String(value);
		this._recompile();
	},
	
	/**
	 * get() gets a cookie variable.
	 * 
	 * @param key	{String}	The key (identifier) for the value.
	 * 
	 * @return the value, or false if not found.
	 * @type String/Boolean
	 */
	get:								function(key) {
		if (this.exists(key)) {
			return this.cookies[String(key)];
		} else {
			return false;
		}
	},
	
	/**
	 * unset() unsets (deletes) a cookie variable.
	 * 
	 * @param key	{String}	The key to remove
	 */
	unset:								function(key) {
		if (this.exists(key)) {
			this.cookies[String(key)] = null;
			this._recompile();
		}
	},
	
	/**
	 * exists() returns whether a key exists in the local cookie object.
	 * 
	 * @param key	{String}	The key to check for the existance of
	 * 
	 * @return whether the key exists or not
	 * @type Boolean
	 */
	exists:							function(key) {
		if (this.cookies[String(key)]) {
			if (this.cookies[String(key)] !== null)
				return true;
		}
		
		return false;
	},
	
	/**
	 * getAlt() gets a cookie variable, or an alternative value if the cookie value is
	 * not found.
	 * 
	 * @param key	{String}	The key (identifier) for the value.
	 * 
	 * @return the value, or the alternative value if not found.
	 * @type Mixed
	 */
	getAlt:							function(key, alt_value) {
		if (this.exists(key)) {
			return this.cookies[String(key)];
		} else {
			return alt_value;
		}
	},
	
	/**
	 * print() prints the local Cookie object values.
	 */
	print:								function() {
		
		var output = "";
		for (var i in this.cookies) {
			if (this.exists(i))
				output += "[" + i + "] = {" + this.cookies[i] + "}\n";
		}
		
		alert(output);
	},
	
	/************************************************************************************
	 * PRIVATE METHODS
	 ***********************************************************************************/
	
	/**
	 * _parse() parses the existing document.cookie and extracts values from it.
	 */
	_parse:								function() {
	
		// Split cookies
		var cookie_str = document.cookie.split("; ");
		
		// For each, break into keys and values
		for (var i = 0; i < cookie_str.length; i++) {
			
			// Split
			var pair = cookie_str[i].split("=");
			
			if (pair[0] == "data") {
				this._parseData(pair[1]);
			}
		}
	},
	
	/**
	 * parseData() parses a passed data string and stores the values from it in the local
	 * cookies object.
	 * 
	 * @parma data_str	{String}	The string to extract values from.
	 */
	_parseData:							function(data_str) {
		
		// Split
		var data = data_str.split("|");
		
		// For each data item
		for (var i = 0; i < data.length; i++) {
			
			// Split
			var pair = data[i].split(":");
			
			// If correct length, decode
			if (pair.length == 2)
				this.cookies[this._decode(pair[0])] = this._decode(pair[1]);
		}
	},
	
	/**
	 * _recompile() recompiles the local object into the document.cookie.
	 */
	_recompile:							function() {
		document.cookie = "data=" + this._recompileData() + "; expires=" + this._getExpiryDate();
	},
	
	/**
	 * _recompileData() recompiles the local cookie object into a data string.
	 * 
	 * @return a string of data
	 * @type String
	 */
	_recompileData:						function() {
		
		var data = [];
		
		for (var i in this.cookies) {
			if (this.cookies[i] !== null)
				data.push(this._encode(i) + ":" + this._encode(this.cookies[i]));
		}
		
		return data.join("|");
	},
	
	/**
	 * _getExpiryDate() gets the expiry date to use for the cookie values.
	 * 
	 * @return a string to use in the expire value of the cookie
	 */
	_getExpiryDate:						function() {
		return "31 Dec 2030 23:59:59 GMT";
	},
	
	/**
	 * _encode() does an encoding similar to PHP's urlencode() function. Used to store
	 * unusual chars that might otherwise break the data string.
	 * Based on a method found here: http://www.albionresearch.com/misc/urlencode.php
	 * 
	 * @param str	{String}	The String to encode.
	 * 
	 * @return the encoded string
	 * @type String
	 */
	_encode:							function(str) {
		
		// Variables
		var encoded_str = "";
		var chr, chr_code;
		
		// For each character
		for (var i = 0; i < str.length; i++) {
			
			// Get the character
			chr = str.charAt(i);
			
			// Empty strings become pluses
			if (chr == " ") {
				encoded_str += "+";
				
			// If this is a safe char, goes straight in
			} else if (this.SAFE_CHARS.indexOf(chr) != -1) {
				encoded_str += chr;
				
			// Otherwise, encode
			} else {
				chr_code = chr.charCodeAt(0);
				encoded_str += (chr_code > 255) ? "+" : ("%" + this.HEX_CHARS.charAt((chr_code >> 4) & 0xF) + this.HEX_CHARS.charAt(chr_code & 0xF));
			}
		}
		
		// Done
		return encoded_str;
	},
	
	/**
	 * _decode() does a decoding similar to PHP's urldecode.
	 * Based on a method found here: http://www.albionresearch.com/misc/urlencode.php
	 * 
	 * @param str	{String}	The String to decode.
	 * 
	 * @return a decoded version of the string.
	 * @type String
	 */
	_decode:							function(str) {
		
		// Variables
		var decoded_str = "";
		var chr, i = 0;
		
		while (i < str.length) {
		
			chr = str.charAt(i);
			
			if (chr == "+") {
				decoded_str += " ";
				i++;
			} else if (chr == "%") {
				if (i < (str.length - 2) && this.HEX_CHARS.indexOf(str.charAt(i + 1)) != -1 && this.HEX_CHARS.indexOf(str.charAt(i + 2)) != -1) {
					decoded_str += unescape(str.substr(i, 3));
					i += 3;
				} else {
					decoded_str += "%ERROR";
					i++;
				}
			} else {
				decoded_str += chr;
				i++;
			}
		}
		
		return decoded_str;
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Browser.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/browser.js
 * FUNCTION:		Contains the Browser object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-20
 ***************************************************************************************/

/**
 * The Browser object contains methods to determine information about the user's
 * browser's capabilities.
 * 
 * @creator Cameron Morrow
 */
var Browser = {
	
	// Version
	_VERSION:							1.1,
	_CLASS:								"Browser",
	
	// Parameters
	ua:									navigator.userAgent.toLowerCase(),
	pl:									navigator.platform.toLowerCase(),
	
	/**
	 * init() initialises the Browser object
	 */
	init:								function() {
		
		// Log this
		log("init", "Initialising Browser (v" + this._VERSION + ")");
		
		// Prepare
		this.initPlatform();
		this.initBrowser();
		this.initEngine();
		this.initVersion();
		this.initFlash();
		this.initDOM();
		
		// Log the browser details
		this.logSummary();
	},
	
	/************************************************************************************
	 * CHECKING METHODS
	 ***********************************************************************************/
	
	/**
	 * doAdminCompatibilityCheck() does a check on the user's browser to see if it is
	 * suitable for use in the Inca2 Admin.
	 */
	doAdminCompatibilityCheck:			function() {
	
		// Get information
		var info = this.getSummary();
		
		// Platform check
		if (info.platform === false) {
			this.failCompatibilityCheck("Could not determine your computer platform.");
		}
			
		// Browser check
		if (info.browser === false) {
			this.failCompatibilityCheck("Could not determine your browser.");
		}
			
		// Check flash
		if (info.flash.available === false) {
			this.failCompatibilityCheck("Flash was not found on your computer.");
		} else if (info.flash.version < 7) {
			for (var i in info.flash) {
				alert(i + " = " + info.flash[i]);
			}
			this.failCompatibilityCheck("Your version of Flash is obsolete and needs to be upgraded.");
		}
	},
	
	/**
	 * failCompatibilityCheck() fails the check for Inca2 Admin compatability.
	 * 
	 * @param error	{String}	The error message to display indicating why it failed.
	 * 
	 */
	failCompatibilityCheck:				function(error) {
		//window.location = "../admin/index_fail.html?error=" + error;
		alert(error);
	},
	
	/************************************************************************************
	 * SUMMARY METHODS
	 ***********************************************************************************/
	
	/**
	 * getSummary() gets a summary of the browser's abilities and properties.
	 * 
	 * @return an object with properties
	 * @type Object
	 */
	getSummary:							function() {
		
		// Create object
		var summary							= {};
		
		// Get values
		summary.platform					= this.getPlatform();
		summary.browser						= this.getBrowser();
		summary.engine						= this.getEngine();
		summary.version						= this.version;
		summary.flash						= this.flash;
		summary.dom							= this.dom;
		
		// Done
		return summary;
	},
	
	/**
	 * logSummary() logs a summary of the browser's abilities and properties
	 */
	logSummary:							function() {
		
		// Get summary
		var summary							= this.getSummary();
		
		// Output to log
		var output1							= "";
		var output2							= "";
		
		// Create output
		output1 += "Browser: " + summary.browser.capitalize() + " (v" + summary.version.major + "." + summary.version.minor + ") on " + summary.platform.capitalize() + "";
		output2 += "Flash available: " + (summary.flash.available ? "yes, v" + summary.flash.version : "no") + "; DOM1 compatible: " + (summary.dom.dom1 ? "yes" : "no") + "; DOM2 compatible: " + (summary.dom.dom2 ? "yes" : "no");
		
		// Log
		log("info", "User agent: " + this.ua);
		log("info", output1);
		log("info", output2);
	},
	 
	/************************************************************************************
	 * PLATFORM METHODS
	 ***********************************************************************************/
	
	/**
	 * initPlatform() initialises the platform object of the Browser.
	 */
	initPlatform:						function() {
		
		// Create Platform
		this.platform				= {};
		
		// Set parameters
		this.platform.isMacPPC		= (this.pl.indexOf("macppc") != -1);
		this.platform.isMacIntel	= (this.pl.indexOf("macintel") != -1);
		this.platform.isMac			= (this.platform.isMacPPC || this.platform.isMacIntel);
		this.platform.isWin			= (this.pl.indexOf("win") != -1);
		this.platform.isLinux		= (this.ua.indexOf("linux") != -1);
	},
	
	/**
	 * getPlatform() gets a string indicating the platform of the Browser.
	 * 
	 * @return the platform
	 * @type String
	 */
	getPlatform:						function() {
		if (this.platform.isMac) {
			return "mac";
		} else if (this.platform.isWin) {
			return "win";
		} else if (this.platform.isLinux) {
			return "linux";
		} else {
			return false;
		}
	},
	 
	/************************************************************************************
	 * BROWSER/VERSION METHODS
	 ***********************************************************************************/
	
	/**
	 * initBrowser() initialises the browser object of the Browser
	 */ 
	initBrowser:						function() {
	
		// Create Browser
		this.browser					= {};
		
		// Set parameters
		this.browser.isKonqueror		= (this.ua.indexOf("konqueror") != -1);
		this.browser.isSafari			= (this.ua.indexOf("safari") != - 1);
		this.browser.isOmniweb			= (this.ua.indexOf("omniweb") != - 1);
		this.browser.isOpera			= (this.ua.indexOf("opera") != -1);
		this.browser.isIcab				= (this.ua.indexOf("icab") != -1);
		this.browser.isAOL				= (this.ua.indexOf("aol") != -1);
		this.browser.isIE				= (this.ua.indexOf("msie") != -1 && !this.browser.isOpera && (this.ua.indexOf("webtv") == -1));
		this.browser.isMozilla			= (this.ua.indexOf("safari") == -1 && this.ua.indexOf("gecko/") + 14 == this.ua.length);
		this.browser.isFirebird			= (this.ua.indexOf("firebird/") != -1);
		this.browser.isFirefox			= (this.ua.indexOf("firefox/") != -1);
		this.browser.isNS				= (this.ua.indexOf("netscape") != -1);
	},
	
	/**
	 * getBrowser() gets a string indicating the browser
	 * 
	 * @return the name of the browser
	 * @type String
	 */
	getBrowser:							function() {
		if (this.browser.isKonqueror) {
			return "konqueror";
		} else if (this.browser.isSafari) {
			return "safari";
		} else if (this.browser.isOmniWeb) {
			return "omniweb";
		} else if (this.browser.isOpera) {
			return "opera";
		} else if (this.browser.isIcab) {
			return "icab";
		} else if (this.browser.isAOL) {
			return "aol";
		} else if (this.browser.isIE) {
			return "ie";
		} else if (this.browser.isMozilla) {
			return "mozilla";
		} else if (this.browser.isFirebird) {
			return "firebird";
		} else if (this.browser.isFirefox) {
			return "firefox";
		} else if (this.browser.isNS) {
			return "netscape";
		} else {
			return false;
		}
	},
	
	/**
	 * initVersion() initialises the browser version object
	 */
	initVersion:						function() {
		
		// Create parameters
		this.version				= {major: false, minor: false};
		
		// Attempt basic values
		this.version.minor			= parseFloat(navigator.appVersion); 
	
		// By Me
		if (this.browser.isFirefox) {
			this.version.minor = parseFloat(this.ua.substr(this.ua.lastIndexOf("firefox/") + 8));
		} else if (this.browser.isIE && this.version.minor >= 4) {
			this.version.minor = this.ua.substr(this.ua.indexOf("msie ") + 5);
			this.version.minor = parseFloat(this.version.minor.substr(0, this.version.minor.indexOf(";")));
		} else if (this.browser.isOpera) {
			this.version.minor = this.ua.substr(this.ua.indexOf("opera/") + 6);
			this.version.minor = parseFloat(this.version.minor.substr(0, this.version.minor.indexOf(" ")));
		
		// Other
		} else if (this.engine.isGecko && !this.browser.isMozilla) {
			this.version.minor = parseFloat(this.ua.substring(this.ua.indexOf("/", this.ua.indexOf("gecko/") + 6) + 1));
		} else if (this.browser.isMozilla) {
			this.version.minor = parseFloat(this.ua.substring(this.ua.indexOf("rv:") + 3));
		} else if (this.browser.isKonqueror) {
			this.version.minor = parseFloat(this.ua.substring(this.ua.indexOf('konqueror/') + 10));
		} else if (this.browser.isSafari) {
			this.version.minor = parseFloat(this.ua.substring(this.ua.lastIndexOf('safari/') + 7));
		} else if (this.browser.isOmniweb) {
			this.version.minor = parseFloat(this.ua.substring(this.ua.lastIndexOf('omniweb/') + 8));
		} else if (this.browser.isIcab) {
			this.version.minor = parseFloat(this.ua.substring(this.ua.indexOf('icab') + 5));
		}
	
		// Establish top level version
		this.version.major = parseInt(this.version.minor);
		this.version.minor = Math.round((this.version.minor - this.version.major) * 10);
	},
	
	/************************************************************************************
	 * ENGINE METHODS
	 ***********************************************************************************/

	/**
	 * initEngine() initialises the engine object
	 */
	initEngine:							function() {
		
		// Create Engine
		this.engine					= {};
		
		// Set parameters
		this.engine.isGecko			= (this.ua.indexOf("gecko") != -1 && this.ua.indexOf("safari") == -1);
		this.engine.isAppleWebKit	= (this.ua.indexOf("applewebkit") != -1);
		this.engine.isTrident		= this.browser.isIE && this.platform.isWin;
	},

	/**
	 * getEngine() gets a string indicating the engine
	 * 
	 * @return the name of the browser's engine
	 * @type String
	 */
	getEngine:							function() {
		if (this.engine.isGecko) {
			return "gecko";
		} else if (this.engine.isAppleWebKit) {
			return "webkit";
		} else if (this.engine.isTrident) {
			return "trident";
		} else {
			return false;
		}
	},
	
	/************************************************************************************
	 * FLASH METHODS
	 ***********************************************************************************/

	/**
	 * initFlash() initialises the flash object
	 */
	initFlash:							function() {
		
		// Create Flash
		this.flash = {available: false, version: false};
		
		// If IE
		if (this.browser.isIE) {
			for (var i = 10; i > 0; i--) {
				try {
					var flash = new ActiveXObject("ShockwaveFlash.ShockwaveFlash." + i);
					this.flash.available	= true;
					this.flash.version		= i;
					break;
				}
				catch(e) { }
			}
		} else {
			try {
				if (navigator.plugins != null) {
					if (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]) {
						this.flash.available	= true;
						this.flash.version		= navigator.plugins["Shockwave Flash 2.0"] ? navigator.plugins["Shockwave Flash 2.0"].description : navigator.plugins["Shockwave Flash"].description;
						this.flash.version		= this.flash.version.split(".").shift();
						this.flash.version		= this.flash.version.split(" ").pop();
					}
				}
			} catch(e) { alert(e); }
		}
	},
	 
	/************************************************************************************
	 * DOM METHODS
	 ***********************************************************************************/
	 
	/**
	 * initDOM() initialises the DOM object
	 */
	initDOM:							function() {
		
		// Create DOM
		this.dom					= {dom1: false, dom2: false};
		
		try {
			this.dom.dom1			= (document.getElementById);
			this.dom.dom2			= (document.addEventListener && document.removeEventListener);
		} catch (e) {}
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Page.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/page.js
 * FUNCTION:		Contains the Page object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-18
 ***************************************************************************************/

/**
 * The Page object handles page-rendering methods, generation of the interface, resizing
 * and moving elements, etc.
 */
var Page = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Page",
	
	// Properties
	preload_images:						[
			"logo-large.png",
			"controls/24/close.png",
			"controls/24/close-on.png",
			"controls/30/clear.png",
			"controls/30/clear-on.png",
			"controls/30/close.png",
			"controls/30/close-on.png",
			"controls/30/maximize.png",
			"controls/30/maximize-on.png",
			"controls/30/minimize.png",
			"controls/30/minimize-on.png",
			"filelist/remove.png",
			"filelist/remove-down.png",
			"log/title-bg.png",
			"log/icons/ajax.png",
			"log/icons/conf.png",
			"log/icons/error.png",
			"log/icons/info.png",
			"log/icons/init.png",
			"login/button-blurred.png",
			"login/button-focused.png",
			"login/input-blurred.png",
			"login/input-focused.png",
			"login/label-blurred.png",
			"login/label-focused.png"],
	_elements:							{},
	_items:								[],
	_max_index:							0,
	_resize_elements:					[],
	
	/**
	 * init() initialises the Page object, adding all elements that make up the page.
	 */
	init:								function() {
		
		// Log
		log("init", "Initialising Page (v" + this._VERSION + ")");
		
		// Create Page elements
		this._createElements();
		
		// Preload images
		this._preload();
		
		// Set a browser style
		this._elements.body.addClassName(Browser.getBrowser());
		
		// Initialise classes and have their elements inserted
		Intervals.init();
		ListBoxes.init();
		Text.init();
		Log.init(3);
		Loader.init(7);
		Administrator.init(4);
		Popups.init(6);
		Alerts.init();
		Administrator.pageRegister(5);
		INCA2Components.init(2);
		ComponentInterface.init(1);
		Balloon.init(8);
		MiniCal.init();
		Files.init();
		JSON.init();
		
		// Draw
		this._prepare();
	},
	
	/**
	 * _createElements() creates the DOM elements for the page.
	 */
	_createElements:					function() {
		
		this._elements.body			= $(document.body);
		this._elements.preloaded	= createElement("div", ["id", "preloaded"]);
		this._elements.content		= createElement("div", ["id", "content"]);
		
		this._elements.body.appendChild(this._elements.preloaded);
	},
	
	/**
	 * insert() inserts an element into the items list, ready to be rendered.
	 * 
	 * @param index		{Integer}		The order in which the element should be drawn.
	 * @param element	{DOM Element}	The element to insert.
	 */
	insert:								function(index, element) {
		
		// Store element
		this._items[index] = element;
		
		// Update maximum index
		this._max_index = (index > this._max_index) ? index : this._max_index;
	},
	
	/**
	 * _prepare() prepares the various elements for use, attaching them to the <body>
	 * and calling any required preparation methods.
	 */
	_prepare:							function() {
		
		// Update elements
		for (var i = 0; i <= this._max_index; i++) {
			if (!isUndefined(this._items[i])) {
				this._elements.body.appendChild(this._items[i]);
			}
		}
		
		// Prepare
					   Log.prepare();
					Loader.prepare();
			 Administrator.prepare();
					Popups.prepare();
		   INCA2Components.prepare();
		ComponentInterface.prepare();
				   Balloon.prepare();
	},
	
	/************************************************************************************
	 * RESIZING
	************************************************************************************/
	
	/**
	 * onResize() is triggered when the browser window is resized. It triggers updates on
	 * the appearance of elements on the page.
	 */
	onResize:							function() {
					   Log.redraw();
					Loader.redraw();
			 Administrator.redraw();
					Popups.redraw();
		   INCA2Components.redraw();
		ComponentInterface.redraw();
				   Balloon.redraw();
				   
		this.resizeElements();
	},
	
	registerResizeElement:				function(element) {
		this._resize_elements.push(element);
	},
	
	resizeElements:						function() {
		
		Alerts.track("Resizing Page elements...", this, "resizeElements");
		//return false;
		for (var i = 0; i < this._resize_elements.length; i++) {
			this.resizeElement(this._resize_elements[i]);
		}
		
		Alerts.track("Resizing Page elements complete.", this, "resizeElements");
	},
	
	resizeElement:						function(id) {
		var element = $(id);
		if (element) {
			var holder = this._getResizeHolder(element);
			if (holder) {
				var holder_height	= this._getHolderHeight(holder);
				var items			= this._getItemsInHolder(holder);
				if (items) {
					
					// Get the total fixed height to determine how much fill-room the varying height items have
					// Also get the varying height items by themselves
					var fixed_height	= 0;
					var scaling_items	= [];
					
					for (var j = 0; j < items.length; j++) {
						if (items[j].scale == "scale") {
							scaling_items.push(items[j].element);
						} else {
							fixed_height += items[j].height;
						}
					}
					
					// Scale the scaling items
					if (scaling_items.length > 0) {
						
						// Get height of each
						var scaling_height = (holder_height - fixed_height) / scaling_items.length;
						
						Alerts.track("Holder: " + holder.id + " (" + holder.className + "); Holder height: " + holder_height + "; fixed height: " + fixed_height + ", scaling height: " + scaling_height, this, "resizeElements");
						
						for (var j = 0; j < scaling_items.length; j++) {
							
							// Resize holder
							Page.simpleSetHeight(scaling_items[j], scaling_height);
							
							// Handle any necessary internal resizing
							scaling_items[j]._object.resize(scaling_height);
						}
					} else {
						Alerts.track("No variable-height items to scale.", this, "resizeElements");
					}
				}
			}
		}
	},
	
	_getHolderHeight:					function(holder) {
		if ($(holder).hasClassName("tabbodyli")) {
			return this.simpleGetHeight($("tabbody")) - 2;
		}
		
		return this.simpleGetHeight(holder);
	},
	
	_getHolderWidth:					function(holder) {
		if ($(holder).hasClassName("tabbodyli")) {
			return this.simpleGetWidth($("tabbody")) - 2;
		}
		
		return this.simpleGetWidth(holder);
	},
	
	_getResizeHolder:					function(element) {
		while (element.parentNode) {
			if (element.id == "sidebar") {
				return element;
			} else if ($(element).hasClassName("tabbodyli")) {
			//} else if ($(element).id == "tabbody") {
				return element;
			}
			
			element = $(element.parentNode);
		}
		
		return false;
	},
	
	_getItemsInHolder:					function(holder) {
		var items = [];
		for (var i = 0; i < holder.childNodes.length; i++) {
			if ($(holder.childNodes[i]).hasClassName("item")) {
				items.push({
					element:	$(holder.childNodes[i]),
					height:		this.simpleGetHeight(holder.childNodes[i]),
					scale:		$(holder.childNodes[i]).hasClassName("listboxitem") ? "scale" : "fixed"
				});
			}
		}
		
		return items;
	},
	
	/************************************************************************************
	 * PRELOADING
	************************************************************************************/
	
	/**
	 * _preload() preloads an array of images.
	 */
	_preload:							function() {
		
		// The prefix to all image URLs
		var prefix = "../admin/assets/images/";
		
		
		// Load all the images		
		for (var i = 0; i < this.preload_images.length; i++) {
			this._preloadImage(prefix + this.preload_images[i]);
		}
	},
	
	/**
	 * _preloadImage() preloads a single URL.
	 * 
	 * @param url	{String}	The URL to the image to preload.
	 */
	_preloadImage:						function(url) {
		this._elements.preloaded.appendChild(createElement("img",
			["alt", "preloaded image"],
			["src",	url]
		));
	},
	
	/************************************************************************************
	 * SIMPLE POSITIONING METHODS
	 * scriptaculous methods can do some funny things like changing z-indexes, so for
	 * most uses these methods are prefered
	************************************************************************************/
	
	// An array of intervals used by the simple* methods
	_simple_intervals:					[],
	
	/******************************************************
	 * GETTING WINDOW DIMENSIONS
	 *****************************************************/
	
	/**
	 * simpleGetWindowDimensions() returns the window dimensions.
	 * 
	 * @return an object with width and height properties.
	 * @type Object
	 */
	simpleGetWindowDimensions:			function() {
		return {
			width:	this.simpleGetWindowWidth(),
			height:	this.simpleGetWindowHeight()
		}
	},
	
	/**
	 * simpleGetWindowWidth() gets the width of the window (cross-browser).
	 *
	 * @return the width of the window.
	 * @type Integer
	 */
	simpleGetWindowWidth:					function() {
		if (Browser.browser.isIE) {
			try {
				return document.documentElement.clientWidth;
			} catch (error) {}
		} else {
			try {
				return window.innerWidth;
			} catch (error) {}
		}
		
		return 0;
	},
	
	/**
	 * simpleGetWindowHeight() gets the height of the window (cross-browser).
	 *
	 * @return the height of the window.
	 * @type Integer
	 */
	simpleGetWindowHeight:				function() {
		if (Browser.browser.isIE) {
			try {
				return document.documentElement.clientHeight;
			} catch (error) {}
		} else {
			try {
				return window.innerHeight;
			} catch (error) {}
		}
		
		return 0;
	},
	
	/******************************************************
	 * GETTING DIMENSIONS AND POSITIONS
	 *****************************************************/
	
	simpleGetDimensions:				function(element) {
		return $(element).getDimensions();
	},
	
	simpleGetWidth:						function(element) {
		return ($(element).getDimensions()).width;
	},
	
	simpleGetHeight:					function(element) {
		return ($(element).getDimensions()).height;
	},
	
	/**
	 * simpleGetPosition() gets the X and Y position of the element.
	 * 
	 * @param element	{DOM Element}	The element to get the position of.
	 * 
	 * @return an object with X and Y properties.
	 * @type Object
	 */
	simpleGetPosition:					function(element) {
		return {
			x:	this.simpleGetX(element),
			y:	this.simpleGetY(element)
		}
	},
	
	/**
	 * simpleGetX() gets the X position of an absolutely-positioned element.
	 * 
	 * @param element	{DOM Element}	The element to get the X-pos of.
	 * 
	 * @return the number of pixels from the left of the left corner of this element.
	 * @type Integer
	 */
	simpleGetX:							function(element) {
		
		// Start measurement
		var left = 0;
		
		if ($(element)) {
		
			// If offset can be found
			if ($(element).offsetParent || $(element).x) {
				if ($(element).offsetParent) {
					while ($(element).offsetParent) {
						left	+= $(element).offsetLeft;
						element	= $(element).offsetParent;
					}
				} else if ($(element).x) {
					left += $(element).x;
				}
			} else {
				Alerts.error("Neither .offsetParent or .x was defined for this element: " + element + " (" + element.className + ")", this, "simpleGetX", false);
				Intervals.stopAll();
				return left;
			}
		} else {
			Alerts.error("Element " + element + " does not exist.", this, "simpleGetX", false);
			Intervals.stopAll();
			return left;
		}
		
		// Return calculated value
		return left;
	},
	
	/**
	 * simpleGetY() gets the Y position of an absolutely-positioned element.
	 * 
	 * @param element	{DOM Element}	The element to get the Y-pos of.
	 * 
	 * @return the number of pixels from the top of the top corner of this element.
	 * @type Integer
	 */
	simpleGetY:							function(element) {
		
		// Start measurement
		var top = 0;
		
		if ($(element)) {
		
			// If offset can be found
			if (element.offsetParent || element.y) {
				if (element.offsetParent) {
					while (element.offsetParent) {
						top += element.offsetTop;
						if (isNumeric(element.scrollTop)) {
							top -= element.scrollTop;
						}
						element = element.offsetParent;
					}
				} else if (element.y) {
					top += element.y;
				}
			} else {
				Alerts.error("Neither .offsetParent or .y was defined for this element.", this, "simpleGetY");
				Intervals.stopAll();
			}
		} else {
			Alerts.error("Element " + element + " does not exist.", this, "simpleGetY");
			Intervals.stopAll();
		}
		
		// Return calculated value
		return top;
	},
	
	/******************************************************
	 * POSITIONING
	 *****************************************************/
	
	/**
	 * simpleCentre() is used to centre an object in the window. For the purposes of this
	 * simple method it is assumed that having a margin top and left of 0 would make the
	 * element appear in the top left corner of the window.
	 * 
	 * @param element	{DOM Element}	The element to centre.
	 * @param animate	{Boolean}		Whether to animate the moving
	 */
	simpleCentre:						function(element, animate) {
		
		// Check element
		if ($(element)) {
		
			// Get information about element
			var dimensions		= this.simpleGetDimensions(element);
			
			// Get window information
			var win_dimensions	= this.simpleGetWindowDimensions();
			
			// Centre them
			this.simpleMoveTo(element, win_dimensions.width / 2 - dimensions.width / 2, win_dimensions.height / 2 - dimensions.height / 2, animate);
		} else {
			Alerts.error("Element " + element + " does not exist.", this, "simpleCentre");
		}
	},
	
	/**
	 * simpleMoveTo() is used to move an object to a passed location. For the purposes of
	 * this simple method it is assumed that having a margin top and left of 0 would make
	 * the element appear in the top left corner of the window.
	 * 
	 * @param element	{DOM Element}	The element to move
	 * @param x			{Integer}		The new X position
	 * @param y			{Integer}		The new Y position
	 * @param animate	{Boolean}		Whether to animate the moving
	 */
	simpleMoveTo:						function(element, x, y, animate) {
		
		if ($(element)) {
		
			// If animating
			if (animate) {
				
				// Get new unique ID for interval
				//var int_id = Page._simple_intervals.length;
				
				// Set up an animation interval
				//Page._simple_intervals[int_id] = setInterval(Page.simpleMoveInterval, 20, int_id, element, x, y);
				Intervals.start("Page.simpleMoveInterval", 20, {element: element, x: x, y: y});
			
			// Otherwise, just move it
			} else {
				$(element).setPosition(x, y);
			}
		} else {
			Alerts.error("Element " + element + " does not exist.", this, "simpleMoveTo");
		}
	},
	
	simpleSetX:							function(element, x) {
		if ($(element)) {
			if (isNumeric(x)) {
				$(element).setX(x);
			} else {
				Alerts.error("X value '" + x + "' must be numeric.", this, "simpleSetX");
			}
		} else {
			Alerts.error("Element " + element + " does not exist.", this, "simpleSetX");
		}
	},
	
	simpleSetY:							function(element, y) {
		if ($(element)) {
			if (isNumeric(y)) {
				$(element).setY(y);
			} else {
				Alerts.error("Y value '" + y + "' must be numeric.", this, "simpleSetY");
			}
		} else {
			Alerts.error("Element " + element + " does not exist.", this, "simpleSetY");
		}
	},
	
	/**
	 * simpleMoveBy
	 */
	simpleMoveBy:						function(element, offset_x, offset_y, animate) {

		if ($(element)) {

			// Get current position
			var position = this.simpleGetPosition(element);
			
			// Apply
			this.simpleMoveTo(element, position.x + offset_x, position.y + offset_y, animate);
		} else {
			Alerts.error("Element " + element + " does not exist.", this, "simpleMoveBy");
		}
	},
	
	/**
	 * simpleMoveInterval() is triggered when an animation to move an element occurs.
	 * 
	 * @param int_id	{Integer}	The ID of the animation interval.
	 */
	simpleMoveInterval:					function(int_id) {
		
		var element		= Intervals.getParam(int_id, "element");
		var x			= Intervals.getParam(int_id, "x");
		var y			= Intervals.getParam(int_id, "y");
		
		if ($(element)) {
		
			// Get current position
			var position = Page.simpleGetPosition(element);
			
			// Get delta
			var delta			= {
				x:				Math.round((x - position.x) / 5),
				y:				Math.round((y - position.y) / 5)
			};
			
			// If almost done, call it quits
			if (Math.abs(delta.x) <= 1 && Math.abs(delta.y) <= 1) {
				Page.simpleMoveTo(element, x, y, false);
				Intervals.stop(int_id);
				
			// Otherwise, update position and proceed
			} else {
				Page.simpleMoveTo(element, position.x + delta.x, position.y + delta.y, false);
			}
		} else {
			Alerts.error("Element '" + element + "' does not exist.", this, "simpleMoveInterval");
			Intervals.stopAll();
		}
	},
	
	/******************************************************
	 * RESIZING
	 *****************************************************/
	
	/**
	 * simpleResize() resizes an element to the passed width and height.
	 * 
	 * @param element	{DOM Element}	The element to resize.
	 * @param width		{Integer}		The desired width of the element.
	 * @param height	{Integer}		The desired height of the element.
	 * @param animate	{Boolean}		Whether to animate the resizing.
	 * 
	 * @return the success of the operation.
	 * @type Boolean
	 */
	simpleResize:						function(element, width, height, animate) {
		return this.simpleSetWidth(element, width) && this.simpleSetHeight(element, height);
	},
	
	/**
	 * simpleResizeBy() resizes an element relatively.
	 * 
	 * @param element		{DOM Element}	The element to resize.
	 * @param offset_width	{Integer}		The change in width.
	 * @param offset_height	{Integer}		The change in height.
	 * @param animate		{Boolean}		Whether to animate the resizing.
	 * 
	 * @return the success of the operation.
	 * @type Boolean
	 */
	simpleResizeBy:						function(element, offset_width, offset_height, animate) {
		
		// Get current dimensions to offset
		var dimensions = this.simpleGetDimensions(element);
		
		// Resize
		return this.simpleResize(element, dimensions.width + offset_width, dimensions.height + offset_height, animate);
	},
	
	/**
	 * simpleSetWidth() sets the width of an element.
	 * 
	 * @param element	{DOM Element}	The element to change.
	 * @param width		{Integer}		The desired width of the element.
	 * 
	 * @return the success of the operation.
	 * @type Boolean
	 */
	simpleSetWidth:						function(element, width) {
		if (isIntegeric(width)) {
			$(element).style.width = parseInt(width) + "px";	
			return true;
		} else if (isString(width)) {
			if (width[width.length - 1] == "%" && isIntegeric(width.substr(0, width.length - 2))) {
				$(element).style.width = width;
				return true;
			} else if (width.substr(width.length - 2) == "px" && isIntegeric(width.substr(0, width.length - 2))) {
				$(element).style.width = width;
				return true;
			}
		} else {
			Alerts.error("Width " + width + " is not a number", this, "simpleSetWidth");
		}
		
		return false;
	},
	
	/**
	 * simpleSetHeight() sets the height of an element.
	 * 
	 * @param element	{DOM Element}	The element to change.
	 * @param height	{Integer}		The desired height of the element.
	 * 
	 * @return the success of the operation.
	 * @type Boolean
	 */
	simpleSetHeight:					function(element, height) {
		if (isIntegeric(height)) {
			$(element).style.height = parseInt(height)  + "px";
			return true;
		} else if (isString(height)) {
			if (height[height.length - 1] == "%" && isIntegeric(height.substr(0, height.length - 2))) {
				$(element).style.height = height;
				return true;
			} else if (height.substr(height.length - 2) == "px" && isIntegeric(height.substr(0, height.length - 2))) {
				$(element).style.height = height;
				return true;
			} else {
				Alerts.error("Invalid height string '" + height + "' was passed.", this, "simpleSetHeight");
			}
		} else {
			Alerts.error("Height " + height + " is not a number or a valid string", this, "simpleSetHeight");
		}
		
		return false;
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Log.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/log.js
 * FUNCTION:		Contains the Log object, LogItems class and logging utility methods.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-18
 ***************************************************************************************/

/**
 * log() is a shortcut for Log.log
 * 
 * @param type		{String}	A type for the message. Doubles as the class when rendering.
 * @param message	{String}	The message to log.
 */
function log(type, message) {
	if (isUndefined(message)) {
		Log.log("info", type);
	} else {
		Log.log(type, message);
	}
}

/**
 * The Log object handles Logging information.
 * 
 * @creator Cameron Morrow
 */
var Log = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Log",
	_MOST_RECENT_FIRST:					true,
	_LOG_OPEN_AND_CLOSE:				false,
	_INITIALLY_OPEN:					false,
	
	// Common properties
	_status:							Admin.OBJECT_STATUS_UNINITIALISED,
	_index:								0,
	
	// Properties
	_items:								[],
	_elements:							{},
	_open:								false,
	_unviewed_items:					0,
	
	/************************************************************************************
	 * INITIALISATION
	 ***********************************************************************************/
	
	/**
	 * init() initialises the Log object.
	 * 
	 * @param index	{Integer}	The z-index of this item in the interface.
	 */
	init:								function(index) {
		
		log("init", "Initialising Log (v" + this._VERSION + ")");
		
		// Create HTML DOM elements
		this._createElements();
		
		// Store Index
		this._index = index;
		
		// Update status
		this._status = Admin.OBJECT_STATUS_INITIALISED;
		
		// Append to interface
		Page.insert(index, this._elements.log);
	},
	
	/**
	 * _createElements() creates the DOM elements for this object.
	 */
	_createElements:					function() {
		
		// Create HTML DOM elements
		this._elements.log			= createElement("div", ["id", "log"]);

		this._elements.title		= createElement("div",
			["id", "logtitle"],
			createElement("table",
				createElement("tbody",
					createElement("tr",
						createElement("td",
							["id", "h3holder"],
							createElement("h3",
								["id", "logheadertext"],
								"Log"
							)
						),
						$(createElement("td",
							$(createElement("a",
								["id", "logclearbutton"],
								"&empty;"
							)).addClassName("greybutton")
						)).addClassName("buttonholder"),
						$(createElement("td",
							$(createElement("a",
								["id", "logtogglebutton"],
								"&uarr;"
							)).addClassName("greybutton")
						)).addClassName("buttonholder")
					)
				)
			)
		);
		this._elements.list			= createElement("ul", ["id", "loglist"]);
		
		// Append them to form a single element
		this._elements.log.appendChild(createElement("div",
			["id",		"loginner"],
			this._elements.title,
			this._elements.list
		));
	},
	
	/**
	 * prepare() prepares this object for use.
	 */
	prepare:							function() {
		
		// Update status
		this._status = Admin.OBJECT_STATUS_PREPARED;
		
		// Insert any previously logged items that haven't been drawn yet
		this._drawUndrawnItems();
		
		// Set initial state
		if (this._INITIALLY_OPEN) {
			this.open();
		} else {
			this.close();
		}
		
		// Set up effects
		$("h3holder").onclick				= function() {
			Log.toggle();
		}
		$("logclearbutton").onclick			= function() {
			Log.clear();
		}
		$("logtogglebutton").onclick		= function() {
			Log.toggle();
		}
	},
	
	/************************************************************************************
	 * LOGGING
	 ***********************************************************************************/
	
	/**
	 * log() logs a message.
	 * 
	 * @param type		{String}	A type for the message. Doubles as the class when
	 * 								rendering.
	 * @param message	{String}	The message to log.
	 */
	log:								function(type, message) {
		
		// Create item
		var item = new LogItem(type, message, (new Date()).time());
		
		// Create item and store it
		this._addItem(item);
		
		// If interface has been prepared
		if (this._status == Admin.OBJECT_STATUS_PREPARED) {
			this._drawItem(item);
		}
	},
	
	/**
	 * clear() clears the currently displayed log.
	 */
	clear:								function() {
		
		// Clear the drawn list
		this._elements.list.innerHTML = "";
		
		// Reset items list
		//this._items = [];
	},
	
	createFeedbackRows:					function(header, details, header_tag) {
		header		= isString(header)		? header		: "Log";
		details		= isObject(details)		? details		: {};
		header_tag	= isString(header_tag)	? header_tag	: "h4";
		
		var str = [];
		for (var i in details) {
			if (isIntegeric(i)) {
				str.push("<tr><td colspan=\"2\">" + details[i] + "</td></tr>");
			} else {
				str.push("<tr><td>" + i + ":</td><td>" + details[i] + "</td></tr>");
			}
		}
		str = "<" + header_tag + ">" + header + ":</" + header_tag + "><table><tbody>" + str.join("") + "</tbody></table>";
		
		return str;
	},
	
	/************************************************************************************
	 * ITEMS
	 ***********************************************************************************/
	
	/**
	 * getItem() gets a particular log item.
	 * 
	 * @param index	{Integer}	The log item number.
	 * 
	 * @return the item if found, false if not.
	 * @type LogItem/Boolean
	 */
	getItem:							function(index) {
		if (isDefined(this._items[index])) {
			return this._items[index];	
		}
		
		return false;
	},
	
	/**
	 * getItems() gets the items array.
	 * 
	 * @return the array of log items.
	 * @type Array
	 */
	getItems:							function() {
		return this._items;
	},
	
	/**
	 * getItemCount() gets the number of items in the items array.
	 * 
	 * @return the number of items.
	 * @type Integer
	 */
	getItemCount:						function() {
		return this._items.length;
	},
	
	/**
	 * _addItem() adds an item to the items array.
	 * 
	 * @param item	{LogItem}	The item to add.
	 */
	_addItem:							function(item) {
		this._items.push(item);
	},
	
	/************************************************************************************
	 * VISIBILITY
	 ***********************************************************************************/
	
	/**
	 * isOpen() returns whether the log is open or not.
	 * 
	 * @return whether the Log is currently opened.
	 * @type Boolean
	 */
	isOpen:								function() {
		return this._open ? true : false;
	},
	
	/**
	 * toggle() toggles the open status of the Log.
	 */
	toggle:								function() {
		
		// Open or close
		if (this.isOpen()) {
			this.close();
		} else {
			this.open();
		}
	},
	
	/**
	 * close() sets the open status of the Log to false.
	 * 
	 * @param suppress_log	{Boolean}	If true, the log entry that is normally added to
	 * 									indicate this action will not be triggered.
	 */
	close:								function(suppress_log) {
		
		// Check logging status
		suppress_log = suppress_log || !this._LOG_OPEN_AND_CLOSE;
		
		// Log spacer
		log("spacer", "&nbsp;");
		
		// Close
		this._open = false;
		
		// Log it?
		if (!suppress_log) {
			log("log", "Log closed");
		}
		
		// Redraw
		this.redraw();
	},
	
	/**
	 * open() sets the open status of the Log to true.
	 * 
	 * @param suppress_log	{Boolean}	If true, the log entry that is normally added to
	 * 									indicate this action will not be triggered.
	 */
	open:								function(suppress_log) {
		
		// Check logging status
		suppress_log = suppress_log || !this._LOG_OPEN_AND_CLOSE;
		
		// Open
		this._open = true;
		
		// Log it?
		if (!suppress_log) {
			log("log", "Log opened");
		}
		
		// Reset unviewed counter
		this._unviewed_items = 0;
		
		// Redraw
		this.redraw();
	},
	
	/************************************************************************************
	 * DRAWING
	 ***********************************************************************************/
	
	/**
	 * _redraw() redraws this element to reflect it's current status, screen size, etc.
	 */
	redraw:								function() {
		
		// Parameters
		var params = {};
		params.open_height		= 400;
		params.title_height		= 30;
		params.height			= this._open ? params.open_height												: params.title_height;
		params.width			= this._open ? Page.simpleGetWindowWidth()										: 400;
		params.x				= this._open ? 0																: Page.simpleGetWindowWidth() - params.width;
		params.y				= this._open ? Page.simpleGetWindowHeight() - params.height						: Page.simpleGetWindowHeight() - params.height;
		
		
		// Update height of elements
		Page.simpleSetHeight(this._elements.title,	params.title_height);
		Page.simpleSetHeight(this._elements.list,	params.height - params.title_height);
		
		// Update overall size and position
		Page.simpleResize(this._elements.log, params.width, params.height);
		Page.simpleMoveTo(this._elements.log, params.x, params.y);
		
		// Update class
		if (this._open) {
			this._elements.log.addClassName("open");
			$("logtogglebutton").innerHTML = "&darr;";
		} else {
			this._elements.log.removeClassName("open");
			$("logtogglebutton").innerHTML = "&uarr;";
		}
		
		// Update title
		this._updateH3();
	},
	
	/**
	 * _drawUndrawnItems() draws any items that have not yet been drawn.
	 */
	_drawUndrawnItems:					function() {
		for (var i = 0; i < this._items.length; i++) {
			if (!this._items[i].getDrawn()) {
				this._drawItem(this._items[i]);
			}
		}
	},
	
	/**
	 * _drawItem() draws a LogItem to the DOM list.
	 * 
	 * @param item	{LogItem}	The item to draw.
	 */
	_drawItem:							function(item) {
		
		// Create element
		var li = $(createElement("li"));
		for (var i = 0; i < item.getType().length; i++) {
			li.addClassName("log" + item.getType()[i]);
		}
		
		
		// Add message
		li.innerHTML = item.getMessage();
		
		// Insert at either beginning or end
		if (this._MOST_RECENT_FIRST) {
			this._elements.list.insertBefore(li, this._elements.list.firstChild);	
		} else {
			this._elements.list.appendChild(li);	
		}
		
		// Indicate item has been drawn
		item.setDrawn(true);
		
		// Update unviewed counter if the log isn't currently open
		if (!this._open) {
			this._unviewed_items++;
		}
		
		// Update title
		this._updateH3();
	},
	
	/**
	 * _updateH3() updates the Log H3.
	 */
	_updateH3:							function() {
		if (this._unviewed_items > 0) {
			$("logheadertext").innerHTML = "Log (" + this._unviewed_items + ")";
		} else {
			$("logheadertext").innerHTML = "Log";
		}
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Loader.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/loader.js
 * FUNCTION:		Contains the Loader object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-18
 ***************************************************************************************/

/**
 * The IncaLoader object handles the Loader box, which provides feedback to the user about
 * actions currently taking place.
 */
var Loader = {

	// Version
	_VERSION:							1.2,
	_CLASS:								"Loader",
	
	// Properties
	_status:							Admin.OBJECT_STATUS_UNINITIALISED,
	_elements:							{},
	_count:								0,
	
	/**
	 * init() initialises the Loader object.
	 */
	init:								function(index) {
		
		// Log
		log("init", "Initialising Loader (v" + this._VERSION + ")");
		
		// Create elements
		this._createElements();
		
		// Update status
		this._status = Admin.OBJECT_STATUS_INITIALISED;
		
		// Append to interface
		Page.insert(index, this._elements.loader);
	},
	
	/**
	 * _createElements() creates the DOM elements for the loader.
	 */
	_createElements:					function() {
		
		// Create text element
		this._elements.text = createElement("div",
			["id", "loadertext"],
			"Loading..."
		);
	
		// Create Loader element
		this._elements.loader = createElement("div",
			["id", "loader"],
			createElement("div",
				["id", "loaderanimholder"],
				createElement("div",
					["id", "loaderanim"],
					this._elements.text
				)
			)
		);
	},
	
	/**
	 * prepare() prepares the Loader for initial usage after it has been inserted into
	 * the page.
	 */
	prepare:							function() {
		
		// Update status
		this._status = Admin.OBJECT_STATUS_PREPARED;
		
		// Draw
		this.redraw();
		
		// Remove any pending loader messages
		this.stop();
	},
	
	/************************************************************************************
	 * MESSAGES
	 ***********************************************************************************/
	
	/**
	 * start() starts to display a message.
	 * 
	 * @param message	{String}	The message to show.
	 */
	start:								function(message) {
		
		// Check message
		message = message || "Loading...";
	
		// Increment counter
		this._count++;
		
		// Load
		log("loader", message);
		
		// Display
		this._show(message);
	},

	/**
	 * stop() stops showing a message. If it is the last message, it will hide the loader.
	 */	
	stop:								function() {
		
		// Decrement counter
		this._count--;
		
		// If finished
		if (this._count <= 0) {
			this._count = 0;
			this._hide();
		}
	},
	
	/************************************************************************************
	 * DRAWING
	 ***********************************************************************************/
	
	/**
	 * redraw() updates the drawing of the loader.
	 */
	redraw:								function() {
		this._elements.loader.style.zIndex = 100;
		
		Page.simpleCentre(this._elements.loader);
	},
	
	/**
	 * _setMessage() sets the current message.
	 */
	_setMessage:						function(message) {
		this._elements.text.innerHTML = message;
	},
	
	/**
	 * _hide() hides the loader.
	 */
	_hide:								function() {
		this._elements.loader.style.display = "none";
	},
	
	/**
	 * _show() shows the loader.
	 */
	_show:								function(message) {
		this._setMessage(message);
		this._elements.loader.style.display = "block";
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Library.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/library.js
 * FUNCTION:		Contains the Library object.
 * CREATOR:			Cam Morrow
 * CREATED:			2008-07-17
 ***************************************************************************************/

/**
 * The Library class handles interactions with the common image library.
 * 
 * @creator Cam Morrow
 */
var Library = {
	
	// Constants
	_VERSION:										1.0,
	_CLASS:											"Library",
	
	_path:											"../admin/assets/images/library/",
	
	getImage:										function(filename, width, height, alt) {
		alt = alt || "";
		
		var image = createElement("img",
			["src", this.getImageURL(filename, width, height)],
			["alt", alt]
		).addClassName("library");
		
		return image;
	},
	
	getImageURL:									function(filename, width, height) {
		width	= width || 16;
		height	= height || width;
		
		return this._path + width + "x" + height + "/" + filename + ".png";
	}
};

/*** @inca-include ../admin/assets/scripts/objects/Popups.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/popups.js
 * FUNCTION:		Contains the Popups object and Popup class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-23
 ***************************************************************************************/

/**
 * The Popups object controls display of popups.
 */
var Popups = {
 	
 	// Constants
	_VERSION:							1.0,
	_CLASS:								"Popups",
	HEADER_HEIGHT:						26,
	BUTTONS_HEIGHT:						35,
	
	// Common properties
	_status:							Admin.OBJECT_STATUS_UNINITIALISED,
	_index:								0,
	
	// Properties
	_elements:							{},
	_items:								{},
	_counter:							0,
	_z_index:							1,
	_focused:							false,

	/**
	 * init() initialises the Popups.
	 */
	init:								function(index) {
		
		// Log
		log("init", "Initialising Popups (v" + this._VERSION + ")");
		
		// Create HTML DOM elements
		this._createElements();
		
		// Store Index
		this._index = index;
		
		// Update status
		this._status = Admin.OBJECT_STATUS_INITIALISED;
		
		// Append to interface
		Page.insert(index, this._elements.popups);
	},

	/**
	 * _createElements() creates the necessary DOM elements for the Popups.
	 */
	_createElements:					function() {
		this._elements.popups = createElement("div", ["id", "popups"]);
	},
	
	/**
	 * prepare() preps the Login elements for display.
	 */
	prepare:							function() {
		
		// Update status
		this._status = Admin.OBJECT_STATUS_PREPARED;
		
		// Draw
		this.redraw();
	},
	
	/************************************************************************************
	 * POPUPS
	 ***********************************************************************************/
	
	/**
	 * getUniqueID() gets a unique ID for a popup via an MD5 hash.
	 * 
	 * @return an MD5 hash derived from the length of the current popup list.
	 * @type String
	 */
	getUniqueID:						function() {
		//return md5("popup" + this._counter);
		return "popup" + this._counter;
	},
	
	/**
	 * make() makes a new popup.
	 * 
	 * @param content	{String}	The initial content for the popup.
	 * @param params	{Object}	The parameters for the popup.
	 */
	make:								function(content, params) {
		return this.open(new Popup(content, params));
	},
	
	/**
	 * open() opens a popup.
	 * 
	 * @param popup	{Popup}	The popup object to open on the page.
	 * 
	 * @return the Popup object ready for further use.
	 * @type Popup
	 */
	open:								function(popup) {
		
		// Store popup
		this._items[popup.getID()] = popup;
		
		// Draw
		this._elements.popups.appendChild(popup.getElement());		
		
		// Set z-index
		popup.getElement().style.zIndex = this._z_index;
		
		// Check it's not exactly on top of an existing one...
		while (!this._positionIsUnique(popup)) {
			popup.moveBy(10, 10);
		}
		
		// Store focused
		this._focused = popup;

		// Focus button
		popup.focusButton();
		
		// Increment counter and z_index
		this._counter++;
		this._z_index++;
		
		// Return popup for use
		return popup;
	},
	
	/**
	 * close() closes a popup.
	 * 
	 * @param popup_id	{String}	The ID of the popup to close.
	 */
	close:								function(popup_id) {
		
		// If the element exists
		if (this._items[popup_id]) {
			this._elements.popups.removeChild(this._items[popup_id].getElement());
			this._items[popup_id] = null;
		} else {
			Alerts.error("Popup '" + popup_id + "' does not exist, cannot remove.", this, "close");
		}
	},
	
	/**
	 * get() gets an existing popup.
	 * 
	 * @param popup_id	{String}	The ID of the popup to find.
	 */
	get:								function(popup_id) {
		
		// If the element exists
		if (this._items[popup_id]) {
			return this._items[popup_id];
		} else {
			return false;
		}
	},
	
	/**
	 * _positionIsUnique() returns whether the X/Y coords of a passed popup are not
	 * currently occupied by another popup.
	 * 
	 * @param popup	{Popup}	The popup to check the uniqueness of.
	 * 
	 * @return whether the popup's position is unique.
	 * @type Boolean
	 */
	_positionIsUnique:					function(popup) {

		var unique_position = popup.getCurrentPosition();
		//var unique_position = {x: 0, y: 0};

		for (var i in this._items) {
			if (isObject(this._items[i])) {
				if (this._items[i].getID() != popup.getID()) {			
					var position = this._items[i].getCurrentPosition();
					//var position = {x: 0, y: 0};
					
					if (position.x == unique_position.x && position.y == unique_position.y) {
						return false;
					}
				}
			}
		}
		
		return true;
	},
	
	/**
	 * moveToTop() moves a popup to the top of the popups.
	 * 
	 * @param popup	{Popup}	The Popup object to move to the top.
	 */
	moveToTop:							function(popup) {
		
		// If not already at the top
		if (this._focused != popup) {
		
			// Set new z-index
			popup.getElement().style.zIndex = this._z_index;
		
			// Increment z-index
			this._z_index++;
			
			// Store focused
			this._focused = popup;
		}
	},
	
	/***********************************************************************************
	 * DRAWING
	 ***********************************************************************************/
	
	/**
	 * redraw() redraws the Popup div.
	 */
	redraw:								function() {		
		for (var i in this._items) {
			if (isObject(this._items[i])) {
				this._items[i].redrawModalBase();
				this._items[i].updatePosition();
			}
		}
	},
	
	/**
	 * getPopupObject()
	 */
	getPopupObject:						function(element) {
		
		// Get element with the ID of the popup
		var popup_element = this.getPopupBaseDiv(element);
		
		// Try and retrieve it
		if (false !== popup_element) {
			return this.get(popup_element.id);
		}
		
		// Fail
		return false;
	},
	
	/**
	 * getPopupDiv() gets the popup div when passed an element inside the popup.
	 * 
	 * @param element	{DOM Element}	The element inside the popup.
	 * 
	 * @return the <div> element of the Popup that the element was part of.
	 * @type DOM Element
	 */
	getPopupDiv:						function(element) {
		while (element.parentNode) {
			if ($(element).hasClassName("popup")) {
				return element;
			} else {
				element = element.parentNode;
			}
		}
		
		return false;
	},
	
	/**
	 * getPopupBaseDiv() gets the popup base div when passed an element inside the popup.
	 * 
	 * @param element	{DOM Element}	The element inside the popup.
	 * 
	 * @return the <div> element of the Popup Base that the element was part of.
	 * @type DOM Element
	 */
	getPopupBaseDiv:					function(element) {
		
		while (element.parentNode) {
			if ($(element).hasClassName("popupbase")) {
				return element;
			} else {
				element = element.parentNode;
			}
		}
		
		return false;
	},
	
	/***********************************************************************************
	 * COMMON TYPE CREATION
	 ***********************************************************************************/
	
	makeConfirm:						function(content, ok_onclick, cancel_onclick, submit_popup_inputs, additional_params) {
		
		// Check onclick strings
		cancel_onclick		= isString(cancel_onclick) ? cancel_onclick : "";
		cancel_onclick		+= ";Popups.getPopupObject(this).close();";
		ok_onclick			= isString(ok_onclick) ? ok_onclick : "";
		ok_onclick			+= ";Popups.getPopupObject(this).close();";
		submit_popup_inputs	= submit_popup_inputs ? true : false;
		additional_params	= isObject(additional_params) ? additional_params : {};
		
		// Add popup input submission if required
		if (submit_popup_inputs) {
			ok_onclick	= "Popups.getPopupObject(this).addInputsToRequest();" + ok_onclick;
		}
		
		// Create parameters for popup
		var params = {
			type:			["alert", "requestconfirmation"],
			title:			"Confirmation Required",
			modal:			true,
			buttons:		[
				new PopupButton("ok", {
					label:		"OK",
					width:		150,
					on_click:	ok_onclick
				}),
				new PopupButton("cancel", {
					label:		"Cancel",
					width:		150,
					on_click:	cancel_onclick
				})
			]
		};
		
		// Apply additional parameters
		for (var i in additional_params) {
			params[i] = additional_params[i];
		}
		
		return this.make(content, params);
	},
	
	makeConfirmEncoded:					function(content, ok_onclick, cancel_onclick, submit_popup_inputs) {
		return this.makeConfirm(unescape(content), unescape(ok_onclick), unescape(cancel_onclick), submit_popup_inputs);
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Mouse.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/mouse.js
 * FUNCTION:		Contains the Mouse object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-24
 ***************************************************************************************/

/**
 * The Mouse object tracks mouse movements, clicking, etc.
 * 
 * @creator Cameron Morrow
 */
var Mouse = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Mouse",
	
	// Properties
	_current_pos:						{},
	
	/**
	 * init() initialises the Mouse object.
	 */
	init:								function() {
		
		// Log
		log("init", "Initialising Mouse (v" + this._VERSION + ")");
		
		// Start tracking DOM events
		document.onmousemove		= Mouse.moveEvent;
		document.onmouseup			= Mouse.upEvent;
	},
	
	/**
	 * upEvent() is called when the mouseup event is fired.
	 * 
	 * @param event	{Event}	On some browsers, an event object is passed with mouse
	 * 						information. On others, a window.event global exists.
	 */
	upEvent:							function(event) {
		
		// Check event
		event = event || window.event;
		
		// Unset dragging
		Drag.stopDrag();
	},
	
	/**
	 * moveEvent() is called when the mousemove event is fired.
	 * 
	 * @param event	{Event}	On some browsers, an event object is passed with mouse
	 * 						information. On others, a window.event global exists.
	 */
	moveEvent:							function(event) {
		
		// Check event
		event = event || window.event;
		
		// Get coordinates
		Mouse._current_pos = Mouse._getCoordsFromEvent(event);
		
		// Track dragging
		Drag.doDragging();
	},
	
	/**
	 * _getCoordsFromEvent() gets the current mouse coords at the passed event.
	 * 
	 * @param event	{Event}	The event to get the coords from.
	 * 
	 * @return an object with x and y properties.
	 * @type Object
	 */
	_getCoordsFromEvent:				function(event) {
		if (event.pageX && event.pageY) {
			return {
				x: event.pageX,
				y: event.pageY
			};
		} else if (event.clientX && event.clientY) {
			return {
				x: event.clientX + document.body.scrollLeft - document.body.clientLeft,
				y: event.clientY + document.body.scrollTop  - document.body.clientTop
			};
		} else {
			return {
				x: 0,
				y: 0
			};
		}
	},
	
	/**
	 * getCurrentPosition() gets the current position of the mouse (or, more accurately,
	 * the position of the mouse at the last triggered event.
	 */ 
	getCurrentPosition:					function() {
		return this._current_pos;
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Drag.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/drag.js
 * FUNCTION:		Contains the Drag object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-24
 ***************************************************************************************/

/**
 * The Drag object handles dragging DOM elements around.
 * 
 * @creator Cameron Morrow
 */
var Drag = {
	
	// Constants
	_VERSION:							1.0,
	_DRAG:								"Drag",
	
	// Properties
	_dragging:							false,
	_drag_start_pos:					{x: 0, mouse_x: 0, mouse_y: 0, y: 0},
	_boundaries:						{top: 0, left: 0, right: 0, bottom: 0},
	
	/**
	 * init() initialises the Drag object.
	 */
	init:								function() {
		
		// Log
		log("init", "Initialising Drag (v" + this._VERSION + ")");
	},
	
	/**
	 * setDraggable makes an item draggable. Note that this is only for objects who are
	 * themselves moved when they are clicked. Elements which act only as a handle to a
	 * larger draggable element should set up their own mousedown methods.
	 * 
	 * @param object	{DOM Element}	The element to be draggable.
	 */
	setDraggable:						function(object) {
		object.onmousedown = function() {
			Drag.startDrag(this);
		}
	},
	
	/**
	 * unsetDraggable makes an item not draggable any more.
	 */
	unsetDraggable:						function(object) {
		object.onmousedown = function() {};
	},
	
	/**
	 * startDrag() starts the dragging of a passed DOM element.
	 * 
	 * @param object		{DOM Element}	The element to be draggable.
	 * @param boundaries	{Object}		The boundaries of where the element should
	 * 										be allowed to be dragged.
	 */
	startDrag:							function(object, boundaries) {
		
		// If not currently dragging
		if (!this.isDragging()) {
			
			// Store dragging object, boundaries, and initial mouse position
			this._dragging			= object;
			this._boundaries		= boundaries || {top: 0, left: 0, right: 0, bottom: 0};
			this._drag_start_pos	= {
				mouse_x:	Mouse.getCurrentPosition().x,
				mouse_y:	Mouse.getCurrentPosition().y,
				x:			Page.simpleGetX(object),
				y:			Page.simpleGetY(object)
			};
		}
	},
	
	/**
	 * stopDrag() stops the current dragging.
	 */
	stopDrag:							function() {
		if (this.isDragging()) {
			this._dragging			= false;
			this._drag_start_pos	= {x: 0, mouse_x: 0, mouse_y: 0, y: 0};
		}
	},
	
	/**
	 * doDragging() executes an update to the current dragging (if applicable). It
	 * derives the mouse movement offset, and updates the position of the dragged
	 * element.
	 */
	doDragging:							function() {
		if (this.isDragging()) {
			
			// Get mouse offset from initial value
			var offset = {
				x: Mouse.getCurrentPosition().x - this._drag_start_pos.mouse_x,
				y: Mouse.getCurrentPosition().y - this._drag_start_pos.mouse_y
			};
			
			// Get new X/Y values of object
			var position = {
				x: this._drag_start_pos.x + offset.x,
				y: this._drag_start_pos.y + offset.y
			};
			
			// Validate position against boundaries
			var proposed = this.validatePosition(this._dragging, position, this._boundaries);
			
			// Position element
			Page.simpleMoveTo(this._dragging, proposed.left, proposed.top);
			
			// Deselect any text
			Drag._deselectText();
		}
	},
	
	validatePosition:					function(object, position, boundaries) {
		
		// Validate against boundaries
		var proposed = {
			top:		position.y,
			bottom:		position.y + Page.simpleGetHeight(object),
			left:		position.x,
			right:		position.x + Page.simpleGetWidth(object)
		};
		
		if (proposed.top < boundaries.top) {
			proposed.top	= boundaries.top;
			proposed.bottom	= boundaries.top + Page.simpleGetHeight(object);
		}
		if (proposed.left < boundaries.left) {
			proposed.left	= boundaries.left;
			proposed.right	= boundaries.left + Page.simpleGetWidth(object);
		}
		if (proposed.bottom > boundaries.bottom) {
			proposed.bottom	= boundaries.bottom;
			proposed.top	= boundaries.bottom - Page.simpleGetHeight(object);
		}
		if (proposed.right > boundaries.right) {
			proposed.right	= boundaries.right;
			proposed.left	= boundaries.right - Page.simpleGetWidth(object);
		}
		
		return proposed;
	},
	
	/**
	 * isDragging() returns whether a drag is currently underway.
	 */
	isDragging:							function() {
		return this._dragging !== false;
	},
	
	/**
	 * _deselectText() attempts to deselect any selected text while dragging, so as to
	 * avoid the unsightly appearance of text being selected while dragging.
	 */
	_deselectText:						function() {
		if (document.selection) {
			document.selection.empty();
		} else if (window.getSelection) {
			window.getSelection().removeAllRanges();
		}
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Request.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/request.js
 * FUNCTION:		Contains the Request object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-24
 ***************************************************************************************/

// Register global responders that will occur on all AJAX requests
Ajax.Responders.register({
	
	/**
	 * onCreate() sets up the time out tracker when a request isc reated.
	 * 
	 * @param request	{Object}	The request that is being made.
	 */
	onCreate: function(request) {
		
		// Store the timeout in the request so that it can be removed later
		request["timeout_obj"] = window.setTimeout(
			function() {
				
				// If we have hit the timeout and the AJAX request is active, abort it and let the user know
				if (Request.callInProgress(request.transport)) {
					
					// Stop
					request.transport.abort();
					
					// Display failure
					Request.timeoutFailure();
					
					// Run the onFailure method if we set one up when creating the AJAX object
					if (request.options["onFailure"]) {
						request.options["onFailure"](request.transport, request.json);
					}
				}
			},
			30000 // 30 seconds
		);
	},
	
	/**
	 * onComplete() is triggered when a Request completes. This override clears the timeout trigger.
	 * 
	 * @param request	{Object}	The Request that is being made.
	 */
	onComplete: function(request) {
		
		// Clear the timeout; the request completed ok
		window.clearTimeout(request["timeout_obj"]);
	}
});

/**
 * The Request object handles making and processing AJAX requests.
 * 
 * @creator Cameron Morrow
 */
var Request = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Request",
	_DEFAULT_METHOD:					"post",
	
	// Properties
	_url:								"",
	_requests:							[],
	_extra_params:						{},
	
	/******************************************************
	 * TIMEOUT ENHANCEMENTS TO PROTOTYPE
	 *****************************************************/
	
	/**
	 * 
	 */
	callInProgress:						function(xmlhttp) {
		
		// Check the state
		switch (xmlhttp.readyState) {
			case 1:
			case 2:
			case 3:
				return true;
				break;
			// Case 4 and 0
			default:
				return false;
				break;
		}
	},
	
	/**
	 * 
	 */
	timeoutFailure:						function() {
		Alerts.error("Sorry, the request timed out. Please try again later.", this, "timeoutFailure", true);
	},
	
	resetExtraParams:					function() {
		this._extra_params = {};
	},
	
	setExtraParams:						function(extra_params) {
		if (isObject(extra_params)) {
			this._extra_params = extra_params;
		}
	},
	
	addExtraParam:						function(key, value) {
		this._extra_params[key] = value;
	},
	
	mergeExtraParams:					function(extra_params) {
		if (isObject(extra_params)) {
			this._extra_params = Objects.merge(this._extra_params, extra_params);
		}
	},
	
	/******************************************************
	 * REQUEST MAKING
	 *****************************************************/
	
	/**
	 * init() initialises the Request object.
	 */
	init:								function() {
	
		// Log
		log("init", "Initialising Request (v" + this._VERSION + ")");
		
		// Define the URL
		this._url = Admin.getProperty("SITE_ADMIN_URL") + Admin.getProperty("AJAX_PATH");
	},
	
	/**
	 * make() makes an AJAX request.
	 * 
	 * @param mode		{String}	The mode to submit to the request.
	 * @param params	{Object}	An object containing properties to submit in the
	 * 								request.
	 * @param success	{Function}	The method to call if the Request succeeds.
	 * @param failure	{Function}	The method to call if the Request fails.
	 * @param method	{String}	The request method - 'get' or 'post'.
	 */
	make:								function(mode, params, success, failure, method, message) {
		
		// Validate arguments
		mode		= mode		|| "";
		params		= params	|| {};
		//success		= success	|| Request._genericSuccess;
		success		= success	|| false;
		failure		= failure	|| Request._genericFailure;
		method		= method	|| this._DEFAULT_METHOD;
		message		= message	|| "Loading...";
		
		// Add extra params
		if (isObject(this._extra_params)) {
			params	= Objects.merge(this._extra_params, params);
		}
		
		// Reset extra params for next request
		this.resetExtraParams();
		
		// Add mode to parameters
		params.mode				= mode;
		params.component_page	= ComponentInterface.getActivePageID();
		params.output_mode		= 4;
		
		// Verify mode
		if (params.mode.length > 0) {
			
			// Store back-linking details
			params._success_method		= success;
			
			// Store request details
			params.request_id			= this._store(params, success, failure, method);
			
			
			var full_url = this.makeURL(params.mode, params);
			
			// Log
			log("ajax", Log.createFeedbackRows("AJAX Request", {
				ID:		params.request_id,
				Mode:	params.mode,
				URL:	full_url +
							" <a class=\"xmllink\" href=\"" + full_url + "&output_mode=1\" target=\"_blank\">XML</a>" +
							" <a class=\"xmllink\" href=\"" + full_url + "&output_mode=4\" target=\"_blank\">JSON</a>" +
							" <a class=\"xmllink\" href=\"" + full_url + "&output_mode=5\" target=\"_blank\">PHP</a>",
				Retry:	"<button type=\"button\" onclick=\"Request.makeFromStored(" + params.request_id + ");\"><small>Retry Request</small></button>"
			}));
			
			// Loader
			Loader.start(message);
			
			// Request
			new Ajax.Request(this._url, {
				method:			method,
				parameters:		params,
				onSuccess:		Request._genericSuccess,
				onFailure:		failure
			});
		} else {
			Alerts.error("No mode was passed to the request.", this, "make", true);
		}
	},
	
	/**
	 * makeURL() generates a (sample) URL of a Request, for logging purposes.
	 * 
	 * @param mode		{String}	The mode to be sent.
	 * @param params	{Object}	The parameters to be sent.
	 * @param skip_js	{Boolean}	Whether to omit properties that exist only for use in
	 * 								the Javascript - prefixed with an underscore.
	 * 
	 * @return a URL.
	 * @type String
	 */
	makeURL:							function(mode, params, skip_js) {
		
		// Check arguments
		skip_js					= isBoolean(skip_js)	? skip_js	: true;
		params					= isObject(params)		? params	: {};
		params.mode				= isString(mode)		? mode		: "";
		params.component_page	= ComponentInterface.getActivePageID();
		
		// Generate parameters
		var req = [];
		for (var i in params) {
			if (isString(i) || isIntegeric(i)) {
				if (!skip_js || String(i).charAt(0) != "_") {
					req.push(i + "=" + escape(params[i]));
				}
			}
		}
		
		// Compile URL
		return this._url + "?" + req.join("&");
	},
	
	/**
	 * makeFromStored() makes a request from a set of stored parameters.
	 * 
	 * @param id	{Mixed}	The ID of the request.
	 */
	makeFromStored:						function(id) {

		// If request is found
		if (isObject(this._requests[id])) {
			this.make(this._requests[id].params.mode, this._requests[id].params, this._requests[id].success, this._requests[id].failure, this._requests[id].method);
		} else {
			Alerts.error("No stored request with ID " + id, this, "makeFromStored");
		}
	},
	
	/**
	 * getStored() gets stored request information.
	 * 
	 * @param id	{Mixed}	The ID of the stored request data.
	 * 
	 * @return the data if found, false if not.
	 * @type Object/Boolean
	 */
	getStored:							function(id) {
		if (isObject(this._requests[id])) {
			return this._requests[id];
		} else {
			Alerts.error("No stored request with ID " + id, this, "getStored");
		}
		
		return false;
	},
	
	/**
	 * getStoredParams() retrieves Request Parameters.
	 * 
	 * @param id	{Mixed}	The ID of the stored request data.
	 * 
	 * @return the parameters if found, an empty object if not.
	 * @type Object
	 */
	getStoredParams:					function(id) {
		var stored = this.getStored(id);
		
		if (stored) {
			return stored.params;
		}
		
		return {};
	},
	
	/**
	 * getStoredParam() gets the value of a single parameter in a stored request.
	 * 
	 * @param id	{Mixed}		The ID of the request.
	 * @param param	{String}	The parameter to get the value of.
	 * 
	 * @return the value of the parameter if found, false if not.
	 * @type Mixed
	 */
	getStoredParam:						function(id, param) {
		
		// Get parameters
		var params = this.getStoredParams(id);
		
		// Return
		return isDefined(params[param]) ? params[param] : false;
	},
	
	/**
	 * getStoredParamPage() gets the value of the _page parameter in a stored request.
	 * 
	 * @param id	{Mixed}	The ID of the request.
	 * 
	 * @return the value of the _page parameter if found, false if not.
	 * @type String/Boolean
	 */
	getStoredParamPage:					function(id) {
		return this.getStoredParam(id, "_page");
	},
	
	/**
	 * store() stores properties of a request so that the request can be re-made again at
	 * a later point.
	 * 
	 * @param params	{Object}	Parameters to pass to the request.
	 * @param success	{Function}	The function to call if the request is made
	 * 								successfully.
	 * @param failure	{Function}	The function to call if the request fails.
	 * @param method	{String}	The method to submit the parameters with.
	 * 
	 * @return the ID of the stored request, to allow the data to be retrieved later.
	 * @type Integer
	 */
	_store:								function(params, success, failure, method) {
		
		// Get ID
		var id = this._requests.length;
		
		// Store ID
		params.request_id = id;
		
		// Store information
		this._requests[id] = {
			params:		params,
			success:	success,
			failure:	failure,
			method:		method
		};
		
		// Return
		return id;
	},
	
	/**
	 * compileMode() attempts to compile a mode parameter from an AJAX request from a
	 * passed component, page, and action.
	 * 
	 * @param component	{String}	The component part of the mode.
	 * @param page		{String}	The page part of the mode.
	 * @param action	{String}	The action part of the mode.
	 * 
	 * @return a mode string.
	 * @type String
	 */
	compileMode:						function(component, page, action) {
		
		// Get default values
		component	= component	|| "";
		page		= page		|| "";
		action		= action	|| "";
		
		// Create a mode
		var mode = [component];
		if (page && page.length > 0) {
			mode.push(page);
		}
		if (action && action.length > 0) {
			mode.push(action);
		}
		
		// Return compiled mode
		return mode.join(".");
	},
	
	/**
	 * _genericSuccess() is a generic AJAX response function, to be used when an AJAX
	 * call does not yet have a correct method to call when it succeeds.
	 * 
	 * @param transport	{Object}	The XML request object.
	 * @param json		{Object}	The returned JSON object (if any).
	 */
	_genericSuccess:					function(transport, json) {
		Loader.stop();
		var json = Request.validateJSON(transport, json);
		if (json) {
			
			// Get stored data
			var params = Request.getStoredParams(json.request_id);
			
			// Show Messages
			Request.showJSONMessages(json);
			
			// Pass off to requested method
			if (isFunction(params._success_method)) {
				log("ajax conf", "Request validated, passing off to custom success method.");
				params._success_method(transport, json);
			} else {
				log("ajax conf", "No custom success method was specified for the Request.");
			}
		} else {
			Alerts.phpError("<p>Response did not return valid JSON:</p><hr />" + transport.responseText + "<hr /><code>" + transport.responseText.split(">").join("&gt;").split("<").join("&lt;") + "</code>");
		}
	},
	
	/**
	 * _genericFailure() is a generic AJAX response function, to be used when an AJAX
	 * call does not yet have a correct method to call when it fails.
	 * 
	 * @param transport	{Object}	The AJAX request object.
	 */
	_genericFailure:					function(transport) {
		Loader.stop();
		log("ajax error", "AJAX request failed.");
	},
	
	_genericException:					function(response) {
		//Alerts.error("<strong>PHP Error</strong>" + response.transport.responseText, this, "_genericException", true);
		Alerts.phpError(response.transport.responseText);
	},
	
	/************************************************************************************
	 * JSON PARSING
	 ***********************************************************************************/
	
	/**
	 * validateResponseText() validates a responseText in a transport to see if we can
	 * get JSON out of it.
	 * 
	 * @param transport	{Object}	The XMLHTTPRequest object.
	 * 
	 * @return the evaluated JSON object if found, null if not.
	 * @type Object/null
	 */
	validateResponseText:				function(transport) {
		
		if (transport) {
			if (transport.responseText) {

				// Establish 'failed' text.
				var text = null;
				
				// Attempt to evaluate as JSON
				try {
					eval("var text = (" + transport.responseText + ")");
				} catch (error) {}
				
				return text;
			}
		}
		
		return null;
	},
	
	/**
	 * validateJSON() checks a passed AJAX JSON response to see if it is valid.
	 * 
	 * @param transport				{Object}	The XMLHTTPRequest object.
	 * @param json					{Object}	The JSON object to check.
	 * @param suppress_login_check	{Boolean}	Whether to ignore the user's login status.
	 * 
	 * @return true if the object is a valid one, false if not.
	 * @type Boolean
	 */
	validateJSON:						function(transport, json, suppress_login_check) {
		
		// Check whether we are blocking the login check
		suppress_login_check = suppress_login_check || false;
		
		// If not JSON is passed directly, check the transport responseText instead
		if (!isObject(json)) {
			json = Request.validateResponseText(transport);
		}
		
		if (isObject(json)) {
			if (isBoolean(json.success) && isIntegeric(json.code) && isArray(json.messages) && isDefined(json.request_id)) {
				
				log("ajax ajaxresponse", Log.createFeedbackRows("Valid AJAX Response receieved", {
					ID:			json.request_id,
					Success:	json.success + "; " + json.code,
					Messages:	json.messages.length
				}));
				
				// If failed because no log in
				if (json.success === false && json.code == 300 && !suppress_login_check) {
					Administrator.startLogin(json.request_id);
				}
				
				return json;
			} else {
				log("ajax error", "JSON object does not contain <em>success</em>, <em>code</em>, and/or <em>type</em> properties.");
			}
		} else {
			log("ajax error", "Response did not return a JSON object.");
		}
		
		return false;
	},
	
	/**
	 * showJSONMessages() shows messages that were stored in the JSON.
	 * 
	 * @param json	{Object}	The JSON object to look in.
	 */
	showJSONMessages:					function(json) {
		
		// Cache responses so that they can be displayed in one popup
		Alerts.startCachingAlerts("JSON Messages");
		
		// Messages
		if (isObject(json.messages)) {
			for (var i = 0; i < json.messages.length; i++) {
				Alerts.jsonMessage(json.messages[i]);
			}
		}
		
		// Errors
		if (isObject(json.errors)) {
			for (var i = 0; i < json.errors.length; i++) {
				Alerts.jsonError(json.errors[i]);
			}
		}
		
		// Display
		Alerts.displayCachedAlerts();
	},
	
	/************************************************************************************
	 * PAGE DATA PARSING
	 * These methods are designed especially for handling Page Action requests/responses.
	 ***********************************************************************************/
	
	/**
	 * handlePageDataResponse() handles a response to a JSON request that contains a
	 * _page (ComponentPage) property and values to be applied.
	 * 
	 * @param json	{Object}	The JSON object.
	 * 
	 * @return true if the json data was in the expected format and applied successfully,
	 * false if not.
	 * @type Boolean
	 */
	handlePageDataResponse:				function(json) {
		
		// Get stored data
		var params = this.getStoredParams(json.request_id);
		
		// If page parameter exists
		if (params._page) {
			Alerts.track("Identified page object, applying values...", this, "handlePageDataResponse");

			try {
				if (isObject(json.data.items)) {
					params._page.applyStructure(json.data.items);
				}
			} catch (e) {
				Alerts.displayException(e);
			}
			
			try {
				params._page.applyValues(json.data.values);
			} catch (e) {
				Alerts.displayException(e);
			}
			
			params._page.activateTab(json.focus);
			Alerts.track("Item values applied successfully.", this, "handlePageDataResponse");
			Page.resizeElements();
			
			// If loading a new page...
			if (isString(json.page_focus)) {
				INCA2Components.clickListPage(params._page.getComponent().getID(), json.page_focus);
			}
			
			return true;
		}
		
		// Fail
		return false;
	},
	
	getPageDataResponsePage:			function(json) {
		
		// Get stored data
		var params = this.getStoredParams(json.request_id);
		
		// Get page
		return isObject(params._page) ? params._page : false;
	},
	
	/************************************************************************************
	 * FORM VALUE PARSING
	 * These methods are used to extract data from form elements for submitting
	 ***********************************************************************************/
	
	/**
	 * _getInputPathValue() takes an input name (which can include array designation, ie
	 * input[a][b]) and generates an object with this path, and the value at the end.
	 * 
	 * @param name_path
	 * @param value
	 * 
	 * @return an object containing the path to the value.
	 * @type Object
	 * 
	 * @example, name_path = input[a][b] and value = 'c', returns:
	 * {input: {a: {b: 'c'}}}
	 */
	_getInputPathValue:					function(name_path, value) {
		
		// Create path
		var path_value = {};
		
		// Break up name path
		var name_path_split = name_path.split("[");
		
		if (name_path_split.length == 1) {
			path_value[name_path] = value;
		} else {
			
			// Trim trailing ']'s
			for (var i = 1; i < name_path_split.length; i++) {
				name_path_split[i] = name_path_split[i].split("]").join("");
			}
			
			// Create path as object
			var current_level = path_value;
			while (name_path_split.length > 0) {
				
				// Extract and remove top path item
				var first = name_path_split.shift();
				
				// Create item
				if (name_path_split.length > 0) {
					current_level[first] = {};
					current_level = current_level[first];
				} else {
					current_level[first] = value;
					break;
				}
			}
		}
		
		// Done
		return path_value;
	},
	
	/**
	 * getInputValues() gets the submit values of an array of HTML inputs (and selects,
	 * etc).
	 * 
	 * @param inputs	{Array}	An array of input elements.
	 * 
	 * @return an object containing the item ID/names and values. Array-named inputs
	 * (input[a][b]) are treated appropriately with sub items.
	 * @type Object
	 */
	getInputValues:						function(inputs) {
		
		// Store values
		var values = {};
		
		// Identify and mark non <input> tags
		for (var i = 0; i < inputs.length; i++) {
			var type = inputs[i].tagName.toLowerCase();
			switch (type) {
				case "input":
					if (inputs[i].hasAttribute("type")) {
						inputs[i]._type = inputs[i].getAttribute("type").toLowerCase();
					} else {
						inputs[i]._type = "text";
					}
					break;
				case "select":
				case "textarea":
					inputs[i]._type = type;
					break;
				default:
					// Bad; should be ignored somehow
					break;
			}
		}
		
		// Extract and store
		for (var i = 0; i < inputs.length; i++) {
	
			// Get ID, name and type of input
			var input_id	= inputs[i].id						? inputs[i].id						: false;
			var input_name	= inputs[i].name					? inputs[i].name					: input_id;
			var type		= inputs[i]._type					? inputs[i]._type					: false;
	
			// If we got an identifier from somewhere		
			if (input_name && type) {
	
				switch (type) {
	
					// Text input
					case "text":
					case "hidden":
					case "password":
					case "select":
					case "textarea":
						values = Objects.merge(values, this._getInputPathValue(input_name, $F(inputs[i])));
						break;
						
					// Checkboxes and Radio Buttons
					case "checkbox":
					case "radio":
						if (inputs[i].checked) {
							
							// Attempt to get value
							var value = $F(inputs[i]) || true;
							
							// Attach value
							values = Objects.merge(values, this._getInputPathValue(input_name, value));
						}
						break;
				}
			} else {
				Alerts.error("Missing input name or type: " . input_id, this, "getInputValues");
			}
		}
		
		return values;
	},
	
	/**
	 * getInputValuesIn() gets the values of inputs in the passed element in an object,
	 * ready to be submitted.
	 * 
	 * @param element	{DOMElement}	The element to get the inputs in.
	 * 
	 * @return an object containing the inputs and their values.
	 * @type Object
	 */
	getInputValuesIn:					function(element) {
		
		// Get inputs
		var inputs	= element.getElementsByTagName("input");
		var selects	= element.getElementsByTagName("select");
		var areas	= element.getElementsByTagName("textarea");
		var items	= [];
		
		// Merge
		for (var i = 0; i < inputs.length; i++) {
			items.push(inputs[i]);
		}
		for (var i = 0; i < selects.length; i++) {
			items.push(selects[i]);
		}
		for (var i = 0; i < areas.length; i++) {
			items.push(areas[i]);
		}
		
		// Get values
		return this.getInputValues(items);
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Alerts.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/alerts.js
 * FUNCTION:		Contains the Alerts object, which handles Error and Message feedback.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-09-26
 ***************************************************************************************/

/**
 * The Alerts object handles user feedback in the form of log entries and popups. It
 * replaces the roles of the old Messages and Errors objects, and is accessed directly
 * by AJAX JSON responses.
 * 
 * @creator Cameron Morrow
 */
var Alerts = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Alerts",
	SOURCE_JS:							"js",
	SOURCE_JSON:						"json",
	SOURCE_MESSAGE:						"message",
	SOURCE_ERROR:						"error",
	SOURCE_PHP:							"php",
	TYPE_PHP:							"php",
	TYPE_FATAL:							"fatal",
	TYPE_ERROR:							"error",
	TYPE_WARNING:						"warning",
	TYPE_INFO:							"info",
	TYPE_CONFIRMATION:					"confirmation",
	TYPE_UNKNOWN:						"unknown",
	POPUP:								true,
	NO_POPUP:							false,
	
	// Properties
	_status:							Admin.OBJECT_STATUS_UNINITIALISED,
	_cached_alerts:						[],
	_cached_alert_title:				"",
	_caching_alerts:					false,
	
	/**
	 * init() intialises the Alerts object.
	 */
	init:								function() {
		
		// Log
		log("init", "Initialising " + this._CLASS + " (v" + this._VERSION + ")");
		
		// Update status
		this._status = Admin.OBJECT_STATUS_INITIALIZED;
	},
	
	/**
	 * _make() makes an Alert.
	 * 
	 * @param obj	{Object}	Parameters for the Alert.
	 */
	_make:								function(obj) {
		
		// If caching
		if (this.isCachingAlerts()) {
			this.cacheAlert(obj);
			
		// Othewise, display now
		} else {
		
			// Log
			this._logAlert(obj);
			
			// Popup
			if (obj.popup == this.POPUP) {
				this._popupAlert(obj);
			}
		}
	},
	
	/**
	 * _logAlert() creates a log entry for an Alert object.
	 * 
	 * @param obj	{Object}	The alert Object.
	 */
	_logAlert:							function(obj) {
		
		// Get class for the log
		var log_class = (obj.type.getValues()).pushAndReturn("alert");
		
		// Output fields
		var output = {
			Message:	obj.message,
			Method:		obj.class_name + (obj.source.has("js") ? "." : "::") + obj.method_name
		};
		
		if (obj["line"] && obj["file"]) {
			output["File"] = obj["file"] + ", line " + obj["line"];
		}
		
		// Create log entry
		log(log_class, Log.createFeedbackRows(this._getAlertTitle(obj), output));
	},
	
	/**
	 * _popupAlert() makes a popup of the Alert.
	 * 
	 * @param obj	{Object}	The alert object.
	 */
	_popupAlert:						function(obj) {
		
		// Add alert class
		var popup_class = (obj.type.getValues()).pushAndReturn("alert");
		
		// Set message up
		var message = obj.message.hasNoTags() ? "<p>" + obj.message + "</p>" : obj.message;
		
		Popups.make(message, {
			type:		popup_class,
			title:		this._getAlertTitle(obj),
			modal:		true,
			height:		200,
			buttons:	[
				new PopupCloseButton("close", {
					label: "Close",
					width: 150
				})
			]
		});
	},
	
	_popupMultiAlerts:					function(objs) {
		
		// Class
		var popup_class = ["alert"];
		
		// Get types
		var popup_types = [];
		for (var i = 0; i < objs.length; i++) {
			for (var j = 0; j < objs[i].type.length; j++) {
				
				// Get type, and search for it in stored types
				var type	= objs[i].type[j];
				var index	= popup_types.search(type);
				
				// If not found, add it
				if (index === false) {
					popup_types.push(type);
				}
			}
		}
		
		// If only one type was found
		if (popup_types.length == 1) {
			popup_class.push(popup_types[0]);
		} else {
			popup_class.push("multialert");
		}
		
		
		
		// Get messages
		messages = createElement("ul").addClassName("multialertlist");
		for (var i = 0; i < objs.length; i++) {
			
			// Create message
			var message = createElement("li",
				(objs[i].message.hasNoTags() ? "<p>" + objs[i].message + "</p>" : objs[i].message)
			).addClassName(objs[i].type.getValues().join(" "));
			
			// Attach
			messages.appendChild(message);
		}
		
		// Create popup
		Popups.make(messages, {
			type:		popup_class,
			title:		this._cached_alert_title,
			modal:		true,
			height:		400,
			buttons:	[
				new PopupCloseButton("close", {
					label: "Close",
					width: 150
				})
			]
		});
	},
	
	/**
	 * _getAlertTitle() attempts to create a title for the alert (log and popup) based on
	 * the source and type.
	 * 
	 * @param obj	{Object}	The alert object.
	 * 
	 * @return a title.
	 * @type String
	 */
	_getAlertTitle:						function(obj) {
		
		// Create
		var source	= this._getPrintableSource(obj.source);
		var type	= this._getPrintableType(obj.type);
		
		return source.join(" ") + " " + type.join(" ");
	},
	
	/**
	 * _getPrintableSource() takes an array of Alert source information and returns an
	 * array of printable versions of the sources.
	 * 
	 * @param source	{Array}	An array of source information.
	 * 
	 * @return an array of printable information about the source.
	 * @type Array
	 */
	_getPrintableSource:				function(source) {
		var printable_source = [];
		
		if (source.has(this.SOURCE_JS)) {
			printable_source.push("Javascript");
		}
		if (source.has(this.SOURCE_JSON)) {
			printable_source.push("JSON");
		}
		if (source.has(this.SOURCE_PHP)) {
			printable_source.push("PHP");
		}
		if (source.has(this.SOURCE_ERROR)) {
			printable_source.push("Error");
		}
		if (source.has(this.SOURCE_MESSAGE)) {
			printable_source.push("Message");
		}
		
		return printable_source;
	},
	
	/**
	 * _getPrintableType() takes an array of Alert types and generates a printable
	 * version.
	 * 
	 * @param type	{Array}	An array of types.
	 * 
	 * @return an array of printable types, ready for concatenation.
	 * @type Array
	 */
	_getPrintableType:					function(type) {
		
		var printable_type = [];
		
		if (type.has(this.TYPE_PHP)) {
			printable_type.push("PHP");
		}
		if (type.has(this.TYPE_FATAL)) {
			printable_type.push("Fatal Error");
		} else if (type.has(this.TYPE_ERROR)) {
			printable_type.push("Error");
		} else if (type.has(this.TYPE_WARNING)) {
			printable_type.push("Warning");
		} else if (type.has(this.TYPE_INFO)) {
			printable_type.push("Information");
		} else if (type.has(this.TYPE_CONFIRMATION)) {
			printable_type.push("Confirmation");
		}
		
		if (type.has(this.TYPE_UNKNOWN)) {
			printable_type.push("Unknown Type");
		}
		
		return printable_type;
	},
	
	/******************************************************
	 * 
	 *****************************************************/
	 
	startCachingAlerts:					function(title) {
		
		// Reset cache list, and prepare for caching
		this.clearCachedAlerts();
		this.setCachingAlerts(true);
		
		// Store title
		this._cached_alert_title = isString(title) ? title : "Cached Alerts";
	},
	
	cacheAlert:							function(obj) {
		if (this.isCachingAlerts()) {
			this._cached_alerts.push(obj);
		}
	},
	
	displayCachedAlerts:				function() {
		if (this._cached_alerts.length > 0) {
		
			// Feedback
			this.track("Displaying " + this._cached_alerts.length + " cached Alerts.", this, "displayCachedAlerts");
		
			// Log them
			for (var i = 0; i < this._cached_alerts.length; i++) {
				this._logAlert(this._cached_alerts[i]);
			}
			
			// Popup(s)
			if (this._cached_alerts.length == 1) {
				if (this._cached_alerts[0].popup == this.POPUP) {
					this._popupAlert(this._cached_alerts[0]);
				}
			} else {
				this._popupMultiAlerts(this._cached_alerts);
			}
			
			// Feedback
			this.track(this._cached_alerts.length + " cached alerts displayed.", this, "displayCachedAlerts");
		} else {
			this.track("No cached alerts to display; proceeding.", this, "displayCachedAlerts");
		}
		
		this.clearCachedAlerts();
		this.setCachingAlerts(false);
	},
	
	clearCachedAlerts:					function() {
		this._cached_alerts = [];
	},
	
	setCachingAlerts:					function(caching_alerts) {
		if (isBoolean(caching_alerts)) {
			this._caching_alerts = caching_alerts;
		}
	},
	
	isCachingAlerts:					function() {
		return this._caching_alerts;
	},
	
	/******************************************************
	 * PREDEFINED TYPES
	 *****************************************************/
	
	/**
	 * error() makes an error Alert.
	 * 
	 * @param message		{String}	The message to display.
	 * @param object		{Object}	The object that triggered this Alert.
	 * @param method_name	{String}	The method in which this Alert was called.
	 * @param popup			{Boolean}	Open a popup showing this alert, as well as the
	 * 									logging.
	 */
	error:								function(message, object, method_name, popup) {
		this._makeJSAlert(message, object, method_name, [this.TYPE_ERROR], popup);
		/*
		if (isDefined(console)) {
			console.error("::ERROR:: %s.%s: %s", this._getJSObjectClassName(object), method_name, message);
		}
		*/
	},
	
	/**
	 * warning() makes a warning Alert.
	 * 
	 * @param message		{String}	The message to display.
	 * @param object		{Object}	The object that triggered this Alert.
	 * @param method_name	{String}	The method in which this Alert was called.
	 * @param popup			{Boolean}	Open a popup showing this alert, as well as the
	 * 									logging.
	 */
	warning:							function(message, object, method_name, popup) {
		this._makeJSAlert(message, object, method_name, [this.TYPE_WARNING], popup);
		/*
		if (isDefined(console)) {
			console.warn("::WARNING:: %s.%s: %s", this._getJSObjectClassName(object), method_name, message);
		}
		*/
	},
	
	/**
	 * conf() makes a confirmation Alert.
	 * 
	 * @param message		{String}	The message to display.
	 * @param object		{Object}	The object that triggered this Alert.
	 * @param method_name	{String}	The method in which this Alert was called.
	 * @param popup			{Boolean}	Open a popup showing this alert, as well as the
	 * 									logging.
	 */
	conf:								function(message, object, method_name, popup) {
		this._makeJSAlert(message, object, method_name, [this.TYPE_CONFIRMATION], popup);
		/*
		if (isDefined(console)) {
			console.log("::CONFIRMATION:: %s.%s: %s", this._getJSObjectClassName(object), method_name, message);
		}
		*/
	},
	
	/**
	 * info() makes an information Alert.
	 * 
	 * @param message		{String}	The message to display.
	 * @param object		{Object}	The object that triggered this Alert.
	 * @param method_name	{String}	The method in which this Alert was called.
	 * @param popup			{Boolean}	Open a popup showing this alert, as well as the
	 * 									logging.
	 */
	info:								function(message, object, method_name, popup) {
		this._makeJSAlert(message, object, method_name, [this.TYPE_INFO], popup);
		/*
		if (isDefined(console)) {
			console.info("::INFO:: %s.%s: %s", this._getJSObjectClassName(object), method_name, message);
		}
		*/
	},
	
	/**
	 * track() logs a tracking Alert - these alerts are primarily for diagnostics and are
	 * never made into Popups.
	 * 
	 * @param message		{String}	The message to display.
	 * @param object		{Object}	The object the tracking occured in.
	 * @param method_name	{String}	The name of the method the tracking occured in.
	 */
	track:								function(message, object, method_name) {		
		log("track diag", Log.createFeedbackRows(this._getJSObjectClassName(object) + "." + method_name + "", {
			0:		message
		}, "h5"));
		
		/*
		if (isDefined(console)) {
			console.log("::TRACK:: %s.%s: %s", this._getJSObjectClassName(object), method_name, message);
		}
		*/
	},
	
	/**
	 * _makeJSAlert() makes an Alert object from passed parameters.
	 * 
	 * @param message		{String}	The message to display.
	 * @param object		{Object}	The object that triggered this Alert.
	 * @param method_name	{String}	The method in which this Alert was called.
	 * @param type			{Array}		An array of types that apply to this Alert.
	 * @param popup			{Boolean}	Open a popup showing this alert, as well as the
	 * 									logging.
	 */
	_makeJSAlert:						function(message, object, method_name, type, popup) {

		// Validate arguments
		message		= (isString(message) || isNumeric(message) || isBoolean(message))	? String(message)						: "";
		object		= (isObject(object))												? this._getJSObjectClassName(object)	: false;
		method_name	= isString(method_name)												? method_name							: false;
		type		= isArray(type)														? type									: ["generic"];
		popup		= isBoolean(popup)													? popup									: false;

		// Create Alert
		this._make(this._createDefaultAlertObject({
			message:		message,
			class_name:		object,
			method_name:	method_name,
			type:			type,
			popup:			popup
		}));
	},
	
	/******************************************************
	 * PREDEFINED JSON TYPES
	 *****************************************************/
	
	phpError:							function(message) {
		this._make({
			file:				"Unknown",
			line:				"N/A",
			class_name:			"Unknown",
			method_name:		"Unknown",
			message:			message,
			type:				this.convertJSONErrorTypeCode(12),
			time:				Date.create(),
			code:				"000",
			source:				[this.SOURCE_PHP, this.SOURCE_ERROR],
			popup:				true
		});
	},
	
	/**
	 * jsonError() throws an Alert from a JSON error.
	 * 
	 * @param obj	{Object}	Parameters for the Alert.
	 */
	jsonError:							function(obj) {
		if (isObject(obj)) {
			this._make({
				file:				obj["file"],
				line:				obj["line"],
				class_name:			obj["class"],
				method_name:		obj["method"],
				message:			obj["message"],
				type:				this.convertJSONErrorTypeCode(obj["type"]),
				time:				Date.create(obj["date"]),
				code:				obj["code"],
				source:				[this.SOURCE_JSON, this.SOURCE_ERROR],
				popup:				true
			});
		}
	},
	
	/**
	 * jsonMessage() throws an Alert from a JSON message.
	 * 
	 * @param obj	{Object}	Parameters for the Alert.
	 */
	jsonMessage:						function(obj) {
		if (isObject(obj)) {
			this._make({
				file:				obj["file"],
				line:				false,
				class_name:			obj["class"],
				method_name:		obj["method"],
				message:			obj["message"],
				type:				this.convertJSONMessageTypeCode(obj["type"]),
				time:				Date.create(obj["time"]),
				code:				false,
				source:				[this.SOURCE_JSON, this.SOURCE_MESSAGE],
				popup:				true
			});
		}
	},
	
	/******************************************************
	 * EXCEPTIONS
	 *****************************************************/
	
	displayException:					function(e) {
		this._makeJSAlert("<p>" + e.name + ": " + e.message + "</p><p>File: " + e.fileName + ", line " + e.lineNumber + "</p>", this, "displayException", ["exception", "error"], true);
	},
	
	/******************************************************
	 * UTIL METHODS
	 *****************************************************/
	
	/**
	 * _getJSObjectClassName() attempts to determine the class name of a JavaScript
	 * object.
	 * 
	 * @param obj	{Mixed}	An object (or something else) to try and get the class of.
	 */
	_getJSObjectClassName:				function(obj) {
		if (isString(obj)) {
			return "String";
		} else if (isNumber(obj)) {
			return "Number";
		} else if (isBoolean(obj)) {
			return "Boolean";
		} else if (isObject(obj)) {
			if (isString(obj._CLASS)) {
				return obj._CLASS;
			}
			
			return "Object";
		}
		
		return false;
	},
	
	/**
	 * _createDefaultAlertObject() creates a default object of parameters for an Alert,
	 * from a passed object containing various parameters.
	 * 
	 * @param obj	{Object}	Parameters for the Alert.
	 * @param type	{Array}		An array for the type property.
	 * 
	 * @return an Alert-suitable object.
	 * @type Object
	 */
	_createDefaultAlertObject:			function(obj) {
		return {
			file:				false,
			line:				false,
			class_name:			isString(obj["class_name"])		? obj["class_name"]		: false,
			method_name:		isString(obj["method_name"])	? obj["method_name"]	: false,
			message:			isString(obj["message"])		? obj["message"]		: "",
			type:				isArray(obj["type"])			? obj["type"]			: ["generic"],
			time:				Date.time(),
			code:				false,
			source:				[this.SOURCE_JS],
			popup:				isBoolean(obj["popup"]) ? obj["popup"] : false
		};
	},
	
	/**
	 * convertJSONErrorTypeCode() converts a JSON Error type code value to an array
	 * indicating what the type code means.
	 * 
	 * @param code	{Integer}	The type code.
	 * 
	 * @return an array indicating the meaning of the code.
	 * @type Array
	 */
	convertJSONErrorTypeCode:			function(code) {
		switch (parseInt(code)) {
			case 0:
				return [this.TYPE_WARNING];
			case 1:
				return [this.TYPE_ERROR];
			case 2:
				return [this.TYPE_FATAL];
			case 10:
				return [this.TYPE_PHP, this.TYPE_WARNING];
			case 11:
				return [this.TYPE_PHP, this.TYPE_ERROR];
			case 12:
				return [this.TYPE_PHP, this.TYPE_FATAL];
		}
		
		return [this.TYPE_UNKNOWN];
	},
	
	/**
	 * convertJSONMessageTypeCode() onverts a JSON Message type code value to an array
	 * indicating what the type code means.
	 * 
	 * @param code	{Integer}	The type code.
	 * 
	 * @return an array indicating the meaning of the code.
	 * @type Array
	 */
	convertJSONMessageTypeCode:			function(code) {
		switch (parseInt(code)) {
			case 1:
				return [this.TYPE_INFO];
			case 2:
				return [this.TYPE_CONFIRMATION];
			case 3:
				return [this.TYPE_WARNING];
			case 4:
				return [this.TYPE_ERROR];
		}
		
		return [this.TYPE_UNKNOWN];
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/INCA2Components.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/components.js
 * FUNCTION:		Contains the Components object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-07-27
 ***************************************************************************************/

/**
 * The INCA2Components object maintains the registry of Components, and handles the DOM
 * elements used to browse and select them.
 * 
 * @creator Cameron Morrow
 */
var INCA2Components = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"INCA2Components",
	
	// Common properties
	_status:							Admin.OBJECT_STATUS_UNINITIALISED,
	_index:								0,
	
	// Properties
	_elements:							{},
	_categories:						{},
	
	/**
	 * init() initialises the Components object.
	 * 
	 * @param index	{Integer}	The index on which the elements will be drawn.
	 */
	init:								function(index) {
		
		// Log
		log("init", "Initialising Components (v" + this._VERSION + ")");
		
		// Create DOM elements
		this._createElements();
		
		// Store Index
		this._index = index;
		
		// Update status
		this._status = Admin.OBJECT_STATUS_INITIALISED;
		
		// Append to interface
		Page.insert(index, this._elements.components);
	},
	
	/**
	 * _createElements() creates the DOM Elements for this object.
	 */
	_createElements:					function() {
		
		this._elements.components	= createElement("div",
			["id", "components"]
		);
		
		this._elements.selector		= createElement("div",
			["id", "componentselector"]
		);
		
		this._elements.list			= $(createElement("div",
			["id", "componentlist"]
		));
		
		this._elements.components.appendChild(this._elements.list);
		this._elements.components.appendChild(this._elements.selector);
	},
	
	/**
	 * prepare() prepares the interface for this element for use.
	 */
	prepare:							function() {
		
		// Draw components
		this._drawComponents();
		
		// Position
		this.redraw();
	},
	
	/************************************************************************************
	 * COMPONENTS AND PAGES
	 ***********************************************************************************/
	
	/**
	 * hasComponent() returns whether a component exists in one of the categories.
	 * 
	 * @param id	{String}	The ID of the component to look for.
	 * 
	 * @return true if the component exists, false if not.
	 * @type Boolean
	 */
	hasComponent:						function(id) {
		for (var i in this._categories) {
			if (this._categories[i].hasComponent(id)) {
				return true;
			}
		}
		
		return false;
	},
	
	/**
	 * getComponent() gets a component stored in one of the categories.
	 * 
	 * @param id	{String}	The ID of the component to get.
	 * 
	 * @return the Component object if found, null if not.
	 * @type Component/null
	 */
	getComponent:						function(id) {
		for (var i in this._categories) {
			if (this._categories[i].hasComponent(id)) {
				return this._categories[i].getComponent(id);
			}
		}
		
		return null;
	},
	
	/**
	 * getComponentPage() attempts to get a ComponentPage.
	 * 
	 * @param component_id	{String}	The ID of the component.
	 * @param page_id		{String}	The ID of the page.
	 */
	getComponentPage:					function(component_id, page_id) {
		
		// Get component
		var component = this.getComponent(component_id);
		
		// If component found, try to get page
		if (component) {
			return component.getPage(page_id);
		}
		
		return false;
	},
	
	/************************************************************************************
	 * CATEGORIES
	 ***********************************************************************************/
	
	/**
	 * addCategory() adds a ComponentCategory object to the list.
	 * 
	 * @param category	{ComponentCategory}	The category object to add.
	 */
	addCategory:						function(category) {
		if (isObject(category)) {
			this._categories[category.getID()] = category;
		}
		
		return category;
	},
	
	/************************************************************************************
	 * DRAWING
	 ***********************************************************************************/
	
	/**
	 * _drawComponents() draws the two lists of Component Categories and items.
	 */
	_drawComponents:					function() {
		
		// Reset
		this._elements.selector.innerHTML	= "";
		this._elements.list.innerHTML		= "";
		
		// Create lists
		var selector_ul						= createElement("ul");
		var list_ul							= createElement("ul");
		
		// For each category
		for (var i in this._categories) {
			
			// Draw selector item
			selector_ul.appendChild(this._categories[i].drawSelectorElement());

			// Draw list item
			list_ul.appendChild(this._categories[i].drawListElement());
		}
		
		// Attach
		this._elements.selector.appendChild(selector_ul);
		this._elements.list.appendChild(list_ul);
	},
	
	/**
	 * redraw() redraws this object's elements when the page is changed.
	 */
	redraw:								function() {
		Page.simpleCentre(this._elements.selector);
	},
	
	/************************************************************************************
	 * METHODS FOR HANDLING THE COMPONENT NAVIGATION LIST
	 ***********************************************************************************/
	
	/**
	 * clickListComponent() is triggered when a component item in the Component List is
	 * clicked.
	 * 
	 * @param component	{String}	The ID of the component that was clicked.
	 * @param page		{String}	Optional. The ID of the default page to load.
	 */
	clickListComponent:					function(component, page) {
		
		// Check page
		page = page || false;
		
		// Load
		ComponentInterface.loadComponent(component, page);
		
		// Hide selector
		this.closeAllListCategories();
		this.hideSelector();
	},
	
	clickListPage:						function(component, page) {
		
		// See if component is the current one
		if (component == ComponentInterface.getActiveComponentID()) {
			ComponentInterface.getActiveComponent().loadPage(page);
		} else {
			ComponentInterface.loadComponent(component, page);
		}
		
		this.closeAllListCategories();
		this.hideSelector();
	},
	
	/**
	 * clickListCategory() handles clicking one of the list Categories.
	 * 
	 * @param id	{String}	The category ID.
	 */
	clickListCategory:					function(id) {
		if (!this.listCategoryIsOpen(id)) {
			this.closeAllListCategories();
		}
		this.toggleListCategory(id);
	},
	
	/**
	 * closeAllListCategories() closes all list Category menus.
	 */
	closeAllListCategories:				function() {
		for (var i in this._categories) {
			if (this.listCategoryIsOpen(i)) {
				this.closeListCategory(i);
			}
		}
	},
	
	/**
	 * openListCategory()
	 */
	openListCategory:					function(id) {
		if (this.listCategoryExists(id)) {
			$("cl-" + id).addClassName("open");
		}
	},
	
	/**
	 * closeListCategory()
	 */
	closeListCategory:					function(id) {
		if (this.listCategoryExists(id)) {
			$("cl-" + id).removeClassName("open");
		}
	},
	
	/**
	 * listCategoryIsOpen()
	 */
	listCategoryIsOpen:					function(id) {
		if (this.listCategoryExists(id)) {
			return $("cl-" + id).hasClassName("open");
		}
	},
	
	/**
	 * listCategoryExists()
	 */
	listCategoryExists:					function(id) {
		return $("cl-" + id) ? true : false;
	},
	
	/**
	 * toggleListCategory()
	 */
	toggleListCategory:					function(id) {
		if (this.listCategoryExists(id)) {
			if (this.listCategoryIsOpen(id)) {
				this.closeListCategory(id);
			} else {
				this.openListCategory(id);
			}
		}
	},
	
	/************************************************************************************
	 * METHODS FOR HANDLING THE COMPONENT NAVIGATION SELECTOR
	 ***********************************************************************************/
	
	showSelector:						function() {
		$("componentselector").removeClassName("hidden");
	},
	
	hideSelector:						function() {
		ComponentInterface.setTabsHomeState(false);
		$("componentselector").addClassName("hidden");
	},
	
	/**
	 * clickSelectorCategory() handles a click on a selector category.
	 * 
	 * @param id	{String}	The ID of the category that was clicked.
	 */
	clickSelectorCategory:				function(id) {

		// Deactivate all others if activating this one
		if (!this.selectorCategoryIsActive(id)) {
			this.deactivateAllSelectorCategories();
		}
		this.toggleSelectorCategory(id);
		
		// Recentre the selector
		Page.simpleCentre(this._elements.selector, true);
	},
	
	/**
	 * selectorCategoryHover() is triggered when the mouse hovers on or off a category.
	 * 
	 * @param id	{String}	The ID of the category.
	 * @param mode	{Boolean}	Whether the mouse went on (true) or off (false)
	 */
	selectorCategoryHover:				function(id, mode) {
		if (this.selectorCategoryExists(id)) {
			if (mode) {
				$("cs-" + id).addClassName("hover");
			} else {
				$("cs-" + id).removeClassName("hover");
			}
		}
	},
	
	/**
	 * selectorCategoryExists() returns whether a selector category DOM element exists.
	 * 
	 * @param id	{String}	The ID of the category.
	 * 
	 * @return true if the category element exists, false if not.
	 */
	selectorCategoryExists:				function(id) {
		return $("cs-" + id) ? true : false;
	},
	
	/**
	 * selectorCategoryIsActive() returns whether a selector category is active
	 * (expanded).
	 * 
	 * @param id	{String}	The ID of the category.
	 * 
	 * @return true if the category element is active, false if not.
	 * @type Boolean
	 */
	selectorCategoryIsActive:			function(id) {
		if (this.selectorCategoryExists(id)) {
			return $("cs-" + id).hasClassName("active");
		}
	},
	
	/**
	 * activateSelectorCategory() makes a selector category active.
	 * 
	 * @param id	{String}	The ID of the category.
	 */
	activateSelectorCategory:			function(id) {
		if (this.selectorCategoryExists(id)) {
			$("cs-" + id).addClassName("active");
		}
	},
	
	/**
	 * deactivateSelectorCategory() makes a selector category inactive.
	 * 
	 * @param id	{String}	The ID of the category.
	 */
	deactivateSelectorCategory:			function(id) {
		if (this.selectorCategoryExists(id)) {
			$("cs-" + id).removeClassName("active");
		}
	},
	
	/**
	 * deactivateAllSelectorCategories() deactivates all selector category DOM elements.
	 */
	deactivateAllSelectorCategories:	function() {
		for (var i in this._categories) {
			if (this.selectorCategoryIsActive(i)) {
				this.deactivateSelectorCategory(i);
			}
		}
	},
	
	/**
	 * toggleSelectorCategory() toggles a selector category between being active and
	 * being inactive.
	 * 
	 * @param id	{String}	The ID of the category.
	 */
	toggleSelectorCategory:				function(id) {
		if (this.selectorCategoryExists(id)) {
			if (this.selectorCategoryIsActive(id)) {
				this.deactivateSelectorCategory(id);
			} else {
				this.activateSelectorCategory(id);
			}
		}
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/ComponentInterface.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/component_interface.js
 * FUNCTION:		Contains the ComponentInterface object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-06
 ***************************************************************************************/

/**
 * The ComponentInterface object handles the pages that make up the interface of a
 * Component.
 * 
 * The structure of the Interface and it's handlers is as follows:
 * - ComponentInterface; which contains
 * 		- An 'active' Component, which contains
 * 			- ComponentPage objects; which contain
 * 				- ComponentTab objects; which contain
 * 					- ComponentItem objects
 * 				- ComponentSidebar object, which contains
 * 					- ComponentItem objects
 * 
 * @creator Cameron Morrow
 */
var ComponentInterface = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"ComponentInterface",
	TAB_HEADER_ID:						"tabheader",
	TAB_HEADER_LIST_ID:					"tabheaderlist",
	TAB_BODY_ID:						"tabbody",
	TAB_BODY_LIST_ID:					"tabbodylist",
	PAGES_ID:							"pagelist",
	PAGES_LIST_ID:						"pagelistul",
	SIDEBAR_ID:							"sidebar",
	
	// Common properties
	_status:							Admin.OBJECT_STATUS_UNINITIALISED,
	_index:								0,
	
	// Properties
	_elements:							{},
	_component:							null,
	_page:								null,
	
	/**
	 * init() initialises the ComponentInterface object.
	 * 
	 * @param index	{Integer}	The index on which the elements will be drawn.
	 */
	init:								function(index) {
		
		// Log
		log("init", "Initialising Component Interface (v" + this._VERSION + ")");
		
		// Create DOM elements
		this._createElements();
		
		// Store Index
		this._index = index;
		
		// Update status
		this._status = Admin.OBJECT_STATUS_INITIALISED;
		
		// Append to interface
		Page.insert(index, this._elements.main);
	},
	
	/**
	 * loadComponent() loads a passed component ID as the active component.
	 * 
	 * @param id	{String}	The ID of the component.
	 * @param page	{String}	Optional. The default page to load.
	 */
	loadComponent:						function(id, page) {

		// Check page
		page = page || false;
		
		// If component eixsts
		if (INCA2Components.hasComponent(id)) {
			
			// Activate
			this.makeActiveComponent(INCA2Components.getComponent(id));
			
			// And load page
			this._component.loadPage(page);
			
		} else {
			log("error", "Component '" + id + "' does not exist!");
		}
	},
	
	/************************************************************************************
	 * ACTIVE COMPONENTS AND PAGES
	 ***********************************************************************************/
	
	/**
	 * makeActiveComponent() makes the passed Component object the active component,
	 * storing it in the ComponentInterface object and displaying it's pages.
	 * 
	 * @param component	{Component}	The component to make active.
	 */
	makeActiveComponent:				function(component) {
		
		// Store as active
		this._component = component;
		
		// Header
		Admin.updateTitle(component.getName());
	},
	
	/**
	 * getActiveComponent gets the active Component.
	 * 
	 * @return the active Component object.
	 * @type Component
	 */
	getActiveComponent:					function() {
		return this._component;
	},
	
	/**
	 * hasActiveComponent() returns whether this is an active Component.
	 * 
	 * @return true if an active component has been set.
	 * @type Boolean
	 */
	hasActiveComponent:					function() {
		return this._component ? true : false;
	},
	
	/**
	 * getActiveComponentID returns the ID of the active Component, if found.
	 * 
	 * @return the ID of the active Component, or false if no Component is active.
	 * @type String/Boolean
	 */
	getActiveComponentID:				function() {
		if (this.hasActiveComponent()) {
			return this._component.getID();
		}
		
		return false;
	},
	
	/**
	 * makeActivePage() makes the passed Page the active page.
	 * 
	 * @param page	{ComponentPage}	The page to make the active one.
	 */
	makeActivePage:						function(page) {
		this._page = page;
	},
	
	/**
	 * getActivePage() returns the current active page.
	 * 
	 * @return the active ComponentPage object, or false if none is set.
	 * @type ComponentPage/Boolean
	 */
	getActivePage:						function() {
		return this._page;
	},
	
	getActivePageID:					function() {
		return this.hasActivePage() ? this._page.getID() : "";
	},
	
	/**
	 * hasActivePage() returns whether an active Page has been set.
	 */
	hasActivePage:						function() {
		return isObject(this._page) ? true : false;
	},
	
	/**
	 * executePageAction() executes an action on the currently active page.
	 * 
	 * @param action	{String}	The action to run.
	 * @param params	{Object}	Parameters to pass to the action.
	 * @param success	{Function}	The method to run on success.
	 * @param failure	{Function}	The method to run on failure.
	 * @param message	{String}	The message to display on the loader.
	 */
	executePageAction:					function(action, params, success, failure, message) {
		if (this._page) {
			this._page.executeAction(action, params, success, failure, message);
		}
	},
	
	/************************************************************************************
	 * INTERACTION METHODS
	 ***********************************************************************************/
	
	/**
	 * clickTab() handles clicking on a Page Tab.
	 */
	clickTab:							function(tab_id) {
		if (this.hasActivePage()) {
			this._page.activateTab(tab_id);
		} else {
			Alerts.error("Could not select Tab as no active page was found.", this, "clickTab");
		}
	},
	
	/************************************************************************************
	 * DOM METHODS
	 ***********************************************************************************/
	
	/**
	 * _createElements() creates the DOM elements for this object.
	 */
	_createElements:					function() {
		
		this._elements.main = createElement("div",
			["id", "main"],
			this._elements.page_list,
			createElement("div",
				["id", "maincontainer"],
				createElement("div",
					["id", ComponentInterface.SIDEBAR_ID]
				),
				createElement("div",
					["id", "tabholder"],
					createElement("div",
						["id", ComponentInterface.TAB_HEADER_ID],
						createElement("ul",
							["id", ComponentInterface.TAB_HEADER_LIST_ID]
						)
					),
					createElement("div",
						["id", ComponentInterface.TAB_BODY_ID],
						createElement("ul",
							["id", ComponentInterface.TAB_BODY_LIST_ID]
						).addClassName("home")
					)
				)
			)
		);
	},
	
	setTabsHomeState:					function(active) {
		if (active) {
			$(ComponentInterface.TAB_BODY_LIST_ID).addClassName("home");
		} else {
			$(ComponentInterface.TAB_BODY_LIST_ID).removeClassName("home");
		}
	},
	
	/**
	 * prepare() prepares the Object for use.
	 */
	prepare:							function() {
		
		// Get references to important DOM elements
		this._elements.sidebar		= $("sidebar");
		this._elements.tabholder	= $("tabholder");
		this._elements.tabheader	= $("tabheader");
		this._elements.tabbody		= $("tabbody");
		this._elements.container	= $("maincontainer");
		
		// Redraw
		this.redraw();
	},

	/**
	 * redraw() is used to redraw this object's DOM elements.
	 */
	redraw:								function() {
		
		var params = {
			width:				Page.simpleGetWindowWidth() - 12,
			height:				Page.simpleGetWindowHeight() - 107,
			sidebar_width:		0,
			tab_padding:		0,
			tab_header_height:	30,
			tab_body_border:	1
		};
		
		// Check sidebar settings
		if (this.hasActivePage()) {
			if (this.getActivePage().getHasSidebar()) {
				params.sidebar_width = 300;
			}
		}
		
		// Outer
		Page.simpleSetWidth(this._elements.main, params.width);
		Page.simpleSetHeight(this._elements.main, params.height);
		
		// Container
		Page.simpleSetWidth(this._elements.container, params.width);
		Page.simpleSetHeight(this._elements.container, params.height);
		
		// Sidebar
		Page.simpleSetWidth(this._elements.sidebar, params.sidebar_width);
		Page.simpleSetHeight(this._elements.sidebar, params.height);
		
		// Tabholder
		Page.simpleSetWidth(this._elements.tabholder, params.width - params.sidebar_width);
		Page.simpleSetHeight(this._elements.tabholder, params.height);
		Page.simpleSetX(this._elements.tabholder, params.sidebar_width);
		
		// Tab header
		Page.simpleSetWidth(this._elements.tabheader, params.width - params.sidebar_width);
		Page.simpleSetHeight(this._elements.tabheader, params.tab_header_height);
		
		// Tab body
		Page.simpleSetWidth(this._elements.tabbody, params.width - params.sidebar_width);
		Page.simpleSetHeight(this._elements.tabbody, params.height - params.tab_header_height);
		Page.simpleSetY(this._elements.tabbody, params.tab_header_height);
		
		// Tab item bodies
		var tabs = $$(".tabbodyli");
		for (var i = 0; i < tabs.length; i++) {
			//Page.simpleSetWidth(tabs[i], params.width - params.sidebar_width);
			//Page.simpleSetHeight(tabs[i], params.height - params.tab_header_height);
		}
		
		// Position
		Page.simpleMoveTo(this._elements.main, 5, 70);
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Intervals.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/intervals.js
 * FUNCTION:		Contains the Intervals object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-08
 ***************************************************************************************/

/**
 * The Intervals object handles running methods at intervals. This is a work around to
 * Internet Explorer's fucked-up interval handling.
 * 
 * @creator Cameron Morrow
 */
var Intervals = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Intervals",
	
	// Properties
	_intervals:							[],
	_params:							[],
	
	init:								function() {
		
		// Log
		log("init", "Initialising Intervals (v" + this._VERSION + ")");
	},
	
	/**
	 * getID() gets a fresh ID to use.
	 * 
	 * @return a fresh ID to use for the interval.
	 * @type Integer
	 */
	getID:								function() {
		return Intervals._intervals.length;
	},
	
	/************************************************************************************
	 * INTERVAL METHODS
	 ***********************************************************************************/
	
	/**
	 * start() starts a new Interval.
	 * 
	 * @param method_name	{String}	The full name of the method to call on interval.
	 * @param interval		{Integer}	The number of milliseconds between each call.
	 * @param params		{Object}	An object containing parameters that the called
	 * 									method may need.
	 */
	start:								function(method_name, interval, params) {
		
		// Validate variables
		method_name		= isString(method_name)	? method_name	: "";
		interval		= isNumber(interval)	? interval		: 1000;
		params			= isObject(params)		? params		: {};
		
		// Get an ID
		var id = this.getID();
		
		// Store parameters
		this._params[id]		= params;
		
		// Create interval
		this._intervals[id]		= setInterval(method_name + "(" + id + ")", interval);
	},
	
	/**
	 * stop() stops an Interval from recurring.
	 * 
	 * @param id	{Integer}	The interval ID to stop.
	 */
	stop:								function(id) {
		clearInterval(Intervals._intervals[id]);
	},
	
	/**
	 * stopAll() stops all Intervals.
	 */
	stopAll:							function() {
		for (var i = 0; i < Intervals._intervals.length; i++) {
			Intervals.stop(i);
		}
	},
	
	/************************************************************************************
	 * PARAMETER METHODS
	 ***********************************************************************************/
	
	/**
	 * hasParams() returns whether parameters exist for the passed ID.
	 * 
	 * @param id	{Integer}	The Interval ID.
	 * 
	 * @return true if parameters exist, false if not.
	 * @type Boolean
	 */
	hasParams:							function(id) {
		return isObject(this._params[id]);
	},
	
	/**
	 * hasParam() returns whether an Interval has the passed parameter.
	 * 
	 * @param id		{Integer}	The Interval ID.
	 * @param param_id	{Mixed}		The parameter ID.
	 * 
	 * @return true if the parameter has been set, false if not.
	 * @type Boolean
	 */
	hasParam:							function(id, param_id) {
		if (this.hasParams(id)) {
			var params = this.getParams(id);
			return isDefined(params[param_id]);
		}
		
		return false;
	},
	
	/**
	 * getParams() returns the parameters object for the passed Interval.
	 * 
	 * @param id	{Integer}	The Interval ID.
	 * 
	 * @return the parameters object if found, an empty object if not.
	 * @type Object
	 */
	getParams:							function(id) {
		if (this.hasParams(id)) {
			return this._params[id];
		}
		
		return {};
	},
	
	/**
	 * getParam() gets a parameter value for an Interval.
	 * 
	 * @param id		{Integer}	The Interval ID.
	 * @param param_id	{Mixed}		The parameter ID.
	 * 
	 * @return the parameter value if found, false if not.
	 * @type Mixed
	 */
	getParam:							function(id, param_id) {
		if (this.hasParam(id, param_id)) {
			var params = this.getParams(id);
			return params[param_id];
		}
		
		return false;
	},
	
	/**
	 * setParam() sets a parameter value for an Interval.
	 * 
	 * @param id		{Integer}	The Interval ID.
	 * @param param_id	{Mixed}		The parameter ID.
	 * @param value		{Mixed}		The parameter value.
	 */
	setParam:							function(id, param_id, value) {
		if (this.hasParams(id)) {
			var params = this.getParams(id);
			params[param_id] = value;
		} else {
			var params = {};
			params[param_id] = value;
			this._params[id] = params;
		}
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Balloon.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/balloon.js
 * FUNCTION:		Contains the Balloon object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-09-06
 ***************************************************************************************/

/**
 * The Balloon object controls the display of a small Balloon help display, that pops
 * up next to other elements to provide information to the user.
 * 
 * @creator Cameron Morrow
 */
var Balloon = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Balloon",
	_CONTENT_WIDTH:						300,
	_Z_INDEX:							1100,
	
	// Common properties
	_status:							Admin.OBJECT_STATUS_UNINITIALISED,
	_index:								0,
	
	// Properties
	_elements:							{},
	_arrow_offset:						{x: 0, y: 0},
	_current_element:					false,
	
	init:								function(index) {
		
		log("init", "Initialising Balloon (v" + this._VERSION + ")");
		
		// Create HTML DOM elements
		this._createElements();
		
		// Store Index
		this._index = index;
		
		// Update status
		this._status = Admin.OBJECT_STATUS_INITIALISED;
		
		// Append to interface
		Page.insert(index, this._elements.balloon);
	},
	
	_createElements:					function() {
		
		this._elements.balloon = createElement("div",
			["id", "balloon"],
			createElement("table",
				["cellspacing", "0"],
				createElement("tbody",
					createElement("tr",
						createElement("td",
							["id", "spacer1"],
							createElement("span",
								"&nbsp;"
							)
						).addClassName("spacer"),
						
						createElement("td",
							["id", "balloontl"],
							["rowspan", "2"],
							"&nbsp;"
						).addClassName("frame").addClassName("corner"),
						
						createElement("td",
							["id", "balloont"],
							createElement("div",
								["id", "balloonarrowt"],
								"&nbsp;"
							).addClassName("balloonarrow")
						).addClassName("frame").addClassName("hside"),
						
						createElement("td",
							["id", "balloontr"],
							["rowspan", "2"],
							"&nbsp;"
						).addClassName("frame").addClassName("corner")
					),
					
					createElement("tr",
						createElement("td",
							["id", "spacer2"],
							createElement("span",
								"&nbsp;"
							)
						).addClassName("spacer"),
						
						createElement("td",
							["id", "balloonm"],
							["rowspan", "3"],
							"MAIN<br />MAIN2<br />MAIN3<br />MAIN4"
						)
					),
					
					createElement("tr",
						createElement("td",
							["id", "spacer3"],
							createElement("span",
								"&nbsp;"
							)
						).addClassName("spacer"),
						
						createElement("td",
							["id", "balloonl"],
							"&nbsp;"
						).addClassName("frame").addClassName("vside"),
						
						createElement("td",
							["id", "balloonr"],
							"&nbsp;"
						).addClassName("frame").addClassName("vside")
					),
					
					createElement("tr",
						createElement("td",
							["id", "spacer4"],
							createElement("span",
								"&nbsp;"
							)
						).addClassName("spacer"),
						
						createElement("td",
							["id", "balloonbl"],
							["rowspan", "2"],
							"&nbsp;"
						).addClassName("frame").addClassName("corner"),
						
						createElement("td",
							["id", "balloonbr"],
							["rowspan", "2"],
							"&nbsp;"
						).addClassName("frame").addClassName("corner")
					),
					
					createElement("tr",
						createElement("td",
							["id", "spacer5"],
							createElement("span",
								"&nbsp;"
							)
						).addClassName("spacer"),
						
						createElement("td",
							["id", "balloonb"],
							createElement("div",
								["id", "balloonarrowb"],
								"&nbsp;"
							).addClassName("balloonarrow").addClassName("right")
						).addClassName("frame").addClassName("hside")
					)
				)
			)
		);
		
		
		/*
		this._elements.balloon = $(createElement("div",
			["id", "balloon"],
			"<table cellspacing=\"0\">" +
				"<tbody>" +
					"<tr>" +
						"<td id=\"spacer1\" class=\"spacer\"><span>&nbsp;</span></td>" +
						"<td id=\"balloontl\" class=\"frame corner\" rowspan=\"2\">&nbsp;</td>" +
						"<td id=\"balloont\" class=\"frame hside\">" +
							"<div id=\"balloonarrowt\" class=\"balloonarrow\">&nbsp;</div>" +
						"</td>" +
						"<td id=\"balloontr\" class=\"frame corner\" rowspan=\"2\">&nbsp;</td>" +
					"</tr>" +
					"<tr>" +
						"<td id=\"spacer2\" class=\"spacer\"><span>&nbsp;</span></td>" +
						"<td id=\"balloonm\" rowspan=\"3\">MAIN<br />MAIN2<br />MAIN3<br />MAIN4</td>" +
					"</tr>" +
					"<tr>" +
						"<td id=\"spacer3\" class=\"spacer\"><span>&nbsp;</span></td>" +
						"<td id=\"balloonl\" class=\"frame vside\">&nbsp;</td>" +
						"<td id=\"balloonr\" class=\"frame vside\">&nbsp;</td>" +
					"</tr>" +
					"<tr>" +
						"<td id=\"spacer4\" class=\"spacer\"><span>&nbsp;</span></td>" +
						"<td id=\"balloonbl\" class=\"frame corner\" rowspan=\"2\">&nbsp;</td>" +
						"<td id=\"balloonbr\" class=\"frame corner\" rowspan=\"2\">&nbsp;</td>" +
					"</tr>" +
					"<tr>" +
						"<td id=\"spacer5\" class=\"spacer\"><span>&nbsp;</span></td>" +
						"<td id=\"balloonb\" class=\"frame hside\">" +
							"<div id=\"balloonarrowb\" class=\"balloonarrow right\">&nbsp;</div>" +
						"</td>" +
					"</tr>" +
				"</tbody>" +
			"</table>"
		));
		*/
	},
	
	/**
	 * prepare() preps the Login elements for display.
	 */
	prepare:							function() {
		
		// Update status
		this._status = Admin.OBJECT_STATUS_PREPARED;
		
		// Draw
		this.redraw();
		
		// Update and hide
		this.setArrowPositionUp();
		this.setArrowDirectionLeft();
		this.hide();
	},
	
	/************************************************************************************
	 * CURRENT ELEMENT METHODS
	 ***********************************************************************************/
	
	/**
	 * setCurrentElement() sets the current element object.
	 * 
	 * @param element	{DOMElement/Boolean}	The DOM element to make current, or false
	 * 											for no element.
	 */
	setCurrentElement:					function(element) {
		this._current_element = element;
	},
	
	/**
	 * unsetCurrentElement() removes the current element.
	 */
	unsetCurrentElement:				function() {
		this.setCurrentElement(false);
	},
	
	/**
	 * getCurrentElement() gets the current element.
	 * 
	 * @return the current element object, or false if none is set.
	 * @type DOMElement/Boolean
	 */
	getCurrentElement:					function() {
		return this._current_element;
	},
	
	/**
	 * hasCurrentElement() returns whether a current element has been set.
	 * 
	 * @return true if an element has been set, false if not.
	 * @type Boolean
	 */
	hasCurrentElement:					function() {
		return this.getCurrentElement() ? true : false;
	},
	
	/************************************************************************************
	 * DISPLAY METHODS
	 ***********************************************************************************/
	
	/**
	 * showAt() shows the Balloon at a passed X/Y value. This method is usually called
	 * via the showAtElement() method, but can be accessed directly as required.
	 * 
	 * @param msg		{String}	The message to display in the Balloon.
	 * @param x			{Integer}	The X-position on the page to display the Ballon
	 * 								'pointer' at.
	 * @param y			{Integer}	The Y-position on the page.
	 * @param encoded	{Boolean}	Whether the msg has been encoded with Javascript's
	 * 								escape() method.
	 */
	showAt:								function(msg, x, y, encoded) {
		
		// Display
		this.show();
		
		// Update message and dimensions
		Page.simpleSetWidth($("balloonm"), this._CONTENT_WIDTH);
		this._setMessage(msg, encoded);
		
		// Calculate position
		var w = Page.simpleGetWindowDimensions();
		this._setArrowPosition(y <= w.height / 1.5);
		this._setArrowDirection(x <= w.width / 1.5);
		
		// Set position
		this._setPosition(x, y);
	},
	
	/**
	 * showAtElement()
	 *
	 * @param msg		{String}		The message to display.
	 * @param obj		{DOMElement}	The DOM Element to display at.
	 * @param encoded	{Boolean}		Whether the message is encoded.
	 * @param params	{Object}		Parameters indicating where the Balloon should
	 * 									appear relative to the object - an X and Y
	 * 									property with 'top', 'bottom', 'middle', etc.
	 */
	showAtElement:						function(msg, obj, encoded, params) {
		
		// Get position & dimensions
		var p = Page.simpleGetPosition(obj);
		var d = Page.simpleGetDimensions(obj);
		
		// If can get object info OK
		if (isNumeric(p.x) && isNumeric(p.y) && isNumeric(d.width) && isNumeric(d.height)) {
			
			// Get screen dimensions and balloon dimensions
			var w = Page.simpleGetWindowDimensions();
			var b = this.getOuterDimensions();
			
			// If can get these OK too
			if (isNumeric(w.width) && isNumeric(w.height) && isNumeric(b.width) && isNumeric(b.height)) {
				
				// Define position parameters
				params = params || {};
				if (isUndefined(params.x)) {
					params.x = "middle";
				}
				if (isUndefined(params.y)) {
					if (p.y + d.height < w.height - b.height) {
						params.y = "bottom";
					} else {
						params.y = "top";
					}
				}
				
				// Define offsets
				var x = p.x;
				var y = p.y;
				if (params.x == "middle") {
					x += 0.5 * d.width;
				} else if (params.x == "right") {
					x += d.width;
				}
				if (params.y == "middle") {
					y += 0.5 * d.height;
				} else if (params.y == "bottom") {
					y += d.height;
				}
				
				// Show
				this.showAt(msg, x, y, encoded);
				
				// Remember this element
				this.setCurrentElement(obj);
			} else {
				Alerts.error("Window or Balloon dimensions.", this, "showAtElement");
			}
		} else {
			Alerts.error("Could not get object's dimensions or position.", this, "showAtElement");
		}
	},
	
	/**
	 * _setMessage() sets the message of the Balloon help.
	 * 
	 * @param msg		{String}	The message content. Can contains HTML if required.
	 * @param encoded	{Boolean}	Whether the message is escape()'d.
	 */
	_setMessage:						function(msg, encoded) {
		
		// Update HTML
		$("balloonm").innerHTML = encoded ? unescape(msg) : msg;
		
		// Update offset calculations with new height of content
		this._updateArrowOffset();
	},
	
	/**
	 * _setPosition() sets the position of the Balloon arrow.
	 * 
	 * @param x	{Integer}	The X coordinate the Balloon arrow should point to.
	 * @param y	{Integer}	The Y coordinate the Balloon arrow should point to.
	 */
	_setPosition:						function(x, y) {
		Page.simpleMoveTo(this._elements.balloon, x - this._arrow_offset.x, y - this._arrow_offset.y);
	},	
	
	/**
	 * redraw() doesn't do shit.
	 */
	redraw:								function() {
		
	},
	
	
	/************************************************************************************
	 * SHOW/HIDE METHODS
	 ***********************************************************************************/
	
	/**
	 * show() makes the Balloon visible.
	 */
	show:								function() {
		$(this._elements.balloon).removeClassName("hidden");
		$(this._elements.balloon).style.zIndex = this._Z_INDEX;
	},
	
	/**
	 * hide() makes the Balloon invisible.
	 */
	hide:								function() {
		$(this._elements.balloon).addClassName("hidden");
	},
	
	hideIfElement:						function(element) {
		if (this.isShown() && element == this.getCurrentElement()) {
			this.hide();
		}
	},
	
	/**
	 * isShown() returns whether the Balloon is currently shown.
	 * 
	 * @return true if it is currently visible, false if not.
	 * @type Boolean
	 */
	isShown:							function() {
		return !$(this._elements.balloon).hasClassName("hidden");
	},
	
	/**
	 * getInnerDimensions() gets the current inner width and height of the content. This
	 * is used to calculate vertical offsets for the arrows (based on changing heights
	 * due to changing inner content).
	 * 
	 * @return an object with width and height properties.
	 * @type Object
	 */
	getInnerDimensions:					function() {
		return Page.simpleGetDimensions($("balloonm"));
	},
	
	getOuterDimensions:					function() {
		var d = this.getInnerDimensions();
		return {
			width:	d.width + 40,
			height:	d.height + 40
		};
	},
	
	/************************************************************************************
	 * ARROW METHODS
	 ***********************************************************************************/
	 
	/**
	 * _setArrowPosition() sets the position (vertical) the arrow should point to.
	 * 
	 * @param up	{Boolean}	If true, arrow should be pointing up, otherwise, down.
	 */
	_setArrowPosition:					function(up) {
		if (up) {
			$("balloonarrowt").removeClassName("hidden");
			$("balloonarrowb").addClassName("hidden");
		} else {
			$("balloonarrowb").removeClassName("hidden");
			$("balloonarrowt").addClassName("hidden");
		}
		this._updateArrowOffset();
	},
	
	/**
	 * _setArrowDirection() sets the direction (side) the arrow should appear on.
	 * 
	 * @param left	{Boolean}	If true, arrow should be left, otherwise, right.
	 */
	_setArrowDirection:					function(left) {
		if (left) {
			$("balloonarrowt").removeClassName("right");
			$("balloonarrowb").removeClassName("right");
		} else {
			$("balloonarrowt").addClassName("right");
			$("balloonarrowb").addClassName("right");
		}
		this._updateArrowOffset();
	},
	
	/**
	 * setArrowPositionUp() sets the arrow's position to be upwards.
	 */
	setArrowPositionUp:					function() {
		this._setArrowPosition(true);
	},
	
	/**
	 * setArrowPositionDown() sets the arrow's position to be downwards.
	 */
	setArrowPositionDown:				function() {
		this._setArrowPosition(false);
	},
	
	/**
	 * getArrowPosition() gets the arrow's T/B position.
	 * 
	 * @return true if the arrow is on the top, false if on the bottom.
	 * @type Boolean
	 */
	getArrowPosition:					function() {
		return !$("balloonarrowt").hasClassName("hidden");
	},
	
	/**
	 * setArrowDirectionLeft() sets the arrow's direction to be left.
	 */
	setArrowDirectionLeft:				function() {
		this._setArrowDirection(true);
	},
	
	/**
	 * setArrowDirectionRight() sets the arrow's direction to be right.
	 */
	setArrowDirectionRight:				function() {
		this._setArrowDirection(false);
	},
	
	/**
	 * getArrowDirection() gets the arrow's L/R direction.
	 * 
	 * @return true if the arrow is on the left, false if on the right.
	 * @type Boolean
	 */
	getArrowDirection:					function() {
		return !$("balloonarrowt").hasClassName("right");
	},
	
	/**
	 * _updateArrowOffset() updates the offset from the top left corner to the active
	 * arrow pointer, used for calculating positions.
	 */
	_updateArrowOffset:					function() {
		
		// Get initial status
		var state = this.isShown();
		
		// Show for measuring purposes
		this.show();
		
		// Get inner dimensions
		var inner = this.getInnerDimensions();
		
		// Update dimensions
		this._arrow_offset.x = this.getArrowDirection() ? 40 : 25 + inner.width - 15;
		this._arrow_offset.y = this.getArrowPosition() ? 0 : 25 + inner.height + 25;
		
		// If originally hidden, hide now
		if (!state) {
			this.hide();
		}
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Text.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/text.js
 * FUNCTION:		Contains the Text object.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-09-07
 ***************************************************************************************/

/**
 * The Text object allows character stripping/validation, and other similar stuff.
 * 
 * @creator Cameron Morrow
 * 
 * @requires Errors
 */
var Text = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"Text",
	
	// Properties
	_lists:								{},
	_list_counter:						0,
	
	/**
	 * init() initialises the Text object.
	 */
	init:								function() {
		
		// Log
		log("init", "Initialising " + this._CLASS + " (v" + this._VERSION + ")");
		log("info", this._list_counter + " character lists available.")
	},
	
	/************************************************************************************
	 * CHAR LISTS
	 ***********************************************************************************/
	
	/**
	 * addCharList() adds a character list.
	 * 
	 * @param id	{String}	The ID of the character list.
	 * @param chrs	{Array}		A list of characters to be available in this list.
	 */
	addCharList:						function(id, chrs) {
		if (isArray(chrs) && isString(id)) {
			if (!this.hasCharList(id)) {
				this._lists[id] = chrs;
				this._list_counter++;
			} else {
				Alerts.error("There is already a character list with the ID '" + id + "'.", this, "addCharList");
			}
		}
	},
	
	/**
	 * hasCharList() returns whether a character list with the passed ID exists.
	 * 
	 * @param id	{String}	An ID of a character list to look for.
	 * 
	 * @return true if the character list exists, false otherwise.
	 * @type Boolean
	 */
	hasCharList:						function(id) {
		return isDefined(this._lists[id]) ? true : false;
	},
	
	/**
	 * getCharList() gets a character list.
	 * 
	 * @param id	{String}	An ID of a character list to get.
	 * 
	 * @return the character list if it exists, false if not.
	 * @type Array/Boolean
	 */
	getCharList:						function(id) {
		if (this.hasCharList(id)) {
			return this._lists[id];
		} else {
			return false;
		}
	},
	
	/************************************************************************************
	 * VALIDATION
	 ***********************************************************************************/
	
	/**
	 * strip() strips out invalid characters from a string, returning the string with
	 * invalid characters removed.
	 * 
	 * @param str	{String}	The string to remove chars from.
	 * @param chrs	{String}	An ID of a character list to get valid chars from.
	 * 
	 * @return the string with invalid chars removed.
	 * @type String
	 */
	strip:								function(str, chrs) {
		
		// Validate
		if (isString(str)) {
			if (this.hasCharList(chrs)) {
				
				// Get char list
				chrs = this.getCharList(chrs);
				
				// Create new string of valid chars in original str
				var stripped_str = "";
				for (var i = 0; i < str.length; i++) {
					if (chrs.search(str.charAt(i)) !== false) {
						stripped_str += str.charAt(i);
					}
				}
				
				// Return
				return stripped_str;
			} else {
				Alerts.error("No character list with the ID '" + chrs + "' was found.", this, "strip");
			}
		} else {
			Alerts.error("First argument (" + str + ") must be a string.", this, "strip");
		}
		
		// Fail
		return "";
	},
	
	/**
	 * getBadChars() gets an array of characters in a string that would be removed by the
	 * strip() method.
	 * 
	 * @param str	{String}	The string to check.
	 * @param chrs	{String}	An ID of a character list to get valid chars from.
	 * 
	 * @return an array of 'bad' characters.
	 * @type Array
	 */
	getBadChars:						function(str, chrs) {
		
		// Validate
		if (isString(str)) {
			if (this.hasCharList(chrs)) {
				
				// Get char list
				chrs = this.getCharList(chrs);
				
				// Get list of bad chars
				var bad = [];
				for (var i = 0; i < str.length; i++) {
					if (chrs.search(str.charAt(i)) === false) {
						if (bad.search(str.charAt(i)) === false) {
							bad.push(str.charAt(i));
						}
					}
				}
				
				// Return
				return bad;
			} else {
				Alerts.error("No character list with the ID '" + chrs + "' was found.", this, "getBadChars");
			}
		} else {
			Alerts.error("First argument (" + str + ") must be a string.", this, "getBadChars");
		}
		
		// Fail
		return [];
	},
	
	/**
	 * validate() returns whether a string contains only valid characters.
	 * 
	 * @param str	{String}	The string to inspect.
	 * @param chrs	{String}	An ID of a character list to get valid chars from.
	 * 
	 * @return true if the string contains only valid chars, false otherwise.
	 * @type Boolean
	 */
	validate:							function(str, chrs) {
		return this.strip(str, chrs) == str;
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/MiniCal.js ***/

var MiniCal = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"MiniCal",
	_POPUP_WIDTH:						200,
	_POPUP_HEIGHT:						280,
	
	_ELEMENT_ID:						"minical",
	_ELEMENT_CALENDAR_ID:				"calendar",
	_ELEMENT_TIME_ID:					"time",
	_ELEMENT_MONTHS_ID:					"months",
	_ELEMENT_CLOSE_ID:					"close",
	_ELEMENT_HOURS_ID:					"hours",
	_ELEMENT_MINUTES_ID:				"minutes",
	
	// Properties
	_selector:							false,
	_popup:								false,
	_date:								0,
	
	/**
	 * init() initializes the MiniCal object.
	 */
	init:								function() {
		log("init", "Initialising " + this._CLASS + " (v" + this._VERSION + ")");
	},
	
	/**
	 * open() opens the MiniCal for a passed Date/Time selector.
	 * 
	 * @param selector	{DateTimeSelectorDOM}	The selector object to open the calendar
	 * 											for.
	 */
	open:								function(selector) {
		this.setActiveSelector(selector);
		this.setActiveDate(selector.getAbsoluteValue());
		this.openPopup();
	},
	
	/**
	 * close() closes the MiniCal.
	 */
	close:								function() {
		this.unsetActiveSelector();
	},
	
	/************************************************************************************
	 * DISPLAY METHODS
	 ***********************************************************************************/
	
	/**
	 * updateDisplay() updates the Calendar display when the date changes, or when
	 * the calendar is opened.
	 */
	updateDisplay:						function() {
		this.displayHeader();
		this.displayCalendar();
		this.displayTime();
	},
	
	/**
	 * displayCalendar() updates the Calendar part of the Popup.
	 */
	displayCalendar:					function() {
		
		if ($(this._ELEMENT_ID + "-" + this._ELEMENT_CALENDAR_ID)) {
			
			// Get element & date
			var c = $(this._ELEMENT_ID + "-" + this._ELEMENT_CALENDAR_ID);
			var d = Date.create(this._date);
			
			// Reset
			c.innerHTML = "";
			
			// Draw day names
			for (var i = 0; i < 7; i++) {
				c.appendChild(this._createDayBlock(["dayname"], (Date.getAllDayNames()[i]).charAt(0)));
			}
			
			// Draw spacers
			for (var i = 0; i < d.getDayOf1st(); i++) {
				c.appendChild(this._createDayBlock(["spacer"], "&nbsp;"));
			}
			
			// Draw days
			for (var i = 1; i <= d.getMaxDate(); i++) {
				var classes = ["day"];
				
				if (i == d.getDate()) {
					classes.push("active");
				}
				
				c.appendChild(this._createDayBlock(classes, String(i), "MiniCal.dayBlockClick(" + i + ");"));
			}
		}
	},
	
	/**
	 * displayHeader() updates the display of the Header.
	 */
	displayHeader:						function() {
		
		// If element found
		if ($(this._ELEMENT_ID + "-" + this._ELEMENT_MONTHS_ID)) {
		
			// Update header
			$(this._ELEMENT_ID + "-" + this._ELEMENT_MONTHS_ID + "-title").innerHTML = (Date.create(this._date)).format("F, Y");
		}
	},
	
	/**
	 * displayTime() updates the display of the time.
	 */
	displayTime:						function() {
		
		// If elements found
		if ($(this._ELEMENT_ID + "-" + this._ELEMENT_HOURS_ID) && $(this._ELEMENT_ID + "-" + this._ELEMENT_MINUTES_ID)) {
			
			// Get elements and date
			var h = $(this._ELEMENT_ID + "-" + this._ELEMENT_HOURS_ID);
			var m = $(this._ELEMENT_ID + "-" + this._ELEMENT_MINUTES_ID);
			var d = Date.create(this._date);
			
			// Update
			h.value = d.getHours();
			m.value = d.getMinutes();
		}
	},
	
	/************************************************************************************
	 * DOM METHODS
	 ***********************************************************************************/
	
	monthBlockClick:					function(offset) {
		
		// Create date based on current date
		var d = Date.create(this._date);
		
		// Offset month
		if (offset > 0) {
			d.incrementMonth(offset);
		} else {
			d.decrementMonth(Math.abs(offset));
		}
		
		// Store
		this._date = d.time();
		
		// Update appearance
		this.updateDisplay();
		
		// Apply
		this.applyDateToSelector();
	},
	
	dayBlockClick:						function(date) {
		
		// Create date based on current date and new day
		var d = Date.create(this._date);
		
		// Set new day
		d.setDate(date);
		
		// Store
		this._date = d.time();
		
		// Update appearance
		this.updateDisplay();
		
		// Apply
		this.applyDateToSelector();
	},
	
	hourChange:							function() {
		
		// Vars
		var e = $(this._ELEMENT_ID + "-" + this._ELEMENT_HOURS_ID);
		var v = e.value;
		var d = Date.create(this._date);
		
		// If valid
		if (Date.isValidHour(v)) {
			
			// Set new value
			d.setHours(v);
			
			// Store
			this._date = d.time();
			
			// Apply
			this.applyDateToSelector();
			
			// Hide Balloon if neccessary
			Balloon.hideIfElement(e);
			
			// Update
			this.updateDisplay();
		} else {
			Balloon.showAtElement("<h4>Invalid Hour value</h4><p>The hours value must contain a number between 0 and 23.</p>", e);
		}
	},
	
	minuteChange:						function() {
		
		// Vars
		var e = $(this._ELEMENT_ID + "-" + this._ELEMENT_MINUTES_ID);
		var v = e.value;
		var d = Date.create(this._date);
		
		// If valid
		if (Date.isValidMinute(v)) {
			
			// Set new value
			d.setMinutes(v);
			
			// Store
			this._date = d.time();
			
			// Apply
			this.applyDateToSelector();
			
			// Hide Balloon if neccessary
			Balloon.hideIfElement(e);
			
			// Update
			this.updateDisplay();
		} else {
			Balloon.showAtElement("<h4>Invalid Minute value</h4><p>The hours value must contain a number between 0 and 59.</p>", e);
		}
	},
	
	_createDayBlock:					function(classes, label, on_click) {
		
		// Check variables
		classes		= isArray(classes)		? classes	: [];
		label		= isString(label)		? label		: "";
		on_click	= isString(on_click)	? on_click	: "";
		
		// Generate
		var e = $(createElement("div",
			createElement("span", label)
		)).addClassName("dayblock");
		
		// Add events
		eval("e.onclick = function() { " + on_click + " }");
		
		for (var i = 0; i < classes.length; i++) {
			e.addClassName(classes[i]);
		}
		
		return e;
	},
	
	/***********************************************************************************
	 * POPUP METHODS
	 ***********************************************************************************/
	
	initPopup:							function() {
		if (!this._popup) {
			this._popup = new Popup("<div id=\"" + this._ELEMENT_ID + "\"></div>", {
				type:			["minical"],
				title:			"Calendar",
				has_close_box:	false,
				modal:			true,
				width:			this._POPUP_WIDTH,
				height:			this._POPUP_HEIGHT,
				buttons:		[
					new PopupButton("close", {
						label:		"Close",
						on_click:	"MiniCal.closePopup();",
						width:		150
					})
				]
			});
		}
	},
	
	openPopup:							function() {
		
		// Ensure popup is initialised
		this.initPopup();
		
		// Open
		Popups.open(this._popup);
		
		// Setup
		this.setPopupContent();
		
		// Display calendar
		this.updateDisplay();
		
		// Ensure it is not hidden
		this._popup.unhide();
	},
	
	closePopup:							function() {
		Popups.close(this._popup.getID());
	},
	
	setPopupContent:					function() {
		
		// If drawn
		if ($(this._ELEMENT_ID)) {
			
			// Get reference
			var e = $(this._ELEMENT_ID);
			
			// Reset
			e.innerHTML = "";
			
			// Set height
			Page.simpleResize(e, this._POPUP_WIDTH, this._POPUP_HEIGHT - Popups.BUTTONS_HEIGHT - Popups.HEADER_HEIGHT);
			
			// Add month changer
			e.appendChild(createElement("div",
				["id", this._ELEMENT_ID + "-" + this._ELEMENT_MONTHS_ID],
				$(createElement("button",
					"&larr;",
					["onclick", "MiniCal.monthBlockClick(-1);"],
					["id", this._ELEMENT_ID + "-" + this._ELEMENT_MONTHS_ID + "-prev"]
				)),
				$(createElement("button",
					"&rarr;",
					["onclick", "MiniCal.monthBlockClick(1);"],
					["id", this._ELEMENT_ID + "-" + this._ELEMENT_MONTHS_ID + "-next"]
				)),
				createElement("h4",
					["id", this._ELEMENT_ID + "-" + this._ELEMENT_MONTHS_ID + "-title"],
					(Date.create(this._date)).format("F, Y")
				)
			));
			
			// Add calendar
			e.appendChild(createElement("div",
				["id", this._ELEMENT_ID + "-" + this._ELEMENT_CALENDAR_ID]
			));
			
			// Add time
			e.appendChild(createElement("div",
				["id", this._ELEMENT_ID + "-" + this._ELEMENT_TIME_ID],
				createElement("form",
					"Time: ",
					createElement("input",
						["id", this._ELEMENT_ID + "-" + this._ELEMENT_HOURS_ID],
						["type", "text"],
						["size", "2"],
						["maxlength", "2"],
						["onblur", "MiniCal.hourChange();"]
					),
					":",
					createElement("input",
						["id", this._ELEMENT_ID + "-" + this._ELEMENT_MINUTES_ID],
						["type", "text"],
						["size", "2"],
						["maxlength", "2"],
						["onblur", "MiniCal.minuteChange();"]
					)
				)
			));
		}
	},
	
	/************************************************************************************
	 * DATE METHODS
	 ***********************************************************************************/
	
	setActiveDate:						function(date) {
		this._date = date;
	},
	
	applyDateToSelector:				function() {
		if (this._selector) {
			this._selector.setValue(this._date);
		}
	},
	
	/************************************************************************************
	 * SELECTOR METHODS
	 ***********************************************************************************/
	
	/**
	 * setActiveSelector() sets the active selector.
	 * 
	 * @param selector	{DateTimeSelectorDOM}	The selector object.
	 */
	setActiveSelector:					function(selector) {
		this._selector = selector;
	},
	
	/**
	 * unsetActiveSelector() unsets the active selector.
	 */
	unsetActiveSelector:				function() {
		this._selector = false;
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/Files.js ***/

var Files = {

	// Constants
	_CLASS:								"Files",
	_VERSION:							1.0,
	
	/**
	 * init() initialises the Files object.
	 */
	init:								function() {
		log("init", "Initialising " + this._CLASS + " (v" + this._VERSION + ")");
	},
	
	/**
	 * getFilesIn() gets a list of files in a list of directories.
	 * 
	 * @param dirs				{Array}		An array of directories.
	 * @param params			{Object}	Additional parameters to pass to the request.
	 * @param success_method	{Function}	The method to call if the files are retrieved
	 * 										successfully. This method will be passed two
	 * 										arguments - an array of files, and the
	 * 										Request parameters.
	 * @param failure_method	{Function}	The method to call if the files are not
	 * 										retrieved. Note - this is currently not
	 * 										functional.
	 */
	getFilesIn:							function(dirs, params, success_method, failure_method) {
		
		// Validate
		dirs			= isArray(dirs)					? dirs				: (isString(dirs) ? [dirs] : false);
		params			= isObject(params)				? params			: {};
		success_method	= isFunction(success_method)	? success_method	: Request._genericSuccess;
		failure_method	= isFunction(failure_method)	? failure_method	: Request._genericFailure;
		
		// Check
		if (isArray(dirs)) {
			if (isFunction(success_method)) {
			
				// Add directories
				for (var i = 0; i < dirs.length; i++) {
					params["dir" + (i + 1)] = dirs[i];
				}
				
				// Set up methods to call
				params.__success = success_method;
				params.__failure = failure_method;
				
				// Make request
				Request.make("com.inca2.core.admin.files.home.readdirectory", params, Files.getFilesInSuccess, Files.getFilesInFailure, false, "Getting file list...");
			} else {
				Alerts.error("Third argument (success method) must be a function.", this, "getFilesIn");
			}
		} else {
			Alerts.error("First argument (directories) must be an array or a string.", this, "getFilesIn");
		}
	},
	
	/**
	 * getFilesInFailure() is triggered when an error occurs fetching a list of files in
	 * a directory or directories.
	 * 
	 * @param transport	{XMLTransport}	The transport object.
	 */
	getFilesInFailure:					function(transport) {
		Loader.stop();
		log("ajax error", "Failed to retrieve file list.");
		// TODO: Figure out how to get failure method from parameters and call it
	},
	
	/**
	 * getFilesInSuccess() is called when a successful response is returned from an
	 * attempt to get a file list.
	 * 
	 * @param transport	{XMLTransport}	The transport object.
	 * @param json		{Object}		The returned JSON object.
	 */
	getFilesInSuccess:					function(transport, json) {
		
		// Get parameters
		var params = Request.getStored(json.request_id).params;
		
		// Get file names
		var files = [];
		for (var i = 0; i < json.data.values.length; i++) {
			files.push(json.data.values[i]);
		}
		
		// Call the success method
		params.__success(files, params);
	},

	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/JSON.js ***/

var JSON = {
	
	// Constants
	_CLASS:								"JSON",
	_VERSION:							1.0,
	
	// Properties
	_status:							Admin.OBJECT_STATUS_UNINITIALISED,
	
	init:								function() {
		log("init", "Initialising " + this._CLASS + " (v" + this._VERSION + ")");
		this._status = Admin.OBJECT_STATUS_INITIALIZED;
	},
	
	convert:							function(variable) {
		if (isBoolean(variable)) {
			return this._convertBoolean(variable);
		} else if (isString(variable)) {
			return this._convertString(variable);
		} else if (isNumber(variable)) {
			return this._convertNumber(variable);
		} else if (isArray(variable)) {
			return this._convertArray(variable);
		} else if (isObject(variable) && !isFunction(variable)) {
			return this._convertObject(variable);
		}
		
		return "!ERROR!";
	},
	
	_convertBoolean:					function(variable) {
		return variable ? "true" : "false";
	},
	
	_convertString:						function(variable) {
		return "\"" + variable.split("\"").join("\\\"") + "\"";
	},
	
	_convertNumber:						function(variable) {
		return String(variable);
	},
	
	_convertArray:						function(variable) {
		var children = [];
		for (var i = 0; i < variable.length; i++) {
			children.push(this.convert(variable[i]));
		}
		
		return "[" + children.join(",") + "]";
	},
	
	_convertObject:						function(variable) {
		
		var children = [];
		for (var i in variable) {
			children.push(this.convert(i) + ":" + this.convert(variable[i]));
		}
		
		return "{" + children.join(",") + "}";
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/ListBoxes.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/list_boxes.js
 * FUNCTION:		Contains the ListBoxes object
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-09
 ***************************************************************************************/

/**
 * The ListBoxes object contains a register of ListBox items.
 */
var ListBoxes = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"ListBoxes",
	OPTIMIZED_THRESHOLD:				100,
	
	// Properties
	_lbs:								{},
	
	/**
	 * init() initialises the ListBoxes object.
	 */
	init:								function() {
		
		// Log
		log("init", "Initialising ListBoxes (v" + this._VERSION + ")");
	},
	
	/**
	 * create() creates and returns a new ListBox object.
	 * 
	 * @param id		{String}	The ID of the listbox. Used for registering, DOM
	 * 								element creation, etc.
	 * @param label		{String}	The label for the ListBox.
	 * @param params	{Object}	An object containing parameters for the ListBox.
	 * @param columns	{Array}		An array of column information for the List.
	 * @param values	{Array}		An array of rows of values.
	 */
	create:								function(id, label, params, columns, values) {
		this._lbs[id] = new ListBox(id, label, params, columns, values);
		
		return this._lbs[id];
	},
	
	/**
	 * has() returns whether a ListBox with the passed ID exists.
	 * 
	 * @param id	{String}	The ID of the ListBox.
	 * 
	 * @return true if the ListBox exists, false if not.
	 * @type Boolean
	 */
	has:								function(id) {
		
		// Check arguments
		id = id || null;
		
		// Check existance
		return isObject(this._lbs[id]);
	},
	
	/**
	 * get() gets a ListBox if it exists.
	 * 
	 * @param id	{String}	The ID of the ListBox.
	 * 
	 * @return the ListBox if it exists, false if not.
	 * @type ListBox/Boolean
	 */
	get:								function(id) {
		
		// Check arguments
		id = id || null;
		
		// If exists, return it
		if (this.has(id)) {
			return this._lbs[id];
		}
		
		// Fail
		return false;
	},
	
	/************************************************************************************
	 * PASS-THROUGH METHODS
	 ***********************************************************************************/
	
	/**
	 * sort() passes a Sort command to a registered ListBox.
	 * 
	 * @param id		{String}	The ListBox to sort.
	 * @param column	{String}	The column to sort on.
	 */
	sort:								function(id, column) {
		
		// If ListBox exists...
		if (this.has(id)) {
			
			// .. pass the sort on
			this._lbs[id].sort(column);
		}
	},
	
	/**
	 * search() passes a Search command to a registered ListBox.
	 * 
	 * @param id	{String}	The ListBox to search.
	 */
	search:								function(id) {
		if (this.has(id)) {
			this._lbs[id].search();
		}
	},
	
	/************************************************************************************
	 * SORTING
	 ***********************************************************************************/
	
	/**
	 * compareValuesNumeric() compares two ListBox columns with numeric values.
	 * 
	 * @param a	{Object}	The first column.
	 * @param b	{Object}	The second column.
	 * 
	 * @return 1 if the numeric value of a is greater than b, -1 if b > a, 0 otherwise.
	 * @type Integer
	 */
	compareValuesNumeric:				function(a, b) {
		
		a = isNumeric(a["value"]) ? Number(a["value"]) : 0;
		b = isNumeric(b["value"]) ? Number(b["value"]) : 0;
		
		if (a === null && b !== null) {
			return 1;
		} else if (a !== null && b === null) {
			return -1;
		} else if (a > b) {
			return 1;
		} else if (a < b) {
			return -1;
		} else {
			return 0;
		}
	},
	
	compareValues:						function(a, b) {
		
		a = a["value"];
		b = b["value"];
		
		if (a === null && b !== null) {
			return 1;
		} else if (a !== null && b === null) {
			return -1;
		} else if (a > b) {
			return 1;
		} else if (a < b) {
			return -1;
		} else {
			return 0;
		}
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/objects/TreeBoxes.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/tree_boxes.js
 * FUNCTION:		Contains the TreeBoxes object
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-13
 ***************************************************************************************/

/**
 * The TreeBoxes object maintains the registry of TreeBox instances.
 * 
 * @creator Cameron Morrow
 */
var TreeBoxes = {
	
	// Constants
	_VERSION:							1.0,
	_CLASS:								"TreeBoxes",
	
	// Properties
	_tbs:								{},
	_ids:								0,
	_dragging:							{
		tb: false,
		id: false
	},
	
	/**
	 * init() initialises the TreeBoxes object.
	 */
	init:								function() {
		
		// Log
		log("init", "Initialising TreeBoxes (v" + this._VERSION + ")");
	},
	
	getUniqueID:						function() {
		
		// Increment ID
		this._ids++;
		
		// Return ID
		return this._ids;
	},
	
	/************************************************************************************
	 * INSTANCE-HANDLING METHODS
	 ***********************************************************************************/
	
	/**
	 * create() creates and registers a new TreeBox.
	 * 
	 * @param id
	 * @param label
	 * @param params
	 * @param data
	 * 
	 * @return the created TreeBox object, ready for use.
	 * @type TreeBox
	 */
	create:								function(id, label, params, data) {
		
		// Create TreeBox object and register it
		this._tbs[id] = new TreeBox(id, label, params, data);
		
		// Return for use
		return this._tbs[id];
	},
	
	/**
	 * has() returns whether a TreeBox with the passed ID exists.
	 * 
	 * @param id	{String}	The ID of the TreeBox to look for.
	 * 
	 * @return true if the TreeBox exists, false if not.
	 * @type Boolean
	 */
	has:								function(id) {
		return isObject(this._tbs[id]);
	},
	
	/**
	 * get() gets a TreeBox with the passed ID.
	 * 
	 * @param id	{String}	The ID of the TreeBox to look for.
	 * 
	 * @return the TreeBox with the passed ID if it exists, false if not.
	 * @type TreeBox/Boolean
	 */
	get:								function(id) {
		if (this.has(id)) {
			this._tbs[id];
		}
		
		return false;
	},
	
	clone:								function(branch, new_id) {
		// ??
	},
	
	/************************************************************************************
	 * DRAGGING AND EXPANSIONS
	 ***********************************************************************************/
	
	/**
	 * toggleExpansion() toggles the expansion of a branch in a TreeBox.
	 * 
	 * @param tb_id	{String}	The ID of the TreeBox.
	 * @param id	{String}	The ID of the branch.
	 */
	toggleExpansion:					function(tb_id, id) {
		if (this.has(tb_id)) {
			var branch = this._tbs[tb_id].getBranch(id);
			
			if (branch) {
				branch.toggleExpansion();
			}
		}
	},
	
	/**
	 * startDragging() starts the dragging of a Branch in a TreeBox.
	 * 
	 * @param tb_id	{String}	The ID of the TreeBox.
	 * @param id	{String}	The ID of the branch.
	 */
	startDragging:						function(tb_id, id) {
		
		// Store parameters
		this._dragging.tb = tb_id;
		this._dragging.id = id;
		
		Admin.updateTitle("starting: " + tb_id + "; " + id);
	},
	
	/**
	 * stopDragging() stops the dragging of a Branch in a TreeBox.
	 * 
	 * @param tb_id	{String}	The ID of the TreeBox.
	 * @param id	{String}	The ID of the branch.
	 */
	stopDragging:						function(tb_id, id) {
		
		if (this._dragging.tb == tb_id && this._dragging.id !== false && id !== this._dragging.id) {
			this._tbs[this._dragging.tb].moveBranch(this._dragging.id, id);
			this._tbs[this._dragging.tb].redraw();
			
			this._dragging.tb = false;
			this._dragging.id = false;
		}
		
		Admin.updateTitle("stopping: " + tb_id + "; " + id);
	},
	
	end: true
};

/*** @inca-include ../admin/assets/scripts/classes/LogItem.js ***/

/**
 * LogItem() instantiates a LogItem object.
 * 
 * @param type		{String}	The type of item.
 * @param message	{String}	The log message.
 * @param date		{Integer}	The date this item was generated.
 */
function LogItem(type, message, date) {
	
	// Store parameters
	this._type			= LogItem.parseType(type);
	this._message		= message;
	this._date			= date;
	this._drawn			= false;
}

LogItem.parseType						= function(type) {
	if (isArray(type)) {
		return type;
	} else if (isString(type)) {
		return type.split(" ");
	} else {
		return [];
	}
}

/**
 * LogItem.getType() gets the type of log item.
 * 
 * @return the value of the _type property.
 * @type String
 */
LogItem.prototype.getType				= function() {
	return this._type;
}

/**
 * LogItem.getMessage() gets the message of a log item.
 * 
 * @return the value of the _message property.
 * @type String
 */
LogItem.prototype.getMessage			= function() {
	return this._message;
}

/**
 * getDate() gets the date of a log item.
 * 
 * @return the value of the _date property.
 * @type Integer
 */
LogItem.prototype.getDate				= function() {
	return this._date;
}

/**
 * getDrawn() gets whether this log item has been drawn or not.
 * 
 * @return the value of the _drawn property.
 * @type Boolean
 */
LogItem.prototype.getDrawn				= function() {
	return this._drawn;
}

/**
 * setDrawn() sets whether this item has been drawn.
 * 
 * @param drawn	{Boolean}	Whether this item has been drawn.
 */
LogItem.prototype.setDrawn				= function(drawn) {
	this._drawn = drawn;
}

/*** @inca-include ../admin/assets/scripts/classes/ComponentCategory.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/component_category.js
 * FUNCTION:		Contains the ComponentCategory class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-02
 ***************************************************************************************/

/**
 * The ComponentCategory class contains components.
 * 
 * @param id		{String}		The ID of the category.
 * @param name		{String}		The name/label of the category.
 * @param 3 - n		{Component}		All subsequent arguments are Components to add to
 * 									this Category.
 */
function ComponentCategory(id, name) {
	
	// Constants
	this._CLASS			= "ComponentCategory";
	
	// Properties
	this._id			= id;
	this._name			= name;
	this._components	= {};
	
	// Components
	for (var i = 2; i < arguments.length; i++) {
		this.addComponent(arguments[i]);
	}
}

/**
 * getID() gets the Category's ID.
 * 
 * @return the ID.
 * @type String
 */
ComponentCategory.prototype.getID					= function() {
	return this._id;
}

/**
 * getName() gets the Category's name.
 * 
 * @return the name.
 * @type String
 */
ComponentCategory.prototype.getName					= function() {
	return this._name;
}

/****************************************************************************************
 * COMPONENT METHODS
 ***************************************************************************************/

/**
 * addComponent() adds a Component to this Category.
 * 
 * @param component	{Component}	The Component object to add.
 */
ComponentCategory.prototype.addComponent			= function(component) {
	if (isObject(component)) {
		this._components[component.getID()] = component;
	}
	
	return component;
}

/**
 * getComponents() returns the object containing the Components in this Category.
 * 
 * @return the Components.
 * @type Object
 */
ComponentCategory.prototype.getComponents			= function() {
	return this._components;
}

/**
 * getComponent() gets a Component in this Category.
 * 
 * @param id	{String}	The ID of the Component.
 * 
 * @return the Component if found; false if not.
 * @type Component/Boolean
 */
ComponentCategory.prototype.getComponent			= function(id) {
	if (this.hasComponent(id)) {
		return this._components[id];
	} else {
		return false;
	}
}

/**
 * hasComponent() returns whether this Category has the requested Component.
 * 
 * @param id	{String}	The ID of the component to look for.
 * 
 * @return true if the Component with the ID is found, false if not.
 * @type Boolean
 */
ComponentCategory.prototype.hasComponent			= function(id) {
	return isDefined(this._components[id]);
}

/****************************************************************************************
 * DOM METHODS
 ***************************************************************************************/

/**
 * drawListElement() draws the <li> element for this Category in the Component List.
 * 
 * @return the generated <li> DOM Element.
 * @type DOMElement
 */
ComponentCategory.prototype.drawListElement			= function() {
	
	// Create LI
	var li = createElement("li", ["id", "cl-" + this.getID()]);
		
	// Create header
	var header = createElement("div",
		createElement("a",
			["href", "javascript:INCA2Components.clickListCategory('" + this.getID() + "');"],
			createElement("h4", this.getName())
		)
	).addClassName("header");
	
	// Add a back-reference
	header._object = this;
	
	// Create items
	var ul = createElement("ul");
	for (var i in this._components) {
		if (this._components[i].pageCount() > 0) {
			ul.appendChild(this._components[i].drawListElement());
		}
	}
	
	// Attach items
	li.appendChild(header);
	li.appendChild(ul);
	
	// Return
	return li;
}

/**
 * drawSelectorElement() draws the <li> element for this Category in the Component
 * Selector.
 * 
 * @return the generated <li> DOM Element.
 * @type DOMElement
 */
ComponentCategory.prototype.drawSelectorElement		= function() {
	
	// Create LI
	var li = createElement("li", ["id", "cs-" + this.getID()]);
	
	// Create header
	var header = createElement("div",
		createElement("a",
			["href", "javascript:INCA2Components.clickSelectorCategory('" + this.getID() + "');"],
			["onmouseover",	"javascript:INCA2Components.selectorCategoryHover('" + this.getID() + "', true);"],
			["onmouseout",	"javascript:INCA2Components.selectorCategoryHover('" + this.getID() + "', false);"],
			["onmouseup",	"javascript:INCA2Components.selectorCategoryHover('" + this.getID() + "', false);"],
			createElement("h3", this.getName())
		)
	).addClassName("header");
	
	// Add a backreference
	header._object = this;
	
	// Add effects
	/*
	header.onmouseover = function() {
		Balloon.showAtElement("<strong>" + this._object.getName() + ":</strong> Click to see components in this Category", this, false);
	}
	header.onmouseout = function() {
		Balloon.hide();
	}
	*/
	
	// Create items
	var ul = createElement("ul");
	for (var i in this._components) {
		if (this._components[i].pageCount() > 0) {
			ul.appendChild(this._components[i].drawListElement());
		}
	}
	
	// Attach items
	li.appendChild(header);
	li.appendChild(ul);
	
	// Return
	return li;
}

/*** @inca-include ../admin/assets/scripts/classes/INCA2Component.js ***/

/**
 * The Component class contains a single Component.
 * 
 * @creator Cameron Morrow
 * 
 * @param id	{String}	The ID of the Component.
 * @param name	{String}	The name of the Component.
 * @param core	{Boolean}	Whether this is a core Component.
 * @param desc	{String}	A description of the Component.
 * @param pages	{Array}		An array of (initially unloaded) ComponentPage objects.
 */
function INCA2Component(id, name, core, desc, pages) {
	
	// Constants
	this._CLASS		= "INCA2Component";
	
	// Store parameters
	this._id		= id		|| "";
	this._name		= name		|| "";
	this._core		= core		|| false;
	this._desc		= desc		|| "";
	this._pages		= pages		|| [];
	
	// Update pages to have a reference to this component
	this._updatePageReferences();
	
	// Category is set post-instantiation
	this._category = null;
}

/**
 * setCategory() sets the category of the component.
 * 
 * @param category	{String}	The category ID.
 */
INCA2Component.prototype.setCategory						= function(category) {
	this._category = category;
}

/**
 * getID() gets the ID of the Component.
 * 
 * @return the ID.
 * @type String
 */
INCA2Component.prototype.getID							= function() {
	return this._id;
}

/**
 * getName() gets the name of the Component.
 * 
 * @return the name.
 * @type String
 */
INCA2Component.prototype.getName							= function() {
	return this._name;
}

/**
 * getCategory() gets the category ID of the Component.
 * 
 * @return the category ID.
 * @type String
 */
INCA2Component.prototype.getCategory						= function() {
	return this._category;
}

/**
 * getCore() gets whether this is a core Component.
 * 
 * @return true if the Component is a core Component.
 * @type Boolean
 */
INCA2Component.prototype.getCore							= function() {
	return this._core;
}

/****************************************************************************************
 * PAGES
 ***************************************************************************************/

/**
 * _updatePageReferences() updates the component references each Page has to be the
 * Component object itself, rather than it's ID. This is run post-instantiation.
 */
INCA2Component.prototype._updatePageReferences			= function() {
	for (var i = 0; i < this._pages.length; i++) {
		this._pages[i].setComponent(this);
	}
}

/**
 * getPages() gets the array of pages in this Component.
 * 
 * @return the pages array.
 * @type Array
 */
INCA2Component.prototype.getPages						= function() {
	return this._pages;
}

/**
 * getPage() gets a page in this Component if it exists.
 * 
 * @param page_id	{String}	The ID of the page.
 * 
 * @return the ComponentPage object if found, false if not.
 * @type ComponentPage/Boolean
 */
INCA2Component.prototype.getPage							= function(page_id) {
	for (var i = 0; i < this._pages.length; i++) {
		if (this._pages[i].getID() == page_id) {
			return this._pages[i];
		}
	}
	
	return false;
}

/**
 * hasPages() returns whether this component has any pages.
 * 
 * @return true if there is at least one page, false if not.
 * @type Boolean
 */
INCA2Component.prototype.hasPages						= function() {
	return this._pages.length > 0;
}

/**
 * hasPage() returns whether a passed page ID exists in this Component.
 * 
 * @param page_id	{String}	The ID of the page to look for.
 * 
 * @return true if the page exists, false if not.
 * @type Boolean
 */
INCA2Component.prototype.hasPage							= function(page_id) {
	return this.getPage(page_id) ? true : false;
}

/**
 * pageCount() returns a count on the pages this Component has.
 * 
 * @return the number of pages the component has.
 */
INCA2Component.prototype.pageCount						= function() {
	return this._pages.length;
}

/**
 * loadPage() attempts to load a Component page of this Component.
 * 
 * @param page_id	{String}	The ID of the page.
 */
INCA2Component.prototype.loadPage						= function(page_id) {
	
	// Get the requested page
	var page = this.getPage(page_id);
	
	if (page) {
		page.load();
	} else {
		log("error", "Component.loadPage: Page '" + page_id + "' does not exist.");
	}
}

INCA2Component.prototype.addPage							= function(page) {
	page.setComponent(this);
	this._pages.push(page);
	return page;
}

/****************************************************************************************
 * DOM METHODS
 ***************************************************************************************/

/**
 * drawListElement() draws the <li> element for this Component used in the Components
 * List and Selector dom elements.
 * 
 * @return the <li> DOM Element.
 * @type DOMElement
 */
INCA2Component.prototype.drawListElement					= function() {
	
	// If has a page, create link
	if (this.pageCount() > 0) {
		var li = createElement("li",
			createElement("a",
				["href", "javascript:INCA2Components.clickListComponent('" + this.getID() + "', '" + this._pages[0].getID() + "');"],
				this.getName()
			)
		);
	} else {
		return createElement("li");
		var li = createElement("li",
			createElement("em",
				this.getName()
			)
		);
	}
	
	// Give it a class
	if (this.getCore()) {
		$(li).addClassName("core");
	} else {
		$(li).addClassName("project");
	}
	
	// If pages exist
	if (this.pageCount() > 1) {
		var ul = createElement("ul");
		for (var i = 0; i < this._pages.length; i++) {
			var pli = createElement("li",
				createElement("a",
					["href", "javascript:INCA2Components.clickListPage('" + this.getID() + "', '" + this._pages[i].getID() + "');"],
					this._pages[i].getLabel()
				)
			);
			ul.appendChild(pli);
		}
		li.appendChild(ul);
	}
	
	// Display
	return li;

}

/*** @inca-include ../admin/assets/scripts/classes/ComponentItem.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/component_item.js
 * FUNCTION:		Contains the ComponentItem class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-08
 ***************************************************************************************/

/**
 * The ComponentItem class allows creation of individual items in the INCA2 Admin. Items
 * may be anything from Lists and TreeBoxes, to Buttons, to Forms, to containers for
 * other Items.
 * 
 * @creator Cameron Morrow
 * 
 * @param id		{String}	The ID of the Item.
 * @param label		{String}	The label of the Item.
 * @param type		{String}	The type of Item -- used when parsing.
 * @param data		{Object}	The source data object of this Item to parse.
 * @param parent	{Mixed}		The parent object of this Item.
 */
function ComponentItem(id, label, type, data, parent) {
	
	Alerts.track("Creating ComponentItem " + label + " (" + id + ") [" + type + "]", this, "ComponentItem");
	
	// Constants
	this._CLASS			= "ComponentItem";
	
	// Passed parameters
	this._id			= id		|| "";
	this._label			= label		|| "";
	this._type			= type		|| "";
	this._parent		= parent	|| false;
	this._items			= {};
	this._item_count	= 0;
	
	// Derived values
	this._element		= new GenericDOM(this._id);
	
	// Post-parse properties
	this._load_action	= false;
	
	// Check wrap and desc
	this._wrap			= isBoolean(data.wrap)			? data.wrap			: true;
	this._label_pos		= isIntegeric(data.label_pos)	? data.label_pos	: ComponentItem.NO_LABEL;
	this._desc			= isString(data.desc)			? data.desc			: false;
	
	// Parse
	this.parseData(data);
}

ComponentItem.NO_LABEL		= 0;
ComponentItem.LABEL_TOP		= 1;
ComponentItem.LABEL_LEFT	= 2;

/****************************************************************************************
 * Get/Set Methods
 ***************************************************************************************/

/**
 * getID() gets the ID of the Item.
 * 
 * @return the Item's ID.
 * @type String
 */
ComponentItem.prototype.getID			= function() {
	return this._id;
}

/**
 * getLabel() gets the label of this Item.
 * 
 * @return the label.
 * @type String
 */
ComponentItem.prototype.getLabel		= function() {
	return this._label;
}

/**
 * getDesc() returns the description for this item.
 * 
 * @return the description.
 * @type Boolean/String
 */
ComponentItem.prototype.getDesc			= function() {
	return this._desc;
}

/**
 * hasDesc() returns true if a desc exists, false if not.
 * 
 * @return whether a description exists for this Item.
 * @type Boolean
 */
ComponentItem.prototype.hasDesc			= function() {
	return this._desc ? true : false;
}

/**
 * getType() gets the type of Item.
 * 
 * @return the Item type.
 * @type String
 */
ComponentItem.prototype.getType			= function() {
	return this._type;
}

/**
 * getParent() gets the Parent object of this Item.
 * 
 * @return the parent object.
 * @type Object
 */
ComponentItem.prototype.getParent		= function() {
	return this._parent;
}

/**
 * getLoadAction() gets the action to call when this Item has loaded.
 * 
 * @return the action to run when this item has loaded.
 * @type String
 */
ComponentItem.prototype.getLoadAction	= function() {
	return this._load_action;
}

/**
 * hasLoadAction() returns whether a load action has been specified for this item.
 * 
 * @return true if a load action has been set.
 * @type Boolean
 */
ComponentItem.prototype.hasLoadAction	= function() {
	return this._load_action !== false;
}

/**
 * getItems() gets the value of the _items property for 'items' items.
 * 
 * @return the _items object.
 * @type Object
 */
ComponentItem.prototype.getItems		= function() {
	return this._items;
}

/**
 * getItem() gets an item under this item.
 * 
 * @param id		{String}	The ID of the item to get.
 * @param recursive	{Boolean}	Whether to search recursively under this item's children.
 * 
 * @return the item if found, false if not.
 * @type ComponentItem/Boolean
 */
ComponentItem.prototype.getItem			= function(id, recursive) {
	
	// For each item
	for (var i in this._items) {
		
		// If ID matches
		if (i == id) {
			return this._items[i];
			
		// Else if searching recursively...
		} else if (recursive) {
			
			// Check children of item
			var r = this._items[i].getItem(id, recursive);
			
			// If match, return, otherwise, proceed with loop
			if (r !== false) {
				return r;
			}
		}
	}
	
	// Fail
	return false;
}

/**
 * hasItem() returns whether an item with the passed ID exists under this item.
 * 
 * @param id		{String}	The ID of the item to look for.
 * @param recursive	{Boolean}	Whether to search recursively under this item's children.
 * 
 * @return whether the Item exists.
 * @type Boolean
 */
ComponentItem.prototype.hasItem			= function(id, recursive) {
	return this.getItem(id, recursive) ? true : false;
}

/**
 * getItemCount() returns the number of children Items.
 * 
 * @return the value of the _item_count property.
 * @type Integer
 */
ComponentItem.prototype.getItemCount	= function() {
	return this._item_count;
}

/**
 * getSubmitValues() gets values from this Item to submit in a form. By default returns
 * nothing - form items should override this method with their own implementation.
 * 
 * @return an empty object.
 * @type Object
 */
ComponentItem.prototype.getSubmitValues	= function() {
	return {};
}

/**
 * setValue() sets the value of this item - used for the contents of forms, etc. It
 * should be implemented directly by these types, but a base type is provided in any
 * case.
 * 
 * @param value	{Mixed}	The new value.
 */
ComponentItem.prototype.setValue		= function(value) {
	if (isObject(this._element)) {
		this.getElement().setValue(value);
	}
}

/**
 * setStructure() updates the structure of this item - used to dynamically update the
 * contents of drop downs, for example.
 * 
 * @return false.
 * @type Boolean
 */
ComponentItem.prototype.setStructure	= function(structure) {
	this.getElement().setStructure(structure);
}

/**
 * getElement() gets the element for an Item. The element handles DOM-related stuff.
 * 
 * @return the element.
 * @type Mixed
 */
ComponentItem.prototype.getElement		= function() {
	return this._element;
}

/**
 * applyValues() applies an object of item IDs and values to the children of this item,
 * updating their values if they have a value in the passed object.
 * 
 * @param values	{Object}	An object containing values to apply.
 */
ComponentItem.prototype.applyValues		= function(values) {
	
	// For each value
	for (var i in values) {
		
		// If ID is this item's
		if (i == this._id) {
			this.setValue(values[i]);
		} else {
			
			// Find item in children
			var o = this.getItem(i, true);
			
			// If found
			if (o !== false) {
				o.setValue(values[i]);
			} else {
				Alerts.error("Item " + i + " does not exist to apply value to.",
					this, "applyValues");
			}
		}
	}
}

/**
 * getPage() gets the Page that this item is in.
 * 
 * @return the ComponentPage object if found, false if not.
 * @type ComponentPage/Boolean
 */
ComponentItem.prototype.getPage			= function() {
	var t = this.getTab();
	
	if (isObject(t) && isDefined(t._page)) {
		return t._page;
	} else {
		var p = this._parent;
		while (p) {
			if (isDefined(p._CLASS) && p._CLASS == "ComponentPage") {
				return p;
			}
			p = p._parent;
		}
	}
	
	return false;
}

/**
 * getTab() gets the Tab that this item is in (if it is, in fact, in a Tab).
 * 
 * @return the ComponentTab object if found, false if not.
 * @type ComponentTab/Boolean
 */
ComponentItem.prototype.getTab			= function() {
	var t = this._parent;
	
	while (t) {
		if (isDefined(t._CLASS) && t._CLASS == "ComponentTab") {
			return t;
		}
		t = t._parent;
	}
	
	return false;
}

/****************************************************************************************
 * Drawing Methods
 ***************************************************************************************/

ComponentItem.prototype.draw			= function() {
	
	// Create div and draw
	var div = this.generateContainerDiv();
	
	if (this._label_pos == ComponentItem.NO_LABEL) {
		div.appendChild(this.getElement().draw());
	} else if (this._label_pos == ComponentItem.LABEL_LEFT) {
		div.appendChild(createElement("table",
			createElement("tbody",
				createElement("tr",
					createElement("td",
						this.drawFormInputLabel()
					).addClassName("inputleft"),
					createElement("td",
						this.getElement().draw()
					).addClassName("inputright")
				)
			)
		).addClassName("formrow"));
	} else {
		div.appendChild(createElement("table",
			createElement("tbody",
				createElement("tr",
					createElement("td",
						this.drawFormInputLabel()
					).addClassName("inputboth")
				),
				createElement("tr",
					createElement("td",
						this.getElement().draw()
					).addClassName("inputboth")
				)
			)
		).addClassName("formrow"));
	}
	
	// Done
	return div;
}

/**
 * drawFormInputLabel() draws the label for a Form Input
 * 
 * @return a DOM Element with the label.
 * @type DOMElement
 */
ComponentItem.prototype.drawFormInputLabel			= function(label) {
	
	label = label || this._label;
	
	return $(createElement("div",
		createElement("label",
			["for",		this._id],
			["title",	this._id + ": " + this._desc],
			label + ":"
		),
		createElement("p", this._desc)
	)).addClassName("labelholder");
}

/**
 * addChildItemsToElement() add drawn child items to a passed DOM Element.
 * 
 * @param element	{DOMElement}	The element to add drawn children to.
 */
ComponentItem.prototype.addChildItemsToElement		= function(element) {
	if ($(element)) {
		for (var i in this._items) {
			element.appendChild(this._items[i].draw());
		}
	}
}

/**
 * generateComtainerDiv() generates a generic item container, used in most (all) item
 * drawing methods.
 * 
 * @return a DOM Element.
 * @type DOMElement
 */
ComponentItem.prototype.generateContainerDiv		= function() {
	
	// Create 'branded' div with ID and class
	var div = createElement("div",
		["id", "item-" + this.getID()]
	).addClassName("item").addClassName(this._type + "item");
	
	// Add Back-reference
	div._object = this;
	
	// Return
	return div;
}

/****************************************************************************************
 * Parsing Methods
 ***************************************************************************************/

/**
 * parseItemChildren() parses child items of a created Item.
 * 
 * @param items	{Object}	An object of data to get child items from.
 */
ComponentItem.prototype.parseItemChildren			= function(items) {
	if (isObject(items)) {
		for (var i in items) {
			var item = items[i];
			if (item.id && isDefined(item.label) && item.type) {
				this._items[item.id] = new ComponentItem(item.id, item.label, item.type, item, this);
				this._item_count++;
			}
		}
	}
}

/*** @inca-include ../admin/assets/scripts/classes/component_item_extensions.js ***/

/**
 * getSubmitValuesItems() gets the values to submit from an item container.
 * 
 * @return an object with the item IDs and values.
 * @type Object
 */
ComponentItem.getSubmitValuesItems					= function() {
	
	// Create object
	var values = {};
	
	// For each child
	for (var i in this._items) {
		
		log("Getting submission value for: " + i);
		
		// Get item values
		var item_values = this._items[i].getSubmitValues();
		
		if (isObject(item_values)) {
			values = Objects.merge(values, item_values);
		} else {
			Alerts.error("Could not retrieve submission values for items of " + this.getID(), this, "getSubmitValuesItems");
			return false;
		}
	}
	
	// Return
	return values;
}

/****************************************************************************************
 * TYPE-SPECIFIC METHODS
 * These methods are attached to the Component Items during parsing, when it is
 * determined they are needed.
 ***************************************************************************************/

ComponentItem.setStructureMultiSelect	= function(structure) {
	this._element.setStructure(structure);
}

/**
 * getTreeBox() gets a TreeBox attached to this Tree item.
 * 
 * @return the TreeBox.
 * @type TreeBox
 */
ComponentItem.getTreeBox				= function() {
	return this._tb;
}

/**
 * getElement() gets the element for an Item. The element handles DOM-related stuff.
 * 
 * @return the element.
 * @type Mixed
 */
ComponentItem.getElement				= function() {
	return this._element;
}

/**
 * getRange() gets the range element from this CheckboxRange or RadioButtonRange object.
 * 
 * @return the range object.
 * @type CheckBoxRange/RadioButtonRange
 */
ComponentItem.getRange					= function() {
	return this._range;
}

/**
 * getValue() is used for visibility items to get their value (visibility).
 * 
 * @return the current status of the visibility.
 * @type Boolean
 */
ComponentItem.getValue					= function() {
	return this._value;
}

/****************************************************************************************
 * SUBMITTING METHODS
 ***************************************************************************************/

/**
 * getSubmitValuesSelect() gets the submit value of a select item.
 * 
 * @return an object with the item's ID and value.
 * @type Object
 */
ComponentItem.getSubmitValuesMultiSelect			= function() {
	
	// Create object
	var values = {};
	
	// Get current value
	values[this._id] = this.getElement().getSelectedValues().join(",");
	
	// Return
	return values;
}

/**
 * getSubmitValuesCheckbox() gets the submit value for a checkbox range - an array of
 * checked values.
 * 
 * @return an object with the item's ID and value.
 * @type Object
 */
ComponentItem.getSubmitValuesCheckbox				= function() {
	
	// Create object
	var values = {};
	
	// Get current value
	values[this._id] = this._range.getCheckedValues();
	
	// Return
	return values;
}

/**
 * getSubmitValuesCheckbox() gets the submit value for a radio button range - an array of
 * checked values.
 * 
 * @return an object with the item's ID and value.
 * @type Object
 */
ComponentItem.getSubmitValuesRadioButton			= function() {
	
	// Create object
	var values = {};
	
	// Get current value
	values[this._id] = this._range.getCheckedValues();
	
	// Return
	return values;
}

ComponentItem.getSubmitValuesList					= function() {
	
	// Get values object to populate, and element to get children of
	var values	= {};
	var element	= $("lb-" + this.getElement().getID());
	
	// Get values
	var values = Request.getInputValuesIn(element);
	
	// Create a wrapper object and convert values to JSON
	var obj = {};
	obj[this._id] = JSON.convert(values);
	
	// Done
	return obj;
}

/****************************************************************************************
 * SET VALUE METHODS
 ***************************************************************************************/

ComponentItem.setValueMultiSelect					= function(value) {
	this.getElement().setValue(value);
}

/**
 * setValueRange() sets the value of an Range (Radio Button or Checkbox) item.
 * 
 * @param value	{Array/Object/String}	Items to be checked.
 */
ComponentItem.setValueRange							= function(value) {
	this._range.setChecked(value);
}

/**
 * setValueLB() sets the value of an ListBox item.
 * 
 * @param value	{Array}	The value to set.
 */
ComponentItem.setValueLB							= function(value) {
	this.getElement().setValues(value);
}

/****************************************************************************************
 * RESIZING METHODS
 * Methods for dynamic resizing of elements when the page size changes.
 ***************************************************************************************/

/**
 * resize() is a generic resizer for elements, to be triggered when a page resize occurs
 * and the element is a 'scaling' element (ie not a fixed height).
 * 
 * @param height	{Integer}	The new height.
 */
ComponentItem.prototype.resize						= function(height) {
	return false;
}

/**
 * resizeLB() is an implementation of resizing for a ListBox element.
 * 
 * @param height	{Integer}	The new height.
 */
ComponentItem.resizeListBox							= function(height) {
	this.getElement().setHeight(height - 6);
}

/**********************************************************
 * DOM Elements
 *********************************************************/

ComponentItem.drawListBox							= function() {
	
	// Create div and draw
	var div = this.generateContainerDiv();
	div.appendChild(this.getElement().draw());
	
	// Register (TODO: Make this happen only if listbox isn't a fixed size...)
	Page.registerResizeElement(div);
	
	// Done
	return div;
}

ComponentItem.drawTreeBox							= function() {
	
	// Create div and draw
	var div = this.generateContainerDiv();
	div.appendChild(this.getElement().draw());
	
	// Done
	return div;
}

/**********************************************************
 * Specialised Form Elements
 *********************************************************/

ComponentItem.drawMultiSelect						= function() {
	
	// Create div and draw
	var div = this.generateContainerDiv();
	div.appendChild($(createElement("table",
		createElement("tbody",
			createElement("tr",
				$(createElement("td",
					this.drawFormInputLabel()
				)).addClassName("inputleft"),
				createElement("td",
					this.getElement().draw()
				)
			)
		)
	)).addClassName("formrow"));
	
	// Done
	return div;
}

/****************************************************************************************
 * PARSING
 ***************************************************************************************/

ComponentItem._parsers								= {};

ComponentItem.registerParser						= function(type, parser) {
	ComponentItem._parsers[type] = parser;
}

ComponentItem.hasParserFor							= function(type) {
	return ComponentItem._parsers[type] ? true : false;
}

ComponentItem.getParserFor							= function(type) {
	return ComponentItem.hasParserFor(type) ? ComponentItem._parsers[type] : false;
}

/**
 * parseData() parses the AJAX-returned data for this Item. Parsed data is added to
 * appropriate variables and methods to get this data are attached.
 */
ComponentItem.prototype.parseData					= function(data) {
	
	// Cache load action for future use
	if (isString(data.load_action)) {
		this.getPage().cacheLoadAction(data.load_action);
		// TODO: Store load action for later reloads (?)
	}
	
	if (ComponentItem.hasParserFor(this._type)) {
		
		// Get parser method
		var parser = ComponentItem.getParserFor(this._type);
		
		// Call parser method, with object
		parser(this, data);
	} else {
		switch (this._type) {
			
			/**************************************************
			 * DOM ELEMENTS
			 *************************************************/
			case "listbox":
				
				// Properties
				this._element				= ListBoxes.create(
					this.getID(),
					this.getLabel(),
					{
						width:				isString(data.width)				? data.width			: false,
						height:				isString(data.height)				? data.height			: false,
						min_row_height:		isIntegeric(data.min_row_height)	? data.min_row_height	: false
					},
					isArray(data.columns) ? data.columns : [],
					isArray(data.values) ? data.values : []
				);
				
				// Methods
				this.draw					= ComponentItem.drawListBox;
				this.getElement				= ComponentItem.getElement;
				this.setValue				= ComponentItem.setValueLB;
				this.resize					= ComponentItem.resizeListBox;
				this.getSubmitValues		= ComponentItem.getSubmitValuesList;
				break;
				
			case "treebox":
				break;
				
			/**************************************************
			 * SPECIALISED FORM INPUT ELEMENTS
			 *************************************************/
				
			case "multiselect":
				
				// Properties
				this._desc					= isString(data.desc) ? data.desc : false;
				this._element				= new MultiSelectorDOM(
					this.getID(),
					isObject(data.options) ? data.options : {},
					{
						value:		(isArray(data.value))		? data.value		: [],
						on_change:	isString(data.on_change)	? data.on_change	: false,
						size:		isInteger(data.size)		? data.size			: false
					}
				);
				
				// Methods
				this.draw					= ComponentItem.drawMultiSelect;
				this.getSubmitValues		= ComponentItem.getSubmitValuesMultiSelect;
				this.getElement				= ComponentItem.getElement;
				this.setValue				= ComponentItem.setValueMultiSelect;
				this.setStructure			= ComponentItem.setStructureMultiSelect;
				break;
		}
	}
	
	/******************************************************
	 * CHILDREN ITEMS
	 *****************************************************/
	this.parseItemChildren(data.items);
}

/*** @inca-include ../admin/assets/scripts/classes/ComponentTab.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/component_tab.js
 * FUNCTION:		Contains the ComponentTab class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-06
 ***************************************************************************************/

/**
 * The ComponentTab class contains a single Component Tab.
 * 
 * @creator Cameron Morrow
 * 
 * @param id	{String}		The ID of this Tab.
 * @param label	{String}		The label for this Tab.
 * @param items	{Object}		An object containing the items (unparsed) in this Tab.
 * @param page	{ComponentPage}	The page that contains this tab.
 */
function ComponentTab(id, label, items, page) {
	
	// Constants
	this._CLASS		= "ComponentTab";
	
	// Properties
	this._id		= id;
	this._label		= label;
	this._page		= page;
	this._elements	= {};
	
	// Items
	this._items		= {};
	this._parseItems(items);
	
	// Generate DOM
	this._generateDOM();
}

/**
 * getID() gets the ID of this Tab.
 * 
 * @return the ID.
 * @type String
 */
ComponentTab.prototype.getID			= function() {
	return this._id;
}

/**
 * getLabel() gets the Label of this Tab.
 * 
 * @return the label.
 * @type String
 */
ComponentTab.prototype.getLabel			= function() {
	return this._label;
}

/****************************************************************************************
 * ITEM METHODS
 ***************************************************************************************/

/**
 * getItems() gets the (parsed) Items in this Tab.
 * 
 * @return the object containing the Items.
 * @type Object
 */
ComponentTab.prototype.getItems			= function() {
	return this._items;
}

/**
 * getItem() gets an Item in this Tab.
 * 
 * @param item_id	{String}	The ID of the item to look for.
 * 
 * @return the ComponentItem if found, false if not.
 * @type ComponentItem/Boolean
 */
ComponentTab.prototype.getItem						= function(id) {
	
	// Check sidebar
	for (var i in this._items) {
		
		// If ID matches
		if (i == id) {
			return this._items[id];
			
		// Otherwise, search children
		} else {
			
			// Check children of item
			var r = this._items[i].getItem(id, true);
			
			// If match, return, otherwise, proceed with loop
			if (r !== false) {
				return r;
			}
		}
	}
	
	return false;
}

/**
 * hasItem() returns whether this Tab contains an item with the passed ID.
 * 
 * @param item_id	{String}	The ID of the item to look for.
 * 
 * @return whether the item exists in this Tab.
 * @type Boolean
 */
ComponentTab.prototype.hasItem						= function(id) {
	return this.getItem(id) ? true : false;
}

/**
 * addItem() adds an item to this Tab.
 * 
 * @param item	{ComponentItem}	The Item to add.
 */
ComponentTab.prototype.addItem			= function(item) {
	if (isObject(item)) {
		this._items[item.getID()] = item;
	}
}

/**
 * _parseItems() parses the passed items object (returned from an AJAX call) into a
 * series of ComponentItem objects.
 * 
 * @param items	{Object}	An object containing items.
 */
ComponentTab.prototype._parseItems		= function(items) {
	for (var i in items) {
		var item = items[i];
		if (item.id && item.label && item.type) {
			this.addItem(new ComponentItem(item.id, item.label, item.type, item, this));
		}
	}
}

// Inherit ComponentItem's submit item method
ComponentTab.prototype.getSubmitValues	= ComponentItem.getSubmitValuesItems;

/****************************************************************************************
 * DOM METHODS
 ***************************************************************************************/

ComponentTab.prototype._generateDOM		= function() {
	this._elements.body = createElement("li",
		["id", "tb-" + this.getID()]
	).addClassName("tabbodyli");
	
	for (var i in this._items) {
		this._elements.body.appendChild(this._items[i].draw());
	}
	
	this._elements.header = createElement("li",
		["id", "th-" + this.getID()],
		createElement("a",
			["href", "javascript:ComponentInterface.clickTab('" + this.getID() + "');"],
			createElement("h3",
				createElement("em",
					this.getLabel()
				)
			)
		)
	);
}

/**
 * draw() draws the Tab header and Body.
 */
ComponentTab.prototype.draw				= function() {
	Alerts.track("Drawing tab " + this._id + "!", this, "draw");
	$(ComponentInterface.TAB_HEADER_LIST_ID).appendChild(
		this.drawHeader()
	);
	$(ComponentInterface.TAB_BODY_LIST_ID).appendChild(
		this.drawBody()
	);
}

/**
 * drawBody() draws the Tab Body content.
 * 
 * @return an <li> DOM Element to attach to the Tab Body list.
 * @type DOMElement
 */
ComponentTab.prototype.drawBody			= function() {
	return this._elements.body;
}

/**
 * drawHeader() draws the Header item for this Tab.
 * 
 * @return an <li> DOM Element to display in the Tabs Header.
 * @type DOMElement
 */
ComponentTab.prototype.drawHeader		= function() {
	return this._elements.header;
}

/**
 * activate() changes the styles on this Tab to make it active.
 */
ComponentTab.prototype.activate			= function() {
	this._elements.body.addClassName("active");
	this._elements.header.addClassName("active");
}

/**
 * deactivate() changes the styles on this Tab to make it inactive.
 */
ComponentTab.prototype.deactivate		= function() {
	this._elements.body.removeClassName("active");
	this._elements.header.removeClassName("active");
}

/*** @inca-include ../admin/assets/scripts/classes/ComponentPage.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/component_page.js
 * FUNCTION:		Contains the ComponentPage class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-02
 ***************************************************************************************/

/**
 * The ComponentPage class handles a page in a Component. Each page can have it's own
 * tabs, sidebar structure, etc.
 * 
 * When a ComponentPage is first created, it does not have Tabs, Sidebar information,
 * etc. This information is loaded by an AJAX request to the Component's PHP.
 * 
 * @creator Cameron Morrow
 * 
 * @param id		{String}	The ID of the page.
 * @param label		{String}	The name of the page.
 */
function ComponentPage(id, label) {
	
	// Constants
	this._CLASS			= "ComponentPage";
	
	// Store parameters
	this._id			= id	|| "";
	this._label			= label	|| "";
	
	// Other properties
	this._component		= false;
	this._loaded		= false;
	this._tabs			= {};
	this._active_tab	= false;
	this._has_sidebar	= false;
	this._sidebar		= {};
}

/**
 * getID() gets the ID of this Component Page.
 * 
 * @return the ID.
 * @type String
 */
ComponentPage.prototype.getID						= function() {
	return this._id;
}

/**
 * getLabel() gets the Label of this Component Page.
 * 
 * @return the label.
 * @type String
 */
ComponentPage.prototype.getLabel					= function() {
	return this._label;
}

/**
 * getComponent() gets the Component this Page sits in.
 * 
 * @return the Component object that contains this page.
 * @type Component
 */
ComponentPage.prototype.getComponent				= function() {
	return this._component;
}

/**
 * setComponent() sets the Component this Page is in.
 * 
 * @param component	{Component}	The Component to set for this page.
 */
ComponentPage.prototype.setComponent				= function(component) {
	this._component = component;	
}

/**
 * getLoaded() gets whether this Component Page has been loaded.
 * 
 * @return true if a load has occured, false if not.
 * @type Boolean
 */
ComponentPage.prototype.getLoaded					= function() {
	return this._loaded;
}

/**
 * setLoaded() sets whether this Component Page has been loaded.
 * 
 * @param loaded	{Boolean}	Whether this page has been loaded.
 */
ComponentPage.prototype.setLoaded					= function(loaded) {
	this._loaded = loaded;
}

/**
 * getHasSidebar() returns whether this Page has a sidebar. Note - this method is useless
 * if called before a Load has occured.
 * 
 * @return true if the Page has a sidebar, false if not.
 * @type Boolean
 */
ComponentPage.prototype.getHasSidebar				= function() {
	return this._has_sidebar;
}

/***************************************************************************************
 * LOADING
 ***************************************************************************************/
 
/**
 * load() loads this Page from the PHP. Loading the Page pulls in structure data and
 * initial layout information.
 */
ComponentPage.prototype.load						= function() {
	
	// If not yet loaded
	if (this.getLoaded() != true) {
		
		// Log
		Alerts.track("Loading Page '" + this.getComponent().getID() + "." + this.getID() + "'", this, "load");
	
		// Create parameters
		var params = {
			//mode:		this.getComponent().getID() + "." + this.getID(),
			mode:		"com.inca2.core.admin.administration.loadpage",
			component:	this.getComponent().getID(),
			page:		this.getID()
		};
		
		// Make request
		Request.make(params.mode, params, this.loadSuccess, false, false, "Loading page...");
		
	// Else redraw from already loaded data
	} else {
		
		// Log
		Alerts.track("Already loaded Page '" + this.getComponent().getID() + "." + this.getID() + "', displaying", this, "load");
		
		// Display
		this.display();
	}
}

/**
 * loadSuccess() is called when a load() call returns AJAX data.
 * 
 * @param transport	{Object}	The XML Request object.
 * @param json		{Object}	The returned JSON data.
 */
ComponentPage.prototype.loadSuccess					= function(transport, json) {
	
	// Get parameters
	var params = Request.getStored(json.request_id).params;
	
	// Get reference to page object, as 'this' doesn't work on AJAX methods
	var page = INCA2Components.getComponentPage(params.component, params.page);
	
	// If page is found
	if (page) {
		
		// Reset list of load actions, ready for more
		page.resetLoadActions();
		
		Alerts.track("Page '" + page.getLabel() + "' parsing tabs and sidebar elements...", page, "loadSuccess");
		
		// Parse tabs and sidebar
		page.parseTabs(json.data);
		page.parseSidebar(json.data);
		
		// Display
		page.setLoaded(true);
		page.display();
		
		// Execute load actions
		page.executeLoadActions();
	} else {
		Alerts.error("Page object, expected to be found in JSON parameters, was not retrieved.", page, "loadSuccess");
	}
}

/**
 * display() displays a page, once it has been loaded.
 */
ComponentPage.prototype.display						= function() {
	
	Alerts.track("Page '" + this.getLabel() + "' rendering...", this, "display");
	
	// Draw
	this.redrawTabs();
	this.redrawSidebar();
	
	// Select a default tab to be active if non currently are
	this.selectDefaultActiveTab();
	
	// Make it active
	ComponentInterface.makeActivePage(this);
	
	// Redraw main interface
	ComponentInterface.redraw();
	
	// Resize elements
	Page.resizeElements();
}

ComponentPage.prototype.resetLoadActions			= function() {
	this._load_actions = [];
}

ComponentPage.prototype.cacheLoadAction				= function(action) {
	this._load_actions.push(action);
}

ComponentPage.prototype.executeLoadActions			= function() {
	
	// If there are actions to load
	if (this._load_actions.length > 0) {
		Alerts.track("Executing " + this._load_actions.length + " on-load action(s) for Page '" + this.getLabel() + "'", this, "executeLoadActions");
		this.executeLoadAction(0);
	} else {
		Alerts.track("No load actions to be executed for Page '" + this.getLabel() + "'", this, "executeLoadActions");
	}
}

ComponentPage.prototype.executeLoadAction			= function(index) {
	
	// Validate
	index	= index || 0;
	
	// Make request
	Request.make(this._load_actions[index], {_page: this, _index: index}, this.executeLoadActionSuccess, false, false, "Performing Load Action " + index + "...");
}

ComponentPage.prototype.executeLoadActionSuccess	= function(transport, json) {
	if (Request.handlePageDataResponse(json)) {
		
		// Get page
		var page	= Request.getStoredParamPage(json.request_id);
		var index	= Request.getStoredParam(json.request_id, "_index");
	
		// Load next action
		if (page._load_actions.length > index + 1) {
			page.executeLoadAction(index + 1);
		} else {
			log("conf", "Finished load actions (" + page._load_actions.length + " action(s) executed).");
		}
	}
}

/**
 * executeAction() performed an Action Request for this page.
 * 
 * @param action	{String}	The action to run.
 * @param params	{Object}	Parameters to pass to the action.
 * @param success	{Function}	The method to run on success.
 * @param failure	{Function}	The method to run on failure.
 * @param message	{String}	The message to display on the loader.
 */
ComponentPage.prototype.executeAction				= function(action, params, success, failure, message) {
	
	// Check arguments
	action		= isString(action) ? action	: "";
	params		= isObject(params) ? params	: {};
	success		= success	|| this.executeActionSuccess;
	failure		= failure	|| this.executeActionFailure;
	message		= message	|| "Executing Action...";
	
	// Add additional parameters
	params._page = this;
	
	// Pass request
	Request.make(action, params, success, failure, false, message);
}

/**
 * executeActionSuccess() is the default handler for a successful executeAction call. It
 * attempts to apply any returned values to the page, as well as displaying any messages.
 * 
 * @param transport	{XMLHTTPTransport}	The transport object.
 * @param json		{Object}			The returned JSON object.
 */
ComponentPage.prototype.executeActionSuccess		= function(transport, json) {
	if (Request.handlePageDataResponse(json)) {
		log("conf", "Page action was executed successfully.");
	}
}

/**
 * executeActionFailure() is the default handler for a failed executeAction call.
 */
ComponentPage.prototype.executeActionFailure		= function() {
	Loader.stop();
}

/****************************************************************************************
 * ITEMS
 ***************************************************************************************/

/**
 * getItem() gets an item from in this page. The item can be in either the sidebar or in
 * one of the tabs.
 * 
 * @param id	{String}	The Item ID.
 * 
 * @return the item if found, false if not.
 * @type ComponentItem/Boolean
 */
ComponentPage.prototype.getItem						= function(id) {
	
	// Check sidebar
	for (var i in this._sidebar) {

		// If ID matches
		if (i == id) {
			return this._sidebar[id];
			
		// Else search children
		} else {
			
			// Check children of item
			var r = this._sidebar[i].getItem(id);
			
			// If match, return, otherwise, proceed with loop
			if (r !== false) {
				return r;
			}
		}
	}

	// Check tabs	
	for (var i in this._tabs) {
		
		// Check tab's items
		var r = this._tabs[i].getItem(id);
		
		// If found, return, otherwise, proceed
		if (r !== false) {
			return r;
		}
	}
	
	return false;
}

/**
 * hasItem() returns whether this Page has an Item with the passed ID.
 * 
 * @param id	{String}	The Item ID.
 * 
 * @return true if the Item exists, false if not.
 * @type Boolean
 */
ComponentPage.prototype.hasItem						= function(id) {
	return this.getItem(id) ? true : false;
}

/**
 * applyValues() applies values to the items in this page.
 * 
 * @param values	{Object}	An object containing properties (Item IDs) and values
 * 								to apply to them.
 */
ComponentPage.prototype.applyValues					= function(values) {
	
	// For each value
	for (var i in values) {
		
		// Find item in children
		var o = this.getItem(i);
		
		// If found
		if (o !== false) {
			o.setValue(values[i]);
		} else {
			Alerts.error("Item " + i + " does not exist to apply value to.", this, "applyValues");
		}
	}
}

ComponentPage.prototype.applyStructure				= function(items) {
	
	// For each value
	for (var i in items) {
		
		// Find item in children
		var o = this.getItem(i);
		
		// If found
		if (o !== false) {
			o.setStructure(items[i]);
		} else {
			Alerts.error("Item " + i + " does not exist to apply structure to.", this, "applyStructure");
		}
	}
}

/**
 * getSubmitValues() collates submittable values from all items in this page's tabs.
 * 
 * @return an object containing submittable values for all inputs on all tabs.
 * @type Object
 */
ComponentPage.prototype.getSubmitValues				= function() {
	
	// Create object
	var values = {};
	
	// For each tab
	for (var i in this._tabs) {
		
		// Get item values
		var item_values = this._tabs[i].getSubmitValues();
		
		if (isObject(item_values)) {
			values = Objects.merge(values, item_values);
			Alerts.track("Retrieved " + Objects.getPropertyCount(item_values) + " submission value(s) from " + i + ": " + Objects.getProperties(item_values), this, "getSubmitValues");
		} else {
			Alerts.error("Tab " + i + " returned invalid submission values.", this, "getSubmitValues");
			return false;
		}
	}
	
	// Return
	return values;
}

/****************************************************************************************
 * SIDEBAR
 ***************************************************************************************/

/**
 * parseSidebar() parses returned Sidebar data for items.
 * 
 * @param data	{Object}	The data returned from an AJAX call.
 */
ComponentPage.prototype.parseSidebar	= function(data) {
	
	// If sidebar items were found
	if (data.sidebar.items) {
		
		// Note that we have sidebar, for when redrawing
		this._has_sidebar = true;
		
		// Parse the items
		for (var i in data.sidebar.items) {
			var item = data.sidebar.items[i];
			if (item.id && item.label && item.type) {
				this._sidebar[item.id] = new ComponentItem(item.id, item.label, item.type, item, this);
			}
		}
	} else {
		this._has_sidebar = false;
	}
}

ComponentPage.prototype.redrawSidebar	= function() {
	// Redraw each item
	$(ComponentInterface.SIDEBAR_ID).innerHTML = "";
	for (var i in this._sidebar) {
		$(ComponentInterface.SIDEBAR_ID).appendChild(
			this._sidebar[i].draw()
		);
	}
}

/****************************************************************************************
 * TABS
 ***************************************************************************************/

/**
 * parseTabs() parses returned data for tabs and their items.
 * 
 * @param data	{Object}	The data returned from an AJAX call.
 */
ComponentPage.prototype.parseTabs		= function(data) {

	// Reset list
	this.resetTabs();
	
	// If tabs were found
	if (data.tabs) {
		
		// For each tab, create a new ComponentTab object
		for (var i in data.tabs) {
			var tab = data.tabs[i];
			if (tab.id && tab.label && tab.items) {
				this.addTab(new ComponentTab(tab.id, tab.label, tab.items, this));
			}
		}
	}
}

/**
 * resetTabs() resets the tab list of this page.
 */
ComponentPage.prototype.resetTabs		= function() {
	this._tabs = {};
}

/**
 * addTab() adds a ComponentTab to this Page.
 * 
 * @param tab	{ComponentTab}	The ComponenTab to add to this page.
 */
ComponentPage.prototype.addTab			= function(tab) {
	this._tabs[tab.getID()] = tab;
}

/**
 * hasTab() returns whether this page has a Tab with the passed ID.
 * 
 * @param tab_id	{String}	The ID of the tab.
 * 
 * @return true if the Tab is found, false if not.
 * @type Boolean
 */
ComponentPage.prototype.hasTab			= function(tab_id) {
	
	// Check arguments
	tab_id = tab_id || null;
	
	// Return whether it exists
	return isObject(this._tabs[tab_id]);
}

/**
 * getTab() gets a Tab from this Page.
 * 
 * @param tab_id	{String}	The ID of the tab.
 * 
 * @return the Tab if found, false if not.
 * @type ComponentTab/Boolean
 */
ComponentPage.prototype.getTab			= function(tab_id) {
	
	// Check arguments
	tab_id = tab_id || null;
	
	// If tab exists
	if (this.hasTab(tab_id)) {
		return this._tabs[tab_id];
	}
	
	// Fail
	return false;
}

/**
 * activateTab() makes a tab with the passed ID active.
 * 
 * @param tab_id	{String}	The ID of the tab.
 */
ComponentPage.prototype.activateTab		= function(tab_id) {
	
	// Check arguments
	tab_id = tab_id || null;
	
	// If tab exists, activate it
	if (this.hasTab(tab_id)) {
		
		// Deactivate any others
		this.deactivateTabs(tab_id);
		
		// Update tab
		this._tabs[tab_id].activate();
		
		// Store
		this._active_tab = this._tabs[tab_id];
		
		// Redraw
		Page.onResize();
	}
}

/**
 * hasActiveTab() returns whether the page has an active tab.
 * 
 * @return true if an active tab has been set, false if not.
 * @type Boolean
 */
ComponentPage.prototype.hasActiveTab	= function() {
	return isObject(this._active_tab);
}

/**
 * getActiveTab() returns the active tab.
 * 
 * @return the active ComponentTab, or false if not found.
 * @type ComponentTab/Boolean
 */
ComponentPage.prototype.getActiveTab	= function() {
	if (this.hasActiveTab()) {
		return this._active_tab;
	}
	
	return false;
}

/**
 * selectDefaultActiveTab() selects a default tab to be active. If a tab is already
 * selected, it is re-activated to ensure the display is updated.
 */
ComponentPage.prototype.selectDefaultActiveTab		= function() {
	
	// If no active tab
	if (!this.hasActiveTab()) {
		
		// Get first tab
		for (var i in this._tabs) {
			
			// Activate it
			this.activateTab(i);
			break;
		}
	} else {
		
		// Reselect, to ensure display is updated
		this.activateTab(this.getActiveTab().getID());
	}
}

/**
 * deactivateTabs() turns off all tabs in this Page.
 * 
 * @param exeception	{String}	Optional. The ID of a tab to not turn off.
 */
ComponentPage.prototype.deactivateTabs	= function(exception) {
	
	// Check arguments
	exception = exception || null;
	
	// Deactivate each tab
	for (var i in this._tabs) {
		if (i !== exception) {
			this._tabs[i].deactivate();
		}
	}
}

/**
 * redrawTabs() redraws the Tabs in this Page.
 */
ComponentPage.prototype.redrawTabs		= function() {
	
	var header	= $(ComponentInterface.TAB_HEADER_LIST_ID);
	var body	= $(ComponentInterface.TAB_BODY_LIST_ID);
	
	// Remove content
	while (header.hasChildNodes()) {
		header.removeChild(header.firstChild);
	}
	while (body.hasChildNodes()) {
		body.removeChild(body.firstChild);
	}
	
	// Redraw each one
	for (var i in this._tabs) {
		this._tabs[i].draw();
	}
}

/*** @inca-include ../admin/assets/scripts/classes/MethodResponse.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/method_response.js
 * FUNCTION:		Contains the MethodResponse class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2008-07-15
 ***************************************************************************************/

/**
 * The MethodResponse class provides a wrapper for returning data from methods, allowing
 * information and feedback to be provided about the success of the method.
 * 
 * @param status	{Boolean}	The success (true/false) of the method that is returning
 * 								this object.
 * @param reason	{String}	The reason for the method failing, if it did.
 * @param output	{Mixed}		The output generated by the method.
 */
function MethodResponse(status, reason, output) {
	
	// Set some default values
	this._status		= false;
	this._reason		= MethodResponse.NO_REASON;
	this._output		= MethodResponse.NO_OUTPUT;
	
	// Apply the passed constructor values
	this.setStatus(status);
	this.setReason(reason);
	this.setOutput(output);
}

// Constants
MethodResponse.NO_REASON		= "";
MethodResponse.NO_OUTPUT		= null;

/**
 * asObject() returns the contents of this object as a generic JS object with 3
 * properties: status, reason and output.
 * 
 * @return an object.
 * @type Object
 */
MethodResponse.prototype.asObject					= function() {
	return {
		status:		this._status,
		reason:		this._reason,
		output:		this._output
	};
}

/**
 * setStatus() sets the status (success) of this MethodResponse.
 * 
 * @param status	{Boolean}	The status to apply.
 */
MethodResponse.prototype.setStatus					= function(status) {
	this._status = (isBoolean(status)) ? status : this._status;
}

/**
 * setReason() sets the reason that this MethodResponse has the status it does.
 * 
 * @param reason	{String}	Details about why the MethodResponse has it's
 * 								current status.
 */
MethodResponse.prototype.setReason					= function(reason) {
	this._reason = (isString(reason) || isInteger(reason)) ? reason : this._reason;
}

/**
 * setOutput() sets the output of this MethodResponse.
 * 
 * @param output	{Mixed}	The output to store.
 */
MethodResponse.prototype.setOutput					= function(output) {
	this._output = output;
}

/**
 * getStatus() gets the current status (success state) of this object.
 * 
 * @return the status.
 * @type Boolean
 */
MethodResponse.prototype.getStatus					= function() {
	return this._status;
}

/**
 * wasSuccessful() is an alias for getStatus.
 * 
 * @return the status.
 * @type Boolean
 */
MethodResponse.prototype.wasSuccessful				= function() {
	return this._status ? true : false;
}

/**
 * getReason() gets this Object's reason for it's state.
 * 
 * @return the reason.
 * @type String
 */
MethodResponse.prototype.getReason					= function() {
	return this._reason;
}

/**
 * getOutput() gets the output of this object.
 * 
 * @return the output.
 * @type Mixed
 */
MethodResponse.prototype.getOutput					= function() {
	return this._output;
}

/**
 * hasOutput() returns whether output has been set for this object.
 * 
 * @return true if output is not null, false otherwise.
 * @type Boolean
 */
MethodResponse.prototype.hasOutput					= function() {
	return this._output != MethodResponse.NO_REASON;
}

/*** @inca-include ../admin/assets/scripts/classes/DateInput.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/date_input.js
 * FUNCTION:		Contains the DateInput class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2008-04-11
 ***************************************************************************************/

/****************************************************************************************
 * The DateInput object
 ***************************************************************************************/

/**
 * The DateInput object handles a User Date Input. Used by the DateTrackerEntry object,
 * amongst others.
 * 
 * @param id					{Mixed}		The ID of the Input.
 * @param date					{Integer}	The initial Date, appropriate to the Mode.
 * @param date_mode				{Integer}	The initial Date Mode.
 * @param available_date_modes	{Array}		An array of Date Modes that this input can
 * 											be set to.
 * @param display_mode			{Integer}	The display mode (whether to show the time).
 */
function DateInput(id, date, date_mode, available_date_modes, display_mode) {
	
	// Define base properties and values
	this._id					= id;
	this._date					= Date.create(0);
	this._date_mode				= DateInput.DATE_MODE_DISABLED;
	this._available_date_modes	= [DateInput.DATE_MODE_ABSOLUTE, DateInput.DATE_MODE_RELATIVE, DateInput.DATE_MODE_DISABLED];
	this._display_mode			= DateInput.DISPLAY_MODE_WITH_TIME;
	this._elements				= {};
	
	// Store Date value and Mode
	this.setDateValueAndMode(date, date_mode);
	
	// Validate Available Date Modes
	if (DateInput.isValidDateModeList(available_date_modes)) {

		// For each item
		for (var i = 0; i < available_date_modes.length; i++) {
			
			// Add
			this.addAvailableDateMode(parseInt(available_date_modes[i]));
		}
	}
	
	// Validate display mode
	if (DateInput.isValidDisplayMode(display_mode)) {
		this._display_mode = parseInt(display_mode);
	}

	// Generate DOM elements
	this._generateDOMElements();
}

// Properties
DateInput.prototype._CLASS							= "DateInput";

/**
 * getID() gets the ID of the Date Input.
 * 
 * @return the ID.
 * @type String
 */
DateInput.prototype.getID							= function() {
	return this._id;
}

/**
 * setDate() sets a new Date for this Date Input.
 * 
 * @param date	{Integer/Date}	Either a time stamp, or a Date object.
 */
DateInput.prototype.setDate							= function(date) {
	if (isIntegeric(date)) {
		this._date = Date.create(date);
	} else if (isDefined(date.time)) {
		this._date = Date.create(date.time());
	}
}

/**
 * setDateValueAndMode() sets both the Date and Mode (usually from a load of a Page
 * with a DateTimeSelect input on it, and refreshes the DOM.
 * 
 * 
 */
DateInput.prototype.setDateValueAndMode				= function(date_value, date_mode) {
	
	if (DateInput.isValidDate(date_value) && DateInput.isValidDateMode(date_mode)) {
	
		// Store Date Mode
		this._date_mode = parseInt(date_mode);
		
		// Store Date
		switch (this._date_mode) {
			case DateInput.DATE_MODE_ABSOLUTE:
				this._date = Date.create(parseInt(date_value));
				break;
			case DateInput.DATE_MODE_RELATIVE:
				this._date = Date.create(Admin.getProperty("PROJECT_START_DATE", 0) + parseInt(date_value));
				break;
			case DateInput.DATE_MODE_DISABLED:
				this._date = Date.create(Admin.getProperty("PROJECT_START_DATE", 0));
				break;
		}
	}
}

/**
 * getDate() gets the Date object for this Date Input.
 * 
 * @return the Date object.
 * @type Date
 */
DateInput.prototype.getDate							= function() {
	return this._date;
}

/**
 * setDateMode() sets a new Date Mode for this Date Input.
 * 
 * @param date_mode	{Integer}	The new Date Mode.
 */
DateInput.prototype.setDateMode						= function(date_mode) {
	if (DateInput.isValidDateMode(date_mode)) {
		this._date_mode = parseInt(date_mode);
	}
}

/**
 * getDateMode() gets the Date Mode for this Date Input.
 * 
 * @return the Date Mode.
 * @type Integer
 */
DateInput.prototype.getDateMode						= function() {
	return this._date_mode;
}

/**
 * getDateValue() gets the value of the Date of this Date Input (the Timestamp value,
 * in other words). This value is appropriate to the current Date Mode, so if the mode
 * is currently relative this value will be relative.
 * 
 * @return a integer for the value of the date, appropriate to the current mode.
 * @type Integer
 */
DateInput.prototype.getDateValue					= function() {
	switch (this._date_mode) {
		
		// Absolute
		case DateInput.DATE_MODE_ABSOLUTE:
			return this._date.time();
			break;
		
		// Relative
		case DateInput.DATE_MODE_RELATIVE:
			return this._date.time() - Admin.getProperty("PROJECT_START_DATE", 0);
			break;
		
		// Disabled
		default:
		case DateInput.DATE_MODE_DISABLED:
			return 0;
			break;
	}
}

/**
 * setValue() sets the value from the MiniCal.
 * 
 * @param value	{Integer}	An absolute date value.
 */
DateInput.prototype.setValue						= function(value) {
	
	// Apply date
	this.setDate(value);
	
	// Update elements
	this.updateDOMElements();
}

/**
 * getAbsoluteValue() gets the current Date input value in Absolute terms.
 * 
 * @return the absolute timestamp value of this Date input.
 * @type Integer
 */
DateInput.prototype.getAbsoluteValue				= function() {
	return this._date.time();
}

/**********************************************************
 * Date Mode methods
 *********************************************************/

/**
 * dateModeIsAvailable() returns whether a passed date mode is available to be used.
 * 
 * @param date_mode	{Integer}	The date mode to look for.
 * 
 * @return true if it is, false if not.
 * @type Boolean
 */
DateInput.prototype.dateModeIsAvailable				= function(date_mode) {
	if (DateInput.isValidDateMode(date_mode)) {
		for (var i = 0; i < this._available_date_modes.length; i++) {
			if (date_mode == this._available_date_modes[i]) {
				return true;
			}
		}
	}
	
	return false;
}

/**
 * addAvailableDateMode() adds an available date mode.
 * 
 * @param date_mode	{Integer}	The date mode to add.
 * 
 * @return the success of the operation.
 * @type Boolean
 */
DateInput.prototype.addAvailableDateMode			= function(date_mode) {
	
	// If argument is valid
	if (DateInput.isValidDateMode(date_mode)) {
		
		// If not already on the list
		if (!this.dateModeIsAvailable(date_mode)) {
			
			// Add date mode
			this._available_date_modes.push(date_mode);
			
			// OK
			return true;
		}
	}
	
	return false;
}

/**********************************************************
 * Date retrieving methods
 *********************************************************/

DateInput.prototype.getAbsoluteDate					= function() {
	return this._date.time();
}

DateInput.prototype.getAbsoluteYear					= function() {
	return this._date.getFullYear();
}

DateInput.prototype.getAbsoluteMonth				= function() {
	return this._date.getMonthNumber();
}

DateInput.prototype.getAbsoluteMonthDate			= function() {
	return this._date.getDate();
}

DateInput.prototype.getAbsoluteHours				= function() {
	return this._date.getHours();
}

DateInput.prototype.getAbsoluteMinutes				= function() {
	return this._date.getMinutes();
}

DateInput.prototype.getRelativeDate					= function() {
	return this.getAbsoluteDate() - Admin.getProperty("PROJECT_START_DATE", 0);
}

DateInput.prototype.getRelativeWeeks				= function() {
	return (DateInput.numberToObject(this.getRelativeDate())).w;
}

DateInput.prototype.getRelativeDays					= function() {
	return (DateInput.numberToObject(this.getRelativeDate())).d;
}

DateInput.prototype.getRelativeHours				= function() {
	return (DateInput.numberToObject(this.getRelativeDate())).h;
}

DateInput.prototype.getRelativeMinutes				= function() {
	return (DateInput.numberToObject(this.getRelativeDate())).m;
}

/**********************************************************
 * Event methods
 *********************************************************/

/**
 * onChangeAbsolute() is triggered when an Absolute field changes. It updates the
 * value of the Date Input from the Absolute fields.
 */
DateInput.prototype.onChangeAbsolute				= function() {
	
	// Set Date
	this.setDate(this.getValueFromAbsoluteFields());
	
	// Update elements
	this.updateDOMElements();
}

/**
 * onChangeRelative() is triggered when an Relative field changes. It updates the
 * value of the Date Input from the Relative fields.
 */
DateInput.prototype.onChangeRelative				= function() {
	
	// Set Date
	this.setDate(this.getValueFromRelativeFields());
	
	// Update elements
	this.updateDOMElements();
}

/**
 * onChangeDateMode() is triggered when a the Mode field changes. It updates the
 * date mode of the Date Input.
 */
DateInput.prototype.onChangeDateMode				= function() {
	
	// Set Date Mode
	this.setDateMode(this._elements.mode_selector.getSelectedValue());
	
	// Update elements
	this.updateDOMElements();
}

/**********************************************************
 * DOM reading methods
 *********************************************************/

/**
 * getValueFromAbsoluteFields() gets the value of the Absolute fields.
 * 
 * @return a timestamp value based on the contents of the Absolute fields.
 * @type Integer
 */
DateInput.prototype.getValueFromAbsoluteFields		= function() {
	
	// Get values
	var values = {};
	values.y = this._elements.absolute.y.getSelectedValue();
	values.m = this._elements.absolute.m.getSelectedValue();
	values.d = this._elements.absolute.d.getSelectedValue();
	values.h = this._elements.absolute.h.getSelectedValue();
	values.i = this._elements.absolute.i.getSelectedValue();
	
	// Validate to ensure the date is OK
	if (Date.isValid(values.y, values.m, values.d, values.h, values.i, 0)) {	
		
		// Create date with the values
		var date = Date.create(0);
		date.setYear(values.y);
		date.setMonth(values.m - 1);
		date.setDate(values.d);
		date.setHours(values.h);
		date.setMinutes(values.i);
		
		// Log for prosperity
		log("Date/time element <em>absolute</em> value is: " + date.format("Y-m-d H:i O"));
		
		return date.time();
	} else {
		log("error", "Absolute date value '" + values.y + "-" + values.m + "-" + values.d + ", " + values.h + ":" + values.m + " is not valid!");
		return 0;
	}
}

/**
 * getValueFromRelativeFields() gets the value of the Relative fields.
 * 
 * @return a timestamp value based on the contents of the Relative fields.
 * @type Integer
 */
DateInput.prototype.getValueFromRelativeFields		= function() {
	
	// Get new date
	var date = DateInput.objectToNumber({
		w: this._elements.relative.w.getValue(),
		d: this._elements.relative.d.getValue(),
		h: this._elements.relative.h.getValue(),
		m: this._elements.relative.m.getValue(),
		s: 0
	}) + Admin.getProperty("PROJECT_START_DATE", 0);
	
	// Log for prosperity
	log("Date/time element <em>relative</em> value is: " + (Date.create(date)).format("Y-m-d H:i O"));
	
	return date;
}

/**********************************************************
 * DOM generation methods
 *********************************************************/

/**
 * updateDOMElements() updates the DOM Elements to reflect the current state.
 */
DateInput.prototype.updateDOMElements				= function() {
	
	// Update absolute values
	this._elements.absolute.y.setValue(this.getAbsoluteYear());
	this._elements.absolute.m.setValue(this.getAbsoluteMonth());
	this._elements.absolute.d.setValue(this.getAbsoluteMonthDate());
	this._elements.absolute.h.setValue(this.getAbsoluteHours());
	this._elements.absolute.i.setValue(this.getAbsoluteMinutes());
	
	// Update relative values
	this._elements.relative.w.setValue(this.getRelativeWeeks());
	this._elements.relative.d.setValue(this.getRelativeDays());
	this._elements.relative.h.setValue(this.getRelativeHours());
	this._elements.relative.m.setValue(this.getRelativeMinutes());
	
	// Update summary
	
	
	// Hide unused modes
	switch (this._date_mode) {
		case DateInput.DATE_MODE_ABSOLUTE:
			this._elements.wrappers.absolute.removeClassName("dateinputmodeoff");
			this._elements.wrappers.relative.addClassName("dateinputmodeoff");
			this._elements.wrappers.disabled.addClassName("dateinputmodeoff");
			
			var relative = DateInput.numberToObject(this._date.time() - Admin.getProperty("PROJECT_START_DATE", 0));
			
			this._elements.summary.innerHTML = relative.w + " weeks, " + relative.d + " days, " + relative.h + " hours and " + relative.m + " minutes";
			break;
		case DateInput.DATE_MODE_RELATIVE:
			this._elements.wrappers.absolute.addClassName("dateinputmodeoff");
			this._elements.wrappers.relative.removeClassName("dateinputmodeoff");
			this._elements.wrappers.disabled.addClassName("dateinputmodeoff");
			this._elements.summary.innerHTML = this._date.format("Y-m-d H:i");
			break;
		case DateInput.DATE_MODE_DISABLED:
			this._elements.wrappers.absolute.addClassName("dateinputmodeoff");
			this._elements.wrappers.relative.addClassName("dateinputmodeoff");
			this._elements.wrappers.disabled.removeClassName("dateinputmodeoff");
			this._elements.summary.innerHTML = "No Date";
			break;
	}
}

/**
 * _generateDOMElements()
 */
DateInput.prototype._generateDOMElements			= function() {
	
	// Create absolute elements
	this._elements.absolute = {
		y: new SelectorDOM(this._id + "-absolute-y", mergeArraysToObject(Date.getNumberRange(1950, 2050)), {
			value:		this.getAbsoluteYear(),
			on_change:	"this._selector.datetime.onChangeAbsolute();"
		}),
		m: new SelectorDOM(this._id + "-absolute-m", mergeArraysToObject(Date.getAllMonthNumbers(), Date.getAllMonthNames()), {
			value:		this.getAbsoluteMonth(),
			on_change:	"this._selector.datetime.onChangeAbsolute();"
		}),
		d: new SelectorDOM(this._id + "-absolute-d", mergeArraysToObject(Date.getAllDateNumbers()), {
			value:		this.getAbsoluteMonthDate(),
			on_change:	"this._selector.datetime.onChangeAbsolute();"
		}),
		h: new SelectorDOM(this._id + "-absolute-h", mergeArraysToObject(Date.getAllHourNumbers()), {
			value:		this.getAbsoluteHours(),
			on_change:	"this._selector.datetime.onChangeAbsolute();"
		}),
		i: new SelectorDOM(this._id + "-absolute-i", mergeArraysToObject(Date.getAllMinuteNumbers(), Date.getAllMinuteNumbers(true, true)), {
			value:		this.getAbsoluteMinutes(),
			on_change:	"this._selector.datetime.onChangeAbsolute();"
		})
	};
	
	// Create relative elements
	this._elements.relative = {
		w: new TextInputDOM(this._id + "-relative-w", this.getRelativeWeeks(),		{
			chars:			"negative_numbers",
			length:			4,
			max_length:		8,
			on_change:		"this._textinput.datetime.onChangeRelative();"
		}),
		d: new TextInputDOM(this._id + "-relative-d", this.getRelativeDays(),		{
			chars:			"negative_numbers",
			length:			2,
			max_length:		4,
			on_change:		"this._textinput.datetime.onChangeRelative();"
		}),
		h: new TextInputDOM(this._id + "-relative-h", this.getRelativeHours(),		{
			chars:			"negative_numbers",
			length:			2,
			max_length:		4,
			on_change:		"this._textinput.datetime.onChangeRelative();"
		}),
		m: new TextInputDOM(this._id + "-relative-m", this.getRelativeMinutes(),	{
			chars:			"negative_numbers",
			length:			2,
			max_length:		4,
			on_change:		"this._textinput.datetime.onChangeRelative();"
		})
	};
	
	this._elements.disabled = {
		date: $(createElement("div", "&nbsp;"))
	}
	
	this._elements.summary = $(createElement("div",
		"Summary"
	)).addClassName("dateinputsummary");
	
	// Create wrappers
	this._elements.wrappers = {
		
		// Absolute
		absolute:		$(createElement("div",
			["id", this._id + "-absolute"],
			$(createElement("p",
				this._elements.absolute.d.draw(true),
				createElement("span", " "),
				this._elements.absolute.m.draw(true),
				createElement("span", " "),
				this._elements.absolute.y.draw(true)
			)),
			$(createElement("p",
				createElement("span", ""),
				this._elements.absolute.h.draw(true),
				createElement("span", ":"),
				this._elements.absolute.i.draw(true)
			))
		)).addClassName("dateinputwrapper").addClassName("dateinputabsolute"),
		
		// Relative
		relative:		$(createElement("div",
			["id", this._id + "-relative"],
			$(createElement("p",		
				this._elements.relative.w.getInput(),
				createElement("span", " Weeks, "),
				this._elements.relative.d.getInput(),
				createElement("span", " Days")
			)),
			$(createElement("p",
				this._elements.relative.h.getInput(),
				createElement("span", " Hours and "),
				this._elements.relative.m.getInput(),
				createElement("span", " Minutes")
			))
		)).addClassName("dateinputwrapper").addClassName("dateinputrelative"),
		
		// Disabled
		disabled:		$(createElement("div",
			["id", this._id + "-disabled"],
			this._elements.disabled.date
		)).addClassName("dateinputwrapper").addClassName("dateinputdisabled")
	}
	
	// Selector
	this._elements.mode_selector = new SelectorDOM(this._id + "-datemode",
		{
			0: "Disabled",
			1: "Absolute",
			2: "Relative"
		},
		{
			value:		this._date_mode,
			on_change:	"this._selector.datetime.onChangeDateMode();"
		}
	);
	
	// Minical Button
	this._elements.minical_button = $(createElement("button",
		["type", "button"],
		createElement("img",
			["src", "../admin/assets/images/calendar/icon.png"],
			["alt", "Open the Mini-Calendar"]
		)
	));
	
	// Events
	this._elements.minical_button._dateinput = this;
	this._elements.minical_button.onclick = function() {
		MiniCal.open(this._dateinput);
	}
	
	// Table Cells
	this._elements.cells = {
		top:	$(createElement("td",
			["valign", "top"],
			["colspan", "3"],
			this._elements.summary
		)),
		left:	$(createElement("td",
			["valign", "top"],
			$(createElement("p",
				this._elements.mode_selector.draw(true)
			))
		)),
		middle:	$(createElement("td",
			["valign", "top"],
			this._elements.wrappers.absolute,
			this._elements.wrappers.relative,
			this._elements.wrappers.disabled
		)),
		right: $(createElement("td",
			["valign", "top"],
			$(createElement("p",
				this._elements.minical_button
			))
		)).addClassName("minicalholder")
	};
	
	// Wrapper & Table
	this._elements.wrapper = $(createElement("div",
		["id", this._id + "-wrapper"],
		$(createElement("table",
			["cellspacing", "0"],
			createElement("tr",
				this._elements.cells.top
			),
			createElement("tr",
				this._elements.cells.left,
				this._elements.cells.middle,
				this._elements.cells.right
			)
		)).addClassName("dateinputtable")
	)).addClassName("dateinput");
	
	// Add back-references
	this._elements.absolute.y.datetime		= this;
	this._elements.absolute.m.datetime		= this;
	this._elements.absolute.d.datetime		= this;
	this._elements.absolute.h.datetime		= this;
	this._elements.absolute.i.datetime		= this;
	this._elements.relative.w.datetime		= this;
	this._elements.relative.d.datetime		= this;
	this._elements.relative.h.datetime		= this;
	this._elements.relative.m.datetime		= this;
	this._elements.mode_selector.datetime	= this;
	
	// Refresh
	this.updateDOMElements();
}

DateInput.prototype.draw							= function() {
	return this._elements.wrapper;
}

DateInput.prototype.redraw							= function() {
	return true;
}

/**********************************************************
 * Static Methods & Properties
 *********************************************************/

// Constants
DateInput.DATE_MODE_ABSOLUTE						= 1;
DateInput.DATE_MODE_RELATIVE						= 2;
DateInput.DATE_MODE_DISABLED						= 0;
DateInput.DISPLAY_MODE_WITH_TIME				= 1;
DateInput.DISPLAY_MODE_WITHOUT_TIME					= 2;

/*******************************************
 * Validation Methods
 ******************************************/

/**
 * isValidDate() returns whether the passed date is valid.
 * 
 * @param date	{Integer}	The date to check.
 * 
 * @return whether the date is valid.
 * @type Boolean
 */
DateInput.isValidDate								= function(date) {
	return isIntegeric(date);
}

/**
 * isValidDateMode() returns whether the passed date mode is valid.
 * 
 * @param date_mode	{Integer}	The date mode to check.
 * 
 * @return whether the date mode is valid.
 * @type Boolean
 */
DateInput.isValidDateMode							= function(date_mode) {
	return date_mode == DateInput.DATE_MODE_ABSOLUTE || date_mode == DateInput.DATE_MODE_RELATIVE || date_mode == DateInput.DATE_MODE_DISABLED;
}

/**
 * isValidDateModeList() returns whether the passed date mode list is valid.
 * 
 * @param date_modes	{Array}	The date mode list to check.
 * 
 * @return whether the date mode list is valid.
 * @type Boolean
 */
DateInput.isValidDateModeList						= function(date_modes) {
	return isArray(date_modes);
}

/**
 * isValidDisplayMode() returns whether the passed display mode is valid.
 * 
 * @param display_mode	{Integer}	The display mode to check.
 * 
 * @return whether the display mode is valid.
 * @type Boolean
 */
DateInput.isValidDisplayMode						= function(display_mode) {
	return display_mode == DateInput.DISPLAY_MODE_WITH_TIME || display_mode == DateInput.DISPLAY_MODE_WITHOUT_TIME;
}

/*******************************************
 * Conversion Methods
 ******************************************/

/**
 * numberToObject() converts a number (usually a relative date) to an object containing
 * properties for how many weeks, days, hours, minutes and seconds make it up.
 * 
 * @param number	{Integer}	The number to convert.
 * 
 * @return an Object with w, d, h, m and s properties.
 * @type Object
 */
DateInput.numberToObject							= function(number) {
	
	// Get template
	var object = {
		w: 0,
		d: 0,
		h: 0,
		m: 0,
		s: 0
	};
	
	// Weeks
	object.w = Math.floor(number / Date.SECONDS_IN_WEEK);
	number -= object.w * Date.SECONDS_IN_WEEK;
	
	// Days
	object.d = Math.floor(number / Date.SECONDS_IN_DAY);
	number -= object.d * Date.SECONDS_IN_DAY;
	
	// Hours
	object.h = Math.floor(number / Date.SECONDS_IN_HOUR);
	number -= object.h * Date.SECONDS_IN_HOUR;
	
	// Minutes
	object.m = Math.floor(number / Date.SECONDS_IN_MINUTE);
	number -= object.m * Date.SECONDS_IN_MINUTE;
	
	// Seconds
	object.s = number;
	
	return object;
}

/**
 * objectToNumber() converts an object like that generated by numberToObject back into
 * a number.
 * 
 * @param object	{Object}	An object with w, d, h, m and s properties.
 * 
 * @return the number that this object was made from.
 * @type Integer
 */
DateInput.objectToNumber							= function(object) {
	
	// Verify variables
	object		= isObject(object)		? object				: {};
	object.w	= isIntegeric(object.w) ? parseInt(object.w)	: 0;
	object.d	= isIntegeric(object.d) ? parseInt(object.d)	: 0;
	object.h	= isIntegeric(object.h) ? parseInt(object.h)	: 0;
	object.m	= isIntegeric(object.m) ? parseInt(object.m)	: 0;
	object.s	= isIntegeric(object.s) ? parseInt(object.s)	: 0;
	
	// Compile into seconds
	var number = (object.w * Date.SECONDS_IN_WEEK) + (object.d * Date.SECONDS_IN_DAY) + (object.h * Date.SECONDS_IN_HOUR) + (object.m * Date.SECONDS_IN_MINUTE) + object.s;
	
	// Done
	return number;
}

/*** @inca-include ../admin/assets/scripts/classes/ListBox.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/list_box.js
 * FUNCTION:		Contains the ListBox, ListBoxRow and ListBoxHeader classes.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-10
 ***************************************************************************************/

/**
 * The ListBox class contains a single ListBox. ListBoxes are used to present lists of
 * non-changing, sortable, searchable data.
 * 
 * @creator Cameron Morrow
 * 
 * @param id		{String}	The ID of the listbox. Used for registering, DOM
 * 								element creation, etc.
 * @param label		{String}	The label for the ListBox.
 * @param params	{Object}	An object containing parameters for the ListBox.
 * @param columns	{Array}		An array of column information for the List.
 * @param values	{Array}		An array of rows of values.
 */
function ListBox(id, label, params, columns, values) {
	
	// Constants
	this._CLASS		= "ListBox";
	
	// Store parameters
	this._id		= isString(id)		? id		: "";
	this._label		= isString(label)	? label		: "";
	this._params	= isObject(params)	? params	: {};
	
	// Define other parameters
	this._columns				= {};
	this._values				= [];
	this._current_sort_index	= false;
	this._current_sort_asc		= true;
	this._search_terms			= [];
	this._searchable_cols		= [];
	this._elements				= {};
	this._ordering				= false;
	
	// Parse
	this._parseColumns(columns);
	this._parseValues(values);
	
	// Generate DOM
	this._generateDOM();
}

/**
 * getID() gets the ID of the ListBox.
 * 
 * @return the ID.
 * @type String
 */
ListBox.prototype.getID					= function() {
	return this._id;
}

/**
 * getLabel() gets the Label of this ListBox.
 * 
 * @return the label.
 * @type String
 */
ListBox.prototype.getLabel				= function() {
	return this._label;
}

/**
 * getColumns() gets the columns array.
 * 
 * @return the columns object.
 * @type Object
 */
ListBox.prototype.getColumns			= function() {
	return this._columns;
}

/**
 * setColumns() sets a new set of columns for this ListBox.
 * 
 * @param columns	{Array}	An array of objects to be parsed into columns.
 */
ListBox.prototype.setColumns			= function(columns) {
	this._parseColumns(columns);
}

/**
 * getColumn() gets a column.
 * 
 * @param id	{String}	The ID of the column.
 * 
 * @return the column if it exists, false if not.
 * @type ListBoxHeader/Boolean
 */
ListBox.prototype.getColumn				= function(id) {
	if (this.hasColumn(id)) {
		return this._columns[id];
	}
}

/**
 * hasColumn() returns wheter a column exists.
 * 
 * @param id	{String}	The ID of the column.
 * 
 * @return whether the column exists.
 * @type Boolean
 */
ListBox.prototype.hasColumn				= function(id) {
	return isObject(this._columns[id]);
}

/**
 * getColumnIndex() gets the index of a column.
 * 
 * @param id	{String}	The ID of the column.
 * 
 * @return the index of the column, or false if not found.
 * @type Integer/Boolean
 */
ListBox.prototype.getColumnIndex		= function(id) {
	
	// Get the index
	var index = 0;
	for (var i in this._columns) {
		if (i == id) {
			return index;
		}
		index++;
	}
	
	// Fail
	return false;
}

/**
 * getValues() returns the values array.
 * 
 * @return the values array.
 * @type Array
 */
ListBox.prototype.getValues				= function() {
	return this._values;
}

/**
 * setValues() sets a new set of values for this ListBox.
 * 
 * @param values	{Array}	An array of objects to be parsed into rows.
 */
ListBox.prototype.setValues				= function(values) {
	this._parseValues(values);
	this.redraw();
}

/**
 * getSearchableRows() returns the list of searchable rows.
 * 
 * @return the array of r
 */
ListBox.prototype.getSearchableColumns	= function() {
	return this._searchable_cols;
}

/****************************************************************************************
 * PARSING METHODS
 ***************************************************************************************/

/**
 * _parseColumns() parses the passed column object into ListBoxHeader objects.
 * 
 * @param data	{Array/Object}	The source object or array to parse.
 */
ListBox.prototype._parseColumns			= function(data) {
	
	// Reset columns
	this._columns = {};
	
	// For each element
	for (var i in data) {
		if (data[i].id && data[i].label) {
			
			// Searchable?
			var searchable = data[i].searchable || false;
			
			// Create header
			this._columns[data[i].id] = new ListBoxHeader(data[i].id, data[i].label, data[i].numeric || false, data[i].searchable || true, data[i].width || false, this);
			
			// Add to searchable column, if applicable
			if (searchable) {
				this._searchable_cols.push(data[i].id);
			}
		}
	}
}

/**
 * _parseValues() parses the passed values object into ListBoxRow objects.
 * 
 * @param data	{Array/Object}	The source data object or array to parse.
 */
ListBox.prototype._parseValues			= function(data) {
	
	// Reset values
	this._values = [];
	
	// For each element
	for (var i in data) {
		if (isObject(data[i]) && !isFunction(data[i])) {
			var index = this._values.length;
			this._values[index] = new ListBoxRow(index, data[i], this, this._params.min_row_height);
		}
	}
}

/****************************************************************************************
 * SORTING/SEARCHING METHODS
 ***************************************************************************************/

/**
 * sort() sorts the ListBox's contents.
 * 
 * @param column	{String}	The ID of the column to sort on.
 */
ListBox.prototype.sort					= function(column) {
	
	if (this.hasColumn(column)) {
		
		// Extract values from the rows for this column
		var values = this.getColumnValues(column);
		
		// Sort as either numeric or string
		if (this._columns[column].getNumeric()) {
			values = values.sort(ListBoxes.compareValuesNumeric);
		} else {
			values = values.sort(ListBoxes.compareValues);
		}
		
		// Get the new row order from the values
		for (var i = 0; i < values.length; i++) {
			values[i] = values[i].id;
		}
		
		// If already sorted in this column, flip it around
		if (column == this._current_sort_index) {
			if (this._current_sort_asc) {
				values.reverse();
			}
			this._current_sort_asc = !this._current_sort_asc;
			
		// Otherwise, mark as sorting on this column
		} else {
			this._current_sort_index	= column;
			this._current_sort_asc		= true;
		}
		
		this._ordering = values;
		this._updateDOM();
	}
}

/**
 * getColumnValues() gets the values of a column in all the current rows.
 * 
 * @param column	{String}	The column ID to get the values of.
 * 
 * @return an array of objects containing a row ID and a value for the requested column.
 * @type Array
 */
ListBox.prototype.getColumnValues		= function(column) {
	
	var values = [];
	
	for (var i = 0; i < this._values.length; i++) {
		values[i] = {
			id:		i,
			value:	this._values[i].getValue(column)
		};
	}
	
	return values;
}

/**
 * search() attempts a search of the ListBox's rows based on the content of the LB's
 * search box.
 */
ListBox.prototype.search				= function() {
	
	// Variables
	var i, vl = this._values.length, sl, row;
	
	// Get search terms
	this._search_terms = $F("lb-" + this.getID() + "-search").trim().toLowerCase().split(" ");
	sl = this._search_terms.length;
	
	for (i = 0; i < vl; i++) {
		row = $("lb-" + this.getID() + "-r-" + i);
		if (row) {
			if (sl == 0) {
				$(row).removeClassName("hidden");
			} else if (this._values[i].search(this._search_terms)) {
				$(row).removeClassName("hidden");
			} else {
				$(row).addClassName("hidden");
			}
		}
	}
}

/**************************************************************************************
 * DOM METHODS
 *************************************************************************************/

ListBox.prototype.applyDimensions		= function() {
	
	// Check vars
	var width	= this._params.width	? this._params.width	: false;
	var height	= this._params.height	? this._params.height	: false;
	
	// Validate
	width = isIntegeric(width) ? width + "px" : width;
	height = isIntegeric(height) ? height + "px" : height;
	
	// Set if needed
	if (width) {
		this.setWidth(width);
	}
	if (height) {
		this.setHeight(height);
	}
}

/**
 * setHeight() sets the height of the (main) element. Note - not the wrapper.
 * 
 * @param height	{Integer}	The new height.
 */
ListBox.prototype.setHeight				= function(height) {
	if (this._elements.div) {
		Page.simpleSetHeight(this._elements.div, height);
	}
}

/**
 * setHeight() sets the width of the (main) element. Note - not the wrapper.
 * 
 * @param width	{Integer}	The new width.
 */
ListBox.prototype.setWidth				= function(width) {
	if (this._elements.div) {
		Page.simpleSetWidth(this._elements.div, width);
	}
}

ListBox.prototype._generateDOM			= function() {

	// Create container
	this._elements.div = createElement("div",
		["id", "lb-" + this.getID()]
	).addClassName("listbox");
	
	// Create wrapper
	this._elements.wrapper = createElement("div",
		["id", "lb-" + this.getID() + "-wrapper"],
		this._elements.div
	).addClassName("listboxwrapper");
	
	// Create Search
	this._elements.search = createElement("form",
		["onsubmit", "return false;"],
		["action", "#"],
		["method", "get"],
		(this._values.length > ListBoxes.OPTIMIZED_THRESHOLD ?
			"<a href=\"#\" class=\"greybutton\" onclick=\"ListBoxes.search('" + this.getID() + "');\" title=\"Search List\">&rarr;</a>"
		:
			""
		),
		createElement("p",
			createElement("label",
				["for", "lb-" + this.getID() + "-search"],
				"Search: "
			),
			createElement("input",
				["id", "lb-" + this.getID() + "-search"],
				["size", "30"],
				["onkeyup", this._values.length <= ListBoxes.OPTIMIZED_THRESHOLD ? "ListBoxes.search('" + this.getID() + "');" : ""],
				["value", ""]
			)
		)
	).addClassName("listboxsearch");
	
	// Create head and Head Table
	this._elements.head = createElement("tr");
	for (var i in this._columns) {
		this._elements.head.appendChild(this._columns[i].draw());
	}
	this._elements.headtable = createElement("table",
		["cellspacing", "0"],
		["id", "lb-" + this.getID() + "-th"],
		createElement("tbody",
			this._elements.head
		)
	).addClassName("listboxhead");
	
	// Create body table
	this._elements.bodycontent = createElement("tbody");
	
	// Create <thead> that contains spacers to make sure column width is correct
	this._elements.bodyhead = createElement("tr");
	for (var i in this._columns) {
		this._elements.bodyhead.appendChild(createElement("td",
			["width", this._columns[i].getWidth()],
			"&nbsp;"
		));
	}
	this._elements.body = createElement("div",
		createElement("table",
			["cellspacing", "0"],
			["id", "lb-" + this.getID() + "-tb"],
			createElement("thead",
				this._elements.bodyhead
			),
			this._elements.bodycontent
		)
	).addClassName("listboxbody");
	
	// Attach elements
	this._elements.div.appendChild(this._elements.search);
	this._elements.div.appendChild(this._elements.headtable);
	this._elements.div.appendChild(this._elements.body);
	
	// Resize if needed
	this.applyDimensions();
}

ListBox.prototype._updateDOM			= function() {
	
	// Remove old content
	while (this._elements.bodycontent.hasChildNodes()) {
		this._elements.bodycontent.removeChild(this._elements.bodycontent.firstChild);
	}
	
	// If an ordering has been set, use that, otherwise use default order
	if (isArray(this._ordering)) {
		
		// Draw in order
		for (var i = 0; i < this._ordering.length; i++) {
			this._elements.bodycontent.appendChild(this._values[this._ordering[i]].draw());
		}
		
		// Update header
		for (i in this._columns) {
			if (i == this._current_sort_index) {
				this._columns[i].highlight(this._current_sort_asc);
			} else {
				this._columns[i].unhighlight();
			}
		}
		
		// Highlight columns
		if (this._values.length <= ListBoxes.OPTIMIZED_THRESHOLD) {
			var index = this.getColumnIndex(this._current_sort_index);
			for (var i = 0; i < this._values.length; i++) {
				this._values[i].highlightCell(index);
			}
		}
	} else {
		for (var i = 0; i < this._values.length; i++) {
			this._elements.bodycontent.appendChild(this._values[i].draw());
		}
	}
	
	// Alternate rows
	this.alternateRows();
}

ListBox.prototype.draw					= function() {
	this._updateDOM();
	return this._elements.wrapper;
}

ListBox.prototype.redraw				= ListBox.prototype.draw;

ListBox.prototype.alternateRows			= function() {


	var counter = 0;
	
	var elements = this._elements.div.select("tr.listboxrow");
	
	for (var i = 0; i < elements.length; i++) {
		var element = elements[i];
		
		if (counter % 2 == 0) {
			element.addClassName("alt");
		} else {
			element.removeClassName("alt");
		}
		
		counter++;
	}

}

/*** @inca-include ../admin/assets/scripts/classes/ListBoxRow.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/list_box_row.js
 * FUNCTION:		Contains the ListBoxRow class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-10
 ***************************************************************************************/

/**
 * The ListBoxRow class handles a single row in a ListBox.
 * 
 * @creator Cameron Morrow
 * 
 * @param id		{Mixed}		A row ID (usually an Integer counter).
 * @param values	{Object}	An object containing parameters with values for each
 * 								column for this row.
 * @param list_box	{ListBox}	A reference to the parent ListBox.
 */
function ListBoxRow(id, values, list_box, min_height) {
	
	// Constants
	this._CLASS		= "ListBoxRow";
	
	// Store parameters
	this._id			= id			|| 0;
	this._values		= values		|| {};
	this._list_box		= list_box		|| false;
	this._min_height	= min_height	|| false;
	this._elements		= {};
	
	// Parse values
	this._parseValues();
	
	// Generate DOM
	this._generateDOM();
}

/**
 * getID() gets the ID of the row.
 * 
 * @return the ID.
 * @type Mixed
 */
ListBoxRow.prototype.getID							= function() {
	return this._id;
}

/**
 * getValues() gets the values of this row.
 * 
 * @return the values object.
 * @type Object
 */
ListBoxRow.prototype.getValues						= function() {
	return this._values;
}

ListBoxRow.prototype._parseValues					= function() {
	for (var i in this._values) {
		if (!isObject(this._values[i])) {
			this._values[i] = {
				display:	this._values[i],
				value:		this._values[i]
			};
		} else {
			if (isUndefined(this._values[i].value) || isUndefined(this._values[i].display)) {
				this._values[i] = {
					display:	null,
					value:		null
				};
				break;
			}
		}
		
		this._values[i].value	= String(this._values[i].value).stripTags().trim().toLowerCase();
		this._values[i].display = isObject(this._values[i].display) ? this._values[i].display : String(this._values[i].display);
	}
}

/****************************************************************************************
 * SORTING/SEARCHING METHODS
 ***************************************************************************************/

ListBoxRow.prototype.hasValue						= function(column) {
	return isDefined(this._values[column]);
} 

ListBoxRow.prototype.getValue						= function(column) {
	if (this.hasValue(column)) {
		if (isDefined(this._values[column].value)) {
			return this._values[column].value;
		}
	}
	
	return null;
}

ListBoxRow.prototype.search							= function(search_terms) {
	
	var cols = this._list_box.getSearchableColumns(), i, j, term, search_in;
	
	// Watch for a match
	var match = false;
	
	// For each search term
	for (i = 0; i < search_terms.length; i++) {
		term = search_terms[i];
		
		// For each searchable column
		for (j = 0; j < cols.length; j++) {
			search_in = this._values[cols[j]].value;
			
			// If we find a match...
			if (search_in.indexOf(term) != -1) {
				match = true;
				break;
			}
		}
		
		if (match) {
			break;
		}
	}
	
	return match;
}

/****************************************************************************************
 * DOM METHODS
 ***************************************************************************************/

ListBoxRow.prototype._generateDOM					= function() {
	this._elements.tr = createElement("tr",
		["id", "lb-" + this._list_box.getID() + "-r-" + this.getID()],
		["class", "listboxrow"]
	);
	this._elements.cells = [];
	
	// For each column
	for (var i in this._list_box.getColumns()) {
		
		// Create cell
		var td = createElement("td");
		if (this._min_height) {
			//td.setAttribute("style", "height: " + this._min_height + "px");
			td.style.height = this._min_height + "px";
		}
		
		// Add the value
		if (isObject(this._values[i]) && isDefined(this._values[i].value) && isDefined(this._values[i].display)) {
			//td.innerHTML = this._values[i].display;
			var inner = createElement("div", this._values[i].display);
		} else {
			var inner = createElement("div", "FAIL");
		}
		
		inner.addClassName("inner");
		td.appendChild(inner);
		
		// Attach cell
		this._elements.cells.push(td);
		this._elements.tr.appendChild(td);
	}
}

ListBoxRow.prototype.highlightCell					= function(index) {
	var cell;
	for (var i = 0; i < this._elements.cells.length; i++) {
		cell = this._elements.cells[i];
		
		if (i == index) {
			cell.addClassName("highlight");
		} else {
			cell.removeClassName("highlight");
		}
	}
}

/**
 * draw() draws a <tr> tag for this row.
 * 
 * @param include_widths	{Boolean}	Include column widths on these cells?
 * 
 * @return the <tr> DOM Element.
 * @type DOMElement
 */
ListBoxRow.prototype.draw							= function() {
	return this._elements.tr;
}
 

/*** @inca-include ../admin/assets/scripts/classes/ListBoxHeader.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/list_box_header.js
 * FUNCTION:		Contains the ListBoxHeader class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-10
 ***************************************************************************************/

/**
 * The ListBoxHeader class contains a column header for a ListBox.
 * 
 * @creator Cameron Morrow
 * 
 * @param id			{String}	The ID of the column.
 * @param label			{String}	The column header label.
 * @param numeric		{Boolean}	Whether the column contains numbers. Affects
 * 									searching.
 * @param searchable	{Boolean}	Whether this column's values are indexed for
 * 									searching.
 * @param width			{String}	A number (either percent or absolute) to apply as the
 * 									width of this column.
 * @param list_box		{ListBox}	A reference to the parent ListBox.
 */
function ListBoxHeader(id, label, numeric, searchable, width,  list_box) {
	
	// Constants
	this._CLASS			= "ListBoxHeader";
	
	// Properties
	this._id			= id			|| "";
	this._label			= label			|| "Unnamed Column";
	this._numeric		= numeric		|| false;
	this._searchable	= searchable	|| false;
	this._width			= width			|| "1";
	this._list_box		= list_box		|| false;
	this._elements		= {};
	
	this._generateDOM();
}

/**
 * getID() gets the ID of the Header.
 * 
 * @return the ID.
 * @type String
 */
ListBoxHeader.prototype.getID						= function() {
	return this._id;
}

/**
 * getLabel() gets the Label of the Header.
 * 
 * @return the label.
 * @type String
 */
ListBoxHeader.prototype.getLabel					= function() {
	return this._label;
}

/**
 * getNumeric() gets whether the column contains numeric values.
 * 
 * @return true if the column contains numeric values; false if not.
 * @type Boolean
 */
ListBoxHeader.prototype.getNumeric					= function() {
	return this._numeric ? true : false;
}

/**
 * getSearchable() returns whether this column's values should be searchable.
 *
 * @return true if they are searchable; false if not.
 * @type Boolean
 */
ListBoxHeader.prototype.getSearchable				= function() {
	return this._searchable ? true : false;
}

/**
 * getWidth() gets the column width.
 * 
 * @return the width.
 * @type String
 */
ListBoxHeader.prototype.getWidth					= function() {
	return this._width;
}

/****************************************************************************************
 * DOM METHODS
 ***************************************************************************************/

ListBoxHeader.prototype.highlight					= function(ascending) {
	this._elements.th.addClassName("sorted").removeClassName("asc").removeClassName("desc").addClassName(ascending ? "asc" : "desc");
}

ListBoxHeader.prototype.unhighlight					= function() {
	this._elements.th.removeClassName("sorted");
}

ListBoxHeader.prototype._generateDOM				= function() {
	
	this._elements.th = createElement("th",
		["id", "lb-" + this._list_box.getID() + "-h-" + this.getID()],
		(
			this.getWidth() ?
				["width", this.getWidth()]
			:
				""
		)
	);
	
	if (this.getNumeric()) {
		this._elements.th.addClassName("numeric");
	}
	if (this.getSearchable()) {
		this._elements.th.addClassName("searchable");
	}
	
	this._elements.a = createElement("a",
		["href", "javascript:ListBoxes.sort('" + this._list_box.getID() + "', '" + this.getID() + "');"],
		this.getLabel()
	);
	
	this._elements.th.appendChild(this._elements.a);
}

/**
 * draw() draws the <th> tag containing this header.
 * 
 * @return a <th> DOM Element.
 * @type DOMElement
 */
ListBoxHeader.prototype.draw						= function() {
	return this._elements.th;
}

/*** @inca-include ../admin/assets/scripts/classes/TreeBox.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/tree_box.js
 * FUNCTION:		Contains the TreeBox class
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-13
 ***************************************************************************************/

/**
 * The TreeBox class handles a single TreeBox object.
 * 
 * @creator Cameron Morrow
 * 
 * @param id		{String}		The TreeBox ID.
 * @param label		{String}		The Label of the TreeBox.
 * @param params	{Object}		An object containing additional TreeBox parameters.
 * @param data		{Array/Object}	Data to parse as branches of the TreeBox. Can be an
 * 									array of branches, or an object to parse as a branch.
 */
function TreeBox(id, label, params, data) {
	
	// Constants
	this._CLASS		= "TreeBox";
	
	// Store properties
	this._id		= isString(id)		? id		: "";
	this._label		= isString(label)	? label		: "";
	this._params	= isObject(params)	? params	: {};
	this._treebox	= this;
	
	// Parse data
	this._branches = this._parseData(isArray(data) ? data : [data]);
}

/****************************************************************************************
 * TEMPLATE METHODS
 * These methods are inherited by both TreeBox objects and also TreeBoxBranches.
 ***************************************************************************************/

/**
 * _parseData() parses intialization JSON data and attempts to get Branches out of it,
 * which are added to the _branches property of the object.
 * 
 * @param data	{Object/Array}	The source JSON data to parse.
 * 
 * @return an array of Branches.
 * @type Array
 */
TreeBox._parseData						= function(data) {
	
	// Returned array
	var branches = [];
	
	// For each element in the data
	for (var i in data) {
		
		// Create a branch
		var branch = this._parseBranch(data[i]);
		
		// If created OK, add
		if (this.isBranch(branch)) {
			branches.push(branch);
		}
	}
	
	return branches;
}

/**
 * _parseBranch() parses an object to create a Branch out of it.
 * 
 * @param branch	{Object}	An object that can potentially be a branch.
 * 
 * @return a created Branch object if successful, false if not.
 * @type TreeBoxBranch/Boolean
 */
TreeBox._parseBranch					= function(branch) {
	
	// If already a branch
	if (this.isBranch(branch)) {
		return branch;
		
	// If an object that looks like it could be a branch
	} else if (isDefined(branch.label)) {
		if (!isNumeric(branch.id)) {
			branch.id = TreeBoxes.getUniqueID();
		}
		
		return new TreeBoxBranch(branch.id, branch.label, this, this._treebox, branch.params || {}, branch.branches || {});
	}
	
	// Fail
	return false;
}

/**
 * getBranch() attempts to get a branch with the passed ID.
 * 
 * @param id		{Mixed}		The Branch ID to look for.
 * @param recursive	{Boolean}	Whether to search recursively through children branches
 * 								as well.
 * @param index		{Boolean}	Return the index of the branch, rather than the branch
 * 								itself.
 * 
 * @return the Branch (or index) if found, false if not.
 * @type Integer/TreeBoxBranch/Boolean
 */
TreeBox.getBranch						= function(id, recursive, index) {
	
	// Verify arguments
	id			= id || "";
	recursive	= isBoolean(recursive) ? recursive : true;
	index		= isBoolean(index) ? index : false;
	
	// For each branch
	for (var i in this._branches) {
		
		// Make sure it IS a branch
		if (this.isBranch(this._branches[i])) {
		
			// If ID is a match
			if (this._branches[i].getID() == id) {
				if (index) {
					return i;
				} else {
					return this._branches[i];
				}
				
			// Otherwise, if checking recursively, check children as well
			} else if (recursive) {
				var child = this._branches[i].getBranch(id, true);
				if (child) {
					return child;
				}
			}
		}
	}
	
	// Fail
	return false;
}

/**
 * getBranchIndex() attempts to get the index of the branch with the passed ID in it's
 * parent's branches array.
 * 
 * @param id		{Mixed}		The Branch ID to look for.
 * @param recursive	{Boolean}	Whether to search recursively through children branches
 * 								as well.
 * 
 * @return the Branch index if found, false if not.
 * @type Integer/Boolean
 */
TreeBox.getBranchIndex					= function(id, recursive) {
	return this.getBranch(id, recursive, true);
}

/**
 * hasBranch() returns whether a Branch with the passed ID exists under this Object.
 * 
 * @param id		{Mixed}		The Branch ID to look for.
 * @param recursive	{Boolean}	Whether to search recursively through children branches
 * 								as well.
 * 
 * @return true if the Branch exists, false if not.
 * @type Boolean
 */
TreeBox.hasBranch						= function(id, recursive) {
	return this.isBranch(this.getBranch(id, recursive));
}

/**
 * getBranchParent() gets the parent of a branch, if the branch exists. Note that this
 * method is always recursive, as if we know the branch exists directly under this object
 * then we already know the parent.
 * 
 * @param id	{Mixed}	The ID of the Branch.
 * 
 * @return the parent object if found, false if not.
 * @type TreeBox/TreeBoxBranch/Boolean
 */
TreeBox.getBranchParent					= function(id) {
	
	// Verify arguments
	id = id || "";
	
	// Get branch
	var branch = this.getBranch(id);
	
	// Return parent if branch was found
	if (this.isBranch(branch)) {
		return branch.getParent();
	}
	
	// Fail
	return false;
}

/**
 * removeBranch() removes a branch.
 * 
 * @param id	{Mixed}	The ID of the branch.
 * 
 * @return this object, for continued use.
 * @type TreeBox/TreeBoxBranch
 */
TreeBox.removeBranch					= function(id) {
	
	// Get the branch to remove
	var branch = this.getBranch(id);
	
	// If branch exists
	if (this.isBranch(branch)) {
		
		// Get parent and index of branch to remove
		var parent = branch.getParent();
		var index = parent.getBranchIndex(id, false);
		
		// Remove branch
		parent._branches.remove(index);
		
		// Redraw
		parent.redraw();
	}
	
	// Return object for continued use
	return this;
}

/**
 * isBranch() returns whether the passed object is a branch.
 * 
 * @param obj	{Mixed}	The object to check.
 * 
 * @return true if it is a branch, false if not.
 * @type Boolean
 */
TreeBox.isBranch						= function(obj) {
	return obj.getBranches ? true : false;
}

/**
 * getBranches() gets the branches of this Object.
 * 
 * @return the Array containing the branches.
 * @type Array
 */
TreeBox.getBranches						= function() {
	return this._branches;
}

/**
 * hasBranches() returns whether this Object has any branches.
 * 
 * @return true if branches exist, false if not.
 * @type Boolean
 */
TreeBox.hasBranches						= function() {
	return this._branches.length > 0;
}

/**
 * appendBranch() appends a new branch to this Object.
 * 
 * @param data	{Object}	Either a Branch object or unparsed data that makes a Branch.
 * 
 * @return the added Branch, or false if not added.
 * @type TreeBoxBranch/Boolean
 */
TreeBox.appendBranch					= function(data) {
	
	// Try to create a branch
	var branch = this._parseBranch(data);
	
	// If we can make a valid branch out of it
	if (this.isBranch(branch)) {
		
		// Attach branch
		this._branches.push(branch);
		
		// Redraw
		this.redraw();
		
		// Return created branch
		return branch;
	}
	
	// Fail
	return false;
}

/**
 * unshiftBranch() inserts a new branch at the beginning of this Object's children.
 * 
 * @param data	{Object}	Either a Branch object or unparsed data that makes a Branch.
 * 
 * @return the added Branch, or false if not added.
 * @type TreeBoxBranch/Boolean
 */
TreeBox.unshiftBranch					= function(data) {
	
	// Try to create a branch
	var branch = this._parseBranch(data);
	
	// If we can make a valid branch out of it
	if (this.isBranch(branch)) {
		
		// Attach branch
		this._branches.unshift(branch);
		
		// Redraw
		this.redraw();
		
		// Return created branch
		return branch;
	}
	
	// Fail
	return false;
}

/**
 * insertBranchBefore() inserts a branch before another branch with the given ID.
 * 
 * Note: the branch ID does NOT have to be a child of the object this method is called
 * on, it just has to be somewhere below it.
 * 
 * @param id	{Mixed}		The ID of a branch to insert a new branch before.
 * @param data	{Object}	
 */
TreeBox.insertBranchBefore				= function(id, data) {
	
	var before = this.getBranch(id);
	var branch = this._parseBranch(data);
	
	if (this.isBranch(before) && this.isBranch(branch)) {
		
		// Get parent and index of branch to insert before
		var parent	= branch.getParent();
		var index	= parent.getBranchIndex(id, false);
		
		// Remove branch
		parent._branches.splice(index, 0, branch);
		
		// Redraw
		parent.redraw();
		
		// Return created branch
		return branch;
	}
	
	// Fail
	return false;
}

/****************************************************************************************
 * UTILITY METHODS
 ***************************************************************************************/

/**
 * getID() gets the ID of the TreeBox.
 * 
 * @return the ID.
 * @type String
 */
TreeBox.prototype.getID					= function() {
	return this._id;
}

/**
 * getLabel() gets the Label of the TreeBox.
 * 
 * @return the Label.
 * @type String
 */
TreeBox.prototype.getLabel				= function() {
	return this._label;
}

/**
 * getParams() gets the parameters object of the TreeBox.
 * 
 * @return the params object.
 * @type Object
 */
TreeBox.prototype.getParams				= function() {
	return this._params;
}

/**
 * hasParam() returns whether this TreeBox has the passed parameter.
 * 
 * @param id	{Mixed}	The parameter ID.
 * 
 * @return true if the parameter exists, false if not.
 * @type Boolean
 */
TreeBox.prototype.hasParam				= function(id) {
	return isDefined(this._params[id]);
}

/**
 * getParam() returns the value of the requested parameter in this TreeBox.
 * 
 * @param id	{Mixed}	The parameter ID.
 * 
 * @return the value of the parameter if found, false if not.
 * @type Mixed
 */
TreeBox.prototype.getParam				= function(id) {
	if (this.hasParam(id)) {
		return this._params[id];
	}
	
	return false;
}

/****************************************************************************************
 * INHERITED METHODS
 ***************************************************************************************/

TreeBox.prototype._parseData					= TreeBox._parseData;
TreeBox.prototype._parseBranch					= TreeBox._parseBranch;
TreeBox.prototype.getBranch						= TreeBox.getBranch;
TreeBox.prototype.getBranchIndex				= TreeBox.getBranchIndex;
TreeBox.prototype.hasBranch						= TreeBox.hasBranch;
TreeBox.prototype.isBranch						= TreeBox.isBranch;
TreeBox.prototype.hasBranches					= TreeBox.hasBranches;
TreeBox.prototype.getBranches					= TreeBox.getBranches;
TreeBox.prototype.removeBranch					= TreeBox.removeBranch;
TreeBox.prototype.appendBranch					= TreeBox.appendBranch;
TreeBox.prototype.unshiftBranch					= TreeBox.unshiftBranch;
TreeBox.prototype.insertBranchBefore			= TreeBox.insertBranchBefore;

/****************************************************************************************
 * MOVING METHODS
 ***************************************************************************************/

/**
 * moveBranch() moves a branch with the passed ID to a new parent.
 * 
 * @param id		{Mixed}	The ID of the branch to move.
 * @param parent_id	{Mixed}	The ID of the parent branch to move this branch under.
 */
TreeBox.prototype.moveBranch			= function(id, parent_id) {
	
	// Get the branch objects of the new parent and the branch to move
	var branch	= this.getBranch(id);
	var parent	= this.getBranch(parent_id);
	
	// If both exists
	if (this.isBranch(branch) && isObject(parent)) {
		
		// TODO: CHECK THAT PARENT IS NOT THE SAME AS, AND IS NOT BELOW, THE BRANCH
		if (branch.getID() != parent.getID()) {
		
			if (branch.getBranch(parent.getID()) === false) {
		
				if (branch.getParent().getID() !== parent.getID()) {
			
					// Add branch to end of new parent's branches
					parent._branches.push(branch);
					
					// Remove from original
					parent.removeBranch(id);
					
					// Set the new parent of the branch
					branch.setParent(parent);
					
					// Redraw the new parent, with it's new child
					parent.redraw();
				} else {
					// ERRAR: ALREADY HAS THIS PARENT
					alert("branch already has that parent, fail.");
				}
			} else {
				// ERRAR: BRANCH CAN'T BE ABOVE IT'S OWN PARENT, FOOL!
				alert("branch above parent, fail.");
			}
		} else {
			// ERRAR: BRANCH CAN'T BE IT'S OWN PARENT, FOOL!
			alert("branch = parent, fail");
		}
	} else {
		alert("epic fail");
	}
}

/****************************************************************************************
 * DRAWING METHODS
 ***************************************************************************************/

/**
 * draw() draws the treebox from the top level.
 */
TreeBox.prototype.draw					= function() {
	
	// Create container
	var div = createElement("div",
		["id", "tb-" + this.getID()]
	);
	$(div).addClassName("treebox");
	
	var children = createElement("ul");
	
	for (var i in this._branches) {
		if (this.isBranch(this._branches[i])) {
			children.appendChild(this._branches[i].draw());
		}
	}
	
	div.appendChild(children);
	
	return div;
}

/**
 * redraw() redraws an already drawn TreeBox. It assumes that the generated TreeBox
 * element is already created and drawn.
 */
TreeBox.prototype.redraw			= function() {
	var element = $("tb-" + this.getID());
	
	if (element) {
		element.parentNode.insertBefore(this.draw(), element);
		element.parentNode.removeChild(element);
	}
}

/*** @inca-include ../admin/assets/scripts/classes/TreeBoxBranch.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/tree_box_branch.js
 * FUNCTION:		Contains the TreeBoxBranch class
 * CREATOR:			Cameron Morrow
 * CREATED:			2007-08-13
 ***************************************************************************************/

/**
 * The TreeBoxBranch class.
 * 
 * @creator Cameron Morrow
 * 
 * @param id		{Integer/String}	The ID of the Branch.
 * @param label		{String}			The Branch label.
 * @param parent	{Object}			The parent of the Branch.
 * @param treebox	{Object}			The Treebox containing this Branch.
 * @param params	{Object}			Additional Parameters.
 * @param data		{Object/Array}		Data to parse as branches of the Branch.
 */
function TreeBoxBranch(id, label, parent, treebox, params, data) {
	
	// Constants
	this._CLASS		= "TreeBoxBranch";
	
	// Store properties
	this._id		= isString(id) || isIntegeric(id)	? id		: "";
	this._label		= isString(label)					? label		: "";
	this._parent	= isObject(parent)					? parent	: null;
	this._treebox	= isObject(treebox)					? treebox	: null;
	this._params	= isObject(params)					? params	: {};
	this._expanded	= true;
	
	// Parse
	this._branches = this._parseData(data);
}

/**
 * getID() gets the ID of the Branch.
 * 
 * @return the ID.
 * @type String/Integer
 */
TreeBoxBranch.prototype.getID			= function() {
	return this._id;
}

/**
 * getLabel() gets the label of the Branch.
 * 
 * @return the label.
 * @type String
 */
TreeBoxBranch.prototype.getLabel		= function() {
	return this._label;
}

/**
 * getParent() gets the parent Object of this branch.
 * 
 * @return the parent.
 * @type Object
 */
TreeBoxBranch.prototype.getParent		= function() {
	return this._parent;
}

/**
 * setParent() sets the parent Object of this branch.
 * 
 * @param parent	{Object}	The parent object.
 */
TreeBoxBranch.prototype.setParent		= function(parent) {
	this._parent = parent;
}

/**
 * getParams() gets the parameters object of the Branch.
 * 
 * @return the parameters.
 * @type Object
 */
TreeBoxBranch.prototype.getParams		= function() {
	return this._params;
}

/**
 * hasParam() returns whether this Branch has the passed parameter.
 * 
 * @param id	{Mixed}	The parameter ID.
 * 
 * @return true if the parameter exists, false if not.
 * @type Boolean
 */
TreeBoxBranch.prototype.hasParam		= function(id) {
	return isDefined(this._params[id]);
}

/**
 * getParam() returns the value of the requested parameter in this Branch.
 * 
 * @param id	{Mixed}	The parameter ID.
 * 
 * @return the value of the parameter if found, false if not.
 * @type Mixed
 */
TreeBoxBranch.prototype.getParam		= function(id) {
	if (this.hasParam(id)) {
		return this._params[id];
	}
	
	return false;
}

/***************************************************************************************** INHERITED METHODS
 ***************************************************************************************/

TreeBoxBranch.prototype._parseData					= TreeBox._parseData;
TreeBoxBranch.prototype._parseBranch				= TreeBox._parseBranch;
TreeBoxBranch.prototype.getBranch					= TreeBox.getBranch;
TreeBoxBranch.prototype.getBranchIndex				= TreeBox.getBranchIndex;
TreeBoxBranch.prototype.hasBranch					= TreeBox.hasBranch;
TreeBoxBranch.prototype.isBranch					= TreeBox.isBranch;
TreeBoxBranch.prototype.hasBranches					= TreeBox.hasBranches;
TreeBoxBranch.prototype.getBranches					= TreeBox.getBranches;
TreeBoxBranch.prototype.removeBranch				= TreeBox.removeBranch;
TreeBoxBranch.prototype.appendBranch				= TreeBox.appendBranch;
TreeBoxBranch.prototype.unshiftBranch				= TreeBox.unshiftBranch;
TreeBoxBranch.prototype.insertBranchBefore			= TreeBox.insertBranchBefore;



TreeBoxBranch.prototype.getSiblingBranches				= function() {
	
	var parent = this.getParent();
	
	if (this.isBranch(parent)) {
		return parent.getBranches();
	}
	
	return false;
}

TreeBoxBranch.prototype.getIndex						= function() {
	var siblings = this.getSiblingBranches();
	
	for (var i = 0; i < siblings.length; i++) {
		if (siblings[i].getID() == this.getID()) {
			return i;
		}
	}
	
	return false;
}

TreeBoxBranch.prototype.getNextSiblingBranch			= function() {
	
	var siblings = this.getSiblingBranches();
	
	if (isArray(siblings)) {
		if (isDefined(siblings[this.getIndex() + 1])) {
			return siblings[this.getIndex() + 1];
		}
	}
	
	return false;
}

TreeBoxBranch.prototype.getPreviousSiblingBranch		= function() {
	
	var siblings = this.getSiblingBranches();
	
	if (isArray(siblings)) {
		if (isDefined(siblings[this.getIndex() - 1])) {
			return siblings[this.getIndex() - 1];
		}
	}
	
	return false;
}

TreeBoxBranch.prototype.moveBranchDown					= function() {
	
	var next	= this.getNextSiblingBranch();
	var index	= this.getIndex();
	var parent	= this.getParent();
	
	if (this.isBranch(next)) {
		parent._branches[index + 1] = this;
		parent._branches[index] = next;
	
		parent.redraw();
	}
}

TreeBoxBranch.prototype.moveBranchUp					= function() {
	
	var previous	= this.getPreviousSiblingBranch();
	var index		= this.getIndex();
	var parent		= this.getParent();
	
	if (this.isBranch(previous)) {
		parent._branches[index - 1] = this;
		parent._branches[index] = previous;
		
		parent.redraw();
	}
	
	
}

/****************************************************************************************
 * DRAWING METHODS
 ***************************************************************************************/

/**
 * draw() draws this Branch.
 */
TreeBoxBranch.prototype.draw			= function() {
	
	// Create list item
	var li = $(createElement("li", ["id", "tb-" + this._treebox.getID() + "-b-" + this.getID()])).addClassName("tbb");
	if (this.hasBranches()) {
		li.addClassName("children");
	}
	
	
	
	li.appendChild(this.drawHeader());
	//li.innerHTML = this.drawHeader();
	li.appendChild(this.drawChildren());
	
	return li;
}

TreeBoxBranch.prototype.drawHeader		= function() {
	/*
	// Create
	var header = createElement("tr");
	
	// Add toggler
	header.appendChild(createElement("td", 
		["class",		"toggle open"],
		["onclick",		"TreeBox.toggleClick(this);"],
		"<em>TOG</em>"
	));
	
	// Add an icon
	header.appendChild(createElement("td",
		["class",		"icon"],
		"<em>ICON</em>"
	));
	
	// Add a title
	header.appendChild(createElement("th",
		this.getLabel() + " <small>" + this.getID() + "</small>",
		["onclick",		this.hasParam("onclick") ? this.getParam("onclick") : ""]
	));
	
	// Return header wrapped in a table
	return createElement("table", ["cellspacing", "0"], header);
	*/
	
	var header = "<table cellspacing=\"0\"><tbody><tr>" +
		"<td class=\"toggle open\" onclick=\"TreeBoxes.toggleExpansion('" + this._treebox.getID() + "', '" + this.getID() + "');\">&nbsp;</td>" +
		"<td class=\"icon\">&nbsp;</td>" +
		"<th onmousedown=\"TreeBoxes.startDragging('" + this._treebox.getID() + "', '" + this.getID() + "');\" onmouseup=\"TreeBoxes.stopDragging('" + this._treebox.getID() + "', '" + this.getID() + "');\">" + this.getLabel() + " <small>" + this.getID() + "</small></th>" +
	"</tr></tbody></table>";
	
	return createElement("div", header);
	
}

TreeBoxBranch.prototype.drawChildren	= function() {
	
	var children = createElement("ul");
	for (var i in this._branches) {
		if (this.isBranch(this._branches[i])) {
			children.appendChild(this._branches[i].draw());
		}
	}
	
	return children;
}

TreeBoxBranch.prototype.getElement		= function() {
	return $("tb-" + this._treebox.getID() + "-b-" + this.getID());
}

TreeBoxBranch.prototype.expand			= function() {
	var element = this.getElement();
	this._expanded = true;
	element.removeClassName("closed");
}

TreeBoxBranch.prototype.contract		= function() {
	var element = this.getElement();
	this._expanded = false;
	element.addClassName("closed");
}

TreeBoxBranch.prototype.isExpanded		= function() {
	return this._expanded;
}

TreeBoxBranch.prototype.toggleExpansion	= function() {
	if (this.isExpanded()) {
		this.contract();
	} else {
		this.expand();
	}
}

TreeBoxBranch.prototype.redraw			= function() {
	var element = this.getElement();
	
	if (element) {
		element.parentNode.insertBefore(this.draw(), element);
		element.parentNode.removeChild(element);
	}
}

/*** @inca-include ../admin/assets/scripts/classes/Popup.js ***/

/****************************************************************************************
 * THE POPUP CLASS
 ***************************************************************************************/

/**
 * The Popup class handles a single Popup.
 * 
 * @creator Cameron Morrow
 * 
 * @param content	{String}	The initial content of the popup.
 * @param params	{Object}	A list of parameters for the popup.
 */
function Popup(content, params) {
 	
 	// Constants
 	this._CLASS						= "Popup";
 	
 	// Store properties
	this._id						= Popups.getUniqueID();
	this._elements					= {};
	this._content					= content;
	
	// Store parameters
	this._params					= {
		type:			isArray(params.type)			? params.type				: [],
		title:			isString(params.title)			? params.title				: "Popup",
		movable:		isBoolean(params.movable)		? params.movable			: true,
		has_close_box:	isBoolean(params.has_close_box)	? params.has_close_box		: true,
		width:			isIntegeric(params.width)		? parseInt(params.width)	: 600,
		height:			isIntegeric(params.height)		? parseInt(params.height)	: 300,
		x:				isIntegeric(params.x)			? parseInt(params.x)		: false,
		y:				isIntegeric(params.y)			? parseInt(params.y)		: false,
		buttons:		isArray(params.buttons)			? params.buttons			: [],
		modal:			isBoolean(params.modal)			? params.modal				: false
	};
	
	// Validate X and Y position
	if (this._params.x === false && this._params.y === false) {
		this._params.x = (0.5 * Page.simpleGetWindowWidth()) - (0.5 * this._params.width);
		this._params.y = (0.5 * Page.simpleGetWindowHeight()) - (0.5 * this._params.height);
	}
	
	// Create DOM elements
	this._createElements();
	
	// Draw
	this.redraw();
	
	// Position
	this.moveTo(this._params.x, this._params.y);
}

/**
 * _createElements() creates the DOM elements for this popup.
 */
Popup.prototype._createElements		= function() {
	
	// Base - for modal popups this covers up the rest of the screen.
	this._elements.base			= $(createElement("div",
		["id",		this._id]
	)).addClassName("popupbase");
	
	// Popup - the main popup element, including the drop shadow.
	this._elements.popup		= $(createElement("div",
		["id",		this._id + "-main"]
	)).addClassName("popup");
	for (var i = 0; i < this._params.type.length; i++) {
		$(this._elements.popup).addClassName(this._params.type[i]);
	}
	
	// The header
	this._elements.header		= $(createElement("div",
		["id", this._id + "-header"]
	)).addClassName("popupheader");
	
	// The title of the popup
	this._elements.title		= createElement("h3",
		["id", this._id + "-title"],
		this._params.title
	);
	
	// The close box of the popup	
	this._elements.close_button = $(createElement("a",
		["href",		"javascript:void(0);"],
		["onclick",		"Popups.close('" + this._id + "');"],
		"&times;"
	)).addClassName("greybutton");
	
	// The content of the popup
	this._elements.content		= $(createElement("div",
		["id",		this._id + "-content"]
	)).addClassName("popupcontent");
	
	// The inner element, above the drop shadow, containing the header and content
	this._elements.inner = $(createElement("div",
		this._elements.header,
		this._elements.content
	)).addClassName("popupinner");
	
	// Buttons
	if (this._params.buttons.length > 0) {
		var buttons = $(createElement("div",
			["id", this._id + "-buttons"]
		)).addClassName("popupbuttons");
		for (var i = 0; i < this._params.buttons.length; i++) {
			var button = this._params.buttons[i];
			if (isObject(this._params.buttons[i])) {
				if (button._CLASS == PopupCloseButton.prototype._CLASS || button._CLASS == PopupButton.prototype._CLASS) {
					buttons.appendChild(button.draw());
				} else {
					Alerts.error("Object in button list is not really a button, is a " + Alerts._getJSObjectClassName(button), this, "_createElements");
				}
			} else {
				Alerts.error("Item in button list is not an Object.", this, "_createElements");
			}
		}
		
		this._elements.inner.appendChild(buttons);
		
		this._elements.buttons = buttons;
	} else {
		this._elements.buttons = false;
	}
	
	// Attach all elements
	this._elements.header.appendChild(this._elements.close_button);
	this._elements.header.appendChild(this._elements.title);
	this._elements.popup.appendChild(this._elements.inner);
	this._elements.base.appendChild(this._elements.popup);
	
	
	// Set up dragging
	if (this._params.movable) {
		this._elements.title.onmousedown = function() {
			//alert(this.innerHTML);
			Drag.startDrag(Popups.getPopupDiv(this), {top: 0, left: 0, bottom: Page.simpleGetWindowHeight(), right: Page.simpleGetWindowWidth()});
		}
	}
	
	// Set up moving to top
	this._elements.title.onmouseup = function() {
		Popups.moveToTop(Popups.getPopupObject(this));
	}
}

/**
 * getID() gets the ID of this popup.
 * 
 * @return the value of the _id property.
 * @type String
 */
Popup.prototype.getID					= function() {
	return this._id;
}

/**
 * getElement() gets the base element of this popup.
 * 
 * @return the value of the base DOM element containing the entire popup.
 * @type DOM Element
 */
Popup.prototype.getElement				= function() {
	return this._elements.base;
}

Popup.prototype.getCurrentPosition		= function() {
	return Page.simpleGetPosition(this._elements.popup);
}

Popup.prototype.setContent				= function(content) {
	this._content = content;
	this.redraw();
}

Popup.prototype.moveTo					= function(x, y) {
	Page.simpleMoveTo(this._elements.popup, x, y);
}

Popup.prototype.moveBy					= function(x, y) {
	Page.simpleMoveBy(this._elements.popup, x, y);
}

Popup.prototype.hide					= function() {
	this._elements.popup.addClassName("hidden");
}

Popup.prototype.unhide					= function() {
	this._elements.popup.removeClassName("hidden");
}

Popup.prototype.close					= function() {
	Popups.close(this._id);
}

Popup.prototype.isModal					= function() {
	if (isDefined(this._params.modal)) {
		return this._params.modal;
	}
	
	return false;
}

/**
 * redraw() redraws the popup with updated attributes, content, etc.
 */
Popup.prototype.redraw					= function() {
	
	// Resize inner & content
	Page.simpleResize(this._elements.inner, this._params.width, this._params.height);
	Page.simpleSetHeight(this._elements.content, this._params.height - Popups.HEADER_HEIGHT - Popups.BUTTONS_HEIGHT);
	
	// Update modal base
	this.redrawModalBase();
	
	// Update position
	//this.updatePosition();
	
	// If has close box, enable now
	if (this._params.has_close_box) {
		$(this._elements.header).addClassName("closable");
	} else {
		$(this._elements.header).removeClassName("closable");
	}
	
	// Set content
	if (isString(this._content)) {
		this._elements.content.innerHTML = this._content;
	} else if (isObject(this._content)) {
		this._elements.content.innerHTML = "";
		this._elements.content.appendChild(this._content);
	}
}

/**
 * redrawModalBase() redraws the modal (click-preventing) cover.
 */
Popup.prototype.redrawModalBase			= function() {
	if (this.isModal()) {
		Page.simpleResize(this._elements.base, Page.simpleGetWindowWidth(), Page.simpleGetWindowHeight());
		this._elements.base.addClassName("modal");
	} else {
		Page.simpleResize(this._elements.base, 0, 0);
		this._elements.base.removeClassName("modal");
	}
}

/**
 * updatePosition() verifies the position of the Popup is ok in the current window size.
 */
Popup.prototype.updatePosition			= function() {

	// If element exists
	if (this._elements.popup) {
	
		// Get new X/Y values of object
		var position = {
			x: Page.simpleGetX(this._elements.popup),
			y: Page.simpleGetY(this._elements.popup)
		};
		
		// Validate position against boundaries
		var proposed = Drag.validatePosition(this._elements.popup, position, {top: 0, left: 0, bottom: Page.simpleGetWindowHeight(), right: Page.simpleGetWindowWidth()});
		
		// Position element
		Page.simpleMoveTo(this._elements.popup, proposed.left, proposed.top);
	}
}

Popup.prototype.addInputsToRequest		= function() {
	//Request.addExtraParam('hito', 'yuo');
	
	var values = Request.getInputValuesIn(this._elements.popup);
	
	Request.mergeExtraParams(values);
}

Popup.prototype.focusButton				= function() {
	if (this._elements.buttons) {
		
		// Collect button <input> elements
		var buttons = this._elements.buttons.getElementsByTagName("button");
		
		// If at least one was found, focus the first
		if (buttons.length > 0) {
			buttons[0].focus();
		}
	}
}

/*** @inca-include ../admin/assets/scripts/classes/PopupButton.js ***/

/****************************************************************************************
 * THE POPUP BUTTON CLASSES
 ***************************************************************************************/

/**********************************************************
 * POPUP BUTTON
 *********************************************************/

/**
 * The PopupButton() class allows creations of buttons to appear on the bottom of Popups.
 * 
 * @creator Cameron Morrow
 * 
 * @param id		{String}	An ID for the object.
 * @param params	{Object}	An object containing parameters for the Button.
 */
function PopupButton(id, params) {
	this._id = isString(id) ? id : "";
	this._parseParams(params);
}

// Properties
PopupButton.prototype.ID_SUFFIX						= "popupbutton";
PopupButton.prototype._CLASS						= "PopupButton";

/**
 * _parseParams() parses the parameters object passed to the instantiator to get useful
 * parameters for the Button.
 * 
 * @param params	{Object}	An object containing parameters for the Button.
 */
PopupButton.prototype._parseParams					= function(params) {
	this._params = {};
	
	if (isObject(params)) {
		this._params = {
			type:			isString(params.type)		? params.type				: "button",
			label:			isString(params.label)		? params.label				: "Unlabelled Button",
			on_click:		isString(params.on_click)	? params.on_click			: "",
			width:			isIntegeric(params.width)	? parseInt(params.width)	: false,
			params:			isObject(params.params)		? params.params				: {}
		};
	}
}

/**
 * draw() draws the Button.
 * 
 * @return a DOM Element for the button.
 * @type DOMElement
 */
PopupButton.prototype.draw							= function() {
	
	// Create element
	this._element = $(createElement("button",
		["id", this._id + "-" + this.ID_SUFFIX],
//		["onclick", this._params.on_click],
		this._params.label
	));
	
	eval("this._element.onclick = function() { " + this._params.on_click + " }");
	
	// Store other parameters
	this._element._params = this._params.params;
	
	// Optional width
	if (isIntegeric(this._params.width)) {
		Page.simpleSetWidth(this._element, this._params.width);
	}
	
	// Done
	return this._element;
}

/**********************************************************
 * POPUP CLOSE BUTTON
 * Extension of the button that closes the Popup it
 * appears in.
 *********************************************************/

/**
 * The PopupCloseButton class is an extension of the PopupButton class to create buttons
 * that close their Popup.
 * 
 * @creator Cameron Morrow
 * 
 * @param id		{String}	An ID for the object.
 * @param params	{Object}	An object containing parameters for the Button.
 */
function PopupCloseButton(id, params) {
	
	// Store ID
	this._id = isString(id) ? id : "";
	
	// Verify parameters
	params = isObject(params) ? params : {};
	params.on_click = "Popups.getPopupObject(this).close();";
	
	// Parse
	this._parseParams(params);
}

// Properties
PopupCloseButton.prototype.ID_SUFFIX				= "popupclosebutton";
PopupCloseButton.prototype._CLASS					= "PopupCloseButton";

// Inherited methods
PopupCloseButton.prototype._parseParams				= PopupButton.prototype._parseParams;
PopupCloseButton.prototype.draw						= PopupButton.prototype.draw;

/*** @inca-include ../admin/assets/scripts/classes/Flags.js ***/

function Flags() {
	this._value	= 0;
	
	for (var i = 0; i < arguments.length; i++) {
		this.set(arguments[i]);
	}
};

Flags.prototype.getValue				= function() {
	return this._value;
}

Flags.prototype.set						= function() {
	if (arguments.length > 0) {
		for (var i = 0; i < arguments.length; i++) {
			this._modify(arguments[i], true);
		}
	}
	return this;
};

Flags.prototype.remove					= function() {
	if (arguments.length > 0) {
		for (var i = 0; i < arguments.length; i++) {
			this._modify(arguments[i], false);
		}
	}
	return this;
};

Flags.prototype.has						= function(flag) {
	if (this._isValidFlag(flag)) {
		if (this._value & flag) {
			return true;
		}
	}
	
	return false;
};

Flags.prototype._modify					= function(flag, set) {
	set = set ? true : false;
	
	if (this._isValidFlag(flag)) {
		this._value = set ? (this._value | flag) : (this._value & ~flag);
		return true;
	}
	
	return false;
};

Flags.prototype._isValidFlag			= function(flag) {
	if (isIntegeric(flag)) {
		flag = parseInt(flag);
		if (flag > 0) {
			if (flag == 1 || flag % 2 == 0) {
				return true;
			}
		}
	}
	
	return false;
};

Flags.prototype.getFlags				= function() {
	
	// Create list of flags
	var flags = [];
	
	// Get highest flag
	var flag = this._getMaximumFlag();
	
	// Repeat until done
	while (flag > 0) {
		
		// Test if flag exists
		if (this._value & flag) {
			flags.push(flag);
		}
		
		// Try the next smallest flag
		flag = flag / 2;
	}
	
	// Done
	return flags;
};

Flags.prototype._getMaximumFlag			= function() {
	
	// Count the power of two multipliers
	var flag_counter = 0;
		
	// While the power still fits, increment it
	while (Math.pow(2, flag_counter + 1) <= this._value) {
		flag_counter++;
	}
	
	// OK, found the maximum
	return Math.pow(2, flag_counter);
};

Flags.prototype.toString				= function() {
	return "" + this._value;
};

/*** @inca-include ../admin/assets/scripts/classes/items/generic.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/items/generic.js
 * FUNCTION:		Contains the GenericDOM class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2008-06-05
 ***************************************************************************************/

/**
 * The GenericDOM class is a generic class containing a DOM element that does not have a
 * specific handler.
 * 
 * @param id	{String}	The item ID.
 */
function GenericDOM(id) {
	this._id = id || "";
}

GenericDOM.prototype.setValue						= function(value) {
	Alerts.warning("Setting a value on a Generic item: " + this._id);
}

GenericDOM.prototype.setStructure					= function(value) {
	Alerts.warning("Setting structure on a Generic item: " + this._id);
}

GenericDOM.prototype.draw							= function() {
	return $(createElement("div", "Generic Element [" + this._id + "]"));
}

/*** @inca-include ../admin/assets/scripts/classes/items/date_time_select.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/items/date_time_select.js
 * FUNCTION:		Contains the DateTimeSelect object which handles parsing of
 * 					DateTimeSelect Component Items.
 * CREATOR:			Cameron Morrow
 * CREATED:			2008-05-16
 ***************************************************************************************/

var DateTimeSelect = {
	
	parseComponentItem:				function(component_item, data) {
		
		// Create Date Input element
		component_item._element						= new DateInput(
			component_item.getID(),
			data.value.value,
			data.value.mode,
			false,
			DateInput.DISPLAY_MODE_WITH_TIME
		);
		
		/**
		 * getSubmitValues() gets an object containing the values this Input should submit.
		 * 
		 * @return an Object with properties and values to submit.
		 * @type Object
		 */
		component_item.getSubmitValues				= function() {
			
			var values = {};
			values[this._id + "-dtmode"]	= this.getElement().getDateMode();
			values[this._id]				= this.getElement().getDateValue();
			
			return values;
		}
		
		/**
		 * getElement() gets the element used in this Item.
		 */
		component_item.getElement					= ComponentItem.getElement;
		
		/**
		 * setValue() sets the value of a DateTimeSelector item.
		 * 
		 * @param value	{Object}	An object with 'value' and 'mode' properties.
		 */
		component_item.setValue						= function(value) {
			
			// Validate
			if (isObject(value)) {
				if (isIntegeric(value.value) && isIntegeric(value.mode)) {
					
					// Set value and mode
					this.getElement().setDateValueAndMode(value.value, value.mode);

					// Redraw
					this.getElement().updateDOMElements();
				} else {
					Alerts.warning("Date Time Selector 'value' and 'mode' values were not Integeric: value: " + value.value + ", mode: " + value.mode, this, "setValue", true);
				}
			} else {
				Alerts.warning("Date Time Selector '" + this._id + "' was expecting an object value, was given: " . value, this, "setValue", true);
			}
		}
	}
};

ComponentItem.registerParser("datetimeselect", DateTimeSelect.parseComponentItem);

/*** @inca-include ../admin/assets/scripts/classes/items/selector.js ***/

/****************************************************************************************
 * FILE:			admin/assets/scripts/classes/items/selector.js
 * FUNCTION:		Contains the SelectorCI object and SelectorDOM class.
 * CREATOR:			Cameron Morrow
 * CREATED:			2008-06-04
 ***************************************************************************************/

/**
 * The SelectorCI object handles parsing of CIs that are destined to be selectors.
 * 
 * @creator Cam Morrow
 */
var SelectorCI = {
	
	parseComponentItem:				function(component_item, data) {

		component_item._element			= new SelectorDOM(
			component_item.getID(),
			isObject(data.options) ? data.options : {},
			{
				value:		(isString(data.value) || isInt(data.value))		? data.value		: false,
				index:		false,
				on_change:	isString(data.on_change)						? data.on_change	: false,
				required:	isBoolean(data.required)						? data.required		: false
			}
		);
		
		component_item.getSubmitValues				= function() {
			
			// Create object
			var values = {};
			
			// Get current value
			values[this._id] = this.getElement().getSelectedValue();
			
			// Return
			return values;
		};
	}
};

ComponentItem.registerParser("select", SelectorCI.parseComponentItem);

/**
 * The SelectorDOM class allows construction of <select> tags.
 * 
 * @creator Cameron Morrow
 * 
 * @param id		{String}			The ID of the DOM element to create.
 * @param values	{Object}			An object containing property IDs and Values that
 * 										represent values and labels for the <select>:
 * 										{
 * 											1: 'One',
 * 											2: 'Two',
 * 											3: 'Three'
 * 										}
 * @param params	{Object}			An object containing properties for the Selector,
 * 										including value, index, on_change, etc.
 * 										{
 * 											value: '3',
 * 											index: false
 * 										}
 */
function SelectorDOM(id, values, params) {
	
	// Store
	this._id			= isString(id)					? id				: "";
	this._on_change		= isString(params.on_change)	? params.on_change	: false;
	this._required		= isBoolean(params.required)	? params.required	: false;
	this._elements		= {};

	// Validate
	this._validateSelected(params || {value: false, index: false});
	this._validateValues(values || {});
	this._generateDOM();
}

/**
 * _validateSelected() validates the passed selected object to derive a correct local
 * selected object.
 * 
 * @param selected	{Object}	An object to parse to determine the selected option.
 */
SelectorDOM.prototype._validateSelected	= function(selected) {
	
	// Set initial state
	this._selected = {
		index:	false,
		value:	false
	}
	
	// If an appropriate object was passed
	if (isObject(selected)) {
		
		// Set index
		if (isBoolean(selected.index)) {
			this._selected.index = selected.index;
		}
		
		// Set value
		if (isDefined(selected.value)) {
			this._selected.value = selected.value;
		}
	}
}

/**
 * _validateValues() validates an object to derive values and their labels for this
 * <select>.
 * 
 * @param values	{Object}	An object containing properties and values.
 */
SelectorDOM.prototype._validateValues		= function(values) {
	
	// Set initial state
	this._values = {};
	
	// If an object was passed
	if (isObject(values)) {
		
		// For each passed value in the values object
		for (var i in values) {
			
			// If index is appropriate format
			if (isString(i) || isIntegeric(i)) {
				
				// If value is appropriate format
				if (isString(values[i]) || isNumeric(values[i])) {
					this._values[String(i)] = String(values[i]);
				} else if (isBoolean(values[i])) {
					this._values[String(i)] = values[i] ? "True" : "False";
				}
			}
		}
	}
}

/**
 * setStructure() updates structural elements of this item.
 * 
 * @param params	{Object}	The new parameters to set.
 */
SelectorDOM.prototype.setStructure			= function(params) {

	// If updating options
	if (isDefined(params["options"])) {
		this._validateValues(params["options"]);
	}
	
	// Regenerate DOM
	this.redraw();
}

/**
 * getIndexOfValue() gets the index of a passed value ID in this item's value list.
 * 
 * @param value	{String}	The value to look for.
 * 
 * @return the index of the value in the values object, or false if not found.
 * @type Boolean/Integer
 */
SelectorDOM.prototype.getIndexOfValue		= function(value) {
	var index = 0;
	for (var i in this._values) {
		if (String(i) == String(value)) {
			return index;
		}
		index++;
	}
	
	return false;
}

/**
 * getSelected() gets the current value of the selected object.
 * 
 * @return the selected object.
 * @type Object
 */
SelectorDOM.prototype.getSelected			= function() {
	return this._selected;
}

/**
 * getSelectedValue() gets the current selected value of this input.
 * 
 * @return the current value.
 * @type String
 */
SelectorDOM.prototype.getSelectedValue		= function() {
	return $F(this._elements.select);
}

/**
 * setValue() sets the value of this item to the passed value. It also updates the DOM
 * element of this object as well, if it has already been created.
 * 
 * @param value	{String}	The value to set as active.
 */
SelectorDOM.prototype.setValue				= function(value) {
	
	// Check value exists
	var index = this.getIndexOfValue(value);
	
	// If OK
	if (index !== false) {
		
		// Store value
		this._selected.value = value;
		this._selected.index = false;
		
		// Update DOM element
		if ($(this._id)) {
			$(this._id).selectedIndex = index;
		}
	}
}

/**
 * onChange() is triggered whenever the <select> DOM Element associated with this object
 * is changed. It updates the current selected value with the new value.
 */
SelectorDOM.prototype.onChange				= function() {
	this._selected.value	= $F(this._id);
	this._selected.index	= false;
}


/**********************************************************
 * DOM Methods
 *********************************************************/

/**
 * _generateDOM() generates the DOM elements for this item.
 */
SelectorDOM.prototype._generateDOM					= function() {
	this._elements.select = $(createElement("select",
		["id",		this._id],
		["name",	this._id]
	)).addClassName("select");
	
	// OnChange setup
	this._elements.select.setAttribute("onchange", "this._selector.onChange(); " + this._on_change);
	
	// Add a back-reference
	this._elements.select._selector = this;
	
	// Regenerate options
	this._regenerateOptions();
}

/**
 * _regenerateOptions() regenerates the options list for this item.
 */
SelectorDOM.prototype._regenerateOptions			= function() {
	
	// Reset contents
	while (this._elements.select.hasChildNodes()) {
		this._elements.select.removeChild(this._elements.select.firstChild);
	}
	
	// Populate
	var counter = 0;
	for (var i in this._values) {
		var option = createElement("option", ["value", i], this._values[i]);
		
		if (this._selected.index && this._selected.value == counter) {
			option.setAttribute("selected", "selected");
		} else if (this._selected.value == i) {
			option.setAttribute("selected", "selected");
		}
		
		this._elements.select.appendChild(option);
		counter++;
	}
}

/**
 * draw() draws the <select> for this SelectorDOM object.
 * 
 * @param no_wrap	{Boolean}	If true, the normal div wrapper is omitted, and just the
 * 								<select> is returned.
 * 
 * @return a DOM Element.
 * @type DOMElement
 */
SelectorDOM.prototype.draw							= function() {
	return this._elements.select;
}

/**
 * getElement() returns the DOM element of this select drop down.
 * 
 * @return the DOM element.
 * @type DOMElement
 */
SelectorDOM.prototype.getElement					= function() {
	return $(this._id);
}

/**
 * redraw() redraws the DOM element, updating it's existing stage element.
 */
SelectorDOM.prototype.redraw						= function() {
	this._regene