/*
 * JS-Utils.js - Version 0.2
 *
 * $Id: JS-Utils.js 28 2009-08-24 01:41:48Z terence $
 *
 * Copyright (c) 2008 Terence Honles (terence.honles.com)
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 *
 */

function $JSUtils(localTo) {
/*
 * Munging Constants
 */

var undefined;
var array_slice = Array.prototype.slice;
var abs = Math.abs;
var max = Math.max;
var min = Math.min;
var pow = Math.pow;

/*
 * Exceptions
 */

// Error that you are not using a supported browser
//   message - Error message
function UnsupportedBrowserError(message) {
	var error = Error.call(this, message);
	error.name = 'UnsupportedBrowserError';
}

/*
 * Constants
 */

// @constant
var Key = {
	Backspace:8,
	Tab:9,
	Enter:13,
	Escape:27,
	Space:32,
	Left:37,
	Right:39,
	Up:38,
	Down:40
};

/*
 * Utility functions
 */

function $generate(xmlFragment, parameters) {
	// @private
	if (!$generate.div) $generate.div = document.createElement('div');

	$generate.div.innerHTML = xmlFragment;

	var element = $generate.div.firstChild;

	for (var i in parameters)
		if (i.slice(0,2) == 'on')
			$addEventListener(element, i, parameters[i]);

	return element;
}

function $addEventListener(element, event, callback) {
	if (element.attachEvent)
		element.attachEvent(event, callback);
	else
		element.addEventListener(event.slice(2), callback, false);
}


// Equivalent to arguments[0] || arguments[1] || ...
// [arglist] - variable number of arguments
function any() {
	var argument = undefined;

	for (var i in arguments) {
		argument = arguments[i];

		if (argument) return argument;
	}

	return argument;
}

// Equivalent to arguments[0] && arguments[1] && ...
// [arglist] - variable number of arguments
function all() {
	var argument = undefined;

	for (var i in arguments) {
		argument = arguments[i];

		if (!argument) return argument;
	}

	return argument;
}

// Applies fn to slices of the remaining arguments
// (i.e. for two additional arguments this would
// result in [fn(arguments[1][0], arguments[2][0]),
//            fn(arguments[1][1], arguments[2][1]),
//            fn(arguments[1][2], arguments[2][2]),
//            ...])
//
//   fn - may be any one of the following:
//     + null - fn <= function() { return arguments; }
//     + An array where the first element is the scope and the
//       second is the callback
//     + A <Handler> instance
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
//   (arglist)+ - variable number of arrays
function map(fn) {
	var results = [];
	var handler = null;

	if (fn === null) {
		handler = Handler([{}, function() {
			return array_slice.call(arguments, 0);
		}]);
	} else {
		handler = Handler(fn);
	}

	var args = array_slice.call(arguments, 1);
	var length = args.length;

	if (!length)
		throw new TypeError('Map must be called with at least 2 arguments');

	if (length == 1) {
		length = args[0].length;
		var argument = args[0];

		for (var i=0; i<length; i++)
			results.push(handler(argument[i]));

	} else {
		length = max.apply({}, map(array_length, args));

		for (var i=0; i<length; i++)
			results.push(handler.apply(map([{}, get_index(i)], args)));
	}

	return results;
}

// Access length as a function rather than a property
//   _array - array to determine the length of
function array_length(_array) {
	return _array.length;
}

// Create a function to access it's first argument's index 'index'
//   index - index to retrieve
function get_index(index) {
	return function(argument) { return argument[index]; };
}

// a shortcut function to allow the mapping function to be
// applied on the following arguments
// equivalent to map(fn, [arg1, arg2, arg3, ...])
function mapAll(fn) {
	var args = array_slice.call(arguments, 1);
	return map(fn, args);
}

// Applies fn to slices of the remaining arguments
// if the return is true then that slice is added to the result set
//
// you can think of the base case as the normal filter
// (i.e. filter(function(x) { return (x%2 == 0) }, [0,1,2,3,4]) => [0,2,4])
//
// the case where there is multiple arguments
//
// result = filter(fn, <arguments>); produces the same result as
//
// function _fn() {
//   if (fn.apply(scope, arguments)) return arguments;
//   else return false;
// }
//
// result = filter(null, map(_fn, <arguments>));
//
//   fn - may be any one of the following:
//     + null - fn <= function(item) { return item || false }
//     + An array where the first element is the scope and the
//       second is the callback
//     + A <Handler> instance
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
//   (arglist)+ - variable number of arrays
function filter(fn) {
	var results = [];
	var handler = null;

	if (fn === null) {
		handler = Handler([{}, function(argument) {
			return argument || false;
		}]);
	} else {
		handler = Handler(fn);
	}

	var args = array_slice.call(arguments, 1);
	var length = args.length;

	if (!length)
		throw new TypeError('Filter must be called with at least 2 arguments');

	if (length == 1) {
		length = args[0].length;
		var argument = args[0];

		for (var i=0; i<length; i++) {
			var item = argument[i];
			if (handler(item)) results.push(item);
		}
	} else {
		length = max.apply({}, map(array_length, args));

		for (var i=0; i<length; i++) {
			var item = map([{}, get_index(i)], args);
			if (handler.apply(item)) results.push(item);
		}
	}

	return results;
}

// a shortcut function to allow the filtering function to be
// applied on the following arguments
// equivalent to filter(fn, [arg1, arg2, arg3, ...])
function filterAll() {
	var args = array_slice.call(arguments, 1);
	return filter(arguments[0], args);
}

var reduce_short_sequence = 'Reduce sequence must be at least length 1';

// Initializes the result to a slice of the remaining arguments
// Iteratively applies fn to result and slices of the remaining arguments
//
// (i.e. reduce(function(x,y) {x+y}, [1,2,3,4]) => 10 and
// reduce(function(x,y) {
//   var result=[];
//   for (var i in x) result.push(x[i] + y[i]);
//   return result;
// }, [1,2,3,4], [1,2,3,4], [10,20,30,40], ...) => sums elements at a
// particular index
//
//   fn - may be any one of the following:
//     + null - fn <= function(item) { return item || false }
//     + An array where the first element is the scope and the
//       second is the callback
//     + A <Handler> instance
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
//   (arglist)+ - variable number of arrays
function reduce(fn) {
	var result = null;
	var handler = Handler(fn);

	var args = array_slice.call(arguments, 1);
	var length = args.length;

	if (!length)
		throw new TypeError('Reduce must be called with at least 2 arguments');

	if (length == 1) {
		length = args[0].length
		var argument = args[0];

		if (!length)
			throw new TypeError(reduce_short_sequence);

		result = argument[0];

		for (var i=1; i<length; i++)
			result = handler(result, argument[i]);
	} else {
		length = max.apply({}, map(array_length, args));

		if (!length)
			throw new TypeError(reduce_short_sequence);

		result = map([{}, get_index(0)], args);

		for (var i=1; i<length; i++)
			result = handler(result, map([{}, get_index(i)], args));
	}

	return result;
}

// Returns a range of integers starting from
// start ending before stop with a step of 'step'
//   [start] - defaults to 0, starting point
//   stop - ending point
//   [step] - defaults to 1, step size 
function range(start, stop, step) {
	var length = arguments.length;

	if (!length) throw new TypeError('Range requires at least one argument');
	else if (length == 1) {
		var stop = start
		var start = 0;
	}

	if (!step) step = 1;
	var result = [];

	for (; start<stop; start+=step)
		result.push(start);

	return result;
}

// returns the sum of all the arguments, initialized with 0
// [arglist] - variable number of arguments to sum up
function sum() {
	return reduce([{}, plus], [0].concat(array_slice.call(arguments, 0)));
}

// helper function for sum
// adds the first and second arguments together
function plus(one, two) {
	return one+two;
}

// Exposes Utils to the given object
//   localTo - object to attach to
function exposeUtils(localTo) {
	localTo.abs = abs;
	localTo.any = any;
	localTo.all = all;
	localTo.filter = filter;
	localTo.filterAll = filterAll;
	localTo.map = map;
	localTo.mapAll = mapAll;
	localTo.pow = pow;
	localTo.range = range;
	localTo.reduce = reduce;
	localTo.sum = sum;
	localTo.exposeUtils = exposeUtils;
}

/*
 * Classes
 */

// -------
// Handler
// -------

// @private
// @constant
// bad handler message
var BadHandler = 'Handler must be or contain a callback function';


// Handler
//   handler - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A <Handler> instance
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
// (ie Handler(function() {alert('handled'); }); )
function Handler(handler) {
	if (!handler) throw TypeError(BadHandler);

	var _handler = function() {
		return _handler.callback.apply(_handler.scope, arguments);
	}

	if (handler.callback) {
		_handler.scope = handler.scope;
		_handler.callback = handler.callback;
	} else if (handler[1]) {
		_handler.scope = handler[0];
		_handler.callback = handler[1];
	} else {
		_handler.callback = handler;
	}

	if (typeof(_handler.callback) != 'function')
		throw TypeError(BadHandler);

	if (!_handler.scope) _handler.scope = window;

	for (var i in Handler.prototype) {
		_handler[i] = Handler.prototype[i];
	}

	return _handler;
}

// Test for equality
//   other - other instance to test equivalence with
Handler.prototype.equals = function(other) {
	return (this.callback == other.callback) && (this.scope == other.scope);
}

// Apply the array "args" to the callback function
//   [junk] - we don't want to override the scope
//   args - an <Array> of arguments
Handler.prototype.apply = function(junk, args) {
	var len = arguments.length;

	if (len == 1) {
		// apply the first argument if there is only one (no scope given)
		return this.callback.apply(this.scope, junk);
	} else if (len == 2) { // discard the scope if given
		return this.callback.apply(this.scope, args);
	} else {
		throw new TypeError('Wrong number of arguments for apply');
	}
}

// -------------
// EventRegistration
// -------------

// Creates a set of event handlers based on the object provided
//   flags - object with binarily disjoint values
//           (ie. flags = {one:1, two:2, three:4...})
function EventRegistration(flags) {
	if (!flags || typeof(flags) != 'object')
		throw new TypeError('Flags must be a flag object');

	// @private
	this.event = {};

	for (var name in flags) {
		var flag = flags[name];
		this.event[flag] = {name:name, handlers:[]};
	}

	return this;
}

// Adds Event Listeners
//   eventTypes - must be one or more flags (ie. flags.one | flags.two)
//   handler - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
EventRegistration.prototype.addEventListener = function(eventTypes, handler) {
	for (var event in this.event) {
		if (eventTypes & event)
			this.event[event].handlers.push(Handler(handler));
	}

	return this;
}

// Removes Event Listener
//   eventTypes - must be one or more flags (ie. flags.one | flags.two)
//   handler - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
EventRegistration.prototype.removeEventListener = function(eventTypes, handler) {
	var _handler = Handler(handler);

	for (var event in this.event) {
		if (eventTypes & event) {
			var handlers = this.event[event].handlers;

			for (var i in handlers) {
				if (handlers[i].equals(_handler)) {
					handlers.splice(i, 1);
					break;
				}
			}
		}
	}

	return this;
}

// Dispatches a certain event and notifies all listeners
//   eventType - single flag to trigger
//   [arglist] - variable number of arguments
EventRegistration.prototype.dispatchEvent = function(eventType) {
	if (!(eventType in this.event))
		throw new TypeError('Event not supported!');

	var handlers = this.event[eventType].handlers;
	var args = array_slice.call(arguments, 1);

	for (var i in handlers)
		handlers[i].apply(args);
}

// -------------
// AJAXRequestor
// -------------

// @private
// @constant
// AJAX request type
var AJAXSupport = {
	XMLHttpRequest:1,
	MSXML2: 2,
	XMLHTTP:4,
	None:8
};

// @private
// @constant
// <XHR> constructors
var XHR = {
	1 : function () { return new XMLHttpRequest(); },
	2 : function () { return new ActiveXObject('Msxml2.XMLHTTP'); },
	4 : function () { return new ActiveXObject('Microsoft.XMLHTTP'); },
	8 : function () {
		throw new UnsupportedBrowserError('Your browser does not support AJAX!');
	}
}

// @private
// @constant
// AJAX preference ordering (most -> least)
var AJAXAttemptOrdering = [
	AJAXSupport.XMLHttpRequest,
	AJAXSupport.MSXML2,
	AJAXSupport.XMLHTTP,
	AJAXSupport.None
];

// @constant
// Listenable events
var AJAXEvent = {
	Open:1,
	Sending:2,
	Receiving:4,
	Loaded:8,
	Success:16,
	Failure:32,
	FailureClient:64,
	FailureServer:128
};

// @private
// @constant
// <XHR>.readyState values
var AJAXState = {
	Uninitialized:0,
	Open:1,
	Sent:2,
	Receiving:3,
	Loaded:4
};

// @private
// @constant
// supported <XHR> method values
var HTTPMethod = {
	GET: 0,
	POST: 1
};

// @private
// Receives onrequestchange events and dispatches events accordingly
//   requestor - <AJAXRequestor> instance
//   request - <XHR> instance
function AJAXRequestStateChange(requestor, request) {
	switch(request.readyState) {
		case AJAXState.Open:
			requestor.dispatchEvent(AJAXEvent.Open, request);
			break;
		case AJAXState.Sent:
			requestor.dispatchEvent(AJAXEvent.Sending, request);
			break;
		case AJAXState.Receiving:
			requestor.dispatchEvent(AJAXEvent.Receiving, request);
			break;
		case AJAXState.Loaded:
			requestor.dispatchEvent(AJAXEvent.Loaded, request);
			if ((request.status >= 200 && request.status < 400) ||
					request.status == 0) {
				requestor.dispatchEvent(AJAXEvent.Success, request);
			} else if (request.status >= 400 && request.status < 600) {
				requestor.dispatchEvent(AJAXEvent.Failure, request);
				if (request.status < 500)
					requestor.dispatchEvent(AJAXEvent.FailureClient, request);
				else
					requestor.dispatchEvent(AJAXEvent.FailureServer, request);
			}
	}
}

// AJAXRequestor Constuctor
//   url - url to send the request to
//   method - the HTTP method to use
//   [content] - the serialized post information to send with the request
//   [parameters] - additional parameters (dictionary)
//      + async - default 'false', specifies whther to send the
//        request asynchronously
//      + send - default 'true', specifies whether or not to send
//        the first request (or wait)
//      + OnOpen - shortcut for adding an event listener
//      + OnSend - shortcut for adding an event listener
//      + OnReceive - shortcut for adding an event listener
//      + OnLoad - shortcut for adding an event listener
//      + OnSuccess - shortcut for adding an event listener
//      + OnFailure - shortcut for adding an event listener
//      + OnFailureClient - shortcut for adding an event listener
//      + OnFailureServer - shortcut for adding an event listener
function AJAXRequestor(url, method, content, parameters) {
	// @private
	this.requests = [this.createRequestObject(url, method, content)];

	// @private
	this.eventHandlers = new EventRegistration(AJAXEvent);	

	if (!parameters) parameters = {};
	// @private
	this.async = parameters.async != false;
	
	if (parameters.OnOpen)
		this.addEventListener(AJAXEvent.Open, parameters.OnOpen);
	if (parameters.OnSend)
		this.addEventListener(AJAXEvent.Sending, parameters.OnSend);
	if (parameters.OnReceive)
		this.addEventListener(AJAXEvent.Receiving, parameters.OnReceive);
	if (parameters.OnLoad)
		this.addEventListener(AJAXEvent.Loaded, parameters.OnLoad);
	if (parameters.OnSuccess)
		this.addEventListener(AJAXEvent.Success, parameters.OnSuccess);
	if (parameters.OnFailure)
		this.addEventListener(AJAXEvent.Failure, parameters.OnFailure);
	if (parameters.OnFailureClient)
		this.addEventListener(AJAXEvent.FailureClient, 
									parameters.OnFailureClient);
	if (parameters.OnFailureServer)
		this.addEventListener(AJAXEvent.FailureServer,
									parameters.OnFailureServer);

	if (parameters.send !== false) this.sendRequests();
	
	return this;
}

// Adds an additional request to the <AJAXRequestor> instance queue
//   url - url to send the request to
//   method - the HTTP method to use
//   [content] - the serialized post information to send with the request
AJAXRequestor.prototype.addRequest = function(url, method, content) {
	return this.requests.push(
								this.createRequestObject(url, method,content));
}

// Adds an additional request to the <AJAXRequestor> instance queue
// and sends the request
//   url - url to send the request to
//   method - the HTTP method to use
//   [content] - the serialized post information to send with the request
AJAXRequestor.prototype.addAndSendRequest = function(url, method, content) {
	this.sendRequest(this.addRequest(url, method, content));
}

// Trys to create a request object
//   url - url to send the request to
//   method - the HTTP method to use
//   [content] - the serialized post information to send with the request
AJAXRequestor.prototype.createRequestObject = function(url, method, content) {
	if (arguments.length < 2)
		throw new TypeError('url and method are required parameters!');
	
	var request = {opened:false, sent:false, url:url};
	request.content = content ? content:'';

	if (method.toLowerCase() == 'post')
		request.method = HTTPMethod.POST;
	else
		request.method = HTTPMethod.GET;


	if (!AJAXRequestor.AJAXType) {
		for (var i in AJAXAttemptOrdering) {
			try {
				AJAXRequestor.AJAXType = AJAXAttemptOrdering[i];
				request.request = XHR[AJAXRequestor.AJAXType]();
				break;
			} catch (e) {
				// if no support let error bubble up
				if (AJAXRequestor.AJAXType == AJAXSupport.None)
					throw e;
			}
		}
	} else
		request.request = XHR[AJAXRequestor.AJAXType]();
	
	var staticthis = this;
	request.request.onreadystatechange = function() {
		AJAXRequestStateChange(staticthis, request.request);
	};

	return request;
}

// opens the <XHR> instance if needed
//   requestID - id of the request to try to open
AJAXRequestor.prototype.openRequest = function(requestID) {
	if (requestID >= this.requests.length) return;
	
	var request = this.requests[requestID];
	if (!request.opened) {
		try {
			request.opened = true;
			var method = request.method == HTTPMethod.POST ? 'POST':'GET';
			request.request.open(method, request.url, this.async);
		} catch(e) {
			request.opened = false;
			throw e;
		}
	}
}

// Sends all buffered requests
AJAXRequestor.prototype.sendRequests = function() {
	for (var i in this.requests)
		this.sendRequest(i);
}

// Sends buffered request
//   requestID - id of the request to try to send
AJAXRequestor.prototype.sendRequest = function(requestID) {
	if (!(requestID in this.requests)) return;
	
	var request = this.requests[requestID];
	if (!request.opened) this.openRequest(requestID);
	if (!request.sent) {
		if (this.method == HTTPMethod.POST)
			request.request.setRequestHeader('Content-type',
												'application/x-www-form-urlencoded');
		
		try {
			request.sent = true;
			request.request.send(request.content);
		} catch(e) {
			request.sent = false;
			throw e;
		}
	}
}

// Clears all finished requests
AJAXRequestor.prototype.clearFinishedRequests = function() {
	var requests = this.requests;
	this.requests = [];

	for (var i in requests) {
		var request = requests[i];
		if (request.request.readyState != AJAXState.Loaded)
			this.requests.push(request)
	}
}

// Adds Event Listener(s)
//   eventType - must be AJAXEvent(s)
//   handler - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
// (ie <AJAXRequestor>.addEventListener(AJAXEvent.Success | 
//                                      AJAXEvent.FailureClient,
//                                      handleSuccessORClientError);)
AJAXRequestor.prototype.addEventListener = function(eventTypes, handler) {
	this.eventHandlers.addEventListener(eventTypes, handler);

	return this;
}

// Removes Event Listener(s)
//   eventType - must be AJAXEvent(s)
//   handler - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
// (ie <AJAXRequestor>.removeEventListener(AJAXEvent.Success | 
//                                      AJAXEvent.FailureClient,
//                                      handleSuccessORClientError);)
AJAXRequestor.prototype.removeEventListener = function(eventTypes, handler) {
	this.eventHandlers.removeEventListener(eventTypes, handler);

	return this;
}

// Dispatches a certain event and notifies all listeners
//   eventType - must be AJAXEvent(s)
//   request - listeners are called with the specified request
AJAXRequestor.prototype.dispatchEvent = function(eventType, request) {
	this.eventHandlers.dispatchEvent(eventType, request);

	return this;
}

// ----------
// CallFilter
// ----------

// @private
// <CallFilter> cache
var CallFilters = {};

// @private
// Number of <CallFilter> instances created
var CallFiltersLength = 0;

// CallFilter Constructor
//   [name] - call filter name
//   func - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
//   delay - delay until function is called
function CallFilter(name, func, delay) {
	if (!name) {
		name = 'filter' + CallFiltersLength++;
	}
	
	var cached = CallFilters[name];
	if (!cached) {
		cached = CallFilters[name] = this;
	}

	if (!cached.delay && (!delay || isNaN(delay))) {
		cached.delay = 300;
	} else if (delay) {
		cached.delay = Number(delay);
	}

	cached.name = name;

	if (func) cached.func = Handler(func);

	return cached;
}

// Exposes CallFilter to the given object
//   localTo - object to attach to
CallFilter.Expose = function(localTo) {
	localTo.CallFilter = CallFilter;
}

// Changes the filter delay (doesn't effect calls that were already tried)
//   delay - new delay to filter with
CallFilter.prototype.setDelay = function(delay) {
	if (!isNaN(delay)) {
		this.delay = Number(delay);
	}

	return this;
}

// sets the function to call
//   func - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
CallFilter.prototype.setFn = function(func) {
	if (func) this.func = Handler(func);
	return this;
}

// tries to call the set function
//   [arglist] - variable number of arguments to pass to the function
CallFilter.prototype.tryCall = function() {
	if (this.timer !== null && this.timer !== undefined)
		clearTimeout(this.timer);

	if (!this.func) {
		throw new ReferenceError('Call function is not set! Use: <obj>.setFn');
	}

	var staticthis = this;
	var staticarg = arguments;
	this.timer = setTimeout(function() {
		staticthis.func.apply(staticarg);
	}, this.delay);

	return this;
}

// ------------------
// AbsoluteAsRelative
// ------------------

// @private
// <AbsoluteAsRelative> cache
var AbsoluteAsRelativeCache = [];

// @private
// <AbsoluteAsRelative> cache
var AbsoluteAsRelativeFastCache = {};

// Allows easy positioning of absolute elements
//   offsetObject - object to determine the offsets off of
//   [disableCaching] - defaults 'false' whether the caching
//                      should be disabled
function AbsoluteAsRelative(offsetObject, disableCaching) {
	if (offsetObject.constructor == String || typeof offsetObject == 'string')
		offsetObject = document.getElementById(offsetObject);

	if (!offsetObject) 
		throw TypeError('Offset object must be either an HTML element or a ' +
								'string specifying ones id');

	var id = offsetObject.id;
	if (!disableCaching) {
		var result = null;

		if (id && (result = AbsoluteAsRelativeFastCache[id]))
			return result;

		for (var i in AbsoluteAsRelativeCache) {
			result = AbsoluteAsRelativeCache[i];
			if (offsetObject == result.offsetParent) return result;
		}
	}

	// @private
	this.offsetParent = offsetObject

	// @private
	this.enableCache = disableCaching != true;

	// @private
	this.$ = {};

	if (!disableCaching) {
		if (id) AbsoluteAsRelativeFastCache[id] = this;
		else AbsoluteAsRelativeCache.push(this);
	}

	return this;
}

// Exposes AbsoluteAsRelative to the given object
//   localTo - object to attach to
AbsoluteAsRelative.Expose = function(localTo) {
	localTo.AbsoluteAsRelative = AbsoluteAsRelative;
}

// Determines the complete offsetTop from the root element
AbsoluteAsRelative.prototype.offsetTop = function() {
	if (this.enableCache && this.$.offsetTop) return this.$.offsetTop;

	var par = this.offsetParent;
	var offset = 0;
	while (par != null) {
		offset += par.offsetTop;
		par = par.offsetParent;
	}

	return this.$.offsetTop = offset;
}

// Determines the complete offsetLeft from the root element
AbsoluteAsRelative.prototype.offsetLeft = function() {
	if (this.enableCache && this.$.offsetLeft) return this.$.offsetLeft;

	var par = this.offsetParent;
	var offset = 0;
	while (par != null) {
		offset += par.offsetLeft;
		par = par.offsetParent;
	}

	return this.$.offsetLeft = offset;
}

// Determines the complete offsetHeight from the root element
AbsoluteAsRelative.prototype.offsetHeight = function() {
	return this.offsetTop() + this.offsetParent.offsetHeight;
}

// Determines the complete offsetWidth from the root element
AbsoluteAsRelative.prototype.offsetWidth = function() {
	return this.offsetLeft() + this.offsetParent.offsetWidth;
}

// Clears the instance cache
AbsoluteAsRelative.prototype.clear = function() {
	this.$ = {};

	return this;
}


// -----
// Modal
// -----

// @private
// list of modal containers
var ModalList = [];

// @constant
var ModalEvent = {
	Initialize:1,
	Show:2,
	Hide:4
}

// Modal Constuctor
//   destination - where to append the modal object to
//   parameters - additional parameters (dictionary)
//      + closeButton - optional html fragment to render as the
//        close button (should call "close" as this is not done
//        for you.
//      + paddingTop - distance from the top of the screen in 
//        pixels which the content should be displayed at
//      + OnInitialize - shortcut for adding an event listener
//      + OnHide - shortcut for adding an event listener
//      + OnShow - shortcut for adding an event listener
function Modal(destination, parameters) {
	if (!destination || !destination.appendChild)
		throw new TypeError('You should be able to append the modal object to the destination');

	// @private
	this.eventHandlers = new EventRegistration(ModalEvent);

	// @private
	this.visible = false;

	this.id = ModalList.length;
	
	// @private
	this.container = $generate('<div id="modal' + this.id + '" class="modal-container" style="display:none;">' +
										'<div class="modal-mask"></div><div class="modal-page">' +
										'<div class="modal-dialog-outer"><div class="modal-dialog-inner">' +
										'</div></div></div></div>');

	destination.appendChild(this.container);

	this.mask = this.container.firstChild;

	// @private
	this.page = this.container.lastChild;

	this.dialog = this.page.firstChild;

	// @private
	this.inner = this.dialog.firstChild;

	var staticthis = this;

	// @private
	this.closeButton = $generate('<a class="modal-close">close</a>', {
			onclick:function() { staticthis.hide(); }
	});

	// @private
	this.paddingTop = 250;

	if (parameters) {
		// @private
		this.closeButton = parameters.closeButton || this.closeButton

		// @private
		this.paddingTop = parameters.paddingTop || this.paddingTop;

		for (var event in ModalEvent) {
			var param = parameters['On' + event];
			if (param)
				this.addEventListener(ModalEvent[event], param);
		}
	}

	// @private
	this.content = $generate('<div class="modal-contents"></div>');

	this.inner.appendChild(this.closeButton);
	this.inner.appendChild(this.content);
	
	ModalList.push(this);
	return this;
}

// Exposes Modal to the given object
//   localTo - object to attach to
Modal.Expose = function(localTo) {
	localTo.Modal = Modal;
	localTo.ModalEvent = ModalEvent;
}

// Retrieves the specified modal dialog
//   id - id to retrieve
Modal.Get = function(id) {
	return ModalList[id];
}

// Closes the specified modal dialog
//   id - id to close
Modal.Hide = function(id) {
	return ModalList[id].hide();
}

// Showes the specified modal dialog
//   id - id to show
Modal.Show = function(id) {
	return ModalList[id].show();
}

// Closes a modal object
Modal.prototype.hide = function() {
	if (this.visible) {
		this.container.style.display = 'none';
		this.visible = false;
		this.dispatchEvent(ModalEvent.Hide, this);
	}

	return this;
}

// Displays a modal object
Modal.prototype.show = function() {
	if (!this.visible) {
		this.container.style.display = '';
		this.visible = true;
		this.dispatchEvent(ModalEvent.Show, this);
	}

	return this;
}

// Sets the contents of a modal object
//   content - content to set the modal to display
Modal.prototype.setContent = function(content) {
	this.content.innerHTML = '';
	this.content.appendChild(content);

	return this;
}

// Resizes a modal container
Modal.prototype.resize = function() {
	var height = (window.innerHeight || document.body.clientHeight ||
						document.documentElement.clientHeight || 0);

	var scrollMax = (window.scrollMaxY || document.body.scrollHeight ||
							document.body.offsetHeight || 0);

	this.container.style.height = (height + scrollMax) + 'px';
	return this;
}

// Repositions a modal container so it is a certain height
// off the top of the screen.
Modal.prototype.reposition = function() {
	var scroll = (window.scrollY || document.body.scrollTop ||
						document.documentElement.scrollTop || 0);

	this.dialog.style.marginTop = (scroll + this.paddingTop) + 'px';
	return this;
}

// Adds Event Listener(s)
//   eventType - must be ModalEvent(s)
//   handler - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
Modal.prototype.addEventListener = function(eventTypes, handler) {
	this.eventHandlers.addEventListener(eventTypes, handler);

	return this;
}

// Removes Event Listener(s)
//   eventType - must be ModalEvent(s)
//   handler - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
Modal.prototype.removeEventListener = function(eventTypes, handler) {
	this.eventHandlers.removeEventListener(eventTypes, handler);

	return this;
}

// Dispatches a certain event and notifies all listeners
//   eventType - must be ModalEvent(s)
//   [arglist] - variable number of arguments
Modal.prototype.dispatchEvent = function() {
	this.eventHandlers.dispatchEvent.apply(this.eventHandlers, arguments);

	return this;
}

// --------
// DropDown
// --------

// @constant
var DropDownEvent = {
	Initialize:1,
	Position:2,
	KeyPress:4,
	NextItem:8,
	PreviousItem:16,
	Show:32,
	Hide:64,
	Select:128,
	Cancel:256
};

// DropDown Constuctor
//   container - id of container to hold modal items
//   parameters - additional parameters (dictionary)
//      + nextKeys - the keycodes which will select the next item
//      + previousKeys - the keycodes which will select the previous item
//      + selectKeys - the keycodes which will select the current item
//      + cancelKeys - the keycodes which will cancel selection
//      + selectedColor - the background-color of the selected item
//      + OnInitialize - shortcut for adding an event listener
//      + OnPosition - shortcut for adding an event listener
//      + OnKeyPress - shortcut for adding an event listener
//      + OnNextItem - shortcut for adding an event listener
//      + OnPreviousItem - shortcut for adding an event listener
//      + OnShow - shortcut for adding an event listener
//      + OnHide - shortcut for adding an event listener
//      + OnSelect - shortcut for adding an event listener
//      + OnCancel - shortcut for adding an event listener
function DropDown(container, parameters) {
	// @private
	this.eventHandlers = new EventRegistration(DropDownEvent);

	// @private
	this.container = container;

	// @private
	this.selected = null;

	if (!parameters) parameters = {};
	// @private
	this.nextKeys = parameters.nextKeys || DropDown.NextKeys;

	// @private
	this.previousKeys = parameters.previousKeys || DropDown.PreviousKeys;

	// @private
	this.selectKeys = parameters.selectKeys || DropDown.SelectKeys;

	// @private
	this.cancelKeys = parameters.cancelKeys || DropDown.CancelKeys;

	// @private
	this.selectedColor = parameters.selectedColor || DropDown.SelectedColor;

	this.onKeyDown = Handler([this, DropDown.prototype.onKeyDown]);
	this.onKeyUp = Handler([this, DropDown.prototype.onKeyUp]);

	for (var event in DropDownEvent) {
		var param = parameters['On' + event];
		if (param)
			this.addEventListener(DropDownEvent[event], param);
	}

	this.dispatchEvent(DropDownEvent.Initialize, this.container);
	return this;
}

DropDown.NextKeys = [Key.Down, Key.Right];
DropDown.PreviousKeys = [Key.Up, Key.Left];
DropDown.SelectKeys = [Key.Tab, Key.Enter, Key.Space];
DropDown.CancelKeys = [Key.Escape];

DropDown.SelectedColor = '#BBDDFF';

// Exposes DropDown to the given object
//   localTo - object to attach to
DropDown.Expose = function(localTo) {
	localTo.DropDown = DropDown;
	localTo.DropDownEvent = DropDownEvent;
}

// Sets the drop down contents
//   contents - should be a list with list items inside
DropDown.prototype.setContents = function(contents) {
	this.selected = null;

	this.container.innerHTML = '';
	this.container.appendChild(contents);

	return this;
}

// selects the next item
DropDown.prototype.selectNext = function() {
	var next = true;
	if (this.selected) {
		this.selected.style.backgroundColor = '';

		if (this.selected.nextSibling)
			this.selected = this.selected.nextSibling;
		else
			next = false;
	} else {
		this.selected = this.container.firstChild.firstChild;
	}

	if (next) {
		this.selected.style.backgroundColor = this.selectedColor;
		this.dispatchEvent(DropDownEvent.NextItem, this.selected);
	}
}

// select previous item
DropDown.prototype.selectPrevious = function() {
	if (this.selected) {
		this.selected.style.backgroundColor = '';
		this.selected = this.selected.previousSibling;

		if (this.selected)
			this.selected.style.backgroundColor = this.selectedColor;

		this.dispatchEvent(DropDownEvent.PreviousItem, this.selected);
	}
}

// function to bind to the keyDown event of the input
//   event - KeyDown event
DropDown.prototype.onKeyDown = function(event) {
	var key = event.which;

	if (this.visible) {
		// if there is even a reason to try going backwards
		if (this.selected) {
			for (var i in this.previousKeys) {
				if (key == this.previousKeys[i]) {
					this.selectPrevious();
					return false;
				}
			}
		}

		for (var i in this.nextKeys) {
			if (key == this.nextKeys[i]) {
				this.selectNext();
				return false;
			}
		}

		for (var i in this.selectKeys) {
			if (key == this.selectKeys[i]) {
				this.dispatchEvent(DropDownEvent.Select, this.selected);
				return false;
			}
		}

		for (var i in this.cancelKeys) {	
			if (key == this.cancelKeys[i]) {
				this.dispatchEvent(DropDownEvent.Cancel, this.selected);
				this.hide()
				return false;
			}
		}
	}

	return true;
}

// function to bind to the keyUp event of the input
//   event - KeyUp event
DropDown.prototype.onKeyUp = function(event) {
	var key = event.which;

	if (key < 32 && key != Key.Backspace) return true;

	for (var i in this.previousKeys)
		if (key == this.previousKeys[i]) return true;

	for (var i in this.nextKeys)
		if (key == this.nextKeys[i]) return true;

	for (var i in this.selectKeys)
		if (key == this.selectKeys[i]) return true;

	for (var i in this.cancelKeys)
		if (key == this.cancelKeys[i]) return true;

	this.dispatchEvent(DropDownEvent.KeyPress, event);

	return true;
}

// shows the DropDown object
DropDown.prototype.show = function() {
	if (!this.visible) {
		this.dispatchEvent(DropDownEvent.Position, this.container);
		this.visible = true;
		this.container.style.display = '';
		this.dispatchEvent(DropDownEvent.Show, this.container);
	}

	return this;
}

// hides the DropDown object
DropDown.prototype.hide = function() {
	if (this.visible) {
		this.visible = false;
		this.container.style.display = 'none';
		this.dispatchEvent(DropDownEvent.Hide, this.container);
	}

	return this;
}

// Adds Event Listener(s)
//   eventType - must be DropDownEvent(s)
//   handler - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
DropDown.prototype.addEventListener = function(eventTypes, handler) {
	this.eventHandlers.addEventListener(eventTypes, handler);

	return this;
}

// Removes Event Listener(s)
//   eventType - must be DropDownEvent(s)
//   handler - may be any one of the following:
//     + An array where the first element is the scope and the
//       second is the callback
//     + A dictionary where 'callback' is the callback and 'scope'
//       is the scope
//     + A function where the scope will be defaulted to window
DropDown.prototype.removeEventListener = function(eventTypes, handler) {
	this.eventHandlers.removeEventListener(eventTypes, handler);

	return this;
}

// Dispatches a certain event and notifies all listeners
//   eventType - must be DropDownEvent(s)
//   [arglist] - variable number of arguments
DropDown.prototype.dispatchEvent = function() {
	this.eventHandlers.dispatchEvent.apply(this.eventHandlers, arguments);

	return this;
}

// exposing function
function exposeJSUtils(localTo) {
	localTo.JSUtils = {
		errors : {
			UnsupportedBrowserError : UnsupportedBrowserError
		},
		ui : {},
		Key : Key,
	};

	DropDown.Expose(localTo.JSUtils.ui);
	Modal.Expose(localTo.JSUtils.ui);
	AbsoluteAsRelative.Expose(localTo.JSUtils);
	CallFilter.Expose(localTo.JSUtils);
	exposeUtils(localTo.JSUtils);

	localTo.AJAXRequestor = AJAXRequestor;
	localTo.AJAXEvent = AJAXEvent;
	localTo.exposeJSUtils = exposeJSUtils;
	localTo.gen = $generate;
	localTo.add = $addEventListener;
}

exposeJSUtils(localTo);
}

$JSUtils(window);
