﻿(function ($) {

	/* ---------- jQuery Element Extensions ---------- */

	$.fn.alignCenter = function (options) {
		options = $.extend({
			forceAbsolute: false,
			container: window,
			completeHandler: null
		}, options);

		return this.each(function (i) {
			var el = $(this);
			var jWin = $(options.container);
			var isWin = options.container == window;

			if (options.forceAbsolute) {
				if (isWin) {
					el.remove().appendTo("body");
				} else {
					el.remove().appendTo(jWin.get(0));
				}
			}

			el.css("position", "absolute");
			var heightFudge = isWin ? 2.0 : 1.8;
			var x = (isWin ? jWin.width() : jWin.outerWidth()) / 2 - el.outerWidth() / 2;
			var y = (isWin ? jWin.height() : jWin.outerHeight()) / heightFudge - el.outerHeight() / 2;
			el.css("left", x + jWin.scrollLeft()).css("top", y + jWin.scrollTop());

			if (options.completeHandler) options.completeHandler(this);
		});
	};

	$.fn.chooseDate = function (options, callback) {
		options = $.extend({
			format: "d MMMM, yyyy"
		}, options);

		$.dateChooser.show(this, options, callback);

		return this;
	};

	$.fn.enableIE6Transparency = function (options) {
		options = $.extend({
			blankImageUrl: "/i/_.gif"
		}, options);

		if (navigator.appName == "Microsoft Internet Explorer" && parseInt(navigator.appVersion) == 4 && (navigator.appVersion.indexOf("MSIE 5.5") != -1 || navigator.appVersion.indexOf("MSIE 6.0") != -1)) {
			return this.each(function (i) { $(this).css("filter", "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled='true', src='" + $(this).attr("src") + "')").attr("src", options.blankImageUrl); });
		} else {
			return this;
		}
	};

	$.fn.enableIE6BackgroundTransparency = function (options) {
		options = $.extend({
			blankImageUrl: "/i/_.gif",
			scale: false
		}, options);

		if (navigator.appName == "Microsoft Internet Explorer" && parseInt(navigator.appVersion) == 4 && (navigator.appVersion.indexOf("MSIE 5.5") != -1 || navigator.appVersion.indexOf("MSIE 6.0") != -1)) {
			return this.each(function (i) {
				var bgCss = $(this).css("background-image");
				$(this).css({ "background-image": "none", "filter": "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled='true', sizingMethod='" + (options.scale ? "scale" : "crop") + "', src='" + bgCss.substring(5, bgCss.length - 2) + "')" });
			});
		} else {
			return this;
		}
	};

	$.fn.limitHeight = function (options) {
		options = $.extend({
			value: 100
		}, options);

		return this.each(function (i) {
			if ($(this).height() > options.value) $(this).css({ "height": options.value + "px", "overflow": "hidden", "text-overflow": "ellipsis" });
		});
	};

	$.fn.overlay = function (options, callback) {
		options = $.extend({
			zIndex: 1000,
			color: "#303132",
			opacity: 0.87,
			click: null
		}, options);

		$.overlay.show(options);

		var iMax = this.length;
		return this.each(function (i) {
			$(this).addClass("Phizz_Overlay").css({ "z-index": (options.zIndex + $.overlay.zIndexStack.length + 1), "visibility": "visible", "opacity": 0 }).alignCenter().fadeTo(200, 1, i == iMax - 1 ? callback : null);
		});
	};

	$.fn.unoverlay = function (callback) {
		$.overlay.hide();

		var iMax = this.length;
		return this.each(function (i) {
			$(this).removeClass("Phizz_Overlay").fadeTo(200, 0, function () { $(this).css({ "top:": "-2000px", "visibility": "hidden" }); if (i == iMax - 1 && callback != null) callback(); });
		});
	};

	$.fn.rotateImages = function (options) {
		options = $.extend({
			urlArray: [],
			delay: 5000,
			speed: 750,
			fadeOutBottomImage: false
		}, options);

		return this.each(function (i) {
			$.rotator.initialise($(this), options.urlArray, options.delay, options.speed, options.fadeOutBottomImage);
		});
	}

	$.fn.stopRotateImages = function () {
		return this.each(function (i) {
			$.rotator.stop($(this));
		});
	}




	/* ---------- Backwards Compatibility ---------- */

	$.backwardsCompatibility = {
		hideIE6Foreground: function () {
			$("object, select").each(function () {
				$(this).css("visibility", "hidden").addClass("IE6Foreground");
			});
		},
		unhideIE6Foreground: function () {
			$(".IE6Foreground").css("visibility", "visible").removeClass("IE6Foreground");
		}
	}




	/* ---------- Browser Detection ---------- */

	$.browser = {
		isWindows: function () { return /Windows/i.test(navigator.userAgent); },
		isIE: function () { return /MSIE/i.test(navigator.userAgent); },
		isIE6: function () { return /MSIE 6/i.test(navigator.userAgent); },
		isIE7: function () { return /MSIE 7/i.test(navigator.userAgent); },
		isSafari: function () { return /Safari/i.test(navigator.userAgent); }
	}




	/* ---------- Cookies ---------- */

	$.cookie = {
		set: function (name, value, days) {
			var expires = "";
			if (days) {
				var date = new Date();
				date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
				var expires = "; expires=" + date.toGMTString();
			}
			document.cookie = name + "=" + value + expires + "; path=/";
		},

		get: function (name) {
			var nameEQ = name + "=";
			var ca = document.cookie.split(';');
			for (var i = 0; i < ca.length; i++) {
				var 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;
		},

		remove: function (name) {
			$.cookie.set(name, "", -1);
		}
	}




	/* ---------- Date Chooser  ---------- */

	$.dateChooser = {
		jqTarget: null,
		format: null,
		selectedDate: null,

		show: function (jqTarget, options, callback) {
			if (document.getElementById("DateChooser") == null) {
				$(document.body).append("<div id=\"DateChooser\"><div class=\"content\"><table class=\"navigation\"><tbody><tr><td onclick=\"$.dateChooser.addYears(-1);\">&lt; Year</td><td onclick=\"$.dateChooser.addMonths(-1);\">&lt; Month</td><th> </th><td onclick=\"$.dateChooser.addMonths(1);\">Month &gt;</td><td onclick=\"$.dateChooser.addYears(1);\">Year &gt;</td></tr></tbody></table><div id=\"DateChooserCalendar\"> </div></div></div>");
				$("#DateChooser table.navigation td").mouseover(function () { $(this).addClass("focus"); }).mouseout(function () { $(this).removeClass("focus"); });
			}

			this.jqTarget = jqTarget;
			this.format = options.format;

			var date = $.dateTime.isValid(jqTarget.eq(0).val(), this.format) ? $.dateTime.parse(jqTarget.eq(0).val(), this.format) : new Date();
			this.selectedDate = new Date(date.getFullYear(), date.getMonth(), 1);
			this.refresh();

			$("#DateChooser").overlay({ click: function () { $.dateChooser.hide(); } }, callback);
		},

		hide: function () {
			$("#DateChooser").unoverlay();
		},

		refresh: function () {
			$("#DateChooser table.navigation th").html($.dateTime.monthNames[this.selectedDate.getMonth()].substr(0, 3).concat(" ", this.selectedDate.getFullYear()));

			var sBody = "";
			var i = 1;
			var firstDayOfWeek = this.selectedDate.getDay() - 1; if (firstDayOfWeek == -1) firstDayOfWeek = 6;
			var lastDayInMonth = $.dateTime.daysInMonth(this.selectedDate);
			var today = new Date();

			while (i <= lastDayInMonth) {
				sBody = sBody.concat("<tr>");
				for (var d = 0; d < 7; d++) {
					if ((i == 1 && firstDayOfWeek > d) || i == lastDayInMonth + 1) {
						sBody = sBody.concat("<td class=\"e\">&nbsp;</td>");
					} else {
						sBody = sBody.concat("<td", (this.selectedDate.getFullYear() == today.getFullYear() && this.selectedDate.getMonth() == today.getMonth() && i == today.getDate() ? " class=\"today\"" : ""), ">", i++, "</td>");
					}
				}
				sBody = sBody.concat("</tr>");
			}

			$("#DateChooserCalendar").html("<table class=\"calendar\" cellpadding=\"0\" cellspacing=\"0\"><thead><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th><th>S</th></thead><tbody>".concat(sBody, "</tbody></table>"));
			$("#DateChooser table.calendar td:not(.e)").click(function () { $.dateChooser.submit(parseInt($(this).html())); }).mouseover(function () { $(this).addClass("focus"); }).mouseout(function () { $(this).removeClass("focus"); });
		},

		submit: function (day) {
			this.selectedDate.setDate(day);
			this.jqTarget.val($.dateTime.toString(this.selectedDate, this.format));
			this.jqTarget.keyup();
			this.hide();
		},

		addMonths: function (n) {
			this.selectedDate.setMonth(this.selectedDate.getMonth() + n);
			this.refresh();
		},

		addYears: function (n) {
			this.selectedDate.setFullYear(this.selectedDate.getFullYear() + n);
			this.refresh();
		}
	};




	/* ---------- Date ---------- */

	$.dateTime = {

		monthNames: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
		dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],

		toString: function (dt, strFormat) {
			strFormat = strFormat.replace(/yyyy/g, dt.getFullYear()).replace(/yy/g, dt.getFullYear().toString().substr(2, 2));
			strFormat = strFormat.replace(/MMMM/g, "$1").replace(/MMM/g, "$2").replace(/MM/g, $.padStart((dt.getMonth() + 1).toString(), "0", 2)).replace(/M/g, dt.getMonth() + 1);
			strFormat = strFormat.replace(/dddd/g, "$3").replace(/ddd/g, "$4").replace(/dd/g, $.padStart(dt.getDate().toString(), "0", 2)).replace(/d/g, dt.getDate());
			strFormat = strFormat.replace(/HH/g, $.padStart(dt.getHours().toString(), "0", 2)).replace(/H/g, dt.getHours());
			strFormat = strFormat.replace(/hh/g, $.padStart((dt.getHours() > 12 ? dt.getHours() - 12 : dt.getHours()).toString(), "0", 2)).replace(/h/g, dt.getHours() > 12 ? dt.getHours() - 12 : dt.getHours());
			strFormat = strFormat.replace(/mm/g, $.padStart(dt.getMinutes().toString(), "0", 2)).replace(/m/g, dt.getMinutes());
			strFormat = strFormat.replace(/ss/g, $.padStart(dt.getSeconds().toString(), "0", 2)).replace(/s/g, dt.getSeconds());
			strFormat = strFormat.replace(/tt/g, dt.getHours() > 12 ? "PM" : "AM").replace(/t/g, dt.getHours() > 12 ? "P" : "A");
			strFormat = strFormat.replace(/\$1/g, this.monthNames[dt.getMonth()]).replace(/\$2/g, this.monthNames[dt.getMonth()].substr(0, 3)).replace(/\$3/g, this.dayNames[dt.getDay()]).replace(/\$4/g, this.dayNames[dt.getDay()].substr(0, 3));
			return strFormat;
		},

		isValid: function (strDate, strFormat) {
			var dt;
			try {
				dt = this.parse(strDate, strFormat);
				return true;
			} catch (err) { }
			return false;
		},

		parse: function (strDate, strFormat) {
			strDate = strDate.toLowerCase();

			var i;
			if (strFormat.indexOf("MMMM") > -1) for (i = 0; i < this.monthNames.length; i++) if (strDate.indexOf(this.monthNames[i].toLowerCase()) > -1) {
				strDate = strDate.replace(this.monthNames[i].toLowerCase(), i < 9 ? "0" + (i + 1) : i + 1);
				strFormat = strFormat.replace("MMMM", "MM");
				break;
			}
			if (strFormat.indexOf("MMM") > -1) for (i = 0; i < this.monthNames.length; i++) if (strDate.indexOf(this.monthNames[i].toLowerCase().substr(0, 3)) > -1) {
				strDate = strDate.replace(this.monthNames[i].toLowerCase().substr(0, 3), i < 9 ? "0" + (i + 1) : i + 1);
				strFormat = strFormat.replace("MMM", "MM");
				break;
			}

			var parsedDateElements = { year: null, month: null, day: null, hours: null, minutes: null, seconds: null, ampm: null };

			var tokens = new Array();
			while (strFormat.length > 0) {
				var obj = (function (s) {
					var token = "";
					if (s.search(/[yMdHhmst]/) == 0) {
						do {
							token = token.concat(s.charAt(0));
							s = s.substr(1);
						} while (s.length > 0 && s.charAt(0) == token.charAt(0));
					} else {
						token = "'";
						do {
							token = token.concat(s.charAt(0));
							s = s.substr(1);
						} while (s.length > 0 && s.search(/[yMdHhmst]/) != 0);
						token = token.concat("'");
					}
					return { token: token, strFormat: s };
				})(strFormat);
				tokens.push(obj.token);
				strFormat = obj.strFormat;
			}

			for (i = 0; i < tokens.length; i++) {
				var token = tokens[i];
				if (/^'[^']+'$/.test(token)) {
					token = token.substr(1, token.length - 2);
					if (strDate.indexOf(token) == 0) {
						strDate = strDate.substr(token.length);
					} else {
						throw new Error("Input string does not match format string.");
					}
				} else if (token == "yyyy") {
					if (parsedDateElements.year != null) throw new Error("Invalid format string. Year cannot be parsed more than once.");
					parsedDateElements.year = parseInt(strDate.substr(0, 4));
					strDate = strDate.substr(4);
				} else if (token == "yy") {
					if (parsedDateElements.year != null) throw new Error("Invalid format string. Year cannot be parsed more than once.");
					parsedDateElements.year = parseInt("20".concat(strDate.substr(0, 2)));
					strDate = strDate.substr(2);
				} else if (token == "MM") {
					if (parsedDateElements.month != null) throw new Error("Invalid format string. Month cannot be parsed more than once.");
					parsedDateElements.month = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
					strDate = strDate.substr(2);
				} else if (token == "M") {
					if (parsedDateElements.month != null) throw new Error("Invalid format string. Month cannot be parsed more than once.");
					if (/^1[012]/.test(strDate)) {
						parsedDateElements.month = parseInt(strDate.substr(0, 2));
						strDate = strDate.substr(2);
					} else {
						parsedDateElements.month = parseInt(strDate.substr(0, 1));
						strDate = strDate.substr(1);
					}
				} else if (token == "dd") {
					if (parsedDateElements.day != null) throw new Error("Invalid format string. Day cannot be parsed more than once.");
					if (!/^(0[1-9]|[12][0-9]|3[01])/.test(strDate)) throw new Error("Input string does not match format string.");
					parsedDateElements.day = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
					strDate = strDate.substr(2);
				} else if (token == "d") {
					if (parsedDateElements.day != null) throw new Error("Invalid format string. Day cannot be parsed more than once.");
					if (!/^([1-9]|[12][0-9]|3[01])/.test(strDate)) throw new Error("Input string does not match format string.");
					if (/^([12][0-9]|3[01])/.test(strDate)) {
						parsedDateElements.day = parseInt(strDate.substr(0, 2));
						strDate = strDate.substr(2);
					} else {
						parsedDateElements.day = parseInt(strDate.substr(0, 1));
						strDate = strDate.substr(1);
					}
				} else if (token == "HH") {
					if (parsedDateElements.hours != null) throw new Error("Invalid format string. Hours cannot be parsed more than once.");
					if (!/^([0-1][0-9]|2[0-3])/.test(strDate)) throw new Error("Input string does not match format string.");
					parsedDateElements.hours = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
					strDate = strDate.substr(2);
				} else if (token == "H") {
					if (parsedDateElements.hours != null) throw new Error("Invalid format string. Hours cannot be parsed more than once.");
					if (!/^(0-9|1[0-9]|2[0-3])/.test(strDate)) throw new Error("Input string does not match format string.");
					if (/^(1[0-9]|2[0-3])/.test(strDate)) {
						parsedDateElements.hours = parseInt(strDate.substr(0, 2));
						strDate = strDate.substr(2);
					} else {
						parsedDateElements.hours = parseInt(strDate.substr(0, 1));
						strDate = strDate.substr(1);
					}
				} else if (token == "hh") {
					if (parsedDateElements.hours != null) throw new Error("Invalid format string. Hours cannot be parsed more than once.");
					if (!/^(0[1-9]|1[0-2])/.test(strDate)) throw new Error("Input string does not match format string.");
					parsedDateElements.hours = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
					strDate = strDate.substr(2);
				} else if (token == "h") {
					if (parsedDateElements.hours != null) throw new Error("Invalid format string. Hours cannot be parsed more than once.");
					if (!/^([1-9]|1[0-2])/.test(strDate)) throw new Error("Input string does not match format string.");
					if (/^1[0-2]/.test(strDate)) {
						parsedDateElements.hours = parseInt(strDate.substr(0, 2));
						strDate = strDate.substr(2);
					} else {
						parsedDateElements.hours = parseInt(strDate.substr(0, 1));
						strDate = strDate.substr(1);
					}
				} else if (token == "mm") {
					if (parsedDateElements.minutes != null) throw new Error("Invalid format string. Minutes cannot be parsed more than once.");
					if (!/^[0-5]?[0-9]/.test(strDate)) throw new Error("Input string does not match format string.");
					parsedDateElements.minutes = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
					strDate = strDate.substr(2);
				} else if (token == "m") {
					if (parsedDateElements.minutes != null) throw new Error("Invalid format string. Minutes cannot be parsed more than once.");
					if (/^[1-5][0-9]/.test(strDate)) {
						parsedDateElements.minutes = parseInt(strDate.substr(0, 2));
						strDate = strDate.substr(2);
					} else {
						parsedDateElements.minutes = parseInt(strDate.substr(0, 1));
						strDate = strDate.substr(1);
					}
				} else if (token == "ss") {
					if (parsedDateElements.seconds != null) throw new Error("Invalid format string. Seconds cannot be parsed more than once.");
					if (!/^[0-5]?[0-9]/.test(strDate)) throw new Error("Input string does not match format string.");
					parsedDateElements.seconds = parseInt(strDate.charAt(0) == "0" ? strDate.substr(1, 1) : strDate.substr(0, 2));
					strDate = strDate.substr(2);
				} else if (token == "s") {
					if (parsedDateElements.seconds != null) throw new Error("Invalid format string. Seconds cannot be parsed more than once.");
					if (/^[1-5][0-9]/.test(strDate)) {
						parsedDateElements.seconds = parseInt(strDate.substr(0, 2));
						strDate = strDate.substr(2);
					} else {
						parsedDateElements.seconds = parseInt(strDate.substr(0, 1));
						strDate = strDate.substr(1);
					}
				} else if (token == "tt") {
					if (parsedDateElements.ampm != null) throw new Error("Invalid format string. AM/PM cannot be parsed more than once.");
					if (!/^[ap]m/.test(strDate)) throw new Error("Input string does not match format string.");
					parsedDateElements.ampm = strDate.substr(0, 1);
					strDate = strDate.substr(2);
				} else if (token == "t") {
					if (parsedDateElements.ampm != null) throw new Error("Invalid format string. AM/PM cannot be parsed more than once.");
					if (!/^[ap]/.test(strDate)) throw new Error("Input string does not match format string.");
					parsedDateElements.ampm = strDate.substr(0, 1);
					strDate = strDate.substr(1);
				}
			}

			if (strDate.length > 0) throw new Error("Input string does not match format string.");

			if (parsedDateElements.hours < 12 && parsedDateElements.ampm == "p") parsedDateElements.hours += 12;

			var dt = new Date();
			if (parsedDateElements.year != null) dt.setFullYear(parsedDateElements.year);
			dt.setMonth(parsedDateElements.month != null ? parsedDateElements.month - 1 : 0);
			dt.setDate(parsedDateElements.day != null ? parsedDateElements.day : 1);
			dt.setHours(parsedDateElements.hours != null ? parsedDateElements.hours : 0);
			dt.setMinutes(parsedDateElements.minutes != null ? parsedDateElements.minutes : 0);
			dt.setSeconds(parsedDateElements.seconds != null ? parsedDateElements.seconds : 0);

			return dt;
		},

		daysInMonth: function (date) {
			switch (date.getMonth() + 1) {
				case 1: case 3: case 5: case 7: case 8: case 10: case 12: return 31; break;
				case 4: case 6: case 9: case 11: return 30; break;
				case 2: return this.isLeapYear(date.getFullYear()) ? 29 : 28; break;
			}
		},

		isLeapYear: function (year) {
			return year % 4 == 0 ? (year % 100 == 0 ? year % 400 == 0 : true) : false;
		}

	};




	/* ---------- Navigation  ---------- */

	$.navigate = function (url) {
		if (window.event) window.event.returnValue = false;
		window.location.href = url;
	};

	$.openWindow = function (url) {
		window.open(url);
	};

	$.queryString = {
		path: null,
		keys: null,
		values: null,
		anchor: null,

		parse: function () {
			if (this.keys != null) return;
			var url = window.location.href;

			var anchorIndex = url.indexOf("#");
			if (anchorIndex > -1) {
				this.anchor = url.substr(anchorIndex + 1);
				url = url.substr(0, anchorIndex);
			}

			this.keys = new Array();
			this.values = new Array();
			if (url.indexOf("?") > -1) {
				var n = url.indexOf("?");
				this.path = url.substr(0, n);
				var pairs = url.substr(n + 1).split("&");
				for (var i = 0; i < pairs.length; i++) {
					n = pairs[i].indexOf("=");
					this.append(pairs[i].substr(0, n), $.urlDecode(pairs[i].substr(n + 1)), true);
				}
			} else {
				this.path = url;
			}
		},

		commit: function () {
			var qs = "";
			for (var i = 0; i < this.keys.length; i++) qs = qs.concat(qs.length > 0 ? "&" : "", $.urlEncode(this.keys[i]), "=", $.urlEncode(this.values[this.keys[i]]));
			$.navigate(this.path + (qs.length > 0 ? "?" + qs : "") + (this.anchor != null && this.anchor.length > 0 ? "#" + this.anchor : ""));
		},

		get: function (key) {
			this.parse();
			return this.values[key];
		},

		hasKey: function (key) {
			return this.get(key) != null;
		},

		append: function (key, value, cancelCommit) {
			this.parse();

			if (this.hasKey(key) && this.values[key].length > 0) {
				if (this.values[key] != value) this.values[key] = this.values[key].concat(",", value);
			} else {
				this.keys.push(key);
				this.values[key] = value;
			}

			if (!cancelCommit) this.commit();
		},

		appendMultiple: function (keysAndValues, cancelCommit) {
			for (var key in keysAndValues) this.append(key, keysAndValues[key], true);
			if (!cancelCommit) this.commit();
		},

		set: function (key, value, cancelCommit) {
			this.parse();

			if (!this.hasKey(key)) this.keys.push(key);
			this.values[key] = value.toString();

			if (!cancelCommit) this.commit();
		},

		setMultiple: function (keysAndValues, cancelCommit) {
			for (var key in keysAndValues) this.set(key, keysAndValues[key], true);
			if (!cancelCommit) this.commit();
		},

		remove: function (key, cancelCommit) {
			this.parse();

			if (this.hasKey(key)) {
				for (var i = 0; i < this.keys.length; i++) if (this.keys[i] == key) {
					this.keys.splice(i, 1);
					break;
				}
				delete this.values[key];
			}

			if (!cancelCommit) this.commit();
		},

		removeMultiple: function (keys, cancelCommit) {
			for (var i = 0; i < keys.length; i++) this.remove(keys[i], true);
			if (!cancelCommit) this.commit();
		},

		removeAll: function (cancelCommit) {
			this.keys = new Array();
			this.values = new Array();
			if (!cancelCommit) this.commit();
		},

		removeAllExcept: function (key, cancelCommit) {
			this.removeAllExceptMultiple([key], cancelCommit);
		},

		removeAllExceptMultiple: function (keys, cancelCommit) {
			var newKeys = new Array();
			var newValues = new Array();

			for (var i = 0; i < keys.length; i++) {
				if (this.hasKey(keys[i])) {
					newKeys.push(keys[i]);
					newValues[keys[i]] = this.values[keys[i]];
				}
			}

			this.keys = newKeys;
			this.values = newValues;

			if (!cancelCommit) this.commit();
		},

		unappend: function (key, value, cancelCommit) {
			this.parse();

			if (this.hasKey(key) && this.values[key].length > 0) {
				if (this.values[key] == value) {
					this.remove(key, true);
				} else {
					this.values[key] = this.values[key].replace(new RegExp("(^|,)" + value + "(,|$)", "g"), "");
				}
			}

			if (!cancelCommit) this.commit();
		}

	};




	/* ---------- Overlay  ---------- */

	$.overlay = {
		zIndexStack: new Array(),

		show: function (options) {
			if (document.getElementById("Phizz_Overlay") == null) $(document.body).append("<div id=\"Phizz_Overlay\" style=\"position:absolute;visibility:hidden;width:100%;height:1px;top:-1px;left:0px;\">&nbsp;</div>");

			this.zIndexStack.push(options.zIndex);
			var jq = $("#Phizz_Overlay");
			jq.css("z-index", options.zIndex);

			if (this.zIndexStack.length == 1) {
				jq.css({ "visibility": "visible", "top": "0px", "height": $(document).height(), "opacity": 0, "background-color": options.color }).fadeTo(200, options.opacity);
				if ($.browser.isIE6()) $.backwardsCompatibility.hideIE6Foreground();
				$(window).bind("resize", $.overlay.onWindowResize).bind("scroll", $.overlay.onWindowScroll);
				//} else if (parseFloat(jq.css("opacity")) != options.opacity) {
			}

			if (options.click != null) jq.bind("click", options.click);
		},

		hide: function () {
			this.zIndexStack.pop();
			if (this.zIndexStack.length == 0) {
				$("#Phizz_Overlay").fadeTo(200, 0, function () { $(this).css({ "visibility": "hidden", "top": "-1px", "height": "1px" }); if ($.browser.isIE6()) $.backwardsCompatibility.unhideIE6Foreground(); }).unbind("click");
				$(window).unbind("resize", $.overlay.onWindowResize).unbind("scroll", $.overlay.onWindowScroll);
			} else {
				$("#Phizz_Overlay").css("z-index", this.zIndexStack[this.zIndexStack.length - 1]);
			}
		},

		onWindowResize: function () {
			$(".Phizz_Overlay").alignCenter();
		},

		onWindowScroll: function () {
			$(".Phizz_Overlay").alignCenter();
		}
	};




	/* ---------- Rotator  ---------- */

	//TODO: handle elements with no id, handle elements with no <img>, handle non-<img> / multiple <img> child elements
	$.rotator = {
		urlArrays: null,
		indices: null,
		delays: null,
		speeds: null,
		fadeOutBottomImages: null,
		timeoutIds: null,

		initialise: function (jq, urlArray, delay, speed, fadeOutBottomImage) {
			if (this.urlArrays == null) this.urlArrays = new Array();
			if (this.indices == null) this.indices = new Array();
			if (this.delays == null) this.delays = new Array();
			if (this.speeds == null) this.speeds = new Array();
			if (this.fadeOutBottomImages == null) this.fadeOutBottomImages = new Array();
			if (this.timeoutIds == null) this.timeoutIds = new Array();

			var id = jq.attr("id");
			this.indices[id] = 0;
			this.delays[id] = delay;
			this.speeds[id] = speed;
			this.fadeOutBottomImages[id] = fadeOutBottomImage;

			this.urlArrays[id] = new Array(urlArray.length);
			var initialSrc = jq.find("img:eq(0)").attr("src");
			for (var i = 0; i < this.urlArrays[id].length; i++) {
				this.urlArrays[id][i] = urlArray.splice(Math.floor(Math.random() * urlArray.length), 1)[0];
				if (this.urlArrays[id][i] == initialSrc) { this.indices[id] = i; this.indices[id]; }
			}
			this.timeoutIds[id] = setTimeout(function () { $.rotator.rotate(jq); }, this.delays[id]);
		},

		rotate: function (jq) {
			var id = jq.attr("id");
			this.indices[id] = (this.indices[id] == this.urlArrays[id].length - 1) ? 0 : this.indices[id] + 1;
			var img = new Image();
			$(img).load(function () {
				$(this).hide();
				jq.append(this);
				if ($.rotator.fadeOutBottomImages[id] == true) jq.find("img:eq(0)").fadeOut($.rotator.speeds[id]);
				$(this).fadeIn($.rotator.speeds[id], function () {
					jq.find("img:eq(0)").remove();
					$.rotator.timeoutIds[id] = setTimeout(function () { $.rotator.rotate(jq); }, $.rotator.delays[id]);
				});
			}).attr("src", this.urlArrays[id][this.indices[id]]);
		},

		stop: function (jq) {
			clearTimeout($.rotator.timeoutIds[jq.attr("id")]);
		}
	};




	/* ---------- Utilities ---------- */

	$.indexOfFirst = function (str, arrayofSubstrings) {
		var n = str.length;
		for (var i = 0; i < arrayofSubstrings.length; i++) {
			var m = str.indexOf(arrayofSubstrings[i]);
			if (m > -1) n = Math.min(n, m);
		}
		return n == str.length ? -1 : n;
	},

    $.padStart = function (strToPad, chrPadding, intLength) {
    	while (strToPad.length < intLength) strToPad = chrPadding.concat(strToPad);
    	return strToPad;
    };

	$.padEnd = function (strToPad, chrPadding, intLength) {
		while (strToPad.length < intLength) strToPad = strToPad.concat(chrPadding);
		return strToPad;
	};

	$.preloadImages = function () {
		for (var i = 0; i < arguments.length; i++) jQuery("<img>").attr("src", arguments[i]);
	};

	$.substringAfter = function (s1, s2) {
		return s1.substr(s1.indexOf(s2) + 1);
	};

	$.substringAfterLast = function (s1, s2) {
		return s1.substr(s1.lastIndexOf(s2) + 1);
	};

	$.substringBefore = function (s1, s2) {
		return s1.indexOf(s2) > -1 ? s1.substr(0, s1.indexOf(s2)) : s1;
	};

	$.substringBeforeLast = function (s1, s2) {
		return s1.indexOf(s2) > -1 ? s1.substr(0, s1.lastIndexOf(s2)) : s1;
	};

	$.toValidHtmlId = function (id) {
		if (/^[a-zA-Z]([a-zA-Z0-9\-_])*$/.test(id)) return id;
		id = id.replace(/[^a-zA-Z0-9\-_]+/g, "_");
		id = id.replace(/^_+/, "");
		return id;
	};

	$.urlEncode = function (value) {
		if (value == null) {
			return null;

		} else if (typeof value == "object") {
			var result = "";
			for (var key in value) result = result.concat(result.length > 0 ? "&" : "", key, "=", $.urlEncode(value[key]));
			return result;

		} else {
			var SAFECHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.!~*'()";
			var HEX = "0123456789abcdef";
			value = value.toString();

			var result = "";
			for (var i = 0; i < value.length; i++) {
				var c = value.charAt(i);
				if (c == " ") {
					result += "+";
				} else if (SAFECHARS.indexOf(c) != -1) {
					result += c;
				} else {
					var charCode = c.charCodeAt(0);
					if (charCode > 255) {
						result += "+";
					} else {
						result += "%";
						result += HEX.charAt((charCode >> 4) & 0xF);
						result += HEX.charAt(charCode & 0xF);
					}
				}
			}

			return result;
		}
	};

	$.urlDecode = function (value) {
		var HEXCHARS = "0123456789ABCDEFabcdef";

		var result = "";
		var i = 0;
		while (i < value.length) {
			var c = value.charAt(i);
			if (c == "+") {
				result += " ";
				i++;
			} else if (c == "%") {
				if (i < (value.length - 2) && HEXCHARS.indexOf(value.charAt(i + 1)) != -1 && HEXCHARS.indexOf(value.charAt(i + 2)) != -1) {
					result += unescape(value.substr(i, 3));
					i += 3;
				} else {
					result += "%[ERROR]";
					i++;
				}
			} else {
				result += c;
				i++;
			}
		}

		return result;
	};




	/* ---------- Validation  ---------- */

	$.validation = {

		alert: function (s) { window.alert(s); },
		autoFocus: true,
		element: null,

		setElement: function (el) {
			this.element = el;
			if (this.autoFocus && $(el).css("visibility") != "hidden") el.focus();
		},

		hasValue: function (id) {
			var el = document.getElementById(id);
			if ($.trim(el.value).length == 0) {
				this.alert("Please provide a value for the " + el.title + " field.");
				this.setElement(el);
				return false;
			}
			return true;
		},

		matchesRegex: function (id, regExp, errorMessage, allowEmpty) {
			var el = document.getElementById(id);
			if (allowEmpty && el.value.length == 0) return true;
			if (!regExp.test(el.value)) {
				this.alert(errorMessage ? errorMessage : "Please enter a valid value.");
				this.setElement(el);
				return false;
			}
			return true;
		},

		hasEquivalence: function (id1, id2) {
			var el1 = document.getElementById(id1);
			var el2 = document.getElementById(id2);
			if ($.trim(el1.value) != $.trim(el2.value)) {
				this.alert("The " + el1.title + " and " + el2.title + " field values must be equivalent.");
				this.setElement(el1);
				return false;
			}
			return true;
		},

		matchesEmail: function (id, allowEmpty) {
			var el = document.getElementById(id);
			if (allowEmpty && el.value.length == 0) return true;
			if (!this.isEmail(el.value)) {
				this.alert("Please enter a valid email address in the " + el.title + " field.");
				this.setElement(el);
				return false;
			}
			return true;
		},

		isEmail: function (s) {
			return /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(s);
		},

		matchesInteger: function (id, allowEmpty) {
			var el = document.getElementById(id);
			if (allowEmpty && el.value.length == 0) return true;
			if (!this.isInteger(el.value)) {
				this.alert("Please enter a valid integer value in the " + el.title + " field.");
				this.setElement(el);
				return false;
			}
			return true;
		},

		isInteger: function (s) {
			return /^-?\d+$/.test(s);
		},

		matchesDecimal: function (id, allowEmpty) {
			var el = document.getElementById(id);
			if (allowEmpty && el.value.length == 0) return true;
			if (!this.isDecimal(el.value)) {
				this.alert("Please enter a valid numeric value in the " + el.title + " field.");
				this.setElement(el);
				return false;
			}
			return true;
		},

		isDecimal: function (s) {
			return /^-?\d+(\.\d+)?$/.test(s);
		},

		matchesDate: function (id, formatString, allowEmpty) {
			var el = document.getElementById(id);
			if (allowEmpty && el.value.length == 0) return true;
			if (!$.dateTime.isValid(el.value, formatString)) {
				this.alert("Please enter a valid date value in the " + el.title + " field, in the format " + formatString + ".");
				this.setElement(el);
				return false;
			}
			return true;
		},

		matchesHexColor: function (id, allowEmpty) {
			var el = document.getElementById(id);
			if (allowEmpty && el.value.length == 0) return true;
			if (!this.isHexColor(el.value)) {
				this.alert("Please enter a valid hexadecimal colour value in the " + el.title + " field.");
				this.setElement(el);
				return false;
			}
			return true;
		},

		isHexColor: function (s) {
			return /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s);
		},

		hasSelection: function (id, allowEmpty, errorMessage) {
			var el = document.getElementById(id);

			if (el != null && el.nodeName.toLowerCase() == "select") { // if this is a dropdown selection
				// if (el.selectedIndex == 0 || (!allowEmpty && el.options[el.selectedIndex].value.length == 0)) {
				if (el.selectedIndex == 0 && el.options[el.selectedIndex].value.length == 0) {
					this.alert(errorMessage ? errorMessage : "A value must be selected for the " + el.title + " field.");
					this.setElement(el);
					return false;
				}
				return true;

			} else if (document.getElementsByName(id).length > 0) { // if this is a checkbox or radio button list
				var checked = false;
				var elArray = document.getElementsByName(id);
				for (var i = 0; i < elArray.length; i++) if (elArray[i].checked) {
					checked = true;
					break;
				}
				if (!checked) {
					this.alert(errorMessage ? errorMessage : "A value must be selected for the " + elArray[0].title + " field.");
					this.setElement(elArray[0]);
					return false;
				}
				return true;
			}
		},

		hasSelectionCount: function (name, minimum, maximum, errorMessage) {
			var jq = $("*[name=" + name + "]");
			var elFirst = jq.get(0);
			var count = jq.filter(":checked").length;
			if (minimum == 0 && maximum > 0 && count > maximum) {
				this.alert(errorMessage ? errorMessage : "A maximum of " + maximum + " item" + (maximum == 1 ? "" : "s") + " can be selected for the " + elFirst.title + " field.");
				this.setElement(elFirst);
				return false;
			} else if (minimum > 0 && maximum == 0 && count < minimum) {
				this.alert(errorMessage ? errorMessage : "A minimum of " + minimum + " item" + (minimum == 1 ? "" : "s") + " must be selected for the " + elFirst.title + " field.");
				this.setElement(elFirst);
				return false;
			} else if (minimum > 0 && maximum > 0 && (count < minimum || count > maximum)) {
				this.alert(errorMessage ? errorMessage : "Between " + minimum + " and " + maximum + " items must be selected for the " + elFirst.title + " field.");
				this.setElement(elFirst);
				return false;
			}
			return true;
		}

	};




	/* ---------- XML ---------- */

	$.createXmlDocument = function (xml) {
		var objXmlDocument = null;
		if (window.ActiveXObject) {
			objXmlDocument = new ActiveXObject("MSXML2.FreeThreadedDOMDocument.3.0");
			if (xml != null && typeof (xml) == "string") {
				objXmlDocument.async = false;
				objXmlDocument.loadXML(xml);
			}
		} else if (window.DOMParser) {
			objXmlDocument = (xml != null && typeof (xml) == "string") ? (new DOMParser()).parseFromString(xml, "text/xml") : document.implementation.createDocument("ns", "root", null);
		}
		return objXmlDocument;
	},

	$.convertXmlDocumentToString = function (objXmlDocument) {
		if (window.ActiveXObject) {
			return objXmlDocument.documentElement.xml;
		} else if (window.DOMParser) {
			return (new XMLSerializer()).serializeToString(objXmlDocument);
		}
		return "";
	},

	$.xslt = function (objXmlDocument, objXslDocument, params) {
		if (window.ActiveXObject) {
			var objXSLTemplate = new ActiveXObject("MSXML2.XSLTemplate.3.0");
			objXSLTemplate.stylesheet = objXslDocument;
			var objProcessor = objXSLTemplate.createProcessor();
			objProcessor.input = objXmlDocument;
			if (params != null && typeof (params) == "object") for (var key in params) objProcessor.addParameter(key, params[key]);
			objProcessor.transform();
			return objProcessor.output;
		} else if (window.XSLTProcessor) {
			var objProcessor = new XSLTProcessor();
			objProcessor.importStylesheet(objXslDocument);
			if (params != null && typeof (params) == "object") for (var key in params) objProcessor.setParameter("", key, params[key]);
			var objXmlTransformation = objProcessor.transformToDocument(objXmlDocument);
			return (new XMLSerializer()).serializeToString(objXmlTransformation);
		}
		return "";
	}

})(jQuery);
