/// <reference path="jquery.intellisense.js"/>  
/**
 * Cobalt Elements for jQuery
 * base - a collection of base methods and functions.
 *
 * These methods are build on the jQuery and interface foundation to provider
 * client functionality to the Cobalt CMS system.
 *
 * Copyright (c) 2007 Scorpion Design, Inc.
 *
 */
(function($) {

	// Global settings.
	$._DIR =
		{
			_UPPER_RIGHT : { top : true, left: false },
			_UPPER_LEFT : { top : true, left: true },
			_LOWER_RIGHT : { top : false, left: false },
			_LOWER_LEFT : { top : false, left: true},
			_CENTER : { center : true }
		};

	// Record the IE6 browser test.  Note that Vista IE7 has both "MSIE 6.0" and "MSIE 7.0" in the user-agent string.
	$.browser.msie6 = $.browser.msie && /MSIE 6\.0/i.test(window.navigator.userAgent) && !/MSIE 7\.0/i.test(window.navigator.userAgent);

	$.cssNum = function(el, prop) { return parseInt($.css(el.jquery?el[0]:el,prop))||0; };

	// Fire a specific event once all images are loaded.
	$.onImagesLoaded = function(fn,tolerance)
		{
			// Ensure we have a valid function.
			if (!$.isFunction(fn)) return;
			
			var images = $('img').filter(function(el,i) { return !this.complete; });
				
			if (images.length > tolerance)
			{
				window.$loadedImageCount = images.length;
				window.$imagesLoadedTolerance = $.toInt(tolerance);
				if (!window.$onImagesLoaded) window.$onImagesLoaded = [];
				window.$onImagesLoaded.push(fn);
				images.one('load',$.imageHasLoaded);
				
				// Set a catch-all timeout to fire the events after 20 seconds even if all the images aren't loaded.
				setTimeout($.fireImagesLoaded,20000);
			}
			else
				fn();
		};

	// Each time an image is loaded, check to see if we can fire the event.
	$.imageHasLoaded = function()
		{
			// Ensure we still have functions to execute.
			if (!window.$onImagesLoaded) return;

			// Reduce the number of images left.
			try { window.$loadedImageCount--; } catch(ex) {}
			
			// If we've reached 0 or the tolerance, fire the events.
			if ((window.$loadedImageCount||0) <= (window.$imagesLoadedTolerance||0))
			{
				$.fireImagesLoaded();
			}
		};

	$.fireImagesLoaded = function()
		{
			// Ensure we still have functions to execute.
			if (!window.$onImagesLoaded) return;

			for (var i=0;i<window.$onImagesLoaded.length;i++)
			{
				if ($.isFunction(window.$onImagesLoaded[i]))
					window.$onImagesLoaded[i]();
			}
			window.$onImagesLoaded = null;
		};

	// Parse the url as a function or as a string with a default property
	$.getAjaxUrl = function(url,params)
		{
		    if (!url)
		        return '';
		    else if ($.isFunction(url))
		        return url(params);
		    else
		    {
		        // Remove any named anchors in the link.
		        url = url.replace(/#\w+/,'');
		        
		        // If we have parameters, add them to the querystring portion of the url.
		        if (params)
		        {
					url = url.split('?');
					var query = {};
					if (url.length>1)
					{
						// Build the querystring into a dictionary.
						var parts = url[1].split('&');
						for (var i=0;i<parts.length;i++)
						{
							var pair = parts[i].split('=');
							query[pair[0]] = pair.length?pair[1]:'';
						} 
					}

					// Extend the supplied parameters, ignoring case.
					for (var p1 in params)
					{
						var existing = false;
						for (var p2 in query)
						{
							if (p1.toLowerCase()==p2.toLowerCase())
							{
								query[p2] = params[p1];
								existing = true;
								break;
							}
						}
						if (!existing)
							query[p1] = params[p1];
					}

					// Convert the query dictionary to a string.					
					var sb = [];
					for (var p in query)
					{
						var val = query[p];
						if (val===null || val==='')
							continue;
						else
							sb.push(p+'='+val);
					}

					// Assign the completed url.					
					url = url[0]+'?'+sb.join('&');
				}

		        return url;
		    }
		};

	// Obtains the CSS position and size of the element.
	$.fn.absPosition = function(innerSize)
		{
			if (this.length==0) return {};
			
			// Get the element and initialize the variables.
			var el = this[0];
			var o,w,h;
			var node = el.nodeName && el.nodeName.toLowerCase();

			// If a window has been passed, get the scrollbar position to measure the window offset.
			if (el.navigator && el.document)
			{
				var doc = el.document;
				o = {
						left:Math.max(doc.documentElement.scrollLeft, doc.body.scrollLeft),
						top:Math.max(doc.documentElement.scrollTop, doc.body.scrollTop)
					};
				if (innerSize)
				{
					w = this.innerWidth();
					h = this.innerHeight();
				}
				else
				{
					w = this.width();
					h = this.height();
				}
			}
			// If the document or body object has been passed.
			else if (el.body || node=='body')
			{
				// The document gets a 0, the body gets a calculated offset.
				if (el.body)
					o = { left : 0, top : 0 };
				else
					o = this.offset();
					
				var wn = $(el.defaultView||el.parentWindow||(el.ownerDocument&&el.ownerDocument.parentWindow));
				if (innerSize)
				{
					w = Math.max(this.innerWidth(),wn.width());
					h = Math.max(this.innerHeight(),wn.height());
				}
				else
				{
					w = Math.max(this.width(),wn.width());
					h = Math.max(this.height(),wn.height());
				}
			}
			else
			{
				// Get the offset and size.
				o = this.offset();

				if (innerSize)
				{
					if (node == 'tr')
					{
						o = this.children('td:first').offset();
						if (o)
						{
							var td = this.children('td:last');
							var tdo = td.offset();
							w = tdo.left-o.left+td.innerWidth();
							h = td.innerHeight();
						}
						else
						{
							o = this.offset();
							w = this.innerWidth();
							h = this.innerHeight();
						}
					}
					else
					{
						w = this.innerWidth();
						h = this.innerHeight();
					}
				}
				else
				{
					w = this[0].offsetWidth;
					h = this[0].offsetHeight;
					
					if (w+h==0 && node=='a' && this[0].childNodes.length==1)
					{
						w = this[0].childNodes[0].offsetWidth;
						h = this[0].childNodes[0].offsetHeight;
					}
				}

				if (w+h==0)
				{
					w = this[0].scrollWidth;
					h = this[0].scrollHeight;
				}
			}
			
			return {
					left : o.left,
					top : o.top,
					width : w,
					height : h
				   };
		};

	// Obtains the CSS position and size of the element relative to the offset parent.
	$.fn.relPosition = function(innerSize)
		{
			var p = this.absPosition(innerSize);
			if (!this.is('body,html'))
			{
				var op = this.offsetParent();
				if (this.length && op.length && this[0].ownerDocument!=op[0])
				{
					var o = op.offset();
					p.left -= (o.left-op[0].scrollLeft);
					p.top -= (o.top-op[0].scrollTop);
				}
			}
			return {
				left : p.left,
				top : p.top,
				width : p.width,
				height : p.height
			};
		};

    $.fn.outerHtml = function()
        {
            if (this.length)
            {
				if (this[0].outerHTML)
					return this[0].outerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "");
				else
				{
					var div = $('<div style="display:none"></div>');
					var clone = $(this[0].cloneNode(false)).html(this.html()).appendTo(div);
					var outer = div.html();
					div.remove();
					return outer.replace(/ jQuery\d+="(?:\d+|null)"/g, "");
                }
            }
            else
                return null;
        };

	// Set the left/top CSS position of an element relative to a base object, depending 
	// on which corner to align to, and the direction to fly it.
	//
	// base - the base element that this element will be relative to.  Can optionally be a mouse click event.
	// corner - the starting position, i.e. which corner of the base element to use.
	// direction - which direction should the element fly from the starting position.
	// offset - optionally offset the position {left:0,top:0}
	// bounding - optional position describing the bounding box {left:0,top:0,width:100,height:100}
	$.fn.relativePos = function(base,corner,direction,offset,bounding,isreverse)
		{
			// Fix the Safari client position.
			if ($.browser.safari && typeof base.clientX != 'undefined')
			{
				var doc = document.documentElement, body = document.body;
				base.clientX = base.clientX - (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0);
				base.clientY = base.clientY - (doc && doc.scrollTop  || body && body.scrollTop  || 0) + (doc.clientLeft || 0);
			}
			
			// Get the position of the elements (if the base is a click event, get the coordinates of the click).
			if (this.length==0) return {};
			var wn = $(window).absPosition();
			var p1 = base.clientX||base.clientY ? { left:(base.clientX||0)+wn.left,top:(base.clientY||0)+wn.top,width:0,height:0 } : $(base).absPosition();
			var p2 = this.absPosition();

			// If we have no width and height, then make the element visible and try it again.
			if (!p2.width && !p2.height)
			{
				this.css({left:-999,top:-999,display:'inline'});
				p2 = { width: this.width(), height: this.height() };
			}
			
			// Get the CSS position of the elements one relative to the other.
			isreverse = isreverse||{};
			var css = $.extend($.boundingPos(p1,p2,bounding||wn,corner,direction,isreverse),{display:'block'});
			var fixed = this.css('position')=='fixed';
			
			// Add any defined offsets.
			if (offset)
			{
				if (isreverse.left) offset.left = 0-(offset.left||0);
				if (isreverse.top) offset.top = 0-(offset.top||0);
				css.left += offset.left||0;
				css.top += offset.top||0;
			}

			// Adjust for the offset parent.
			var op = this.offsetParent();
			if (op.length && op[0]!=document && op[0]!=document.documentElement)
			{
				var p3 = op.offset();
				css.left -= p3.left;
				css.top -= p3.top;
			}
			
			if (fixed)
			{
				css.top -= (window.pageYOffset||document.documentElement.scrollTop||0);
				css.left -= (window.pageXOffset||document.documentElement.scrollLeft||0);
			}
			else
				css.position = 'absolute';

			/*
			// Look for a containing block to adjust the positioning values.
			var parent = this[0].parentNode;
			while (parent && parent!=document && parent!=document.body)
			{
				var pos = $.css(parent,'position');
				// Note that only MSIE cares about relatively position table elements.
				if (/^absolute|fixed$/.test(pos) || (pos=='relative' && ($.browser.msie || !/^table$/i.test(parent.nodeName))))
				{
					var p3 = $(parent).offset();
					css.left -= p3.left;
					css.top -= p3.top;
					break;
				}
				
				try { parent = parent.parentNode; }
				catch (e) { parent = null; }
			}
			*/

			// Assign the position.
			return this.css(css);
		};

	/**
	 * Take a base position, an element size and a bounding area (usually the viewport), the corner the direction,
	 * and calculate the position the element should be at.  If an element will break the bounding area, try to 
	 * fly it the other direction if there is room.
	 *
	 * @base - The base element position and size.
	 * @el - The element width and height.
	 * @bounding - The bounding box for the element position, defaults to the viewport.
	 * @corner - Which corner to fly off of, using the $.DIR enumeration.
	 * @dir - Which direction to fly, using the $.DIR enumeration.
	 */
	$.boundingPos = function(base,el,bounding,corner,dir,isreverse)
		{
			// Assign defaults.
			if (!bounding) { bounding = $(window).absPosition(); }
			if (!corner) { corner = $._DIR._LOWER_RIGHT; }
			if (!dir) { dir = $._DIR._LOWER_RIGHT; }

			// If we are centering the element.
			if (corner.center)
			{
				var css = {
							left : (base.width-el.width)/2 + base.left,
							top : (base.height-el.height)/2 + base.top
						  };

				if (css.top < bounding.top) css.top = bounding.top;
				if (css.left < bounding.left) css.left = bounding.left;
				
				return css;
			}

			// Get the bounding bottom and right positions.
			bounding.right = bounding.left + bounding.width;
			bounding.bottom = bounding.top + bounding.height;

			// Calculate the left position, and the optional reversed position
			var l =  base.left + (corner.left?0:base.width) - (dir.left?el.width:0);
			var l2 = base.left + (corner.left?base.width:0) - (dir.left?0:el.width);
			
			// Calculate the top position, and the optional reversed position
			var t =  base.top + (corner.top?0:base.height) - (dir.top?el.height:0);
			var t2 = base.top + (corner.top?base.height:0) - (dir.top?0:el.height);

			// If we break the bounding box left or right, see if it fits if the reverse position is used.
			if ((l<bounding.left && l2+el.width<=bounding.right) || (l+el.width>bounding.right && l2>=bounding.left))
			{
				l = l2;
				if (isreverse) isreverse.left = true;
			}

			// If we break the bounding box top or bottom, see if it fits if the reverse position is used.
			if ((t<bounding.top && t2+el.height<=bounding.bottom) || (t+el.height>bounding.bottom && t2>=bounding.top))
			{
				t = t2;
				if (isreverse) isreverse.top = true;
			}

			// We can't fly the element to an impossible position (off the pages upper right).
			if (l<0) { l = 0; }
			if (t<0) { t = 0; }

			return { left : l, top : t };
		};

	// Using a search pattern, returns the current element if it matches, or walks up the tree to the first matching parent.
	$.fn.container = function(pattern)
		{
			if (this.is(pattern))
				return this;
			else if (this.length)
			{
				var p = this[0].stepParent||this[0].parentNode;
				while ( p && !$(p).is(pattern) ) { try { p = p.stepParent||p.parentNode; } catch(error) { p = null; }; }

				if (p)
					return $(p);
				else
					return new jQuery([]);
			}
		};

	// Hover a class 
	$.fn.hoverClass = function(overClass)
		{
			return this.each(function(i)
				{
					this.$self = $(this);
					this.$overClass = overClass;
					this.$self.hover($.overClass,$.outClass);
				});
		};

	$.fn.toggleValue = function()
		{
			return this.bind('focus',$.clearValue).bind('blur',$.restoreValue);
		};

	$.clearValue = function(e)
		{
			// If we haven't already assigned the original value, do so now.
			if (typeof(this.value)!='undefined' && typeof(this.origValue)=='undefined')
				this.origValue = this.value;

			// If we have an original value and it matches the current value, blank it out.
			if (this.origValue && this.origValue==this.value)
				this.value = '';
		};

	$.restoreValue = function(e)
		{
			// If the current value is blank and we have an original value, re-assign it.
			if (typeof(this.value)!='undefined' && !this.value && this.origValue)
				this.value = this.origValue;
		};

	// Swap an image on mouseover.
	$.fn.hoverImage = function(swap)
		{
			return this.each(function(i)
				{
					// Get a jQuery representation of this object.
					this.$self = $(this);

					// Get the hover image.
					// If an array is passed, but it isn't long enough, do nothing.
					// If no swap is passed, check for the overImg attribute.
					var img = swap?(swap.push?(swap.length>i?swap[i]:null):swap):this.$self.attr('_overimg');
					if (!img) { return; }

					// If we have a "sticky" hover image.
					var href = null;
					if ($.toBool(this.$self.attr('_sticky')))
					{
						// Get the path to the parent anchor.
						href = this.$self.parent('a').attr('href');
					}

					// If the href matches the current page url.
					if (href && window.location.href.split('?').shift().endsWith(href))
					{
						// Set the hover image.
						this.$self.attr('src',img);
					}
					else
					{
						// Assign the hover values.
						this.$self.attr('_outimg',this.$self.attr('src'));
						this.$self.hover($.overImage,$.outImage);
						
						$.preload(img);
					}
				});
		};

	// Fire a modified hover event, that checks uses the "stepParent" property to determine if we've 
	// moused on/out of an element that should be treated like a parent, but it not a DOM parent.
	//
	// Note that the stepParent property needs to be assigned to the appropriate elements before 
	// this function will work properly.
	$.fn.hoverRelated = function(fnOver, fnOut, child) {

		// A private function for handling mouse 'hovering'
		function handleHover(event) {
			// Check if mouse(over|out) are still within the same parent element
			var parent = event.relatedTarget;
	
			// Traverse up the tree
			while ( parent && parent != this ) try { parent = parent.stepParent||parent.parentNode; } catch(error) { parent = this; };
			
			// If we actually just moused on to a sub-element, ignore it
			if ( parent == this ) return false;

			// Get any stepParent.
			var step = this.stepParent;

			// Check to see if we moused over a stepchild or stepparent.
			var p = event.relatedTarget;
			while (p) { if (p==this || (step&&step==p)) return false; try { p = p.stepParent||p.parentNode; } catch(ex) { p=null }; }

			if (event.type=='mouseout'||event.type=='mouseleave')
			{
				// If we have a stepParent, trigger its mouseout event.
				if (step)
				{
					$(step).trigger('mouseout', [event]);
					return false;
				}

				// Fire the event
				var result = fnOut.apply(this, [event]);

				// Look for a step parent one level higher and fire its mouseout event.
				var step = null;
				p = this.parentNode;
				while ( !step && p ) { try { step = p.stepParent; p = p.parentNode; } catch(ex) { p = null; } }
				if (step) { $(step).trigger('mouseout', [event]); }
				
				return result;
			}
			else
				return fnOver.apply(step||this, [event]);
		}

		// If the handler already has a guid, assign it to the function pointer.
		if (fnOver.guid) { handleHover.guid = fnOver.guid; }

		// Bind the function to the two event listeners
		this.mouseover(handleHover).mouseout(handleHover);

		// If the handler doesn't already have a guid, grab it from the function pointer.
		if (!fnOver.giud) { fnOver.guid = handleHover.guid; }
		if (!fnOut.giud) { fnOut.guid = handleHover.guid; }

		// Set up the step children.
		if (child && child.length && this.length)
		{
			// Get the stepParent value.
			var stepParent = this[0];
			
			// Iterate through each of the stepchildren and assign each one.
			for (var i=0;i<child.length;i++)
			{
				child[i].stepParent = stepParent;
				$(child[i]).mouseover(handleHover).mouseout(handleHover);
			}
		}
	},

	// Assign a context menu event.
	$.fn.contextmenu = function(handler,althandler)
		{
			if (!handler) return this;

			// Create a function pointer that will validate a context-click.
			var fn = function(e)
				{
					// Both the contextmenu and the mousedown events are bound, if the contextmenu event has already
					// fired on this event, there is no need to fire the mousedown event.
					if (e.contextExecuted)
						return;

					// If we are doing a context-click, or if we have a metaKey (CTRL or CMD) held down while clicking.
					if (e.type=='contextmenu'||e.metaKey)
					{
						// Run the function and stop propagation (to prevent the default context menu from appearing).
						if (!handler.apply(this,arguments)==true)
						{
							e.preventDefault();
							e.stopPropagation();
							e.contextExecuted = true;
							return false;
						}
					}
					// Otherwise apply the alt handler.
					else if ($.isFunction(althandler))
					{
						althandler.apply(this,arguments);
					}
				};
			
			// If the handler already has a guid, assign it to the function pointer.
			if (handler.guid) { fn.guid = handler.guid; }
			
			// Bind the event.
			this.bind('contextmenu.cm',fn).bind('mousedown.cm',fn);

			// If the handler doesn't already have a guid, grab it from the function pointer.
			if (!handler.giud) { handler.guid = fn.guid; }
			
			return this;
		},

	// Unbind the context menu event.
	$.fn.uncontextmenu = function(handler)
		{
			return this.unbind('contextmenu.cm').unbind('mousedown.cm');
		},

	// Assign an escape key event.
	$.fn.escape = function(handler,data)
		{
			if (!handler) return this;
			
			// Create a function pointer that will validate the escape key.
			var fn = function(e)
				{
					if (e.which==27)
					{
						return handler.apply(this,arguments);
					}
				};

			// If the handler already has a guid, assign it to the function pointer.
			if (handler.guid) { fn.guid = handler.guid; }
			
			// Bind the event.
			this.bind('keydown',data,fn);
			
			// If the handler doesn't already have a guid, grab it from the function pointer.
			if (!handler.giud) { handler.guid = fn.guid; }
			
			return this;
		},

	// Unbind the escape handler.
	$.fn.unescape = function(handler)
		{
			return this.unbind('keydown',handler);
		},

	// Assign an enter key event.
	$.fn.onenter = function(handler,data)
		{
			if (!handler) return this;
			
			// Create a function pointer that will validate the escape key.
			var fn = function(e)
				{
					if (e.which==13)
					{
						return handler.apply(this,arguments);
					}
				};

			// If the handler already has a guid, assign it to the function pointer.
			if (handler.guid) { fn.guid = handler.guid; }
			
			// Bind the event.
			this.bind('keydown',data,fn);
			
			// If the handler doesn't already have a guid, grab it from the function pointer.
			if (!handler.giud) { handler.guid = fn.guid; }
			
			return this;
		},

	// Unbind the escape handler.
	$.fn.unonenter = function(handler)
		{
			return this.unbind('keydown',handler);
		},

	// Create an floating div that "ghosts" from one position to another.
	$.fn.floatTo = function(dest,css,easing,fn)
		{
			// Check to see if the function is defined earlier in the argument list.
			if (!fn)
			{
				if ($.isFunction(css))
				{
					fn = css;
					css = {};
				}
				if ($.isFunction(easing))
				{
					fn = easing;
					easing = null;
				}
			}

			// Check to see if the easing is defined in the CSS argument.
			if (typeof css == 'string')
			{
				easing = css;
				css = {};
			}
			
			// If we have a source and destination element.
			if (this.length&&dest)
			{
				var p1 = $.extend(css,this.absPosition());
				var p2 = $(dest).absPosition();
				css = $.extend({backgroundColor:'#ffffff',opacity:.5},css);
				css.position = 'absolute';

				var div = $('<div>&nbsp;</div>');
				div.css(css);
				div.appendTo(document.body);

				var complete = $.isFunction(fn) ? function(){div.remove();fn();} : function(){div.remove()};
				div.animate(p2,easing,complete);
			}
		};

	// Show a loading indicator in front of the element.
	$.fn.loading = function(timeout,options)
		{
			// Default timeout is 10 seconds.  Timeout can be canceled by expressly setting 0 as the parameter.
			if (timeout==null || typeof(timeout)=='undefined') timeout = 20000;

			var o = options||{};
			var offset = o.offset;
			var message = o.message;
			var opacity = o.opacity||.25;
			var mrelative = o.mrelative;

			return this.each(function(i)
				{
					var el = $(this);
					if (!this.isloading)
					{
						// Get the element to attach the loading zone to.
						var node = this.nodeName && this.nodeName.toLowerCase();
						var attach;
						if (el.is('table,tbody,tr'))
						{
							// If we're a table element, we need the first visible td to attach the loading element to.
							attach = el.find('td:visible:first');

							// If we couldn't find any, attach it to the table's parent.
							if (!attach.length)
							{
								if (el.is('table'))
									attach = el.parent();
								else
									attach = el.parents('table:first').parent();
							}
						}
						else if (!el.is('td,div,body'))
							attach = el.parents().filter('div,td,body').filter(':visible').eq(0);
						else
							attach = el;

						var pos = attach.is('td') ? attach.parents('table:first') : attach;
						if (node!='body' && pos.css('position')=='static')
						{
							pos.css('position','relative');
							el.data('oldposition',pos)
						}

						// Get the element's relative position.
						var dim = el.relPosition(true);

						// if the element is the same as the relatively positioned element, zero out the top/left.
						if (el[0]==pos[0])
						{
							dim.left = 0;
							dim.top = 0;
						}

						// Assign the other properties.
						dim.opacity = opacity;

						var iframe = $($.browser.msie ? '<iframe class="ui_blocked" style="z-index:1998;border:none;margin:0;padding:0;position:absolute;" frameBorder="0" src="Shared/blank.html"></iframe>' : '<div style="display:none"></div>').css(dim);
						var div = $('<div class="ui_blocked" style="position:absolute;z-index:1999;"></div>').css(dim);

						if (message=='')
						{
							// Blank message
							message = '<span></span>';
						}
						else if (!message)
						{
							// Default message.
							message = '<div class="load_screen" style="position:absolute;z-index:2000;">\
										<table cellpadding=0 cellspacing=0 border=0 class="load_table">\
											<tr>\
												<td><div class="load_indicator">&nbsp;</div></td>\
												<td>&nbsp;Loading...</td>\
											</tr>\
										</table>\
									</div>';
						}
						else if (!/</.test(message))
						{
							// Text-only message.
							message = '<div style="position:absolute;z-index:2000;">'+message+'</>';
						}

						var msg = $(message);

						// Create the block.
						this.isloading = 
							$([iframe[0],div[0],msg[0]])
								.appendTo(attach);

						// Position the message.
						mrelative = mrelative || (node=='body' ? window : this);
						msg.relativePos(mrelative,{center:true},null,offset,$(document).absPosition());

						// If there is a timeout, set it.
						if (timeout)
						{
							var loadingTimeout = setTimeout(function()
								{
									// Turn off the loading icon.
									el.doneLoading();

									// If the loading icon is visible, give an alery.
									if (el.is(':visible') && el.parents(':hidden').length==0)
										alert('Timeout -- the process could not be completed.');
								},timeout);
							el.data('loading:timeout',loadingTimeout);
						}
					}
				});
		};

	// Remove the loading indicator.
	$.fn.doneLoading = function()
		{
			return this.each(function(i)
				{
					if (this.isloading)
					{
						this.isloading.remove();
						this.isloading = null;
						var el = $(this);
						var loadingTimeout = el.data('loading:timeout');
						if (loadingTimeout) clearTimeout(loadingTimeout);
						el.removeData('loading:timeout');
						el.removeAttr('isloading');

						var pos = el.data('oldposition');
						if (pos)
						{
							pos.css('position','static');
							el.removeData('oldposition');
						}
					}
				});
		};

	// Select the end of a control (i.e. move the cursor to the end of its editable area).
	$.fn.selectEnd = function()
		{
			// Get the first element.
			var el = this.length?this[0]:null;

			// Select the end.			
			if (el) { $.selectEnd(el); }			

			return this;
		};

	// Remove a named style from this collection of element.
	$.fn.removeStyle = function(name)
		{
			// Convert the style name to a lower-case string array.
			if (!name)
				return this;
			else if (name.toLowerCase)
				name = [ name.toLowerCase() ];
			else if (name.length)
			{
				for (var i=0;i<name.length;i++)
					name[i] = (''+name[i]).toLowerCase();
			}
			else
				return this;

			// Remove the styles from each of the elements.
			return this.each(function(i) {

				var el = $(this);
				var styles = (el.attr('style')||'').split(';');
				
				for (var i=styles.length-1;i>=0;i--)
				{
					var style = $.trim(styles[i].split(':')[0]).toLowerCase();
					for (var j=0;j<name.length;j++)
					{
						// If we found a matching style (or it matches a meta-style).
						if (style==name[j] || style.indexOf(name[j]+'-')==0)
						{
							// Remove it.
							if (i==styles.length-1)
								styles.pop();
							else if (i==0)
								styles.shift();
							else
								styles.splice(i,i);

							break;
						}
					}
				}

				// Assign the changed style.
				var newstyles = styles.join(';');
				if (newstyles)
					el.attr('style',newstyles);
				else
					el.removeAttr('style');

			});
		},

	// Get the first child document inside an iframe.
	$.fn.frameDoc = function()
		{
			// If the jQuery object has an IFRAME in its collection, return a jquery element of the first child document found.
			var doc;
			for (var i=0;i<this.length;i++)
			{
				if (this[i].nodeName)
				{
					switch (this[i].nodeName.toUpperCase())
					{
						case "IFRAME":
						case "FRAME":
							doc = this[i].contentDocument ? this[i].contentDocument : this[i].contentWindow ? this[i].contentWindow.document : null;
							break;
						default:
							if (this[i].body!='undefined') { doc = this[i]; }
							break;
					}
				}
				if (doc) { return new $(doc,doc); }
			}
			return new $();
		};

	// Get the first child document body inside an iframe.
	$.fn.frameBody = function()
		{
			// If the jQuery object has an IFRAME in its collection, return a jquery element of the first child document found.
			var body;
			for (var i=0;i<this.length;i++)
			{
				if (this[i].nodeName)
				{
					switch (this[i].nodeName.toUpperCase())
					{
						case "IFRAME":
						case "FRAME":
							body = this[i].contentDocument ? this[i].contentDocument.body : this[i].contentWindow ? this[i].contentWindow.document.body : null;
							break;
						case "BODY":
							body = this[i];
							break;
						default:
							if (this[i].body!='undefined')
							{
								if (this[i].body==null) { this[i].appendChild(this[i].createElement('BODY')); }
								body = this[i].body;
							}
							break;
					}
				}
				if (body) { return new $(body); }
			}
			return new $();
		};

	// Get the first child document body inside an iframe.
	$.fn.frameAppend = function(a)
		{
			if (!$.browser.msie)
			{
				// For mozilla/opera browers, perform a standard jQuery append.
				this.frameBody().append(a);
			}
			else
			{
				// MSIE doesn't allow node elements to be created in one frame or document and appended to the other.
				// In this case, get the string representation of the elements and append the HTML directly.  Note that this
				// will break any event handling set in code.
				var body = this.frameBody();
				if (body.length>0) { body = body[0]; }
				else { return this; }

				// For each element the supplied collection, add it to the document body of the iframe.
				$.each(arguments,function(i,arg)
					{
						if (!arg) return;
						if (arg.constructor == Number) { arg = arg.toString(); }
						if (arg.outerHTML) { arg = arg.outerHTML; }
						body.innerHTML += arg;
					});
			}
			return this;
		};

	// When pressing enter inside a form element, have it submit itself (rather than the first submit button on the whole page).
	$.fn.selfsubmit = function()
		{
			var btn = this.find("input:submit,input:image,a[href*='__doPostBack']:not([onclick])").eq(0);
			this.find('input:text,input:password').data('submitbtn',btn).onenter(function(e){
				var btn = $(this).data('submitbtn');
				if (btn && btn.length)
				{
					btn[0].click();
					return false;
				}
			});
			return this;
		};

	// Deserializes row data to an object.
	$.fn.getRow = function(attr)
		{
			if (!this.length)
				return {};

			// And its row.
			var row = this[0].$row;
			if (!row)
			{
				// Initialize it if it doesn't exist already.
				try { row = eval('('+this.attr(attr||'_row')+')') }
				catch(ex) { row = {}; }
				this[0].$row = row = row||{};
			}

			// Return the row.
			return row;
		};

	// Log the current state.
	$.fn.log = function(msg)
		{
			if (console) console.log("%s: %o", msg, this);
			return this;
		};

	$.extend(
	{
		// Returns a div overlay, which can be faded in to cover the page.
		_pageOverlay : null,
		_overlayFrame : null,
		pageOverlay : function(click)
			{
				if (!$._pageOverlay)
				{
					// If there are any page floaters, the iframe fix below the div will obscure them.
					if ($.pageFloaters().length>0)
					{
						$._overlayFrame = $('<iframe src="javascript:;" style="position:absolute;left:0px;top:0px;border:0;z-index:0;display:none;"></iframe>');
						$._overlayFrame.css({opacity:0});
						$._overlayFrame.appendTo(document.body);
					}
					$._pageOverlay = $('<div id=c_pageOverlay style="position:absolute;z-index:1;top:0px;left:0px;background:#000;border:none;display:none;"></div>');
					$._pageOverlay.css({opacity:0});
					$._pageOverlay.appendTo(document.body);
					if (click) { $._pageOverlay.click($.isFunction(click) ? click : function(e) { $.overlayOut(); }); }
				}
				return $._pageOverlay;
			},

		// Get the size of the overlay.
		overlaySize : function()
			{
				// Get the size of the page and window -- the larger of the two will be used for the overlay size.
				var p1 = $(document.body).absPosition();
				var p2 = $(window).absPosition();
				var l = p1.left<p2.left?p1.left:p2.left;
				var t = (p1.top<p2.top?p1.top:p2.top)+$.toInt($(document.body).css('paddingTop'));
				var w = p1.width>p2.width?p1.width:p2.width;
				var h = p1.height>p2.height?p1.height:p2.height;
				return {
							left:l,
							top:t,
							width:w,
							height:h
					   };
			},

		// Fade the in the overlay.
		overlayIn : function(speed,opacity,fn)
			{
				if ($.isFunction(speed))
				{
					fn = speed;
					speed = null;
					opacity = null;
				}
				else if ($.isFunction(opacity))
				{
					fn = opacity;
					opacity = null;
				}
				
				// Add the display block to the page first, as this will affect the document size properties for msie.
				$.pageOverlay().css({display:'block',width:0,height:0});

				var css = $.overlaySize();
				$.pageOverlay().css(css);
				if ($._overlayFrame) { $._overlayFrame.css(css); }
				$.pageOverlay().animate( { opacity:opacity||.6}, speed||400, fn );
				$(window).bind('resize',$.resizeOverlay);
			},

		// Fade out the overlay.
		overlayOut : function(speed,fn)
			{
				var ol = $.pageOverlay();
				ol.animate( { opacity:0}, speed||400, function()
					{
						ol.css({display:'none'});
						if ($._overlayFrame) { $._overlayFrame.css({display:'none'}); }
						if ($.isFunction(fn)) { fn(); }
					});
				$(window).unbind('resize',$.resizeOverlay);
			},

		// Returns an array of "floaters", or elements that will always float on top of a later, such as dropdownlists in msie6.
		// Note that flash can also have this problem, but this can be solved by adding the wmode="transparent" attribute and
		// wrapping the flash object in an element with a z-index:0.  This solution works in IE6, IE7, FF1.5, FF2.0 and Opera.
		// This is a preferred solution than adding flash objects as floaters.
		_pageFloaters : null,
		pageFloaters : function()
			{
				if (jQuery._pageFloaters==null)
				{
					var f = new Array();
					// IE6 will put select objects in front of other layers.
					if (jQuery.browser.msie6) { jQuery('select').each(function() { f.push(this); } ); }
					jQuery._pageFloaters = f;
				}
				return jQuery._pageFloaters;
			},

		// When the window is resized, resize the overlay.
		resizeOverlay : function()
			{
				var css = $.overlaySize();
				$.pageOverlay().css(css);
				if ($._overlayFrame) { $._overlayFrame.css(css); }
			},

		// Popup a url in a thickbox iframe.
		thickbox : function(o)
			{
				// Get the parameters.
				o = o||{};
				var url = o.url;
				var width = o.width||600;
				var height = o.height||400;
				var noclose = o.noclose;

				// Ensure we at least have a url.
				if (!url) return;
				
				// Create the div and pop it up in the middle of the viewport.
				var scroll = (o.noscroll && ' scrolling="no"')||'';
				var div = $('<div id="tb_div" style="display:none;position:absolute;z-index:100"><div class="tb_close" style="text-align:right"><input type="button" value="Close"></div><iframe id="tb_iframe" frameborder="0" src="'+url+'"'+scroll+'></iframe></div>')
					.appendTo(document.body)
					.css({width:width,height:height})
					.relativePos(window,{center:true})
					.css({opacity:0,display:'none'});


				// Get the button.
				var btn = div.find('div.tb_close');
				if (noclose)
					btn.hide();
				else
					btn.find('input:button').click($.closeThickbox);

				// Get the dimensions of the iframe.
				var w = div.innerWidth();
				var h = div.innerHeight() - (o.noclose ? 0 : btn.outerHeight());
				div.find('iframe').css({width:w,height:h});

				// Bring in the overlay and fade in the popup.
				$.overlayIn(function(){ div.css({display:'block'}).fadeTo(300,1);div=null; });
			},

		// Close any thickbox popups.
		closeThickbox : function()
			{
				// Get the thickbox div.
				var div = $('#tb_div');
				
				// Fade the div and the overlay.
				div.fadeTo(300,0,function() { $.overlayOut(); div.remove(); div=null; });
			},

		// Move the cursor to the end of a control.
		selectEnd : function(control)
			{
				if (control.focus) { control.focus(); }
				if (control.createTextRange)
				{
					var range = control.createTextRange();
					range.collapse(false);
					range.select();
				}
				else if (control.setSelectionRange)
				{
					var length = control.value.length;
					control.setSelectionRange(length, length);
				}
			},

		// Insert text in the text area at the specified position.
		selectionInsert : function(area,insert)
			{
				if (area.setSelectionRange)
				{
					// Get the selection bounds
					var start = area.selectionStart;
					var end = area.selectionEnd;

					//Break up the text by selection
					var text = area.value;
					var pre = String.substring(text, 0, start);
					var sel = String.substring(text, start, end);
					var post = String.substring(text, end);

					//Insert the text at the beginning of the selection
					text = pre + insert + sel + post;

					//Put the text in the textarea
					area.value = text;

					//Re-establish the selection, adjusted for the added characters.
					area.selectionStart = start + insert.length;
					area.selectionEnd = end + insert.length;
					return false;
				}
				else if (document.selection && document.selection.createRange)
				{
					area.focus();
					var range = document.selection.createRange();
					range.text = insert;
					range.collapse(false);
					return false;
				}
			},

		// Insert text in the text area at the specified position.
		selectionWrap : function(area,wrap1,wrap2)
			{
				if (area.setSelectionRange)
				{
					// Get the selection bounds
					var start = area.selectionStart;
					var end = area.selectionEnd;

					//Break up the text by selection
					var text = area.value;
					var pre = String.substring(text, 0, start);
					var sel = String.substring(text, start, end);
					var post = String.substring(text, end);

					//Insert the text at the beginning of the selection
					text = pre + wrap1 + sel + wrap2 + post;

					//Put the text in the textarea
					area.value = text;

					//Re-establish the selection, adjusted for the added characters.
					area.focus();
					area.setSelectionRange(start+wrap1.length,end+wrap1.length);
					return false;
				}
				else if (document.selection && document.selection.createRange)
				{
					area.focus();
					var range = document.selection.createRange();
					range.text = wrap1 + range.text + wrap2;
					range.moveEnd('character',0-wrap2.length);
					range.collapse(false);
					range.select();
					return false;
				}
			},

		// Remove text at the cursor position, if it exists.
		removeAtSelection : function(area,remove,before)
			{
				// Get the start position.
				var text = area.value;

				if (area.setSelectionRange)
				{
					// Get the selection bounds
					var start = area.selectionStart-(before?remove.length:0);
					var end = area.selectionEnd+(before?0:remove.length);

					//Break up the text by selection
					var text = area.value;
					var pre = String.substring(text, 0, start);
					var sel = String.substring(text, start, end);
					var post = String.substring(text, end);

					if (sel==remove)
					{
						// Remove the selection.
						area.value = pre + post;

						//Re-establish the selection, adjusted for the removed characters.
						area.focus();
						area.setSelectionRange(start+start);
						return false;
					}
				}
				else if (area.createTextRange)
				{
					// Get the selection range.
					area.focus();
					var sel	= document.selection.createRange();

					// Expand it to cover the characters to remove.
					if (before)
						sel.moveStart('character',0-remove.length);
					else
						sel.moveEnd('character',remove.length);

					// If we have a match, remove it.
					if (sel.text==remove)
					{
						sel.text = '';
						sel.select();
						return false;
					}
				}
			},

		// Get the start position of the selected area.
		startPosition : function(area)
			{
				// Get the start position.
				var pos;
				if (area.setSelectionRange)
					return area.selectionStart;
				else if (document.selection) {
					area.focus();
					var c = "\001";
					var sel	= document.selection.createRange();
					var txt	= sel.text;
					var dul	= sel.duplicate();
					var len	= 0;
					try{ dul.moveToElementText(area); }catch(e) { return 0; }
					sel.text = txt + c;
					len	= (dul.text.indexOf(c));
					sel.moveStart('character',-1);
					sel.text	= "";
					return len;
				}
				else
					return -1;
			},

		// Resize an element so there is no scrollbar needed.
		autoHeight : function(e)
			{
				var s = this.scrollHeight;
				var o = this.offsetHeight;
				if (s&&s>o)
					this.style.height = s+'px';
			},

		// Self-contained function to handle the hoverClass methods (preventing unnecessary closures).
		overClass : function(e)
			{
				if (this.$self) { this.$self.addClass(this.$overClass); }
			},

		// Self-contained function to handle the hoverClass methods (preventing unnecessary closures).
		outClass : function(e)
			{
				if (this.$self) { this.$self.removeClass(this.$overClass); }
			},

		// Self-contained function to handle the overImage methods (preventing unnecessary closures).
		overImage : function(e)
			{
				if (this.$self) { this.$self.attr('src',this.$self.attr('_overimg')); }
			},

		// Self-contained function to handle the outImage methods (preventing unnecessary closures).
		outImage : function(e)
			{
				if (this.$self) { this.$self.attr('src',this.$self.attr('_outimg')); }
			},

		// Add a hidden image to the page with the supplied url to preload the image.
		preload : function(img)
			{
				// Get the image preloader.
				var preloader = $('#hidden_preloader');
				if (!preloader.length) { preloader = $('<div id="hidden_preloader" style="display:none;"></div>').appendTo(document.body); }

				var imgs = $.makeArray(img);
				for (var i=0;i<imgs.length;i++)
				{
					// Add the image to the preloader if it isn't already there.
					var img = imgs[i];
					if (preloader.find("img[src$='"+img+"']").length==0)
						preloader.append('<img src="'+img+'">');
				}
			},

		aposRegex : new RegExp("'","g"),
		quotRegex : new RegExp('"','g'),

		// Encode a string for safe storage in a text field, as well as save for form/querystring submission to the server.
		encode : function(val)
			{
				if (val==null)
					return '';
				else
					return encodeURIComponent(val+"").replace($.aposRegex,'%27').replace($.quotRegex,'%22');
			},

		// Decode an encoded string.
		decode : function(val)
			{
				return val?decodeURIComponent(val):'';
			},

		// Log something to firebug.
		log : function(obj)
			{
				if (typeof(firebug)!='undefined')
					firebug.d && firebug.d.console && firebug.d.console.cmd && firebug.d.console.cmd.log(obj);
				else if (typeof(console)!='undefined')
					console && console.log && console.log(obj);
			},

		// Inspect an element in firebug.
		inspect : function(el)
			{
				if (el && el.jquery)
					el = el.length ? el[0] : null;
				if (!el) return;
				if (typeof(firebug)!='undefined')
					firebug.inspect && firebug.inspect(el);
			},

		// Return a text value, parsed as an integer.  Return any NaN as 0.
		toInt : function(amount)
			{
				if (!amount) return 0;
				if (typeof(amount)=='string')
				{
					// Format the number, removing any leading zeroes so it doesn't get treated as an octal.
					amount = $.trimLeft(amount.replace(/[^\d.-]/g,''),'0');
				}
				var val = parseInt(amount);
				if (isNaN(val)) { return 0; }
				else { return val; }
			},

		// Conver the value to an 8-bit number.
		toByte : function(amount)
			{
				var val = $.toInt(amount);
				if (val<0) return 0;
				else if (val>255) return 255;
				else return val;
			},

		// Trim from the left side of a string.
		trimLeft : function(value,character)
			{
				// Ensure we have a value, converting to a string as necessary.
				if (!value) return '';
				value = value + '';

				// Trim the character (defaulting to a space).
				while (value.charAt(0) == (character||' ')) {
					value = value.substring(1);
				}
				return value;
			},

		// Return a text value, parsed as a float.  Return any NaN as 0.
		toFloat : function(amount)
			{
				if (!amount) return 0.00;
				var factor = 1.00;
				if (typeof(amount)=='string')
				{
					if (/%\s*$/.test(amount)) factor = 0.01;
					amount = amount.replace(/[^\d.-]/g,'');
				}
				var val = parseFloat(amount) * factor;
				if (isNaN(val)) { return 0.00; }
				else { return val; }
			},

		// Return a value, parsed as a boolean.
		toBool : function(val)
			{
				// If no value was supplied, return false.
				if (typeof(val)=='undefined') return false;
				if (val==null) return false;
				
				// If we already have a boolean value, return it.
				if (val==true) return true;
				if (val==false) return false;
				
				// Conver the value to a string and check for a match on a "TRUE" type value.
				switch ((''+val).toUpperCase())
				{
					case "1":
					case "ON":
					case "TRUE":
					case "YES":
					case "SUCCESS":
						return true;
					default:
						return false;
				}
			},

		// Cast a value as a 2-digit currency with comma-seperators.
		currencyFormat : function(amount)
			{
				// Parse the value as a float.
				var i = $.toFloat(amount);
				if(isNaN(i)) { i = 0.00; }

				// Get any minus sign.
				var minus = '';
				if(i < 0) { minus = '-'; }

				// Get both sides of the decimal.
				i = Math.abs(i);
				i = parseInt((i + .005) * 100);
				i = i / 100;

				// Build the string.
				s = new String(i);
				if(s.indexOf('.') < 0) { s += '.00'; }
				if(s.indexOf('.') == (s.length - 2)) { s += '0'; }
				s = minus + s;

				var delimiter = ",";
				var a = s.split('.',2);
				var d = a[1];
				var i = parseInt(a[0]);
				if(isNaN(i)) { return ''; }
				var minus = '';
				if(i < 0) { minus = '-'; }
				i = Math.abs(i);
				var n = new String(i);
				var a = [];
				while(n.length > 3)
				{
					var nn = n.substr(n.length-3);
					a.unshift(nn);
					n = n.substr(0,n.length-3);
				}
				if(n.length > 0) { a.unshift(n); }
				n = a.join(delimiter);
				if(d.length < 1) { amount = n; }
				else { amount = n + '.' + d; }
				amount = minus + amount;
				return '$'+amount;

			},

		// Format a file size in bytes.
		kbFormat : function(bytes,upper)
			{
				if (bytes<1024)
					return bytes+(upper?'B':'b');
				else if (bytes<(1024*1024))
					return (Math.round(bytes/1024*10)/10)+(upper?'KB':'kb');
				else
					return (Math.round(bytes/1024/1024*10)/10)+(upper?'MB':'mb');
			},

		// Format the data with .NET pattern matches.
		dateTimeFormat : function(date,format)
			{
				if (!date) return '';
				if (!format) format = 'd-MMM-yyyy';
				return format.replace($.dateTimeRegex,function()
					{
						return $.dateTimeProcess(date, Array.prototype.slice.call( arguments, 1 ));
					});
			},
		dateTimeRegex : /(d+)|(h+)|(H+)|(m+)|(M+)|(s+)|(t+)|(T+)|(y+)|(D+)/g,
		leadingZero : function(val)
			{
				if (val<10)
					return '0'+val;
				else
					return val;
			},
		dateTimeProcess : function(dateTime,args)
			{
				for (var i=0;i<args.length;i++)
				{
					var key = args[i];
					if (!key) continue;
					switch (i)
					{
						case 0:
							switch (key.length)
							{
								case 1:
									return dateTime.getDate();
								case 2:
									return $.leadingZero(dateTime.getDate());
								case 3:
									switch (dateTime.getDay())
									{
										case 0:
											return 'Sun';
										case 1:
											return 'Mon';
										case 2:
											return 'Tue';
										case 3:
											return 'Wed';
										case 4:
											return 'Thu';
										case 5:
											return 'Fri';
										case 6:
											return 'Sat';
										default:
											return '';
									}
								default:
									switch (dateTime.getDay())
									{
										case 0:
											return 'Sunday';
										case 1:
											return 'Monday';
										case 2:
											return 'Tuesday';
										case 3:
											return 'Wednesday';
										case 4:
											return 'Thursday';
										case 5:
											return 'Friday';
										case 6:
											return 'Saturday';
										default:
											return '';
									}
							}
						case 1:
							var hours = dateTime.getHours();
							if (hours>12) hours-=12;
							return key.length==1 ? hours||12 : $.leadingZero(hours||12);
						case 2:
							return key.length==1 ? dateTime.getHours() : $.leadingZero(dateTime.getHours());
						case 3:
							return key.length==1 ? dateTime.getMinutes() : $.leadingZero(dateTime.getMinutes());
						case 4:
							switch (key.length)
							{
								case 1:
									return dateTime.getMonth()+1;
								case 2:
									return $.leadingZero(dateTime.getMonth()+1);
								case 3:
									switch (dateTime.getMonth()+1)
									{
										case 1:
											return "Jan";
										case 2:
											return "Feb";
										case 3:
											return "Mar";
										case 4:
											return "Apr";
										case 5:
											return "May";
										case 6:
											return "Jun";
										case 7:
											return "Jul";
										case 8:
											return "Aug";
										case 9:
											return "Sep";
										case 10:
											return "Oct";
										case 11:
											return "Nov";
										case 12:
											return "Dec";
										default:
											return '';
									}
								default:
									switch (dateTime.getMonth()+1)
									{
										case 1:
											return "January";
										case 2:
											return "February";
										case 3:
											return "March";
										case 4:
											return "April";
										case 5:
											return "May";
										case 6:
											return "June";
										case 7:
											return "July";
										case 8:
											return "August";
										case 9:
											return "September";
										case 10:
											return "October";
										case 11:
											return "November";
										case 12:
											return "December";
										default:
											return '';
									}
							}
						case 5:
							return key.length==1 ? dateTime.getSeconds() : $.leadingZero(dateTime.getSeconds());
						case 6:
						case 7:
							return (dateTime.getHours()<12 ? 'A' : 'P') + (key.length==1 ? '' : 'M');
						case 8:
							return key.length==2 ? dateTime.getFullYear().toString().substr(2) : dateTime.getFullYear();
						case 9:
							switch (key.length)
							{
								case 1:
									return dateTime.getDate();
								case 2:
									return $.leadingZero(dateTime.getDate());
								default:
									var d = dateTime.getDate();
									switch (d%10)
									{
										case 1:
											return d+'st';
										case 2:
											return d+'nd';
										case 3:
											return d+'rd';
										default:
											return d+'th';
									}
							}
					}
				}
			},

		// Clean up nonstandard html.
		cleanHtml : function(html,indent,absolute)
			{
				// If we don't have anything, return blank.
				if (!html) return '';

				// HTML cleanup regexes.
				var rclean1 = new RegExp('\\s+(?:jQuery\\d+|sizcache|sizset)\\s*=\\s*"[^"]*"','g');
				var rclean2 = new RegExp('</?\\w+\\b','g');
				var rclean3a= new RegExp('<\\w[^>]*?>','g');
				var rclean3b= new RegExp('([\\w-\\$]+)\\s*=\\s*(?:([\'""])([\\s\\S]*?)\\2|([^\\s\'"">]+))','g');
				var rclean4 = new RegExp('"','g');
				var rclean5 = new RegExp('style\\s*=\\s*"[^"]*"','gi');
				var rclean6 = new RegExp('\\b([\\w-]+)\\s*:\\s*([^;]+)','g');
				var rclean7 = new RegExp('^[^<]*','g');
				var rclean8 = new RegExp('(</?)(\\w+\\b|\\!)([^>]*>)([^<]*)','g');
				var rclean9 = new RegExp('^\s+');

				// Clean the tag and attributes so they are clean.
				html = html.replace(rclean1,'')
					.replace(rclean2,function(m){return m.toLowerCase();})
					.replace(rclean3a,function(m)
						{
							return m.replace(rclean3b,function(m,m1,m2,m3,m4)
								{
									return m1+'="'+(m3||m4||'').replace(rclean4,'')+'"';
								});
						})
					.replace(rclean5,function(m)
						{
							return m.replace(rclean6,function(m,m1,m2){return m1.toLowerCase()+':'+m2;});
						});

				if (!absolute)
				{
					var body = $(document.body);
					var _root = body.attr('_root');
					var _base = body.attr('_base').replace(/([\.])/g,'\\$1');
					var absolute = new RegExp("(<[^>]+)((?:src|href|background)\s*=\s*['\"]?)(?:"+_root+'|'+_base+')','gi');
					html = html.replace(absolute,function(m,m1,m2){
						if (m1.startsWith('<base '))
							return m;
						else
							return m1+m2;
					});
				}

				// If we've been asked to indent the HTML.
				if (indent)
				{
					// Initialize the parent-child rules.
					var nochild = function(tag,parent) {
						switch (tag)
						{
							case 'html':
								return true;
							case 'li':
								return parent.tag=='li';
							default:
								return false;
						}
					};

					// Initialize the self-closing rules.
					var selfclosing = function(item) {
						if (!item) return false;
						if (item.html.endsWith('/>'))
							return true;
						else
						{
							switch (item.tag)
							{
								case 'area':
								case 'base':
								case 'br':
								case 'hr':
								case 'img':
								case 'input':
								case 'link':
								case 'meta':
								case 'param':
									return true;
								default:
									return false;
							}
						}
					};

					// Initialize the array of page objects.
					var page = {};
					page.items = [];

					// If we have a literal token at the beginning of the HTML, add it directly.
					var m = rclean7.exec(html);
					if (m)
					{
						var start = m[0].replace(/^\s+/,'').replace(/\s+$/,' ');
						if (start)
							page.items.push({text:start});
					}

					// Iterate through the HTMLTags / literal content.
					var literal = null;
					var parent = page;
					while(m=rclean8.exec(html))
					{
						// We will ignore the tbody tag completely.
						if (m[2]=='tbody')
							continue;

						// Is this a closing tag?
						var close = m[1]=='</' || (m[2]=='!' && m[3].endsWith('-->'));

						var pop = null;
						if (close)
						{
							// If we're closing the tag, walk up the tree.
							var p = parent;
							while (p && p.tag!=m[2]) { p = p.parent; }

							// If we found NO matching opening tag, we have some illegal HTML.  Add the closing tag directly.
							if (!p)
							{
								if (!parent.items) parent.items = [];
								parent.items.push({text:m[0]});
							}
							else
							{
								// Otherwise, add the closing tag.
								p.closing = m[1]+m[2]+m[3];
								if (m[4]) p.closingText = m[4];

								// Walk up the tree one level.
								parent = p.parent;
							}
						}
						else
						{
							// Create a new item.
							var item = {
								tag : m[2],
								html : m[1]+m[2]+m[3],
								text : m[4]
							};

							// Fix msie-bug of not always closing LI elements.
							if (item.tag=='li') item.closing = '</li>';

							// If this tag can't be a child of the current element, move up on level so it is added as a sibling.
							if (parent.parent && nochild(item.tag,parent))
								parent = item.parent = parent.parent;

							// Note the relationship and add the item to the array.
							if (!parent.items) parent.items = [];
							if (parent.items.length) item.prev = parent.items[parent.items.length-1];
							item.parent = parent;
							parent.items.push(item);

							// Unless self-closed, the item is the new parent.
							if (selfclosing(item))
								item.self = true;
							else
								parent = item;
						}
					}

					// Certain elements that contain only one child will not be indented in a nested fashion.
					var nesting = function(parent) {
						if (!parent || !parent.items || !parent.items.length) return false;
						switch (parent.tag)
						{
							case 'html':
							case 'head':
							case 'body':
								return true;
							case 'i':
							case 'b':
							case 'em':
							case 'strong':
								return false;
							case 'td':
							case 'a':
								return parent.items.length>1 || parent.items[0].tag=='div';
							default:
								return true;
						}
					};

					// If we've turned on nesting, should we turn it back on at the end of certain tags?
					var renesting = function(parent) {
						if (!parent) return false;
						switch (parent.tag)
						{
							case 'td':
							case 'tr':
								return true;
							default:
								return false;
						}
					};

					// Clean a set of named styles.
					var cleanstyle = function(text,indent) {

						// If we have no content, return an empty string.
						text = $.trim(text);
						if (!text) return '';

						// Declare the css regex.
						var r_comments = /\/\*[\s\S]*?\*\//g;
						var r_style = /([\w:\.#,\*\-\\\s]+)\{([^\}]*)\}/g;
						var r_rgb = /rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i;
						var r_selector = /(^| |,)(\w+)(\.\w+)?/g;

						// Remove the commends and build the CSS object.
						var sheet = text.replace(r_comments,'');
						var css = [];
						var m;
						while(m=r_style.exec(sheet))
						{
							var s = $.trim(m[1]).replace(/\s+/,' ');
							css.push({s:s,v:$.trim(m[2])});
						}

						// Iterate through the css rules.
						var sb = ['\n'];
						for (var i=0;i<css.length;i++)
						{
							// Add the selector.
							var name = $.trim(css[i].s.replace(r_selector,function(m,m1,m2,m3){return m1+m2.toLowerCase()+(m3||'');}));
							if (indent) sb.push(indent);
							if (i==0) sb.push('\t');
							sb.push(name);
							sb.push(' {\n');

							// Get the rules for the selector.
							var styles = css[i].v.split(';');
							for (var j=0;j<styles.length;j++)
							{
								var style = styles[j].split(':');
								if (style.length==2)
								{
									// If we have an RGB color definition, convert it to a hex color.
									if (r_rgb.test(style[1])) {
										style[1] = style[1].replace(r_rgb,function(m,m1,m2,m3){
											var c = {};
											c.r = parseInt(m1);
											c.g = parseInt(m2);
											c.b = parseInt(m3);
											var hex = 
												(c.r<16?'0':'')+c.r.toString(16)+
												(c.g<16?'0':'')+c.g.toString(16)+
												(c.b<16?'0':'')+c.b.toString(16);
											return '#'+hex;
										});
									}

									// Add the style rule.
									if (indent) sb.push(indent);
									sb.push('        ');
									sb.push($.trim(style[0].toLowerCase()));
									sb.push(': ');
									sb.push($.trim(style[1]));
									sb.push(';\n');
								}
							}

							// Close off the selector.
							if (indent) sb.push(indent);
							sb.push('\t');
							sb.push('}\n');
							if (indent) sb.push(indent);
						}

						return sb.join('');
					};

					// Clean multiple white space so it is just a single one.
					var clean = function(text) {
						return (''+text).replace(/\s+/g,' ');
					};

					// Trim the text after the opening tag.
					var trimcontent = function(text,parent,indent) {
						// If we don't have parent elements, or we have a PRE tag, return the text unchanged.
						if (!parent||!parent.parent||!text||parent.tag=='pre') return text;

						if (parent.tag=='style')
							// Style content gets cleaned up.
							return cleanstyle(text,indent);
						else if (parent.tag=='head' || parent.parent.tag=='head')
							// Anything in the head gets fully trimmed.
							return $.trim(clean(text));
						else
							// Clean any leading spaces of the opening content inside a tag.
							return clean(text).replace(/^\s+/,'');
					};

					// Trim the text after the closing tag.
					var trimend = function(text,parent) {
						// If we don't have parent elements, return the text unchanged.
						if (!parent||!parent.parent||!text) return text;

						// Anything in the head gets fully trimmed.
						if (parent.tag=='head' || parent.parent.tag=='head')
							return $.trim(clean(text));
						else
						{
							if (parent.parent.items[parent.parent.items.length-1]==parent)
								// If this is the last child tag of its parent, trim the white-space off the end.
								return clean(text).replace(/\s+$/,'');
							else
								// Otherwise clean the white space of the text.
								return clean(text);
						}
					};

					// Recursive function to render the HTML.
					var render = function(parent,sb,level,nest) {
						if (parent)
						{
							// Make any special adjustments.
							switch (parent.tag)
							{
								case 'html':
									if (level>0) level -=1;
									break;
								case 'head':
								case 'body':
									level -=1;
									break;
							}
							
							// Get the size of the indent.
							var tabs = [];
							for (var i=0;i<level;i++) tabs.push('\t');
							var indent = tabs.join('');

							// Add any opening html.
							if (parent.html)
							{
								// Add an indent if we're nesting this item.
								if (nest && sb[sb.length-1]=='\n')
									sb.push(indent);
								sb.push(parent.html);
							}

							if (parent.self)
							{
								// A self-closing item will get a line-feed after it unless we're not nesting.
								if (nest) sb.push('\n');
							}
							else
							{
								// If the current item shouldn't be nested, note that fact.
								if (!nesting(parent))
									nest = false;
								else if (parent.tag)
								{
									if (parent.tag!='p') sb.push('\n');
									nest = true;
								}
							}

							// Record if we're nesting the parent or not.
							parent.nest = nest;

							// If there is any literal text after the tag opening, add it.
							if (parent.text)
							{
								var text = trimcontent(parent.text,parent,indent);
								if (text)
								{
									if (nest && sb.length && sb[sb.length-1]=='\n')
									{
										sb.push(indent);
										if (parent.tag!='br') sb.push('\t');
									}
									sb.push(text);
									if (nest) sb.push('\n');
								}
							}
							
							// If we have child elements, recurse this method.
							if (parent.items && parent.items.length)
							{
								for (var i=0;i<parent.items.length;i++)
								{
									// If the first tag is a hard line break, add a line-feed.
									if (nest && parent.items[i].tag=='br' && sb.length>0 && sb[sb.length-1]!='\n') sb.push('\n');

									// Render each of the child elements.
									render(parent.items[i],sb,level+1,nest);
								}
							}

							// If there is any closing html, add it.
							if (parent.closing)
							{
								if (nest && sb[sb.length-1]=='\n') sb.push(indent);
								sb.push(parent.closing);
								if (parent.parent) nest = parent.parent.nest;
							}

							// If there is any closing text, add it.
							if (parent.closingText)
							{
								var text = trimend(parent.closingText,parent);
								if (text) sb.push(text);
							}

							// If we need to add a line feed, do so.
							if (parent.closing && nest)
								sb.push('\n');
						}
					};
					
					var sb = [];
					render(page,sb,-1,true);
					html = sb.join('');
				}
				
				return html;
			}
	});

	// Get a dictionary of query variables.
	$.getQuery = function()
	{
		// Grab the querystring part of the url.
		var query = {};
		var items = window.location.href.split('?').pop().split('&');
		
		// Iterate through each of the elements.
		for (var i=0;i<items.length;i++)
		{
			// Assign anything that has a proper name/value pair.
			var pair = items[i].split('=');
			if (pair.length==2)
				query[pair[0].toLowerCase()] = pair[1];
		}
		
		return query;
	};

		// Get a querystring value.
	$.getQueryString = function(field)
	{
		// Return the querystring.
		if (field) field = field.toLowerCase();
		return $.getQuery()[field];
	};

	// Add an item to an array, if it doesn't exist already.
	Array.prototype.addItem = function(item)
	{
		if (!item) return;
		for (var i=0;i<this.length;i++)
		{
			var el = this[i];
			if (el==item)
				return this;
		}
		this.push(item);
		return this;
	};

	// Remove an item from an array, if it exists.
	Array.prototype.removeItem = function(item)
	{
		if (!item&&item!='') return;
		for (var i=0;i<this.length;i++)
		{
			var el = this[i];
			if (el==item)
			{
				if (i==0)
					this.shift();
				else if (i==this.length-1)
					this.pop();
				else
					this.splice(i,1);
				return this;
			}
		}
		return this;
	};

	// What is the index of this item in the array?
	Array.prototype.indexOf = function(item)
	{
		if (!item) return;
		for (var i=0;i<this.length;i++)
		{
			var el = this[i];
			if (el==item)
				return i;
		}
		return -1;
	};

	// Case-insensitive string comparison match.
	String.prototype.startsWith = function(data)
	{
		if (!data) return false;
		var data2 = (''+data).toLowerCase();
		return this.toLowerCase().indexOf(data2) == 0;
	};

	// Case-insensitive string comparison match.
	String.prototype.endsWith = function(data)
	{
		if (!data) return false;
		var data2 = (''+data).toLowerCase();
		return this.toLowerCase().indexOf(data2) == this.length-data2.length;
	};

})(jQuery);
