function evenOddifyTableRows(tbody)
{
	console.warn("Deprecated use of global function evenOddifyTableRows(); use static method Lib24watchDomModifier.evenOddifyTableRows() instead");
	return Lib24watchDomModifier.evenOddifyTableRows(tbody);
}

function generateRandomString(minLength, maxLength, useUppercase, useLowercase, useNumbers, useSymbols)
{
	console.warn("Deprecated use of global function generateRandomString(); use static method String.generateRandom() instead");
	return String.generateRandom(minLength, maxLength, useUppercase, useLowercase, useNumbers, useSymbols);
}

function constrainDragDrop(draggable, usePositionedOffset)
{
	console.warn("Deprecated use of global function constrainDragDrop(); use instance method Draggable.constrain() instead");
	return draggable.constrain(usePositionedOffset);
}

function constrainDragStart(constrainToContainer, draggable, e, usePositionedOffset)
{
	console.warn("Deprecated use of global function constrainDragStart(); use instance method Draggable.constrainStart() instead");
	return draggable.constrainStart(constrainToContainer, e, usePositionedOffset);
}

// random integer in the given range (inclusive)
if (typeof Math.getRandomInt === "undefined")
{
	Math.getRandomInt = function(min, max)
	{
		return Math.floor(Math.random() * (max - min + 1)) + min;
	};
}

if (typeof String.generateRandom === "undefined")
{
	String.generateRandom = function(minLength, maxLength, useUppercase, useLowercase, useNumbers, useSymbols)
	{
		if (minLength <= 0 || maxLength <= 0 || minLength > maxLength)
		{
			throw "Not a valid length range";
		}

		if (!useUppercase && !useLowercase && !useNumbers && !useSymbols)
		{
			throw "Must allow at least one type of character";
		}

		var lowercase = "abcdefghijklmnopqrstuvwxyz";
		var uppercase = lowercase.toUpperCase();
		var numbers = "0123456789";
		var symbols = "!@#$%^&*()-=[]|<>?/~{},._+";

		var pool = "";
		if (useUppercase)
		{
			pool += uppercase;
		}

		if (useLowercase)
		{
			pool += lowercase;
		}

		if (useNumbers)
		{
			pool += numbers;
		}

		if (useSymbols)
		{
			pool += symbols;
		}

		var length = Math.getRandomInt(minLength, maxLength);
		var str = "";

		for (var i = 0; i < length; i++)
		{
			str += pool.charAt(Math.getRandomInt(0, pool.length - 1));
		}

		return str;
	};
}

// select an option based on its value
if (typeof Element !== "undefined")
{
	if (typeof Element.setSelectedValue === "undefined")
	{
		Element.addMethods(
			{
				setSelectedValue: function($super, value)
				{
					if ($super.nodeName.toLowerCase() !== "select")
					{
						throw "This method can only be called on <select> elements";
					}

					var options = $super.immediateDescendants();
					for (var i = 0; i < options.length; i++)
					{
						var option = options[i];

						if (option.nodeName.toLowerCase() === "option" && option.value === value.toString())
						{
							$super.selectedIndex = i;
							break;
						}
					}

					return $super;
				}
			}
		);
	}

	// swap two Prototype elements
	Element.addMethods(
		{
			swapWith: function(original, other)
			{
				var t = original.parentNode.insertBefore(document.createTextNode(''), original);
				other.parentNode.insertBefore(original, other);
				t.parentNode.insertBefore(other, t);
				t.parentNode.removeChild(t);

				return original;
			}
		}
	);
}

// randomize an array
if (typeof Array.prototype.shuffle === "undefined")
{
	Array.prototype.shuffle = function()
	{
		var shuffled = [];
		while (this.length > 0)
		{
			var i = Math.getRandomInt(0, this.length - 1);
			shuffled.push(this.splice(i, 1)[0]);
		}

		var elem;
		do
		{
			elem = shuffled.pop();
			if (typeof elem !== "undefined")
			{
				this.push(elem);
			}
		}
		while (typeof elem !== "undefined");

		return this;
	};
}

if (typeof Draggable !== "undefined")
{
	if (typeof Draggable.prototype.constrain === "undefined")
	{
		Draggable.prototype.constrain = function(usePositionedOffset)
		{
			var offset = (usePositionedOffset ? this.element.positionedOffset() : this.element.cumulativeOffset());
			var width = this.element.getWidth();
			var height = this.element.getHeight();

			var limit = this.containerCorners;

			var x = offset[0] - limit.topLeft[0];
			var y = offset[1] - limit.topLeft[1];

			var xUpperLimit = limit.width - width;
			var yUpperLimit = limit.height - height;

			if (x < 0)
			{
				x = 0;
			}
			else if (x >= xUpperLimit)
			{
				x = xUpperLimit;
			}

			if (y < 0)
			{
				y = 0;
			}
			else if (y >= yUpperLimit)
			{
				y = yUpperLimit;
			}

			this.element.setStyle(
				{
					top: (y - this.originalPositionedOffset[1] + limit.topLeft[1]) + "px",
					left: (x - this.originalPositionedOffset[0] + limit.topLeft[0]) + "px"
				}
			);
		};
	}

	if (typeof Draggable.prototype.constrainStart === "undefined")
	{
		Draggable.prototype.constrainStart = function(constrainToContainer, e, usePositionedOffset)
		{
			constrainToContainer = $(constrainToContainer);

			var offset = (usePositionedOffset ? constrainToContainer.positionedOffset() : constrainToContainer.cumulativeOffset());
			var width = constrainToContainer.getWidth();
			var height = constrainToContainer.getHeight();

			this.containerCorners =
			{
				topLeft: [offset[0], offset[1]],
				bottomRight: [offset[0] + width, offset[1] + height],
				width: width,
				height: height
			};

			if (typeof this.originalPositionedOffset === "undefined")
			{
				this.originalPositionedOffset = (usePositionedOffset ? this.element.positionedOffset() : this.element.cumulativeOffset());
			}
		};
	}
}

Object.extend(
	Event,
	{
		wheel: function (event)
		{
			var delta = 0;
			if (!event)
			{
				event = window.event;
			}

			if (event.wheelDelta)
			{
				delta = event.wheelDelta / 120; 
			}
			else if (event.detail)
			{
				delta = -event.detail / 3;
			}

			return Math.round(delta); // Safari Round
		}
	}
);

if (typeof console === "undefined" || (typeof console.warn === "undefined" && typeof console.log === "undefined"))
{
	var console =
	{
		useFakeConsole: false, // override in your own code

		debug: function(message)
		{
			console.insertMessage(message, "gray");
		},

		info: function(message)
		{
			console.insertMessage(message, "black");
		},

		warn: function(message)
		{
			console.insertMessage(message, "orange");
		},

		error: function(message)
		{
			console.insertMessage(message, "red");
		},

		log: function(message)
		{
			console.insertMessage(message, "blue");
		},

		insertMessage: function(message, color)
		{
			if (console.useFakeConsole)
			{
				console.getOutputElement().insert(
					new Element(
						"li"
					).setStyle(
						{
							color: color,
							padding: "2px 0px 2px 0px"
						}
					).update(message.escapeHTML())
				);
			}
		},

		getOutputElement: function()
		{
			var elem = $("debug-console");

			if (!elem)
			{
				elem = new Element(
					"ul",
					{
						id: "debug-console"
					}
				).setStyle(
					{
						listStyle: "none",
						margin: "0px",
						padding: "10px",
						backgroundColor: "white",
						color: "black",
						position: "absolute",
						zIndex: 100
					}
				);

				$$("body")[0].insert(
					{
						top: elem
					}
				);
			}

			return elem;
		}
	};

	window.console = console;
}

function initTinyMCE()
{
	// http://wiki.moxiecode.com/index.php/TinyMCE:Configuration
	var options = {
		mode: "specific_textareas",
		plugins: "paste" + (typeof tinyMceExtraPlugins !== "undefined" ? "," + tinyMceExtraPlugins : ""),
		editor_selector: /\s?tinymce\s?/,
		theme: "advanced",
		button_tile_map: true,
		theme_advanced_toolbar_location: "top",
		theme_advanced_toolbar_align: "left",
		theme_advanced_statusbar_location: "bottom",
		theme_advanced_buttons1: tinyMceButtons,
		theme_advanced_buttons2: "",
		theme_advanced_buttons3: "",
		theme_advanced_path: false,
		theme_advanced_resizing: true,
		apply_source_formatting: true,
		fix_list_elements: true,
		fix_table_elements: true,
		fix_nesting: true,
		relative_urls: false,
		content_css: "/css/lib24watch/tinymce_content.css"
	};

	if (typeof tinyMceExtraOptions !== "undefined")
	{
		options = $H(options).merge(tinyMceExtraOptions).toObject();
	}

	Event.fire(
		document,
		"lib24watch:beforeTinyMceInit",
		{
			tinyMce: tinyMCE,
			options: options
		}
	);

	tinyMCE.init(options);
	
	Event.fire(
		document,
		"lib24watch:afterTinyMceInit",
		{
			tinyMce: tinyMCE
		}
	);
}

Event.observe(
	window,
	"load",
	function()
	{
		if (typeof tinyMCE !== "undefined")
		{
			initTinyMCE();
		}
	}
);

/*
 * Copyright (c) 2009 Simo Kinnunen.
 * Licensed under the MIT license.
 *
 * @version 1.09i
 */
try
{
	var Cufon=(function(){var m=function(){return m.replace.apply(null,arguments)};var x=m.DOM={ready:(function(){var C=false,E={loaded:1,complete:1};var B=[],D=function(){if(C){return}C=true;for(var F;F=B.shift();F()){}};if(document.addEventListener){document.addEventListener("DOMContentLoaded",D,false);window.addEventListener("pageshow",D,false)}if(!window.opera&&document.readyState){(function(){E[document.readyState]?D():setTimeout(arguments.callee,10)})()}if(document.readyState&&document.createStyleSheet){(function(){try{document.body.doScroll("left");D()}catch(F){setTimeout(arguments.callee,1)}})()}q(window,"load",D);return function(F){if(!arguments.length){D()}else{C?F():B.push(F)}}})(),root:function(){return document.documentElement||document.body}};var n=m.CSS={Size:function(C,B){this.value=parseFloat(C);this.unit=String(C).match(/[a-z%]*$/)[0]||"px";this.convert=function(D){return D/B*this.value};this.convertFrom=function(D){return D/this.value*B};this.toString=function(){return this.value+this.unit}},addClass:function(C,B){var D=C.className;C.className=D+(D&&" ")+B;return C},color:j(function(C){var B={};B.color=C.replace(/^rgba\((.*?),\s*([\d.]+)\)/,function(E,D,F){B.opacity=parseFloat(F);return"rgb("+D+")"});return B}),fontStretch:j(function(B){if(typeof B=="number"){return B}if(/%$/.test(B)){return parseFloat(B)/100}return{"ultra-condensed":0.5,"extra-condensed":0.625,condensed:0.75,"semi-condensed":0.875,"semi-expanded":1.125,expanded:1.25,"extra-expanded":1.5,"ultra-expanded":2}[B]||1}),getStyle:function(C){var B=document.defaultView;if(B&&B.getComputedStyle){return new a(B.getComputedStyle(C,null))}if(C.currentStyle){return new a(C.currentStyle)}return new a(C.style)},gradient:j(function(F){var G={id:F,type:F.match(/^-([a-z]+)-gradient\(/)[1],stops:[]},C=F.substr(F.indexOf("(")).match(/([\d.]+=)?(#[a-f0-9]+|[a-z]+\(.*?\)|[a-z]+)/ig);for(var E=0,B=C.length,D;E<B;++E){D=C[E].split("=",2).reverse();G.stops.push([D[1]||E/(B-1),D[0]])}return G}),quotedList:j(function(E){var D=[],C=/\s*((["'])([\s\S]*?[^\\])\2|[^,]+)\s*/g,B;while(B=C.exec(E)){D.push(B[3]||B[1])}return D}),recognizesMedia:j(function(G){var E=document.createElement("style"),D,C,B;E.type="text/css";E.media=G;try{E.appendChild(document.createTextNode("/**/"))}catch(F){}C=g("head")[0];C.insertBefore(E,C.firstChild);D=(E.sheet||E.styleSheet);B=D&&!D.disabled;C.removeChild(E);return B}),removeClass:function(D,C){var B=RegExp("(?:^|\\s+)"+C+"(?=\\s|$)","g");D.className=D.className.replace(B,"");return D},supports:function(D,C){var B=document.createElement("span").style;if(B[D]===undefined){return false}B[D]=C;return B[D]===C},textAlign:function(E,D,B,C){if(D.get("textAlign")=="right"){if(B>0){E=" "+E}}else{if(B<C-1){E+=" "}}return E},textShadow:j(function(F){if(F=="none"){return null}var E=[],G={},B,C=0;var D=/(#[a-f0-9]+|[a-z]+\(.*?\)|[a-z]+)|(-?[\d.]+[a-z%]*)|,/ig;while(B=D.exec(F)){if(B[0]==","){E.push(G);G={};C=0}else{if(B[1]){G.color=B[1]}else{G[["offX","offY","blur"][C++]]=B[2]}}}E.push(G);return E}),textTransform:(function(){var B={uppercase:function(C){return C.toUpperCase()},lowercase:function(C){return C.toLowerCase()},capitalize:function(C){return C.replace(/\b./g,function(D){return D.toUpperCase()})}};return function(E,D){var C=B[D.get("textTransform")];return C?C(E):E}})(),whiteSpace:(function(){var D={inline:1,"inline-block":1,"run-in":1};var C=/^\s+/,B=/\s+$/;return function(H,F,G,E){if(E){if(E.nodeName.toLowerCase()=="br"){H=H.replace(C,"")}}if(D[F.get("display")]){return H}if(!G.previousSibling){H=H.replace(C,"")}if(!G.nextSibling){H=H.replace(B,"")}return H}})()};n.ready=(function(){var B=!n.recognizesMedia("all"),E=false;var D=[],H=function(){B=true;for(var K;K=D.shift();K()){}};var I=g("link"),J=g("style");function C(K){return K.disabled||G(K.sheet,K.media||"screen")}function G(M,P){if(!n.recognizesMedia(P||"all")){return true}if(!M||M.disabled){return false}try{var Q=M.cssRules,O;if(Q){search:for(var L=0,K=Q.length;O=Q[L],L<K;++L){switch(O.type){case 2:break;case 3:if(!G(O.styleSheet,O.media.mediaText)){return false}break;default:break search}}}}catch(N){}return true}function F(){if(document.createStyleSheet){return true}var L,K;for(K=0;L=I[K];++K){if(L.rel.toLowerCase()=="stylesheet"&&!C(L)){return false}}for(K=0;L=J[K];++K){if(!C(L)){return false}}return true}x.ready(function(){if(!E){E=n.getStyle(document.body).isUsable()}if(B||(E&&F())){H()}else{setTimeout(arguments.callee,10)}});return function(K){if(B){K()}else{D.push(K)}}})();function s(D){var C=this.face=D.face,B={"\u0020":1,"\u00a0":1,"\u3000":1};this.glyphs=D.glyphs;this.w=D.w;this.baseSize=parseInt(C["units-per-em"],10);this.family=C["font-family"].toLowerCase();this.weight=C["font-weight"];this.style=C["font-style"]||"normal";this.viewBox=(function(){var F=C.bbox.split(/\s+/);var E={minX:parseInt(F[0],10),minY:parseInt(F[1],10),maxX:parseInt(F[2],10),maxY:parseInt(F[3],10)};E.width=E.maxX-E.minX;E.height=E.maxY-E.minY;E.toString=function(){return[this.minX,this.minY,this.width,this.height].join(" ")};return E})();this.ascent=-parseInt(C.ascent,10);this.descent=-parseInt(C.descent,10);this.height=-this.ascent+this.descent;this.spacing=function(L,N,E){var O=this.glyphs,M,K,G,P=[],F=0,J=-1,I=-1,H;while(H=L[++J]){M=O[H]||this.missingGlyph;if(!M){continue}if(K){F-=G=K[H]||0;P[I]-=G}F+=P[++I]=~~(M.w||this.w)+N+(B[H]?E:0);K=M.k}P.total=F;return P}}function f(){var C={},B={oblique:"italic",italic:"oblique"};this.add=function(D){(C[D.style]||(C[D.style]={}))[D.weight]=D};this.get=function(H,I){var G=C[H]||C[B[H]]||C.normal||C.italic||C.oblique;if(!G){return null}I={normal:400,bold:700}[I]||parseInt(I,10);if(G[I]){return G[I]}var E={1:1,99:0}[I%100],K=[],F,D;if(E===undefined){E=I>400}if(I==500){I=400}for(var J in G){if(!k(G,J)){continue}J=parseInt(J,10);if(!F||J<F){F=J}if(!D||J>D){D=J}K.push(J)}if(I<F){I=F}if(I>D){I=D}K.sort(function(M,L){return(E?(M>=I&&L>=I)?M<L:M>L:(M<=I&&L<=I)?M>L:M<L)?-1:1});return G[K[0]]}}function r(){function D(F,G){if(F.contains){return F.contains(G)}return F.compareDocumentPosition(G)&16}function B(G){var F=G.relatedTarget;if(!F||D(this,F)){return}C(this,G.type=="mouseover")}function E(F){C(this,F.type=="mouseenter")}function C(F,G){setTimeout(function(){var H=d.get(F).options;m.replace(F,G?h(H,H.hover):H,true)},10)}this.attach=function(F){if(F.onmouseenter===undefined){q(F,"mouseover",B);q(F,"mouseout",B)}else{q(F,"mouseenter",E);q(F,"mouseleave",E)}}}function u(){var C=[],D={};function B(H){var E=[],G;for(var F=0;G=H[F];++F){E[F]=C[D[G]]}return E}this.add=function(F,E){D[F]=C.push(E)-1};this.repeat=function(){var E=arguments.length?B(arguments):C,F;for(var G=0;F=E[G++];){m.replace(F[0],F[1],true)}}}function A(){var D={},B=0;function C(E){return E.cufid||(E.cufid=++B)}this.get=function(E){var F=C(E);return D[F]||(D[F]={})}}function a(B){var D={},C={};this.extend=function(E){for(var F in E){if(k(E,F)){D[F]=E[F]}}return this};this.get=function(E){return D[E]!=undefined?D[E]:B[E]};this.getSize=function(F,E){return C[F]||(C[F]=new n.Size(this.get(F),E))};this.isUsable=function(){return !!B}}function q(C,B,D){if(C.addEventListener){C.addEventListener(B,D,false)}else{if(C.attachEvent){C.attachEvent("on"+B,function(){return D.call(C,window.event)})}}}function v(C,B){var D=d.get(C);if(D.options){return C}if(B.hover&&B.hoverables[C.nodeName.toLowerCase()]){b.attach(C)}D.options=B;return C}function j(B){var C={};return function(D){if(!k(C,D)){C[D]=B.apply(null,arguments)}return C[D]}}function c(F,E){var B=n.quotedList(E.get("fontFamily").toLowerCase()),D;for(var C=0;D=B[C];++C){if(i[D]){return i[D].get(E.get("fontStyle"),E.get("fontWeight"))}}return null}function g(B){return document.getElementsByTagName(B)}function k(C,B){return C.hasOwnProperty(B)}function h(){var C={},B,F;for(var E=0,D=arguments.length;B=arguments[E],E<D;++E){for(F in B){if(k(B,F)){C[F]=B[F]}}}return C}function o(E,M,C,N,F,D){var K=document.createDocumentFragment(),H;if(M===""){return K}var L=N.separate;var I=M.split(p[L]),B=(L=="words");if(B&&t){if(/^\s/.test(M)){I.unshift("")}if(/\s$/.test(M)){I.push("")}}for(var J=0,G=I.length;J<G;++J){H=z[N.engine](E,B?n.textAlign(I[J],C,J,G):I[J],C,N,F,D,J<G-1);if(H){K.appendChild(H)}}return K}function l(D,M){var C=D.nodeName.toLowerCase();if(M.ignore[C]){return}var E=!M.textless[C];var B=n.getStyle(v(D,M)).extend(M);var F=c(D,B),G,K,I,H,L,J;if(!F){return}for(G=D.firstChild;G;G=I){K=G.nodeType;I=G.nextSibling;if(E&&K==3){if(H){H.appendData(G.data);D.removeChild(G)}else{H=G}if(I){continue}}if(H){D.replaceChild(o(F,n.whiteSpace(H.data,B,H,J),B,M,G,D),H);H=null}if(K==1){if(G.firstChild){if(G.nodeName.toLowerCase()=="cufon"){z[M.engine](F,null,B,M,G,D)}else{arguments.callee(G,M)}}J=G}}}var t=" ".split(/\s+/).length==0;var d=new A();var b=new r();var y=new u();var e=false;var z={},i={},w={autoDetect:false,engine:null,forceHitArea:false,hover:false,hoverables:{a:true},ignore:{applet:1,canvas:1,col:1,colgroup:1,head:1,iframe:1,map:1,optgroup:1,option:1,script:1,select:1,style:1,textarea:1,title:1,pre:1},printable:true,selector:(window.Sizzle||(window.jQuery&&function(B){return jQuery(B)})||(window.dojo&&dojo.query)||(window.Ext&&Ext.query)||(window.YAHOO&&YAHOO.util&&YAHOO.util.Selector&&YAHOO.util.Selector.query)||(window.$$&&function(B){return $$(B)})||(window.$&&function(B){return $(B)})||(document.querySelectorAll&&function(B){return document.querySelectorAll(B)})||g),separate:"words",textless:{dl:1,html:1,ol:1,table:1,tbody:1,thead:1,tfoot:1,tr:1,ul:1},textShadow:"none"};var p={words:/\s/.test("\u00a0")?/[^\S\u00a0]+/:/\s+/,characters:"",none:/^/};m.now=function(){x.ready();return m};m.refresh=function(){y.repeat.apply(y,arguments);return m};m.registerEngine=function(C,B){if(!B){return m}z[C]=B;return m.set("engine",C)};m.registerFont=function(D){if(!D){return m}var B=new s(D),C=B.family;if(!i[C]){i[C]=new f()}i[C].add(B);return m.set("fontFamily",'"'+C+'"')};m.replace=function(D,C,B){C=h(w,C);if(!C.engine){return m}if(!e){n.addClass(x.root(),"cufon-active cufon-loading");n.ready(function(){n.addClass(n.removeClass(x.root(),"cufon-loading"),"cufon-ready")});e=true}if(C.hover){C.forceHitArea=true}if(C.autoDetect){delete C.fontFamily}if(typeof C.textShadow=="string"){C.textShadow=n.textShadow(C.textShadow)}if(typeof C.color=="string"&&/^-/.test(C.color)){C.textGradient=n.gradient(C.color)}else{delete C.textGradient}if(!B){y.add(D,arguments)}if(D.nodeType||typeof D=="string"){D=[D]}n.ready(function(){for(var F=0,E=D.length;F<E;++F){var G=D[F];if(typeof G=="string"){m.replace(C.selector(G),C,true)}else{l(G,C)}}});return m};m.set=function(B,C){w[B]=C;return m};return m})();Cufon.registerEngine("vml",(function(){var e=document.namespaces;if(!e){return}e.add("cvml","urn:schemas-microsoft-com:vml");e=null;var b=document.createElement("cvml:shape");b.style.behavior="url(#default#VML)";if(!b.coordsize){return}b=null;var h=(document.documentMode||0)<8;document.write(('<style type="text/css">cufoncanvas{text-indent:0;}@media screen{cvml\\:shape,cvml\\:rect,cvml\\:fill,cvml\\:shadow{behavior:url(#default#VML);display:block;antialias:true;position:absolute;}cufoncanvas{position:absolute;text-align:left;}cufon{display:inline-block;position:relative;vertical-align:'+(h?"middle":"text-bottom")+";}cufon cufontext{position:absolute;left:-10000in;font-size:1px;}a cufon{cursor:pointer}}@media print{cufon cufoncanvas{display:none;}}</style>").replace(/;/g,"!important;"));function c(i,j){return a(i,/(?:em|ex|%)$|^[a-z-]+$/i.test(j)?"1em":j)}function a(l,m){if(m==="0"){return 0}if(/px$/i.test(m)){return parseFloat(m)}var k=l.style.left,j=l.runtimeStyle.left;l.runtimeStyle.left=l.currentStyle.left;l.style.left=m.replace("%","em");var i=l.style.pixelLeft;l.style.left=k;l.runtimeStyle.left=j;return i}function f(l,k,j,n){var i="computed"+n,m=k[i];if(isNaN(m)){m=k.get(n);k[i]=m=(m=="normal")?0:~~j.convertFrom(a(l,m))}return m}var g={};function d(p){var q=p.id;if(!g[q]){var n=p.stops,o=document.createElement("cvml:fill"),i=[];o.type="gradient";o.angle=180;o.focus="0";o.method="sigma";o.color=n[0][1];for(var m=1,l=n.length-1;m<l;++m){i.push(n[m][0]*100+"% "+n[m][1])}o.colors=i.join(",");o.color2=n[l][1];g[q]=o}return g[q]}return function(ac,G,Y,C,K,ad,W){var n=(G===null);if(n){G=K.alt}var I=ac.viewBox;var p=Y.computedFontSize||(Y.computedFontSize=new Cufon.CSS.Size(c(ad,Y.get("fontSize"))+"px",ac.baseSize));var y,q;if(n){y=K;q=K.firstChild}else{y=document.createElement("cufon");y.className="cufon cufon-vml";y.alt=G;q=document.createElement("cufoncanvas");y.appendChild(q);if(C.printable){var Z=document.createElement("cufontext");Z.appendChild(document.createTextNode(G));y.appendChild(Z)}if(!W){y.appendChild(document.createElement("cvml:shape"))}}var ai=y.style;var R=q.style;var l=p.convert(I.height),af=Math.ceil(l);var V=af/l;var P=V*Cufon.CSS.fontStretch(Y.get("fontStretch"));var U=I.minX,T=I.minY;R.height=af;R.top=Math.round(p.convert(T-ac.ascent));R.left=Math.round(p.convert(U));ai.height=p.convert(ac.height)+"px";var F=Y.get("color");var ag=Cufon.CSS.textTransform(G,Y).split("");var L=ac.spacing(ag,f(ad,Y,p,"letterSpacing"),f(ad,Y,p,"wordSpacing"));if(!L.length){return null}var k=L.total;var x=-U+k+(I.width-L[L.length-1]);var ah=p.convert(x*P),X=Math.round(ah);var O=x+","+I.height,m;var J="r"+O+"ns";var u=C.textGradient&&d(C.textGradient);var o=ac.glyphs,S=0;var H=C.textShadow;var ab=-1,aa=0,w;while(w=ag[++ab]){var D=o[ag[ab]]||ac.missingGlyph,v;if(!D){continue}if(n){v=q.childNodes[aa];while(v.firstChild){v.removeChild(v.firstChild)}}else{v=document.createElement("cvml:shape");q.appendChild(v)}v.stroked="f";v.coordsize=O;v.coordorigin=m=(U-S)+","+T;v.path=(D.d?"m"+D.d+"xe":"")+"m"+m+J;v.fillcolor=F;if(u){v.appendChild(u.cloneNode(false))}var ae=v.style;ae.width=X;ae.height=af;if(H){var s=H[0],r=H[1];var B=Cufon.CSS.color(s.color),z;var N=document.createElement("cvml:shadow");N.on="t";N.color=B.color;N.offset=s.offX+","+s.offY;if(r){z=Cufon.CSS.color(r.color);N.type="double";N.color2=z.color;N.offset2=r.offX+","+r.offY}N.opacity=B.opacity||(z&&z.opacity)||1;v.appendChild(N)}S+=L[aa++]}var M=v.nextSibling,t,A;if(C.forceHitArea){if(!M){M=document.createElement("cvml:rect");M.stroked="f";M.className="cufon-vml-cover";t=document.createElement("cvml:fill");t.opacity=0;M.appendChild(t);q.appendChild(M)}A=M.style;A.width=X;A.height=af}else{if(M){q.removeChild(M)}}ai.width=Math.max(Math.ceil(p.convert(k*P)),0);if(h){var Q=Y.computedYAdjust;if(Q===undefined){var E=Y.get("lineHeight");if(E=="normal"){E="1em"}else{if(!isNaN(E)){E+="em"}}Y.computedYAdjust=Q=0.5*(a(ad,E)-parseFloat(ai.height))}if(Q){ai.marginTop=Math.ceil(Q)+"px";ai.marginBottom=Q+"px"}}return y}})());Cufon.registerEngine("canvas",(function(){var b=document.createElement("canvas");if(!b||!b.getContext||!b.getContext.apply){return}b=null;var a=Cufon.CSS.supports("display","inline-block");var e=!a&&(document.compatMode=="BackCompat"||/frameset|transitional/i.test(document.doctype.publicId));var f=document.createElement("style");f.type="text/css";f.appendChild(document.createTextNode(("cufon{text-indent:0;}@media screen,projection{cufon{display:inline;display:inline-block;position:relative;vertical-align:middle;"+(e?"":"font-size:1px;line-height:1px;")+"}cufon cufontext{display:-moz-inline-box;display:inline-block;width:0;height:0;overflow:hidden;text-indent:-10000in;}"+(a?"cufon canvas{position:relative;}":"cufon canvas{position:absolute;}")+"}@media print{cufon{padding:0;}cufon canvas{display:none;}}").replace(/;/g,"!important;")));document.getElementsByTagName("head")[0].appendChild(f);function d(p,h){var n=0,m=0;var g=[],o=/([mrvxe])([^a-z]*)/g,k;generate:for(var j=0;k=o.exec(p);++j){var l=k[2].split(",");switch(k[1]){case"v":g[j]={m:"bezierCurveTo",a:[n+~~l[0],m+~~l[1],n+~~l[2],m+~~l[3],n+=~~l[4],m+=~~l[5]]};break;case"r":g[j]={m:"lineTo",a:[n+=~~l[0],m+=~~l[1]]};break;case"m":g[j]={m:"moveTo",a:[n=~~l[0],m=~~l[1]]};break;case"x":g[j]={m:"closePath"};break;case"e":break generate}h[g[j].m].apply(h,g[j].a)}return g}function c(m,k){for(var j=0,h=m.length;j<h;++j){var g=m[j];k[g.m].apply(k,g.a)}}return function(V,w,P,t,C,W){var k=(w===null);if(k){w=C.getAttribute("alt")}var A=V.viewBox;var m=P.getSize("fontSize",V.baseSize);var B=0,O=0,N=0,u=0;var z=t.textShadow,L=[];if(z){for(var U=z.length;U--;){var F=z[U];var K=m.convertFrom(parseFloat(F.offX));var I=m.convertFrom(parseFloat(F.offY));L[U]=[K,I];if(I<B){B=I}if(K>O){O=K}if(I>N){N=I}if(K<u){u=K}}}var Z=Cufon.CSS.textTransform(w,P).split("");var E=V.spacing(Z,~~m.convertFrom(parseFloat(P.get("letterSpacing"))||0),~~m.convertFrom(parseFloat(P.get("wordSpacing"))||0));if(!E.length){return null}var h=E.total;O+=A.width-E[E.length-1];u+=A.minX;var s,n;if(k){s=C;n=C.firstChild}else{s=document.createElement("cufon");s.className="cufon cufon-canvas";s.setAttribute("alt",w);n=document.createElement("canvas");s.appendChild(n);if(t.printable){var S=document.createElement("cufontext");S.appendChild(document.createTextNode(w));s.appendChild(S)}}var aa=s.style;var H=n.style;var j=m.convert(A.height);var Y=Math.ceil(j);var M=Y/j;var G=M*Cufon.CSS.fontStretch(P.get("fontStretch"));var J=h*G;var Q=Math.ceil(m.convert(J+O-u));var o=Math.ceil(m.convert(A.height-B+N));n.width=Q;n.height=o;H.width=Q+"px";H.height=o+"px";B+=A.minY;H.top=Math.round(m.convert(B-V.ascent))+"px";H.left=Math.round(m.convert(u))+"px";var r=Math.max(Math.ceil(m.convert(J)),0)+"px";if(a){aa.width=r;aa.height=m.convert(V.height)+"px"}else{aa.paddingLeft=r;aa.paddingBottom=(m.convert(V.height)-1)+"px"}var X=n.getContext("2d"),D=j/A.height;X.scale(D,D*M);X.translate(-u,-B);X.save();function T(){var x=V.glyphs,ab,l=-1,g=-1,y;X.scale(G,1);while(y=Z[++l]){var ab=x[Z[l]]||V.missingGlyph;if(!ab){continue}if(ab.d){X.beginPath();if(ab.code){c(ab.code,X)}else{ab.code=d("m"+ab.d,X)}X.fill()}X.translate(E[++g],0)}X.restore()}if(z){for(var U=z.length;U--;){var F=z[U];X.save();X.fillStyle=F.color;X.translate.apply(X,L[U]);T()}}var q=t.textGradient;if(q){var v=q.stops,p=X.createLinearGradient(0,A.minY,0,A.maxY);for(var U=0,R=v.length;U<R;++U){p.addColorStop.apply(p,v[U])}X.fillStyle=p}else{X.fillStyle=P.get("color")}T();return s}})());
}
catch (e)
{
	console.warn("Cufon failure: " + e);
}

var Lib24watchDomModifier =
{
	bodyTag: null
};

Lib24watchDomModifier.addCss = function(url)
{
	var link = new Element(
		"link",
		{
			rel: "stylesheet",
			type: "text/css",
			href: url
		}
	);

	var body = Lib24watchDomModifier.getRoot();
	body.up().down("head").insert(link);
};

Lib24watchDomModifier.getRoot = function()
{
	if (Lib24watchDomModifier.bodyTag === null)
	{
		Lib24watchDomModifier.bodyTag = $(document.body);
	}

	return Lib24watchDomModifier.bodyTag;
};

Lib24watchDomModifier.applyMoneyFields = function()
{
	$$("div.input.money input[type=\"text\"]").each(
		function(input)
		{
			input.insert(
				{
					before: new Element("span").update("$")
				}
			);
		}
	);
};

Lib24watchDomModifier.classifyTableCells = function()
{
	var cells = $$("td, th");
	
	if (cells)
	{
		cells.each(
			function(cell)
			{
				var isCheckboxCell = false;
				var isActionCell = false;

				for (var i = 0; i < cell.childNodes.length; i++)
				{
					var child = Element.extend(cell.childNodes[i]);

					if (child.nodeType == 3)
					{
						if (child.nodeValue.strip() === "")
						{
							continue;
						}
						else
						{
							isCheckboxCell = false;
							isActionCell = false;
							break;
						}
					}

					if (child.nodeName && child.nodeName.toLowerCase() === "input")
					{
						if (child.getAttribute("type") === "hidden")
						{
							continue;
						}
						else if (child.getAttribute("type") === "checkbox")
						{
							isCheckboxCell = true;
							continue;
						}
					}
					else if (child.nodeName && child.nodeName.toLowerCase() === "a" && child.hasClassName("action"))
					{
						isActionCell = true;
						continue;
					}
					else
					{
						isCheckboxCell = false;
						isActionCell = false;
						break;
					}
				}

				if (isCheckboxCell)
				{
					cell.addClassName("checkbox");
				}
				else if (isActionCell)
				{
					cell.addClassName("action");
				}
			}
		);
	}
};

Lib24watchDomModifier.evenOddifyTableRows = function(tbody)
{
	var rows = tbody.immediateDescendants();

	for (var i = 0; i < rows.length; i++)
	{
		rows[i].addClassName(i % 2 == 0 ? "odd" : "even");
		rows[i].removeClassName(i % 2 == 0 ? "even" : "odd");
	}
};

Lib24watchDomModifier.relExternalInNewWindow = function(root)
{
	if (typeof root === "undefined")
	{
		root = Lib24watchDomModifier.getRoot();
	}

	root.select("a[rel~=\"external\"]").each(
		function(a)
		{
			a.setAttribute("target", "_blank");
		}
	);
};

Lib24watchDomModifier.evenOddifyAllTableRows = function()
{
	$$("table:not(.no-evenoddify) tbody").each(Lib24watchDomModifier.evenOddifyTableRows);
};

Lib24watchDomModifier.applyColumnCheckboxes = function(root)
{
	if (typeof root === "undefined")
	{
		root = Lib24watchDomModifier.getRoot();
	}

	var eventTypes = ["click", "keypress"];

	root.select("thead input[type=\"checkbox\"]").each(
		function(input)
		{
			var listener = function(e)
			{
				e.element().up("table").select("tbody input[type=\"checkbox\"]").each(
					function(innerInput)
					{
						innerInput.checked = e.element().checked;
					}
				);
			};

			eventTypes.each(
				function(eventType)
				{
					input.observe(eventType, listener);
				}
			);
		}
	);

	root.select("tbody input[type=\"checkbox\"]").each(
		function(input)
		{
			var listener = function(e)
			{
				var elems = e.element().up("table").select("tbody input[type=\"checkbox\"]");
				var checkedCount = 0;
				elems.each(
					function(innerInput)
					{
						if (innerInput.checked)
						{
							checkedCount++;
						}
					}
				);

				var checkbox = e.element().up("table").down("thead input[type=\"checkbox\"]");
				if (checkbox)
				{
					checkbox.checked = (elems.length == checkedCount);
				}
			};
			eventTypes.each(
				function(eventType)
				{
					input.observe(eventType, listener);
				}
			);
		}
	);
};

Lib24watchDomModifier.applyPasswordStrengthMeter = function(span)
{
	if (typeof span === "undefined")
	{
		$$("span.password-strength").each(Lib24watchDomModifier.applyPasswordStrengthMeter);
	}
	else
	{
		span.setStyle(
			{
				display: "none"
			}
		);

		var input = span.up("div.input").down("input[type=\"password\"]");

		input.observe(
			"keyup",
			function()
			{
				if (input.value.length == 0)
				{
					span.setStyle(
						{
							display: "none"
						}
					);
				}
				else
				{
					span.setStyle(
						{
							display: ""
						}
					);

					if (/^[a-zA-Z]+$/.match(input.value))
					{
						strength = "worst";
					}
					else if (/^[a-zA-Z0-9]{,6}$/.match(input.value))
					{
						strength = "bad";
					}
					else if (input.value.length < 8 || /^[a-zA-Z0-9]+$/.match(input.value))
					{
						strength = "good";
					}
					else
					{
						strength = "best";
					}

					span.removeClassName("worst");
					span.removeClassName("bad");
					span.removeClassName("good");
					span.removeClassName("best");

					span.addClassName(strength);

					switch (strength)
					{
						case "worst":
							span.update("Very poor password strength");
							break;

						case "bad":
							span.update("Poor password strength");
							break;

						case "good":
							span.update("Average password strength");
							break;

						case "best":
							span.update("Best password strength");
							break;
					}
				}
			}
		);
	}
};

var Callout = Class.create(
	{
		initialize: function(forElem, width, closeOnBodyClick)
		{
			this.forElem = $(forElem);
			if (typeof this.forElem === "undefined")
			{
				throw "Couldn't find reference element for this callout";
			}

			if (!Callout.styleLoaded)
			{
				Lib24watchDomModifier.addCss("/css/lib24watch/callout.css");
				Callout.styleLoaded = true;
			}

			if (typeof closeOnBodyClick === "undefined")
			{
				closeOnBodyClick = true;
			}

			this.width = width;
			this.div = null;
			this.isInserted = false;
			this.closeOnBodyClick = closeOnBodyClick;
				
			this.onBodyClickedHandler = null;

			this.initDom();

			Callout.instances.push(this);

			if (this.closeOnBodyClick)
			{
				this.onBodyClickedHandler = this.onBodyClicked.bind(this);
				$(document.body).up().observe("mousedown", this.onBodyClickedHandler);
			}
		},

		onBodyClicked: function(e)
		{
			var elem = Event.findElement(e);

			var closeCallout = true;

			while (elem !== null)
			{
				if (elem === this.div)
				{
					closeCallout = false;
					break;
				}

				elem = elem.parentNode;
			}

			if (closeCallout)
			{
				this.close();
			}
		},

		initDom: function()
		{
			if (this.div !== null && typeof this.div !== "undefined")
			{
				this.div.remove();
			}

			this.div = this.buildDom();
			this.contentDiv = this.div.down("div.content");

			if (typeof this.width !== "undefined")
			{
				this.setWidth(this.width);
			}
		},

		setPosition: function()
		{
			var offset = this.forElem.cumulativeOffset();
			var centerOffset = (this.forElem.getWidth() / 2) - (this.div.getWidth() / 2);

			this.div.setStyle(
				{
					top: (offset.top + this.forElem.getHeight()) + "px",
					left: (offset.left + centerOffset) + "px"
				}
			);
		},

		show: function()
		{
			this.closeOthers();

			if (!this.isInserted)
			{
				$$("body")[0].insert(this.div);
				this.setPosition();

				this.isInserted = true;
			}

			new Effect.Appear(
				this.div,
				{
					duration: 0.3
				}
			);
		},

		hide: function()
		{
			new Effect.Fade(
				this.div,
				{
					duration: 0.3
				}
			);
		},

		close: function()
		{
			if (this.div !== null)
			{
				this.div.remove();
				this.div = null;
				this.isInserted = false;

				if (this.onBodyClickedHandler)
				{
					Event.stopObserving($(document.body), "click", this.onBodyClickedHandler);
				}
			}
		},

		closeOthers: function()
		{
			for (var i = 0; i < Callout.instances.length; i++)
			{
				if (Callout.instances[i] !== this)
				{
					Callout.instances[i].close();
				}
			}
		},

		getContentArea: function()
		{
			return this.contentDiv;
		},

		update: function(content)
		{
			this.contentDiv.update(content);
		},

		setWidth: function(width)
		{
			if (!isNaN(width))
			{
				width = width + "px";
			}

			this.div.setStyle(
				{
					width: width
				}
			);
		},

		buildDom: function()
		{
			var div = new Element("div").addClassName("callout");
			var top = new Element("span").addClassName("top");
			div.insert(top);

			["top", "bottom"].each(
				function(vertical)
				{
					var elem = new Element("div").addClassName(vertical);

					["left", "right"].each(
						function(horizontal)
						{
							var span = new Element("span").addClassName(horizontal);
							elem.insert(span);
						}
					);

					div.insert(elem);
				}
			);

			var contentDiv = new Element("div").addClassName("content");
			div.insert(contentDiv);

			div.setStyle(
				{
					display: "none"
				}
			);

			return div;
		}
	}
);

Callout.close = function(callout)
{
	callout.close();
};

Callout.instances = [];

Callout.closeAll = function()
{
	Callout.instances.each(
		function(callout)
		{
			callout.close();
		}
	);
};

Callout.styleLoaded = false;

var SpinnerInput = Class.create(
	{
		initialize: function(legalValues, toReplace)
		{
			if (!legalValues || typeof legalValues.length === "undefined" || legalValues.length === 0)
			{
				throw "Legal values must be an array with at least one element";
			}

			this.legalValues = legalValues;

			this.spinner = this.createDom();
			toReplace.replace(this.spinner);

			this.onChangeListeners = [];

			this.setSelectedIndex(0);
		},
		
		addClassName: function(className)
		{
			this.spinner.addClassName(className);
			return this;
		},

		removeClassName: function(className)
		{
			this.spinner.removeClassName(className);
			return this;
		},

		addOnChangeListener: function(listener)
		{
			if (typeof listener !== "function")
			{
				throw "Provided listener isn't a function";
			}

			this.onChangeListeners.push(listener);
			return this;
		},

		removeOnChangeListener: function(listener)
		{
			for (var i = this.onChangeListeners.length - 1; i >= 0; i--)
			{
				if (this.onChangeListeners[i] === listener)
				{
					delete this.onChangeListeners[i];
				}
			}

			return this;
		},

		notifyOnChangeListeners: function(value, index)
		{
			for (var i = 0; i < this.onChangeListeners.length; i++)
			{
				this.onChangeListeners[i](value, index, this);
			}
		},

		getValue: function()
		{
			return this.legalValues[this.index];
		},

		nextValue: function()
		{
			this.setSelectedIndex(this.index + 1);
		},

		previousValue: function()
		{
			this.setSelectedIndex(this.index - 1);
		},

		onMouseWheel: function(e)
		{
			e.stop();

			if (Event.wheel(e) < 0)
			{
				this.previousValue();
			}
			else
			{
				this.nextValue();
			}
		},

		createDom: function()
		{
			var spinner = new Element("span").addClassName("spinner-input");
			
			spinner.observe("DOMMouseScroll", this.onMouseWheel.bind(this));
			spinner.observe("mousewheel", this.onMouseWheel.bind(this));

			this.currentValueSpan = new Element("span").addClassName("current-value");

			var upButton = new Element(
				"a",
				{
					href: "javascript:void(0)"
				}
			).addClassName("up").update("Next");
			upButton.observe("click", this.nextValue.bind(this));

			var downButton = new Element(
				"a",
				{
					href: "javascript:void(0)"
				}
			).addClassName("down").update("Previous");
			downButton.observe("click", this.previousValue.bind(this));

			spinner.insert(this.currentValueSpan);
			spinner.insert(downButton);
			spinner.insert(upButton);

			return spinner;
		},

		setSelectedValue: function(value)
		{
			for (var i = 0; i < this.legalValues.length; i++)
			{
				if (value == this.legalValues[i])
				{
					this.setSelectedIndex(i);
					return this;
				}
			}

			throw "Value \"" + value + "\" is not a legal value for this spinner input";
		},

		setSelectedIndex: function(index)
		{
			if (index < 0)
			{
				index = this.legalValues.length - 1;
			}
			else if (index >= this.legalValues.length)
			{
				index = 0;
			}

			this.index = index;

			this.currentValueSpan.update(this.legalValues[this.index].toString().escapeHTML());
			this.notifyOnChangeListeners(this.legalValues[this.index], this.index);

			return this;
		}
	}
);

var DateTimePicker = Class.create(
	{
		initialize: function(cakeDiv)
		{
			if (typeof DateTimePicker.localizedStrings === "undefined")
			{
				throw "No localized strings available; they should have been set in 24Watch's head.ctp element";
			}

			var cakeDiv = $(cakeDiv);
			if (typeof cakeDiv === "undefined")
			{
				throw "Couldn't find element to bind to for this date/time picker";
			}

			var existing = cakeDiv.retrieve("dateTimePickerInstance");
			if (existing)
			{
				return existing;
			}

			if (cakeDiv.nodeName.toLowerCase() !== "div")
			{
				throw "Passed element isn't a <div>; it's a <" + cakeDiv.nodeName + ">";
			}

			if (!cakeDiv.hasClassName("input"))
			{
				throw "Passed <div> doesn't have a class of \"input\"";
			}

			if (!cakeDiv.hasClassName("date") && !cakeDiv.hasClassName("time") && !cakeDiv.hasClassName("datetime"))
			{
				throw "Passed <div> doesn't have a class of \"date\", \"time\" or \"datetime\"";
			}

			var name = cakeDiv.down("select").getAttribute("name");
			var match = /(.*)\[.*?\]$/.exec(name);
			if (!match)
			{
				throw "Couldn't determine base name for CakePHP field";
			}

			if (!DateTimePicker.styleLoaded)
			{
				Lib24watchDomModifier.addCss("/css/lib24watch/date-time-picker.css");
				DateTimePicker.styleLoaded = true;
			}

			this.name = match[1];
			this.cakeDiv = cakeDiv;
			this.dateTime = null;

			this.dateInput = null;
			this.timeInput = null;

			this.activeCallout = null;

			this.changeListeners = [];

			this.refactorMarkupOnInit();

			cakeDiv.store("dateTimePickerInstance", this);
		},

		getDateTimeFromInputs: function()
		{
			var now = new Date();
			var dateTime = new Date();

			if (this.dateInput)
			{
				var value = $F(this.dateInput).strip();

				if (value.length > 0)
				{
					try
					{
						var date = new Date(value);
						dateTime.setFullYear(date.getFullYear());
						dateTime.setMonth(date.getMonth());
						dateTime.setDate(date.getDate());
					}
					catch (e)
					{
						dateTime.setFullYear(now.getFullYear());
						dateTime.setMonth(now.getMonth());
						dateTime.setDate(now.getDate());
					}
				}
				else
				{
					dateTime = null;
				}
			}

			if (this.timeInput && dateTime !== null)
			{
				var value = $F(this.timeInput).strip();

				if (value.length > 0)
				{
					value = dateTime.getFullYear() + "/" + (dateTime.getMonth() + 1) + "/" + dateTime.getDate() + " " + value;

					try
					{
						var date = new Date(value);

						dateTime.setHours(date.getHours());
						dateTime.setMinutes(date.getMinutes());
					}
					catch (e)
					{
						dateTime.setHours(now.getHours());
						dateTime.setMinutes(now.getMinutes());
					}
				}
			}

			if (dateTime !== null && isNaN(dateTime.getFullYear()))
			{
				dateTime = now;
			}

			return dateTime;
		},

		getDateTimeFromSelects: function()
		{
			var date = new Date();
			date.setSeconds(0);

			var hours = date.getHours();
			var isPm = false;

			this.cakeDiv.select("select").each(
				function(select)
				{
					if (select.getValue() === "")
					{
						date = null;
						throw $break;
					}

					var match = /\[([a-z]+)\]$/.exec(select.getAttribute("name"));
					if (match)
					{
						var value = select.getValue().replace(/^0*(.*)/, "$1");
						if (value === "")
						{
							value = 0;
						}
						else
						{
							value = parseInt(value);
						}

						switch (match[1])
						{
							case "month":
								date.setMonth(value - 1);
								break;

							case "day":
								date.setDate(value);
								break;

							case "year":
								date.setFullYear(value);
								break;

							case "hour":
								hours = parseInt(value);
								break;

							case "min":
								date.setMinutes(value);
								break;

							case "meridian":
								isPm = select.getValue() === "pm";
								break;

							default:
								console.warn("Unrecognized select base name " + match[1]);
								break;
						}
					}
					else
					{
						console.warn("Ignoring apparently non-CakePHP-style select name " + select.getAttribute("name"));
					}
				}
			);

			if (date !== null)
			{
				if (isPm && hours < 12)
				{
					hours += 12;
				}
				else if (!isPm && hours === 12)
				{
					hours = 0;
				}

				date.setHours(hours);
			}

			return date;
		},

		removeSelects: function()
		{
			var parentNode = this.cakeDiv;
			var nodes = parentNode.childNodes;

			var foundSelect = false;
			var toDelete = [];

			for (var i = 0; i < nodes.length; i++)
			{
				var node = nodes[i];

				if (node.nodeType === 1)
				{
					if (node.nodeName.toLowerCase() === "select")
					{
						foundSelect = true;
						toDelete.push(node);
					}
					else if (foundSelect)
					{
						break;
					}
				}
				else if (foundSelect && node.nodeType === 3)
				{
					toDelete.push(node);
				}
			}

			toDelete.each(
				function(node)
				{
					node.parentNode.removeChild(node);
				}
			);
		},

		refactorMarkupOnInit: function()
		{
			this.allowEmpty = typeof this.cakeDiv.down("option[value=\"\"]") !== "undefined";

			var dateTime = this.getDateTimeFromSelects();
			this.removeSelects();
			
			var label = this.cakeDiv.down("label");
			var toInsert = [];

			if (this.cakeDiv.hasClassName("date") || this.cakeDiv.hasClassName("datetime"))
			{
				this.dateInput = this.createDateInput();
				toInsert.push(this.dateInput);
			}

			if (this.cakeDiv.hasClassName("time") || this.cakeDiv.hasClassName("datetime"))
			{
				this.timeInput = this.createTimeInput();
				toInsert.push(this.timeInput);
			}

			if (label)
			{
				toInsert.reverse();
			}

			for (var i = 0; i < toInsert.length; i++)
			{
				if (label)
				{
					label.insert(
						{
							after: toInsert[i]
						}
					);
				}
				else
				{
					this.cakeDiv.insert(toInsert[i]);
				}
			}

			this.addHiddenInputs();

			this.setDateTime(dateTime);
		},

		addHiddenInputs: function()
		{
			this.cakeDiv.select("input[type=\"hidden\"]").each(Element.remove);

			var inputs = new Hash();

			if (this.cakeDiv.hasClassName("date") || this.cakeDiv.hasClassName("datetime"))
			{
				if (this.dateTime === null)
				{
					inputs.set("day", "");
					inputs.set("month", "");
					inputs.set("year", "");
				}
				else
				{
					inputs.set("day", this.dateTime.getDate());
					inputs.set("month", this.dateTime.getMonth() + 1);
					inputs.set("year", this.dateTime.getFullYear());
				}
			}

			if (this.cakeDiv.hasClassName("time") || this.cakeDiv.hasClassName("datetime"))
			{
				if (this.dateTime === null)
				{
					inputs.set("hour", "");
					inputs.set("min", "");
					inputs.set("meridian", "");
				}
				else
				{
					inputs.set("meridian", "am");

					var hour = this.dateTime.getHours();
					if (hour >= 12)
					{
						inputs.set("meridian", "pm");
						if (hour > 12)
						{
							hour -= 12;
						}
					}
					else if (hour === 0)
					{
						hour = 12;
					}

					inputs.set("hour", hour);
					inputs.set("min", this.dateTime.getMinutes());
				}
			}

			inputs.each(
				function(pair)
				{
					var input = new Element(
						"input",
						{
							name: this.name + "[" + pair.key + "]",
							type: "hidden"
						}
					).setValue(pair.value);

					this.cakeDiv.insert(input);
				}.bind(this)
			);
		},
		
		nextMonth: function(dateTime, calendarDiv)
		{
			this.updateMonth(dateTime, calendarDiv, true);
		},

		previousMonth: function(dateTime, calendarDiv)
		{
			this.updateMonth(dateTime, calendarDiv, false);
		},

		updateMonth: function(dateTime, calendarDiv, isNext)
		{
			var newMonth = dateTime.getMonth() + (isNext ? 1 : -1);
			var newYear = dateTime.getFullYear();

			if (newMonth >= 12)
			{
				newMonth = 0;
				newYear++;
			}
			else if (newMonth < 0)
			{
				newMonth = 11;
				newYear--;
			}

			dateTime.setMonth(newMonth);
			dateTime.setFullYear(newYear);

			var newCalendarDiv = this.createCalendarTable(dateTime);

			calendarDiv.up("div.content").update(newCalendarDiv);
		},

		getShortMonthName: function(month)
		{
			return DateTimePicker.localizedStrings.monthNames[month];
		},

		getShortDayName: function(day)
		{
			return DateTimePicker.localizedStrings.shortDayNames[day];
		},

		createCalendarTable: function(dateTime)
		{
			var calendarDiv = new Element("div").addClassName("date-picker");

			var table = new Element("table").addClassName("date-picker");

			var now = new Date();
			
			if (dateTime === null)
			{
				dateTime = now;
			}

			var previousMonth = new Date(dateTime.getFullYear(), dateTime.getMonth() - 1, 1, 0, 0, 0, 0);
			var previous = this.getShortMonthName(previousMonth.getMonth());
			if (previousMonth.getFullYear() !== dateTime.getFullYear())
			{
				previous += " " + previousMonth.getFullYear();
			}

			var previousLink;

			if (this.bounding === DateTimePicker.TODAY_OR_LATER &&
					(dateTime.getFullYear() < now.getFullYear() || (dateTime.getFullYear() === now.getFullYear() && dateTime.getMonth() <= now.getMonth())))
			{
				previousLink = new Element(
					"span",
					{
						"class": "disabled previous"
					}
				).update(previous);
			}
			else
			{
				previousLink = new Element(
					"a",
					{
						href: "javascript:void(0)",
						"class": "previous"
					}
				).update(previous);

				previousLink.observe(
					"click",
					function()
					{
						this.previousMonth(dateTime, calendarDiv);
					}.bind(this)
				);
			}

			var nextMonth = new Date(dateTime.getFullYear(), dateTime.getMonth() + 1, 1, 0, 0, 0, 0);
			var next = this.getShortMonthName(nextMonth.getMonth());
			if (nextMonth.getFullYear() !== dateTime.getFullYear())
			{
				next += " " + nextMonth.getFullYear();
			}

			var nextLink;
			
			if (this.bounding === DateTimePicker.TODAY_OR_EARLIER &&
					(dateTime.getFullYear() > now.getFullYear() || (dateTime.getFullYear() === now.getFullYear() && dateTime.getMonth() >= now.getMonth())))
			{
				nextLink = new Element(
					"span",
					{
						"class": "disabled next"
					}
				).update(next);
			}
			else
			{
				nextLink = new Element(
					"a",
					{
						href: "javascript:void(0)",
						"class": "next"
					}
				).update(next);

				nextLink.observe(
					"click",
					function()
					{
						this.nextMonth(dateTime, calendarDiv);
					}.bind(this)
				);
			}

			var nav = new Element("span").addClassName("date-picker-nav");
			nav.update(DateTimePicker.localizedStrings.monthNames[dateTime.getMonth()] + " " + dateTime.getFullYear());

			var monthNav = new Element("p").addClassName("month-nav");
			monthNav.insert(previousLink);
			monthNav.insert(nextLink);
			monthNav.insert(nav);

			calendarDiv.insert(monthNav);

			var thead = new Element("thead");
			var tr = new Element("tr");

			for (var i = 0; i < 7; i++)
			{
				tr.insert(new Element("th").update(this.getShortDayName(i)));
			}

			thead.insert(tr);
			table.insert(thead);

			var tbody = new Element("tbody");
			var date = new Date(dateTime.getFullYear(), dateTime.getMonth(), 1, 0, 0, 0, 0);

			while (date.getDay() > 0)
			{
				date.setDate(date.getDate() - 1);
			}

			var endDate = new Date(dateTime.getFullYear(), dateTime.getMonth() + 1, 0, 0, 0, 0, 0);

			while (endDate.getDay() < 6)
			{
				endDate.setDate(endDate.getDate() + 1);
			}

			tr = new Element("tr");
			i = 0;

			while (date.getTime() <= endDate.getTime())
			{
				if (i % 7 === 0)
				{
					if (i > 0)
					{
						tbody.insert(tr);
					}

					tr = new Element("tr");
				}

				var classes = [];
				if (date.getMonth() !== dateTime.getMonth())
				{
					classes.push("not-this-month");
				}

				if (this.dateTime !== null && (date.getFullYear() === this.dateTime.getFullYear() && date.getMonth() === this.dateTime.getMonth() && date.getDate() === this.dateTime.getDate()))
				{
					classes.push("selected");
				}

				if (date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate())
				{
					classes.push("today");
				}

				if (i % 7 === 0 || (i + 1) % 7 === 0)
				{
					classes.push("weekend");
				}
				else
				{
					classes.push("weekday");
				}

				var cellDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), dateTime.getHours(), dateTime.getMinutes(), 0, 0);
				var cellClass = classes.length > 0 ? classes.join(" ") : null;

				var isCellEnabled = true;
				if (this.bounding === DateTimePicker.TODAY_OR_LATER &&
						(cellDate.getFullYear() < now.getFullYear() ||
						(cellDate.getFullYear() === now.getFullYear() &&
						(cellDate.getMonth() < now.getMonth() ||
						(cellDate.getMonth() === now.getMonth() && cellDate.getDate() < now.getDate())))))
				{
					isCellEnabled = false;
				}
				else if (this.bounding === DateTimePicker.TODAY_OR_EARLIER &&
						(cellDate.getFullYear() > now.getFullYear() ||
						(cellDate.getFullYear() === now.getFullYear() &&
						(cellDate.getMonth() > now.getMonth() ||
						(cellDate.getMonth() === now.getMonth() && cellDate.getDate() > now.getDate())))))
				{
					isCellEnabled = false;
				}

				var cell = this.createCalendarDayCell(cellDate, cellClass, isCellEnabled);

				tr.insert(cell);

				date.setDate(date.getDate() + 1);
				
				i++;
			}

			tbody.insert(tr);
			table.insert(tbody);

			calendarDiv.insert(table);

			if (this.allowEmpty)
			{
				var p = this.createEmptyLink();
				calendarDiv.insert(p);
			}

			return calendarDiv;
		},

		createEmptyLink: function()
		{
			var p = new Element("p").addClassName("clear");

			var linkText;
			if (this.dateInput && this.timeInput)
			{
				linkText = DateTimePicker.localizedStrings.clearDateTime;
			}
			else if (this.dateInput && !this.timeInput)
			{
				linkText = DateTimePicker.localizedStrings.clearDate;
			}
			else if (!this.dateInput && this.timeInput)
			{
				linkText = DateTimePicker.localizedStrings.clearTime;
			}
			else
			{
				throw "lol wut there isn't a date AND time";
			}

			var a = new Element(
				"a",
				{
					href: "javascript:void(0)"
				}
			).update(linkText);

			a.observe(
				"click",
				function(e)
				{
					e.stop();
					this.onDateSelected(null);
				}.bind(this)
			);

			p.insert(a);

			return p;
		},

		onDateSelected: function(dateTime)
		{
			this.activeCallout.close();
			this.setDateTime(dateTime);
		},
		
		createCalendarDayCell: function(dateTime, className, isEnabled)
		{
			var td = new Element("td").update(dateTime.getDate());

			if (typeof className !== "undefined" && className !== null)
			{
				td.addClassName(className);
			}

			if (isEnabled)
			{
				td.addClassName("enabled");
				td.observe(
					"click",
					function()
					{
						this.onDateSelected(dateTime);
					}.bind(this)
				);
			}
			else
			{
				td.addClassName("disabled");
			}

			return td;
		},

		createDateInput: function()
		{
			var input = new Element(
				"input",
				{
					type: "text",
					name: "date-time-picker-placeholder",
					length: 10,
					maxlength: 10
				}
			).addClassName("date");

			input.observe("focus", this.showDateCallout.bind(this));
			input.observe("change", this.updateDateTimeFromInputs.bind(this));

			return input;
		},

		updateDateTimeFromInputs: function()
		{
			var dateTime = this.getDateTimeFromInputs();
			this.setDateTime(dateTime);
		},

		createTimeInput: function()
		{
			var input = new Element(
				"input",
				{
					type: "text",
					name: "date-time-picker-placeholder",
					length: 8,
					maxlength: 8
				}
			).addClassName("time");

			input.observe("focus", this.showTimeCallout.bind(this));
			input.observe("change", this.updateDateTimeFromInputs.bind(this));

			return input;
		},

		showDateCallout: function()
		{
			this.activeCallout = new Callout(this.dateInput);
			this.activeCallout.div.addClassName("date-picker");

			this.dateTime = this.getDateTimeFromInputs();

			var table = this.createCalendarTable(this.dateTime === null ? null : new Date(this.dateTime));
			this.activeCallout.update(table);

			this.activeCallout.show();
		},

		convertAmPmHours: function(hours, isPm)
		{
			if (isPm)
			{
				hours += 12;
			}

			if (hours == 12 && !isPm)
			{
				hours = 0;
			}
			else if (hours >= 24)
			{
				hours = 12;
			}

			return hours;
		},

		showTimeCallout: function()
		{
			this.activeCallout = new Callout(this.timeInput);
			this.activeCallout.div.addClassName("time-picker");

			var effectiveDateTime = this.getDateTimeFromInputs();
			if (effectiveDateTime === null)
			{
				effectiveDateTime = new Date();
			}

			effectiveDateTime.setSeconds(0);

			var contentDiv = this.activeCallout.getContentArea();

			var hourPlaceholder = new Element("span");
			var minuteFirstDigitPlaceholder = new Element("span");
			var minuteSecondDigitPlaceholder = new Element("span");
			var amPmPlaceholder = new Element("span");

			var colon /* lol */ = new Element("span").addClassName("colon").update(":");

			contentDiv.insert(hourPlaceholder);
			contentDiv.insert(colon);
			contentDiv.insert(minuteFirstDigitPlaceholder);
			contentDiv.insert(minuteSecondDigitPlaceholder);
			contentDiv.insert(amPmPlaceholder);

			var onChange = function()
			{
				var hours = this.convertAmPmHours(hourSpinner.getValue(), amPmSpinner.getValue() == "PM");
				var minutes = (minuteFirstDigitSpinner.getValue() * 10) + minuteSecondDigitSpinner.getValue();

				effectiveDateTime.setHours(hours);
				effectiveDateTime.setMinutes(minutes);

				this.setDateTime(effectiveDateTime);
			}.bind(this);

			var hourSpinner = new SpinnerInput([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], hourPlaceholder).addClassName("hours");
			var hours = effectiveDateTime.getHours();
			hourSpinner.setSelectedValue(hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours));
			hourSpinner.addOnChangeListener(onChange);

			var minuteFirstDigitSpinner = new SpinnerInput([0, 1, 2, 3, 4, 5], minuteFirstDigitPlaceholder).addClassName("minutes-first-digit");
			minuteFirstDigitSpinner.setSelectedValue(Math.floor(effectiveDateTime.getMinutes() / 10))
			minuteFirstDigitSpinner.addOnChangeListener(onChange);

			var minuteSecondDigitSpinner = new SpinnerInput([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], minuteSecondDigitPlaceholder).addClassName("minutes-second-digit");
			minuteSecondDigitSpinner.setSelectedValue(effectiveDateTime.getMinutes() % 10)
			minuteSecondDigitSpinner.addOnChangeListener(onChange);

			var amPmSpinner = new SpinnerInput(["AM", "PM"], amPmPlaceholder).addClassName("am-pm");
			amPmSpinner.setSelectedValue(hours >= 12 ? "PM" : "AM");
			amPmSpinner.addOnChangeListener(onChange);

			if (this.allowEmpty)
			{
				var p = this.createEmptyLink();
				contentDiv.insert(p);
			}

			this.activeCallout.show();
		},

		getDateTime: function()
		{
			var dateTime = null;
			if (this.dateTime !== null)
			{
				dateTime = new Date(this.dateTime);
			}

			return dateTime;
		},
		
		setDateTime: function(dateTime)
		{
			this.dateTime = dateTime;

			this.updateTextInputs();
			this.addHiddenInputs();
			this.notifyChangeListeners();
		},

		addChangeListener: function(listener)
		{
			if (typeof listener !== "function")
			{
				throw "Specified listener isn't a function";
			}

			if (this.findChangeListener(listener) !== false)
			{
				console.warn("Specified listener is already bound; ignoring request");
			}
			else
			{
				this.changeListeners.push(listener);
			}
		},

		findChangeListener: function(listener)
		{
			for (var i = 0; i < this.changeListeners.length; i++)
			{
				if (this.changeListeners[i] === listener)
				{
					return i;
				}
			}

			return false;
		},

		removeChangeListener: function(listener)
		{
			var index = this.findChangeListener(listener);

			if (index === false)
			{
				throw "Specified listener isn't registered";
			}
			else
			{
				delete this.changeListeners[index];
			}
		},

		notifyChangeListeners: function()
		{
			for (var i = 0; i < this.changeListeners.length; i++)
			{
				this.changeListeners[i](this);
			}
		},

		updateTextInputs: function()
		{
			var value;

			if (this.dateInput)
			{
				if (this.dateTime !== null)
				{
					value = (this.dateTime.getMonth() + 1) + "/" + this.dateTime.getDate() + "/" + this.dateTime.getFullYear();
				}
				else
				{
					value = "";
				}

				this.dateInput.setValue(value);
			}

			if (this.timeInput)
			{
				if (this.dateTime !== null)
				{
					var hours = this.dateTime.getHours();
					var minutes = this.dateTime.getMinutes();
					var isPm = hours >= 12;

					if (isPm && hours > 12)
					{
						hours -= 12;
					}
					else if (hours === 0)
					{
						hours = 12;
					}

					value = hours + ":" + (minutes < 10 ? (minutes === 0 ? "00" : "0" + minutes) : minutes) + " " + (isPm ? "pm" : "am");
				}
				else
				{
					value = "";
				}

				this.timeInput.setValue(value);
			}
		}
	}
);

DateTimePicker.TODAY_OR_EARLIER = -1;
DateTimePicker.TODAY_OR_LATER = 1;

DateTimePicker.getInstanceFor = function(cakeDiv)
{
	var cakeDiv = $(cakeDiv);

	if (!cakeDiv)
	{
		throw "Couldn't find specified element";
	}

	return cakeDiv.retrieve("dateTimePickerInstance");
};

DateTimePicker.bindAll = function(root)
{
	if (typeof root === "undefined")
	{
		root = $$("body")[0];
	}

	root.select("div.input.date, div.input.time, div.input.datetime").each(
		function(elem)
		{
			new DateTimePicker(elem);
		}
	);
};

DateTimePicker.styleLoaded = false;

var HintedTextField = Class.create(
	{
		initialize: function(input)
		{
			this.input = $(input);

			if (typeof this.input === "undefined")
			{
				throw "Passed input is undefined";
			}

			if (typeof this.input.nodeName === "undefined")
			{
				throw "Passed input lacks a nodeName property";
			}

			var nodeName = this.input.nodeName.toLowerCase().strip();
			this.isInput = (nodeName === "input");
			this.isTextarea = (nodeName === "textarea");

			if (!this.isInput && !this.isTextarea)
			{
				throw "Passed input is not an <input /> or <textarea /> tag";
			}
			
			this.inputType = (this.isInput ? this.input.readAttribute("type").strip().toLowerCase() : null);
			this.isInputText = (this.isInput && this.inputType === "text");
			this.isInputPassword = (this.isInput && this.inputType === "password");

			if (this.isInput && !this.isInputText && !this.isInputPassword)
			{
				throw "Passed input is a <input type=\"" + input.readAttribute("text") + "\" />; must be type \"text\" or \"password\" instead";
			}

			var existing = this.input.retrieve("hintedTextFieldInstance");
			if (existing)
			{
				console.warn("There's already a HintedTextField bound to this input; returning it");
				return existing;
			}

			this.defaultValue = this.input.readAttribute("title");
			if (this.defaultValue === null)
			{
				throw "Passed input lacks a \"title\" attribute";
			}

			// rubbish IE8- requires that you clone the input for passwords; you can't change the type at runtime
			this.cloneInputInstead = false;
			if (Prototype.Browser.IE)
			{
				var match = /MSIE ([0-9]+)/.exec(navigator.userAgent);
				if (parseInt(match[1]) < 9)
				{
					this.cloneInputInstead = true;
				}
			}

			this.observeInput(this.input);
			this.input.up("form").observe("submit", this.onFormSubmit.bind(this));

			this.updateFieldStatus(false);

			this.input.store("hintedTextFieldInstance", this);
		},

		observeInput: function(input)
		{
			input.observe("focus", this.onFocus.bind(this));
			input.observe("blur", this.onBlur.bind(this));
		},

		onFocus: function(e)
		{
			this.updateFieldStatus(true);
		},

		onBlur: function(e)
		{
			this.updateFieldStatus(false);
		},

		cloneInput: function(newType)
		{
			var input = "<" + this.input.nodeName + " type=\"" + newType.escapeHTML() + "\"";
			for (var i = 0; i < this.input.attributes.length; i++)
			{
				var key = this.input.attributes[i].name;
				if (key !== "type")
				{
					var value = this.input.readAttribute(key);
					if (typeof value === "string" && value !== null)
					{
						input += " " + key + "=\"" + value.escapeHTML() + "\"";
					}
				}
			}

			input += " />";

			input = Element.extend(document.createElement(input));
			input.store("hintedTextFieldInstance", this);
					
			this.observeInput(input);

			return input;
		},

		setFieldType: function(newType)
		{
			var currentType = this.input.getAttribute("type");
			if (this.isInput && currentType != newType)
			{
				if (this.cloneInputInstead)
				{
					this.input = this.input.replace(this.cloneInput(newType));
					this.input = $(this.input.identify());
				}
				else
				{
					this.input.setAttribute("type", newType);
				}
			}
		},

		resetFieldType: function()
		{
			if (this.isInput)
			{
				var type = null;
				if (this.isInputText)
				{
					type = "text";
				}
				else if (this.isInputPassword)
				{
					type = "password";
				}
				else
				{
					throw "Can't determine original type of this input";
				}

				this.setFieldType(type);
			}
		},

		updateFieldStatus: function(focus)
		{
			if (focus && $F(this.input) == this.defaultValue)
			{
				this.input.removeClassName("hinted");
				this.input.setValue("");
				this.resetFieldType();
				var focus = function()
				{
					this.input.focus();
				}.bind(this);

				if (this.cloneInputInstead)
				{
					focus.defer();
				}
				else
				{
					focus();
				}
			}
			else if (!focus && $F(this.input) == "")
			{
				this.input.addClassName("hinted");
				this.input.setValue(this.defaultValue);
				this.setFieldType("text");
			}
		},

		onFormSubmit: function(e)
		{
			this.resetFieldType();

			if ($F(this.input) == this.defaultValue)
			{
				this.input.setValue("");
			}
		}
	}
);

HintedTextField.bindAll = function()
{
	var fields = $$("input.autohinted, textarea.autohinted");
	for (var i = 0; i < fields.length; i++)
	{
		new HintedTextField(fields[i]);
	}
};

var ImagePreloader = Class.create(
	{
		initialize: function(srcs, onComplete, emitDebugMessages)
		{
			this.stackSize = srcs.length;
			this.emitDebugMessages = (typeof emitDebugMessages !== "undefined" && emitDebugMessages);

			srcs.each(
				function(src)
				{
					if (this.emitDebugMessages)
					{
						console.debug("Preloading image \"" + src + "\"...");
					}

					var img = new Image();

					img.onload = function()
					{
						if (this.emitDebugMessages)
						{
							console.debug("Image at \"" + src + "\" done preloading, stack size is now " + this.stackSize);
						}
						this.imageLoaded(src, true, onComplete);
					}.bind(this);
					
					img.onerror = function()
					{
						console.warn("Image at \"" + src + "\" failed to preload, stack size is now " + this.stackSize);
						this.imageLoaded(src, false, onComplete);
					}.bind(this);

					img.src = src;
				}.bind(this)
			);
		},

		imageLoaded: function(src, success, onComplete)
		{
			this.stackSize--;

			if (success)
			{
				if (this.emitDebugMessages)
				{
					console.debug("Image \"" + src + "\" preloaded");
				}
			}
			else
			{
				console.warn("Image \"" + src + "\" failed to load");
			}

			if (this.stackSize <= 0)
			{
				if (this.emitDebugMessages)
				{
					console.debug("All requested images preloaded for this preloader");
				}
				onComplete();
			}
		}
	}
);

var InlineAjaxValidation = Class.create(
	{
		initialize: function(span, url)
		{
			if (typeof span === "undefined" || !span || !span.nodeName || span.nodeName.toLowerCase() !== "span")
			{
				throw "span not provided or is not a <span>";
			}

			if (typeof url === "undefined" || url === null || url === false)
			{
				throw "URL not provided"
			}

			var div = span.up("div.input");
			if (!div)
			{
				throw "span is not inside a <div class=\"input ... \">";
			}

			var input = div.down("input[type=\"text\"]");
			if (!input)
			{
				throw "span's <div class=\"input ... \"> does not contain an <input type=\"text\" />";
			}

			this.input = input;
			this.span = span;
			this.url = url;

			this.bindEvents();
		},

		onBlur: function(e)
		{
			if (this.input.getValue().length == 0)
			{
				this.span.setStyle(
					{
						display: "none"
					}
				);
			}
			else
			{
				this.span.setStyle(
					{
						display: ""
					}
				);

				this.span.addClassName("busy");

				this.span.removeClassName("success");
				this.span.removeClassName("error");

				this.span.removeAttribute("title");

				this.span.update("Validating...");

				this.request = new Ajax.Request(
					this.url,
					{
						parameters:
						{
							"data[value]": this.input.getValue()
						},
						method: "post",
						onSuccess: this.onAjaxSuccess.bind(this),
						onFailure: this.onAjaxFailure.bind(this)
					}
				);
			}
		},

		onAjaxSuccess: function(response)
		{
			this.span.removeClassName("busy");

			if (!response.responseXML)
			{
				this.span.addClassName("error");
				this.span.update("XML wasn't returned");
			}
			else
			{
				this.span.addClassName(response.responseXML.documentElement.getAttribute("type"));
				this.span.update(response.responseXML.documentElement.firstChild.nodeValue);
			}
		},

		onAjaxFailure: function(response)
		{
			this.span.removeClassName("busy");
			this.span.addClassName("error");

			var exceptionMessage = response.getHeader("X-Exception-Message");
			this.span.update("HTTP " + response.status + " " + (exceptionMessage !== null ? exceptionMessage : "(no exception)"));
		},

		bindEvents: function()
		{
			this.span.setStyle(
				{
					display: "none"
				}
			);

			this.input.observe("blur", this.onBlur.bind(this));
		}
	}
);

var Lib24watchPagination = Class.create(
	{
		initialize: function(form, localizedStrings)
		{
			this.form = $(form);
			this.localizedStrings = localizedStrings;

			this.pageField = this.form.down("input[type=\"text\"]");
			this.limitField = this.form.down("select");

			this.form.observe("submit", this.onFormSubmit.bind(this));
			this.limitField.observe("change", this.onFormSubmit.bind(this));
		},

		onFormSubmit: function(e)
		{
			e.stop();

			var page = $F(this.pageField);
			var limit = $F(this.limitField);

			var totalPages = parseInt($F(this.form.down("input[name=\"totalPages\"]")));

			if (/^[1-9][0-9]*$/.test(page))
			{
				page = parseInt(page);
				if (page > totalPages)
				{
					page = totalPages;
				}
				else if (page <= 0)
				{
					page = 1;
				}
			}
			else
			{
				page = 1;
			}

			this.pageField.disable();
			this.limitField.disable();

			var template = new Template($F(this.form.down("input[name=\"baseUrl\"]")));
			window.location = template.evaluate(
				{
					page: page,
					limit: limit
				}
			);
		}
	}
);

Lib24watchPagination.bindAll = function()
{
	$$("form.pagination").each(
		function(form)
		{
			new Lib24watchPagination(form);
		}
	);
}

var SortableTable = Class.create(
	{
		initialize: function(table)
		{
			if (typeof Sortable === "undefined")
			{
				throw "Scriptaculous Sortable class not loaded";
			}

			this.table = $(table);

			if (!this.table)
			{
				throw "No table provided or no table matches given ID";
			}

			this.tbody = null;
			this.table.childElements().each(
				function(elem)
				{
					if (elem.nodeName.toLowerCase() === "tbody")
					{
						this.tbody = elem;
						throw $break;
					}
				}.bind(this)
			);

			if (!this.tbody)
			{
				throw "Provided table doesn't have an immediate <tbody> descendant";
			}

			this.form = this.table.up("form");
			if (!this.form)
			{
				throw "Provided table is not inside a <form>";
			}

			this.trs = this.tbody.childElements();
			if (this.trs.length === 0)
			{
				throw "<tbody> doesn't have any rows (which is also invalid markup)";
			}

			this.collectPrimaryKeys();

			this.trs.each(
				function(tr)
				{
					tr.identify();
				}
			);

			Sortable.create(
				this.tbody,
				{
					tag: "tr",
					only: "sortable",
					onUpdate: function()
					{
						Lib24watchDomModifier.evenOddifyTableRows(this.tbody);
						this.postRowOrder();
					}.bind(this)
				}
			);
		},

		collectPrimaryKeys: function()
		{
			var primaryKeys = [];

			var i = 0;
			var primaryKey = null;

			this.trs.each(
				function(tr)
				{
					i++;

					var hiddenInput = null;
					var hiddenInputs = tr.select("input[type=\"hidden\"]");

					primaryKey = tr.retrieve("primaryKey", null);

					if (primaryKey === null)
					{
						switch (hiddenInputs.length)
						{
							case 0:
								console.warn("Row #" + i + " in this table doesn't have a hidden input; it will not be sortable");
								break;

							case 1:
								hiddenInput = hiddenInputs[0];
								break;

							default:
								var candidateInput = null;
								hiddenInputs.each(
									function(input)
									{
										if (input.hasClassName("primary-key"))
										{
											if (candidateInput)
											{
												console.warn("Row #" + i + " has ambigious hidden inputs; two or more hidden inputs " +
														"exist and two or more have a class of \"primary-key\". This row will not be sortable.");
												candidateInput = null;
												throw $break;
											}
											candidateInput = input;
										}
									}
								);

								if (candidateInput)
								{
									hiddenInput = candidateInput;
								}
								break;
						}

						if (hiddenInput)
						{
							primaryKey = $F(hiddenInput);
							if (primaryKey.length === 0)
							{
								console.warn("Hidden input for this <tr> has a value of an empty string; it will not be sortable");
								primaryKey = null;
							}
						}
					}

					if (primaryKey !== null)
					{
						primaryKeys.push(primaryKey);
						tr.store("primaryKey", primaryKey).addClassName("sortable");
					}
				}.bind(this)
			);

			if (primaryKeys.length === 0)
			{
				console.warn("No known primary keys in this table; no rows will be sortable");
			}
			else
			{
				var uniquePrimaryKeys = primaryKeys.uniq();
				var duplicateCount = primaryKeys.length - uniquePrimaryKeys.length;
				if (duplicateCount > 0)
				{
					console.warn("This table contains " + duplicateCount + " duplicate primary keys; sorting could be unpredictable on these rows");
				}
			}
		},

		getCurrentOrder: function()
		{
			var primaryKeys = [];

			var trs = this.tbody.childElements();
			if (trs.length !== this.trs.length)
			{
				console.warn("Number of <tr>s in this <tbody> changed; expected " + this.trs.length + " but found " +
						trs.length + ". Sorting behavior may be unpredictable for the unaccounted or extra rows.");
			}

			trs.each(
				function(tr)
				{
					var primaryKey = tr.retrieve("primaryKey", null);

					if (primaryKey !== null)
					{
						primaryKeys.push(primaryKey);
					}
				}
			);

			if (primaryKeys.length !== trs.length)
			{
				console.warn("<tbody> has " + trs.length + " rows but only " + primaryKeys.length + " primary keys found; some rows won't be sorted");
			}

			return primaryKeys;
		},

		postRowOrder: function()
		{
			var data =
			{
				"data[]": this.getCurrentOrder()
			};

			new Ajax.Request(
				this.form.readAttribute("action"),
				{
					parameters: data,
					onSuccess: function(response)
					{
						if (typeof response.responseJSON === "undefined" || !response.responseJSON)
						{
							this.onAjaxError(response);
						}
					}.bind(this),
					onFailure: this.onAjaxError.bind(this)
				}
			);
		},

		onAjaxSuccess: function(response)
		{
			console.info("cool story bro");
		},

		onAjaxError: function(response)
		{
			var message = null;

			if (typeof SortableTable.localizedStrings === "undefined")
			{
				message = "There was an error saving the order of these items.";
			}
			else
			{
				message = SortableTable.localizedStrings.onAjaxError;
			}

			alert(message);
		}
	}
);

SortableTable.bindAll = function()
{
	$$("table.sortable").each(
		function(table)
		{
			new SortableTable(table);
		}
	);
}

document.observe(
	"dom:loaded",
	function()
	{
		Lib24watchDomModifier.applyMoneyFields();
		Lib24watchDomModifier.classifyTableCells();
		Lib24watchDomModifier.evenOddifyAllTableRows();
		Lib24watchDomModifier.relExternalInNewWindow();
		Lib24watchDomModifier.applyColumnCheckboxes();
		Lib24watchDomModifier.applyPasswordStrengthMeter();

		DateTimePicker.bindAll();
		HintedTextField.bindAll();
		Lib24watchPagination.bindAll();
		SortableTable.bindAll();
	}
);


