/**
 * Abilitation JavaScript Library
 * 
 * @author Neil Martin
 * @version 1.0
 */

// JSLint
// var $, jQuery, Image, window, document, navigator, escape, alert, console, XMLSerializer;


// Define standard JavaScript library namespaces.
// Note, additional namespaces may be attached the the root Abl object
var Abl = {};
Abl.Browser = {};
Abl.Cms = {};
Abl.Cms.Editors = {};
Abl.Cookie = {};
Abl.DateTime = {};
Abl.DEBUG = {};
Abl.IO = {};
Abl.Json = {};
Abl.Math = {};
Abl.String = {};
Abl.UI = {};
Abl.WebControls = {};
Abl.Window = {};
Abl.Xml = {};

Abl.version = "1.0.0";

Abl.DEBUG.debug = 1;				// 1==Show in FireFox; 2==Show in IE
Abl.DEBUG.useAlert = false;	// Always use alert()
Abl.DEBUG.trace = function(msg) {
	if (Abl.DEBUG.debug) {
		if ((typeof console === "object") && (typeof console.debug === "function")&&(!Abl.DEBUG.useAlert)) {
			console.debug(msg);
		}
		else if ((Abl.DEBUG.debug > 1)||(Abl.DEBUG.useAlert)) {
			alert(msg);
		}
	}
};
Abl.DEBUG.keyEvent = function(evt) {
	var	key = evt.which
			msg = "key:" + key + " ";
	if (evt.shiftKey) { msg += "+shift"; }
	if (evt.altKey)   { msg += "+alt"; }
	if (evt.ctrlKey)  { msg += "+ctrl"; }
	Abl.DEBUG.trace(msg);
};


/**
 * Browser Utilities
 */
Abl.Browser.isIE6 = function() {
	// Thanks to jQuery BlockUI http://malsup.com/jquery/block 
	var	mode = document.documentMode || 0,
			ie6 = (($.browser.msie)  && (/MSIE 6\.0/.test(navigator.userAgent)) && (!mode));
	return ie6;
};



/**
 * XML Utilities
 */
Abl.Xml.encode = function(s) {
	// Note that replace("&", "&amp;") has to be the first replace so we don't replace other already escaped &.
	return ((s)&&(typeof s === 'string')) ? s.replace(/\&/g,'&'+'amp;').replace(/</g,'&'+'lt;')
		.replace(/>/g,'&'+'gt;').replace(/\'/g,'&'+'apos;').replace(/\"/g,'&'+'quot;') : "";
};

Abl.Xml.decode = function(s) {
	return ((s)&&(typeof s === 'string')) ? s.replace(/\&lt;/g,'<').replace(/\&gt;/g,'>')
		.replace(/\&apos;/g,'\'').replace(/\&quot;/g,'\"').replace(/\&amp;/g,'&') : "";
};

Abl.Xml.serialize = function (xData) {
	return (xData.xml) ? xData.xml : (new XMLSerializer()).serializeToString(xData);
};



/**
 * Abl.Uri
 * This class supersedes Abl.Window.Uri (the old class will eventually be replaced).
 *
 * The Abl.Uri() class constructor accepts any of the following types and attempts
 * to extract the url/href data property from the object.  This raw string data is
 * then parsed with a regular expression to extract the relevant uri properties.
 *
 * Note, all paths must be specified as absolute, i.e. '/Default.aspx' - relative
 * paths are not supported and will cause parsing errors!
 */
Abl.Uri = function(obj) {
	var	_self = this,
			url, localUrl, host, protocol, domain, port, path, folder, file, fileLessExtension, fragment, query,
			regexUrl = /^(((https?|ftp):\/\/)([^\/:\?\#]*)?(:(\d*))?)?([^\?#]*)?((\?|#)(.*))?$/i,
			regexPath = /^((\/)?.*\/)?(.*)$/i;

	// Url groups (http://www.abldev.com:8080/Admin/Cms/Default.aspx?name=neil):	=> url
	// group[1] = 'http://www.abldev.com:8080'												=> host
	// group[3] = 'http'																				=> protocol
	// group[4] = 'www.abldev.com'																=> domain
	// group[6] = '8080'																				=> port
	// group[7] = '/Admin/Cms/Default.aspx'													=> path
	// group[9] = '?'																					=> fragment
	// group[10] = 'name=neil'																		=> query
	
	// Path groups (/Admin/Cms/Default.aspx):													=> path
	// group[1] = '/Admin/Cms/'																	=> folder
	// group[3] = 'Default.aspx'																	=> file

	/**********************************************************************************************
	* Uri Fragments/Property Accessors                                                            *
	**********************************************************************************************/
	// Example:
	// var uri = new Abl.Uri("http://www.abldev.com:63886/Admin/Cms/Default.aspx?pageid=27&userid=14");
	
	this.getUrl			= function() { return url; };
	this.getLocalUrl	= function() { return localUrl; };
	this.getHost		= function() { return host; };
	this.getProtocol	= function() { return protocol; };
	this.getDomain		= function() { return domain; };
	this.getPort		= function() { return port; };
	this.getPath		= function() { return path; };
	this.getFolder		= function() { return folder; };
	this.getFile		= function() { return file; };
	this.getFragment	= function() { return fragment; };
	this.getQuery		= function() { return query; };
	this.getFileLessExtension = function() { return fileLessExtension; };
	
	this.getProperties = function() {
		return ({
			url: url,
			localUrl: localUrl,
			host: host,
			protocol: protocol,
			domain: domain,
			port: port,
			path: path,
			folder: folder,
			file: file,
			fileLessExtension: fileLessExtension,
			fragment: fragment,
			query: query
		});
	};
	
	this.getParams = function() {
	  // define an object to contain the parsed query data
	  var	result = {},
			queryString = query.replace('+', ' '),	// replace plus signs in the query string with spaces
			queryComponents, i, keyValuePair, key, value;

		// split the query string around ampersands and semicolons
		queryComponents = queryString.split(/[&;]/g);

		// loop over the query string components
		for (i = 0; i < queryComponents.length; i++){
			// extract this component's key-value pair
			keyValuePair = queryComponents[i].split('=');
			key = decodeURIComponent(keyValuePair[0]);
			value = decodeURIComponent(keyValuePair[1]);
			result[key] = value;
		}

		// return the parsed query data
		return result;
	};
	
	this.getParam = function(name) {
		var params = _self.getParams();
		return (params) ? params[name] : null;
	};
	
	
	this.printProperties = function() {
		var s = "", p, props = this.getProperties();
		for (p in props) {
			if (props.hasOwnProperty(p)) {
				if (s.length) { s += "\r\n"; }
				s += p + ": " + ((props[p]) ? props[p] : "--");
			}
		}
		alert(s);
	};

	

	this.compare = function(target, element, partialMatch) {
		var	targetUri = (target instanceof Abl.Uri) ? target : new Abl.Uri(target);
		element = (element) ? element.toLowerCase() : "url";
		
		// Provide a more sophisticated comparison option for folders.  Partial
		// matching is useful when comparing links with the current page - see
		// 'folderMatch' class attribute in Abl.UI.Menu
		function compareFolder(folderA, folderB, partialMatch) {
			var	compareLength;
			
			if ((typeof folderA !== "string") || (typeof folderB !== "string")) {
				return false;
			}
			
			folderA = folderA.toLowerCase();
			folderB = folderB.toLowerCase();
			
			if (partialMatch) {
				compareLength = Math.min(folderA.length, folderB.length);
				return (folderA.substr(0, compareLength) === folderB.substr(0, compareLength));
			} else {
				return (folderA === folderB);
			}
		}
		
		switch (element) {
			case "url":			return (url.toLowerCase() === targetUri.getUrl().toLowerCase());
			case "localurl":	return (localUrl.toLowerCase() === targetUri.getLocalUrl().toLowerCase());
			case "host":		return (host.toLowerCase() === targetUri.getHost().toLowerCase());
			case "protocol":	return (protocol.toLowerCase() === targetUri.getProtocol().toLowerCase());
			case "domain":		return (domain.toLowerCase() === targetUri.getDomain().toLowerCase());
			case "port":		return (port.toLowerCase() === targetUri.getPort().toLowerCase());
			case "path":		return (path.toLowerCase() === targetUri.getPath().toLowerCase());
			case "folder":		return compareFolder(folder, targetUri.getFolder(), partialMatch);
			case "file":		return (file.toLowerCase() === targetUri.getFile().toLowerCase());
			case "fileLessExtension":	return (fileLessExtension.toLowerCase() === targetUri.getFileLessExtension().toLowerCase());
			case "fragment":	return (fragment.toLowerCase() === targetUri.getFragment().toLowerCase());
			case "query":		return (query.toLowerCase() === targetUri.getQuery().toLowerCase());
			default:
				throw "Illegal comparison element '" + element + "'!";
		}
	};
	

	/**********************************************************************************************
	* Address/Uri Management                                                                      *
	**********************************************************************************************/
	function getLocalUrl(url) {
		var	domain = window.location.protocol + '//' + window.location.hostname;
		
		if (!url) { return ""; }
		if (window.location.port) { domain += ":" + window.location.port; }
		
		if (url.toLowerCase().indexOf(domain.toLowerCase()) === 0) {
			return url.substr(domain.length);
		} else {
			return url;
		}
	}

	function clear() {
		localUrl		= "";
		host			= "";
		protocol		= "";
		domain		= "";
		port			= "";
		path			= "";
		folder		= "";
		file			= "";
		fileLessExtension = "";
		fragment		= "";
		query			= "";
	}

	function setUrl(s) {
		url = s || "";
		localUrl = getLocalUrl(url);
		
		var urlMatch = regexUrl.exec(url), pathMatch;
		if (urlMatch) {
			host			= urlMatch[1]  || "";	// 'http://www.abldev.com:8080'
			protocol		= urlMatch[3]  || "";	// 'http'
			domain		= urlMatch[4]  || "";	// 'www.abldev.com'
			port			= urlMatch[6]  || "";	// '8080'
			path			= urlMatch[7]  || "";	// '/Admin/Cms/Default.aspx'
			fragment		= urlMatch[9]  || "";	// '?'
			query			= urlMatch[10] || "";	// 'name=neil'
			
			pathMatch = regexPath.exec(path);
			folder		= pathMatch[1] || "";	// '/Admin/Cms/'
			file			= pathMatch[3] || "";	// 'Default.aspx'
			fileLessExtension = file.match(/^[^\.]*/)[0];
		} else {
			clear();
		}
	}

	this.setUri = function(obj) {
		if (typeof obj === 'string') {
			setUrl(obj);
		} else 
		if ((typeof obj === 'object') && (obj.href)) {
			setUrl(obj.href);
		} else
		if ((obj) && (obj instanceof Abl.Uri)) {
			setUrl(obj.getUrl());
		} else
		if ((obj) && (obj instanceof jQuery) && (obj[0]) && (obj[0].href)) {
			setUrl(obj[0].href);
		} else {
			setUrl(window.location.href);
		}
	};
	
	
	// Initialise the object
	this.setUri(obj);
};



/******************************************************************
** Abl - Core Functions
******************************************************************/
/**
 * chain()
 *
 * Chains an array of functions together and calls via the context
 * provided
 */
Abl.chain = function(oldFunc, newFunc) {
	var funcList, i;
	if (typeof oldFunc !== 'function') {
		return newFunc;
	} else {
		funcList = [oldFunc, newFunc];
		return function() {  
			for (i = 0; i < funcList.length; i++) {  
				funcList[i]();  
			}
		};
	}
};


/******************************************************************
** Abl.DateTime - Core Functions
******************************************************************/
/**
 * Converts a number representing a time period to milliseconds
 * 
 * @param {Number} n The time to be converted
 * @param {String} t Where 's'==seconds, 'm'==minutes, 'h'==hours, 'd'==days, 'w'==weeks
 */
Abl.DateTime.toMilliseconds = function(n, t) {
	switch (t) {
		case "s": return (n * 1000);
		case "m": return (n * 60 * 1000);
		case "h": return (n * 60 * 60 * 1000);
		case "d": return (n * 24 * 60 * 60 * 1000);
		case "w": return (n * 7 * 24 * 60 * 60 * 1000);
		default: return n;
	}
};


/******************************************************************
** Abl.Cookie - Core Functions
******************************************************************/
/**
 * Stores a named cookie
 * 
 * @param {String} name			The name of the cookie
 * @param {String} value		The cookie's value
 * @param {Number} ms			The cookie's expiration period
 * @param {String} timePeriod	See Abl.DateTime.toMilliseconds()
 */
Abl.Cookie.set = function(name, value, t, timePeriod) {
	var date, expires;
	if (t) {
		date = new Date();
		date.setTime(date.getTime() + Abl.DateTime.toMilliseconds(t, timePeriod));
		expires = "; expires=" + date.toGMTString();
	}
	document.cookie = name + "=" + escape(value) + expires + "; path=/";
};

/**
 * Rerieves a named cookie
 * 
 * @param {String} name			The name of the cookie to be retrieved
 */
Abl.Cookie.get = function(name) {
	var	nameEQ = name + "=",
			ca = document.cookie.split(';'),
			i, c;

	for (i = 0; i < ca.length; i++) {
		c = ca[i];
		while (c.charAt(0) === ' ') {
			c = c.substring(1, c.length);
		}
		if (c.indexOf(nameEQ) === 0) {
			return c.substring(nameEQ.length, c.length);
		}
	}
	return null;
};

/**
 * Retrieves a named cookie as an integer value
 * 
 * @param {String} name			The name of the cookie to be retrieved
 */
Abl.Cookie.getInt = function(name) {
	var x = Abl.Cookie.get(name);
	return (x) ? parseInt(x, 10) : 0;
};

/**
 * Retrieves a named cookie as an float value
 * 
 * @param {String} name			The name of the cookie to be retrieved
 */
Abl.Cookie.getFloat = function(name) {
	var x = Abl.Cookie.get(name);
	return (x) ? parseFloat(x) : 0.0;
};


/**
 * Provides a cross-browser method for obtaining the DOM document of
 * a named frame element
 */
Abl.getIFrameDocument = function(id) {
	var doc = null;

	if (document.frames) {
		doc = document.frames[id].document;
	} else {
		doc = document.getElementById(id).contentDocument;
	}

	return doc;
};

Abl.createIFrame = function(options) {
	return $("<iframe></iframe>")
		.attr({
			"tabindex": options.tabIndex || "-1",
			"frameBorder": options.frameBorder || "0",
			"border": "0 none",
			"src": options.source || "about:blank",
			"width": options.width || 100,
			"height": options.height || 100
		})
		.css({
			"border": "0 none",				// Note the logic on determining a width/height for the iFrame
			"width": options.width || 100,
			"height": options.height || 100
		});
};


/******************************************************************
** Abl.Math - Core Functions
******************************************************************/
/**
 * Forces the supplied value to fall within the specified bounds.
 * 
 * @param {Number} val	The value to be constrained
 * @param {Number} min	The lowest allowable value
 * @param {Number} max	The highest allowable value
 */
Math.constrain = function(val, min, max){
	return Math.min(Math.max(val, min), max);
};

Math.getInt = function(o, radix) {
	var x = parseInt(o, radix || 10);
	return (isNaN(x) ? 0 : x);
};

/**
 * Forces the width/height parameters to fall within the specified range.
 *
 * @param {Integer} width			Original width
 * @param {Integer} height			Original height
 * @param {Integer} maxWidth		Maximum allowable width
 * @param {Integer} maxHeight		Maximum allowable height
 * @return {Array}					An array of the adjusted width/height
 */Math.forceFit = function(width, height, maxWidth, maxHeight) {
	if (width > maxWidth) {
		height = parseInt(height * (maxWidth / width), 10);
		width = maxWidth;
	}
	if (height > maxHeight) {
		width = parseInt(width * (maxHeight / height), 10);
		height = maxHeight;
	}
	return [width, height];
};



/******************************************************************
** Abl.String - Core Functions
******************************************************************/
/**
 * Strips leading and trailing white-space from the string.
 */
String.prototype.trim = function() {
	return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};

/**
 * Pads the left-hand side of the string to obtain the target
 * length.
 * 
 * @param {String} ch	The character to use as padding
 * @param {Object} len	The target length of the string
 */
String.prototype.padLeft = function(ch, len) {
	var s = this;
	while (s.length < len) {
		s = ch + s;
   }
	return s;
};

/**
 * Pads the right-hand side of the string to obtain the target
 * length.
 * 
 * @param {String} ch	The character to use as padding
 * @param {Number} len	The target length of the string
 */
String.prototype.padRight = function(ch, len) {
	var s = this;
	while (s.length < len) {
		s += ch;
   }
	return s;
};


/**
 * Truncates a string of text back to the nearest word-break falling
 * on or before the maximum allowable length.  Optionally
 * adds a suffix string to the truncated string.
 *
 * @param {Number}	maxLength	The maximum allowable string length
 * @param {Number}	range		The number of characters to work back
 *										from the specified break point in order
 *										to find a word-break
 * @param {String}	suffix	String to be appended to the truncated 
 *										result
 */
String.prototype.trimToIntro = function(maxLength, range, suffix) {
	var text = this, i;
	if (text.length > maxLength)
	{
		i = maxLength - range;
		if (i < 0) { i = 0; }
		while (((i < maxLength) && (text.substr(i, 1) !== " ")))
		{
			i++;
		}
		text = text.substr(0, i).trim() + suffix;
	}
	return text;
};

String.prototype.isSpace = function() {
	return ((this.length) && (/\s/.test(this)));
};
String.prototype.isUpper = function() {
	return ((this.length) && (this.substr(0,1) >= 'A') && (this.substr(0,1) <= "Z"));
};
String.prototype.isLower = function() {
	return ((this.length) && (this.substr(0,1) >= 'a') && (this.substr(0,1) <= "z"));
};

/**
 * Converts a string to 'proper case' notation and optionally
 * splits words with embedded uppercase characters.
 * "my name isNeil".toProperCase(true) === "My Name Is Neil"
 */
String.prototype.toProperCase = function(split) {
	var s = "", ch = "", inSpace = true, i = 0, len = (this.length - 1);
	for (i = 0; i <= len; i++) {
		ch = this.substr(i,1);
		s += (inSpace) ? ch.toUpperCase() : ch;
		inSpace =  ch.isSpace();
		if ((split) && (i < len)) {
			if ((ch.isLower()) && (this.substr(i+1,1).isUpper())) {
				s += " ";
			}
		}
	}
	return s;
};


/******************************************************************
** Abl.UI - Core Functions
******************************************************************/
/**
 * Returns the height of the document in pixels.
 * @return {Number} height of the document
 */
Abl.UI.getPageHeight = function() {
	return $(document).height();
};

/**
 * Returns the width of the document in pixels.
 * @return {Number} width of the document
 */
Abl.UI.getPageWidth = function() {
	return $(document).width();
};

Abl.UI.getPageMetrics = function() {
	return ({
		size: {width: $(document).width(), height: $(document).height()},
		scroll: {left: $(document).scrollLeft(), top: $(document).scrollTop()}
	});
};



/**
 * Returns the height of the browser viewport in pixels.
 * @return {Number} height of the viewport
 */
Abl.UI.getWindowHeight = function() {
	return $(window).height();
};

/**
 * Returns the width of the browser viewport in pixels.
 * @return {Number} width of the viewport
 */
Abl.UI.getWindowWidth = function() {
	return $(window).width();
};


Abl.UI.getWindowMetrics = function() {
	return ({
		size: {width: $(window).width(), height: $(window).height()},
		scroll: {left: $(window).scrollLeft(), top: $(window).scrollTop()}
	});
};


/******************************************************************
** Abl.Window - Core Functions
******************************************************************/
/**
 * Creates a new Uri() object representing a web address.
 * 
 * The constructor will optionally take a string or a html
 * anchor tag element to specify the address,  If no parameter
 * is specified the current page's address is used (via the
 * window.location.href property)
 * 
 * @constructor
 * @param {String, Object} [The web address reference]
 * @return {Object}
 */
Abl.Window.Uri = function(ref) {
	this.href = '';
	if (typeof ref === 'string') {
		this.href = ref;
	} else 
	if ((typeof ref === 'object')&&(ref.href)) {
		this.href = ref.href;
	} else
	if ((ref)&&(ref instanceof Abl.Window.Uri)) {
		this.href = ref.href;		
	} else
	if ((ref)&&(ref instanceof jQuery)&&(ref[0])&&(ref[0].href)) {
		this.href = ref[0].href;		
	} else {
		this.href = window.location.href;
	}
};

/**
 * Returns the query string element of the Uri object.
 * 
 * @return {string} Returns the Uri's query string
 */
Abl.Window.Uri.prototype.getQueryString = function() {
	var i = this.href.indexOf("?");
	if (i < 0) {
		return '';
	} else {
		return this.href.substr(i+1);
	}
};

/**
 * Returns the local address of the Uri object. For example:
 * 
 *		http://www.myweb.com/ContactUs/index.html?type=enquiry
 *		/ContactUs/index.html?type=enquiry
 * 
 * @return {String} The local address
 */
Abl.Window.Uri.prototype.getLocalUrl = function() {
	return Abl.Window.Uri.stripLocation(this.href);
};

/**
 * Returns the base/core url of the Uri object. For example:
 * 
 *		http://www.myweb.com/ContactUs/index.html?type=enquiry
 *		/ContactUs/index.html
 * 
 * @return {String} The base address
 */
Abl.Window.Uri.prototype.getBaseUrl = function() {
	var s = Abl.Window.Uri.stripLocation(this.href);
	return Abl.Window.Uri.stripQueryString(s);
};

/**
 * Returns the file name of a url
 * Example: getFileName('/Content/Default.aspx') === 'Default.aspx'
 */
Abl.Window.Uri.getFileName = function(ref) {
	var	uri = new Abl.Window.Uri(ref),
			i = uri.href.lastIndexOf("/"),
			s = (i >= 0) ? uri.href.substr(i+1) : uri.href;
	return Abl.Window.Uri.stripQueryString(s);
};

/**
 * Strips the current location's protocol information from
 * the supplied href/url.
 * 
 * @param {Object}	Reference to the href string/Uri() constructor object
 * @return {String}	The original href less the protocol
 */
Abl.Window.Uri.stripProtocol = function(ref) {
	var uri = new Abl.Window.Uri(ref);
	if (uri.href.indexOf(window.location.protocol) === 0) {
		return uri.href.substr(window.location.protocol.length);
	} else {
		return uri.href;
	}
};


/**
 * Strips the current location's protocol, hostname and port 
 * information from the supplied href/url.
 * 
 * @param {Object}	Reference to the href string/Uri() constructor object
 * @return {String}	The original href less the protocol, hostname
 *							and port information
 */
Abl.Window.Uri.stripLocation = function(ref) {
	var	uri = new Abl.Window.Uri(ref),
			domain = window.location.protocol + '//' + window.location.hostname;

	if (window.location.port) {
		domain += ":" + window.location.port;
	}
	if (uri.href.indexOf(domain) === 0) {
		return uri.href.substr(domain.length);
	} else {
		return uri.href;
	}
};

/**
 * Strips the current location's query string data
 * from the supplied href/url.
 * 
 * @param {Object}	Reference to the href string/Uri() constructor object
 * @return {String}	The original href less the query string/bookmark
 */
Abl.Window.Uri.stripQueryString = function(ref) {
	var uri = new Abl.Window.Uri(ref), i;
	
	// Strip everyting after the first '?' or '#' ...
	i = uri.href.search(/(\?|#)/);	
	if (i >= 0) {
		uri.href = uri.href.substr(0,i);
	}
	return uri.href;
};

/**
 * Strips the current location's protocol, hostname, port 
 * and query string information from the supplied href/url.
 * 
 * @param {Object}	Reference to the href string/Uri() constructor object
 * @return {String}	The original href less the protocol,
 *							hostname, port and query string information
 */
Abl.Window.Uri.getBaseUrl = function(ref) {
	var	uri = new Abl.Window.Uri(ref),
			s = Abl.Window.Uri.stripLocation(uri.href);
	return Abl.Window.Uri.stripQueryString(s);
};


Abl.Window.Uri.parseQueryString = function(queryString) {

	// define an object to contain the parsed query data
	var	result = {},
		queryComponents,
		i, keyValuePair, key, value;

	// if a query string wasn't specified, use the query string from the URI
	if (typeof queryString === "undefined") {
		queryString = (window.location.search) ? window.location.search : '';
	}

	// remove the leading question mark from the query string if it is present
	if (queryString.charAt(0) === "?") {
		queryString = queryString.substring(1);
	}

	// replace plus signs in the query string with spaces
	queryString = queryString.replace('+', ' ');

	// split the query string around ampersands and semicolons
	queryComponents = queryString.split(/[&;]/g);

	// loop over the query string components
	for (i = 0; i < queryComponents.length; i++){
		// extract this component's key-value pair
		keyValuePair = queryComponents[i].split('=');
		key = decodeURIComponent(keyValuePair[0]);
		value = decodeURIComponent(keyValuePair[1]);
		result[key] = value;
	}

	// return the parsed query data
	return result;
};


Abl.Window.Uri.getParam = function(n, queryString) {
	var qs = Abl.Window.Uri.parseQueryString(queryString);
	return (qs) ? qs[n] : null;
};




/****************************************************************************/
/** Extensions to base jQuery functionality                                **/
/****************************************************************************/
(function($) {

	/*
	** These five 'message' functions provide easy access to an
	** xml based message data structure.
	**
	** See Abl.Web.HttpHandlers.XmlHandler.BuildErrorMessage()
	**
	** Is the data structure of the form <message />?
	*/
	$.fn.isMessage = function() {
		return (this.children(":first").is("message"));
	};

	// Is this an error message <message status="error" />
	$.fn.isErrorMessage = function() {
		return (this.children(":first").attr("status") === "error");
	};
	
	// Is this a success message <message status="success" />
	$.fn.isSuccessMessage = function() {
		return (this.children(":first").attr("status") === "success");
	};

	// Covert xml to json message format
	$.fn.toJsonMessage = function() {
		var	j = {},
				$root = this.children(":first"),
				status = $root.attr("status"),
				$data = $root.children("data");

		if (!$root.is("message")) { throw "Invalid xml message format"; }

		if (status === "error") {
			j.error = $root.children("error").text();
		} else if (this.isSuccessMessage()) {
			j.success = $root.children("success").text();
		} else {
			throw "Unrecognised message status '" + status + "'!";
		}

		if ($data.length > 0) { j.data = $data.text(); }
		return j;
	};
	
	
	$.fn.getMessage = function() {
		var	j = this.toJsonMessage(),
				msg = j.error || j.success || "";
		if (j.data) {
			msg += "\r\n\r\n" + j.data;
		}
		return msg;
	};
	
	
	// Returns the bookmark text from a link element.  If the 'evaluate'
	// argument is true, then the function treats the bookmark as a json
	// string and attempts to evaluate it with eval().
	//
	// <a id=lnk" href="#Fred">Link</a> => $("#lnk").getBookmark() == "Fred"
	//
	// <a id=lnk" href="#{name: 'Neil'}">Link</a> => $("#lnk").getBookmark(true) == {name: 'Neil'}
	$.fn.getBookmark = function(evaluate) {
		var	regex = /#(.*)$/,
				$link = this.filter("a").eq(0),
				href = $link.attr("href"),
				match = regex.exec(href);
		if (match) {
			return (evaluate) ? eval("(" + match[1] + ")") : match[1];
		} else {
			return null;
		}
	};


	$.fn.setFocus = function(selectContent) {
		var $ctrl;
		return this.each(function() {
			$ctrl = $(this);
			if (($ctrl.is("textarea, select, :text")) && ($ctrl.is(":visible")) && (!$ctrl.is(":disabled"))) {
				if (this.focus) { 
					this.focus();
					if ((selectContent) && (this.select)) {
						this.select();
					}
				}
			}
		});
	};

	// Sets all the checkboxes in the selection to checked/unchecked
	$.fn.setCheckbox = function(checked) {
		var $chk;
		return this.each(function() {
			$chk = $(this);
			if ($chk.is(":checkbox")) {
				$chk.attr("checked", ((checked) ? "checked" : ""));
			}
		});
	};

	$.fn.enable = function(enabled) {
		var $ctrl;
		return this.each(function() {
			$ctrl = $(this);
			if ($ctrl.is(":input")) {
				if (enabled) {
					$ctrl.attr("disabled", "").removeClass("disabled");
				} else {
					$ctrl.attr("disabled", "disabled").addClass("disabled");
				}
			}
		});
	};
	

	/**
	 * $.alernateRows
	 *
	 * Adds odd/even styling to all tables in selection.
	 */	
	$.fn.alternateRows = function() {
		return this.each(function() {
			if ($(this).is("table")) {
				$(this).find("tr:not(:has(th)):odd").addClass("odd");
				$(this).find("tr:not(:has(th)):even").addClass("even");
			}
		});
	};


	/**
	 * $.wrapper()
	 * 
	 * Wraps the inner content of an element with a <div /> for each class of 
	 * the classList array.  This method is used, for example, by the panel
	 * method to add additional markup for css styling.
	 * 
	 * @param {Array} classList
	 */
	$.fn.wrapper = function(classList) {
		classList = classList || $.fn.wrapper.defaultList;
		return this.each(function() {
			var $this = $(this), i, wrapper;
			for (i = classList.length - 1; i >= 0; i--) {
				wrapper = "<div class='" + classList[i] + "'></div>";
				$this.wrapInner(wrapper);
			}
		});
	};
	
	/**
	 * The default wrapper classList for adding the classic '8-points of the compass'
	 * markup.
	 */
	$.fn.wrapper.defaultList = ["e", "s", "w", "ne", "se", "sw", "nw", "panelBody"];
	


	/**
	 * Use the method to create a simple top/body/bottom panel structure
	 * of the form:
	 * <div class='myPanel'>
	 *		<div class='topBorder'>
	 *				<div class='bottomBorder'>
	 *				<div class='panelBody'>Original content goes here</div>
	 *			</div>
	 *		</div>
	 * </div>
	 *
	 * If the parameter 'stacked' is set to true, the structure is rendered 
	 * as follows:
	 * <div class='myPanel'>
	 *		<div class='topBorder'></div>
	 *		<div class='panelBody'>Original content goes here</div>
	 *		<div class='bottomBorder'></div>
	 * </div>
	 */
	$.fn.simplePanel = function(options) {
		var params = $.extend({}, $.fn.simplePanel.defaults, options);
		return this.each(function() {
			if (params.stacked) {
				$(this)
				.wrapInner("<div class='" + params.bodyPanelClass + "'></div>")
				.append("<div class='" + params.bottomBorderClass + "'></div>")
				.prepend("<div class='" + params.topBorderClass + "'></div>");
			} else {
				$(this)
				.wrapInner("<div class='" + params.bodyPanelClass + "'></div>")
				.wrapInner("<div class='" + params.bottomBorderClass + "'></div>")
				.wrapInner("<div class='" + params.topBorderClass + "'></div>");
			}
		});
	};
	$.fn.simplePanel.defaults = {
		stacked:					false,
		bodyPanelClass:		"panelBody",
		topBorderClass:		"topBorder",
		bottomBorderClass:	"bottomBorder"
	};


	/**
	 * $.panel()
	 * 
	 * Wraps the inner content of the specified elements with a nested structure
	 * of div elements which can then have 8-points of images applies to them.
	 * 
	 * @param {String} className	The css class name of the panel theme
	 * @param {Object} options		Property list of options.
	 * 
	 * @see jquery.ui.draggable
	 */
	$.fn.panel = function(className, options) {
		var params = $.extend({}, $.fn.panel.defaults, options);
		
		return this.each(function() {
			var	$e = $(this),
					$panelBody,
					$titlebar,
					$header,
					$closeLink;
			
			if (className) { $e.addClass(className); }
			$e.css(params.css);
			
			// Add additional markup for '8-points of the compass' styling - note
			// use of default $.fn.wrapper.default array.
			$e.wrapper();

			// Get a reference to the inner content			
			$panelBody = $("div.panelBody", $e);


			// Set dimensions
			if (params.width) {
				$e.width(params.width);
			}
			if (params.height) {
				$panelBody.height(params.height);
			}
			
			if ((params.innerWidth) || (params.innerHeight)) {
				$e.css("position", "absolute");
				$panelBody.width(params.innerWidth);
				$panelBody.height(params.innerHeight);
			}
			
			// Manage the creation and presentation of the titlebar if specified
			if (params.titlebar) {
				// Create a titlebar and insert it before the existing
				// (inner) content
				$titlebar = $("<div class='titlebar'></div>");
				$panelBody.before($titlebar);
				
				// If title text has been specified, use it!
				if (params.title) {
					$titlebar.append("<h2 class='title'>" + params.title + "</h2>");
				} else {
					// Otherwise, see if the first element of the (inner) content is
					// a header (h1, h2, h3 etc.).  If it is, move it into the
					// titlebar container - neat eh? :-)
					$header = $panelBody.children(":first-child:header").addClass("title");
					if ($header.length) {
						$titlebar.append($header);
					}
				}
			}

			if (params.closeLink) {
				$closeLink = $("<a></a>")
				.addClass(params.closeLinkClass)
				.attr("title", params.closeLinkTitle)
				.attr("href", params.closeLinkHref)
				.text(params.closeLinkText)
				.appendTo($titlebar);
			}

			// Configure jquery.ui.draggable features
			if ((params.draggable) && ($e.draggable)) {
				$e.draggable($.extend({handle: ($titlebar) ? $titlebar : $e}, params.dragOptions));
			}
		});
	};

	/**
	 * Default option settings for $panel
	 * Note, additional jquery.ui.draggable options may be added to this
	 * property list.
	 */
	$.fn.panel.defaults = {
		width:				null,
		height:				null,
		innerWidth:			null,
		innerHeight:		null,
		titlebar:			true,			// Set to 'true' to render a title bar container
		title:				"",			// Titlebar text
		closeLink:			false,
		closeLinkClass:	"close",
		closeLinkTitle:	"Close",
		closeLinkHref:		"#close",
		closeLinkText:		"X",
		css: { },
		draggable:			true,			// Set to 'true' to enable dragging
		dragOptions: {						// jquery.ui.draggable options list
			containment:	"parent"		// jquery.ui.draggable option - was 'parent'
		}
	};

	/**
	 * Calculates the overall distances (width/height) between the
	 * outer container ($panel) and the content (div.panelBody).
	 * 
	 * @param	{Object} $panel	The jQuery panel to be interrogated
	 * @return	{Object}		A width, height dictionary object
	 */
	$.fn.panel.getPanelOffsets = function($panel) {
		var	width = $panel.width(),
				height = $panel.height(),
				$panelBody = $("div.panelBody", $panel),
				innerWidth = $panelBody.width(),
				innerHeight = $panelBody.height();

		return {
			width: width - innerWidth,
			height: height - innerHeight	
		};	
	};


	$.fn.panel.setContentArea = function($panel, width, height) {
		var	offset = $.fn.panel.getPanelOffsets($panel),
				$panelBody = $("div.panelBody", $panel);
		
		if (width) {
			$panel.width(width + offset.width);
			$panelBody.width(width);
		}
		
		if (height) {
			$panel.height(height + offset.height);
			$panelBody.height(height);
		}
	};


	/*
	** jQuery Clamshell
	** Provides $.hideClamshell(), $.showClamshell() and $.toggleClamshell() functionality
	** against a standard nested list structure:
	** 
	** <ul>
	**		<li>
	**			<a />
	**			<ul>
	**				<li><a /></li>
	**			</ul>
	**		</li>
	**	</ul>
	**
	** All methods should be called against a member <li> element.
	*/

	/**
	 * $.hideClamshell()
	 * Hides the sub-content of the referenced <li> element
	 */
	$.fn.hideClamshell = function(speed, callback) {
		return this.each(function() {
			var	$li = $(this),						// the reference <li> element
					$link = $li.children("a"),		// the link <a> element
					$sub = $li.children("ul");		// the <ul> sub-content
			
			// The animated version of $.hide() does not work unless the element
			// is already visible - this is of no use to us as we need it to be 
			// collapsed when it does become visible - hence the code branch below.
			$link.attr("title", "Expand");
			$li.removeClass("expanded").addClass("collapsed");
			if ((speed) && ($sub.is(":visible"))) {
				$sub.hide(speed, function() {
					if (typeof callback === 'function') { callback.call(this, false); }
				});
			} else {
				$sub.hide();
				if (typeof callback === 'function') { callback.call(this, false); }
			}
		});
	};


	/**
	 * $.showClamshell()
	 * Shows the sub-content of the referenced <li> element
	 */
	$.fn.showClamshell = function(speed, callback) {

		// Local method to show the sub-content and set the css classes and title attributes
		function show($li, $link, $sub) {
			$link.attr("title", "Collapse");
			$li.removeClass("collapsed").addClass("expanded");
			if ((speed) && (!$sub.is(":visible"))) {
				$sub.show(speed, function() {
					if (typeof callback === 'function') { callback.call(this, true); }
				});
			} else {
				$sub.show();
				if (typeof callback === 'function') { callback.call(this, true); }
			}
		}

		return this.each(function() {
			var	$li = $(this),						// the reference <li> element
					$link = $li.children("a"),		// the link <a> element
					$sub = $li.children("ul"),		// the <ul> sub-content
					$parent = $li.parent("ul");	// the starting <ul> parent element - we ultimately
															// want the parent <li> element - if there is one

			// Try and locate the containing parent <li> element.  If it's already
			// visible then we don't need to recurse back up the hierarchy
			$parent = (($parent.length > 0) && (!$parent.is(":visible"))) ? $parent = $parent.parent("li") : null;
			
			if (($parent) && ($parent.length > 0)) {
				// If we have a valid, invisible <li> container, then we need to show that first (recursively)
				$parent.showClamshell(speed, function() { show($li, $link, $sub); });
			} else {
				// We just need to handle this sub-content
				show($li, $link, $sub);
			}
		});
	};

	/**
	 * $.toggleClamshell()
	 * Toggles the sub-content visibility of the referenced <li> element
	 */
	$.fn.toggleClamshell = function(speed, callback) {
		return this.each(function() {
			var	$li = $(this), $sub = $li.children("ul");

			if ($sub.is(":visible")) {
				$li.hideClamshell(speed, function(visible) {
					if (typeof callback === 'function') { callback.call(this, visible); }
				});
			} else {
				$li.showClamshell(speed, function(visible) {
					if (typeof callback === 'function') { callback.call(this, visible); }
				});
			}
		});
	};



	/**
	 * Provide IE6 support for min-height
	 * @param {Integer} minHeight
	 */
	$.fn.minHeight = function(minHeight) {
		return this.each(function() {
			var $elem = jQuery(this);
			if ((!minHeight)||(typeof(minHeight) !== "number")) {
				minHeight = parseInt($elem.css("min-height"), 10);
			}
			if ((minHeight)&&(typeof(minHeight) === "number") && ($elem.height() < minHeight)) {
				$elem.height(minHeight);
			}
		});
	};



	/**
	 * Provide IE6 support for min-width
	 * @param {Integer} minHeight
	 */
	$.fn.minWidth = function(minWidth) {
		return this.each(function() {
			var $elem = jQuery(this);
			if ((!minWidth)||(typeof(minWidth) !== "number")) {
				minWidth = parseInt($elem.css("min-width"), 10);
			}
			if ((minWidth)&&(typeof(minWidth) === "number") && ($elem.width() < minWidth)) {
				$elem.width(minWidth);
			}
		});
	};

}(jQuery));



/**
 * Objectifies the jQuery.panel() method result to provide an OO panel
 * class.
 * 
 * This class exposes the following jQuery objects:
 * 
 *		.$container		The outer div.className container
 *		.$body			The inner div.panelBody content container
 *		.$titlebar		The div.titlebar container (if specified)
 *		.closeLink		The a.closeLinkClass close link (if specified)
 * 
 * The class maps the following events:
 * 
 *		onClose			Raised when the $closeLink link is clicked
 */
Abl.UI.Panel = function(panel, className, options) {
	var	_self = this,
			params = $.extend(true, {}, $.fn.panel.defaults, Abl.UI.Panel.defaults, options);
	
	this.$ = (panel instanceof jQuery) ? panel : $(panel);
	this.$.panel(className, params);
	this.$body = $("div.panelBody", this.$);
	this.$titlebar = $("div.titlebar", this.$);
	this.$closeLink = $("a." + params.closeLinkClass, this.$).click(function(evt) {
		evt.preventDefault();
		if (typeof params.onClose === 'function') {
			params.onClose.call(_self, evt);
		}		
	});
	
	this.setContentArea = function(width, height) {
		$.panel.setContentArea(this.$, width, height);
	};
	
	this.isVisible = function() {
		return this.$.is(":visible");
	};
	
	
	this.dispose = function() {
		this.$closeLink.unbind("click");
	};
};


/**
 * Extends $.fn.panel.defaults
 */
Abl.UI.Panel.defaults = {
	onClose: null
};



/*
** ImagePreLoader
** Loads an array of images of the form...
**
**		var gallery = [
**			{ src: "/img/bathroom.jpg", alt: "Stylish Bathroom" },
**			{ src: "/img/tanker.jpg", alt: "Drainage Tanker" }
**		];
**		var images = new Abl.UI.ImagePreLoader(gallery);
**
*/
Abl.UI.ImagePreLoader = function(images, options) {
	var	_self = this,
			params = $.extend(true, {}, Abl.UI.ImagePreLoader.defaults, options),
			noImages = images.length,
			i = 0,
			noLoaded = 0,
			noErrors = 0,
			noAborted = 0,
			noProcessed = 0,
			bIsLoaded = false,
			index = 0,
			aImages = [];


	/*
	** Event Handlers
	*/
	function onComplete() {
		noProcessed++;
		if (noProcessed === noImages) {
			bIsLoaded = true;
			if (typeof params.onLoaded === 'function') {
				params.onLoaded.call(_self);
			}
		}
	}

	function onLoad() {
		$(this).data("preload").loaded = true;
		noLoaded++;
		onComplete();
	}

	function onError() {
		$(this).data("preload").error = true;
		noErrors++;
		if (typeof params.onError === 'function') {
			params.onError.call($(this));
		}
		onComplete();
	}

	function onAbort() {
		$(this).data("preload").aborted = true;
		noAborted++;
		if (typeof params.onAbort === 'function') {
			params.onAbort.call($(this));
		}
		onComplete();
	}



	/*
	** Private Methods
	*/
	function loadImage(imageData) {
		var $img = $("<img />");
		
		if (typeof imageData === 'string') {
			imageData = {src: imageData}
		}
		
		aImages.push($img);
		$img.bind("load", onLoad);
		$img.bind("error", onError);
		$img.bind("abort", onAbort);
		$img.data("preload", { loaded: false, error: false, aborted: false });
		$img.attr({
			src: imageData.src,
			alt: imageData.alt
		});
	}


	/*
	** Public Properties
	*/
	this.isLoaded = function() {
		return bIsLoaded;
	};

	this.getNoImages = function() {
		return noImages;
	};

	this.getNoLoaded = function() {
		return noLoaded;
	};

	this.getNoErrors = function() {
		return noErrors;
	};

	this.getNoAborted = function() {
		return noAborted;
	};

	this.getIndex = function() {
		return index;
	};


	/*
	** Public Methods
	*/
	this.setIndex = function(i) {
		if ((i < 0) || (i >= noImages)) { i = 0; }
		index = i;
	};

	this.setNextImage = function() {
		this.setIndex(index + 1);
	};

	this.getImage = function() {
		// alert("Returning image: " + (index + 1) + " of " + aImages.length + " images");
		return aImages[index];
	};

	this.getNextImage = function() {
		this.setNextImage();
		return this.getImage();
	};


	this.dispose = function() {
		var i, $img;
		for (i = 0; i < aImages.length; i++) {
			$img = aImages[i];
			$img.removeData("preload").unbind();
		}
	};


	/*
	** Initialse the object
	*/
	for (i = 0; i < images.length; i++) {
		loadImage(images[i]);
	}

};

Abl.UI.ImagePreLoader.defaults = {
	onLoaded: null,
	onError: null,
	onAbort: null
};


Abl.Json.unescape = function(s) {
	if ((s) && (typeof s === 'string')) {
		s = s.replace(/\\\'/g, "\'");		// Replace single quote
		s = s.replace(/\\\"/g, "\"");		// Replace double quote
	}
	return s;
};



/*
** Creates and manages an image thumbnail object.
**
** var thumb = Abl.UI.Thumbnail($container, {
**		onLoad: function() { alert("Image Loaded!"); }
**	});
** thumb.loadImage("/img/HondaSP2.jpg");
*/

Abl.UI.Thumbnail = function(container, options) {
	return (function(container, options) {
		var	foo = {},
				$container = (container instanceof jQuery) ? container : $(container),
				img = null;
			
		foo.params = $.extend(true, {}, Abl.UI.Thumbnail.defaults, options);


		/*******************************************************************************************
		* Image Management                                                                         *
		*******************************************************************************************/
		foo.clearImage = function() {
			if (img) {
				img.onload = null;
				img = null;
			}
			$container.find("img.thumbnail").remove();
			$container.find("div.sizeInfo").remove();
		};



		/*******************************************************************************************
		* Image Access                                                                             *
		*******************************************************************************************/
		foo.getDimensions = function() {
			var	dims = {width: 0, height: 0};
			
			if (img) {
				dims.width = img.width;
				dims.height = img.height;
			}
			return dims;
		};
		
		foo.getImage = function() {
			return img;
		};

		// Will accept a width, height array or a data dictionary fror comparison
		foo.equalSize = function(dims) {
			var	d = (dims instanceof Array) ? {width: dims[0], height: dims[1]} : dims;
			return ((img) && (img.width === d.width) && (img.height === d.height));
		};


		/*******************************************************************************************
		* Event Handlers                                                                           *
		*******************************************************************************************/
		function onLoad() {
			var	size = Math.forceFit(img.width, img.height, foo.params.thumbWidth, foo.params.thumbHeight),
					$sizeInfo = null,
					scale = 0,
					scaleText = "";

			$("<img />").attr({
				width: size[0],
				height: size[1],
				alt: img.alt,
				title: img.alt,
				src: img.src
			}).css({
				width: size[0] + "px",
				height: size[1] + "px"
			}).addClass("thumbnail").appendTo($container);

			if (foo.params.showInfo) {
				$sizeInfo = $("<div class='sizeInfo'></div>").appendTo($container);
				$("<span class='dim'></span>").text("Size: " + img.width + " x " + img.height).appendTo($sizeInfo);

				scale = parseInt((size[0] / img.width) * 100, 10);
				scaleText = (scale === 100) ? "(Shown full size)" : "(Shown at " + scale + "% of actual size)";
				$("<span class='scale'></span>").text(scaleText).appendTo($sizeInfo);
			}
			
			if (typeof foo.params.onLoad === "function") {
				foo.params.onLoad(img);
			}
		}

		foo.addOnLoadEventHandler = function(fn) {
			foo.params.onLoad = Abl.chain(foo.params.onLoad, fn);
		};


		/*******************************************************************************************
		* Image Loading Methods and (internal) Events                                              *
		*******************************************************************************************/
		foo.loadImage = function(url) {
			img = new Image();
			img.onload = onLoad;
			img.src = url;
		};

		
		/*******************************************************************************************
		* Object Methods                                                                           *
		*******************************************************************************************/
		foo.dispose = function() {
			foo.clearImage();
		};
		
		return foo;
	}(container, options));
};

Abl.UI.Thumbnail.defaults = {
	thumbWidth: 150,
	thumbHeight: 100,
	showInfo: true
};