/*!
 * Zedcore CMS javascript framework.
 * Copyright 2009-2010 Zedcore Systems Ltd
 */

/*global YAHOO: false, ZC: true */
/*jslint browser: true, white: true, strict: true */

/**
 * Base classes for our javascript framework
 * @module Base
 */
if (typeof ZC === "undefined" || !ZC) {
    /**
     * The ZC global namespace object.  If ZC is already defined, the
     * existing ZC object will not be overwritten so that defined
     * namespaces are preserved.
	 * @class ZC
	 * @namespace
     * @static
     */
    var ZC = {};
}

/**
 * The following is based on the YAHOO.namespace function in YUI. It's
 * basically the same, but uses ZC as the namespace instead.
 * @method Namespace
 * @param  {String*} arguments 1-n namespaces to create
 * @return {Object}  A reference to the last namespace object created
 */
ZC.Namespace = function() {
    var a=arguments, o=null, i, j, d;
    for (i=0; i<a.length; i=i+1) {
        d=a[i].split(".");
        o=ZC;

        // ZC is implied, so it is ignored if it is included
        for (j=(d[0] == "ZC") ? 1 : 0; j<d.length; j=j+1) {
            o[d[j]]=o[d[j]] || {};
            o=o[d[j]];
        }
    }

    return o;
};

(function(){
// aliases for oft-used objects
var L = YAHOO.lang, Dom = YAHOO.util.Dom, Evt = YAHOO.util.Event;

// from dojo - used by the clone method below
var extraNames, extraLen, empty = {};
for(var i in {toString: 1}){ extraNames = []; break; }
extraNames = extraNames || ["hasOwnProperty", "valueOf", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "constructor"];
extraLen = extraNames.length;

// private var for GetScrollbarWidth
var iScrollbarWidth;

var fnArrayWrap = function(aArray, fnCallback, oThis, fnImpl)
{
	if (L.isUndefined(aArray))
		return [];

	if (!L.isFunction(fnCallback))
		throw new TypeError();

	if (!L.isUndefined(aArray.length))
	{
		return fnImpl.call(aArray, fnCallback, oThis);
	}
	else
	{
		return fnImpl.call(this.Keys(aArray), this.ObjectArrayWrapper(aArray, fnCallback), oThis);
	}
}

var sprintfRegex = /%%|%(\d+\$)?([-+#0 ]*)(\*\d+\$|\*|\d+)?(\.(\*\d+\$|\*|\d+))?([scboxXuidfegEG])/g;
/**
 * @class Util
 * @namespace ZC
 * @static
 */
ZC.Util = {
	
	sCurrentDomain: 'Core',
	
	/* * * * I18N UTILS * * * */
	/* See http://office.zedcore.com/wiki/index.php/CMS:Spec:Javascript_L10N */
	
	/**
	 * Javascript sprintf, from http://hexmen.com/js/sprintf.js
	 * @param {String} sFormat format string
	 * @param {mixed} [...] parameters
	 */
	sprintf: function() {
		function pad(str, len, chr, leftJustify) {
			var padding = (str.length >= len) ? '' : Array(1 + len - str.length >>> 0).join(chr);
			return leftJustify ? str + padding : padding + str;
		}

		function justify(value, prefix, leftJustify, minWidth, zeroPad) {
			var diff = minWidth - value.length;
			if (diff > 0) {
				if (leftJustify || !zeroPad) {
				value = pad(value, minWidth, ' ', leftJustify);
				} else {
				value = value.slice(0, prefix.length) + pad('', diff, '0', true) + value.slice(prefix.length);
				}
			}
			return value;
		}

		function formatBaseX(value, base, prefix, leftJustify, minWidth, precision, zeroPad) {
			// Note: casts negative numbers to positive ones
			var number = value >>> 0;
			prefix = prefix && number && {'2': '0b', '8': '0', '16': '0x'}[base] || '';
			value = prefix + pad(number.toString(base), precision || 0, '0', false);
			return justify(value, prefix, leftJustify, minWidth, zeroPad);
		}

		function formatString(value, leftJustify, minWidth, precision, zeroPad) {
			if (precision != null) {
				value = value.slice(0, precision);
			}
			return justify(value, '', leftJustify, minWidth, zeroPad);
		}

		var a = arguments, i = 0, format = a[i++];
		return format.replace(sprintfRegex, function(substring, valueIndex, flags, minWidth, ignore, precision, type) {
			if (substring == '%%') return '%';

			// parse flags
			var leftJustify = false, positivePrefix = '', zeroPad = false, prefixBaseX = false;
			for (var j = 0; flags && j < flags.length; j++) switch (flags.charAt(j)) {
				case ' ': positivePrefix = ' '; break;
				case '+': positivePrefix = '+'; break;
				case '-': leftJustify = true; break;
				case '0': zeroPad = true; break;
				case '#': prefixBaseX = true; break;
			}

			// parameters may be null, undefined, empty-string or real valued
			// we want to ignore null, undefined and empty-string values

			if (!minWidth) {
				minWidth = 0;
			} else if (minWidth == '*') {
				minWidth = +a[i++];
			} else if (minWidth.charAt(0) == '*') {
				minWidth = +a[minWidth.slice(1, -1)];
			} else {
				minWidth = +minWidth;
			}

			// Note: undocumented perl feature:
			if (minWidth < 0) {
				minWidth = -minWidth;
				leftJustify = true;
			}

			if (!isFinite(minWidth)) {
				throw new Error('sprintf: (minimum-)width must be finite');
			}

			if (!precision) {
				precision = 'fFeE'.indexOf(type) > -1 ? 6 : (type == 'd') ? 0 : void(0);
			} else if (precision == '*') {
				precision = +a[i++];
			} else if (precision.charAt(0) == '*') {
				precision = +a[precision.slice(1, -1)];
			} else {
				precision = +precision;
			}

			// grab value using valueIndex if required?
			var value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++];

			switch (type) {
			case 's': return formatString(String(value), leftJustify, minWidth, precision, zeroPad);
			case 'c': return formatString(String.fromCharCode(+value), leftJustify, minWidth, precision, zeroPad);
			case 'b': return formatBaseX(value, 2, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
			case 'o': return formatBaseX(value, 8, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
			case 'x': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
			case 'X': return formatBaseX(value, 16, prefixBaseX, leftJustify, minWidth, precision, zeroPad).toUpperCase();
			case 'u': return formatBaseX(value, 10, prefixBaseX, leftJustify, minWidth, precision, zeroPad);
			case 'i':
			case 'd': {
					  var number = parseInt(+value);
					  var prefix = number < 0 ? '-' : positivePrefix;
					  value = prefix + pad(String(Math.abs(number)), precision, '0', false);
					  return justify(value, prefix, leftJustify, minWidth, zeroPad);
				  }
			case 'e':
			case 'E':
			case 'f':
			case 'F':
			case 'g':
			case 'G':
					  {
					  var number = +value;
					  var prefix = number < 0 ? '-' : positivePrefix;
					  var method = ['toExponential', 'toFixed', 'toPrecision']['efg'.indexOf(type.toLowerCase())];
					  var textTransform = ['toString', 'toUpperCase']['eEfFgG'.indexOf(type) % 2];
					  value = prefix + Math.abs(number)[method](precision);
					  return justify(value, prefix, leftJustify, minWidth, zeroPad)[textTransform]();
				  }
			default: return substring;
			}
		});
	},

	/**
	 * Repeats a string
	 *
	 * @param sStr the string to be repeated
	 * @param iMultiplier Number of times to repeat the string
	 */
	StrRepeat: function(sStr, iMultiplier)
	{
		if (iMultiplier < 0)
			YAHOO.log('StrRepeat: iMultiplier should be positive', 'error');
		return new Array(iMultiplier + 1).join('*');
	},

	/**
	 * Trims leading and trailing whitespace from a string.
	 *
	 * @param sStr the string to be trimmed
	 */
	Trim: function(sStr)
	{
		return sStr.replace(/(^\s*|\s*$)/g, '');
	},
	
	/**
	 * Sets the translation domain to search for translations when calls are made to GetText and family
	 *
	 * @param {String} } sTextDomain the domain to use for future translations. Omit for no change.
	 * @return {String}  The text domain that will be used for future translations
	 */
	 TextDomain: function (sTextDomain)
	 {
	 	 if(!L.isUndefined(sTextDomain))
		 	this.sCurrentDomain = sTextDomain;
		 return this.sCurrentDomain;
	 },

	/**
	 * Translates a string.
	 *
	 * @param {String} sStr the string to translate
	 * @return {String} the translated string, or the original string if there's no translation available
	 */
	GetText: function (sStr)
	{        
		var sTextDomain = ZC.Util.TextDomain();
		if (!L.isUndefined(ZC.oTranslations))
		{
			try
			{
				if(!L.isUndefined(ZC.oTranslations[sTextDomain][sStr][1]) && ZC.oTranslations[sTextDomain][sStr][1])
					sStr = ZC.oTranslations[sTextDomain][sStr][1];			
			}
			catch (e ) 
			{
				// This is the only cross-browser way to specify which exceptions are supposed to be caught...
				if (!(e instanceof TypeError))
					throw e;	
			}
		}
			
		return sStr;
	},

	/**
	 * Translates a string, using correct plural form (some languages have up to 4 different options!)
	 *
	 * @param {String} sSingStr the string to translate, singular form
	 * @param {String} sPlurStr the string to translate, plural form
	 * @param {Number} iCount the number to return the string for
	 * @return {String} the translated string, or the original string if there's no translation available
	 */
	NGetText: function (sSingStr, sPlurStr, iCount)
	{
		var sString, nplurals, plural, n = iCount, sTextDomain = ZC.Util.TextDomain();
		
		if (!L.isUndefined(ZC.oTranslations))
		{
			// Try to parse out which plural form this string should be in.
			try
			{
				// This should set nplural - the number of plural forms in this language and also given n == iCount set plural to the correct plural form number.
				eval(ZC.oTranslations[sTextDomain][""]['Plural-Forms']);
				plural = Number(plural);
			}
			catch (e){}
			finally
			{
				// If the plural form could not be figured out or is invalid, default to English/Western Europe style
				if(!L.isNumber(nplurals) || !L.isNumber(plural) || plural > nplurals)
				{
					nplurals = 2;
					plural = (iCount == 1) ? 0 : 1;
				}	
			}                                               
			
			try
			{
				if(!L.isUndefined(ZC.oTranslations[sTextDomain][sSingStr][plural +1]) && ZC.oTranslations[sTextDomain][sSingStr][plural +1])
					sString = ZC.oTranslations[sTextDomain][sSingStr][plural +1];			
			}
			catch (e ) 
			{
				if (!(e instanceof TypeError))
					throw e;	
			}
		}

		if(!sString)
		{
			if(iCount == 1)
				sString = sSingStr;
			else
				sString = sPlurStr;	
		}
		
		return sString;
	},
	
	/**
	 * Translates a string.
	 *
	 * @param {String} sDomain the translation domain to look up the translate in
	 * @param {String} sStr the string to translate
	 * @return {String} the translated string, or the original string if there's no translation available
	 */
	 DGetText: function (sDomain, sStr)
	 {
	 	 var sOldDomain = ZC.Util.TextDomain(), sTranslatedStr;
	 	 ZC.Util.TextDomain(sDomain);
	 	 sTranslatedStr = ZC.Util.GetText(sStr);
	 	 ZC.Util.TextDomain(sOldDomain);
	 	 return sTranslatedStr;
	 },
	 
	 /**
	 * Translates a string.
	 *
	 * @param {String} sDomain the translation domain to look up the translate in
	 * @param {String} sSingStr the string to translate, singular form
	 * @param {String} sPlurStr the string to translate, plural form
	 * @param {Number} iCount the number to return the string for
	 * @return {String} the translated string, or the original string if there's no translation available
	 */
	 DNGetText: function (sDomain, sSingStr, sPlurStr, iCount)
	 {
	 	 var sOldDomain = ZC.Util.TextDomain(), sTranslatedStr;
	 	 ZC.Util.TextDomain(sDomain);
	 	 sTranslatedStr = ZC.Util.NGetText(sSingStr, sPlurStr, iCount);
	 	 ZC.Util.TextDomain(sOldDomain);
	 	 return sTranslatedStr;
	 },
	
	 /**
	  * Replaces {tags} in sContent with values from aTags. Should work the same as the CMS function of the same name.
	  *
	  * @param {String} sContent content to replace tags in
	  * @param {Object} aTags values for tags
	  * @param {Boolean} bBlankUnknownTags if true, then unknown tags are replaced with the empty string. If false (default), they are left intact.
	  * @param {RegExp} oRegex alternative regex to use for tags (used internally by ReplaceHashTags)
	  */
	ReplaceTags: function(sContent, aTags, bBlankUnknownTags, oRegex)
	{
		var fnReplace = function(sStr, sTagName) 
		{ 
			if (L.isUndefined(aTags[sTagName]))
				return bBlankUnknownTags ? "" : sStr;
		   
			return aTags[sTagName];
		}

		if (L.isUndefined(oRegex))
			oRegex = new RegExp('\{(.*?)\}', 'g');

		// Replace anything in between braces with the corresponding value in $aTags
		sContent = sContent.replace(oRegex, fnReplace);

		// HTML editor prefixes URLs with / when we use {tagname} as a URL. Correct the result.
		sContent = sContent.replace('"/http', '"http');

		return sContent;
	},

	/**
	 * Replaces #TAGS in sContent with values from aTags. Should work the same as the CMS function _zc.
	 *
	 * @param {String} sContent content to replace tags in
	 * @param {Object} aTags values for tags
	 */
	ReplaceHashTags: function(sContent, aTags)
	{
		return ZC.Util.ReplaceTags(sContent, aTags, false, new RegExp('#(.+?)\\b', 'g'));
	},

	/* * * * ANIMATION UTILS * * * */

	/**
	 * Wraps the contents of an existing element in a &lt;div&gt; tag, to make
	 * animation easier (table cells have issues).
	 *
	 * e.g:
	 * <pre>&lt;td&gt;&lt;strong&gt;Hello world&lt;/strong&gt;&lt;/td&gt;</pre> 
	 * becomes:
	 * <pre>&lt;td&gt;&lt;div&gt;&lt;strong&gt;Hello world&lt;/strong&gt;&lt;/div&gt;&lt;/td&gt;</pre>
	 *
	 * @param {HTMLElement} elSrc source element to wrap the contents of
	 * @return {HTMLElement} the new wrapping element
	 */
	WrapContents: function(elSrc)
	{
		var	aChildren = elSrc.childNodes,
			elDiv = document.createElement('div');

		while(aChildren.length)
			elDiv.appendChild(aChildren[0]);

		elSrc.appendChild(elDiv);
		return elDiv;
	},

	/**
	 * Scrolls the current window to the position indicated by mTo.
	 * Requires the YUI Animation module to be loaded.
	 *
	 * @param {mixed} mTo Can either be an array of two integers [x, y] describing the position to scroll to, or a HTML element (in which case the window is scrolled to that element)
	 * @param [{Number}] fDuration Animation duration in seconds (default = 1)
	 * @param [{Function}] fnEasing The easing function to use, probably from YAHOO.util.Easing (default = YAHOO.util.Easing.easeOut)
	 */
	ScrollPage: function(mTo, fDuration, fnEasing)
	{
		var aToXY, oAnim;

		if (L.isArray(mTo))
		{
			aToXY = mTo;
		}
		else
		{
			aToXY = Dom.getXY(mTo);
		}

		if (L.isUndefined(fDuration))
			fDuration = 1;

		if (L.isUndefined(fnEasing))
			fnEasing = YAHOO.util.Easing.easeOut;

		if (aToXY)
		{
			this.GetPageScrollTop();		// populates this.bUseDocumentElement
			oAnim = new YAHOO.util.Scroll(this.bUseDocumentElement ? document.documentElement : document.body, { scroll: { to : aToXY }  }, fDuration, fnEasing);
			oAnim.animate();  
		}
	},

	/**
	 * @return {Number} the number of pixels that the page body has been scrolled down in the browser.
	 */
	GetPageScrollTop: function()
	{
		if (L.isUndefined(this.bUseDocumentElement))
		{
			// Need to use document.documentElement in Standards Compliance mode, and document.body in Quirks mode
			// If in the wrong mode, then setting scrollTop will have no effect, so we can use this to test which one to use.
			var iExistingScrollTop = document.documentElement.scrollTop;

			document.documentElement.scrollTop = iExistingScrollTop + 1;
			this.bUseDocumentElement = (document.documentElement.scrollTop == iExistingScrollTop + 1);
			document.documentElement.scrollTop = iExistingScrollTop;
		}

		return (this.bUseDocumentElement ? document.documentElement.scrollTop : document.body.scrollTop);
	},

	/**
	 * Sets the scrollTop value of the page (browser scroll posistion)
	 * @param {Number} iScrollTop the value to set, in pixels
	 */
	SetPageScrollTop: function(iScrollTop)
	{
		if (L.isUndefined(this.bUseDocumentElement))
			this.GetPageScrollTop();

		if (this.bUseDocumentElement)
			document.documentElement.scrollTop = iScrollTop;
		else
			document.body.scrollTop = iScrollTop;
	},

	/**
	 * Updates an element on the page, with optional animation.
	 *
	 * Attribs recognised:
	 * <dl>
	 * <dt>HTML			</dt><dd> The new HTML to update the element with 										</dd>
	 * <dt>Animation	</dt><dd> Override the animation type. (one of: "none" or "fade")						</dd>
	 * <dt>Scope		</dt><dd> Scope for the On* functions. (defaults to the element we're updating)			</dd>
	 * <dt>OnStart   	</dt><dd> Function to run when starting the update.										</dd>
	 * <dt>OnHalfway 	</dt><dd> Function to run when halfway (old element gone, new element about to appear).	</dd>
	 * <dt>OnComplete	</dt><dd> Function to run when done.													</dd>
	 * </dl>
	 *
	 * NOTE: HTML is optional, you may want a custom method of updating the element (using OnHalfway), but still have the animation, etc.
	 *
	 * @param {String/HTMLElement} sID the element id, or element object
	 * @param {Object} oAttribs attribs for defining the update
	 */
	UpdateElement: function(sID, oAttribs)
	{
		var el = Dom.get(sID), 
			sAnimation, oAnimOut, oAnimIn;

		if (L.isUndefined(oAttribs.OnStart))
		{
			oAttribs.OnStart = function() {}
		}
		if (L.isUndefined(oAttribs.OnHalfway))
		{
			oAttribs.OnHalfway = function() {}
		}
		if (L.isUndefined(oAttribs.OnComplete))
		{
			oAttribs.OnComplete = function() {}
		}
		if (L.isUndefined(oAttribs.Scope))
		{
			oAttribs.Scope = el;
		}


		if (el)
		{
			sAnimation = 'fade';
			// IE seems to break with the "fade" animation, so force no animation for IE users.
			if (YAHOO.env.ua.ie || !YAHOO.env.getVersion('animation') || Dom.hasClass(el, 'hidden') || Dom.hasClass(el, 'hide'))
			{
				sAnimation = 'none';
			}
			else if (!L.isUndefined(oAttribs.Animation))
			{
				sAnimation = oAttribs.Animation;
			}

			switch (sAnimation)
			{
				case 'fade':
					oAnimIn = new YAHOO.util.Anim(el, { opacity: { to: 1 } }, 0.1);
					oAnimOut = new YAHOO.util.Anim(el, { opacity: { from: Dom.getStyle(el, 'opacity'), to: 0 } }, 0.1);

					oAnimOut.onStart.subscribe(oAttribs.OnStart, oAttribs.Scope, true);
					oAnimOut.onComplete.subscribe(function() { 
						if (!L.isUndefined(oAttribs.HTML))
							el.innerHTML = oAttribs.HTML;
						oAttribs.OnHalfway.call(oAttribs.Scope);
						ZC.JSManager.AutoTooltips(el);
						oAnimIn.animate();
					});
					oAnimIn.onComplete.subscribe(function() { 
						oAttribs.OnComplete.call(oAttribs.Scope);
						L.later(10, el, function() { Dom.setStyle(this, 'opacity', ''); });
					}, this, true);
					oAnimOut.animate();
					break;

				case 'none': 
				default:
					oAttribs.OnStart.call(oAttribs.Scope);
					oAttribs.OnHalfway.call(oAttribs.Scope);
					
					if (!L.isUndefined(oAttribs.HTML))
						el.innerHTML = oAttribs.HTML;

					ZC.JSManager.AutoTooltips(el);
					oAttribs.OnComplete.call(oAttribs.Scope);
					break;
			}
		}
		else
		{
			YAHOO.log('UpdateElement: unable to locate element with id ' + sID, 'warn', 'UpdateElement');

			oAttribs.OnStart.call(oAttribs.Scope);
			oAttribs.OnHalfway.call(oAttribs.Scope);
			oAttribs.OnComplete.call(oAttribs.Scope);
		}
	},

	/* * * * ARRAY UTILS * * * */

	/**
	 * Helper function that makes it easy to use an object as an argument to the other array functions below.
	 * The wrapper keeps the scope it is called with, so you can override this as normal.
	 * Use it to wrap a function that expects (Value, Key) as its arguments, and iterate over the object keys rather than the object itself, i.e:
	 *
	 *     aResult = U.Map(U.Keys(oObj), U.ObjectArrayWrapper(fnMyFunc), this);
	 *
	 * (the above is unnecessary in practice, since the array-related functions below do it automatically when passed an object)
	 *
	 * @param {Object} oObj the object being iterated over
	 * @param {Function} fnWrapped the function to be wrapped
	 * @return {Function} a wrapper function
	 */
	ObjectArrayWrapper: function(oObj, fnWrapped)
	{
		return function(sObjKey) { return fnWrapped.call(this, oObj[sObjKey], sObjKey); }
	},

	/**
	 * Searches an array for a value.
	 * @param Value value to search for
	 * @param {Array} aSearch array to search
	 * @return {Boolean} true if found
	 */
	InArray: function (Value, aSearch)
	{
		return (this.IndexOf(aSearch, Value) >= 0);
	},

	/*
	 * the following methods are present in Javascript 1.6, which is currently supported in Firefox 1.5+, but (afaik) no other browsers yet.
	 * The MDC javascript reference contains sample implementations for all of these methods, which is where I got the code from
	 * (see http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array)
	 *
	 * We make use of the (faster) browser-based implementations where available. Passing an object in will automatically use ObjectArrayWrapper.
	 */

	/**
	 * Returns the first index at which a given element can be found in the array, or -1 if it is not present.
	 * Where available, this is just an alias for Array.indexOf
	 *
	 * @see <a href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/indexOf">Array.indexOf reference @ MDC</a>
	 * @param {Array} aSearch array to search
	 * @param SearchElement Element to locate in the array.
	 * @param {Number} fromIndex The index at which to begin the search.
	 */
	IndexOf: function(aSearch, SearchElement, iFrom)
	{
		var fnImpl;

		if (Array.prototype.indexOf)
			fnImpl = Array.prototype.indexOf;
		else
			fnImpl = function(SearchElement, iFrom)
			{
				iFrom = Number(iFrom) || 0;
				var iLen = this.length;
				if (iFrom < 0)
				  iFrom += iLen;

				for (; iFrom < iLen; iFrom++)
				{
				  if (iFrom in this &&
					  this[iFrom] === SearchElement)
					return iFrom;
				}
				return -1;
			}

		return fnImpl.call(aSearch, SearchElement, iFrom);
	},

	/**
	 * Returns the last index at which a given element can be found in the array, or -1 if it is not present.
	 * Where available, this is just an alias for Array.lastIndexOf
	 *
	 * @see <a href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/lastIndexOf">Array.lastIndexOf reference @ MDC</a>
	 * @param {Array} aSearch array to search
	 * @param SearchElement Element to locate in the array.
	 * @param {Number} fromIndex The index at which to begin the search.
	 */
	LastIndexOf: function(aSearch, SearchElement, iFrom)
	{
		var fnImpl;

		if (Array.prototype.lastIndexOf)
			fnImpl = Array.prototype.lastIndexOf;
		else
			fnImpl = function(SearchElement, iFrom)
			{
				var iLen = this.length;
				iFrom = Number(iFrom) || 0;
				if (isNaN(iFrom))
					iFrom = iLen - 1;
				else
				{
					if (iFrom < 0)
						iFrom += iLen;
					else if (iFrom >= iLen)
						iFrom = iLen - 1;
				}

				for (; iFrom > -1; iFrom--)
				{
				  if (iFrom in this &&
					  this[iFrom] === SearchElement)
					return iFrom;
				}
				return -1;
			}

		return fnImpl.call(aSearch, SearchElement, iFrom);
	},

	/**
	 * Executes a provided function once per array element.
	 * Where available, this is just an alias for Array.forEach
	 * Also works with objects, unlike Array.forEach.
	 *
	 * @see <a href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/forEach">Array.forEach reference @ MDC</a>
	 * @param {Array/Object} aArray the array or object to iterate through
	 * @param {Function} fnCallback Function to execute for each element.
	 * @param {Object} oThis Object to use as 'this' when executing callback.
	 */
	ForEach: function(aArray, fnCallback, oThis)
	{
		var fnImpl;
	   
		if (Array.prototype.forEach)
			fnImpl = Array.prototype.forEach;
		else
			fnImpl = function (fnCallback, oThis)
			{
				if (!L.isFunction(fnCallback))
					throw new TypeError();

				for (var i = 0, iMax = this.length; i < iMax; i++)
				{
					if (i in this)
						fnCallback.call(oThis, this[i], i, this);
				}
			}

		fnArrayWrap.call(this, aArray, fnCallback, oThis, fnImpl);
	},

	/**
	 * Creates a new array with the results of calling a provided function on every element in this array.
	 * Where available, this is just an alias for Array.map
	 *
	 * @see <a href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map">Array.map reference @ MDC</a>
	 * @param {Array} aArray array to work on
	 * @param {Function} fnCallback Function that produces an element of the new Array from an element of the current one.
	 * @param {Object} oThis Object to use as 'this' when executing callback.
	 */
	Map: function(aArray, fnCallback, oThis)
	{
		var fnImpl;

		if (Array.prototype.map)
			fnImpl = Array.prototype.map;
		else
			fnImpl = function(fnCallback, oThis)
			{
				if (!L.isFunction(fnCallback))
					throw new TypeError();

				var iLen = this.length;

				var aResult = new Array(iLen);
				for (var i = 0; i < iLen; i++)
				{
					if (i in this)
						aResult[i] = fnCallback.call(oThis, this[i], i, this);
				}

				return aResult;
			}

		return fnArrayWrap.call(this, aArray, fnCallback, oThis, fnImpl);
	},

	/**
	 * Creates a new array with all elements that pass the test implemented by the provided function.
	 * Where available, this is just an alias for Array.filter
	 *
	 * @see <a href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/filter">Array.filter reference @ MDC</a>
	 * @param {Array} aArray array to work on
	 * @param {Function} fnCallback Function to test each element of the array.
	 * @param {Object} oThis Object to use as 'this' when executing callback.
	 */
	Filter: function(aArray, fnCallback, oThis)
	{
		var fnImpl;

		if (Array.prototype.filter)
			fnImpl = Array.prototype.filter;
		else
			fnImpl = function(fnCallback, oThis)
			{
				if (!L.isFunction(fnCallback))
					throw new TypeError();

				var iLen = this.length;

				var aResult = new Array();
				for (var i = 0; i < iLen; i++)
				{
					if (i in this)
					{
						var Val = this[i];
						if (fnCallback.call(oThis, Val, i, this))
							aResult.push(Val);
					}
				}

				return aResult;
			}

		return fnArrayWrap.call(this, aArray, fnCallback, oThis, fnImpl);
	},

	/**
	 * Creates a new array containing two arrays. The first contains all elements that pass the test implemented by the provided function, the second those elements that fail.
	 * @param {Array} aArray array to work on
	 * @param {Function} fnCallback Function to test each element of the array.
	 * @param {Object} oThis Object to use as 'this' when executing callback.
	 */
	Partition: function(aArray, fnCallback, oThis)
	{
		fn = function (fnCallback, oThis)
		{
			if (!L.isFunction(fnCallback))
					throw new TypeError();

			var iLen = this.length;

			var aResult = new Array([], []);
			for (var i = 0; i < iLen; i++)
			{
				if (i in this)
				{
					var Val = this[i];
					if (fnCallback.call(oThis, Val, i, this))
						aResult[0].push(Val);
					else
						aResult[1].push(Val);
				}
			}

			return aResult;
		}
		return fnArrayWrap.call(this, aArray, fnCallback, oThis, fn);
	},

	/**
	 * Tests whether some element in the array passes the test implemented by the provided function.
	 * Where available, this is just an alias for Array.some
	 *
	 * @see <a href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/some">Array.some reference @ MDC</a>
	 * @param {Array} aArray array to test
	 * @param {Function} fnCallback Function to test for each element.
	 * @param {Object} oThis Object to use as this when executing callback.
	 */
	Some: function(aArray, fnCallback, oThis)
	{
		var fnImpl;

		if (Array.prototype.some)
			fnImpl = Array.prototype.some;
		else
			fnImpl = function(fnCallback, oThis)
			{
				if (!L.isFunction(fnCallback))
					throw new TypeError();

				var iLen = this.length;

				for (var i = 0; i < iLen; i++)
				{
					if (i in this && fnCallback.call(oThis, this[i], i, this))
						return true;
				}

				return false;
			}

		return fnArrayWrap.call(this, aArray, fnCallback, oThis, fnImpl);
	},

	/**
	 * Tests whether all elements in the array pass the test implemented by the provided function.
	 * Where available, this is just an alias for Array.every
	 *
	 * @see <a href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/every">Array.every reference @ MDC</a>
	 * @param {Array} aArray array to test
	 * @param {Function} fnCallback Function to test for each element.
	 * @param {Object} oThis Object to use as this when executing callback.
	 */
	Every: function(aArray, fnCallback, oThis)
	{
		var fnImpl;

		if(Array.prototype.every)
			fnImpl = Array.prototype.every;
		else
			fnImpl = function(fnCallback, oThis)
			{
				if (!L.isFunction(fnCallback))
					throw new TypeError();

				var iLen = this.length;

				for (var i = 0; i < iLen; i++)
				{
					if (i in this && !fnCallback.call(oThis, this[i], i, this))
						return false;
				}
				return true;
			}

		return fnArrayWrap.call(this, aArray, fnCallback, oThis, fnImpl);
	},

	/**
	 * Returns the keys in the object or array o.
	 * @param {Object/Array} o
	 * @return {Array} the keys from the object
	 */
	Keys: function (o)
	{
		var fnImpl;

		if (Object.prototype.keys)
			fnImpl = Object.prototype.keys;
		else
			fnImpl = function()
			{
				var aKeys = [], Key;
				for (Key in this) 
				{
					if (L.hasOwnProperty(this, Key))
					{
						aKeys.push(Key);
					}
				}

				return aKeys;
			}

		return fnImpl.call(o);
	},

	/**
	 * Returns all the values in the object o.
	 * @param {Object} o
	 * @return {Array} the values in the object
	 */
	Values: function(o)
	{
		return this.Map(o, function(v) { return v; });
	},

	/**
	 * Flattens an array of arrays (or a mixture of values/arrays) to a single array.
	 * @param {Array} aFlatten array to flatten
	 * @return {Array} the flattened array
	 */
	Flatten: function (aFlatten)
	{
		var aResult = [];
		U.ForEach(aFlatten, function(mVal)
		{
			if (L.isArray(mVal))
				aResult.push.apply(aResult, this.Flatten(mVal));
			else
				aResult.push(mVal);
		}, this);
		return aResult;
	},

	/**
	 * Checks for object equality. In javascript (a == b) with two objects
	 * checks that a is the same object as b (instead of checking for two
	 * instances of the same object). This method checks each property of a and
	 * b to see if they're equal.
	 * @param {Object/Array} oA first object to test
	 * @param {Object/Array} oB second object to test
	 * @param {Boolean} bStrict if true, use strict equality test. defaults to false.
	 * @param {Array} aSeen (internal) used when recursing to avoid loops
	 */
	ObjectsEqual: function (oA, oB, bStrict, aSeen)
	{
		if (!L.isArray(oA) && !L.isObject(oA) && !L.isArray(oB) && !L.isObject(oB))
			return bStrict ? (oA === oB) : (oA == oB);

		if (L.isArray(oA) != L.isArray(oB) || typeof oA != typeof oB)
			return false;

		var aKeysA = this.Keys(oA),
			aKeysB = this.Keys(oB),
			i, iMax, k, iIndexB;
		if (aKeysA.length != aKeysB.length)
			return false;

		for (i = 0, iMax = aKeysA.length; i < iMax; i++)
		{
			k = aKeysA[i];
			iIndexB = this.IndexOf(aKeysB, k);
			if (iIndexB == -1)
				return false;

			aKeysB.splice(iIndexB, 1);
			if (bStrict && typeof oA[k] != typeof oB[k])
				return false;

			if (typeof oA[k] == 'object')
			{
			   	if (L.isUndefined(aSeen) || (!this.InArray(oA[k], aSeen) && !this.InArray(oB[k], aSeen)))
				{
					var aNewSeen = aSeen || [];
					aNewSeen.unshift(oA, oB);
					if (!this.ObjectsEqual(oA[k], oB[k], bStrict, aNewSeen))
						return false;
				}
			}
			else
			{
				if (oA[k] != oB[k])
					return false;
			}
		}

		if (aKeysB.length > 0)
			return false;

		return true;
	},

	/**
	 * Clones an object
	 *
	 * Taken from dojo, which is BSD-licensed.
	 *
	 * @param {Object} o object to clone
	 * @return {Object} the clone
	 */
	Clone: function(o){
		// summary:
		//		Clones objects (including DOM nodes) and all children.
		//		Warning: do not clone cyclic structures.
		if(!o || typeof o != "object" || L.isFunction(o)){
			// null, undefined, any non-object, or function
			return o;	// anything
		}
		if(o.nodeType && "cloneNode" in o){
			// DOM Node
			return o.cloneNode(true); // Node
		}
		if(o instanceof Date){
			// Date
			return new Date(o.getTime());	// Date
		}
		var r, i, l, s, name;
		if(L.isArray(o)){
			// array
			r = [];
			for(i = 0, l = o.length; i < l; ++i){
				if(i in o){
					r.push(this.Clone(o[i]));
				}
			}
// we don't clone functions for performance reasons
//		}else if(L.isFunction(o)){
//			// function
//			r = function(){ return o.apply(this, arguments); };
		}else{
			// generic objects
			r = o.constructor ? new o.constructor() : {};
		}
		for(name in o){
			// the "tobj" condition avoid copying properties in "source"
			// inherited from Object.prototype.  For example, if target has a custom
			// toString() method, don't overwrite it with the toString() method
			// that source inherited from Object.prototype
			s = o[name];
			if(!(name in r) || (r[name] !== s && (!(name in empty) || empty[name] !== s))){
				r[name] = this.Clone(s);
			}
		}
		// IE doesn't recognize some custom functions in for..in
		if(extraLen){
			for(i = 0; i < extraLen; ++i){
				name = extraNames[i];
				s = o[name];
				if(!(name in r) || (r[name] !== s && (!(name in empty) || empty[name] !== s))){
					r[name] = s; // functions only, we don't clone them
				}
			}
		}
		return r; // Object
	},

	/* * * * MISC UTILS * * * */
	/**
	 * Cross-browser method for inserting text at the current cursor position
	 *
	 * NOTE: this (or at least the caret-location part of it) should be added to YUI at some point, so we should be able to move to that.
	 *
	 * @param {HTMLElement} elInput the textarea or text input element to insert the string into
	 * @param {String} sText text to insert
	 */
	InsertAtCursor: function(elInput, sText)
	{
		var iScrollPos = elInput.scrollTop, 
			iStrPos = 0,
			bSelStart = !L.isUndefined(elInput.selectionStart),
			oRange, sStart, sEnd;	

		if (bSelStart)
		{
			iStrPos = elInput.selectionStart;
		}
		else
		{
			elInput.focus();
			oRange = document.selection.createRange();
			oRange.moveStart ('character', -elInput.value.length);
			iStrPos = oRange.text.length;
		}

		sStart = elInput.value.substring(0,iStrPos);  
		sEnd = elInput.value.substring(iStrPos); 
		elInput.value = sStart + sText + sEnd;
		iStrPos += sText.length;
		elInput.focus();

		if (bSelStart)
		{
			elInput.selectionStart = iStrPos;
			elInput.selectionEnd = iStrPos;
		}
		else
		{
			oRange = document.selection.createRange();
			oRange.moveStart ('character', -elInput.value.length);
			oRange.moveStart ('character', strPos);
			oRange.moveEnd ('character', 0);
			oRange.select();
		}
		elInput.scrollTop = iScrollPos;
	},

	/**
	 * Measures the width of a scrollbar, in pixels. It's different on every
	 * browser of course, and may be further altered by the user's settings.
	 *
	 * This function adds a couple of divs to the page to measure them with and
	 * without the scrollbar.
	 *
	 * NOTE: I am assuming that the width of a vertical scrollbar is the same
	 * as the height of a horizontal one. If not, I'm sure it's not too hard to
	 * refactor this code to do the horizontal as well.
	 *
	 * @return {Number} the width of a scrollbar in pixels
	 */
	GetScrollbarWidth: function()
	{
		if (!L.isUndefined(iScrollbarWidth))
			return iScrollbarWidth;

		var elScroll = null, elInner = null, iNoScroll = 0, iScroll = 0;

		// Outer scrolling div
		elScroll = document.createElement('div');
		Dom.setStyle(elScroll, 'position', 'absolute');
		Dom.setStyle(elScroll, 'top', '-1000px');
		Dom.setStyle(elScroll, 'left', '-1000px');
		Dom.setStyle(elScroll, 'width', '100px');
		Dom.setStyle(elScroll, 'height', '50px');
		// Start with no scrollbar
		Dom.setStyle(elScroll, 'overflow', 'hidden');

		// Inner content div
		elInner = document.createElement('div');
		Dom.setStyle(elInner, 'width', '100%');
		Dom.setStyle(elInner, 'height', '200px');

		elScroll.appendChild(elInner);
		document.body.appendChild(elScroll);

		// Width of the inner div sans scrollbar
		iNoScroll = elInner.offsetWidth;

		// Width of the inner div with scrollbar
		Dom.setStyle(elScroll, 'overflow', 'scroll');
		iScroll = elInner.offsetWidth;
		if (iNoScroll == iScroll)
			iScroll = elScroll.clientWidth;

		document.body.removeChild(elScroll);
		iScrollbarWidth = (iNoScroll - iScroll);
		return iScrollbarWidth;
	},

	/**
	 * Displays an alert message to the user. If the YUI SimpleDialog control is loaded, then it uses that, otherwise falls back to a regular alert().
	 * NOTE: Unlike alert(), the YUI dialog will return control to the caller immediately. To perform an action once the user confirms, pass in an OKHandler.
	 *
	 * Attribs accepted:
	 * <dl>
	 * <dt> Icon:      </dt><dd> icon to use ALARM (default), BLOCK, WARN, HELP, INFO, and TIP.         </dd>
	 * <dt> OKHandler: </dt><dd> function to run once the user has confirmed.							</dd>
	 * <dt> Scope:     </dt><dd> scope to call the OKHandler with.										</dd>
	 * </dl>
	 *
	 * @param {String} sMessage alert message
	 * @param {Object} oAttribs extra attribs to control the alert. For backwards compatibility, if this is a function it's treated as an object with an "OKHandler" 
	 */
	Alert: function(sMessage, oParams)
	{
		if (L.isUndefined(oParams))
		{
			oParams = {};
		}
		else if (L.isFunction(oParams))
		{
			oParams = { OKHandler: oParams };
		}
		
		if (L.isUndefined(oParams.OKHandler))
		{
			oParams.OKHandler = function(){};
		}

		if (L.isUndefined(oParams.Scope))
		{
			oParams.Scope = window;
		}

		if (!L.isUndefined(YAHOO.widget.SimpleDialog))
		{
			var oPanelConfig,
				oPanel;
				
			oPanelConfig = {
				width: oParams.width || "300px",
				fixedcenter: "contained",
				visible: false,
				draggable: false,
				close: false,
				modal: true,
				text: sMessage,
				icon: YAHOO.widget.SimpleDialog.ICON_ALARM,
				constraintoviewport: true,
				buttons: [ { text: "OK", handler: function() { this.destroy(); oParams.OKHandler.call(oParams.Scope); }, isDefault: true } ]
			};

			if (!L.isUndefined(YAHOO.widget.SimpleDialog['ICON_' + oParams.Icon]))
				oPanelConfig.icon = YAHOO.widget.SimpleDialog['ICON_' + oParams.Icon];
			
			oPanel = new YAHOO.widget.SimpleDialog("alert" + Math.floor(Math.random() * 1000), oPanelConfig);
			oPanel.render(document.body);
			oPanel.show();
		}
		else
		{
			alert(sMessage);
			oParams.OKHandler.call(oParams.Scope);
		}
	},

	/**
	 * Displays a confirmation dialog to the user. If the YUI SimpleDialog control is loaded, then it uses that, otherwise falls back to a regular confirm().
	 * NOTE: Unlike confirm(), the YUI dialog will return control to the caller immediately. To perform an action once the user confirms, pass in an OKHandler.
	 * @param {String} sMessage message
	 * @param {Function} fnOKHandler handler to run if the user confirms
	 * @param {Function} fnCancelHandler handler to run if the user cancels
	 */
	Confirm: function(sMessage, oScope, fnOKHandler, fnCancelHandler)
	{
		if (L.isUndefined(fnOKHandler))
			fnOKHandler = function(){};
		if (L.isUndefined(fnCancelHandler))
			fnCancelHandler = function(){};

		if (!L.isUndefined(YAHOO.widget.SimpleDialog))
		{
			if (L.isUndefined(this.oConfirmPanel))
			{
				this.oConfirmPanel = new YAHOO.widget.SimpleDialog("alert",
				{
					width: "300px",
					fixedcenter: true,
					visible: false,
					draggable: false,
					close: false,
					modal: true,
					icon: YAHOO.widget.SimpleDialog.ICON_HELP,
					constraintoviewport: true
				});
				this.oConfirmPanel.render(document.body);
			}

			this.oConfirmPanel.cfg.setProperty('text', sMessage);
			this.oConfirmPanel.cfg.setProperty('buttons', [ 
				{ text: "Yes", handler: function() { this.hide(); fnOKHandler.call(oScope); }, isDefault: true },
				{ text: "No", handler: function() { this.hide(); fnCancelHandler.call(oScope); } } 
			]);
			this.oConfirmPanel.show();
		}
		else
		{
			if (confirm(sMessage))
				fnOKHandler();
			else
				fnCancelHandler();
		}
	},

	/**
	 * This object contains the default methods for the AJAX request "loading
	 * indicator". The default just puts a div in the top-right corner of the
	 * browser window.
	 *
	 * This is used for AjaxRequests which do not specify a LoadingIndicator.
	 *
	 * @property DefaultLoadingIndicator
	 */
	DefaultLoadingIndicator: {
		Show: function()
		{
			if (L.isUndefined(this.elLoadingIndicator))
			{
				this.elLoadingIndicator = document.createElement('div');
				this.elLoadingIndicator.id = 'ajaxloading';
				this.elLoadingIndicator.className = 'hide';
				this.elLoadingIndicator.appendChild(document.createTextNode(ZC.Util.GetText('Please wait…')));

				Dom.setStyle(this.elLoadingIndicator, 'opacity', 0);

				document.body.appendChild(this.elLoadingIndicator);

				if (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 7)
				{
					// IE6 can't handle position: fixed 
					var fnAlignDiv = function() { 
						this.elLoadingIndicator.style.top = Dom.getClientRegion().top + 'px'; 
					}
					fnAlignDiv.call(this);

					Evt.on(window, 'resize', fnAlignDiv, this, true);
					Evt.on(window, 'scroll', fnAlignDiv, this, true);
				}
				Evt.on(window, 'unload', function() { this.elLoadingIndicator = null; }, this, true);
			}
			if (YAHOO.util.Anim)
			{
				var oAnim = new YAHOO.util.Anim(this.elLoadingIndicator, { opacity: { from: 0, to: 0.75 } }, 0.4);
				oAnim.onStart.subscribe(function() { Dom.removeClass(this.elLoadingIndicator, 'hide'); }, this, true);
				oAnim.animate();
			}
			else
			{
				Dom.setStyle(this.elLoadingIndicator, 'opacity', 1);
				Dom.removeClass(this.elLoadingIndicator, 'hide');
			}
		},
		Hide: function()
		{
			if (YAHOO.util.Anim)
			{
				var oAnim = new YAHOO.util.Anim(this.elLoadingIndicator, { opacity: { from: 0.75, to: 0 } }, 0.4);
				oAnim.onComplete.subscribe(function() { Dom.addClass(this.elLoadingIndicator, 'hide'); }, this, true);
				oAnim.animate();
			}
			else
			{
				Dom.addClass(this.elLoadingIndicator, 'hide');
			}
		}
	}
}

var U = ZC.Util, _GT = U.GetText;

/**
 * Generic Get/SetAttrib implementation, which we use to augment other objects
 * @class AttribProvider
 * @private
 */
var AttribProvider = function() {};
AttribProvider.prototype = {
	/**
	 * Gets the named attrib.
	 * @param {string} sName attrib to get
	 * @return Value attribute value
	 */
	GetAttrib: function(sName)
	{
		if (sName in this.aDef)
		{
			return this.aDef[sName];
		}
		else if (!L.isUndefined(this._elInput) && !L.isArray(this._elInput) && Dom.getAttribute(this._elInput, sName))
		{
			return Dom.getAttribute(this._elInput, sName);
		}
		else
		{
			YAHOO.log('Attrib ' + sName + ' is not set', 'error', this.sName + '::GetAttrib');
		}
	},

	/**
	 * If the named attribute is set then its value is returned, else the default argument is returned.
	 * @param {string} sName attrib name to get
	 * @param {string{} mDefault value to return if attribute is not set. Defaults to false
	 * @return Value attribute value
	 */
	GetAttribDefault: function(sName, mDefault)
	{
		if(L.isUndefined(mDefault))
			mDefault = false;

		return this.AttribIsset(sName) ? this.GetAttrib(sName) : mDefault;
	},

	/**
	 * Checks that the named attrib is set
	 * @param {string} sName attrib to check
	 * @return {Boolean} true if the attrib is set
	 */
	AttribIsset: function(sName)
	{
		if (!L.isUndefined(this.aDef[sName]))
			return true;

		if (!L.isUndefined(this._elInput) && !L.isArray(this._elInput) && Dom.getAttribute(this._elInput, sName))
			return true;

		return false;
	},

	/**
	 * Checks that the named attribs are set
	 * @param {Array} aAttribs attribs to check
	 * @return {Boolean} true if the attrib is set
	 */
	AttribsAreSet: function(aAttribs)
	{
		return U.Every(aAttribs, this.AttribIsset, this);
	},

	/**
	 * Sets the value of an attribute. 
	 *
	 * If the object needs to react to an attribute change, then define a
	 * method <code>AttribMethod_<em>AttribName</em></code>. This method will
	 * be called with the new attrib value when the attrib value is changed.
	 *
	 * @param {String} sName attrib name to set
	 * @param Value new attrib value
	 */
	SetAttrib: function(sName, Value)
	{
		var OldValue = this.aDef[sName];
		this.aDef[sName] = Value;

		if (OldValue != Value && L.isFunction(this['AttribMethod_' + sName]))
		{
			this['AttribMethod_' + sName](Value, OldValue);
		}
	},

	/**
	 * Sets the value of many attributes.
	 * @param {Object} oAttribs hash of attrib name => attrib value
	 */
	SetAttribs: function(oAttribs)
	{
		U.ForEach(oAttribs, function(Value, sName)
		{
			this.SetAttrib(sName, Value);
		}, this);
	},

	/**
	 * Unset the value of an attribute
	 * @param {String} sAttrib attrib name
	 */
	UnsetAttrib: function(sName)
	{
		var OldValue = this.aDef[sName];
		delete this.aDef[sName];

		if (L.isFunction(this['AttribMethod_' + sName]))
		{
			this['AttribMethod_' + sName](undefined, OldValue);
		}
	},

	/**
	 * Initial setup for all classes using this provider. Runs any available
	 * AttribMethods for the initial attrib values.
	 *
	 * Call this from the end of your constructor when using this provider.
	 */
	AttribProviderSetup: function()
	{
		this._bAttribProviderSetup = true;
		
		U.ForEach(this.aDef, function(Value, sKey)
		{
			if (L.isFunction(this['AttribMethod_' + sKey]))
			{
				this['AttribMethod_' + sKey](Value);
			}
		}, this);

		this._bAttribProviderSetup = false;
	}
}

/**
 * Generic GetWidget implementation, which we use to augment other objects
 * @class GetWidgetProvider
 * @private
 */
var GetWidgetProvider = function() {};
GetWidgetProvider.prototype = {
	/**
	 * Retrieves a widget by name.
	 *
	 * sSearch may be a dot-separated list of widget names (e.g. "FrmAddEdit.Enabled"), in which case the Enabled widget would be
	 * retrieved from the FrmAddEdit widget. sSearch may also be a single widget name, in which case a depth-first search of the widget tree is
	 * performed, and the first match returned.
	 *
	 * @param {String} sSearch widget name
	 * @return {Object} widget object, or <em>undefined</em> if the widget can't be found
	 */
	GetWidget: function(sSearch)
	{
		var iNumSearchObjects, iDotPos, i, oSearchObject, sSearchPart, sSearchRest, sName, oChildSearch;

		if (L.isObject(sSearch))
		{
			if (sSearch instanceof ZC.Core.Widget)
			{
				return sSearch;
			}
			else if (!L.isUndefined(sSearch.name)) // HTML INPUT element?
			{
				sSearch = sSearch.name;
			}
			else if (!L.isUndefined(sSearch.id)) // some other HTML element?
			{
				sSearch = sSearch.id;
			}
			else
			{
				sSearch = sSearch.toString();
			}
		}

		iNumSearchObjects = this._aSearchObjects.length;
		iDotPos = sSearch.indexOf('.');

		if (iDotPos > -1)
		{
			sSearchPart = sSearch.substr(0, iDotPos);
			sSearchRest = sSearch.substr(iDotPos + 1);

			for (i = 0; i < iNumSearchObjects; ++i)
			{
				oSearchObject = this[this._aSearchObjects[i]];
				if (!L.isUndefined(oSearchObject[sSearchPart]))
				return oSearchObject[sSearchPart].GetWidget(sSearchRest);
			}

			return undefined;
		}

		for (i = 0; i < iNumSearchObjects; ++i)
		{
			oSearchObject = this[this._aSearchObjects[i]];
			if (!L.isUndefined(oSearchObject[sSearch]))
			return oSearchObject[sSearch];

			// descend down the tree looking for the widget
			for (sName in oSearchObject)
			{
				if (L.hasOwnProperty(oSearchObject, sName) && L.isObject(oSearchObject[sName]))
				{
					oChildSearch = oSearchObject[sName].GetWidget(sSearch);
					if (!L.isUndefined(oChildSearch))
						return oChildSearch;
				}
			}
		}

		return undefined;
	},

	/**
	 * Retrieves an array of widgetss that pass the test applied by supplied boolean method
	 * @param {Function} fnMethod A boolean method for testing blocks, the only argument recieved is the block itself.
	 * @param {Object} oScope (optional) scope override for fnMethod
	 * @return {Array} array of blocks.
	 */
	GetWidgetsBy: function(fnMethod, oScope)
	{
		var iNumSearchObjects, i, oSearchObject, sSearchPart, sSearchRest, sName, oChildSearch, aResult = [];

		iNumSearchObjects = this._aSearchObjects.length;

		for (i = 0; i < iNumSearchObjects; ++i)
		{
			oSearchObject = this[this._aSearchObjects[i]];
			U.ForEach(oSearchObject, function(oSearch)
			{
				if (fnMethod.call(oScope, oSearch))
				aResult.push(oSearch);
			});

			// descend down the tree looking for the widget
			for (sName in oSearchObject)
			{
				if (L.hasOwnProperty(oSearchObject, sName))
				{
					aChildSearch = oSearchObject[sName].GetWidgetsBy(fnMethod, oScope);
					aResult.push.apply(aResult, aChildSearch);
				}
			}
		}

		return aResult;
	}
};

/**
 * Generic implementation of events and eventlisteners, which we use to augment other objects
 * @class EventProvider
 * @private
 */
var EventProvider = function() { }
EventProvider.prototype = {
	/**
	 * Destructor. Should be called by the Destruct methods of objects using EventProvider.
	 */
	EventProviderDestruct: function()
	{
		if (!L.isUndefined(this.aEventListeners))
		{
			U.ForEach(this.aEventListeners, function(aELs) { 
				U.ForEach(aELs, function (oEventListener) { 
					try { oEventListener.Destruct(); } 
					catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }
				});
			});
		}
		this.aEventListeners = {};
		this.aEvents = {};
	},

	// these two properties are created when first required.. adding them to
	// this prototype would cause them to be shared amongst the objects that
	// use this class, rather than created anew for each one.
	
	/**
	 * Array of event listeners attached to this widget
	 * @property aEventListeners
	 * @type Object
	 * @private
	 */

	/**
	 * Array of events attached to this widget
	 * @property aEvents
	 * @type Object
	 * @private
	 */

	/**
	 * Adds event handlers to this widget that fire the custom event sEventName.
	 *
	 * If the event name contains "WIDGET", "FORM" or "BLOCK" then these will be
	 * replaced with the form+widget name, form name or block name, making it
	 * easier to create events that are specific to a widget, form or block.
	 *
	 * <p>A note about event ordering: the default for iOrder (if not provided)
	 * starts at 100 and increments for each new event handler added. If you
	 * need to specify an order for your event, the following should be used as
	 * a guide:</p>
	 *
	 * <dl>
	 * <dt>  1 -  49  </dt><dd>  For events which may want to cancel the event.    </dd>
	 * <dt> 50 -  99  </dt><dd>  For events that need to run before the regular handlers, but not if an earlier handler cancels the event.  </dd>
	 * <dt>     500+  </dt><dd>  For anything that you want to run as late as possible. </dd>
	 * </dl>
	 *
	 * Note that two event handlers can have the same order value, but the
	 * order in which they fire is undefined (depends on the stability of the
	 * browser's Array.sort)
	 *
	 * @param {String / Function / Object} EventNameOrFunction Name of the custom event to fire, or a function to call, or a CustomEvent object.
	 * @param {Array} aOn Array of events to attach the event to. May also be a string for attaching a single event.
	 * @param {Object} oScope (optional) scope override for the event handler
	 * @param {Number} iOrder (optional) the order to fire this event handler
	 * @param {Object} oObjectOverride (internal use) the object to pass to the event handler
	 */
	AddEvent: function(EventNameOrFunction, aOn, oScope, iOrder, oObjectOverride)
	{
		var elEvent = this.GetEventElement();

		if (!elEvent && (L.isUndefined(this.HasChildWidgets) || !this.HasChildWidgets()))
		{
			YAHOO.log('this.GetEventElement() is not valid, no child widgets and AddEvent is not overridden', "error", this.sName + '#AddEvent');
			return undefined;
		}

		if (L.isUndefined(oObjectOverride))
			oObjectOverride = this;

		if (!(this instanceof ZC.Core.Form) && !L.isUndefined(this.aChildWidgets))
			U.ForEach(this.aChildWidgets, function(oWidget) { oWidget.AddEvent(EventNameOrFunction, aOn, oScope, iOrder, oObjectOverride) }, this);

		if (!elEvent)
			return;

		if (L.isUndefined(iOrder))
		{
			if (L.isUndefined(this._iEventHandlerOrder))
			{
				this._iEventHandlerOrder = 100;
			}

			iOrder = this._iEventHandlerOrder++;
		}

		if (!L.isArray(aOn))
			aOn = [aOn];

		U.ForEach(aOn, function(sEvent)
		{
			var i, iMax, oEventDef, oCustomEvent;

			if (L.isUndefined(this.aEvents))
			{
				this.aEvents = {};
			}

			if (L.isUndefined(this.aEvents[sEvent]))
			{
				this.aEvents[sEvent] = [];
				this._WireUpEvent(sEvent);
			}

			oEventDef = {
				Scope: oScope,
				ObjectOverride: oObjectOverride,
				Order: iOrder
			};

			if (typeof EventNameOrFunction == 'function')
			{
				oEventDef.Func = EventNameOrFunction;
			}
			else
			{
				if (EventNameOrFunction instanceof YAHOO.util.CustomEvent)
				{
					oCustomEvent = EventNameOrFunction;
				}
				else
				{
					EventNameOrFunction = this._InternalEventName(EventNameOrFunction);
					oCustomEvent = ZC.JSManager.GetEvent(EventNameOrFunction);
				}

				oEventDef.CustomEvent = oCustomEvent;
			}

			for (i = 0, iMax = this.aEvents.length; i < iMax; i++)
			{
				if (U.ObjectsEqual(this.aEvents[i], oEventDef))
				{
					// already listed, on to next event
					return;
				}
			}

			this.aEvents[sEvent].push(oEventDef);
		}, this);
	},

	/**
	 * Adds a number of events given a definition
	 * @param {Object} aDef definition: EventName => [ array of events ]
	 */
	AddEvents: function(aDef)
	{
		U.ForEach(aDef, function(aOn, sEventName) { this.AddEvent(sEventName, aOn); }, this);
	},

	/** 
	 * Wires up an event to the event handler.
	 *
	 * By default, this attaches the named DOM event to the event handler.
	 * Subclasses can override this if they need to wire up events differently,
	 * or to provide extra custom events.
	 *
	 * This method should arrange for the _EventDispatcher method to be called
	 * with this object as the scope, and with the value of sEvent as its first
	 * argument. Any extra arguments (e.g. a reference to a DOM event object)
	 * are passed onto the event listeners. 
	 *
	 * @param {String} sEvent the name of the event to wire up.
	 */
	_WireUpEvent: function(sEvent)
	{
		// special case for load event. we run this from the AfterManagerInit event once everything's been initialised (even stuff that inits in ManagerInit).
		// it's also cleared, then rerun during a AJAX update
		switch (sEvent)
		{
			case 'load':
				ZC.JSManager.GetEvent('OnLoadEvent').subscribe(function(oEvent)
				{
					this._EventDispatcher('load', oEvent);
					/* 
					 * Not sure if this is needed? OnLoadEvent is called right at the end of manager's Init method now, and after AjaxRequests. 
					 *
					 * if (!L.isUndefined(oObj.Event))
					{
						// if we've been passed a custom event, subscribe to that event's subscribeEvent so that we can 
						// run eventlisteners that subscribe later expecting to be run on-"load"
						oObj.Event.subscribeEvent.subscribe(this._SubscribeEventHandler, oObj.Override, this);
					}*/
				}, this, true);
				break;

			case 'ValidationError':
			case 'EnableDisable':
			case 'ShowHide':
				// no wire-up necessary, the relevant methods of widget fire the events when appropriate
				break;					

			default:
				if (!Evt.addListener(this.GetEventElement(), sEvent, function(oEvent) { this._EventDispatcher(sEvent, oEvent); }, this, true))
					YAHOO.log('addListener returned false for ' + sEvent, "warn", this.sName + "._WireUpEvent");
				break;
		}
	},

	/**
	 * Dispatches events to the event listeners.
	 */
	_EventDispatcher: function(/* sEvent, ... */)
	{
		var aArgs = [], sEvent = arguments[0], i, iMax;
				
		for (i = 1, iMax = arguments.length; i < iMax; i++)
		{
			aArgs.push(arguments[i]);
		}

		if (L.isUndefined(this.aEvents) || L.isUndefined(this.aEvents[sEvent]))
			return;

		this.aEvents[sEvent].sort(function(a, b)
		{
			return (a.Order - b.Order);
		});

		U.ForEach(this.aEvents[sEvent], function(oEventDef)
		{
			var aThisArgs = [];

			if (oEventDef.Func)
			{
				[].push.apply(aThisArgs, aArgs);
				aThisArgs.push(this);
				oEventDef.Func.apply(oEventDef.Scope, aThisArgs);
			}
			else
			{
				aThisArgs.push(oEventDef.ObjectOverride);
				[].push.apply(aThisArgs, aArgs);
				aThisArgs.push(this);
				oEventDef.CustomEvent.fire.apply(oEventDef.CustomEvent, aThisArgs); 
			}
		}, this);
	},

	/**
	 * When a custom event is added to this widget to fire on "load", we
	 * subscribe to the subscribeEvent (fired when new objects subscribe to
	 * that event) so that we can immediately fire the event handlers of any
	 * eventlisteners that come along later.
	 * @param {String} sType the type of event that just fired (subscribeEvent)
	 * @param {Array} aArgs the arguments passed to the subscribe method (0 = event handler, 1 = custom object, 2 = scope override)
	 * @param {Object} oObj the widget override passed into AddEvent, or the current widget if none
	 * @private
	 */
	_SubscribeEventHandler: function(sType, aArgs, oObj)
	{
		var fnEventHandler = aArgs[0],
		    aEHArgs = [oObj, aArgs[1]],
		    oOverride = aArgs[2];

		if (oOverride === true)
			oOverride = aArgs[1];

		fnEventHandler.call(oOverride, oEvent.type, aEHArgs);
	},

	/**
	 * Instantiates an EventListener, attached to the current widget, and subscribes to
	 * the named event.
	 * @param {String} sEventName Name of the custom event to subscribe to
	 * @param {Array} aDef The definition for the eventlistener
	 * @return {Object} The new event listener
	 */
	AddEventListener: function(sEventName, aDef)
	{
		sEventName = this._InternalEventName(sEventName);
		var oEvent = ZC.JSManager.GetEvent(sEventName), i, iMax, oEL,
		    fnEventListenerConstructor = ZC.JSManager.GetComponent(aDef.EventListenerType, aDef.Module || 'Core', 'EventListener');

		YAHOO.log("Adding event listener for " + sEventName + " (" + this._InternalEventName(sEventName) + ") to " + this.sName, "debug", "AddEventListener");
		if (fnEventListenerConstructor)
		{
			if (L.isUndefined(this.aEventListeners))
			{
				this.aEventListeners = { };
			}

			if (L.isUndefined(this.aEventListeners[sEventName]))
			{
				this.aEventListeners[sEventName] = [];
			}
			else
			{
				for (i = 0, iMax = this.aEventListeners[sEventName].length; i < iMax; ++i)
				{
					if (U.ObjectsEqual(this.aEventListeners[sEventName][i].aDef, aDef))
					{
						YAHOO.log("Listener already exists, not adding another.", "debug", "AddEventListener");
						return this.aEventListeners[sEventName][i];
					}
				}
			}

			oEL = new fnEventListenerConstructor(this, oEvent, aDef);
			this.aEventListeners[sEventName].push(oEL);
			return oEL;
		}
		else
		{
			throw new Error("Unable to load event listener: " + (aDef.Module || 'Core') + "/" + aDef.EventListenerType);
		}
	},

	/**
	 * Adds a number of eventlisteners given a definition
	 * @param {Object} aDef event listener definition: EventName => [ array of event listener definitions ]
	 */
	AddEventListeners: function(aDef)
	{
		U.ForEach(aDef, function(aELDefs, sEventName)
		{
			U.ForEach(aELDefs, function(aELDef) { 
				try
				{
					this.AddEventListener(sEventName, aELDef) 
				}
				catch (ex)
				{
					YAHOO.log('Unable to add event listener ' + aELDef.EventListenerType + ' for event ' 
						+ sEventName + ' to ' + this.sName + ': ' + ex.message + " (" + ex.fileName + ":" + ex.lineNumber + ")", 'error', this.sName);
				}
			}, this);
		}, this);
	},

	/**
	 * Turn an event name from a def into our internal event name, substituting WIDGET, FORM and BLOCK tags where present.
	 *
	 * WIDGET is replaced with this widget's unique ID. FORM is replaced with the form name. BLOCK is replaced with the block's UniqueID.
	 *
	 * Subclasses may override this to provide other tags if necessary, but
	 * should call their parent on the value as well (even if you've replaced
	 * something, there may be use cases for having two tags in an event name.
	 *
	 * @param {String} External event name
	 * @return {String} Internal event name
	 * @protected
	 */
	_InternalEventName: function(sEventName)
	{
		if (this instanceof ZC.Core.Block)
			return sEventName.replace('BLOCK', this.UniqueID());

		if (this instanceof ZC.Core.Widget)
		{
			var oBlock = this.FindContainingBlock();
			return sEventName.replace('WIDGET', this._WidgetNameToID())
							 .replace('FORM', this.oForm ? this.oForm.sName : 'NoForm')
							 .replace('BLOCK', oBlock ? oBlock.sName : 'NoBlock');
		}
	},

	/**
	 * Checks this object's elements for event handlers for the given event and fires any that are registered.
	 * @param {String} sEvent Event name
	 * @protected
	 */
	_FireEventHandlers: function(sEvent)
	{
		var elEvent = this.GetEventElement(),
			oDOMEvent,
			aHandlers,
			aEventArgs = [], i, iMax;

		if (L.isArray(elEvent))
			elEvent = elEvent[0];

		aHandlers = Evt.getListeners(elEvent, sEvent);
		if (aHandlers)
		{
			for (i = 1, iMax = arguments.length; i < iMax; i++)
				aEventArgs.push(arguments[i]);

			if (aEventArgs.length && aEventArgs[0] && !L.isUndefined(aEventArgs[0].clientY)) 
				oDOMEvent = aEventArgs.shift();

			U.ForEach(aHandlers, function(aHandler)
			{
				var oScope = aHandler.scope,
				    fnHandler = aHandler.fn,
					oArg = aHandler.obj || oScope,
					mEvent = oDOMEvent || aHandler.type;

				fnHandler.call(oScope, mEvent, oArg, aEventArgs);
			});
		}
	}
};

ZC.Namespace('Core');

/**
 * The manager object, responsible for initialising all the JS widgets and events on the page.
 * @class JSManager
 * @namespace ZC
 */
ZC.JSManager = {
	/**
	 * Collection of all of the client-side objects created for this page. Use GetCSO to fetch CSO objects.
	 *
	 * @private
	 * @type Object
	 * @property aClientSideObjects
	 */
	aClientSideObjects: {},

	/**
	 * Collection of all of the custom events defined in this page. Use GetEvent to fetch event objects.
	 *
	 * @private
	 * @type Object
	 * @property aEvents
	 */
	aEvents: {},

	/**
	 * Collection of all of the forms on this page. Use the GetWidget method to retrieve form objects.
	 *
	 * @private
	 * @type Object
	 * @property aForms
	 */
	aForms: {},

	/**
	 * All of the top-level blocks for this page. Use the GetWidget or GetBlock* methods to search the block tree.
	 *
	 * @private
	 * @type Object
	 * @property aBlocks
	 */
	aBlocks: {},

	/**
	 * All of the singleton Validator objects. Use GetValidator to access this.
	 *
	 * @private
	 * @type Object
	 * @property aValidators
	 */
	aValidators: {},

	/**
	 * All of the singleton FormValidator objects. Use GetFormValidator to access this.
	 *
	 * @private
	 * @type Object
	 * @property aFormValidators
	 */
	aFormValidators: {},

	/**
	 * Set just before the page "unload" event fires to indicate that the page
	 * destruction has started.
	 *
	 * This can be used by (for example) AJAX error handlers, to stop them from
	 * warning about an aborted connection if the user hits refresh or
	 * navigates away from the page.  Note that if your request is made via the
	 * AjaxRequest method of ZC.Core.Block, the OnFailure handler will
	 * <strong>not</strong> be called if this flag is true.
	 *
	 * @type Boolean
	 * @property bUnloading
	 */
	bUnloading: false,

	/**
	 * This method gets called from the HTML, with the configuration data
	 * required for the widgets on the page. It is responsible for
	 * instantiating all the other required objects and wiring up events.
	 *
	 * This method fires the custom event 'ManagerInit' once all the widgets on
	 * the page have been set up.
	 *
	 * @param {Object} oConfig
	 */
	Init: function(oConfig, bIframeInit)
	{
		/*		
		 *	Should be fixed in YUI 2.8.0, so this code no longer necessary?
		 *
		if (!bIframeInit && window.frameElement && YAHOO.env.ua.ie)
		{
			// IE chucks out an "operation aborted" error if we try to init in the onDOMReady event in an iframe.
			// if we are loaded in an iframe, run our initialisation on load instead. I think most of the time we use iframes they are hidden
			// on load anyway, so a delay in their init shouldn't matter
			Evt.on(window, 'load', function() { this.Init(oConfig, true); }, this, true);
			return;
		}*/

		// add 'yui-pe' class to the document element. This allows the
		// yui-pe-content class to be added to HTML that is going to be
		// progressively enhanced, which hides it to avoid a flash of
		// un-enhanced content.
	    Dom.addClass(document.documentElement, "yui-pe"); 

		// check for quirks mode - add a class to the document so we can target CSS at quirks/standard mode
		if(document.compatMode == 'CSS1Compat')
		{
			Dom.addClass(document.documentElement, "docmode-standard");
		}
		else
		{
			Dom.addClass(document.documentElement, "docmode-quirks");
		}

		this.oConfig = oConfig;
		if (this.oConfig.DEBUG)
		{
			Evt.throwErrors = true;
			if (YAHOO.widget.Logger)
			{
				if (window.console && console.log)
					YAHOO.widget.Logger.enableBrowserConsole();
				else
				{
					Evt.onDOMReady(function() {
						var myContainer = document.createElement("div");
						document.body.appendChild(myContainer);
						this.oYUILogReader = new YAHOO.widget.LogReader(myContainer, { width: '800px' });
					}, null, this);
				}
			}
		}
		//YAHOO.log("Initialising...", "debug", "ZC.JSManager#Init");

		Evt.onDOMReady(function()
		{
			//YAHOO.log('onDOMReady fired', 'debug', 'ZC.JSManager#Init');

			// Add 'yui-skin-sam' classname to body tag
			Dom.addClass(document.body, 'yui-skin-sam');
			this.oOverlayManager = new YAHOO.widget.OverlayManager();

			if (!L.isUndefined(this.oConfig.ClientSideObjects))
			{
				//YAHOO.log("Started initialising CSOs", "debug", "ZC.JSManager#Init");
				U.ForEach(this.oConfig.ClientSideObjects, function(aDef, sName)
				{
					var fnConstructor = ZC.JSManager.GetComponent(aDef.Type, aDef.Module || 'Core', 'ClientSideObject');
					if (fnConstructor)
						this.aClientSideObjects[sName] = new fnConstructor(aDef);
				}, this);
				//YAHOO.log("Finished initialising CSOs", "debug", "ZC.JSManager#Init");
			}
			if (!L.isUndefined(this.oConfig.Blocks))
			{
				//YAHOO.log("Started initialising Blocks", "debug", "ZC.JSManager#Init");
				U.ForEach(this.oConfig.Blocks, function(aDef, sName)
				{
					this.aBlocks[sName] = ZC.Core.Block.NewFromDef(sName, aDef);
				}, this);
				//YAHOO.log("Finished initialising Blocks", "debug", "ZC.JSManager#Init");
			}
			if (!L.isUndefined(this.oConfig.Forms))
			{
				//YAHOO.log("Started initialising Forms", "debug", "ZC.JSManager#Init");
				U.ForEach(this.oConfig.Forms, function(aDef, sName)
				{
					this.RegisterForm(new ZC.Core.Form(sName, aDef, this));
				}, this);
				//YAHOO.log("Finished initialising Forms", "debug", "ZC.JSManager#Init");
			}

			/**
			 * Custom event that is fired when the manager has finished initialising the elements on the page
			 * @event ManagerInit
			 * @public
			 * @memberof ZC.JSManager
			 */
			var oInitEvent = this.GetEvent('ManagerInit');
			oInitEvent.fireOnce = true;
			oInitEvent.subscribe(function() { this.AutoTooltips(); }, this, true);
			oInitEvent.subscribe(this._AutoPopups, this, true);

			//YAHOO.log('Firing ManagerInit event', 'debug', 'ZC.JSManager#Init');
			oInitEvent.fire();

			var oAfterInitEvent = this.GetEvent('AfterManagerInit');
			oAfterInitEvent.fireOnce = true;
			oAfterInitEvent.fire();

			this.GetEvent('OnLoadEvent').fire();

			if (YAHOO.env.ua.ie)
			{
				// IE has some bizarre rendering bugs which are fixed by forcing a repaint:
				document.body.style.display = 'none';
				document.body.style.display = '';
			}

			Dom.removeClass(document.documentElement, "yui-pe"); 
		}, null, this);

		Evt.on(window, 'beforeunload', function()
		{
			this.bUnloading = true;
		}, this, true);

		Evt.on(window, 'unload', function() 
		{
			/*
			 * Clean up everything when the page is unloaded, required due to memory leaks in IE and FF (possibly others too).
			 * IE6 is worst affected of course..
			 */
			U.ForEach(this.aBlocks, function(oBlock) { 
				try { oBlock.Destruct(); } 
				catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }
			});
			U.ForEach(this.aClientSideObjects, function(oCSO) { 
				try { oCSO.Destruct(); } 
				catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }
			});

			if (this.oOverlayManager)
			{
				var aOverlays = this.oOverlayManager.cfg.getProperty('overlays');
				if (aOverlays)
				{
					U.ForEach(aOverlays, function(oOverlay)
					{
						oOverlay.destroy();
						oOverlay = null;
					});
					aOverlays = null;
				}
				this.oOverlayManager = null;
			}

			this.aClientSideObjects = {};
			this.aEvents = {};
			this.aForms = {};
			this.aBlocks = {};
			this.aValidators = {};
			this.aFormValidators = {};
			this.oAutoTooltips = null;
			this.oYUILogReader = null;

			ZC.JSManager = null;
			ZC = null;
		}, this, true);
	},

	/**
	 * Automatically attaches the YUI Tooltip widget to elements on the page
	 * that have the classname of 'tooltip'. We check that the Tooltip widget
	 * is loaded, and do nothing if not.
	 * @param [{HTMLElement}] optional element to scan for new tooltips
	 */
	AutoTooltips: function(elScan)
	{
		var aTooltips, oTTCfg, aContext;
		
		if (YAHOO.env.getVersion('container'))
		{
			aTooltips = Dom.getElementsByClassName('tooltip', undefined, elScan);
			if (aTooltips.length)
			{
				if (L.isUndefined(this.oAutoTooltips))
				{
					oTTCfg = { context: aTooltips, autofillheight: false, autodismissdelay: 120000, zIndex: 45, showdelay: 200 };
					if (!L.isUndefined(YAHOO.util.Anim))
						oTTCfg.effect = { effect: YAHOO.widget.ContainerEffect.FADE, duration: 0.25 };
					this.oAutoTooltips = new YAHOO.widget.Tooltip('zcb_auto_tt', oTTCfg);
					this.oOverlayManager.register(this.oAutoTooltips);
					this.oAutoTooltips.beforeShowEvent.subscribe(function() { this.oOverlayManager.bringToTop(this.oAutoTooltips); }, this, true);
				}
				else
				{
					aContext = this.oAutoTooltips.cfg.getProperty('context');
					aContext.push.apply(aContext, aTooltips);
					this.oAutoTooltips.cfg.setProperty('context', aContext);
				}
			}
		}
	},

	/**
	 * This method automatically attaches click handlers to any links with a
	 * class of 'popup', that open up a popup to load the target of the link.
	 * The width  and height can be set by adding a class name of the form
	 * 'w<width>h<height>' (e.g. 'w300h300' for a 300x300 window)
	 *
	 * TODO: It would be nice to use a YUI Panel instead. That requires some
	 * changes to the server-side code, so that we can request a URL via AJAX
	 * and receive the data in a suitable format (probably JSON, with extra
	 * data like css/scripts that need loading, dialog title, etc).
	 *
	 * @private
	 */
	_AutoPopups: function()
	{
		var fnOpenPopup = function(elLink, sURL, iWidth, iHeight)
		{
			var sOptions = 'status=1, resizable=1, scrollbars=yes';
			if (!L.isUndefined(iWidth))
				sOptions += ', width = ' + iWidth;
			if (!L.isUndefined(iHeight))
				sOptions += ', height = ' + iHeight;

			window.open(sURL, '_blank', sOptions);
		}

		var aPopups = Dom.getElementsByClassName('popup');
		if (aPopups.length)
		{
			Evt.on(aPopups, 'click', function(oEvent)
			{
				var elLink = Evt.getTarget(oEvent);
				var aMatches = elLink.className.match(/(w(\d+))?(h(\d+))?/);
				var iWidth = aMatches[2];
				var iHeight = aMatches[4];
				fnOpenPopup(elLink, elLink.href, iWidth, iHeight);
				Evt.stopEvent(oEvent);
			});
		}
	},

	/**
	 * Registers a form with the manager
	 * @param {Object} oForm form widget to register
	 */
	RegisterForm: function(oForm)
	{
		this.aForms[oForm.sName] = oForm;
		U.ForEach(oForm.aChildWidgets, function(oWidget) { oWidget.oForm = oForm; });
	},

	/**
	 * Takes a module, type and subtype and returns the required class
	 * @param {String} sCmpName component name, may contain module & type
	 * @param {String} sModule module name
	 * @param {String} sType object type (Block, Widget, EventListener, ...)
	 * @return {Object} the required object constructor
	 */
	GetComponent: function(sCmpName, sModule, sType)
	{
		var sFirstPart, sSecondPart, iFirstPos, iSecondPos, aTry = [], i, iMax;

		if (L.isUndefined(sCmpName))
			return false;

		iFirstPos = sCmpName.indexOf('_');
		if(iFirstPos != -1)
			sFirstPart = sCmpName.substring(0, iFirstPos);

		iSecondPos = sCmpName.indexOf('_', iFirstPos + 1);
		if (iSecondPos != -1)
			sSecondPart = sCmpName.substring(iFirstPos + 1, iSecondPos);

		if (sFirstPart && sSecondPart) // could be module_type_subtype
			aTry.push([sFirstPart, sSecondPart, sCmpName.substring(iSecondPos + 1)]);

		if (sFirstPart)
		{
		   	// could be type_subtype and separate module
			aTry.push([sModule || 'Core', sFirstPart, sCmpName.substring(iFirstPos + 1)]);

			// could be module_type
			aTry.push([sFirstPart, sCmpName.substring(iFirstPos + 1), undefined]);
		}

		aTry.push([sModule || 'Core', sType, sCmpName]);

		for (i = 0, iMax = aTry.length; i < iMax; i++)
		{
			var sM = aTry[i][0];
			var sT = aTry[i][1];
			var sST = aTry[i][2];

			if (L.isUndefined(ZC[sM]))
			{
				//YAHOO.log("Module '" + sM + "' not loaded", "debug", "ZC.JSManager#GetComponent");
				continue;
			}
			if (L.isUndefined(ZC[sM][sT]))
			{
				//YAHOO.log("Object type '" + sT + "' in module '" + sM + "' not loaded", "debug", "ZC.JSManager#GetComponent");
				continue;
			}

			if (L.isUndefined(sST))
			{
				// no subtype
				return ZC[sM][sT];
			}

			if (L.isUndefined(ZC[sM][sT][sST]))
			{
				//YAHOO.log("Object '" + sST + "' (type '" + sT + "') in module '" + sM + "': not loaded", "debug", "ZC.JSManager#GetComponent");
				continue;
			}

			//YAHOO.log("Loaded object ZC." + [sM, sT, sST].join('.'), "debug", "ZC.JSManager#GetComponent");
			return ZC[sM][sT][sST];
		}

		YAHOO.log("Unable to load component (" + [sCmpName, sModule, sType].join(", ") + ")", "warn", "ZC.JSManager#GetComponent");
		return undefined;
	},

	/**
	 * Loads a YUI component with YUI Loader
	 * @param {Object} oConfig config object in the format accepted by YUILoader
	 */
	LoadYUILib: function(oConfig)
	{
		var oYUILoader = this.GetCSO('YUILoader'), fnConstructor;
		if (L.isUndefined(oYUILoader))
		{
			fnConstructor = ZC.JSManager.GetComponent('YUILoader', 'Core', 'ClientSideObject');
			if (!fnConstructor)
			{
				YAHOO.log('Unable to locate YUILoader CSO', 'error', 'ZC.JSManager');
				return false;
			}
			
			this.aClientSideObjects['YUILoader'] = new fnConstructor(aDef);
		}

		oYUILoader.LoadYUILib(oConfig);
	},

	/**
	 * Retrieves a block by UniqueID.
	 * @param {String} sUniqueID to locate
	 * @return {Object} A block object, or <em>undefined</em> if not found
	 */
	GetBlockByUniqueID: function(sUniqueID)
	{
		for (var sName in this.aBlocks)
		{
			if (L.hasOwnProperty(this.aBlocks, sName))
			{
				if (this.aBlocks[sName].UniqueID() == sUniqueID)
					return this.aBlocks[sName];

				var oChildBlock = this.aBlocks[sName].GetBlockByUniqueID(sUniqueID);
				if (oChildBlock)
					return oChildBlock;
			}
		}
		return undefined;
	},

	/**
	 * Retrieves an array of blocks that pass the test applied by supplied boolean method
	 * For optimised performance supply a blockname, only its children will be tested
	 * @param {Function} fnMethod A boolean method for testing blocks, the only argument recieved is the block itself.
	 * @param {String | Object} mRoot (optional) A Block ID or a block to use as a starting point.
	 * @param {Object} oScope (optional) scope override for mRoot
	 * @return {Array} array of blocks.
	 */
	GetBlocksBy: function(fnMethod, mRoot, oScope)
	{
		var aBlocks = [], aRoot = false;
		if(!L.isUndefined(mRoot))
		{
			if(L.isString(mRoot))
				mRoot = this.GetBlockByUniqueID(mRoot);

			if(L.isObject(mRoot))
				aRoot = mRoot.aChildBlocks;
		}
		else
		{
			aRoot = this.aBlocks;
		}

		if(!aRoot)
			return [];

		ZC.Util.ForEach(aRoot, function(oBlock)
		{
			if(fnMethod.call(oScope, oBlock))
				aBlocks.push(oBlock);

			// Recurse through child blocks.
			aBlocks = aBlocks.concat(ZC.JSManager.GetBlocksBy(fnMethod, oBlock, oScope));
		});

		return aBlocks;
	},

	/** 
	 * @private 
	 */
	_aSearchObjects: [ 'aForms', 'aBlocks' ],

	/**
	 * Retrieves a CSO by name
	 * @param {String} sSearch CSO name
	 * @return {Object} the client-side object
	 */
	GetCSO: function(sSearch)
	{
		return this.aClientSideObjects[sSearch];
	},

	/**
	 * Retrieves an Event by name. Creates a new event object if no event exists with this name already.
	 * @param {String} sName Event name
	 * @return {Object} an event object
	 */
	GetEvent: function(sName)
	{
		if (L.isUndefined(this.aEvents[sName]))
			this.aEvents[sName] = new YAHOO.util.CustomEvent(sName);

		return this.aEvents[sName];
	},

	/**
	 * Gets a validator object
	 * @param {String} sName validator class name, either just the name of a validator in the Core_Validator_* namespace, or a full class name
	 * @return {Object} a validator object
	 */
	GetValidator: function(sName)
	{
		if (L.isUndefined(this.oValidatorRegex))
			this.oValidatorRegex = /^(.*)_Validator_(.*)$/;

		return this._GetValidator(sName, this.aValidators, this.oValidatorRegex);
	},

	/**
	 * Gets a form validator object
	 * @param {String} sName validator class name, either just the name of a validator in the Core_FormValidator_* namespace, or a full class name
	 * @return {Object} a form validator object
	 */
	GetFormValidator: function(sName)
	{
		if (L.isUndefined(this.oFormValidatorRegex))
			this.oFormValidatorRegex = /^(.*)_FormValidator_(.*)$/;

		return this._GetValidator(sName, this.aFormValidators, this.oFormValidatorRegex);
	},

	/**
	 * Helper function implementing the common code for Get(Form)Validator#
	 * @private
	 * @param {String} sName validator class name, either just the name of a validator in the Core_FormValidator_* namespace, or a full class name
	 * @param {Object} the array of existing validators
	 * @param {RegExp} the regular expression to match validator class names
	 * @return {Object} a validator object
	 */
	_GetValidator: function(sName, aValidators, oValidatorRegex)
	{
		if (!L.isUndefined(aValidators[sName]))
			return aValidators[sName];

		var aMatches = oValidatorRegex.exec(sName);
		if (!aMatches)
		{
			if (L.isUndefined(ZC.Core.Validator[sName]))
			{
				//YAHOO.log("unable to find validator '" + sName + "'", "info", "ZC.JSManager#GetValidator");
				return undefined;
			}
			return (aValidators[sName] = new ZC.Core.Validator[sName]());
		}
		else
		{
			if (L.isUndefined(ZC[aMatches[1]]) || L.isUndefined(ZC[aMatches[1]].Validator) || L.isUndefined(ZC[aMatches[1]].Validator[aMatches[2]]))
			{
				//YAHOO.log("unable to find validator '" + sName + "'", "info", "ZC.JSManager#GetValidator");
				return undefined;
			}
			return (aValidators[sName] = new ZC[aMatches[1]].Validator[aMatches[2]]());
		}
	},

	/**
	 * Constructs a URL from an array of parameters.
	 *
	 * The key of each item in <code>oParams</code> is the querystring variable name. 
	 * The value can either be a string or a widget (in fact, it can be any object 
	 * which defines a <code>GetValue</code> method).
	 *
	 * sURL may have a query string already present, in which case this is
	 * parsed and added to oParams. Values in oParams override anything
	 * specified in the query string of sURL. The '_ts' value from sURL's query
	 * string is always ignored, and the new state (passed in the JSON config
	 * array) will be used instead, unless it's overridden in oParams.
	 *
	 * @param {Object/String} oParams Query string parameters. 
	 * @param {String} [sURL] Optional URL. Uses the current location (including its query string) if not specified
	 * @return {String} a URL
	 */
	URL: function(oParams, sURL)
	{
		var iQSPos, fnURLArray, aParamParts;

		oParams = oParams || {};
		sURL = sURL || window.location.href.replace(/(\?|#).*/,'');

		if (L.isString(oParams))
		{
			// split param string
			oParams = this.ParseQueryString(oParams);
		}

		// use the state value we've received in the JSON config array, unless the caller has specified a state in oParams
		if (this.oConfig.StateID && L.isUndefined(oParams['_ts']))
			oParams['_ts'] = this.oConfig.StateID;

		if (sURL.lastIndexOf('/') == (sURL.length - 1))
			sURL += ZC.JSManager.oConfig.PAGE_INDEX;

		iQSPos = sURL.indexOf('?');
		if (iQSPos > -1)
		{
			// values specified in oParams take precedence over any present in the URL
			oParams = L.merge(this.ParseQueryString(sURL.substr(iQSPos + 1)), oParams);
			sURL = sURL.substr(0, iQSPos);
		}

		fnEncodeURLParams	= function(mVar, sArg)
		{
			if(mVar.length == 0)
				return '';
			
			var aParamParts = [];
			U.ForEach(mVar, function(Value, Key)
			{
				// values can be widgets
				if (L.isObject(Value) && Value.GetValue)
					Value = Value.GetValue();

				var sQSVar = (sArg ? sArg + '[' + Key + ']' : Key);

				if (L.isArray(Value) || L.isObject(Value))
				{
					aParamParts.push(fnEncodeURLParams(Value, sQSVar));
				}
				else
				{
					aParamParts.push(encodeURIComponent(sQSVar) + '=' + encodeURIComponent(Value));
				}
			});

			return aParamParts.join('&');
		}

		sURL = sURL + '?' + fnEncodeURLParams(oParams);
		return sURL;
	},

	/**
	 * Parses values from a query-string.
	 * @param {String} sParamString the query-string to parse
	 * @return {Object} the parsed values
	 */
	ParseQueryString: function(sParamString)
	{
		var aTmpParams = sParamString.split(/&/),
			oParams = {};

		U.ForEach(aTmpParams, function(sParamPair)
		{
			var aPair = U.Map(sParamPair.split(/=/, 2), decodeURIComponent);
			if (!aPair[0].length || !L.isUndefined(oParams[aPair[0]]))
				return; // continue to next element

			oParams[aPair[0]] = aPair[1] || '';
		});

		return oParams;
	},

	// backwards compatibility:
	Alert: U.Alert,

	/**
	 * Creates a dialog box for a widget, and registers it with the overlay manager.
	 *
	 * @param {String} sCaption the window caption
	 * @param {String/HTMLElement} elBody the body of the dialog box
	 * @param {HTMLElement} [elContainer = document.body] the container for the dialog
	 * @param {Object} [oConfig] additional config for the dialog box
	 * @returns {Object} The YUI dialog object
	 */
	CreateDialog: function(sCaption, elBody, elContainer, oConfig)
	{
		var elDialogContainer = document.createElement('div'),
		    elDialogHeader = document.createElement('div'),
		    elDialogBody = document.createElement('div'),
			oDialog, fnConstructor;

		if (!elContainer.appendChild)
		{
			throw new Error("Invalid elContainer: " + L.dump(elContainer));
		}

		elDialogHeader.className = 'hd';
		elDialogHeader.innerHTML = sCaption;
		elDialogBody.className = 'bd clearfix';

		if (L.isString(elBody))
		{
			elDialogBody.innerHTML = elBody;
		}
		else
		{
			elDialogBody.appendChild(elBody);
		}
		elDialogContainer.appendChild(elDialogHeader);
		elDialogContainer.appendChild(elDialogBody);
		(elContainer || document.body).appendChild(elDialogContainer);

		oConfig = L.merge({
			close: true,
			draggable: true,
			visible: false
		}, (oConfig || {}));

		if (L.isUndefined(oConfig.effect) && YAHOO.env.getVersion('animation'))
		{
			oConfig.effect = { effect:YAHOO.widget.ContainerEffect.FADE, duration:0.25 };
		}

		// We don't want the submit-handling "features" of the Dialog widget, we handle all of that ourselves.
		// We do however use the "yui-dialog" CSS class to give us the right styling on the "default" button, in some places.
		fnConstructor = YAHOO.widget.Panel;
		if (!L.isUndefined(oConfig.buttons))
		{
			// we have buttons.. suggests the caller wants a dialog rather than just a panel.
			fnConstructor = YAHOO.widget.Dialog;
		}

		oDialog = new fnConstructor(elDialogContainer, oConfig);
		Dom.addClass(oDialog.element, YAHOO.widget.Dialog.CSS_DIALOG);

		// YUI is not adding this any more (not sure why) - it's required to stop the IE table bleed-through bug.
		Dom.addClass(oDialog.element, YAHOO.widget.Overlay.CSS_HIDDEN);

		this.oOverlayManager.register(oDialog);
		oDialog.render();
		oDialog.hide();
		return oDialog;
	},

	/**
	 * Destroys a dialog previously created with CreateDialog
	 * @param {Object} the dialog object to destruct
	 */
	DestroyDialog: function(oDialog)
	{
		if (oDialog && oDialog.element)
		{
			this.oOverlayManager.remove(oDialog);

			// work around an IE bug, prevents a security warning if dialog contains a DIV that has a background image
			// http://support.microsoft.com/kb/925014
			oDialog.body.innerHTML = ' ';

			// true = shallow destruction; prevents slow script warning in IE.
			// Any child elements that also need purging should be referred to
			// by other Widget objects that should destroy them anyway.
			oDialog.destroy(true); 
		}
	}
}

L.augmentObject(ZC.JSManager, GetWidgetProvider.prototype);

/**
 * Base class for the Block classes.
 *
 * @class Block
 * @constructor
 * @namespace ZC.Core
 * @uses ZC.AttribProvider
 * @uses ZC.EventProvider
 * @uses ZC.GetWidgetProvider
 * @param {String} sName block name
 * @param {Array} aDef block definition
 * @param {Object} oParent parent block
 */
ZC.Core.Block = function(sName, aDef, oParent)
{
	/**
	 * Block name
	 * @type String
	 * @property sName
	 */
	this.sName = ('_blk_name' in aDef) ? aDef._blk_name : sName;

	/**
	 * Block definition. Use the methods from AttribProvider to access this.
	 * @type Object
	 * @property aDef
	 * @private
	 */
	this.aDef = aDef;

	/**
	 * Parent block
	 * @type Object
	 * @property oParent
	 */
	this.oParent = oParent;

	/**
	 * List of child blocks. Use GetWidget or GetBlocks* methods to access.
	 * @type Object
	 * @property aChildBlocks
	 * @private
	 */
	this.aChildBlocks = {};

	/**
	 * Collection of widgets on this block. Use GetWidget to access it.
	 * @type Object
	 * @property aWidgets
	 * @private
	 */
	this.aWidgets = {};

	/**
	 * Collection of forms on this block. Use GetWidget to access it.
	 * @type Object
	 * @property aForms
	 * @private
	 */
	this.aForms = {};

	//YAHOO.log("Calling CustomSetupStart", "debug", this.sName);
	if (!this.CustomSetupStart())
	{
		if (ZC.JSManager.oConfig.DEBUG && console.log)
			console.log('Warning: CustomSetupStart for ' + this.sName + ' returned false - block will not be set up.');

		throw new Error('CustomSetupStart returned false');
	}
	//YAHOO.log("CustomSetupStart done", "debug", this.sName);

	this.ProcessConfig(aDef, true);

	//YAHOO.log("Calling CustomSetupEnd", "debug", this.sName);
	if (!this.CustomSetupEnd())
	{
		try { this.Destruct(); } // undo anything done by ProcessConfig (attaching to events, etc) 
		catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }

		if (ZC.JSManager.oConfig.DEBUG && console.log)
			console.log('Warning: CustomSetupEnd for ' + this.sName + ' returned false - block will not be set up.');

		throw new Error('CustomSetupEnd returned false');
	}
	//YAHOO.log("CustomSetupEnd done", "debug", this.sName);

	this.AttribProviderSetup();
}

/**
 * Instantiates a new Block object given a definition. Looks for Module & Type
 * attribs in the def to locate the correct block class. If that class can't be
 * found, then the returned object will be an instance of ZC.Core.Block
 *
 * @static
 * @param {String} sName the block name
 * @param {Object} aDef the block definition
 * @param {Object} oParent the parent object
 * @return {Object} the new block object
 */
ZC.Core.Block.NewFromDef = function(sName, aDef, oParent)
{
	//YAHOO.log("Creating block called " + sName + " (" + (aDef.Module || 'Core') + "/" + aDef.Type + ")", "debug", "Core.Block.NewFromDef");
	var fnConstructor;
	if (!L.isUndefined(aDef.Type))
	{
	   	fnConstructor = ZC.JSManager.GetComponent(aDef.Type, aDef.Module || 'Core', 'Block');
	}

	if (L.isUndefined(fnConstructor))
	{
		// fall back to base class
		fnConstructor = ZC.Core.Block;
	}
	return new fnConstructor(sName, aDef, oParent);
}

/**
 * This static method creates an block class and extends the base
 * class. Creates the required namespace if it doesn't already exist.
 *
 * @static
 * @param {String} sBlockClassName the block name
 * @param {String} sModule the module for this block (defaults to Core)
 * @param {String} oParentClass the block to extend (defaults to the base block class)
 * @param {String} sType the block type, if not Block (e.g. Layout or Page)
 * @return {Object} the block object
 */
ZC.Core.Block.Create = function(sBlockClassName, sModule, oParentClass, sType)
{
	sModule = sModule || 'Core';
	oParentClass = oParentClass || ZC.Core.Block;
	sType = sType || 'Block';
	var oNS = ZC.Namespace(sModule + '.' + sType);

	oNS[sBlockClassName] = function(sName, aDef, oForm, oParent)
	{
		oNS[sBlockClassName].superclass.constructor.call(this, sName, aDef, oForm, oParent);
	}
	L.extend(oNS[sBlockClassName], oParentClass);

	return oNS[sBlockClassName];
}

ZC.Core.Block.prototype = {
	/**
	 * Called by the block constructor before the base block class has initialised.
	 *
	 * @return {Boolean} This method should return false if unable to complete the initialization, then the object will be discarded.
	 */
	CustomSetupStart: function()
	{
		return true;
	},

	/**
	 * Processes some configuration data containing new child blocks, widgets and forms.
	 * @param {Object} aDef the configuration data
	 * @param {Boolean} bConstructor (internal use) set to true when called from the constructor
	 */
	ProcessConfig: function(aDef, bConstructor)
	{
		if (!L.isUndefined(aDef.Blocks))
		{
			YAHOO.log("Started initialising child blocks", "debug", this.sName);
			U.ForEach(aDef.Blocks, function(aBlockDef, sBlockName)
			{
				if (L.isUndefined(this.aChildBlocks[sBlockName]) || !Dom.inDocument(this.aChildBlocks[sBlockName].GetElement()))
				{
					if (!L.isUndefined(this.aChildBlocks[sBlockName]))
					{
						try { this.aChildBlocks[sBlockName].Destruct() } 
						catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }

						this.aChildBlocks[sBlockName] = null;
						delete this.aChildBlocks[sBlockName];
					}
					try 
					{
						this.AddChildBlock(sBlockName, aBlockDef);
					}
					catch (ex)
					{
						YAHOO.log('Unable to add block ' + sBlockName + ' to ' + this.sName + ': ' + ex.message + " (" + ex.fileName + ":" + ex.lineNumber + ")", 'error', this.sName);
					}
				}
				else
				{
					this.aChildBlocks[sBlockName].ProcessConfig(aBlockDef);
				}
			}, this);
			YAHOO.log("Finished initialising child blocks", "debug", this.sName);

			delete aDef.Blocks;
		}

		if (!L.isUndefined(aDef.Widgets))
		{
			YAHOO.log("Started initialising widgets", "debug", this.sName);
			U.ForEach(aDef.Widgets, function(aWidgetDef, sWidgetName)
			{
				if (L.isUndefined(this.aWidgets[sWidgetName]) || !Dom.inDocument(this.aWidgets[sWidgetName]._elInput))
				{
					if (!L.isUndefined(this.aWidgets[sWidgetName]))
					{
						try { this.aWidgets[sWidgetName].Destruct() } 
						catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }

						this.aWidgets[sWidgetName] = null;
						delete this.aWidgets[sWidgetName];
					}
					try 
					{
						this.aWidgets[sWidgetName] = ZC.Core.Widget.NewFromDef(sWidgetName, aWidgetDef, undefined, this);
					}
					catch (ex)
					{
						YAHOO.log('Unable to add widget ' + sWidgetName + ' to ' + this.sName + ': ' + ex.message + " (" + ex.fileName + ":" + ex.lineNumber + ")", 'error', this.sName);
					}	
				}
				else
				{
					this.aWidgets[sWidgetName].ProcessConfig(aWidgetDef);
				}
			}, this);
			YAHOO.log("Finished initialising widgets", "debug", this.sName);

			delete aDef.Widgets;
		}

		if (!L.isUndefined(aDef.Forms))
		{
			YAHOO.log("Started initialising forms", "debug", this.sName);
			U.ForEach(aDef.Forms, function(aFormDef, sFormName)
			{
				if (L.isUndefined(this.aForms[sFormName]) || !this.aForms[sFormName] || !Dom.inDocument(this.aForms[sFormName]._elInput))
				{
					if (!L.isUndefined(this.aForms[sFormName]) && this.aForms[sFormName])
					{
						try { this.aForms[sFormName].Destruct() } 
						catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }

						this.aForms[sFormName] = null;
						delete this.aForms[sFormName];
					}
					try
					{
						this.aForms[sFormName] = new ZC.Core.Form(sFormName, aFormDef, this);
						ZC.JSManager.RegisterForm(this.aForms[sFormName]);
					}
					catch (ex)
					{
						YAHOO.log('Unable to add form ' + sFormName + ' to ' + this.sName + ': ' + ex.message + " (" + ex.fileName + ":" + ex.lineNumber + ")", 'error', this.sName);
					}
				}
				else
				{
					this.aForms[sFormName].ProcessConfig(aFormDef);
				}
			}, this);
			YAHOO.log("Finished initialising forms", "debug", this.sName);

			delete aDef.Forms;
		}

		if (!L.isUndefined(aDef.EventListeners))
		{
			this.AddEventListeners(aDef.EventListeners);
			delete aDef.EventListeners;
		}

		if (!L.isUndefined(aDef.Events))
		{
			this.AddEvents(aDef.Events);
			delete aDef.Events;
		}

		if (!bConstructor)
		{
			this.SetAttribs(aDef);
		}
	},

	/**
	 * Adds a child block to this object.
	 * @param {String} sBlockName the name for the new block
	 * @param {Object} aBlockDef the definition for the new block
	 * @return {Object} the new block object
	 */
	AddChildBlock: function(sBlockName, aBlockDef)
	{
		return (this.aChildBlocks[sBlockName] = ZC.Core.Block.NewFromDef(sBlockName, aBlockDef, this));
	},

	/**
	 * Called by the block constructor after the base block class has initialised.
	 *
	 * @return {Boolean} This method should return false if unable to complete the initialization, then the object will be discarded.
	 */
	CustomSetupEnd: function()
	{
		return true;
	},

	/**
	 * Frees up all the references in this object. Called on page unload to avoid memory leaks in IE.
	 */
	Destruct: function()
	{
		this.EventProviderDestruct();

		U.ForEach(this.aChildBlocks, function(oBlock) { 
			try { oBlock.Destruct(); } 
			catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }
		});
		U.ForEach(this.aWidgets, function(oWidget) { 
			try { oWidget.Destruct(); } 
			catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }
		});
		U.ForEach(this.aForms, function(oForm) { 
			try { oForm.Destruct(); } 
			catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }
		});

		if (!L.isUndefined(this._ajaxDialog))
		{
			ZC.JSManager.DestroyDialog(this._ajaxDialog);
			this._ajaxDialog = undefined;
		}

		this.oParent = null;
		this.aChildBlocks = {};
		this.aWidgets = {};
		this.aForms = {};
		this.elBlock = null;
	},

	/**
	 * If the block element ids have a common prefix, set this classvar so that GetElement will find them.
	 * @protected
	 * @type String
	 */
	_IDPrefix: 'blk',

	/**
	 * Gets the element which relates to this block.
	 * @return {HTMLElement} the block element
	 */
	GetElement: function()
	{
		if (L.isUndefined(this.elBlock))
		{
			this.elBlock = Dom.get(this._IDPrefix + this.UniqueID());
		}

		return this.elBlock;
	},

	/**
	 * Returns the element(s) to add events to.
	 * @return {Object/Array} either an element, or array of elements.
	 */
	GetEventElement: function()
	{
		return this.GetElement();
	},

	/**
	 * Returns the block's unique ID. Mirrors the server-side function.
	 *
	 * @return {String} unique identifier for this block
	 */
	UniqueID: function()
	{
		if (this.AttribIsset('BlockID'))
		{
			return this.GetAttrib('BlockID').toString();
		}
		else if (this.oParent)
		{
			return this.oParent.UniqueID() + '_' + this.sName;
		}
		else
		{
			return this.sName;
		}
	},

	/**
	 * Retrieves a block by UniqueID.
	 * @param {String} sUniqueID to locate
	 * @return {Object} A block object, or <code>undefined</code> if not found
	 */
	GetBlockByUniqueID: function(sUniqueID)
	{
		for (var sName in this.aChildBlocks)
		{
			if (L.hasOwnProperty(this.aChildBlocks, sName))
			{
				if (this.aChildBlocks[sName].UniqueID() == sUniqueID)
					return this.aChildBlocks[sName];

				var oChildBlock = this.aChildBlocks[sName].GetBlockByUniqueID(sUniqueID);
				if (oChildBlock)
					return oChildBlock;
			}
		}
		return undefined;
	},

	/**
	 * Sends an AJAX request to the server, to be processed by the server-side equivalent of this block object.
	 *
	 * Takes an object of parameters, can contain the following keys: 
	 * <dl>
	 * <dt> Transaction </dt><dd> name of the AJAX transaction, if specified this will call ProcessAjax_&lt;Transaction&gt; on the server</dd>
	 * <dt> URL         </dt><dd> override the URL that is used for making the request (defaults to current page).</dd>
	 * <dt> GetPostVars </dt><dd> containing name => value/object pairs (as accepted by ZC.JSManager.URL), extra parameters to put on the query</dd>
	 * <dt> Method      </dt><dd> request method ("GET" or "POST", default "GET")</dd>
	 * <dt> CustomObj   </dt><dd> a custom object that is passed to the ProcessAjaxResponse method</dd>
	 * <dt> Form        </dt><dd> a form object, if present will be passed to the YUI Connection Manager for serialisation.</dd>
	 * <dt> Scope       </dt><dd> scope for the On* functions, defaults to this block object. </dd>
	 * <dt> OnSuccess   </dt><dd> a function to run after the request has been successfully processed.</dd>
	 * <dt> OnFailure   </dt><dd> a function to run after if there is a error with the request.</dd>
	 * <dt> OnComplete  </dt><dd> a function to run after the request has returned, regardless of the status.</dd>
	 * <dt> LoadingIndicator</dt><dd> an object that controls a "loading" indicator. It should define two methods, Show and Hide. If not specified a default is used. </dd>
	 * </dl> 
	 *
	 * <em>NOTE:</em> For convenience, if <code>oParams</code> is just a string, then this specifies the "Transaction" attrib, all other options being defaults.
	 *
	 * All parameters are optional.
	 * @param {Object} oParams
	 */
	AjaxRequest: function(oParams)
	{
		var fnRequestSuccess,
			sServerError = '',
			fnFormatErrors = function(oErr) { 
				sServerError += oErr.Message;

				if (oErr.Backtrace) sServerError += oErr.Backtrace;

				sServerError += '<br><br>'; 
			};

		if (L.isUndefined(oParams))
			oParams = {};
		if (L.isString(oParams))
			oParams = { Transaction: oParams };

		if (L.isUndefined(oParams.OnComplete))
			oParams.OnComplete = function() {};
		if (L.isUndefined(oParams.OnSuccess))
			oParams.OnSuccess = function() {};
		if (L.isUndefined(oParams.Scope))
			oParams.Scope = this;
		if (L.isUndefined(oParams.LoadingIndicator))
			oParams.LoadingIndicator = U.DefaultLoadingIndicator;

		if (L.isUndefined(oParams.OnFailure))
		{
			if (ZC.JSManager.oConfig.DEBUG)
			{
				oParams.OnFailure = function(iCode, sStatus, oResp) { 
					if (iCode != -2 && oResp && oResp.responseText) // iCode is -2 if JSON parsing failed, don't bother trying again...
					{
						try 
						{
							oJSONResponse = YAHOO.lang.JSON.parse(oResp.responseText);
							if (oJSONResponse.Errors)
							{
								U.ForEach(oJSONResponse.Errors, fnFormatErrors);
							}
						}
						catch (e) {}
					}
					else if(iCode == -2)
					{
						// probably a PHP fatal error
						sServerError = oResp.responseText;
					}

					U.Alert("AJAX processing error: " + iCode + " " + sStatus + '<br>' + sServerError + '<br>' + (oResp ? oResp.responseXML : ''), { width: "800px" }); 
				};
			}
			else
			{
				oParams.OnFailure = function() {};
			}
		}

		fnRequestSuccess = function(oResp)
		{
			var Get = YAHOO.util.Get,
				oParams = oResp.argument,
				sResponseText,
				oJSONResponse,
				oOnLoadEvent = ZC.JSManager.GetEvent('OnLoadEvent'),
				fnLoadJS,
				fnLoadCSS,
				fnProcessBlocks;

			oOnLoadEvent.unsubscribeAll();

			fnLoadJS = function()
			{
				YAHOO.log("Loading JS..", "debug", "AjaxRequest");
				var aJS = U.Filter(oJSONResponse.LoadJS, function(sJS) { 
					if (!sJS.match(/^https?:\/\//))
					{
						var sPort = '';
						if (window.location.port != 80 && window.location.port != 443)
						{
							sPort = ':' + window.location.port;
						}
						sJS = window.location.protocol + '//' + window.location.hostname + sPort + sJS;
					}

					// Dom.getElementBy returns an empty array if no match was found
					return L.isArray(Dom.getElementBy(function(el) { return (el.src == sJS); }, 'script', document.getElementsByTagName('head')[0]));
				});
				Get.script(aJS, { 
					onSuccess: fnLoadCSS,
					onFailure: function(o)
					{
						oParams.LoadingIndicator.Hide();
						oParams.OnComplete.call(oParams.Scope, oResp);

						// do not call failure handler if the page is being reloaded or navigated away from
						if (!ZC.JSManager.bUnloading)
						{
							oParams.OnFailure.call(oParams.Scope, -4, 'JS loading error: ' + o.msg, oResp);
						}
					},
					scope: this 
				});
			}
			fnLoadCSS = function()
			{
				YAHOO.log("Loading CSS..", "debug", "AjaxRequest");
				U.ForEach(oJSONResponse.LoadCSSMedia, function(sMedia, iIndex)
				{
					var aCSS,
						elHead,
						oGetDef = { 
							attributes: { media: sMedia },
							onFailure: function(o)
							{
								oParams.LoadingIndicator.Hide();
								oParams.OnComplete.call(oParams.Scope, oResp);
								// do not call failure handler if the page is being reloaded or navigated away from
								if (!ZC.JSManager.bUnloading)
								{
									oParams.OnFailure.call(oParams.Scope, -5, 'CSS loading error: ' + o.msg, oResp);
								}
							},
							scope: this
						};

					if (iIndex == (oJSONResponse.LoadCSSMedia.length - 1))
					{
						oGetDef.onSuccess = fnProcessBlocks;
					}
					elHead = document.getElementsByTagName('head')[0];
					// filter out any CSS files we have already loaded
					// this isn't just for efficiency.. loading Core.css twice seems to trigger some FF bugs.
					aCSS = U.Filter(oJSONResponse.LoadCSS[sMedia], function(sCSS) { 
						var iLen = sCSS.length;

						// Dom.getElementBy returns an empty array if no match was found
						return L.isArray(Dom.getElementBy(function(el) { 
							// some browsers include the domain in the href
							// value, even if it's not present in the document.
							// only check the rightmost part of the string for equality.
							var iLen2 = el.href.length;
							if (iLen2 < iLen)
								return false;

							return (el.href.substr(iLen2 - iLen) == sCSS); 
						}, 'link', elHead));
					});
					elHead = null;
					Get.css(aCSS, oGetDef);
				}, this);
			}
			fnProcessBlocks = function()
			{
				YAHOO.log("Processing block updates", "debug", "AjaxRequest");
				var oFinishEvent = new YAHOO.util.CustomEvent(), oFinishTimer;
				oFinishEvent._updateAnimDone = true; // if animations need to be done to update elements, we need to defer the finish event until the animations are done.

				U.ForEach(oJSONResponse.Blocks, function(aBlockData, sBlockUID)
				{
					YAHOO.log("Processing updates for " + sBlockUID, "debug", "AjaxRequest");
					var oBlock;

					if (L.isFunction(this.UniqueID) && sBlockUID == this.UniqueID())
					{
						oBlock = this;
					}
					else
					{
						oBlock = ZC.JSManager.GetBlockByUniqueID(sBlockUID);
					}

					if (oBlock)
					{
						oBlock.ProcessAjaxResponse(aBlockData, oParams.CustomObj, oFinishEvent);
					}
					else
					{
						YAHOO.log("Unable to find block with ID " + sBlockUID, "error", "ProcessAjaxResponse");
					}
				}, this);

				oFinishTimer = L.later(100, this, function() {
					if (oFinishEvent._updateAnimDone)
					{
						oFinishEvent.fire();
						oOnLoadEvent.fire();
						oParams.LoadingIndicator.Hide();
						oParams.OnComplete.call(oParams.Scope);
						oParams.OnSuccess.call(oParams.Scope);
						oFinishTimer.cancel();
					}
				}, {}, true);
			}

			try 
			{
				YAHOO.log("Parsing JSON response", "debug", "AjaxRequest");
				oJSONResponse = YAHOO.lang.JSON.parse(oResp.responseText);
				YAHOO.log("Parsed OK", "debug", "AjaxRequest");
			}
			catch (e)
			{
				oParams.LoadingIndicator.Hide();
				oParams.OnComplete.call(oParams.Scope, oResp);
				oParams.OnFailure.call(oParams.Scope, -2, "JSON parse error", oResp);
				return;
			}

			if (oJSONResponse.GoToPage)
			{
				YAHOO.log("Redirecting to " + oJSONResponse.GoToPage, "debug", "AjaxRequest");
				window.location = oJSONResponse.GoToPage;
				return;
			}

			if (ZC.JSManager.oConfig.DEBUG && oJSONResponse.Errors)
			{
				U.ForEach(oJSONResponse.Errors, fnFormatErrors);
				U.Alert(sServerError, { width: "800px" });
			}

			if (!L.isUndefined(oJSONResponse.StateID))
			{
				ZC.JSManager.oConfig.StateID = oJSONResponse.StateID;
			}

			ZC.JSManager.LoadYUILib({
				require: U.Filter(oJSONResponse.LoadYUI, function(sReq) { return (YAHOO.env.getVersion(sReq) == null); }), 
				onSuccess: fnLoadJS, 
				onFailure: function(o) { 
					oParams.LoadingIndicator.Hide();
					oParams.OnComplete.call(oParams.Scope, oResp);
					// do not call failure handler if the page is being reloaded or navigated away from
					if (!ZC.JSManager.bUnloading)
					{
						oParams.OnFailure.call(oParams.Scope, -3, 'YUI loading error: ' + o.msg, oResp);
					}
				},
				scope: this
			});
		}

		// load connection manager & json library if necessary
		ZC.JSManager.LoadYUILib({
			require: ["connection", "json"],
			scope: this,
			onFailure: function(o) { U.Alert(o.msg); },
			onSuccess: function()
			{
				var Connect = YAHOO.util.Connect,
					bUseFileUpload, bSecure,
					aListeners,
					oGetPostVars = oParams.GetPostVars || {},
					oCallback = {
						scope: this,
						argument: oParams,
						timeout: 30000, // 30 seconds
						failure: function(o) { 
							oParams.LoadingIndicator.Hide();
							oParams.OnComplete.call(oParams.Scope, o); 
							// do not call failure handler if the page is being reloaded or navigated away from
							if (!ZC.JSManager.bUnloading)
							{
								oParams.OnFailure.call(oParams.Scope, o.status, o.statusText, o); 
							}
						},
						// work around a little discrepancy in the Connection Manager API - when an "upload" request is done, the responseText is HTML-escaped.
						// we can get the text value via responseXML though, which contains the document element from the iframe.
						// upstream bug is: http://yuilibrary.com/projects/yui2/ticket/2528941
						upload:  function(o) { var b = o.responseXML.body; o.responseText = (b.textContent || b.innerText); fnRequestSuccess.call(this, o); },
						success: fnRequestSuccess
					};

				// Use conditional comments to add an IE version header to the request. 
				// We use this instead of User-Agent, as it's more reliable (won't be confused by UA switchers)
				/*@cc_on
					Connect.initHeader("X-IE-Version", YAHOO.env.ua.ie);
				@*/

				if (L.isString(oGetPostVars))
				{
					oGetPostVars = ZC.JSManager.ParseQueryString(oGetPostVars);
				}
				
				if (!L.isUndefined(oParams.Form))
				{
					var elForm = oParams.Form._elInput;

					if (!L.isUndefined(YAHOO.widget.Button))
					{
						YAHOO.widget.Button.addHiddenFieldsToForm(elForm);
					}
					bUseFileUpload = (elForm.enctype == "multipart/form-data");
					bSecure = (bUseFileUpload && YAHOO.env.ua.ie && window.location.protocol.toLowerCase() == "https:");
					Connect.setForm(elForm, bUseFileUpload, bSecure);

					// YUI seems to have some bugs wrt multiple submit buttons, so we work around it here:
					var oEndWidget = oParams.Form.GetSelectedEndWidget();
					if (oEndWidget)
						oGetPostVars._ajaxEndWidget = oEndWidget.sName;

					oParams.URL = oParams.URL || oParams.Form.action;
					oParams.Method = oParams.Method || oParams.Form.method;
				}

				if (oParams.Transaction)
				{
					oGetPostVars._ajaxTr = oParams.Transaction;
				}

				if (oParams.Poll)
				{
					oGetPostVars._ajaxPoll = 1;
				}
				else
				{
					oGetPostVars._ajaxBlk = this.UniqueID();
				}
				Connect.asyncRequest(oParams.Method || 'POST', ZC.JSManager.URL(oGetPostVars, (oParams.URL || window.location.href.replace(/#.*$/, ''))), oCallback);

				oParams.LoadingIndicator.Show();
			}
		});
	},

	/**
	 * Performs an Ajax "poll" request. This simply makes the server check all blocks on the page to see if they want to update.
	 */
	AjaxPoll: function()
	{
		this.AjaxRequest({ Poll: true });
	},

	/**
	 * Processes the response from an AjaxRequest.
	 * @param {Object} oResponse the data sent back from the server
	 * @param {Object} oCustom the custom object passed into AjaxRequest
	 * @param {Object} oFinishEvent a custom event that is fired once all of the AJAX response has been processed.
	 */
	ProcessAjaxResponse: function(oResponse, oCustom, oFinishEvent)
	{
		U.ForEach(oResponse, function(oData, sAction)
		{
			switch (sAction)
			{
				case 'UpdateElement':
					U.ForEach(oData, function(oContent, sID)
					{
						oFinishEvent._updateAnimDone = false;
						oContent.OnComplete = function() { oFinishEvent._updateAnimDone = true; }
						U.UpdateElement(sID, oContent);
					});
					break;

				case 'Alert':
					U.Alert(oData.Message, oData);
					break;

				case 'Dialog':
					if (!L.isUndefined(this._ajaxDialog))
					{
						ZC.JSManager.DestroyDialog(this._ajaxDialog);
					}
					this._ajaxDialog = ZC.JSManager.CreateDialog(oData.Header, oData.Body, document.body, oData);
					this._ajaxDialog.setFooter(oData.Footer);
					ZC.JSManager.GetEvent('OnLoadEvent').subscribe(function() { this.show(); this.focus(); }, this._ajaxDialog, true);

					var aForms = this._ajaxDialog.element.getElementsByTagName('form');
					if (aForms)
					{
						U.ForEach(aForms, function(elForm) { elForm._ajaxDialog = this._ajaxDialog; }, this);
					}
					break;

				case 'ProcessConfig':
					oFinishEvent.subscribe(function() { this.ProcessConfig(oData); }, this, true);
					break;

				case 'ScheduleRequest':
					if (L.isUndefined(this._aScheduledRequests))
						this._aScheduledRequests = [];

					U.ForEach(oData, function (mRequest)
					{
						if (!U.Some(this._aScheduledRequests, function(o) { return U.ObjectsEqual(o, mRequest); }))
							this._aScheduledRequests.push(mRequest);
					}, this);

					if (L.isUndefined(this._oScheduledRequestTimer))
					{
						this._oScheduledRequestTimer = oFinishEvent.subscribe(this._RunScheduledRequests, this, true);
					}
					break;

				case 'FireEvent':
					U.ForEach(oData, function (aParamObjects, sEventName)
					{
						var oEvent = ZC.JSManager.GetEvent(sEventName);
						U.ForEach(aParamObjects, oEvent.fire, oEvent);
					});
					break;

				case 'PopulateForm':
					U.ForEach(oData, function (oArgs, sFormName)
					{
						var oForm = this.GetWidget(sFormName);
						if (oForm)
							oForm.PopulateFromArray(oArgs.Values, oArgs.Original);
					}, this);
			}
		}, this);
	},

	/**
	 * Runs the requests in _aScheduledRequests
	 * @private
	 */
	_RunScheduledRequests: function()
	{
		var mRequest;

		// using shift() should cope with requests that are scheduled while processing other scheduled requests
		while (mRequest = this._aScheduledRequests.shift())
		{
			this.AjaxRequest(mRequest);
		}

		this._oScheduledRequestTimer = undefined;
	},

	/**
	 * Closes a dialog opened by an AjaxRequset
	 */
	CloseAjaxDialog: function()
	{
		if (this._ajaxDialog)
		{
			ZC.JSManager.DestroyDialog(this._ajaxDialog);
			this._ajaxDialog = null;
		}
	},

	/** 
	 * @private 
	 */
	_aSearchObjects: [ 'aWidgets', 'aChildBlocks', 'aForms' ]
}

L.augment(ZC.Core.Block, AttribProvider);
L.augment(ZC.Core.Block, GetWidgetProvider);
L.augment(ZC.Core.Block, EventProvider);

/**
 * Base class for Layouts.
 *
 * @class Layout
 * @constructor
 * @namespace ZC.Core
 * @extends ZC.Core.Block
 * @param {String} sName block name
 * @param {Array} aDef block definition
 * @param {Object} oParent parent block
 */
ZC.Core.Layout = function(sName, aDef, oParent)
{
	ZC.Core.Layout.superclass.constructor.call(this, sName, aDef, oParent);
}
L.extend(ZC.Core.Layout, ZC.Core.Block);

/**
 * Base class for Pages.
 *
 * @class Page
 * @constructor
 * @namespace ZC.Core
 * @extends ZC.Core.Block
 * @param {String} sName block name
 * @param {Array} aDef block definition
 * @param {Object} oParent parent block
 */
ZC.Core.Page = function(sName, aDef, oParent)
{
	ZC.Core.Page.superclass.constructor.call(this, sName, aDef, oParent);
}
L.extend(ZC.Core.Page, ZC.Core.Block);

/**
 * Base class for all the Widget classes.
 *
 * @class Widget
 * @namespace ZC.Core
 * @uses ZC.AttribProvider
 * @uses ZC.EventProvider
 * @uses ZC.GetWidgetProvider
 * @constructor
 * @param {String} sName widget name
 * @param {Array} aDef an associative array containing the widget definition
 * @param {Object} oParent the parent of this widget, if available
 */
ZC.Core.Widget = function(sName, aDef, oForm, oParent)
{
	//YAHOO.log("Started initialising " + sName, "debug", sName);

	/**
	 * Widget name
	 * @property sName
	 * @type String
	 */
	this.sName = sName;

	/**
	 * Widget definition
	 * @property aDef
	 * @type Object
	 */
	this.aDef = aDef;

	/**
	 * Parent widget
	 * @property oParent
	 * @type Object
	 */
	this.oParent = oParent;

	/**
	 * Form containing this widget
	 * @property oForm
	 * @type ZC.Core.Form
	 */
	this.oForm = oForm;

	/**
	 * Array of child widgets. Use GetWidget to access.
	 * @property aChildWidgets
	 * @type Object
	 * @private
	 */
	this.aChildWidgets = {};

	/**
	 * Keeps track of whether the widget is enabled or not
	 * @property bEnabled
	 * @type Boolean
	 * @private
	 */
	this.bEnabled = true;

	this._FindElements();
	if (!this.CustomSetupStart())
	{
		throw new Error("CustomSetupStart returned false");
	}

	this.ProcessConfig(this.aDef, true);

	if (!L.isUndefined(aDef.ValidateOnEvents))
	{
		this.AddEvent(this.Validate, aDef.ValidateOnEvents, this);
	}

	if (!this.CustomSetupEnd())
	{
		throw new Error("CustomSetupEnd returned false");
	}
	//YAHOO.log("Finished initialising " + sName, "debug", sName);
	
	this.AttribProviderSetup();
}

/**
 * This static method creates an widget class and extends the base
 * class. Creates the required namespace if it doesn't already exist.
 *
 * @param {String} sWidgetClassName the widget name
 * @param {String} sModule the module for this widget (defaults to Core)
 * @param {String} oParentClass the widget to extend (defaults to the base widget class)
 * @return {Object} the widget object
 */
ZC.Core.Widget.Create = function(sWidgetClassName, sModule, oParentClass)
{
	sModule = sModule || 'Core';
	oParentClass = oParentClass || ZC.Core.Widget;
	var oNS = ZC.Namespace(sModule + '.Widget');

	oNS[sWidgetClassName] = function()
	{
		oNS[sWidgetClassName].superclass.constructor.apply(this, arguments);
	}
	L.extend(oNS[sWidgetClassName], oParentClass);

	return oNS[sWidgetClassName];
}

/**
 * Static factory for instantiating widgets from a widget def.
 *
 * This method checks the def for a module / type name (WidgetType overrides
 * Type), and tries to load the object relating to that widget. If it can't
 * find the object, it falls back to using the base class.
 *
 * @param {String} sName widget name
 * @param {Array} aDef widget definition
 * @param {Object} oParent (optional) parent widget
 * @return {Object} the new widget
 */
ZC.Core.Widget.NewFromDef = function(sName, aDef, oForm, oParent)
{
	var fnConstructor = ZC.JSManager.GetComponent(aDef.WidgetType || aDef.Type, aDef.Module, 'Widget');
	if (L.isUndefined(fnConstructor))
	{
		// fall back to base class
		fnConstructor = ZC.Core.Widget;
	}
	return new fnConstructor(sName, aDef, oForm, oParent);
}

ZC.Core.Widget.prototype = {
	/**
	 * Called by the widget constructor before the base widget class has initialised.
	 *
	 * @return {Boolean} This method should return false if unable to complete the initialization, then the Javascript object will be discarded.
	 */
	CustomSetupStart: function()
	{
		return true;
	},

	/**
	 * Processes configuration data
	 * @param {Object} aDef the configuration to process
	 * @param {Boolean} bConstructor (internal use) set to true when called from the constructor
	 */
	ProcessConfig: function(aDef, bConstructor)
	{
		if (!L.isUndefined(aDef.Widgets))
		{
			//YAHOO.log("Started initialising Widgets", "debug", sName);
			U.ForEach(aDef.Widgets, function(aWidgetDef, sWidgetName)
			{
				if (L.isUndefined(this.aChildWidgets[sWidgetName]) || !Dom.inDocument(this.aChildWidgets[sWidgetName]._elInput))
				{
					if (!L.isUndefined(this.aChildWidgets[sWidgetName]))
					{
						try { this.aChildWidgets[sWidgetName].Destruct() } 
						catch (e) { YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); }

						this.aChildWidgets[sWidgetName] = null;
						delete this.aChildWidgets[sWidgetName];
					}
					try 
					{
						this.AddChildWidget(sWidgetName, aWidgetDef);
					}
					catch (ex)
					{
						YAHOO.log('Unable to add widget ' + sWidgetName + ' to ' + this.sName + ': ' + ex.message + " (" + ex.fileName + ":" + ex.lineNumber + ")", 'error', this.sName);
					}
				}
				else
				{
					this.aChildWidgets[sWidgetName].ProcessConfig(aWidgetDef);
				}
			}, this);
			//YAHOO.log("Finished initialising Widgets", "debug", sName);
			
			delete aDef.Widgets;
		}
	
		// Check that the widget has been initialised OK. We need either an _elInput or some child widgets.
		if ((!this._elInput || (L.isArray(this._elInput) && this._elInput.length == 0)) && !this.HasChildWidgets())
		{
			throw new Error("No input element or child widgets");
		}

		if (!L.isUndefined(aDef.EventListeners))
		{
			this.AddEventListeners(aDef.EventListeners);
			delete aDef.EventListeners;
		}
		// make sure Validation is an array of Validator => msg or null
		if (L.isUndefined(aDef.Validation))
		{
			aDef.Validation = {};
		}
		else if (L.isString(aDef.Validation))
		{
			var sValidator = aDef.Validation;
			aDef.Validation = {};
			aDef.Validation[sValidator] = null;
		}
		else
		{
			var oNumberRegex = /^\d+$/;
			U.ForEach(aDef.Validation, function(sValidator, Key, aValidation)
			{
				if (oNumberRegex.test(Key))
				{
					aValidation[sValidator] = null;
					delete aValidation[Key];
				}
			});
		}
		this.aDef.Validation = L.merge(this.aDef.Validation || {}, aDef.Validation);

		if (!L.isUndefined(aDef.Events))
		{
			this.AddEvents(aDef.Events);
			delete aDef.Events;
		}

		if (!bConstructor)
		{
			this.SetAttribs(aDef);
		}
	},

	/**
	 * Called by the widget constructor after the base widget class has initialised.
	 *
	 * @return {Boolean} This method should return false if unable to complete the initialization, then the Javascript object will be discarded.
	 */
	CustomSetupEnd: function()
	{
		return true;
	},

	/**
	 * Frees up all the references in this object. Called on page unload to avoid memory leaks in IE.
	 *
	 * <p><em>NOTE:</em> destructors should cope with being called more than once, or being
	 * called on a half-initialised object. Check object references are good
	 * before trying to call methods on them!</p>
	 */
	Destruct: function()
	{
		try 
		{
			U.ForEach(this.aChildWidgets, function(oWidget) { 
				oWidget.Destruct();
			});
		}
		catch (e) 
		{ 
			YAHOO.log("Error during destruction: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName);
		}
		this.EventProviderDestruct();

		this.oParent = null;
		this.oForm = null;
		this.aChildWidgets = {};
		this._elInput = null;
		this.elContainer = null;
		this.elValidationContainer = null;
		this.elRODisplay = null;
	},

	/**
	 * Works out the element ID from the widget/form/parent name
	 * @param {String} optional suffix to be appended to the ID
	 * @return {String} element id
	 */
	ID: function()
	{
		var aArgs = arguments;
		var sID = '';

		if (!L.isUndefined(this.aDef.ID))
		{
			sID = this.aDef.ID;
		}
		else
		{
			if (!L.isUndefined(this.oForm))
				sID += this.oForm.sName + '.';

			if (this.AttribIsset('HTMLNamePrefix'))
				sID += this.GetAttrib('HTMLNamePrefix');

			sID += this.sName;
		}
		
		if (!L.isUndefined(aArgs[0]))
			sID += aArgs[0];
		
		return sID.replace(/[ ,()/]/g,"");
	},
	
	/**
	 * Alias for ID()
	 * Works out the element ID from the widget/form/parent name
	 * @protected
	 * @param {String} optional suffix to be appended to the ID
	 * @return {String} element id
	 */
	 _WidgetNameToID: function()
	 {
	 	var aArgs = arguments;
		if (!L.isUndefined(aArgs[0]))
			return this.ID(aArgs[0]);
		else	
			return this.ID();
	 },
	
	/**
	 * Populates _elInput, then calls _FindContainers to populate elContainer
	 * and elValidationContainer.  This default implementation checks the def
	 * for an id, otherwise looks for elements with an ID made up of the widget
	 * name and any parent widget names.
	 * @protected
	 */
	_FindElements: function() {
		var sID = this._WidgetNameToID();

		this._elInput = Dom.get(sID);
		if (this._elInput)
			this._FindContainers();
	},

	/**
	 * By default, the _FindContainers method adds a change handler that updates the "ro-display" element for this widget.
	 * Set this to false to suppress this behaviour.
	 * @property bChangeRODisplay
	 * @type Boolean
	 * @protected
	 */
	bChangeRODisplay: true,

	/**
	 * Populates elContainer and elValidationContainer.
	 * @protected
	 */
	_FindContainers: function()
	{
		var elInput;
		if (L.isArray(this._elInput))
			if (this._elInput.length && this._elInput[0])
				elInput = this._elInput[0];
			else
				return;
		else
			if (this._elInput)
				elInput = this._elInput;
			else
				return;

		if (L.isUndefined(this.elContainer))
		{
			if (this.oParent && this.oParent instanceof ZC.Core.Widget.Group && this.oParent._elInput)
			{
				this.elContainer = this.oParent._elInput;
			}
			else
			{
				this.elContainer = Dom.getAncestorByClassName(elInput, 'form_field') || Dom.get('tr_' + elInput.id) || elInput.parentNode;
			}
		}

		if (this.elContainer)
		{
			if (L.isUndefined(this.elContainer._zcRefCount))
				this.elContainer._zcRefCount = 1;
			else
				++this.elContainer._zcRefCount;

			this.elValidationContainer = Dom.get(this.GetAttribDefault('ValidationID', elInput.id + '_validation'));

			if (!this.elValidationContainer)
			{
				var aValContainers = Dom.getElementsByClassName('form_validation', null, this.elContainer);
				if (!aValContainers.length)
					aValContainers = Dom.getElementsByClassName('validation', null, this.elContainer);

				if (aValContainers.length)
				{
					this.elValidationContainer = aValContainers[0];
				}
				else if (!this.aDef.NoCreateValidation)
				{
					this.elValidationContainer = document.createElement('span');
					this.elValidationContainer.className = 'form_validation hide';
					elInput.parentNode.appendChild(this.elValidationContainer);
				}
			}
		}

		this.elRODisplay = Dom.get(elInput.id + '_rodisplay');
		if (this.elRODisplay && this.bChangeRODisplay)
		{
			this.AddEvent(function() { this.elRODisplay.innerHTML = this.GetHTMLValue(); }, 'change', this);
		}
	},

	/**
	 * Gets the value of this widget.
	 * @return the widget value
	 */
	GetValue: function()
	{
		if (!this._elInput || L.isUndefined(this._elInput.value))
			return undefined;

		return this._elInput.value;
	},

	/**
	 * @return {Boolean} true if the widget has a value.
	 */
	HasValue: function()
	{
		var mValue = this.GetValue();
		if (L.isString(mValue))
		{
			mValue = mValue.replace(/(^\s*|\s*$)/g, '');
		}
		else if (mValue === 0)
		{
			return true;
		}

		return Boolean(mValue);
	},

	/**
	 * Sets the value of this widget
	 * @param Value the value to set the widget to
	 */
	SetValue: function(Value)
	{
		if (!this._elInput || L.isUndefined(this._elInput.value))
		{
			//YAHOO.log("this._elInput is undefined and SetValue is not overridden", "error", this.sName + "#SetValue");
			return undefined;
		}

		if (Value == this._elInput.value)
			return;

		this._elInput.value = Value;

		// reset validation status
		this._bIsValid = undefined;

		this._FireEventHandlers('change');
	},

	/**
	 * Returns the text value of the widget. Used for things like Select and Radio widgets where the caption needs looking up.
	 * @return {String} the text value of the widget
	 */
	GetTextValue: function()
	{
		return this.GetValue();
	},

	/**
	 * Returns the HTML value of the widget. Used for things like Select and Radio widgets where the caption needs looking up.
	 * @return {String} the HTML value of the widget
	 */
	GetHTMLValue: function()
	{
		return this.GetTextValue();
	},

	/**
	 * Gets the caption for this widget. If not set in the def, look for the label element relating to elInput.
	 * @return {String} the widget caption
	 */
	GetCaption: function()
	{
		if (this.aDef.Caption)
			return this.aDef.Caption;

		if (this._elInput)
		{
			var elLabel = this.GetLabelEl();
			return L.isUndefined(elLabel) ? undefined : (elLabel.innerText || elLabel.textContent);
		}

		return undefined;
	},

	/**
	 * Returns the label element for this widget
	 */
	GetLabelEl: function()
	{
		var sInputId = this._WidgetNameToID(), 
			elLabel;

		elLabel = Dom.getElementBy(function(el) { return Dom.getAttribute(el, 'for') == sInputId; }, 'label');

		if (!elLabel && this._elInput)
		{
			Dom.batch(this._elInput, function(elInput) {
				elLabel = elLabel || Dom.getElementBy(function(el) { return Dom.getAttribute(el, 'for') == elInput.id; }, 'label');
			});
		}

		return elLabel;
	},

	/**
	 * Returns the fieldset element containing this widget, or undefined if none.
	 */
	GetFieldsetEl: function()
	{
		var elFieldset, elInput, sName;

		if (this._elInput)
		{
			if (L.isArray(this._elInput))
			{
				elInput = this._elInput[0];
			}
			else
			{
				elInput = this._elInput;
			}
			elFieldset = Dom.getAncestorByTagName(elInput, 'FIELDSET');
		}
		else if (this.aChildWidgets)
		{
			for (sName in this.aChildWidgets)
			{
				if (L.hasOwnProperty(this.aChildWidgets, sName))
				{
					elFieldset = this.aChildWidgets[sName].GetFieldsetEl();
					if (elFieldset)
						break;
				}
			}
		}

		return elFieldset;
	},

	/**
	 * Returns the name of the fieldest containing this widget.
	 */
	GetFieldsetName: function()
	{
		var elFieldset = this.GetFieldsetEl();

		if (elFieldset)
		{
			if (this.oForm)
			{
				return elFieldset.id.replace(new RegExp('^' + this.oForm.sName + '_'), '');
			}
			else
			{
				return elFieldset.id;
			}
		}

		return undefined;
	},

	/**
	 * Checks if the widget is read-only
	 * @return {boolean} true if the widget is readonly
	 */
	IsReadOnly: function()
	{
		return (this.GetAttribDefault('DisplayAs', '').toLowerCase() == 'readonly');
	},

	/**
	 * Clears the widget. Mainly used for searching, this should set the widget to the value it would have on an empty search form.
	 */
	Clear: function()
	{
		if (!this._elInput && !this.HasChildWidgets())
		{
			//YAHOO.log('this._elInput is undefined, no child widgets and Clear is not overridden', "error", this.sName + "#Clear");
			return;
		}

		if (this._elInput && this._elInput.type)
		{
			var sInputType = L.isArray(this._elInput) ? 'array' : this._elInput.type.toLowerCase();
			if (sInputType != 'button' && sInputType != 'submit')
				this.SetValue('');
		}

		U.ForEach(this.aChildWidgets, function(oWidget) { oWidget.Clear() });
	},

	/**
	 * Enables / disables the widget
	 * @param {Boolean} bEnable if false, then this method disables the widget
	 * @param {String} sEnableClass if set, this class is added to an enabled widget, and removed from a disabled one
	 * @param {String} sDisableClass if set, this class is removed from an enabled widget, and added to a disabled one
	 */
	Enable: function(bEnable, sEnableClass, sDisableClass)
	{
		if (!this._elInput && !this.HasChildWidgets())
		{
			//YAHOO.log('this._elInput is undefined, no child widgets and Enable is not overridden', "error", this.sName + "#Enable");
			return undefined;
		}

		if (L.isUndefined(bEnable))
			bEnable = true;

		if (this.bEnabled == bEnable)
			return;

		this.bEnabled = bEnable;

		if (this._elInput)
		{
			Dom.batch(this._elInput, function(el)
			{
				if (L.isUndefined(el.parentNode) || !el.parentNode)
			   		return;
				if (L.isUndefined(el.parentNode.tagName) || !el.parentNode.tagName)
					return;

				if (!bEnable) el.blur();
				el.disabled = !bEnable;
			});
		}

		var sAddClass = bEnable ? sEnableClass : sDisableClass;
		var sRemoveClass = bEnable ? sDisableClass : sEnableClass;
		this.ReplaceClass(sRemoveClass, sAddClass);

		this._EventDispatcher('EnableDisable', bEnable);
	},

	/**
	 * Disables the widget. This just calls this.Enable(false), so you normally don't need to override this if you're already providing Enable.
	 * @param {String} sEnableClass if set, this class is added to an enabled widget, and removed from a disabled one
	 * @param {String} sDisableClass if set, this class is removed from an enabled widget, and added to a disabled one
	 */
	Disable: function(sEnableClass, sDisableClass)
	{
		this.Enable(false, sEnableClass, sDisableClass);
	},

	/**
	 * Attrib method to handle the "Disabled" attrib (calls the Enable method)
	 */
	AttribMethod_Disabled: function(bValue)
	{
		this.Enable(!bValue);
	},

	/**
	 * Check if the widget is enabled or not.
	 */
	IsEnabled: function()
	{
		return this.bEnabled;
	},

	/**
	 * Shows / hides the widget
	 * @param {Boolean} bShow if false, then this method hides the widget
	 */
	Show: function(bShow)
	{
		if (L.isUndefined(bShow))
			bShow = true;

		if (bShow)
		{
			Dom.removeClass(this.elContainer, 'hide');
			if (this.GetAttribDefault('Overlabel'))
				this._ShowOverlabel(this.GetValue() === '');
		}
		else
		{
			Dom.batch(this.elContainer, function(el)
			{
				el.blur();
				Dom.addClass(el, 'hide');
			});
			this._ShowOverlabel(false);
		}

		U.ForEach(this.aChildWidgets, function(oWidget) { oWidget.Show(bShow); });

		this._EventDispatcher('ShowHide', bShow);
	},

	/**
	 * Hides the widget. This just calls this.Show(false), so you normally don't need to override this if you're already providing Show.
	 */
	Hide: function()
	{
		this.Show(false);
	},

	/**
	 * Toggles the visibility
	 */
	AttribMethod_Visible: function(bValue)
	{
		this.Show(bValue);
	},

	/**
	 * Changes the widget's caption.
	 */
	AttribMethod_Caption: function(sCaption)
	{
		// do not change captions during initial setup
		if (this._bAttribProviderSetup)
			return;

		var elLabel = this.GetLabelEl();

		if (elLabel)
		{
			if (this.HasValidator('NotBlank'))
				sCaption += this.GetAttribDefault('RequiredSuffix', (ZC.JSManager.oConfig.REQUIRED_FIELD_CAPTION_SUFFIX || ' *'));

			elLabel.innerHTML = sCaption;
		}
	},

	/**
	 * Sets up the widget for "overlabels", where CSS is used to overlay the label on the element until it's focussed or non-empty
	 */
	AttribMethod_Overlabel: function(bEnabled)
	{
		var elLabel, bShow;

		if (bEnabled)
		{
			if (this._elInput && !L.isArray(this._elInput) && (elLabel = this.GetLabelEl()))
			{
				// HTML5 has a 'placeholder' attrib on input boxes, which does it all for you
				if ('placeholder' in this._elInput)
				{
					this._elInput.placeholder = elLabel.innerHTML;
					Dom.addClass(elLabel, 'hide');
					return;
				}

				Dom.addClass(elLabel, 'zcOverlabel');
				this._ShowOverlabel(this.GetValue() === '' && this.IsVisible()); // set initial styles

				// Set handlers to show and hide labels.
				this.AddEvent(function() { this._ShowOverlabel(false); }, 'focus', this);
				this.AddEvent(function() { if (this.GetValue() === '') this._ShowOverlabel(true); }, 'blur', this);

				// Handle clicks to LABEL elements (for Safari).
				if (YAHOO.env.ua.webkit)
				{
					Evt.on(elLabel, 'click', function () { this.focus(); }, this._elInput, true);
				}
			}
			else
			{
				this.SetAttrib('Overlabel', false);
			}
		}
		else
		{
			if ('placeholder' in this._elInput)
			{
				this._elInput.placeholder = '';
				elLabel = this.GetLabelEl();
				if (elLabel)
					Dom.removeClass(elLabel, 'hide');

				return
			}
			Dom.removeClass(elLabel, 'zcOverlabel');
			Dom.setStyle(elLabel, 'text-indent', '0px');
		}
	},

	/**
	 * Handles the show/hide of overlabels
	 * @private
	 */
	_ShowOverlabel: function(bShow)
	{
		var elLabel = this.GetLabelEl(), oRegion, aXY;
		if (L.isUndefined(bShow))
			bShow = true;

		if (bShow)
		{
			oRegion = Dom.getRegion(this._elInput);

			aXY = [oRegion.x + 5, oRegion.y];
			if (!YAHOO.env.ua.ie)
				aXY[1] += 5;

			Dom.setXY(elLabel, aXY);
		}
		Dom.setStyle(elLabel, 'text-indent', (bShow) ? '0px' : '-4000px');
	},
		
	/**
	 * Returns the visibility of the widget.
	 * @return boolean true if the widget is visible
	 */
	IsVisible: function()
	{
		return !(Dom.hasClass(this._elInput, 'hide') || Dom.hasClass(this.elContainer, 'hide'));
	},

	/**
	 * Adds the given class to the widget's HTML elements - the container, and the input
	 *
	 * @param {String} sClassName class to add
	 */
	AddClass: function(sClassName)
	{
		if (this.elContainer._zcRefCount == 1)
			Dom.addClass(this.elContainer, sClassName);
		if (this._elInput)
			Dom.addClass(this._elInput, sClassName);
	},

	/**
	 * Removes the given class from the widget's HTML elements - the container, and the input
	 *
	 * @param {String} sClassName class to add
	 */
	RemoveClass: function(sClassName)
	{
		if (this.elContainer._zcRefCount == 1)
			Dom.removeClass(this.elContainer, sClassName);
		if (this._elInput)
			Dom.removeClass(this._elInput, sClassName);
	},

	/**
	 * Replaces one class with another on the widget's HTML elements - the container, and the input
	 * If the old class isn't on the element already, it just adds the new class
	 *
	 * @param {String} sClassName class to add
	 * @returns {Array} array of booleans indicating success/failure, the first index refers to the container, the second to the input
	 */
	ReplaceClass: function(sOldClass, sNewClass)
	{
		if (sNewClass)
		{
			if (this.elContainer && this.elContainer._zcRefCount == 1)
				Dom.replaceClass(this.elContainer, sOldClass, sNewClass);
			if (this._elInput)
				Dom.replaceClass(this._elInput, sOldClass, sNewClass);
		}
		else
		{
			if (this.elContainer && this.elContainer._zcRefCount == 1)
				Dom.removeClass(this.elContainer, sOldClass);
			if (this._elInput)
				Dom.removeClass(this._elInput, sOldClass);
		}
	},

	/**
	 * Validates the widget. Calls SetValid.
	 * @return {Boolean} true if the widget is valid
	 */
	Validate: function()
	{
		for (var sName in this.aDef.Validation)
		{
			if (L.hasOwnProperty(this.aDef.Validation, sName))
			{
				var oValidator = ZC.JSManager.GetValidator(sName),
					Value = this.GetValue(), sValidationMessage;

				if (oValidator)
				{
					sValidationMessage = this.aDef.Validation[sName] || oValidator.GetDefaultValidationMsg();
				}

				// strings are trimmed when Manager receives them on the server, so validate the trimmed value here
				if (L.isString(Value))
				{
					Value = U.Trim(Value);
				}

				if (!L.isUndefined(oValidator) && !oValidator.Validate(Value, this, sValidationMessage))
				{
					this.SetValid(false, sValidationMessage);
					this._EventDispatcher('ValidationError', { Widget: this, Validator: sName });
					if (this.oForm && this.oForm._EventDispatcher)
					{
						this.oForm._EventDispatcher('ValidationError', { Widget: this, Validator: sName });
					}
					return false;
				}
			}
		}

		try {
			var bValid = true;
			U.ForEach(this.aChildWidgets, function(oWidget) { 
				if (!oWidget.GetAttribDefault('SkipValidation'))
				{
					bValid = oWidget.Validate() && bValid; 
				}
			});

			this.SetValid(bValid);
			return bValid;
		}
		catch (e)
		{
			YAHOO.log("Error during validation: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); 
		}
	},

	/**
	 * Returns the validation status of this widget. It will run the validators iff they haven't been run already since the widget was last changed.
	 * @return {Boolean} true if the widget is valid
	 */
	IsValid: function()
	{
		if (L.isUndefined(this._bIsValid))
			return this.Validate();

		return this._bIsValid;
	},

	/**
	 * Checks to see if this widget has been changed by comparing its current value to the stored original value.
	 * @return {Boolean} true if the field has been changed
	 */
	HasBeenChanged: function()
	{
		return !U.ObjectsEqual(this.GetValue(), this.oForm.oOriginalValues[this.sName]);
	},

	/**
	 * Sets the widget's validation status, with optional message.
	 * @param {Boolean} bIsValid validation status of the widget
	 * @param {String} sValMsg optional validation message
	 */
	SetValid: function(bIsValid, sValMsg)
	{
		var sOldClass, sNewClass, elValMsg;

		this._bIsValid = bIsValid;
		this.sValidationMessage = sValMsg;

		if (this.GetAttribDefault('ShowValidationStatus', true))
		{
			if (this.elContainer)
			{
				var sOldClass = bIsValid ? 'invalid' : 'valid';
				var sNewClass = bIsValid ? 'valid' : 'invalid';
				Dom.replaceClass(this.elContainer, sOldClass, sNewClass);
			}

			if (this.elValidationContainer)
			{
				if (!bIsValid && sValMsg)
				{
					elValMsg = document.createTextNode(sValMsg);

					if (this.elValidationContainer.firstChild)
						this.elValidationContainer.replaceChild(elValMsg, this.elValidationContainer.firstChild);
					else
						this.elValidationContainer.appendChild(elValMsg);

					Dom.removeClass(this.elValidationContainer, 'hide');
				}
				else
				{
					Dom.addClass(this.elValidationContainer, 'hide');
				}
			}
		}
	},

	/**
	 * Returns the validation message
	 * @return {String} the validation message set on the widget
	 */
	GetValidationMsg: function()
	{
		return this.sValidationMessage;
	},


	/**
	 * Returns the element(s) to add events to.
	 * @return {Object/Array} either an element, or array of elements.
	 */
	GetEventElement: function()
	{
		return this._elInput;
	},

	/**
	 * Adds a child widget from a definition
	 * @param {String} sName the widget name
	 * @param {Array} aDef the widget definition
	 */
	AddChildWidget: function(sName, aDef)
	{
		this.aChildWidgets[sName] = ZC.Core.Widget.NewFromDef(sName, aDef, this.oForm, this);
	},

	/**
	 * Removes a child widget
	 * @param {String} sName the widget name
	 */
	RemoveChildWidget: function(sName)
	{
		this.aChildWidgets[sName].Destruct();
		delete(this.aChildWidgets[sName]);
	},

	/**
	 * Check for child widgets
	 * @returns true if this widget has children
	 */
	HasChildWidgets: function()
	{
		for (var sName in this.aChildWidgets)
		{
			if (L.hasOwnProperty(this.aChildWidgets, sName))
				return true;
		}
		return false;
	},

	/**
	 * Returns the block which contains this widget
	 * @returns {Object} the block that contains this widget, or <code>undefined</code> if not found.
	 */
	FindContainingBlock: function()
	{
		if (this.oParent instanceof ZC.Core.Block)
		{
			return this.oParent;
		}
		else if (!L.isUndefined(this.oForm))
		{
			return this.oForm.FindContainingBlock();
		}
		else if (!L.isUndefined(this.oParent))
		{
			return this.oParent.FindContainingBlock();
		}
		else
		{
			return undefined;
		}
	},

	/**
	 * Check for a validator
	 * @param {String} sName validator name
	 * @returns true if the named validator exists on this widget
	 */
	HasValidator: function(sName)
	{
		return !L.isUndefined((this.GetAttribDefault('Validation', {}))[sName]);
	},

	/** 
	 * @private 
	 */
	_aSearchObjects: [ 'aChildWidgets' ]
}
L.augment(ZC.Core.Widget, AttribProvider);
L.augment(ZC.Core.Widget, GetWidgetProvider);
L.augment(ZC.Core.Widget, EventProvider);

/**
 * Represents a Form, which is just a specialised widget.
 *
 * @class Form
 * @constructor
 * @namespace ZC.Core
 * @extends ZC.Core.Widget
 * @param {String} sName form name
 * @param {Array} aDef an associative array containing the form definition
 * @param {Object} oParent the parent of this form, if available
 */
ZC.Core.Form = function(sName, aDef, oParent)
{
	var fnSubmitHandler, aHiddenIndices;

	if (L.isUndefined(aDef.ShowValidationStatus))
		aDef.ShowValidationStatus = false;

	ZC.Core.Form.superclass.constructor.call(this, sName, aDef, this, oParent);
	
	fnSubmitHandler = function(oEvent)
	{
		var oSelectedEndWidget = this.GetSelectedEndWidget();

		if (this.GetAttribDefault('ValidateOnSubmit') && (!oSelectedEndWidget || !oSelectedEndWidget.GetAttribDefault('SkipValidation')) && !this.IsValid())
		{
			this.SetSelectedEndWidget(undefined);
			Evt.stopEvent(oEvent);


			ZC.JSManager.Alert(_GT("There are still some errors on the form. Please check and re-submit."));
			return false;
		}

		if (this.GetAttribDefault('SubmitWithAJAX'))
		{
			// prevent the default action, but don't prevent other submit handlers from running..
			Evt.preventDefault(oEvent);

			// delay the AJAX submission until the JS engine is idle (i.e. has finished processing submit handlers)
			// required for some submit handlers added by YUI
			L.later(0, this, 'AjaxSubmit');
			this.SetAttrib('SubmitWithAJAX', false); // if for some reason this handler is called again, don't try AJAX a second time.
		}

		return true;
	}

	this.AddEvent(fnSubmitHandler, 'submit', this, 500);

	this.oOriginalValues = this.GetAllFieldValues();

	// find any hidden fields and store them
	this.aHiddenFields = {};
	aHiddenIndices = {};

	Dom.getElementsBy(function(el) { return (el.type == 'hidden'); }, 'input', this._elInput, function(el) 
	{
		var aMatches = el.name.match(/^(.*)\[(.*)\]$/);
		if (aMatches)
		{
			if (L.isUndefined(this.aHiddenFields[aMatches[0]]))
				this.aHiddenFields[aMatches[0]] = {};

			// the server may generate hidden arrays without explicit indices
			if (aMatches[1] == '')
			{
				if (L.isUndefined(aHiddenIndices[aMatches[0]]))
					aHiddenIndices[aMatches[0]] = 0;

				aMatches[1] = aHiddenIndices[aMatches[0]]++;
			}
			this.aHiddenFields[aMatches[0]][aMatches[1]] = el;
		}
		else
		{
			this.aHiddenFields[el.name] = el;
		}
	}, this, true);
}
L.extend(ZC.Core.Form, ZC.Core.Widget);

ZC.Core.Form.prototype.Destruct = function()
{
	this.CloseAjaxDialog();
	this.oOriginalValues = null;

	U.ForEach(U.Keys(this.aHiddenFields), function(k) { this.aHiddenFields[k] = null; }, this);
	this.aHiddenFields = null;	

	ZC.Core.Form.superclass.Destruct.apply(this, arguments);
}

ZC.Core.Form.prototype.CloseAjaxDialog = function()
{
	if (this._elInput && this._elInput._ajaxDialog)
	{
		ZC.JSManager.DestroyDialog(this._elInput._ajaxDialog);
		this._elInput._ajaxDialog = null;
	}
}

ZC.Core.Form.prototype._WidgetNameToID = function()
{
	return this.aDef.ID || this.sName;
}
/**
 * Enables/disables all of the widgets on the form
 * @param {Boolean} bEnable if false, then this method disables the widget
 * @param {String} sEnableClass if set, this class is added to an enabled widget, and removed from a disabled one
 * @param {String} sDisableClass if set, this class is removed from an enabled widget, and added to a disabled one
 */
ZC.Core.Form.prototype.Enable = function ()
{
	var aArgs = arguments;
	U.ForEach(this.aChildWidgets, function(oWidget) { oWidget.Enable.apply(oWidget, aArgs); })
}

/**
 * Get the value of all widgets on this form as an object
 * @return {Object} all the widget values
 */
ZC.Core.Form.prototype.GetAllFieldValues = function()
{
	var oValues = {};
	U.ForEach(this.aChildWidgets, function (oWidget) { oValues[oWidget.sName] = oWidget.GetValue(); });

	return oValues;
}

/**
 * Set the value of all widgets on the form from an object
 * @param {Object} oValues object of widget values
 * @param [{Boolean}] bOriginal if true, stores the new values as the form's original values
 */
ZC.Core.Form.prototype.PopulateFromArray = function(oValues, bOriginal)
{
	U.ForEach(oValues, function(Value, sFieldName)
	{
		this.SetFieldValue(sFieldName, Value);
	}, this);

	if (bOriginal)
		this.oOriginalValues = this.GetAllFieldValues();
}

/**
 * Sets the value of a widget or hidden field
 * @param {String} sFieldName field name
 * @param {Object} Value the field value
 */
ZC.Core.Form.prototype.SetFieldValue = function(sFieldName, Value)
{
	if (!L.isUndefined(this.aChildWidgets[sFieldName]))
	{
		this.aChildWidgets[sFieldName].SetValue(Value);
	}
	else if (!L.isUndefined(this.aHiddenFields[sFieldName]))
	{
		this.AddHiddenField(sFieldName, Value);
	}
}

/**
 * Clears all the widgets on the form
 */
ZC.Core.Form.prototype.Clear = function ()
{
	var aArgs = arguments;
	U.ForEach(this.aChildWidgets, function(oWidget) { oWidget.Clear.apply(oWidget, aArgs); })
}

/**
 * Resets form widgets back to their original values
 */
ZC.Core.Form.prototype.Reset = function()
{
	this.PopulateFromArray(this.oOriginalValues);
}

/**
 * Checks to see if the form has been changed since the page load, or since the last call to PopulateFromArray with bOriginal=true
 * @return {Boolean} true if the form has been changed
 */
ZC.Core.Form.prototype.HasBeenChanged = function()
{
	return !U.ObjectsEqual(this.oOriginalValues, this.GetAllFieldValues());
}

// Forms always check child widgets
ZC.Core.Form.prototype.IsValid = function()
{
	return this.Validate();
}

ZC.Core.Form.prototype.Validate = function()
{
	try {
		var aValidation = this.GetAttribDefault('FormValidation', {}),
			bValid = true, sValidationMessage;

		U.ForEach(aValidation, function(oFormValParams, sName)
		{
			var oValidator = ZC.JSManager.GetFormValidator(sName);
			if (!L.isUndefined(oValidator) && !oValidator.Validate(oFormValParams, this))
			{
				bValid = false;
				sValidationMessage = this.GetAttribDefault('FormValidationMsg', oValidator.GetDefaultValidationMsg());
			}
		}, this);

		U.ForEach(this.aChildWidgets, function(oWidget) { 
			if (!oWidget.GetAttribDefault('__IsField', true)) // only validate widgets that are also fields
				return;

			if (oWidget.AttribIsset('ValidationField') && oWidget.AttribIsset('ValidationValue'))
			{
				var oValidationWidget = this.GetWidget(oWidget.GetAttrib('ValidationField')),
					mValidationValue = oWidget.GetAttrib('ValidationValue'), mValidationFieldValue;

				if (oValidationWidget)
				{
					mValidationFieldValue = oValidationWidget.GetValue();
				}
				else
				{
					YAHOO.log('Unable to find ValidationField ' + oWidget.GetAttrib('ValidationField') + ' for widget ' + oWidget.sName, 'warn');
					return; // continue to next widget, don't try to validate this one.
				}

				if (L.isArray(mValidationFieldValue))
				{
					if (mValidationFieldValue.length == 1)
					{
						mValidationFieldValue = mValidationFieldValue.pop();	// OK we'll allow a single value in an array
					}
					else
					{
						YAHOO.log('Form::Validate(): ' + oWidget.sName + ' uses ' + oValidationWidget.sName + ' as a ValidationField but the latter returns an array of multiple values - the logic is undefined for this.', 'error');
						return;
					}
				}

				if ((L.isArray(mValidationValue) && !U.InArray(mValidationFieldValue, mValidationValue))
					|| (!L.isArray(mValidationValue) && mValidationValue != mValidationFieldValue))
				{
					return;
				}
			}
				
			bValid = oWidget.Validate() && bValid; 
		}, this);

		this.SetValid(bValid, sValidationMessage);
		return bValid;
	}
	catch (e)
	{
		YAHOO.log("Error during validation: " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", "warn", this.sName); 
		return false;
	}
}

/**
 * Submits this form via AJAX. Set the attrib SubmitWithAJAX when creating the
 * form to have this done automatically when the submit button is clicked.
 */
ZC.Core.Form.prototype.AjaxSubmit = function()
{
	if (!this.oParent || !this.oParent.AjaxRequest)
		return;
		
	var oAjaxRequestOptions, 
		fnAjaxReset = function()
	{
		this.SetAttrib('SubmitWithAJAX', true);
	};

	oAjaxRequestOptions = L.merge({ Form: this, OnComplete:fnAjaxReset, Scope:this }, this.GetAttribDefault('ExtraAjaxRequestOptions', {}));

	this.oParent.AjaxRequest(oAjaxRequestOptions);
	if (!this.GetAttribDefault('KeepDialogOnSubmit'))
		this.CloseAjaxDialog();
}


/**
 * Adds a hidden field to the form.
 * @param {String} sName the name of the hidden field
 * @param {String/Array/Object} mValue the value to set the field to
 * @return {HTMLElement} the created element(s)
 */
ZC.Core.Form.prototype.AddHiddenField = function(sName, mValue)
{
	var aResult = {},
		fnAddHidden = function(sName, sValue)
		{
			var elHidden;

			elHidden = document.createElement('input');
			elHidden.type = 'hidden';
			elHidden.name = sName;
			this.appendChild(elHidden);
			elHidden.value = sValue;
			return elHidden;
		};

	// if the hidden field already exists, remove it then re-add
	// saves confusion if the value goes from a scalar to an array, or vice-versa.
	if (!L.isUndefined(this.aHiddenFields[sName]))
	{
		if ('parentNode' in this.aHiddenFields[sName])
		{
			this.aHiddenFields[sName].parentNode.removeChild(this.aHiddenFields[sName]);
		}
		else
		{
			U.ForEach(this.aHiddenFields[sName], function(el) { el.parentNode.removeChild(el); });
		}
	}

	if (L.isArray(mValue) || L.isObject(mValue))
	{
		U.ForEach(mValue, function(sVal, sKey) 
		{
			aResult[sKey] = fnAddHidden.call(this, sName + '[' + sKey + ']', sVal);
		}, this._elInput);
		this.aHiddenFields[sName] = aResult;
	}
	else
	{
		this.aHiddenFields[sName] = fnAddHidden.call(this._elInput, sName, String(mValue));
	}

	return this.aHiddenFields[sName];
}

/**
 * Checks to see if the form has a hidden field with the given name.
 * @param {String} sFieldName the field name to check for
 * @return {Boolean} true if the field exists
 */
ZC.Core.Form.prototype.HasHiddenField = function(sFieldName)
{
	return (sFieldName in this.aHiddenFields);
}

/**
 * Gets the value of a hidden field
 * @param {String} sFieldName the field name to check for
 * @return {String/Array} the field value
 */
ZC.Core.Form.prototype.HiddenFieldValue = function(sFieldName)
{
	var mHiddenField = this.aHiddenFields[sFieldName], aResult = {};

	if ('value' in mHiddenField && !L.isObject(mHiddenField.value))		// check it's not an array of hidden fields with "value" as a key
	{
		return mHiddenField.value;
	}
	else
	{
		U.ForEach(mHiddenField, function (el, sKey) { aResult[sKey] = el.value; });
		return aResult;
	}
}

/**
 * Sets the selected end widget for this form.
 *
 * It's not actually that easy to find out which button submitted a form from
 * an "on submit" handler, so we need to keep track of it ourselves using click
 * events on the widgets. Any widget which submits the form should attach an
 * event that calls this method before the submit handlers run.
 *
 * @param {ZC.Core.Widget} oSelectedEndWidget the widget that submitted the form
 */
ZC.Core.Form.prototype.SetSelectedEndWidget = function(oSelectedEndWidget)
{
	this.oSelectedEndWidget = oSelectedEndWidget;
}

/**
 * Gets the selected end widget for this form.
 *
 * @return {ZC.Core.Widget} the most recent widget that tried to submit the form.
 */
ZC.Core.Form.prototype.GetSelectedEndWidget = function()
{
	return this.oSelectedEndWidget;
}

/**
 * Focusses the first visible, enabled input element on this form.
 */
ZC.Core.Form.prototype.FocusFirstElement = function()
{
	var fnFilter = function(el)
	{
		return el.tagName && U.InArray(el.tagName.toLowerCase(), ['input', 'select', 'textarea']) && el.type != 'hidden' && !el.disabled;
	}
	var aChildNodes = Dom.getElementsBy(fnFilter, false, this._elInput);

	if (aChildNodes.length == 0)
		return;

	if (U.Some(aChildNodes, function(elNode) { return (elNode.tabIndex && elNode.tabIndex > 0); }))
	{
		var elLowestTabIndex;
		U.ForEach(aChildNodes, function(elNode)
		{
			if (!elLowestTabIndex || elLowestTabIndex.tabIndex > elNode.tabIndex)
				elLowestTabIndex = elNode;
		});
		elLowestTabIndex.focus();
	}
	else
		aChildNodes[0].focus();
}

/**
 * Base class for all validators. Validators are singleton objects.
 * @class Validator
 * @constructor
 * @namespace ZC.Core
 */
ZC.Core.Validator = function()
{
	/* empty constructor */
}

/**
 * This creates a validator class and extends the base validator. Creates the required namespace if it doesn't already exist.
 * @param {String} sName the validator name
 * @param {String} sModule the module for this validator (defaults to Core)
 * @return {Object} the validator object
 */
ZC.Core.Validator.Create = function(sName, sModule)
{
	sModule = sModule || 'Core';
	var oNS = ZC.Namespace(sModule + '.Validator');

	oNS[sName] = function() {};
	L.extend(oNS[sName], ZC.Core.Validator);
	oNS[sName].sClassName = sModule + '_Validator_' + sName;

	return oNS[sName];
}

ZC.Core.Validator.prototype = {
	/**
	 * Returns the default validation message, used if the validator def has no custom message.
	 * @return {String} validation message
	 */
	GetDefaultValidationMsg: function()
	{
		return this.sDefaultValidationMessage || _GT("Invalid value");
	},

	/**
	 * Validates the given value.
	 * @param Value Widget value
	 * @param {Object} oWidget The widget we're validating.
	 */
	Validate: function(Value, oWidget)
	{
		if (L.isUndefined(this.oValidationRegex))
		{
			//YAHOO.log('oValidationRegex not defined, and Validate not overridden', "error", this.sClassName);
			return undefined;
		}

		if (Value instanceof Array)
		{
			for (var Key in Value)
			{
				if (L.hasOwnProperty(Value, Key) && !this.oValidationRegex.test(Value[Key]))
					return false;
			}

			return true;
		}
		else
			return this.oValidationRegex.test(Value);
	}
}

/**
 * Base class for event listeners. The constructor takes a (destination) widget
 * object and an event object, and subscribes to the event.
 *
 * @class EventListener
 * @constructor
 * @namespace ZC.Core
 * @uses ZC.AttribProvider
 * @param {Object} oDestination the destination object (can be a widget or a block)
 * @param {Object} oEvent the custom event to subscribe to
 * @param {Object} aDef the event listener definition
 */
ZC.Core.EventListener = function(oDestination, oEvent, aDef)
{
	/**
	 * The destination object (can be a widget or a block)
	 */
	this.oDestination = oDestination;

	/**
	 * This is the same as <code>oDestination</code>, for backwards compatibility.
	 * @deprecated
	 */
	this.oDestWidget = this.oDestination;

	/**
	 * The custom event to subscribe to
	 */
	this.oEvent = oEvent;
	/**
	 * The listener definition
	 */
	this.aDef = aDef;

	if (this.Setup())
	{
		oEvent.subscribe(this._EventHandler, this, this);
	}
	else
	{
		throw new Error('Setup returned false');
	}
	
	this.AttribProviderSetup();
}

/**
 * This static method creates an eventlistener class and extends the base
 * class. Creates the required namespace if it doesn't already exist.
 *
 * @static
 * @param {String} sName the eventlistener name
 * @param {String} sModule the module for this eventlistener (defaults to Core)
 * @param {String} oParentClass the widget to extend (defaults to the base widget class)
 * @return {Object} the eventlistener object
 */
ZC.Core.EventListener.Create = function(sName, sModule, oParentClass)
{
	sModule = sModule || 'Core';
	oParentClass = oParentClass || ZC.Core.EventListener;
	var oNS = ZC.Namespace(sModule + '.EventListener');

	oNS[sName] = function(oDestination, oEvent, aDef)
	{
		oNS[sName].superclass.constructor.call(this, oDestination, oEvent, aDef);
	}
	L.extend(oNS[sName], oParentClass);

	return oNS[sName];
}

ZC.Core.EventListener.prototype = {
	/**
	 * Optional setup method
	 * @return {Boolean} if false, then the EL will not subscribe to the event
	 */
	Setup: function()
	{
		return true;
	},

	/**
	 * Frees up all the references in this object. Called on page unload to avoid memory leaks in IE.
	 */
	Destruct: function()
	{
		YAHOO.log("destructing eventlistener for " + (this.oEvent ? this.oEvent.type : '(null event)') + " on " + (this.oDestination ? this.oDestination.sName : "(null destination)"), "debug", "EventListener::Destruct");

		if (this.oEvent)
			this.oEvent.unsubscribe(this._EventHandler, this);

		this.sDestName = this.oDestination.sName;
		this.oDestination = null;
		this.oDestWidget = null;
		this.oEvent = null;
	},

	/**
	 * translates the parameters we get from YUI
	 * @private
	 * @param {String} sEventName Custom event that fired
	 * @param {Array} aArgs arguments passed to fire, 0 = widget, 1 = browser source event
	 */
	_EventHandler: function(sEventName, aArgs)
	{
		var oEvent = ZC.JSManager.GetEvent(sEventName),
		    oWidget = aArgs[0],
		    oSrcEvent = aArgs[1];

		if (this.oDestination && this.oEvent)
		{
			this.HandleEvent(oWidget, oEvent, oSrcEvent);
		}
	},

	/**
	 * Tests the given value against a single or list of equals / not equals values.
	 * <ul>
	 * <li>if the EqualsAttrib is set, returns true if it contains the value,</li>
	 * <li>if the NotEqualsAttrib is set, returns true if it does not contain the value.</li>
	 * <li>if the ContainsAttrib is set, returns true if Value is an array/object and it contains a value from Contains,</li>
	 * <li>if the NotContainsAttrib is set, returns true if Value is an array/object and it does not contain a value from NotContains.</li>
	 * </ul>
	 *
	 * @protected
	 * @param {String|Array} Value the value to search for
	 * @param String sEqualsAttrib the attrib containing the "equals" list/value
	 * @param String sNotEqualsAttrib the attrib containing the not-"equals" list or value
	 * @param String sContainsAttrib the attrib containing the "contains" list or value
	 * @param String sNotContainsAttrib the attrib containing the not-"contains" list or value
	 * @return {Boolean} true if the above conditions match, otherwise false
	 */
	_SearchLists: function(Value, sEqualsAttrib, sNotEqualsAttrib, sContainsAttrib, sNotContainsAttrib)
	{
		var aVal, i, iMax, j, jMax, bResult = false, bFoundList = false, aContains, aNotContains,
			fnCompare = function(Value, CompareValues, bReturnIfMatch)
			{
				if (L.isArray(CompareValues))
				{
					// not using InArray as we want non-strict equality checking
					for (var mVal in CompareValues)
					{
						if (L.hasOwnProperty(CompareValues, mVal) && U.ObjectsEqual(Value, CompareValues[mVal]))
							return bReturnIfMatch;
					}
					return !bReturnIfMatch;
				}
				else
					return U.ObjectsEqual(Value, CompareValues) ? bReturnIfMatch : !bReturnIfMatch;
			};

		if (this.AttribIsset(sEqualsAttrib))
		{
			bFoundList = true;
			bResult = fnCompare(Value, this.GetAttrib(sEqualsAttrib), true);
		}

		if (this.AttribIsset(sNotEqualsAttrib))
		{
			bFoundList = true;
			bResult = bResult || fnCompare(Value, this.GetAttrib(sNotEqualsAttrib), false);
		}

		if ((this.AttribIsset(sContainsAttrib) || this.AttribIsset(sNotContainsAttrib)) && (L.isArray(Value) || L.isObject(Value)))
		{
			bFoundList = true;
			aContains = this.GetAttribDefault(sContainsAttrib, []);
			aNotContains = this.GetAttribDefault(sNotContainsAttrib, []);

			if (!L.isArray(aContains))
				aContains = [aContains];
			if (!L.isArray(aNotContains))
				aNotContains = [aNotContains];

			if (L.isObject(Value))
			{
				aVal = [];
				U.ForEach(Value, function(o) { aVal.push(o); });
			}
			else
			{
				aVal = Value;
			}

			for (i = 0, iMax = aVal.length; i < iMax; ++i)
			{
				for (j = 0, jMax = aContains.length; j < jMax; ++j)
				{
					if (U.ObjectsEqual(aVal[i], aContains[j]))
					{
						bResult = true;
						break;
					}
				}

				for (j = 0, jMax = aNotContains.length; j < jMax; ++j)
				{
					if (U.ObjectsEqual(aVal[i], aNotContains[j]))
					{
						bResult = false;
						break;
					}
				}
			}
		}

		return (!bFoundList || bResult);
	},

	/**
	 * Gets the value of the given widget. By default, it calls GetValue, but if the attrib GetValueMethod is set it calls that method instead.
	 * @protected
	 * @param {Object} oWidget the widget to get the value from
	 * @return the widget value
	 */
	_GetWidgetValue: function(oWidget)
	{
		return (oWidget[this.GetAttribDefault('GetValueMethod', 'GetValue')])();
	},

	/**
	 * Event handler, called when the event fires.
	 * @param {Object} oWidget the widget that the event fired on
	 * @param {Object} oEvent the custom event that fired
	 * @param {Object} oSrcEvent the browser event that caused this event to fire (if available)
	 */
	HandleEvent: function(oWidget, oEvent, oSrcEvent)
	{
		throw new Error('EventListener::HandleEvent is abstract and must be overridden');
	}
}
L.augment(ZC.Core.EventListener, AttribProvider);

/**
 * Base class for CSOs. The constructor takes a def and sets it on the object.
 * 
 * @class ClientSideObject
 * @constructor
 * @namespace ZC.Core
 * @uses ZC.AttribProvider
 * @param {Object} aDef the CSO definition
 */
ZC.Core.ClientSideObject = function(aDef)
{
	/**
	 * The CSO definition
	 */
	this.aDef = aDef;

	this.Setup();
	
	this.AttribProviderSetup();
}

/**
 * This static method creates an eventlistener class and extends the base
 * class. Creates the required namespace if it doesn't already exist.
 *
 * @static
 * @param {String} sName the eventlistener name
 * @param {String} sModule the module for this eventlistener (defaults to Core)
 * @return {Object} the eventlistener object
 */
ZC.Core.ClientSideObject.Create = function(sName, sModule)
{
	sModule = sModule || 'Core';
	var oNS = ZC.Namespace(sModule + '.ClientSideObject');

	oNS[sName] = function(aDef)
	{
		oNS[sName].superclass.constructor.call(this, aDef);
	}
	L.extend(oNS[sName], ZC.Core.ClientSideObject);

	return oNS[sName];
}

ZC.Core.ClientSideObject.prototype = {
	/**
	 * Setup method
	 */
	Setup: function()
	{
	},

	/**
	 * Destructor
	 */
	Destruct: function()
	{
	}
};
L.augment(ZC.Core.ClientSideObject, AttribProvider);

// end the private scoping
})();
;function showelementsofclass(type,classname, rootid)
{
	root = document.getElementById(rootid);
	if (rootid  && root)
	{
		classitems = YAHOO.util.Dom.getElementsByClassName(classname, type, root);
	}
	else
	{
		classitems = YAHOO.util.Dom.getElementsByClassName(classname, type);
	}

	for(i=0;i<classitems.length;i++)
	{
		if (type.toLowerCase() =='tr' && navigator.appName!='Microsoft Internet Explorer')
		{
			classitems[i].style.display='table-row';
		}
		else if (type.toLowerCase() == 'span')
		{
			classitems[i].style.display='inline';
		}
		else
		{
			classitems[i].style.display='block';
		}
	}
}

function hideelementsofclass(type,classname, rootid)
{
	root = document.getElementById(rootid);
	if (rootid  && root)
	{
		classitems = YAHOO.util.Dom.getElementsByClassName(classname, type, root);
	}
	else
	{
		classitems = YAHOO.util.Dom.getElementsByClassName(classname, type);
	}

	for(i=0;i<classitems.length;i++)
	{
			classitems[i].style.display='none';
	}
}

function showelementwithid(id)
{
	element = document.getElementById(id);
	if(element == null)
	{
		return;
	}
	type = element.tagName;
	if (type.toLowerCase() =='tr' && navigator.appName!='Microsoft Internet Explorer')
		{
			element.style.display='table-row';
		}
		else
		{
			element.style.display='block';
		}
}

function hideelementwithid(id)
{
	element = document.getElementById(id);
	if (element != undefined)
		element.style.display='none';
}


function toggleelementwithid(id)
{
	element = document.getElementById(id);
	if(element != undefined)
	{
		if(element.style.display=='none')
		{
			showelementwithid(id);
		}
		else
		{
			hideelementwithid(id);
		}
	}
}



function toggledivbyclass(name)
{
	classitems=YAHOO.util.Dom.getElementsByClassName(name, 'div');
	for(i=0;i<classitems.length;i++)
	{
		if(classitems[i].style.display!='none')
		{
			classitems[i].style.display='none';
		}
		else
		{
			classitems[i].style.display='block';
		}
	}
}
var timeout=Array;
function displaydiv(name)
{
	document.getElementById(name).style.display='block';
	window.clearTimeout(timeout[name]);
	timeout[name]=window.setTimeout("hidediv('"+name+"');",10000);
}


function displaydivinline(name)
{
	document.getElementById(name).style.display='inline';
	window.clearTimeout(timeout[name]);
	timeout[name]=window.setTimeout("hidediv('"+name+"');",10000);
}

function displayspan(name)
{
	document.getElementById(name).style.display='inline';
	window.clearTimeout(timeout[name]);
	timeout[name]=window.setTimeout("hidediv('"+name+"');",10000);
}
function hidediv(name)
{
	window.clearTimeout(timeout[name]);
	timeout[name]=window.setTimeout("document.getElementById('"+name+"').style.display='none'",100);
}
// Like displaydiv but without the timeout
function showdiv(id)
{
	document.getElementById(id).style.display='block';
}
function togglediv(name)
{
	if(document.getElementById(name).style.display!='none')
	{
		document.getElementById(name).style.display='none';
	}
	else
	{
		document.getElementById(name).style.display='block';
	}
}

function togglelabel(name,label1,label2)
{
	if (document.getElementById(name).innerHTML == label1)
		document.getElementById(name).innerHTML = label2;
	else
		document.getElementById(name).innerHTML = label1;
}

function toggledivandlabel(divname,labelname,hidelabel,showlabel)
{
	if(document.getElementById(divname).style.display!='none')
	{
		document.getElementById(divname).style.display='none';
		document.getElementById(labelname).innerHTML = showlabel;
	}
	else
	{
		document.getElementById(divname).style.display='block';
		document.getElementById(labelname).innerHTML = hidelabel;
	}
}


function nse(to,domain)
{
	document.location='mailto:'+to+'@'+domain;
}

function highlight(n) {
        var div = document.getElementById(n);
        if(!div)
                return;
		div.style.background="rgb(230,240,255)";
}
function nohighlight(n) {
        var div = document.getElementById(n);
        if(!div)
                return;
		div.style.background="transparent";
}

function MM_swapImgRestore() { //v3.0
  var i,x,a=document.MM_sr; for(i=0;a&&i<a.length&&(x=a[i])&&x.oSrc;i++) x.src=x.oSrc;
}

function MM_preloadImages() { //v3.0
  var d=document; if(d.images){ if(!d.MM_p) d.MM_p=new Array();
    var i,j=d.MM_p.length,a=MM_preloadImages.arguments; for(i=0; i<a.length; i++)
    if (a[i].indexOf("#")!=0){ d.MM_p[j]=new Image; d.MM_p[j++].src=a[i];}}
}

function MM_findObj(n, d) { //v4.0
  var p,i,x;  if(!d) d=document; if((p=n.indexOf("?"))>0&&parent.frames.length) {
    d=parent.frames[n.substring(p+1)].document; n=n.substring(0,p);}
  if(!(x=d[n])&&d.all) x=d.all[n]; for (i=0;!x&&i<d.forms.length;i++) x=d.forms[i][n];
  for(i=0;!x&&d.layers&&i<d.layers.length;i++) x=MM_findObj(n,d.layers[i].document);
  if(!x && document.getElementById) x=document.getElementById(n); return x;
}

function MM_swapImage() { //v3.0
  var i,j=0,x,a=MM_swapImage.arguments; document.MM_sr=new Array; for(i=0;i<(a.length-2);i+=3)
   if ((x=MM_findObj(a[i]))!=null){document.MM_sr[j++]=x; if(!x.oSrc) x.oSrc=x.src; x.src=a[i+2];}
}
function ConfirmBlockDelete()
{
return confirm("This will delete the block AND ALL CHILD BLOCKS it may contain.");
}
function ConfirmFolderDelete()
{
return confirm("=== CAUTION ===\n\nThis will delete the folder and ALL PAGES AND SUBFOLDERS\nit may contain.");
}
function ConfirmPageDelete()
{
return confirm("This will delete the page and any contents.");
}
function ConfirmDelete()
{
return confirm("Are you sure you want to delete?");
}
function NotNull(name)
{
	if(document.getElementById(name).value=='')
		alert('You must put a reason for deleting the items.');
	return document.getElementById(name).value!='';
}

/*does all processing on genenric amounts of linked selects!!!
<script type="text/javascript">
<!--
selects=new Array();
selects[0]=document.forms['FrmAdminControl'].elements['test.0'];
selects[1]=document.forms['FrmAdminControl'].elements['test.1'];
selects[2]=document.forms['FrmAdminControl'].elements['test.2'];

initLinkedSelect(selects);
-->

requires following names on options so we know what goes where

<select name"=test.0">
<option value="dog">Dog</option>
<option value="cat">Cat</option>
</select>
<select name="test.1">
<option value="dog-fido">Fido</option>
<option value="dog-rover">Rover</option>
<option value="cat-boots">Boots</option>
<option value="cat-pussy">Pussy</option>
</select>
<select> name="test.2">
<option value="dog-fido-dead">Dead</options>
...
...
</select>
*/



// using a Select create divs with id equal to a options value and watch the div appear!
//set onchange="SelectDivFromDropDown(this)
function SelectDivFromDropDown(select)
{
	for(i=0;i<select.options.length;i++)
	{
		if(select.options[i].value && document.getElementById(select.options[i].value))
			document.getElementById(select.options[i].value).style.display='none';
	}
	if(document.getElementById(select.value))
		document.getElementById(select.value).style.display='block';
}

function LoadIframeFromForm(sURL,sFormName,sDivName,sExtraGetVars)
{
	//Get the URL
	var sURL='';
	sURL=SendAsGetToURL(sURL,sFormName)+sExtraGetVars;
	var ifrmdoc = getIframeDocument('frame.'+sFormName);

	if(document.getElementById(sDivName).style.display!='none')
	{
		document.getElementById(sDivName).style.display='none';
	}
	else
	{
		document.getElementById(sDivName).style.display='block';
		//Redirect the iframe.
		ifrmdoc.body.innerHTML='<p>Loading</p>';
		ifrmdoc.location.replace(sURL);
	}
}

function DocumentLocationFromFormField(sURLPrefix,sFormName,sFieldName,sExtraGetOptions)
{
	sURL = eval('document.'+sFormName+'.'+sFieldName+'.value');
	if (sURL=='')
	{
		return false;
	}

	if (sURL[0] == '#')
	{
		document.location=sURL;
		return false;
	}

	sURL=sURLPrefix+sURL;
	// Same as current page (possible different anchor)?
	if (document.location.href.replace(/[#|\?].*$/,'') == sURL.replace(/[#|\?].*$/,''))
	{
		document.location='#'+sURL.replace(/^.*#/,'');
		return false;
	}
	if (sExtraGetOptions)
		sURL += '?'+sExtraGetOptions;
	document.location = sURL;
	return false;
}

function OpenWindowFromForm(sURL,sFormName,sExtraGetOptions)
{
	sURL=SendAsGetToURL(sURL,sFormName)+sExtraGetOptions;
	window.open(sURL,"","width=200,height=200,location=yes,scrollbars");
}

function SendAsGetToURL(sURL,sFormName)
{
	var sGetString='';
	if(FormCollection[sFormName])
	{
		for (sFieldName	in FormCollection[sFormName])
		{
			if(eval('document.'+sFormName+'.'+sFieldName+'.type')=='checkbox')
			{

				if (eval('document.'+sFormName+'.'+sFieldName+'.checked'))
					sGetString+=sFieldName+'='+eval('document.'+sFormName+'.'+sFieldName+'.checked')+'&';
			}
			else
			{
				sGetString+=sFieldName+'='+eval('document.'+sFormName+'.'+sFieldName+'.value')+'&';
			}

		}
	}
	return sURL+'?'+sGetString;

}
function getIframeDocument(id) {
	if(document.frames)
		return document.frames[id].document;
	var ifrm = getid(id);
	if(ifrm.contentDocument)
		return ifrm.contentDocument;
	else if(ifrm.contentWindow)
		return ifrm.contentWindow.document;
	else if(ifrm.document)
		return ifrm.document;
}
function getid(id) {
	if(document.getElementById)
		return document.getElementById(id);
	else if(document.all)
		return document.all(id);
	return false;
}

function PleaseWait(button)
{
	button.value='PLEASE WAIT';
	button.style.color='red';
	button.onclick= function() {
		alert('Please Wait');
		return false;
	}
	return false;
}

function SendFormValue(ToURL,FormElement,GetVarName)
{
	document.location.replace(ToURL+'?'+GetVarName+'='+FormElement.value);
}
function setIDHTMLToVal(fieldElement,someID)
{
	span=getElementById(someID);
	if(span)
	{
		span.innerHTML=this.value;
	}
}
function docnoperm(s)
{
	alert(s);
	return false;
}

function PutFormHere(sFormName,oParent)
{
	var oForm;
	oForm=document.getElementById(sFormName);
	if(oForm)
	{
		oParent.appendChild(oForm);
	}
}
function SetHiddenValue(sFormName,sFieldName,sValue)
{
	var oForm;
	oForm=document.getElementById(sFormName);
	if(oForm && oForm.elements[sFieldName])
	{
		oForm.elements[sFieldName].value=sValue;
	}
	else
	{
		alert('could not find element '+sFieldName);
	}
}
function MoveCommentFormHere(sElementName,sValue)
{
	oElement=document.getElementById(sElementName);
	SetHiddenValue('FrmComment','parentcommentid',sValue);
	PutFormHere('FrmComment',oElement);
	return false;
}
function ConfirmRecursiveCommentDelete()
{
	return confirm("Are you sure you want to delete this comment\nincluding all those comments made upon it?");
}
function ConfirmArticleDelete()
{
	return confirm("Are you sure you want to delete this item\nincluding all those comments made upon it?");

}

function SetupCommentReply(fromdiv_id, commentid)
{
	fromdiv = document.getElementById(fromdiv_id); 	// fromdiv: the div of the comment
	todiv = document.getElementById('contextcomment'); 	// todiv: an empty dive just above the reply form
	jumptothread_div = document.getElementById('jumptothread'); // jumptothread_div: a second empty div just above the reply form
	title = document.getElementById('formtitle'); // reply form title
	link = document.getElementById('comment_'+commentid);
	link.href='javascript:void(0);';

	// Copy the comment to just above the reply box
	todiv.innerHTML = fromdiv.innerHTML;
	todiv.style.display = 'block';

	// Add a link to jump back to the thread of the comment from the reply
	jumptothread_div.innerHTML = '<a class="link" href="#anchor_comment_' + commentid + '">Jump to thread</a>';
	jumptothread_div.style.display = 'block';

	// Set the title of the reply box
	title.innerHTML = '<strong>Reply to comment</strong>';
	// Set some form inputs so that the reply ends up in the right place in the tree when the form is submitted
	document.forms.FrmComment.parentcommentid.value = commentid;
	// jump to the reply
	ScrollToElement(todiv);
	return false;
}

function ScrollToElement(element)
{
 	var desty = element.offsetTop;
	var thisNode=element;
	 while (thisNode.offsetParent &&
       (thisNode.offsetParent != document.body)) {
	   thisNode = thisNode.offsetParent;
	   desty += thisNode.offsetTop;
	 }
	window.scrollTo(0,desty);
}

function ScrollToID(id)
{
	var id=document.getElementById(id);
	ScrollToElement(id);
}

function SetupNewComment()
{
		// Set the title of the reply box
	title = document.getElementById('formtitle');
	title.innerHTML = '<strong>Add a comment</strong>';
	// Set some form inputs so that the reply ends up in the right place in the tree when the form is submitted
	document.forms.FrmComment.parentcommentid.value = '';
	// Hide the reply divs
	todiv = document.getElementById('contextcomment');
	jumptothread_div = document.getElementById('jumptothread');
	todiv.innerHTML = '';
	jumptothread_div.innerHTML = '';
	return false;
}



function showDebugWindow() {
  window.top.debugWindow =
      window.open("",
                  "Debug",
                  "left=0,top=0,width=300,height=700,scrollbars=yes,"
                  +"status=yes,resizable=yes");
  window.top.debugWindow.opener = self;
  // open the document for writing
  window.top.debugWindow.document.open();
  window.top.debugWindow.document.write(
      "<HTML><HEAD><TITLE>Debug Window</TITLE></HEAD><BODY><PRE>\n");
}

function debugToWindow(text) {
  if (window.top.debugWindow && ! window.top.debugWindow.closed) {
    window.top.debugWindow.document.write(text+"\n");
  }
  else
  {
  	showDebugWindow();
  	 window.top.debugWindow.document.write(text+"\n");
  }
}

function _builddebugstr(obj)
{
	var temp = ""
	for (x in obj)
	{
		 if(typeof(x)=='object')
			temp += _builddebugstr(obj[x]);
		else
			temp += x + ": " + obj[x] + "\n";
	}
	return temp;
}


function debugObject(obj)
{
	if (obj == undefined)
		debugToWindow('undefined');
	else
	{
		var temp = "";
		temp = _builddebugstr(obj);
		debugToWindow(temp);
	}
}

function ClearForm(frm)
{
    for (n=0; n<frm.elements.length; n++)
    {
      switch (frm.elements[n].type)
      {
      	case 'hidden':
      	case 'button':
      	case 'submit':
      		break;
        case 'text':
        case 'textarea':
        case 'password':
        case 'fileupload':
        case 'radio':
        frm.elements[n].value = '';
        break;
        case 'checkbox':
        frm.elements[n].checked = false;
        break;
        case 'select-one':
        case 'select-multiple':

        // this test avoids the reseting of date fields.
        if (frm.elements[n].options[0].value == 'Any' || frm.elements[n].options[0].value == '__Any__' || frm.elements[n].options[0].value == '')
        	frm.elements[n].selectedIndex = 0;
        break;

        default :
        	// DS: I think it's better to silently fail on production sites...
//        	alert('Element of type ' + frm.elements[n].type);

      }
      // if any of the search elements have onchange javascript then run it
      if (frm.elements[n].onchange != undefined)
      {
      	frm.elements[n].onchange({target: frm.elements[n]}); // CB: dummy event object works around bug in checkbox group widget
      }
    }
  }


function SetInitialI18NWidget()
{
	HideNonPreferredI18NWidgets();
	ShowPreferredI18NWidget();
}

function HideNonPreferredI18NWidgets()
{
	for (sWidgetID in aI18NWidgets)
	{
		for(sLangID in aLangs)
		{
			oWidget=document.getElementById(aI18NWidgets[sWidgetID]+aLangs[sLangID]);
			if(oWidget)
			{
				if(aLangs[sLangID]!=sPreferredLang)
				{
					oWidget.style.display='none';
					oFlag=document.getElementById(aI18NWidgets[sWidgetID]+aLangs[sLangID]+'_img');
					oFlag.style.border = '0px solid black';
				}
			}
		}
	}
}

function ShowPreferredI18NWidget()
{
	for (sWidgetID in aI18NWidgets)
	{
		for(sLangID in aLangs)
		{
			oWidget=document.getElementById(aI18NWidgets[sWidgetID]+aLangs[sLangID]);
			if(oWidget)
			{
				if(aLangs[sLangID]==sPreferredLang)
				{
					oWidget.style.display='inline';

					oFlag=document.getElementById(aI18NWidgets[sWidgetID]+aLangs[sLangID]+'_img');
					oFlag.style.border = '1px solid black';
				}
			}
		}
	}
}


function SetPreferredI18NWidget(sLang)
{
	sPreferredLang=sLang;
}

function SwapI18NWidget(sLang)
{
	SetPreferredI18NWidget(sLang);
	HideNonPreferredI18NWidgets();
	ShowPreferredI18NWidget();
}

function ChangeI18NWidgetsFromCheckBox(sLang,oCheckBox)
{
	if(oCheckBox.checked)
	{
		for (sWidgetID in aI18NWidgets)
		{
			oWidget=document.getElementById(aI18NWidgets[sWidgetID]+sLang);
			oWidget.style.display='inline';
			oImage=document.getElementById(aI18NWidgets[sWidgetID]+sLang+'_img');
			oImage.style.display='inline';
		}
	}
	else
	{
		for (sWidgetID in aI18NWidgets)
		{
			oWidget=document.getElementById(aI18NWidgets[sWidgetID]+sLang);
			oWidget.style.display='none';
			oImage=document.getElementById(aI18NWidgets[sWidgetID]+sLang+'_img');
			oImage.style.display='none';
		}

	}
}

function PreventI18NFlagJS()
{
	var done='';
	for (sWidgetID in aI18NWidgets)
	{
		for(sLangID in aLangs)
		{
			oImage=document.getElementById(aI18NWidgets[sWidgetID]+aLangs[sLangID]+'_img');
			if(oImage)
			{
				oImage.onclick=function() {};
				if(aLangs[sLangID]!=sPreferredLang)
					oImage.style.display='none';
			}
		}
	}

}

function moveElementToElement(elementId,moveToElementId)
{
	var moveToElement = document.getElementById(moveToElementId);
	var moveToX = findPosX(moveToElement);
	var moveToY = findPosY(moveToElement);
	moveObject(elementId, moveToX, moveToY);
}


function moveObject(id,x,y)
{
	var styleObject = getStyleObject(id);
    if(styleObject)
    {
		styleObject.left = x;
		styleObject.top = y;
		return true;
    }
    else
    {
		return false;
    }
}
var xOffset = 30;
var yOffset = -5;


function showPopupDiv(targetID, event)
{
	// hide any other popups that are still open
	if (window.currentPopupID)
	{
		var CurrentPopupStyle = getStyleObject(currentPopupID);
		if (CurrentPopupStyle)
		{
			CurrentPopupStyle.visibility = 'hidden';
			window.currentPopupID = false;
		}
	}
	if(event)
	{

		var moveToX = (event.pageX)?event.pageX + xOffset:event.x + xOffset + ((document.body.scrollLeft)?document.body.scrollLeft:0);
		var moveToY = (event.pageY)?event.pageY + yOffset:event.y + yOffset + ((document.body.scrollTop)?document.body.scrollTop:0);
		moveObject(targetID, moveToX, moveToY);
		var styleObject = getStyleObject(targetID);
    	if(styleObject)
    	{
			styleObject.visibility = 'visible';
			window.currentPopupID = targetID;
    	}
	}
}

function hidePopupDiv(targetID)
{
	var styleObject = getStyleObject(targetID);
    	if(styleObject)
    	{
			styleObject.visibility = 'hidden';
			window.currentPopupID = false;
    	}
}

function hidePopupDivDelay(targetID)
{
	window.setTimeout("hidePopupDiv('"+targetID+"')",20);

}

function getStyleObject(objectId) {
    // cross-browser function to get an object's style object given its id
    if(document.getElementById && document.getElementById(objectId)) {
	// W3C DOM
	return document.getElementById(objectId).style;
    } else if (document.all && document.all(objectId)) {
	// MSIE 4 DOM
	return document.all(objectId).style;
    } else if (document.layers && document.layers[objectId]) {
	// NN 4 DOM.. note: this won't find nested layers
	return document.layers[objectId];
    } else {
	return false;
    }
} // getStyleObject



//Some basic AJAX stuff

function createRequestObject() {
    var ro;
    var browser = navigator.appName;
    if(browser == "Microsoft Internet Explorer"){
        ro = new ActiveXObject("Microsoft.XMLHTTP");
    }else{
        ro = new XMLHttpRequest();
    }
    return ro;
}

var http = createRequestObject();
var logIdToUpdate='';

function sndMenuReq(action,obj) {
 	sndReq(action,'div'+obj.id)
}

function sndReq(action,idToUpdate) {
    http.open('get', action);
    http.onreadystatechange = handleResponse;
    http.send(null);
	logIdToUpdate=idToUpdate;
	element=document.getElementById(logIdToUpdate);
	if(element && element.innerHTML=='')
	{
		element.innerHTML='<em>Loading...</em>';
	}
}
function handleResponse() {
    if(http.readyState == 4){
        var response = http.responseText;
		element=document.getElementById(logIdToUpdate);
		if(element)
			element.innerHTML=response;
    }
}

function createCMSFolder(urlToCreate)
{
	var now= new Date();
	var action = '?aj=1&folder='+urlToCreate+'&_tr=FrmMakeCMSFolder&_ts='+Math.round(now.getTime()/1000);
	var main=document.getElementById('Main');
	main.innerHTML='<em>Loading ...</em>';
    http.open('get', action);
    http.onreadystatechange = handleResponseCreateCMSFolder;
    http.send(null);
	logIdToUpdate='Main';
	element=document.getElementById(logIdToUpdate);
	if(element && element.innerHTML=='')
	{
		element.innerHTML='<em>Loading...</em>';
	}
	window.scrollTo(0,0);
}

function handleResponseCreateCMSFolder() {
    if(http.readyState == 4){
        var response = http.responseXML.documentElement;
		var innerHTML='';
		var js='';
		var bGotJS;
		var items;
		element=document.getElementById(logIdToUpdate);
		items=response.getElementsByTagName('content');
		for(i=0;i<items.length;i++)
		{
	        if (items[i].childNodes.length > 1)
			{
        	    innerHTML+= items[i].childNodes[1].nodeValue;
    	    } else {
	            innerHTML+= items[i].firstChild.nodeValue;
			}
		}
		if(element)
			element.innerHTML=innerHTML;
		bGotJS=false;
		items=response.getElementsByTagName('jscript');
		for(i=0;i<items.length;i++)
		{
			bGotJS=true;
	        if (items[i].childNodes.length > 1)
			{
        	    js+= items[i].childNodes[1].nodeValue;
    	    } else {
	            js+= items[i].firstChild.nodeValue;
			}
		}
		eval(js);

    }
}

function createCMSPage(urlToCreate)
{
	var now= new Date();
	var action = '?aj=1&page='+urlToCreate+'&_tr=FrmMakeCMSPage&_ts='+Math.round(now.getTime()/1000);
	var main=document.getElementById('Main');
	main.innerHTML='<em>Loading ...</em>';
	sndReq(action,'Main');
	window.scrollTo(0,0);
}

function displayMap(id)
{
	var oChosenElement;
	var oUnChosenElement;
	for(i=0;i<20;i++)
	{
		oUnChosenElement= document.getElementById('map_'+i)
		if (oUnChosenElement)
		{
			oUnChosenElement.style.display='none';
		}
	}
	oChosenElement=document.getElementById(id);
	if(oChosenElement)
		oChosenElement.style.display='block';
}

function CharsRemaining(textarea)
{
	var max = parseInt(textarea.getAttribute("maxlength"), 10);
	var curr = textarea.value.length;
	return max - curr;
}

function CharsRemaining(textarea)
{
	var max = parseInt(textarea.getAttribute("maxlength"), 10);
	// Ensure that newlines are counted as 2 chars so that the count is standardised across browsers.
	var curr = expandNewlines(textarea.value).length;
	return max - curr;
}
 
/**
	When counting text in textareas we need to ensure that all newlines are two chars. 
	Deal with differing representations of newlines in text areas across browsers.
	(IE uses \r\n, FF and chrome use \n W3C standard says \r\n.)
	When form is submitted it will contain \r\n so this is what PHP will count.
	This function replaces all instances of \n with \r\n
*/
function expandNewlines(string)
{
	// Regex to find any \n not preceeded by a \r.
	// /(?!\r)\n/g, "\r\n");
	// Can you guess which browsers dont support lookaheads in regular expressions?
	// Use an inelegent pair of string replacements instead.
	var expandedString = string.replace(/\n/g, "\r\n").replace(/\r\r\n/g, "\r\n");
	return expandedString;
}


function isMaxLength(obj){
var mlength=obj.getAttribute? parseInt(obj.getAttribute("maxlength")) : ""
if (obj.getAttribute && obj.value.length>mlength)
obj.value=obj.value.substring(0,mlength)
}

function IsNumeric(sText)

{
   var ValidChars = "0123456789.";
   var IsNumber=true;
   var Char;


   for (i = 0; i < sText.length && IsNumber == true; i++)
      {
      Char = sText.charAt(i);
      if (ValidChars.indexOf(Char) == -1)
         {
         IsNumber = false;
         }
      }
   return IsNumber;

   }

  function findPosX(obj)
  {
    var curleft = 0;
    if(obj.offsetParent)
        while(1)
        {
          curleft += obj.offsetLeft;
          if(!obj.offsetParent)
            break;
          obj = obj.offsetParent;
        }
    else if(obj.x)
        curleft += obj.x;
    return curleft;
  }

  function findPosY(obj)
  {
    var curtop = 0;
    if(obj.offsetParent)
        while(1)
        {
          curtop += obj.offsetTop;
          if(!obj.offsetParent)
            break;
          obj = obj.offsetParent;
        }
    else if(obj.y)
        curtop += obj.y;
    return curtop;
  }

  // returns true if given x,y coords are within the boundry of the elmenent
  function coordsInElement(x,y,elementId)
  {
  	e = document.getElementById(elementId);
  	x1pos = findPosX(e);
  	x2pos = x1pos + e.offsetWidth;
  	y1pos = findPosY(e);
  	y2pos = y1pos + e.offsetHeight;
  	if (x >= x1pos && x <= x2pos && y >= y1pos && y <= y2pos)
  		return true;

  	return false;

  }

  // Stop events from bubbling up the DOM.
  // Example usage: onClick="alert('Gufnork fluff!'); stopEventPropogation(event)"
  function stopEventPropagation(event)
  {
  	if (event.stopPropagation)
  	{
  		// Moz
  		event.stopPropagation();
  	}
  	else
  	{
  		// IE
  		window.event.cancelBubble = true;
  	}

  }

  // Return the nearest parent node of specified tagname
  function parentNodeOfType(element, tagname)
  {
  	var pa=element.parentNode;
	while(pa.tagName.toLowerCase()!=tagname.toLowerCase() && pa.parentNode)
	{
		pa=pa.parentNode;
	}

	if (pa.tagName.toLowerCase() == tagname.toLowerCase())
	{
		return pa;
	}

	return undefined;
  }

function eventTarget(event)
{
	if (!event)
		event = window.event;

	if (event.target)
	{
  		element = event.target;
  	}

  	if (event.srcElement)
  		element = event.srcElement;

  	if (element.nodeType == 3) // defeat Safari bug
		element = element.parentNode;

	return element;
}

function showNextTableRow(e)
{
	var thisElement = eventTarget(e);

	if (thisElement.tagName == 'TR')
		tr = thisElement;
	else
		tr = parentNodeOfType(thisElement,'TR');
	if (!tr)
		return;

	nextIndex = tr.rowIndex+1;
	nextTr = tr.parentNode.parentNode.rows[nextIndex];
	nextTr.style.display='';
}

function hideThisTableRow(e)
{
	thisElement = eventTarget(e);
	if (thisElement.tagName == 'TR')
		tr = thisElement;
	else
		tr = parentNodeOfType(thisElement,'TR');

	if (!tr)
		return;
	tr.style.display='none';
}

function hideNextTableRow(e)
{
	var thisElement = eventTarget(e);
	if (thisElement.tagName == 'TR')
		tr = thisElement;
	else
		tr = parentNodeOfType(thisElement,'TR');

	if (!tr)
		return;

	nextIndex = tr.rowIndex+1;
	nextTr = tr.parentNode.parentNode.rows[nextIndex];
	nextTr.style.display='none';
}

function hidePrevTableRow(e)
{
	var thisElement = eventTarget(e);

	if (thisElement.tagName == 'TR')
		tr = thisElement;
	else
		tr = parentNodeOfType(thisElement,'TR');

	if (!tr)
		return;

	prevIndex = tr.rowIndex-1;
	prevTr = tr.parentNode.parentNode.rows[prevIndex];
	prevTr.style.display='none';
}

function showPrevTableRow(e)
{
	var thisElement = eventTarget(e);

	if (thisElement.tagName == 'TR')
		tr = thisElement;
	else
		tr = parentNodeOfType(thisElement,'TR');

	if (!tr)
		return;

	prevIndex = tr.rowIndex-1;
	prevTr = tr.parentNode.parentNode.rows[prevIndex];
	prevTr.style.display='';
}

/* Adds a class to an elements class attribute */
function addClass(element, addClass)
{
	if (!hasClass(element, addClass))
	{
		element.className += (element.className ? ' ' : '') +  addClass;
		return true;
	}
	return false;
}

/* Returns true if element has given class */
/* specify a single class */
/*
Interesting fact about IE 7 (and probably 6). 'class' is a reserved word so IE will produce some vague and entirely useless error message if 'class' is used as
the name of a variable.  Someone should send MS developers on a course in writing error messages, or shoot them.
*/
function hasClass(element, className)
{
	if (element.className == '')
	{
		return false;
	}

	return new RegExp('\\b' + className + '\\b').test(element.className);
}

/* Removes class from elements class attribute */
function removeClass(element, removeClass)
{
	if (hasClass(element, removeClass))
	{
		element.className  = element.className.replace(element.className.indexOf(' ' + removeClass) >= 0 ? ' ' + removeClass : removeClass)

	}
}
;(function() {
var L = YAHOO.lang, Dom = YAHOO.util.Dom, Evt = YAHOO.util.Event, U = ZC.Util, _GT = U.GetText, _zc = U.ReplaceHashTags, 
	YUIPECONTENT = 'yui-pe-content', HIDE = 'hide', ZCWidget = ZC.Core.Widget,
	oWidget, oChildWidget; // oWidget reused for each new class (helps cut code size once minified)

/**
 * @module Core
 */

/**
 * Class for the widgets that want to create YUI buttons. 
 * Allows widgets to specify the button config in a consistent manner.
 *
 * The following attribs are recognised:
 *
 * <dl>
 * <dd> Type			</dd><dt> Overrides the button type from the default (see YUI docs for the different button types) </dt>
 * <dd> IconImage		</dd><dt> URL for the icon image file. </dt>
 * <dd> IconWidth		</dd><dt> Width of the icon in pixels. </dt>
 * <dd> IconNoCaption	</dd><dt> If true, then the button will be displayed with an icon only. </dt>
 * <dd> IconPosition	</dd><dt> Either "left" (default) or "right". </dt>
 * </dl>
 *
 * @class YUIButtonWrapper
 * @constructor
 * @param {HTMLElement/String} elButton the element (or element id) to replace with a YUI button
 * @param {Array/Boolean} aButtonDef the definition for the button (or just boolean true for defaults)
 * @param {Object} oWidget the widget representing this button (the wrapper will fire click event handlers on this widget when the YUI button is clicked)
 */
ZC.Util.YUIButtonWrapper = function(elSrc, aButtonDef, oWidget)
{
	var oYUIButton, elButton, oButtonConfig = {}, fnOnClick = false, fnOnChange = false;

	if (aButtonDef.Type)
	{
		oButtonConfig.type = aButtonDef.Type;
	}
	if (aButtonDef.Caption)
	{
		oButtonConfig.label = aButtonDef.Caption;
	}

	Dom.removeClass(elSrc, YUIPECONTENT);

	if (elSrc.onclick)
	{
		fnOnClick = elSrc.onclick;
	}
	if (elSrc.onchange)
	{
		fnOnChange = elSrc.onchange;
	}

	if (YAHOO.env.ua.gecko && elSrc.getAttribute('type') == 'submit')
	{
		// workaround YUI bug #1909677
		elSrc.type = 'button';
		oButtonConfig.type = 'submit';
	}

	this.oWidget = oWidget;
	this.oYUIButton = oYUIButton = new YAHOO.widget.Button(elSrc, oButtonConfig);

	oYUIButton.zcWidget = oWidget;

	if (elSrc.className)
	{
		oYUIButton.addClass(elSrc.className);
	}

	if (oWidget)
	{
		oYUIButton.on('click', function(oEvent) { this._EventDispatcher('click', oEvent); }, oWidget, true);
		if (fnOnClick)
			oWidget.AddEvent(fnOnClick, 'click', oWidget);

		if (oYUIButton.get('type') == 'checkbox')
		{
			oYUIButton.on('checkedChange', function(oEvent) { 
				this._EventDispatcher('change', oEvent); 
			}, oWidget, true);
			if (fnOnChange)
				oWidget.AddEvent(fnOnChange, 'change', oWidget);
		}
	}

	if (L.isObject(aButtonDef))
	{
		if (aButtonDef.IconImage && aButtonDef.IconWidth)
		{
			elButton = (oYUIButton.getElementsByClassName('first-child'))[0].firstChild;
			Dom.setStyle(elButton, 'background-image', 'url(' + aButtonDef.IconImage + ')');
			Dom.setStyle(elButton, 'background-repeat', 'no-repeat');

			iPadding = Number(aButtonDef.IconWidth);
			if (aButtonDef.IconNoCaption)
			{
				oYUIButton.set('label', '');
				Dom.setStyle(elButton, 'padding-left', iPadding + 'px');
				Dom.setStyle(elButton, 'padding-right', '0px');
				Dom.setStyle(elButton, 'background-position', '50% 50%');
			}
			else
			{
				iPadding += 10;
				if (aButtonDef.IconPosition == 'right')
				{
					Evt.onContentReady(Dom.generateId(elButton), function() {
						var oRegion = Dom.getRegion(elButton);			
						Dom.setStyle(elButton, 'padding-right', iPadding + 'px');
						Dom.setStyle(elButton, 'background-position', (oRegion.width - 7) + 'px 50%');
					});
				}
				else
				{
					Dom.setStyle(elButton, 'padding-left', iPadding + 'px');
					Dom.setStyle(elButton, 'background-position', '7px 50%');
				}
			}
		}
	}
}
ZC.Util.YUIButtonWrapper.prototype = {
	/**
	 * Frees up all the references in this object. Called on page unload to avoid memory leaks in IE.
	 */
	Destruct: function()
	{
		this.oYUIButton = null;
	},

	/**
	 * Enables or disables the button
	 * @param [{Boolean}] bEnable whether to enable or disable the button, defaults to true.
	 */
	Enable: function(bEnable)
	{
		if (L.isUndefined(bEnable))
			bEnable = true;
		this.oYUIButton.set('disabled', !bEnable);
	}
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Checkbox
 */
oWidget = ZCWidget.Create('Checkbox');
oWidget.prototype.CustomSetupEnd = function()
{
	var sCheckedClass = this.GetAttribDefault('CheckedClass', 'checked'), fnChangeHandler, elLabel, aButtonDef;
	if (!this._elInput || !this.elContainer || !sCheckedClass)
		return false;

	if (aButtonDef = this.GetAttribDefault('YUIButtonDef')) // assignment
	{
		if (L.isBoolean(aButtonDef))
			aButtonDef = {};

	   	if (L.isUndefined(aButtonDef.Caption))
		{
			elLabel = this.GetLabelEl();
			Dom.addClass(elLabel, HIDE);
			aButtonDef.Caption = elLabel.innerHTML;
		}
		
		this.oButtonWrapper = new U.YUIButtonWrapper(this._elInput, aButtonDef, this);
		this._elInput = this.oButtonWrapper.oYUIButton.get('element');
	}

	fnChangeHandler = function()
	{
		if (!this._elInput)
			return;

		Dom[this._elInput.checked ? 'addClass' : 'removeClass'](this.elContainer, sCheckedClass);
	}
	Evt.on(this._elInput, 'change', fnChangeHandler, this, true);
	Evt.on(this._elInput, 'click', fnChangeHandler, this, true); // IE doesn't fire change events until blur *sigh*
	fnChangeHandler.call(this);
	return true;
}
oWidget.prototype.Destruct = function()
{
	ZCWidget.Checkbox.superclass.Destruct.apply(this, arguments);

	if (this.oButtonWrapper)
		this.oButtonWrapper.Destruct();
}
oWidget.prototype.Enable = function(bEnable, sEnableClass, sDisableClass)
{
	ZCWidget.Checkbox.superclass.Enable.apply(this, arguments);

	if (this.oButtonWrapper)
		this.oButtonWrapper.Enable(bEnable);
}

oWidget.prototype.GetValue = function()
{
	if (this.oButtonWrapper)
	{
		return this.oButtonWrapper.oYUIButton.get('checked');
	}

	if (!this._elInput)
		return undefined;

	if (this._elInput.type == 'hidden')
		return (this._elInput.value == 1);

	return this._elInput.checked;
}
oWidget.prototype.HasValue = function()
{
	return true;
}
oWidget.prototype.SetValue = function(bValue)
{
	this._elInput.checked = !!bValue;
	this._FireEventHandlers('change');
}
oWidget.prototype.AttribMethod_Caption = function(sValue)
{
	ZCWidget.Checkbox.superclass.AttribMethod_Caption.apply(this, arguments);

	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.oYUIButton.set('label', sValue);
	}
}
/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.Checkbox
 * @class Boolean
 */
ZCWidget.Boolean = oWidget; // alias

/**
 * Helper methods for the Radio and CheckboxGroup widgets, which both use a series of related INPUT elements.
 * NOTE: this overrides the default _FindElements method, so you need to pass <code>true</code> as the third argument to YAHOO.lang.augment
 * @class GroupedInputHelper
 * @private
 */
var GroupedInputHelper = function() {};

GroupedInputHelper.prototype = {
	_FindElements: function()
	{
		var sID = this.ID();
		if (this.IsReadOnly())
		{
			this._elInput = Dom.get(sID);
			return;
		}

		this.elContainer = Dom.get(sID);
		this._elInput = [];

		if (this.aDef.OptionKeys)
		{
			this._elInput = U.Map(this.aDef.OptionKeys, function(sOptKey)
			{
				return Dom.get(this.ID(sOptKey));
			}, this);
			this._elInput = U.Filter(this._elInput, function(el) { return L.isObject(el); }); // filter out nulls

			if (this._elInput.length == 0)
				this._elInput = null;
		}

		if (!this.elContainer)
			this._FindContainers();
	},
	/**
	 * Finds the label element for the given input, or all labels for all radios in this widget if not specified.
	 * @param {Array/HTMLElement} [aInputs] optional element(s) to find label for
	 * @return {Array/HTMLElement} either an array of elements or a single label element
	 */
	GetLabelForElement: function(aInputs)
	{
		var aLabels, bReturnArray = true;

		if (L.isUndefined(aInputs))
		{
			aInputs = this._elInput;
		}
		else if (!L.isArray(aInputs))
		{
			bReturnArray = false;
			aInputs = [aInputs];
		}

		aLabels = U.Map(aInputs, function(elRadio)
		{
			var sInputId = elRadio.id,
				aFindLabels = Dom.getElementsBy(function(el) { return (Dom.getAttribute(el, 'for') == sInputId); }, 'label');

			if (aFindLabels.length == 0)
				return undefined;

			return aFindLabels[0];
		}, this);

		return (bReturnArray) ? aLabels : aLabels[0];
	},

	GetLabelEl: function()
	{
		// the default GetLabelEl will return the label for the first radio/checkbox. We just want to find the label for the widget as a whole.
		var sInputID = this._WidgetNameToID();
		return Dom.getElementBy(function(el) { return (Dom.getAttribute(el, 'for') == sInputID); }, 'label');
	},

	/**
	 * Returns an object containing all of the options for this widget.
	 * @param {Boolean} [bHTML] if true, returns the option labels as HTML (defaults to false)
	 * @return {Object} the options, as key => value pairs
	 */
	GetOptions: function(bHTML)
	{
		var oOptions = {};

		U.ForEach(this._elInput, function (el) { 
			var elLabel = this.GetLabelForElement(el);
			oOptions[el.value] = bHTML ? elLabel.innerHTML : (elLabel.textContent || elLabel.innerText);
		}, this);

		return oOptions;
	}
};

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Radio
 * @uses ZCWidget.GroupedInputHelper
 */
oWidget = ZCWidget.Create('Radio');
L.augment(oWidget, GroupedInputHelper, true);

/**
 * Finds the selected radio input in the radio group.
 * @private
 * @return {Object} reference to the radio INPUT element
 */
oWidget.prototype._GetSelectedInput = function()
{
	if (!this._elInput)
		return undefined;

	for (var i = 0, iMax = this._elInput.length; i < iMax; ++i)
	{
		if (this._elInput[i].checked)
			return this._elInput[i];
	}
	return undefined;
}

oWidget.prototype.GetValue = function()
{
	if (this.IsReadOnly())
		return this._elInput.value;

	var elSelected = this._GetSelectedInput();
	return elSelected ? elSelected.value : undefined;
}
oWidget.prototype.GetHTMLValue = function()
{
	if (this.IsReadOnly())
		return this._elInput.value;

	var elSelected = this._GetSelectedInput(), elLabel;
	if (!elSelected)
		return undefined;

	elLabel = this.GetLabelForElement(elSelected);
	if (!elLabel)
		return undefined;

	return elLabel.innerHTML;
}
oWidget.prototype.GetTextValue = function()
{
	if (this.IsReadOnly())
		return this._elInput.value;

	var elSelected = this._GetSelectedInput(), elLabel;
	if (!elSelected)
		return undefined;

	elLabel = this.GetLabelForElement(elSelected);
	if (!elLabel)
		return undefined;

	return (elLabel.textContent || elLabel.innerText);
}
oWidget.prototype.SetValue = function(Value)
{
	if (L.isUndefined(Value))
		return this.Clear();

	for (var i = 0, iMax = this._elInput.length; i < iMax; ++i)
	{
		if (this._elInput[i].value == Value)
		{
			this._elInput[i].checked = true;
			this._FireEventHandlers('change');
			return;
		}
	}
}
oWidget.prototype.Clear = function()
{
	if (!L.isUndefined(this.GetValue()))
	{
		Dom.batch(this._elInput, function(el) { el.checked = false; });
		this._FireEventHandlers('change');
	}
}


/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class CheckboxGroup
 * @uses ZCWidget.GroupedInputHelper
 */
oWidget = ZCWidget.Create('CheckboxGroup');
L.augment(oWidget, GroupedInputHelper, true);

oWidget.prototype.CustomSetupEnd = function()
{
	Evt.on(this._elInput, 'change', function(oEvent) { this._ToggleSelectedClass(Evt.getTarget(oEvent)); }, this, true);
	if (YAHOO.env.ua.ie)
		Evt.on(this._elInput, 'click', function(oEvent) { this._ToggleSelectedClass(Evt.getTarget(oEvent)); }, this, true);

	/*
	 * Check initial state. The server-side widget adds the class to the tr if
	 * it's set on the server-side, but that doesn't work if the user reloads
	 * the page in their browser.
	 */
	U.ForEach(this._elInput, this._ToggleSelectedClass, this);

	if (this.GetAttribDefault('ShowSelectAllOrNone'))
	{
		var elSelectAll = Dom.get(this._WidgetNameToID('__ALL__')),
			elSelectNone = Dom.get(this._WidgetNameToID('__NONE__'));

		Dom.removeClass(elSelectAll.parentNode, HIDE);
		Evt.on(elSelectAll, 'click', function(oEvent) { Evt.stopEvent(oEvent); this.SetValue(U.Map(this._elInput, function(el) { return el.value; })); }, this, true);
		Evt.on(elSelectNone, 'click', function(oEvent) { Evt.stopEvent(oEvent); this.SetValue([]); }, this, true);
	}

	return true;
}

/**
 * toggle the selected class on an element
 * @private
 */
oWidget.prototype._ToggleSelectedClass = function(el)
{
	if (el)
	{
		var sSelectedClass = this.GetAttribDefault('SelectedClass', 'checked');
			elAncestor = Dom.getAncestorByTagName(el, 'tr');
	
		Dom[el.checked ? 'addClass' : 'removeClass'](elAncestor, sSelectedClass);
	}
}

/**
 * Finds the selected checkboxes input in the checkbox group.
 * @private
 * @return {Array} array of selected checkbox elements
 */
oWidget.prototype._GetSelectedInputs = function()
{
	return U.Filter(this._elInput, function(el) { return el.checked; });
}
oWidget.prototype.GetValue = function()
{
	var elSelected = this._GetSelectedInputs();
	return U.Map(elSelected, function(el) { return el.value; });
}
oWidget.prototype.GetTextValue = function()
{
	var elSelected = this._GetSelectedInputs(), aTextValues;
	if (!elSelected.length)
		return this.aDef.FormattedValueWhenEmpty || '';

	aTextValues = U.Map(this.GetLabelForElement(elSelected), function (el) { return (el.textContent || el.innerText); });
	return aTextValues.join("\n");
}
oWidget.prototype.GetHTMLValue = function()
{
	var elSelected = this._GetSelectedInputs(), aHTMLValues;
	if (!elSelected.length)
		return this.aDef.FormattedValueWhenEmpty || '';

	aHTMLValues = U.Map(this.GetLabelForElement(elSelected), function (el) { return el.innerHTML; });
	return aHTMLValues.join(this.aDef.HTMLValueSeparator || "<br />");
}
oWidget.prototype.SetValue = function(aValue)
{
	var bChanged = false;
	U.ForEach(this._elInput, function(el)
	{
		if (U.InArray(el.value, aValue))
		{
			if (!el.checked)
			{
				el.checked = true;
				bChanged = true;
				this._ToggleSelectedClass(el);
			}
		}
		else
		{
			if (el.checked)
			{
				el.checked = false;
				bChanged = true;
				this._ToggleSelectedClass(el);
			}
		}
	}, this);
	if (bChanged)
		this._FireEventHandlers('change');
}
/**
 * Enable / disable one or more options.
 * @param {mixed} Value Either an option value, or array of option values
 * @param {Boolean} bEnable If true (default), enables this option
 */
oWidget.prototype.EnableOption = function(Value, bEnable)
{
	if (!L.isArray(Value))
		Value = [Value];

	var sAddRemove = bEnable ? 'removeClass' : 'addClass';
	U.ForEach(this._elInput, function(el)
	{
		if (U.InArray(el.value, Value))
		{
			el.disabled = !bEnable;
			var elTR = Dom.getAncestorByTagName(el, 'tr');
			Dom[sAddRemove](elTR, 'disabled');
		}
	});
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Select
 */

oWidget = ZCWidget.Create('Select');
// used in LinkedSelect as well (TODO: rewrite LinkedSelect to use Select widgets)
var fnSetupCopyOptionClass = function()
{
	var elSelectedOption, sCurrentOptionClass, fnChangeHandler;

	fnChangeHandler = function(oEvent, elSelect)
	{
		if (oEvent)
			elSelect = Evt.getTarget(oEvent);

		if (this.sOldOptionClass)
			Dom.removeClass(elSelect, this.sOldOptionClass);

		var elSelectedOption = elSelect.options[elSelect.selectedIndex],
			sNewClass = Dom.getAttribute(elSelectedOption, 'class');

		if (sNewClass)
		{
			Dom.addClass(elSelect, sNewClass);
			this.sOldOptionClass = sNewClass;
		}
	}

	if (this.GetAttribDefault('CopyOptionClass'))
	{
		Evt.on(this._elInput, 'change', function(oEvent)
		{
			var elSelect = Evt.getTarget(oEvent);
			if (!elSelect)
			{
				if (L.isArray(this._elInput))
					elSelect = this._elInput[0];
				else
					elSelect = this._elInput;
			}
			fnChangeHandler.call(this, null, elSelect);
		}, this, true);
		Evt.on(this._elInput, 'keydown', function() 
		{ 
			this.oKeyInterval = L.later(10, this, function() 
			{
				var elSelect;
				if (L.isArray(this._elInput))
					elSelect = this._elInput[0];
				else
					elSelect = this._elInput;

				fnChangeHandler.call(this, false, elSelect); 
			}); 
		}, this, true);
		Evt.on(this._elInput, 'keyup', function() { if (this.oKeyInterval) this.oKeyInterval.cancel(); }, this, true);
		Dom.batch(this._elInput, function(el) { fnChangeHandler.call(this, false, el); }, this, true);
	}

	return true;
}
oWidget.prototype.CustomSetupEnd = fnSetupCopyOptionClass;

oWidget.prototype.GetValue = function(bTextValue)
{
	// lazy binding of the required GetValue implementation
	if (!this._elInput || (this._elInput.type == 'hidden' && this._elInput.tagName.toLowerCase() == 'input'))
	{
		// hidden field (only one option), use parent method
		this.bHiddenField = true;
		this.GetValue = ZCWidget.Select.superclass.GetValue;
	}
	else if (this._elInput.type == 'select-one')
	{
		this.GetValue = function(bTextValue)
		{
			if (!this._elInput)
				return undefined;

			var iIndex = this._elInput.selectedIndex;
			if (iIndex < 0)
				return undefined;

			var oOpt = this._elInput.options[iIndex];
			return bTextValue ? oOpt.text : oOpt.value;
		}
	}
	else
	{
		this.GetValue = function(bTextValue)
		{
			if (!this._elInput)
				return undefined;

			var aValues = [];

			U.ForEach(this._elInput.options, function(oOpt)
		   	{
				if (oOpt.selected) aValues.push(bTextValue ? oOpt.text : oOpt.value);
			});
			return aValues;
		}
	}

	return this.GetValue(bTextValue);
}
oWidget.prototype.GetTextValue = function()
{
	return this._GetFormattedValue("\n");
}
oWidget.prototype.GetHTMLValue = function()
{
	return this._GetFormattedValue(this.aDef.HTMLValueSeparator || '<br />');
}
oWidget.prototype._GetFormattedValue = function(sSeparator)
{
	if (this.bHiddenField)
		return (this.elContainer.textContent || this.elContainer.innerText);

	var Value = this.GetValue(true);

	if (Value.length == 0)
		return (this.aDef.FormattedValueWhenEmpty || '');

	if (!L.isArray(Value))
		return Value;

	return Value.join(sSeparator);
}
oWidget.prototype.SetValue = function(Value)
{
	var bSingle = !L.isArray(Value),
	 	bChanged = false;

	if (this._elInput.type == 'hidden' && this._elInput.tagName.toLowerCase() == 'input')
	{
		return ZCWidget.Select.superclass.SetValue(Value);
	}

	for (var iOpt = 0, iOptMax = this._elInput.length; iOpt < iOptMax; ++iOpt)
	{
		var oOpt = this._elInput.options[iOpt];
		if (bSingle)
		{
			if (oOpt.value == Value)
			{
				if (!oOpt.selected)
				{
					oOpt.selected = true;
					this._FireEventHandlers('change', null, this._elInput);
				}
				return;
			}
		}
		else
		{
			var bInArray = U.InArray(oOpt.value, Value);
			bChanged = bChanged || (oOpt.selected != bInArray);
			oOpt.selected = bInArray;
		}
	}
	if (bChanged)
		this._FireEventHandlers('change', null, this._elInput);
}
/**
 * Handles changes to the "Options" attrib, replacing the existing options.
 * Attempts to keep the current selection (if the previously-selected option
 * keys still exist).
 *
 * @param {Object} AttribValue a list of options, key => caption
 */
oWidget.prototype.AttribMethod_Options = function(AttribValue)
{
	var aSelectedOptions = (this._elInput.type == 'select-one') ? [this.GetValue()] : this.GetValue();
	this._elInput.options.length = 0;

	for (var sOptVal in AttribValue)
	{
		if (L.hasOwnProperty(AttribValue, sOptVal))
		{
			var elOption = document.createElement('option');
			elOption.value = String(sOptVal);
			elOption.innerHTML = String(AttribValue[sOptVal]);
			elOption.selected = U.InArray(sOptVal, aSelectedOptions);

			this._elInput.appendChild(elOption);
		}
	}
}
/**
 * Handles changes to the OptionAttribs attrib: allows the HTML attribs of the option elements to be changed.
 */
oWidget.prototype.AttribMethod_OptionAttribs = function(AttribValue, OldAttribValue)
{
	U.ForEach(Dom.getChildren(this._elInput), function(elOption)
	{
		var sOptVal = elOption.value, 
			aOldAttribs = {}, aNewAttribs = {};

		if (!L.isUndefined(OldAttribValue) && sOptVal in OldAttribValue)
		{
			aOldAttribs = OldAttribValue[sOptVal];
		}
		if (!L.isUndefined(AttribValue) && sOptVal in AttribValue)
		{
			aNewAttribs = AttribValue[sOptVal];
		}

		U.ForEach(aNewAttribs, function(sValue, sAttrib)
		{
			if (sAttrib in aOldAttribs)
				delete(aOldAttribs[sAttrib]);

			elOption[sAttrib] = sValue;

			if (this.GetAttribDefault('CopyOptionClass') && sOptVal == this.GetValue() && sAttrib == 'className')
			{
				if (this.sOldOptionClass)
				{
					Dom.removeClass(this._elInput, this.sOldOptionClass);
					this.sOldOptionClass = null;
				}

				Dom.addClass(this._elInput, sOptVal);
			}
		}, this);

		U.ForEach(aOldAttribs, function(sValue, sAttrib)
		{
			elOption[sAttrib] = '';
		});
	}, this);
}

oWidget.prototype.Clear = function()
{
	if (this._elInput && this._elInput.options)
	{
		var sFirstOptVal = this._elInput.options[0].value;
		if (sFirstOptVal.match(/^(_?_?Any_?_?|)$/i))
			this._elInput.selectedIndex = 0;
	}
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.Select
 * @class Reference
 */
ZCWidget.Create('Reference', 'Core', oWidget);

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Text
 */
oWidget = ZCWidget.Create('Text');
oWidget.prototype.InsertText = function(sText)
{
	U.InsertAtCursor(this._elInput, sText);
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class TextArea
 */
oWidget = ZCWidget.Create('TextArea');
oWidget.prototype.CustomSetupEnd = function()
{
	if (this.aDef.Size)
	{
		this.AddEvent(this.UpdateNRemaining, ['keyup', 'focus'],  this);
		this.AddEvent(this.HideNRemaining, 'blur',  this);
	}

	return true;
}
oWidget.prototype.UpdateNRemaining = function(event)
{
	if (L.isUndefined(this._elRemaining))
	{
		this._elRemaining = document.createElement('span');
		this._elRemaining.className = 'textarea-remainingchars';
		Dom.insertAfter(this._elRemaining, this._elInput);
	}

	var iUsed = this.GetCharCount(), iMax = this.aDef.Size, sRemainingText = '';
	var iRemaining = iMax - iUsed;

	if(iRemaining >= 0)
		sRemainingText = U.sprintf(_GT('Used %1$d of %2$d characters, %3$d remaining.'), iUsed, iMax, iRemaining);
	else
		sRemainingText = U.sprintf(_GT('Used %1$d of %2$d characters.'), iUsed, iMax);

	this._elRemaining.innerHTML = sRemainingText;
	var bOverLimit = iUsed > iMax;
	var sNewClass = bOverLimit ? 'invalid' : 'valid';
	var sOldClass = bOverLimit ? 'valid' : 'invalid';
	Dom.replaceClass(this._elRemaining, sOldClass, sNewClass);

	Dom.removeClass(this._elRemaining, 'invisible');
}
oWidget.prototype.HideNRemaining = function(event)
{
	if (this._elRemaining)
		Dom.addClass(this._elRemaining, 'invisible');
}

oWidget.prototype.GetCharCount = function()
{
	// As you'd expect, different browsers use different line endings when entering text.
	// Our server side code then canonicalises this to \n so that line ending counts match up
	// with word (where \r\n is one character). To be consistent with that we need to ensure that
	// we only count line endings as one character.
	return this.GetValue().replace(/\r\n/g, "\n").length;

	// TODO: the javascript string.length property returns the number of UCS-2 code units inside
	//  the string rather than the number of characters. This is fine except for characters that
	// require more than two bytes to represent in that encoding.
}

oWidget.prototype.Validate = function()
{
	// Enforce the max length
	var iUsed = this.GetCharCount(), iMax = this.aDef.Size;
	var sValMsg = U.sprintf(_GT('Please reduce this to %d characters, it is currently at %d.'), iMax, iUsed);

	if (iUsed > iMax)
	{
		this.SetValid(false, sValMsg);
		return false;
	}
	return ZCWidget.TextArea.superclass.Validate.call(this);
}

oWidget.prototype.InsertText = function(sText)
{
	U.InsertAtCursor(this._elInput, sText);
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Group
 */
oWidget = ZCWidget.Create('Group');
oWidget.prototype.CustomSetupEnd = function()
{
	if (this.aDef.HideFields)
	{
		var sID, elPlaceholder, fnToggleVisible, elOpenCloseLink, sLabelShow, sLabelHide, sShowLinkTitle, sHideLinkTitle;

		sLabelShow = this.GetAttribDefault('LabelShow', _GT('Show'));
		sLabelHide = this.GetAttribDefault('LabelHide', _GT('Hide'));
		sShowLinkTooltip = this.GetAttribDefault('ShowLinkTooltip', _GT('Show this section'));
		sHideLinkTooltip = this.GetAttribDefault('HideLinkTooltip', _GT('Hide this section'));

		sID = this.aDef.ID;
		elPlaceholder = Dom.get(sID + '-toggle');
		this.elContainer = Dom.get(sID + '-hidediv');

		if (!elPlaceholder)
			return true;

		elOpenCloseLink = this.elOpenCloseLink = document.createElement('a');
		elOpenCloseLink.href = '#';
		elOpenCloseLink.innerHTML = sLabelShow;
		this.oOpenCloseTooltip = new YAHOO.widget.Tooltip('grp-oc-tt-' + this.sName, {
			context: elOpenCloseLink, autofillheight: false, autodismissdelay: 120000,
			effect:  { effect: YAHOO.widget.ContainerEffect.FADE, duration: 0.25 },
			text: sShowLinkTooltip
		});
		elPlaceholder.appendChild(elOpenCloseLink);

		var fnToggleVisible = function(event)
		{
			if (event)
				Evt.stopEvent(event);

			if (this.IsVisible())
			{
				this.Hide();
				this.elOpenCloseLink.innerHTML = sLabelShow;
				this.oOpenCloseTooltip.cfg.setProperty('text', sShowLinkTooltip);
			}
			else
			{
				this.Show();
				this.elOpenCloseLink.innerHTML = sLabelHide;
				this.oOpenCloseTooltip.cfg.setProperty('text', sHideLinkTooltip);
			}
		}

		Evt.on(elOpenCloseLink, 'click', fnToggleVisible, this, true);
		if (this.IsVisible())
			fnToggleVisible.call(this);
	}

	return true;
}

oWidget.prototype.GetTextValue = function()
{
	var aResult = U.Map(this.aChildWidgets, function(oWidget) { return oWidget.GetTextValue(); });
	return aResult.join(this.aDef.TextValueSeparator || ", ");
}

oWidget.prototype.GetHTMLValue = function()
{
	var aResult = U.Map(this.aChildWidgets, function(oWidget) { return oWidget.GetHTMLValue(); });
	return aResult.join(this.aDef.HTMLValueSeparator || "<br />");
}

oWidget.prototype.GetValue = function()
{
	var oValue = {};
	U.ForEach(this.aChildWidgets, function(oWidget, sWidgetName) { oValue[sWidgetName] = oWidget.GetValue(); });
	return oValue;
}

oWidget.prototype.Enable = function(bEnable, sEnableClass, sDisableClass)
{
	U.ForEach(this.aChildWidgets, function(oChild) { oChild.Enable(bEnable, sEnableClass, sDisableClass); });
}
/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.Group
 * @class Document
 */
ZCWidget.Create('Document', 'Core', oWidget);
/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.Group
 * @class Group_List
 */
ZCWidget.Create('Group_List', 'Core', oWidget);


/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Group_Matrix
 */
oWidget = ZCWidget.Create('Group_Matrix', 'Core', ZCWidget.Group);
oWidget.prototype.CustomSetupEnd = function()
{
	ZCWidget.Group_Matrix.superclass.CustomSetupEnd.call(this);

	this.elHeading = Dom.getFirstChildBy(this._elInput, function(el) { return el.tagName.toLowerCase() == 'thead'; });
	this.elBody = Dom.getFirstChildBy(this._elInput, function(el) { return el.tagName.toLowerCase() == 'tbody'; });

	var fnOnSubmit = function()
	{
		var aRows = Dom.getChildrenBy(this.elBody, function(el) { return el.tagName.toLowerCase() == 'tr'; }),
			aRowIDs = U.Map(aRows, function (elRow) { return Dom.getFirstChild(elRow).id.replace(this._elInput.id + '__', ''); }, this);

		if (this.GetAttribDefault('NoRowValues'))
		{
			aRowIDs = U.Keys(aRows);
		}

		this.oForm.AddHiddenField(this.sName + '_rowvalues', aRowIDs);
	}
	this.oForm.AddEvent(fnOnSubmit, 'submit', this);

	return true;
}

oWidget.prototype._FindContainers = function()
{
	var elTR = Dom.get('tr_' + this.GetAttrib('WidgetID'));

	if (elTR)
	{
		this.elContainer = elTR;
	}

	return ZCWidget.Group_Matrix.superclass._FindContainers.apply(this, arguments);
}

oWidget.prototype.GetValue = function()
{
	var aValue = {};

	U.ForEach(ZCWidget.Group_Matrix.superclass.GetValue.call(this), function(aKeyValue, sKey)
	{
		if (sKey == 'AddValue' || sKey == 'AddButton')
			return;

		aValue[sKey.substr(6)] = aKeyValue;
	});

	return aValue;
}
	
oWidget.prototype.SetValue = function()
{
	throw new Error("SetValue not implemented for Group_Matrix widgets");
}

/**
 * Adds a row to the table matrix
 * @param {String} sRowID the id for the row
 * @param {String} sCaption the caption HTML for the row
 * @param {Array} aCells array of HTML for each cell
 * @param {Object} oConfig the config array for the new group widget
 */
oWidget.prototype.AddRow = function(sRowID, sCaption, aCells, oConfig)
{
	var elRow = document.createElement('tr'),
		elCaption = document.createElement('th'),
		sCaptionID = this._elInput.id + '__' + sRowID;

	// check it doesn't exist already
	if (Dom.get(sCaptionID))
		return;

	if (!this.GetAttribDefault('NoRowValues'))
	{
		elCaption.scope = 'row';
		elCaption.id = sCaptionID;
		elCaption.innerHTML = sCaption;

		elRow.appendChild(elCaption);
	}

	U.ForEach(aCells, function(sCellHTML)
	{
		var elCell = document.createElement('td');
		elCell.innerHTML = sCellHTML;
		elRow.appendChild(elCell);
	});

	this.elBody.appendChild(elRow);
	Dom.removeClass(this.elHeading, HIDE);

	if (oConfig)
		this.AddChildWidget('gmrow_' + sRowID, oConfig);
}

/**
 * Removes a row from the table matrix
 * @param {String} sRowID the id of the row to remove
 * @return boolean false if the row was not found
 */
oWidget.prototype.RemoveRow = function(sRowID)
{
	var elTH, elRow, sPrefix = this._elInput.id + '__',
		oAnim, fnFinishAnim;

	if (sRowID.indexOf(sPrefix) == -1)
		sRowID = sPrefix + sRowID;

	elTH = Dom.get(sRowID);
	if (elTH)
	{
		elRow = elTH.parentNode;
		fnFinishAnim = function()
		{
			elRow.parentNode.removeChild(elRow);
			if (!Dom.getFirstChildBy(this.elBody, function(el) { return el.tagName.toLowerCase() == 'tr'; }))
			{
				Dom.addClass(this.elHeading, HIDE);
			}
		}
		if (YAHOO.util.Anim)
		{
			oAnim = new YAHOO.util.Anim(elRow, { opacity: { from: 100, to: 0 } }, 0.5);
			oAnim.onComplete.subscribe(fnFinishAnim, this, true);
			oAnim.animate();
		}
		else
		{
			fnFinishAnim.call(this);
		}
	}

	this.RemoveChildWidget('gmrow_' + sRowID.replace(sPrefix, ''));
}

/**
 * Finds the ID of the row containing the given element.
 * @param {HTMLElement} elFind the element to find
 * @return {string} the id of the row containing that element, or undefined if none was found.
 */
oWidget.prototype.FindRowIDWithElement = function(elFind)
{
	var elRow, sRowID,
		sPrefix = this._elInput.id + '__';

	while (Dom.isAncestor(this._elInput, elFind))
	{
		elRow = Dom.getAncestorByTagName(elFind, 'tr');
		sRowID = Dom.getFirstChild(elRow).id;

		if (sRowID.indexOf(sPrefix) < 0)
		{
			// try the parents of the tr we found, in case we have nested tables in our matrix
			elFind = elRow.parentNode;
		}
		else
		{
			return sRowID.replace(sPrefix, '');
		}
	}
	
	return undefined;
}

/**
 * Gets the caption of the row with the given ID.
 * @param {string} the id of a row
 * @return {string} the caption of that row
 */
oWidget.prototype.GetRowCaption = function(sRowID)
{
	var elTH = Dom.get(this._elInput.id + '__' + sRowID);
	if (elTH)
		return (elTH.textContent || elTH.innerText);
	else
		return sRowID;
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class TextInsertion
 */
oWidget = ZCWidget.Create('TextInsertion', 'Core', oWidget);
oWidget.prototype.CustomSetupEnd = function()
{
	ZC.JSManager.GetEvent('ManagerInit').subscribe(function()
	{
		var sDestination, fnInsertClick;

		sDestination = this.GetAttrib('Destination');
		// first try this widget's parent, then this widget's form, then finally search the whole page
		this.oDestWidget = this.oParent.GetWidget(sDestination)
						|| ((this.oParent != this.oForm) && this.oForm.GetWidget(sDestination))
						|| ZC.JSManager.GetWidget(sDestination);

		if (!this.oDestWidget)
			return;

		var fnInsertClick = function()
		{
			this.oDestWidget.InsertText(decodeURIComponent(this.GetWidget('SelectTag').GetValue()));
		}

		this.GetWidget('Insert').AddEvent(fnInsertClick, 'click', this);
		this.GetWidget('SelectTag').AddEvent(fnInsertClick, 'dblclick', this);
	}, this, true);

	return true;
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Submit
 */
oWidget = ZCWidget.Create('Submit');
oWidget.prototype.CustomSetupEnd = function()
{
	var oLoadingPanel, sLoadingMessage, aButtonDef, sConfirmMessage;

	if (!this._elInput)
		return false;

	if (aButtonDef = this.GetAttribDefault('YUIButtonDef')) // assignment
	{
		this.oButtonWrapper = new U.YUIButtonWrapper(this._elInput, aButtonDef, this);
		this._elInput = this.oButtonWrapper.oYUIButton.get('element');
	}

	if (sLoadingMessage = this.GetAttribDefault('LoadingMessage')) // assignment
	{
		// based on a YUI example - displays a modal "loading" dialog when the submit button is clicked
		oLoadingPanel = new YAHOO.widget.Panel(Dom.generateId(this._elInput) + "wait", {
				fixedcenter:true,
				close:false,
				draggable:false,
				zindex:40000,
				modal:true,
				visible:false
			}
		);

		oLoadingPanel.setHeader(sLoadingMessage);
		oLoadingPanel.setBody('<img style="margin: 0 auto; display: block;" width="220" height="19" src="/zc/images/ajax-loader-bar.gif">');
		oLoadingPanel.render(document.body);

		Evt.on(this._elInput, 'click', function() { oLoadingPanel.show(); });
	}

	if (sConfirmMessage = this.GetAttribDefault('Confirm'))
	{
		var fnSubmitHandler = function(oEvent) {
			if (!confirm(sConfirmMessage)) 
				Evt.stopEvent(oEvent); 
		};

		if (this.oButtonWrapper)
		{
			Evt.on(this.oButtonWrapper.oYUIButton.getForm(), 'submit', fnSubmitHandler);
		}
		else
		{
			Evt.on(this._elInput.form, 'submit', fnSubmitHandler);
		}
	}

	this.AddEvent(function() { if (this.oForm) this.oForm.SetSelectedEndWidget(this); }, 'click', this, 50);

	return true;
}

oWidget.prototype.Destruct = function()
{
	ZCWidget.Submit.superclass.Destruct.apply(this, arguments);

	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.Destruct();
		this.oButtonWrapper = null;
	}
}

oWidget.prototype.AttribMethod_value = function(AttribValue)
{
	this._elInput.value = AttribValue;
	if (this.oButtonWrapper)
		this.oButtonWrapper.oYUIButton.set('label', AttribValue);
}

oWidget.prototype.Enable = function(bEnable, sEnableClass, sDisableClass)
{
	ZCWidget.Submit.superclass.Enable.apply(this, arguments);

	if (this.oButtonWrapper)
		this.oButtonWrapper.Enable(bEnable);
}

/**
 * Simulates a click on this widget's button.
 */
oWidget.prototype.Click = function()
{
	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.oYUIButton.fireEvent('click', {});
	}
	else
	{
		this._elInput.click();
	}
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Button
 */
oWidget = ZCWidget.Create('Button');
oWidget.prototype.CustomSetupEnd = function()
{
	var aButtonDef;

	if (!this._elInput)
		return false;

	if (aButtonDef = this.GetAttribDefault('YUIButtonDef')) // assignment
	{
		this.oButtonWrapper = new U.YUIButtonWrapper(this._elInput, aButtonDef, this);
		this._elInput = this.oButtonWrapper.oYUIButton.get('element');
	}

	this.AddEvent(function() { if (this.oForm) this.oForm.SetSelectedEndWidget(this); }, 'click', this, 50);

	return true;
}

oWidget.prototype.Destruct = function()
{
	ZCWidget.Button.superclass.Destruct.apply(this, arguments);

	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.Destruct();
		this.oButtonWrapper = null;
	}
}

oWidget.prototype.Enable = function(bEnable)
{
	ZCWidget.Button.superclass.Enable.call(this, bEnable);
	if (this.oButtonWrapper)
		this.oButtonWrapper.Enable(bEnable);
}

/**
 * Simulates a click on this widget's button.
 */
oWidget.prototype.Click = function()
{
	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.oYUIButton.fireEvent('click', {});
	}
	else
	{
		this._elInput.click();
	}
}

oWidget.prototype._WireUpEvent = function(sEvent)
{
	if (this.oButtonWrapper && sEvent == 'click')
	{
		// handled by the button wrapper class
		return true;
	}

	return ZCWidget.Button.superclass._WireUpEvent.apply(this, arguments);
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.Button
 * @class AutoPopulate
 */
ZCWidget.Create('AutoPopulate', 'Core', ZCWidget.Button);

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Reset
 */
oWidget = ZCWidget.Create('Reset');
oWidget.prototype.CustomSetupEnd = function()
{
	var aButtonDef;

	if (!this._elInput)
		return false;

	if (aButtonDef = this.GetAttribDefault('YUIButtonDef')) // assignment
	{
		this.oButtonWrapper = new U.YUIButtonWrapper(this._elInput, aButtonDef, this);
		this._elInput = this.oButtonWrapper.oYUIButton.get('element');
	}

	this.AddEvent(function(oEvent) 
	{ 
		var sConfirmMessage = this.GetAttribDefault('Confirm');
		if (sConfirmMessage)
		{
			if (!confirm(sConfirmMessage))
			{
				Evt.stopEvent(oEvent);
				return;
			}
		}

		if (this.oForm) 
		{
			this.oForm.Reset();
			Evt.stopEvent(oEvent);
		}
	}, 'click', this, 10);

	return true;
}

oWidget.prototype.Destruct = function()
{
	ZCWidget.Reset.superclass.Destruct.apply(this, arguments);

	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.Destruct();
		this.oButtonWrapper = null;
	}
}

oWidget.prototype.Enable = function(bEnable)
{
	ZCWidget.Reset.superclass.Enable.call(this, bEnable);
	if (this.oButtonWrapper)
		this.oButtonWrapper.Enable(bEnable);
}

/**
 * Simulates a click on this widget's button.
 */
oWidget.prototype.Click = function()
{
	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.oYUIButton.fireEvent('click', {});
	}
	else
	{
		this._elInput.click();
	}
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Button_Clear
 */
oWidget = ZCWidget.Create('Button_Clear');
oWidget.prototype.CustomSetupStart = function()
{
	var elButton, oAttribs, aButtonDef, fnButtonClickHandler, i, iMax;

	if (!this._elInput)
		return false;

	elButton = document.createElement('input');

	oAttribs = this._elInput.attributes;
	// copy attribs from placeholder tag
	for (i = 0, iMax = oAttribs.length; i < iMax; i++)
	{
		Dom.setAttribute(elButton, oAttribs[i].nodeName, oAttribs[i].nodeValue);
	}
	elButton.type = 'button';
	elButton.value = this.aDef.Caption || 'Clear';
	fnButtonClickHandler = function(event)
	{
		if (this.oForm)
			this.oForm.Clear();
		Evt.stopEvent(event);
	}

	Evt.on(elButton, 'click', fnButtonClickHandler, this, true);

	this._elInput.parentNode.replaceChild(elButton, this._elInput);
	this._elInput = elButton;

	if (aButtonDef = this.GetAttribDefault('YUIButtonDef')) // assignment
	{
		this.oButtonWrapper = new U.YUIButtonWrapper(this._elInput, aButtonDef, this);
		this._elInput = this.oButtonWrapper.oYUIButton.get('element');
	}

	this.AddEvent(function() { if (this.oForm) this.oForm.SetSelectedEndWidget(this); }, 'click', this, 50);

	return true;
}

oWidget.prototype.Destruct = function()
{
	ZCWidget.Button_Clear.superclass.Destruct.apply(this, arguments);

	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.Destruct();
		this.oButtonWrapper = null;
	}
}

oWidget.prototype.Enable = function(bEnable)
{
	ZCWidget.Button_Clear.superclass.Enable.call(this, bEnable);
	if (this.oButtonWrapper)
		this.oButtonWrapper.Enable(bEnable);
}

/**
 * Simulates a click on this widget's button.
 */
oWidget.prototype.Click = function()
{
	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.oYUIButton.fireEvent('click', {});
	}
	else
	{
		this._elInput.click();
	}
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Link
 */
oWidget = ZCWidget.Create('Link');
oWidget.prototype.CustomSetupEnd = function()
{
	var aButtonDef, sConfirmMessage, fnClickHandler;

	if (!this._elInput)
	{
		YAHOO.log('CustomSetupEnd: no _elInput', 'warn', 'Link ' + this.sName);
		return false;
	}

	if (aButtonDef = this.GetAttribDefault('YUIButtonDef')) // assignment
	{
		this.oButtonWrapper = new U.YUIButtonWrapper(this._elInput, aButtonDef, this);
		this._elInput = this.oButtonWrapper.oYUIButton.get('element');
	}

	if (sConfirmMessage = this.GetAttribDefault('Confirm'))
	{
		fnClickHandler = function(oEvent) {
			if (!confirm(sConfirmMessage)) 
				Evt.stopEvent(oEvent); 
		};

		this.AddEvent(fnClickHandler, 'click', this, 10);
	}

	this.AddEvent(function() { if (this.oForm) this.oForm.SetSelectedEndWidget(this); }, 'click', this, 50);

	return true;
}

oWidget.prototype.Destruct = function()
{
	ZCWidget.Link.superclass.Destruct.apply(this, arguments);

	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.Destruct();
		this.oButtonWrapper = null;
	}
}

oWidget.prototype.Enable = function(bEnable)
{
	ZCWidget.Link.superclass.Enable.call(this, bEnable);
	if (this.oButtonWrapper)
	{
		var oYUIButton = this.oButtonWrapper.oYUIButton;

		// for some reason YUI doesn't apply the disabled attrib to link
		// buttons, but we can achieve the same result ourselves
		if (this.IsEnabled())
		{
			oYUIButton.removeStateCSSClasses("disabled");
			if (L.isUndefined(this._oldOnClick))
			{
				// work around YUI bug that prevents the onclick handler from
				// being removed by using a blank function as the onclick event
				// handler
				oYUIButton.set('onclick', { fn: function() {} });
			}
			else
			{
				oYUIButton.set('onclick', this._oldOnClick);
			}
		}
		else 
		{
			if (oYUIButton.hasFocus()) 
				oYUIButton.blur();

			oYUIButton.addStateCSSClasses("disabled");

			oYUIButton.removeStateCSSClasses("hover");
			oYUIButton.removeStateCSSClasses("active");
			oYUIButton.removeStateCSSClasses("focus");

			this._oldOnClick = oYUIButton.get('onclick');
			oYUIButton.set('onclick', {
				scope: oYUIButton,
				fn: function(oEvent)
				{
					Evt.stopEvent(oEvent);
					this.blur();
				}
			});
		}
	}
}

oWidget.prototype._WireUpEvent = function(sEvent)
{
	if (sEvent == 'click' && this.oButtonWrapper)
	{
		// do not wire up click events to the widget's element if YUI buttons are in use...
		return;
	}

	return ZCWidget.Link.superclass._WireUpEvent.apply(this, arguments);
}

oWidget.prototype.AttribMethod_href = function(sValue)
{
	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.oYUIButton.set('href', sValue);
	}
	else
	{
		this._elInput.href = sValue;
	}
}

/**
 * Simulates a click on this widget's button.
 */
oWidget.prototype.Click = function()
{
	if (this.oButtonWrapper)
	{
		this.oButtonWrapper.oYUIButton.fireEvent('click', {});
	}
	else
	{
		this._elInput.click();
	}
}

/**
 * Adds a nc=<timestamp> option to the link URL to prevent the browser from caching the target.
 */
oWidget.prototype.PreventCaching = function()
{
	var sURL = this.GetAttribDefault('href', this._elInput.href);

	sURL = ZC.JSManager.URL({ nocache: (new Date()).getTime() }, sURL);
	this.SetAttrib('href', sURL);
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Links_Button
 */
oWidget = ZCWidget.Create('Links_Button');
oWidget.prototype.CustomSetupEnd = function()
{
	var aButtonDef, elCaption, elMenu, elButton, sDefaultHREF, fnRenderToBody, sConfirmMessage;

	if (!this._elInput)
		return false;

	aButtonDef = this.GetAttribDefault('YUIButtonDef');
	elCaption = Dom.getFirstChild(this._elInput);
	Dom.removeClass(elCaption, YUIPECONTENT);

	if (elCaption.tagName.toLowerCase() == 'a')
	{
		sDefaultHREF = elCaption.href;
	}

	elButton = document.createElement('input');
	elButton.type = 'button';
	elButton.value = (elCaption.textContent || elCaption.innerText);
	elButton.className = elCaption.className;
	elButton.id = elCaption.id;
	elCaption.parentNode.replaceChild(elButton, elCaption);

	Dom.removeClass(this._elInput.id + '_menu', YUIPECONTENT);
	this.oYUIButton = new YAHOO.widget.Button(elButton, {
		type: (sDefaultHREF ? 'split' : 'menu'),
		menu: this._elInput.id + '_menu'
	});

	// work-around for z-index issue: render menu to document body element.
	this.oYUIButton.getMenu().subscribe("render",function () {
		document.body.appendChild(this.element);
	});

	if (sConfirmMessage = this.GetAttribDefault('Confirm'))
	{
		fnClickHandler = function(oEvent) {
			if (!confirm(sConfirmMessage)) 
			{
				Evt.stopEvent(oEvent);
				this.bCancelled = true;
			}
			else
			{
				this.bCancelled = false;
			}
		};

		this.AddEvent(fnClickHandler, 'click', this, 10);
	}

	// handle default link + LinksDisabled attrib
	// doesn't use the YUI split/menu button disabled attrib as that stops the menu from appearing
	if (sDefaultHREF && !this.GetAttribDefault('LinksDisabled'))
	{
		this.AddEvent(function(oEvent) {
			if (this.bCancelled)
				return;

			var sTarget = Dom.getAttribute(this._elInput, 'target'), oNewWin;
			if (sTarget && sTarget != '_self')
			{
				var oNewWin = window.open(sDefaultHREF, sTarget);
				if (oNewWin)
					return;

				// failed to open new window (popup blocker?)
				YAHOO.log("window.open returned null, fallback to current window", "debug", "Links_Button");
			}
			
			window.location = sDefaultHREF; 
		}, 'DefaultLink', this, 500);
	}
	else if (this.GetAttribDefault('LinksDisabled'))
	{
		this.oYUIButton.addClass('yui-button-disabled');
		this.oYUIButton.getMenu().cfg.setProperty('disabled', true);
	}
	return true;
}

oWidget.prototype._WireUpEvent = function(sEvent)
{
	switch (sEvent)
	{
		case 'click':
			this.oYUIButton.getMenu().subscribe(sEvent, function(sType, aArgs) { 
				this._EventDispatcher(sEvent, aArgs[0]); 
			}, this, true);

			// fall through
		case 'DefaultLink':
			if (this.oYUIButton.get('type') == 'menu')
				return;

			this.oYUIButton.on('click', function(oDOMEvent) { 
				this._EventDispatcher(sEvent, oDOMEvent); 
			}, this, true);
			break;

		default:
			this.oYUIButton.on(sEvent, function(oDOMEvent) { 
				this._EventDispatcher(sEvent, oDOMEvent); 
			}, this, true);
			break;
	}
}

oWidget.prototype.Destruct = function()
{
	ZCWidget.Links_Button.superclass.Destruct.apply(this, arguments);

	this.oYUIButton = null;
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class MenuButton
 */
oWidget = ZCWidget.Create('MenuButton');
oWidget.prototype.CustomSetupStart = function()
{
	if (this.GetAttribDefault('Widgets', []).length < 2)
		return false;

	// get child widget IDs
	var elSelect = Dom.get(this.aDef.Widgets.MenuOptions.ID),
		elSubmit = Dom.get(this.aDef.Widgets.MenuSubmit.ID), elNewSubmit, elDefault,
		fnMenuItemHandler, fnMakeMenuItems,
		sLoadingMessage, oLoadingPanel,
		mMenu, aMenuGroupTitles = []; // will either be a reference to the select widget or an array

	if (!elSelect || !elSubmit)
		return false;

	this._elInput = elSelect;

	var sType;
	if (!this.AttribIsset('Default'))
	{
		sType = "menu";
		for (var i = 0, iMax = elSelect.options.length; i < iMax; i++)
		{
			if (elSelect.options[i].value == '')
			{
				elSelect.removeChild(elSelect.options[i]);
				break;
			}
		}
	}
	else
	{
		sType = "split";
	}

	if (!this.AttribIsset('Options'))
	{
		// simple menu - pass select widget directly
		mMenu = elSelect;
	}
	else
	{
		// change button type to "button" to stop YUI from automatically
		// submitting the form when selecting an item in the menu.
		//
		// note: we have to clone the existing node and change the type before
		// replacing the old node, as IE fails if you just change the type of
		// an existing input.
		elNewSubmit = document.createElement('input');
		elNewSubmit.type = 'button';
		elNewSubmit.id = elSubmit.id;
		elNewSubmit.name = elSubmit.name;
		elNewSubmit.value = elSubmit.value;
		elSubmit.parentNode.replaceChild(elNewSubmit, elSubmit);
		elSubmit = elNewSubmit;

		// menu can't be represented as a simple select widget, generate menu items
		fnMenuItemHandler = function(sType, aArgs, oObj)
		{
			var bResult = true,
				oButton = oObj.Widget.oYUIMenuButton,
				oHiddenField;

			// scope is the selected menu item
			elSelect.value = this.value;

			// create a hidden field to let the server know this button was clicked
			oObj.Widget.oForm.AddHiddenField(oButton.get('name'), oButton.get('value'));

			// fire general widget click events
			oObj.Widget._FireEventHandlers('click', this);

			// individual options can have click events too
			if (oObj.Events)
			{
				U.ForEach(oObj.Events, function (sEvent)
				{
					bResult = bResult && ZC.JSManager.GetEvent(oObj.Widget._InternalEventName(sEvent)).fire(this, oObj.Widget);
				}, this);
			}
			if (oObj.Widget.GetAttribDefault('Submit', true) && bResult && oObj.Submit)
			{
				oObj.Widget.bSubmitted = true;
				oButton.submitForm();
			}

			return bResult;
		}
		fnMakeMenuItems = function(aOptions)
		{
			var aMenu = [];

			U.ForEach(aOptions, function(mOption, mKey)
			{
				var aSubMenu,
					aItemDef = { value: mKey, onclick: { fn: fnMenuItemHandler, obj: { Widget: this, Submit: true } } };

				if (L.isObject(mOption))
				{
					aItemDef.onclick.obj.Submit = L.isUndefined(mOption.Submit) || mOption.Submit;
					aItemDef.onclick.obj.Events = !L.isUndefined(mOption.Events) ? mOption.Events : false;

					if (!L.isUndefined(mOption.Group))
					{
						// groups are just arrays of menu items

						aItemDef = fnMakeMenuItems.call(this, mOption.Options);
						if (L.isString(aItemDef.Group))
						{
							aMenuGroupTitles.push(aItemDef.Group);
						}
						else
						{
							aMenuGroupTitles.push(false);
						}
					}
					else
					{
						aItemDef.text = mOption.Text || '(empty)';

						if (!L.isUndefined(mOption.Options))
						{
							if (!L.isUndefined(mOption.Selectable) && !mOption.Selectable)
							{
								delete aItemDef.onclick;
							}

							aSubMenu = fnMakeMenuItems.call(this, mOption.Options);
							if (aSubMenu.length)
								aItemDef.submenu = { id: Dom.generateId(), itemdata: aSubMenu };
						}
					}
				}
				else
				{
					aItemDef.text = mOption || '(empty)';
				}

				aMenu.push(aItemDef);
			}, this);

			return aMenu;
		}

		Dom.addClass(elSelect, HIDE);
		mMenu = fnMakeMenuItems.call(this, this.GetAttrib('Options'));
	}

	// workaround YUI bug #1909677
	if (YAHOO.env.ua.gecko)
		elSubmit.type = 'button';

	this.oYUIMenuButton = new YAHOO.widget.Button(
        elSubmit,
        {
            type: sType,
            menu: mMenu
        }
    );
	U.ForEach(aMenuGroupTitles, function(sTitle, iIndex) { if (sTitle) this.oYUIMenuButton.setItemGroupTitle(sTitle, iIndex); }, this);

	if (this.AttribIsset('Default'))
	{
		this.oYUIMenuButton.subscribe('click', function(oEvent) { 
			this.SetValue(this.GetAttrib('Default'));
			this._FireEventHandlers('click', oEvent); 
		}, this, true);

		// check that the Default attrib refers to an element in the select, if not then add it.
		if (!U.Some(elSelect.options, function (elOpt) { return (elOpt.value == this.GetAttrib('Default')); }, this))
		{
			elDefault = document.createElement('option');
			elDefault.value = this.GetAttrib('Default');
			elSelect.appendChild(elDefault);
		}
	}

	if (sLoadingMessage = this.GetAttribDefault('LoadingMessage')) // assignment
	{
		// based on a YUI example - displays a modal "loading" dialog when the submit button is clicked
		oLoadingPanel = new YAHOO.widget.Panel(Dom.generateId(this._elInput) + "wait", {
				fixedcenter:true,
				close:false,
				draggable:false,
				zindex:40000,
				modal:true,
				visible:false
			}
		);

		oLoadingPanel.setHeader(sLoadingMessage);
		oLoadingPanel.setBody('<img style="margin: 0 auto; display: block;" width="220" height="19" src="/zc/images/ajax-loader-bar.gif">');
		oLoadingPanel.render(document.body);

		this.oYUIMenuButton.getMenu().subscribe('click', function() { if (this.bSubmitted) oLoadingPanel.show(); }, this, true);
	}

	this.AddEvent(function() { 
		if (this.oForm)
			this.oForm.SetSelectedEndWidget(this); 

		// the workaround above means that the YUI code no longer submits the form for us, so we now have to do it.
		// submitForm is marked as "protected", but JS doesn't enforce this. It could potentially be broken by a YUI update,
		// but hopefully they'll fix the bug for the next update anyway…

		if (YAHOO.env.ua.gecko)
		{
			L.later(0, this.oYUIMenuButton, function() { 
				// need to set back to submit for YUI to add the button name to the form
				this.get('srcelement').type = 'submit';
				this.submitForm(); 

				// back to button again to keep it working in case form submission doesn't cause a page reload
				this.get('srcelement').type = 'button'; 
			});
		}
	}, 'click', this);

	// don't want to create child widget objects
	this.aDef.Widgets = [];

	return true;
}

oWidget.prototype.Destruct = function()
{
	ZCWidget.MenuButton.superclass.Destruct.apply(this, arguments);

	this.oYUIMenuButton = null;
}

oWidget.prototype._WireUpEvent = function(sEvent)
{
	switch (sEvent)
	{
		case 'click':
			this.oYUIMenuButton.getMenu().subscribe(sEvent, function(sType, aArgs) { 
				this._EventDispatcher(sEvent, aArgs[0]); 
			}, this, true);

			// fall through
		case 'DefaultLink':
			if (this.oYUIMenuButton.get('type') == 'menu')
				return;

			this.oYUIMenuButton.on('click', function(oDOMEvent) { 
				// work around a YUI bug: if the menu is opened, then closed,
				// the default link will return the first item in the menu.
				this.oYUIMenuButton.set('selectedMenuItem', null);

				this._EventDispatcher(sEvent, oDOMEvent); 
			}, this, true);
			break;

		default:
			this.oYUIMenuButton.on(sEvent, function(oDOMEvent) { 
				this._EventDispatcher(sEvent, oDOMEvent); 
			}, this, true);
			break;
	}
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Password
 */
oWidget = ZCWidget.Create('Password');
oWidget.prototype.GetTextValue = function()
{
	return U.StrRepeat('*', this.GetValue().length);
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.Password
 * @class Password_Change
 */
oWidget = ZCWidget.Create('Password_Change', 'Core', oWidget);
oWidget.prototype.GetValue = function()
{
	return this._elInput[0].value;
}
oWidget.prototype._FindElements = function()
{
	var sName;

	sName = this._WidgetNameToID();
	this._elInput = [Dom.get(sName), Dom.get(sName + '_confirm')];
	this._FindContainers();
}
oWidget.prototype.Validate = function()
{
	var sVal1, sVal2;

	sVal1 = this._elInput[0].value;
	sVal2 = this._elInput[1].value;

	if (sVal1 && sVal2 && sVal1 != sVal2)
	{
		this.SetValid(false, _GT('The two passwords do not match'));
		return false;
	}
	return oWidget.superclass.Validate.call(this);
}

/**
 * TODO: move the code from linkedselect.js and from the HTML into here (or probably Widget/LinkedSelect.js).
 * This code is currently quite basic in order to make the Clear button work.
 * The initialisation calls have now been moved from <script> tags to this
 * file, so that linked selects work when they're added after the initial page
 * load (e.g. an AJAX dialog).
 *
 * LinkedSelects should probably end up using Select child widgets on the client-side as well.
 *
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class LinkedSelect
 */

oWidget = ZCWidget.Create('LinkedSelect');
oWidget.prototype.bChangeRODisplay = false; // bit of a hack for now to stop breakage when one select is RO. will be fixed when new linkedselects are done.
oWidget.prototype.CustomSetupEnd = fnSetupCopyOptionClass;

oWidget.prototype.CustomSetupStart = function()
{
	this.sSeparator = this.GetAttrib('Separator');
	if (this._elInput.length)
	{
		initLinkedSelect(this._elInput);
		return true;
	}

	return false;
}

oWidget.prototype._FindElements = function()
{
	var sBaseName, i, elInput;

	sBaseName = this._WidgetNameToID();
	i = this.GetAttribDefault('FirstVisibleLevel', 0);

	this._elInput = [];
	do
	{
		elInput = Dom.get(sBaseName + '.' + i);
		if (elInput)
			this._elInput.push(elInput);
		i++;
	}
	while (elInput);

	this._FindContainers();
}

oWidget.prototype.Clear = function()
{
	U.ForEach(this._elInput, function(elSelect)
	{
		if (U.InArray(elSelect.options[0].value, ['Any', '__Any__', '']))
			elSelect.selectedIndex = 0;

		if (L.isFunction(elSelect.onchange))
			elSelect.onchange();
	});
}

oWidget.prototype.GetValue = function()
{
	var aVal = this.GetCompoundValue();
	if (L.isUndefined(aVal))
		return undefined;
		
	// The compound value is the value of all the selects to this point in an array (was all values
	// concatentated with hypens. GetValue returns the value of the last select only
	// Call GetCompoundValue directly if you also need to know the value of the other selections
	return aVal.pop();
	/*
	if(mVal.lastIndexOf('-') != -1)
	{
		mVal = mVal.substring(mVal.lastIndexOf('-') + 1);
	}
	return mVal;
	*/	
}

/**
 * The option values in second through to the last select were created by concatenating the previously
 * selected values from the earlier selects with hyphens. e.g. third select would contain a value like 123-44-234.
 * Default behaviour (GetValue function) is to return the last component only, however this function
 * returns the whole value from the final select.
 */
oWidget.prototype.GetCompoundValue = function()
{
	var mVal;
	if(L.isUndefined(this._elInput) || L.isUndefined(this._elInput[this._elInput.length -1]))
	{
		return undefined;
	}
	
	mVal = this._elInput[this._elInput.length -1].value;
	aValues = mVal.split(this.GetAttrib('Separator'));
	return aValues;
}

oWidget.prototype.GetTextValue = function()
{
	if(L.isUndefined(this._elInput))
	{
		return undefined;
	}

	var i=0,sText=''; 
	while(i < this._elInput.length)
	{
		if (L.isUndefined(this._elInput[i]))
			return undefined;
			
		sText += this._elInput[i].options[this._elInput[i].selectedIndex].text+' ';
		i++;
	}
	return sText;
}

oWidget.prototype.SetValue = function(mValue)
{
	var oRE, i, iMax, iFirstVisibleLevel = this.GetAttrib('FirstVisibleLevel'), elSelect, aProcessedValue = [], sSeparator = this.GetAttrib('Separator');

	if (L.isString(mValue) || L.isNumber(mValue))
	{
		// convert numbers to strings
		mValue = String(mValue);

		// If we have been supplied the final value only then deduce the full value
		if (this._elInput.length > 0 && mValue.indexOf(sSeparator) == -1)
		{
			oRE = new RegExp(sSeparator + mValue + '$');
			elSelect = this._elInput[this._elInput.length - 1];

			for (i = 0, iMax = elSelect.options.length; i < iMax; i++)
			{
				if (elSelect.options[i].value.match(oRE))
				{
					mValue = elSelect.options[i].value;
					break;
				}
			}
		}
		mValue = mValue.split(sSeparator);

		// Backwards compatibility
		// If we've found a value, then after the explode above $xValue should contain at least two values.
		// If it doesn't then the value may be hyphen-separated, so try exploding with a hyphen.
		if (mValue.length == 1 && mValue[0].indexOf('-') != -1)
		{
			mValue = mValue[0].split('-');
		}
	}

	if (L.isArray(mValue))
	{
		for (i = 0, iMax = mValue.length; i < iMax; i++)
		{
			if (i < iFirstVisibleLevel)
			{
				aProcessedValue.push(mValue[i]);
			}
			else if (!L.isUndefined(this._elInput[i - iFirstVisibleLevel]))
			{
				aProcessedValue.push(mValue[i]);
				this._elInput[i - iFirstVisibleLevel].value = aProcessedValue.join(sSeparator);
			}
			else
			{
				this.SetValid(false);
			}
		}
	}
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.LinkedSelect
 * @class LinkedSelect_AllWidgetValues
 */
oChildWidget = ZCWidget.Create('LinkedSelect_AllWidgetValues', 'Core', ZCWidget.LinkedSelect);
oChildWidget.prototype.GetValue = ZCWidget.LinkedSelect.prototype.GetCompoundValue;
		
/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.LinkedSelect
 * @class LinkedSelect_AllowBlank
 */
ZCWidget.Create('LinkedSelect_AllowBlank', 'Core', ZCWidget.LinkedSelect);
/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.LinkedSelect
 * @class LinkedSelect_LinkedSearch
 */
ZCWidget.Create('LinkedSelect_LinkedSearch', 'Core', ZCWidget.LinkedSelect);
/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.LinkedSelect
 * @class LinkedSelect_Multiple
 */
oChildWidget = ZCWidget.Create('LinkedSelect_Multiple', 'Core', ZCWidget.LinkedSelect);
oChildWidget.prototype.CustomSetupStart = function()
{
	if (!oChildWidget.superclass.CustomSetupStart.apply(this, arguments))
		return false;

	var sFormAndName, sCollectionSelectName, elSelect, elDeselect, fnSelect, fnDeselect;

	sFormAndName = this.oForm.sName + '.' + this.sName;	
	sCollectionSelectName = sFormAndName + '.collection';

	this.elCollection = Dom.get(sCollectionSelectName);

	LinkedMultipleSelect_SynchroniseHiddenField(sCollectionSelectName, sFormAndName);
	LinkedSelect_RegisterMultiple(sFormAndName + '.' + this.GetAttrib('MaxLevel'), sCollectionSelectName);

	fnSelect = function()
	{
		LinkedMultipleSelect_AddSelected(this._elInput, sCollectionSelectName, sFormAndName);
		this._EventDispatcher('change', null);
	}
	fnDeselect = function()
	{
		LinkedMultipleSelect_RemoveSelected(this._elInput, sCollectionSelectName, sFormAndName);
		this._EventDispatcher('change', null);
	}
	Evt.on(this._WidgetNameToID() + '_Select', 'click', fnSelect, this, true);
	Evt.on(this._WidgetNameToID() + '_Deselect', 'click', fnDeselect, this, true);

	return true;
}

oChildWidget.prototype._WireUpEvent = function(sEvent)
{
	if (sEvent == 'change')
		return;

	return oChildWidget.superclass._WireUpEvent.apply(this, arguments);
}

var fnLinkedSelectMultiple_GetValue = function (bTextValue)
{
	if (!this.elCollection)
		return undefined;

	var aValues = [];

	U.ForEach(this.elCollection.options, function(oOpt)
	{
		aValues.push(bTextValue ? oOpt.text : oOpt.value);
	});
	return aValues;
}

oChildWidget.prototype.GetValue = function()
{
	return U.Map(fnLinkedSelectMultiple_GetValue.call(this, false), function(sVal) { return sVal.replace(/^.*-/, '') });
}

oChildWidget.prototype.GetTextValue = function()
{
	return fnLinkedSelectMultiple_GetValue.call(this, true);
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.LinkedSelect
 * @class SelectRelated
 */
oChildWidget = ZCWidget.Create('SelectRelated', 'Core', ZCWidget.LinkedSelect);
oChildWidget.prototype.CustomSetupStart = function()
{
	if (this._elInput.length)
	{
		initSelectRelated(this._elInput);
		return true;
	}
	return false;
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class DateTime_FromRange
 */
oWidget = ZCWidget.Create('DateTime_FromRange');
oWidget.prototype.CustomSetupStart = function()
{
	// convert From/To Date/Time attribs from seconds into Date objects
	U.ForEach(['FromDate','ToDate','FromTime','ToTime'], function(sAttrib)
	{
		if (!this.AttribIsset(sAttrib))
		{
			oWidget.superclass.SetAttrib.call(this, sAttrib, new Date());
		}
		else
		{
			var mValue = this.GetAttrib(sAttrib);
			if (L.isNumber(mValue) || mValue.match(/^\d+$/))
			{
				oWidget.superclass.SetAttrib.call(this, sAttrib, new Date(mValue * 1000));
			}
			else
			{
				oWidget.superclass.SetAttrib.call(this, sAttrib, new Date(mValue));
			}
		}
	}, this);	
	
	return true;
}
oWidget.prototype.GetValue = function()
{
	var oDateWidget = this.aChildWidgets.Date,
		oTimeWidget = this.aChildWidgets.Time,
		// JS cannot parse just a time on its own, add the epoch date for time-only widget.
		aDateValues = ['1970-01-01'], aTimeValues = [''], 
		aValues = [], sValue;

	if (!L.isUndefined(oDateWidget) && !L.isUndefined(oDateWidget.GetValue()))
		aDateValues = oDateWidget.GetValue();
	if (!L.isUndefined(oTimeWidget) && !L.isUndefined(oTimeWidget.GetValue()))
		aTimeValues = oTimeWidget.GetValue();

	U.ForEach(aDateValues, function(sDate)
	{
		U.ForEach(aTimeValues, function(sTime)
		{
			aValues.push(new Date(sDate + ' ' + sTime));
		});
	});

	if (this.GetAttribDefault('Multiple'))
	{
		return aValues;
	}
	else
	{
		return aValues[0];
	}
}

oWidget.prototype.GetTextValue = function()
{
	var mValue = this.GetValue(),
		fnDateFormat = YAHOO.util.Date.format,
		sDisplayFormat = this.GetAttrib('DisplayFormat');

	if (L.isUndefined(mValue))
	{
		return this.GetAttribDefault('DisplayValueWhenNull', '');
	}
	else if (L.isArray(mValue))
	{
		return U.Map(mValue, function(oDate) { return fnDateFormat(oDate, { format: sDisplayFormat }); })
			.join(this.GetAttribDefault('ValueSeparator', ', '));
	}
	else
	{
		return fnDateFormat(this.GetValue, { format: sDisplayFormat });
	}
}

oWidget.prototype.SetValue = function(mValue)
{
	var oDateWidget = this.aChildWidgets.Date,
		oTimeWidget = this.aChildWidgets.Time,
		fnDateFormat = YAHOO.util.Date.format,
		aDateValues = [], aTimeValues = [];

	if (this.GetAttribDefault('Multiple') && L.isArray(mValue))
	{
		if (oDateWidget)
		{
			aDateValues = U.Map(mValue, function(oDate) { fnDateFormat(oDate, "%Y-%m-%d"); });
			oDateWidget.SetValue(aDateValues);
		}
		if (oTimeWidget)
		{
			aTimeValues = U.Map(mValue, function(oDate) { fnDateFormat(oDate, "%H:%M"); });
			oTimeWidget.SetValue(aTimeValues);
		}
	}
	else
	{
		if (!(mValue instanceof Date))
		{
			// maybe its something that can be parsed into a date
			if (L.isNumber(mValue))
			{
				mValue = new Date(mValue * 1000); // seconds -> milliseconds
			}
			else
			{
				mValue = new Date(mValue);
			}
			if (!mValue)
				return;
		}

		if (oDateWidget)
			oDateWidget.SetValue(fnDateFormat(mValue, "%Y-%m-%d"));

		if (oTimeWidget)
			oTimeWidget.SetValue(fnDateFormat(mValue, "%H:%M"));
	}
}

oWidget.prototype.SetAttrib = function(sAttrib, mValue)
{
	var oDateWidget = this.aChildWidgets.Date,
		oTimeWidget = this.aChildWidgets.Time,
		fnDateFormat = YAHOO.util.Date.format,
		oDateOptions = {}, oTimeOptions = {}, mCurrentValue;

	oWidget.superclass.SetAttrib.apply(this, arguments);

	mCurrentValue = this.GetValue();	
	switch (sAttrib)
	{
		case 'DateOptions':
			if (oDateWidget) oDateWidget.SetAttrib('Options', mValue);
			break;
		case 'TimeOptions':
			if (oTimeWidget) oTimeWidget.SetAttrib('Options', mValue);
			break;

		case 'FromDate':
		case 'ToDate':
			if (!oDateWidget) return;

			oFromDate = this.GetAttrib('FromDate');
			oToDate = this.GetAttrib('ToDate');

			for (var oDate = new Date(oFromDate.getTime()); oDate <= oToDate; oDate.setTime(oDate.getTime() + 86400000))
			{
				oDateOptions[fnDateFormat(oDate, {format: "%Y-%m-%d"})] = fnDateFormat(oDate, {format: this.GetAttrib('DateFormat')});
			}

			oDateWidget.SetAttrib('Options', oDateOptions);
			return;

		case 'FromTime':
		case 'ToTime':
			if (!oTimeWidget) return;

			oFromTime = this.GetAttrib('FromTime');
			oToTime = this.GetAttrib('ToTime');

			for (var oDate = new Date(oFromTime.getTime()); oDate <= oToTime; oDate.setTime(oDate.getTime() + 86400000))
			{
				oTimeOptions[fnDateFormat(oDate, {format: "%H:%M"})] = fnDateFormat(oDate, {format: this.GetAttrib('TimeFormat')});
			}

			oTimeWidget.SetAttrib('Options', oTimeOptions);
			break;

		default: return;
	}

	this.SetValue(mCurrentValue);
}
/**
 * @namespace ZC.Core.Widget
 * @class Date_FromRange
 * @extends ZC.Core.Widget.DateTime_FromRange
 */
ZCWidget.Create('Date_FromRange', 'Core', oWidget);
/**
 * @namespace ZC.Core.Widget
 * @class Time_FromRange
 * @extends ZC.Core.Widget.DateTime_FromRange
 */
ZCWidget.Create('Time_FromRange', 'Core', oWidget);

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget
 * @class Integer
 */
oWidget = ZCWidget.Create('Integer');
oWidget.prototype.Validate = function()
{
	if (!ZCWidget.Integer.superclass.Validate.call(this))
	{
		return false;
	}

	var Value = U.Trim(this.GetValue()),
		iMin = this.GetAttribDefault('Min'),
		iMax = this.GetAttribDefault('Max');

	if (Value == '')
	{
		return true;
	}

	if (!Value.match(/^\d+$/))
	{
		this.SetValid(false, _GT(this.GetAttrib('WholeNumberValMsg')));
		return false;
	}

	Value = parseInt(Value);
	if (iMin !== false && Value < iMin)
	{
		this.SetValid(false, _zc(_GT(this.GetAttrib('MinValMsg')), { MIN: iMin }));
		return false;
	}

	if (iMax !== false && Value > iMax)
	{
		this.SetValid(false, _zc(_GT(this.GetAttrib('MaxValMsg')), { MAX: iMax }));
		return false;
	}

	this.SetValid(true);
	return true;
}

/**
 * @namespace ZC.Core.Widget
 * @extends ZC.Core.Widget.Group
 * @class EmailWithDomainSelection
 */
oWidget = ZCWidget.Create('EmailWithDomainSelection', 'Core', ZCWidget.Group);
oWidget.prototype.AttribMethod_DomainOptions = function(xValue)
{
	var aOptions = {}, 
		sCurrentValue = this.aChildWidgets.Domain.GetValue();

	U.ForEach(xValue, function(sOpt) { aOptions[sOpt] = sOpt; });

	if (L.isUndefined(aOptions[sCurrentValue]))
	{
		aOptions[sCurrentValue] = sCurrentValue;
	}

	this.aChildWidgets.Domain.SetAttrib('Options', aOptions);
}
oWidget.prototype.SetValue = function(xValue)
{
	if (L.isObject(xValue))
	{
		this.aChildWidgets.LocalPart.SetValue(xValue.LocalPart);
		this.aChildWidgets.Domain.SetValue(xValue.Domain);
	}
	else if (L.isString(xValue))
	{
		var aValueSplit = xValue.split('@', 2),
			aOptions = U.Clone(this.GetAttribDefault('DomainOptions', []));

		// check that the domain in the value is an option, add it if not
		if (aValueSplit.length == 2 && !U.InArray(aValueSplit[1], aOptions))
		{
			aOptions.push(aValueSplit[1]);
		}

		// not setting DomainOptions attrib, want to keep that at its original value
		this.AttribMethod_DomainOptions(aOptions);

		this.aChildWidgets.LocalPart.SetValue(aValueSplit[0]);
		this.aChildWidgets.Domain.SetValue(aValueSplit[1] || '');
	}
}
oWidget.prototype.GetValue = function()
{
	var sValue = U.Trim(this.aChildWidgets.LocalPart.GetValue()), 
		sDomain = this.aChildWidgets.Domain.GetValue();

	if (sValue && sDomain)
	{
		sValue += '@' + sDomain;
	}

	return sValue;
}



})();
;(function() {
var L = YAHOO.lang, U = ZC.Util, Dom = YAHOO.util.Dom, Evt = YAHOO.util.Event, YUIPECONTENT = 'yui-pe-content', HIDE = 'hide', ZCBlock = ZC.Core.Block;

/**
 * Core CMS components
 * @module Core
 */

/**
 * Handles vertical drop-down menus.
 *
 * @namespace ZC.Core.Block
 * @class Menu_Menu
 * @extends ZC.Core.Block
 */
var oBlock = ZCBlock.Create('Menu_Menu');
oBlock.prototype.CustomSetupEnd = function()
{
	var elMenu, aMenuLinks, oMenu;
   
	elMenu = this.GetElement();
	if (!elMenu)
		return false;

	Dom.removeClass(elMenu, YUIPECONTENT);

	oMenu = new YAHOO.widget.MenuBar(elMenu, { zindex: 5 });
	oMenu.render(); 
	oMenu.show(); 

	U.ForEach(oMenu.getSubmenus(), function(oSubmenu) { oSubmenu.cfg.setProperty('zindex', 5); });

	this.oYUIMenuBar = oMenu;

	aMenuLinks = Dom.getElementsByClassName('menulink', 'a');
	if (aMenuLinks.length)
	{
		Evt.on(aMenuLinks, 'click', function(oEvent)
		{
			var elTarget = Evt.getTarget(oEvent),
				aSubmenus = oMenu.getSubmenus(),
				sGoToMenuID = elTarget.href.replace(/.*#/, ''), 
				i, iMax;

			for (i = 0, iMax = aSubmenus.length; i < iMax; i++)
			{
				if (aSubmenus[i].id == sGoToMenuID)
				{
					aSubmenus[i].show();
					aSubmenus[i].parent.cfg.setProperty('selected', true);
					aSubmenus[i].focus();
					Evt.stopEvent(oEvent);
					return;
				}
			}
		}, this, true);
	}

	return true;
}

oBlock.prototype.Destruct = function()
{
	ZCBlock.Menu_Menu.superclass.Destruct.apply(this, arguments);

	if (this.oYUIMenuBar)
	{
		this.oYUIMenuBar.body.innerHTML = '';
		this.oYUIMenuBar = null;
	}
}

oBlock.prototype._IDPrefix = 'menu-';

/**
 * @namespace ZC.Core.Block
 * @class Layout_Accordion
 * @extends ZC.Core.Block
 */
oBlock = ZCBlock.Create('Accordion', 'Core', undefined, 'Layout');
oBlock.prototype.CustomSetupEnd = function()
{
	var elAccordion = this.GetElement();

	if (!elAccordion)
		return false;

	Dom.removeClass(elAccordion, YUIPECONTENT);
	this.oYUIAccordionView = new YAHOO.widget.AccordionView(elAccordion, {
		animationSpeed: this.GetAttribDefault('AnimSpeed', 0.5),
		collapsible: this.GetAttribDefault('Collapsible', true),
		expandable: this.GetAttribDefault('Multiple', false),
		width: this.GetAttribDefault('Width', '100%')
	});

	this._aAccordionBlocks = this.GetAttribDefault('AccordionBlocks');
	if (this._aAccordionBlocks)
	{
		this.oYUIAccordionView.on('panelOpen', function(oEvent) {
			var iPanelID = oEvent.index;

			// This will return false if a request has been made to load
			// content, which stops the panel from opening. The success handler
			// for the content request will open the panel once the content has
			// arrived.
			return this.LoadPanel(iPanelID, false, oEvent.panel);
		}, this, true);


		// Check for panels with StartOpen and trigger them to load.
		// Done in AfterManagerInit, as the accordion uses ManagerInit to call "setInPage"
		ZC.JSManager.GetEvent('AfterManagerInit').subscribe(function()
		{
			U.ForEach(this._aAccordionBlocks, function(aBlockInfo, iPanelID)
			{
				var elPanelBody;

				if (aBlockInfo.StartOpen)
				{
					this.LoadPanel(iPanelID, false);
				}
			}, this);
		}, this, true);
	}

	return true;
}

oBlock.prototype.Destruct = function()
{
	ZC.Core.Layout.Accordion.superclass.Destruct.apply(this, arguments);

	if (this.oYUIAccordionView)
	{
		this.oYUIAccordionView.destruct();
		this.oYUIAccordionView = null;
	}
}

oBlock.prototype._IDPrefix = 'accordion-';

/**
 * Loads the panel specifed by iPanelID into elPanelBody.
 * @param {Number} iPanelID the id of the panel to load (0 = first panel, etc)
 * @param {Boolean} bReload if true, then the block will be reloaded, otherwise it will ignore panels that are already loaded
 * @param {HTMLElement} elPanel the panel element (optional, will be looked up otherwise)
 */
oBlock.prototype.LoadPanel = function(iPanelID, bReload, elPanel)
{
	var elPanelBody, fnPanelLoaded, sLoadingClass = 'zc-accordion-loading',
		oLoadingIndicator = {
			Show: function() { Dom.addClass(elPanel, sLoadingClass); },
			Hide: function() { Dom.removeClass(elPanel, sLoadingClass); }
		};

	if (L.isUndefined(elPanel))
	{
		elPanelBody = Dom.get('accordion-' + this.UniqueID() + '--' + iPanelID);
		if (elPanelBody)
		{
			elPanel = elPanelBody.parentNode;
		}
		else
		{
			// panel may not be present
			return false;
		}
	}
	else
	{
		elPanelBody = elPanel.lastChild;
	}

	if (elPanelBody && !L.isUndefined(this._aAccordionBlocks[iPanelID]) && (bReload || !this._aAccordionBlocks[iPanelID].Loaded))
	{
		fnPanelLoaded = function()
		{
			this._aAccordionBlocks[iPanelID].Loaded = true;
			this._aAccordionBlocks[iPanelID].ForceReload = false;
			if (this.oYUIAccordionView)
				this.oYUIAccordionView.openPanel(iPanelID);
		}
		fnFailure = function() 
		{
			U.Alert(U.GetText('The server was unable to retreive the data, please try again.')); 
		}

		if (this._aAccordionBlocks[iPanelID].ContentURL)
		{
			var oCallback = {
			customevents: { onStart: oLoadingIndicator.Show, onComplete: oLoadingIndicator.Hide },
				success: function(o) { elPanelBody.innerHTML = o.responseText; fnPanelLoaded.call(this); }, 
				failure: fnFailure,
				scope: this
			};
			YAHOO.util.Connect.asyncRequest('GET', this._aAccordionBlocks[iPanelID].ContentURL, oCallback, null);
		}
		else
		{
			var oGetPostVars = { panelid: iPanelID };

			if (bReload || this._aAccordionBlocks[iPanelID].ForceReload)
				oGetPostVars.reloadPanel = 1;

			this.AjaxRequest({ 
				Transaction: 'FetchBlockSection', 
				GetPostVars: oGetPostVars,
				OnSuccess: fnPanelLoaded,
				OnFailure: fnFailure,
				LoadingIndicator: oLoadingIndicator
			});
		}

		return false;
	}

	return true;
}

/**
 * Reloads panels in the accordion. If the panel is closed, then just flag it as needing a reload.
 * @param {Array} panel ids to reload, or unspecified to reload all
 */
oBlock.prototype.ReloadPanels = function(aPanels)
{
	if (L.isNumber(aPanels))
	{
		aPanels = [aPanels];
	}
	else if (!L.isArray(aPanels))
	{
		aPanels = U.Keys(this._aAccordionBlocks);
	}

	U.ForEach(aPanels, function(iPanelID) { 
		if (this.oYUIAccordionView.isPanelOpen(iPanelID))
		{
			this.LoadPanel(iPanelID, true); 
		}
		else
		{
			this._aAccordionBlocks[iPanelID].Loaded = false;
			this._aAccordionBlocks[iPanelID].ForceReload = true;
		}
	}, this);
}

/*
 * Adds the 'ReloadPanels' and 'SetInPage' action for AJAX responses.
 */
oBlock.prototype.ProcessAjaxResponse = function(oResponse, oCustom, oFinishEvent)
{
	ZC.Core.Layout.Accordion.superclass.ProcessAjaxResponse.apply(this, arguments);

	U.ForEach(oResponse, function(oData, sAction)
	{
		switch (sAction)
		{
			case 'ReloadPanels':
				this.ReloadPanels(oData);
				break;

			case 'SetInPage':
				this.SetInPage(oData);
				break;
		}	
	}, this);
}

/**
 * Used by the server-side code to inform us of panels that are in-page (if
 * deciding based on size, the server-side code doesn't know until after the
 * JSConfig has been output.
 * @param {Array} aInPage array of panel ids that are in-page
 * @private
 */
oBlock.prototype.SetInPage = function(aInPage)
{
	U.ForEach(aInPage, function (iPanelID) { 
		if (!L.isUndefined(this._aAccordionBlocks[iPanelID])) this._aAccordionBlocks[iPanelID].Loaded = true; 
	}, this);
}

/**
 * Called when the AccordionBlocks attrib is set. This will probably be during
 * a reload, so we want to make sure our header elements are updated.
 */
oBlock.prototype.AttribMethod_AccordionBlocks = function(aBlocks)
{
	// called from the constructor (Header is not in the config array on page load), or our accordion doesn't yet exist, do nothing.
	if (!this.oYUIAccordionView || aBlocks.length == 0 || L.isUndefined(aBlocks[0].Header) || this._bAttribProviderSetup)
		return;

	var i, iMax, oPanel;

	for (i = 0, iMax = aBlocks.length; i < iMax; ++i)
	{
		elPanel = this.oYUIAccordionView.getPanel(i);
		this.oYUIAccordionView.setPanelHeader(i, aBlocks[i].Header, aBlocks[i].HeaderClass);
	}
}


/**
 * @namespace ZC.Core.Block
 * @class TabView
 * @extends ZC.Core.Block
 */
oBlock = ZCBlock.Create('TabView');
oBlock.prototype.CustomSetupEnd = function()
{
	var elTabs, aTabs, aTabLinks, sOrientation = this.GetAttribDefault('Orientation', 'top'), bTopAndBottom = false,
		elBottomTabsDiv, elTabParent, elContentParent, fnSwitchTab, fnOnChangeTab, Cookie, sCookieID, iStoredIndex, 
		sChangeTabEvent, oChangeTabEvent, iTabIndex, iSelectedTabIndex;

	elTabs = this.GetElement();
	if (!elTabs)
	{
		YAHOO.log('unable to find element with ID ' + (this._IDPrefix + this.UniqueID()), 'debug', 'TabView');
		return false;
	}

	// special case: we add our own clone of the tabs at the bottom and handle clicks / class changes on them separate from the YUI tabview code
	// TODO: get similar support integrated into YUI so we don't have to do this.
	if (sOrientation == 'top+bottom')
	{
		bTopAndBottom = true;
		sOrientation = 'top';
	}

	this.oYUITabView = new YAHOO.widget.TabView(elTabs, { orientation: sOrientation });

	this.oTabs = {};
	this.aTabIndices = [];
	iTabIndex = iSelectedTabIndex = 0;

	U.ForEach(this.GetAttribDefault('Tabs', []), function(oTabDef, sTabID) 
	{
		var oAjaxTabDef, oTab;

		if (oTabDef.Selected)
			iSelectedTabIndex = iTabIndex;

		if (oTabDef.AjaxURL)
		{
			var oAjaxTabDef = {
				label: oTabDef.Caption,
				dataSrc: oTabDef.AjaxURL,
				cacheData: (L.isUndefined(oTabDef.CacheData) ? true : oTabDef.CacheData),
				active: (iSelectedTabIndex == iTabIndex)
			};

			oTab = new YAHOO.widget.Tab(oAjaxTabDef);
			this.oYUITabView.addTab(oTab, iTabIndex);
		}
		else
		{
			oTab = this.oYUITabView.getTab(iTabIndex);
		}

		// create a mapping from tab id => YUI Tab object, and index => tab ID
		this.oTabs[sTabID] = oTab;
		this.aTabIndices[iTabIndex] = sTabID;

		iTabIndex++;
	}, this);

	if (bTopAndBottom)
	{
		elTabParent = this.oYUITabView.getElementsByClassName('yui-nav', 'ul' )[0];
		elContentParent = this.oYUITabView.getElementsByClassName('yui-content')[0];
		if (elTabParent && elContentParent)
		{
			// we can make use of the YUI tab-styles by creating a container div for the bottom tabs with the yui-navset-bottom class
			elBottomTabsDiv = document.createElement('div');
			elBottomTabsDiv.className = 'yui-navset-bottom';
			elBottomTabsDiv.appendChild(elTabParent.cloneNode(true));
			Dom.insertAfter(elBottomTabsDiv, elContentParent);

			fnSwitchTab = function(oEvent, iIndex)
			{
				Evt.stopEvent(oEvent);
				this.oYUITabView.set('activeIndex', iIndex);
			}
			U.ForEach(Dom.getChildren(elBottomTabsDiv.firstChild), function(elTab, iIndex)
			{
				var oTab, fnPrevAddClass, fnPrevRemoveClass;

				oTab = this.oYUITabView.getTab(iIndex);

				Evt.on(elTab, 'click', fnSwitchTab, iIndex, this);

				fnPrevAddClass = oTab.addClass;
				oTab.addClass = function (sClass)
				{
					Dom.addClass(elTab, sClass);
					fnPrevAddClass.apply(this, arguments);
				}
				fnPrevRemoveClass = oTab.removeClass;
				oTab.removeClass = function (sClass)
				{
					Dom.removeClass(elTab, sClass);
					fnPrevRemoveClass.apply(this, arguments);
				}
			}, this);
		}
	}

	aTabLinks = Dom.getElementsByClassName('tabviewlink', 'a');
	if (aTabLinks.length)
	{
		Evt.on(aTabLinks, 'click', function(oEvent)
		{
			var elTarget = Evt.getTarget(oEvent),
				sTabID = elTarget.href.replace(/.*#/, '');

			if (!L.isUndefined(this.oTabs[sTabID]))
			{
				this.oYUITabView.set('activeTab', this.oTabs[sTabID]);
				Evt.stopEvent(oEvent);
			}
		}, this, true);
	}

	sCookieID = this.GetAttribDefault('StoreSelectedTab');
	if (sCookieID)
	{
		Cookie = YAHOO.util.Cookie;
		iStoredIndex = Cookie.getSub('TabViewSelectedTab', sCookieID, Number);
		if (iStoredIndex)
		{
			this.oYUITabView.set('activeIndex', iStoredIndex);
		}

		this.oYUITabView.on('activeIndexChange', function(oEvent)
		{
			var oExpiry = new Date();
			oExpiry.setUTCFullYear(oExpiry.getUTCFullYear() + 1);

			Cookie.setSub('TabViewSelectedTab', sCookieID, this.oYUITabView.get('activeIndex'), { expires: oExpiry });
		}, this, true);
	}

	sChangeTabEvent = this.GetAttribDefault('ChangeTabEvent');
	if (sChangeTabEvent)
	{
		oChangeTabEvent = ZC.JSManager.GetEvent(sChangeTabEvent);
	}
			
	fnOnChangeTab = function(iNewIndex, iOldIndex)
	{
		// any non-yui classes on the tabs, add a "classname-selected" version, to cope with the fact IE6 can't handle multi-class CSS selectors.
		U.ForEach(aTabs, function(oTab, iIndex) 
		{
			var aClasses = oTab.get('element').className.split(/\s/);
			U.ForEach(aClasses, function (sClass)
			{
				if (sClass == 'selected')
				{
					return;
				}
				if (sClass.substr(sClass.length - 9) == '-selected' && iIndex != iNewIndex)
				{
					oTab.removeClass(sClass);
				}
				else if (iIndex == iNewIndex)
				{
					oTab.addClass(sClass + '-selected');
				}
			});
		});
				
		if (oChangeTabEvent)
		{
			oChangeTabEvent.fire(this.aTabIndices[iNewIndex], this.aTabIndices[iOldIndex]);
		}
	}

	this.oYUITabView.on('activeIndexChange', function(oEvent) { fnOnChangeTab.call(this, oEvent.newValue, oEvent.prevValue); }, this, true);	
	ZC.JSManager.GetEvent('ManagerInit').subscribe(function() { fnOnChangeTab.call(this, this.oYUITabView.get('activeIndex')); }, this, true);

	return true;
}

oBlock.prototype.Destruct = function()
{
	ZCBlock.TabView.superclass.Destruct.apply(this, arguments);

	this.oYUITabView = null;
	this.oTabs = null;
}

oBlock.prototype._IDPrefix = 'tabview-';

/**
 * Enables or disables the given tab
 * @param {String} sTabID the id of the tab to enable/disable
 * @param {Boolean} bEnable if true (default), then enable, otherwise disable
 */
oBlock.prototype.EnableTab = function(sTabID, bEnable)
{
	if (L.isUndefined(bEnable))
		bEnable = true;

	if (!L.isUndefined(this.oTabs[sTabID]))
		this.oTabs[sTabID].set('disabled', !bEnable);
}

/**
 * Disables the given tab
 * @param {String} sTabID the id of the tab to disable
 */
oBlock.prototype.DisableTab = function(sTabID)
{
	this.EnableTab(sTabID, false);
}

/**
 * Adds a class to a tab element
 * @param {String} sTabID the id of the tab to add the class to
 * @param {String} sClass the class to add
 */
oBlock.prototype.AddTabClass = function(sTabID, sClass)
{
	if (!L.isUndefined(this.oTabs[sTabID]))
		this.oTabs[sTabID].addClass(sClass);
}

/**
 * Removes a class from a tab element
 * @param {String} sTabID the id of the tab to remove the class from
 * @param {String} sClass the class to remove
 */
oBlock.prototype.RemoveTabClass = function(sTabID, sClass)
{
	if (!L.isUndefined(this.oTabs[sTabID]))
		this.oTabs[sTabID].removeClass(sClass);
}

/**
 * Changes to a tab by index or name
 * @param {String/Number} mIndex either the index of the tab, or its name
 */
oBlock.prototype.ChangeTab = function(mIndex)
{
	if (L.isString(mIndex) && !mIndex.match(/^\d+$/))
	{
		mIndex = U.IndexOf(this.aTabIndices, mIndex);
	}

	this.oYUITabView.set('activeIndex', Number(mIndex));
}

/**
 * Sets up the sorter/pager links on TableDataAdmin blocks so that they use AJAX requests.
 * @namespace ZC.Core.Block
 * @extends ZC.Core.Block
 * @class Recordset_Load_Search
 */
oBlock = ZCBlock.Create('Recordset_Load_Search');
oBlock.prototype.CustomSetupEnd = function()
{
	var elListInner = Dom.get('list-inner-' + this.UniqueID()),
		sLoadingClass = 'dbad-list-loading',
		elSorterSelect = Dom.get('frmSorter.rstsortby'),
		fnClickHandler;

	if (!elListInner)
		return true;

	fnClickHandler = function(oEvent, elTarget, elListInner)
	{
		Evt.stopEvent(oEvent);
		this.AjaxRequest({ Transaction: 'SortOrPageChange', URL: elTarget.href });
	}

	Evt.delegate(elListInner, 'click', fnClickHandler, '.caption a, .pager a', this, true);

	if (elSorterSelect)
	{
		elSorterSelect.onchange = '';
		Evt.on(elSorterSelect, 'change', function() { 
			this.AjaxRequest({ Transaction: 'SortOrPageChange', GetPostVars: elSorterSelect.value });
		}, this, true);
	}

	return true;
}

/**
 * Displays a progressbar using the YUI ProgressBar widget.
 * @namespace ZC.Core.Block
 * @extends ZC.Core.Block
 * @class YUIProgressBar
 */
oBlock = ZCBlock.Create('YUIProgressBar');
oBlock.prototype.CustomSetupEnd = function()
{
	var elContainer = this.GetElement(), elPB, aConfig, oAnim;

	if (!elContainer)
		return false;

	Dom.removeClass(elContainer, YUIPECONTENT);

	aConfig = {
		anim: Boolean(this.GetAttribDefault('Animation')),
		direction: this.GetAttribDefault('Direction', 'ltr'),
		height: this.GetAttribDefault('Height', '20px'),
		width: this.GetAttribDefault('Width', '200px'),
		value: this.GetAttrib('Value'),
		minValue: this.GetAttribDefault('MinValue'),
		maxValue: this.GetAttribDefault('MaxValue')
	};
	this.oYUIProgressBar = new YAHOO.widget.ProgressBar(aConfig);
	this.oYUIProgressBar.render(elContainer, Dom.getFirstChild(elContainer));

	return true;
}

oBlock.prototype.AttribMethod_Animation = function(aAnimDef, aOldDef)
{
	this.oYUIProgressBar.set('anim', (aAnimDef !== false));

	if (L.isObject(aAnimDef))
	{
		var oAnim = this.oYUIProgressBar.get('anim');
		if (!L.isUndefined(aAnimDef.Duration))
		{
			oAnim.duration = Number(aAnimDef.Duration);
		}
		if (!L.isUndefined(aAnimDef.Easing) && !L.isUndefined(YAHOO.util.Easing[aAnimDef.Easing]))
		{
			oAnim.method = YAHOO.util.Easing[aAnimDef.Easing];
		}
	}
}

oBlock.prototype.AttribMethod_Value = function(Value, OldValue)
{
	if (Value < this.GetAttribDefault('MinValue', 0) || Value > this.GetAttribDefault('MaxValue', 100))
	{
		this.SetAttrib('Value', OldValue);
		return;
	}

	this.oYUIProgressBar.set('value', Value);
	this._UpdateMessage();
}

oBlock.prototype.AttribMethod_MinValue = function(Value)
{
	this.oYUIProgressBar.set('minValue', Value);
	this._UpdateMessage();
}

oBlock.prototype.AttribMethod_MaxValue = function(Value)
{
	this.oYUIProgressBar.set('maxValue', Value);
	this._UpdateMessage();
}

oBlock.prototype.AttribMethod_Width = function(Value)
{
	this.oYUIProgressBar.set('width', Value);
}

oBlock.prototype.AttribMethod_Message = 
oBlock.prototype._UpdateMessage = function()
{
	var sMessageTemplate = this.GetAttribDefault('Message'), aTags = {}, 
		iValue = this.GetAttribDefault('Value'),
		iMinValue = this.GetAttribDefault('MinValue', 0),
		iMaxValue = this.GetAttribDefault('MaxValue', 100);
	
	if (!sMessageTemplate)
		return;

	if (L.isUndefined(this.elMessage))
	{
		this.elMessage = Dom.getFirstChildBy(this.GetElement(), function(el) { return Dom.hasClass(el, 'yui-pb-message'); });

		if (!this.elMessage)
			this.elMessage = false;
	}

	if (this.elMessage === false)
		return;

	aTags = {
		Value: iValue,
		MinValue: iMinValue,
		MaxValue: iMaxValue,
		Remaining: iMaxValue - iValue,
		Percent: Math.round((iValue / (iMaxValue - iMinValue)) * 100)
	};

	this.elMessage.innerHTML = U.ReplaceTags(sMessageTemplate, aTags);
}

var oPopDownMessageBlocks = {};

oBlock = ZCBlock.Create('PopDownMessage');
oBlock.prototype.CustomSetupEnd = function()
{
	var elContainer = this.GetElement(), elPB, aConfig, oAnim;

	if (!elContainer)
		return false;

	oPopDownMessageBlocks[this.UniqueID()] = this;

	// move the message container to the top level of the document to prevent its ancestors from affecting the styling.
	document.body.insertBefore(elContainer, document.body.firstChild);
	Dom.removeClass(elContainer, YUIPECONTENT);
	Dom.addClass(elContainer, HIDE);
	this.CalculateMessageHeight();
	return true;
}

/**
 * Calculates the height of the message element. Should be called after any action which may have caused the height to change.
 */
oBlock.prototype.CalculateMessageHeight = function()
{
	var elContainer = this.GetElement(), bWasHidden = Dom.hasClass(elContainer, HIDE);

	if (bWasHidden)
		Dom.removeClass(elContainer, HIDE);
		
	this.iMessageHeight = Dom.getRegion(elContainer).height;

	if (bWasHidden)
		Dom.addClass(elContainer, HIDE);
	else
		this.UpdateMargin();

	return this.iMessageHeight;
}

oBlock.prototype.AttribMethod_Visible = function (bShow, bOldValue)
{
	var iScrollTop = 0, bIE6 = (YAHOO.env.ua.ie && YAHOO.env.ua.ie < 7),
		elContainer = this.GetElement(), oRegion, 
		fnResizeMessage, fnOnScroll, fnUpdateMargin;
	   
	fnResizeMessage	= function()
	{
		// Make sure the message fills the width of the browser
		var iStyleWidth = Number(Dom.getStyle(elContainer, 'width').replace(/px/, '')),
			bHidden = Dom.hasClass(elContainer, HIDE),
			oRegion;

		if (!L.isNumber(iStyleWidth))
		{
			// Set width to a fixed value, work out the difference between the style width and the actual element width (borders, margins, etc)
			Dom.setStyle(elContainer, 'width', '600px');
			iStyleWidth = 600;
		}

		if (bHidden)
		{
			Dom.setStyle(elContainer, 'top', '-20000px');
			Dom.removeClass(elContainer, HIDE);
		}
		oRegion = Dom.getRegion(elContainer);
		if (bHidden)
		{
			Dom.addClass(elContainer, HIDE);
		}

		Dom.setStyle(elContainer, 'width', (Dom.getViewportWidth() - (oRegion.width - iStyleWidth)) + 'px');
	}

	fnOnScroll = function()
	{
		var iScrollTop = U.GetPageScrollTop();
		Dom.setStyle(elContainer, 'top', iScrollTop + 'px');
	}

	if (this.bShowing == bShow || (!bShow && L.isUndefined(bOldValue)))		// old value is undefined on construction
		return;

	this.bShowing = bShow;

	if (bShow)
	{
		U.ForEach(oPopDownMessageBlocks, function(oBlock)
		{
			if (oBlock != this && oBlock.GetAttribDefault('Visible'))
				oBlock.SetAttrib('Visible', false);
		}, this);

		fnResizeMessage();
		Evt.on(window, 'resize', fnResizeMessage, this, true);
	}
	else
	{
		Evt.removeListener(window, 'resize', fnResizeMessage, this, true);
	}

	if (bIE6)
	{
		// IE6 doesn't support position: fixed
		Dom.setStyle(elContainer, 'position', 'absolute');
	}

	if (this.oMessageAnim)
	{
		this.oMessageAnim.stop();
		this.oMessageAnim = null;
	}

	if (bShow)
	{
		if (bIE6)
		{
			this.oMessageAnim = new YAHOO.util.Anim(elContainer, { opacity: { from: 0, to: 1} }, this.GetAttribDefault('AnimDuration', 0.5));

			this.oMessageAnim.onStart.subscribe(function() 
			{ 
				Dom.removeClass(elContainer, HIDE);
				oRegion = Dom.getRegion(elContainer);
				document.body.style.marginTop = oRegion.height + "px";
				fnOnScroll();
			});
			this.oMessageAnim.onComplete.subscribe(function() { Evt.on(window, 'scroll', fnOnScroll); });
		}
		else
		{
			this.oMessageAnim = new YAHOO.util.Anim(elContainer, { top: { from: (iScrollTop - this.iMessageHeight), to: iScrollTop }}, this.GetAttribDefault('AnimDuration', 0.5));
			this.oMessageAnim.onStart.subscribe(function() { Dom.removeClass(elContainer, HIDE); }, this, true);
		}
	}
	else
	{
		if (bIE6)
		{
			this.oMessageAnim = new YAHOO.util.Anim(elContainer, { opacity: { from: 1, to: 0 } }, this.GetAttribDefault('AnimDuration', 0.5));
			this.oMessageAnim.onStart.subscribe(function() { Evt.removeListener(window, 'scroll', fnOnScroll); });
			this.oMessageAnim.onComplete.subscribe(function() 
			{
				oRegion = Dom.getRegion(elContainer);
				Dom.addClass(elContainer, HIDE);
				document.body.style.marginTop = "0px";
				// IE has some bizarre rendering bugs which are fixed by forcing a repaint:
				document.body.style.display = 'none';
				document.body.style.display = '';
			});
		}
		else
		{
			this.oMessageAnim = new YAHOO.util.Anim(elContainer, { top: { from: iScrollTop, to: (iScrollTop - this.iMessageHeight) }}, this.GetAttribDefault('AnimDuration', 0.5));
			this.oMessageAnim.onComplete.subscribe(function() { Dom.addClass(elContainer, HIDE); });
		}
	}

	if (!bIE6)
	{
		this.oMessageAnim.onComplete.subscribe(function() { 
			L.later(0, this, this.UpdateMargin); 
		}, this, true);
	}
	this.oMessageAnim.animate();
}

// this needs to be co-ordinated between all pop-down blocks, so this timeout is used by all of them
var oUpdateMarginTimeout;

oBlock.prototype.UpdateMargin = function()
{
	var oRegion, iTopMargin, iOldTopMargin, iScrollTop, elContainer = this.GetElement();

	if (!this.GetAttrib('Visible'))
	{
		// check to see if any other messages are now visible, in which case we leave them to update the margin when they're finished animating.
		for (var sBlockName in oPopDownMessageBlocks)
		{
			if (L.hasOwnProperty(oPopDownMessageBlocks, sBlockName) && oPopDownMessageBlocks[sBlockName].GetAttrib('Visible'))
				return;
		}
	}

	oRegion = Dom.getRegion(elContainer);
	if (!oRegion)
	{
		if (Dom.hasClass(elContainer, HIDE))
		{
			Dom.removeClass(elContainer, HIDE);
			oRegion = Dom.getRegion(elContainer);
			Dom.addClass(elContainer, HIDE);
		}

		if (!oRegion)
			return;
	}

	iTopMargin = oRegion.height + Number(Dom.getStyle(elContainer, 'top').replace(/px$/,''));
	iOldTopMargin = Number(Dom.getStyle(document.body, 'margin-top').replace(/px$/,''));
	iScrollTop = U.GetPageScrollTop() + (iTopMargin - iOldTopMargin);

	if (iTopMargin >= 0)
		Dom.setStyle(document.body, 'margin-top', iTopMargin + 'px');

	U.SetPageScrollTop(Math.max(0, iScrollTop));

	if (YAHOO.env.ua.ie)
	{
		// IE has some bizarre rendering bugs which are fixed by forcing a repaint:
		document.body.style.display = 'none';
		document.body.style.display = '';
	}
}

oBlock.prototype.AttribMethod_ContainerClass = function (sNewClass, sOldClass)
{
	var elContainer = this.GetElement();

	if (sOldClass)
	{
		Dom.removeClass(elContainer, sOldClass);
	}

	Dom.addClass(elContainer, sNewClass);
	this.CalculateMessageHeight();
}

oBlock.prototype.AttribMethod_MessageBody = function(sMessage)
{
	var elContainer = this.GetElement();
	elContainer.innerHTML = sMessage;
	this.CalculateMessageHeight();
}

})();
;(function() {
// shorthand aliases
var L = YAHOO.lang, U = ZC.Util, _GT = U.GetText, sprintf = U.sprintf;
// reused to hold new validator class, to cut down the amount of repetition, and to aid with minifying the file
var oValidator;

/**
 * Core CMS components
 * @module Core
 */

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class AlphaNumericNoSpace
 */
oValidator = ZC.Core.Validator.Create("AlphaNumericNoSpace");
oValidator.prototype.sDefaultValidationMessage = _GT("Only A-Z a-z 0-9 _ and - are valid (no spaces)");
oValidator.prototype.oValidationRegex = /^([A-Za-z0-9_-])*$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class AlphaNumericSpace
 */
oValidator = ZC.Core.Validator.Create("AlphaNumericSpace");
oValidator.prototype.sDefaultValidationMessage = _GT("Only A-Z a-z 0-9 _ - and spaces are valid");
oValidator.prototype.oValidationRegex = /^([A-Za-z0-9_ -])*$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class ArrayElementCount
 */
oValidator = ZC.Core.Validator.Create("ArrayElementCount");
oValidator.prototype.sDefaultValidationMessage = _GT("Please select the appropriate number of elements.");
oValidator.prototype.Validate = function (Value, oWidget)
{
	var iMin = oWidget.aDef.MinArrayElements || 0;
	var iMax = oWidget.aDef.MaxArrayElements || 0;
	var iCount;
	
	if (L.isArray(Value))
	{
		iCount = Value.length;
	}
	else if (L.isString(Value) && Value.length)
	{
		iCount = 1;
	}
	else
	{
		iCount = 0; 	// zero length string
	}
	
	if (iCount == 0) return true;
	
	return (!((iMin && iCount < iMin) || (iMax && iCount > iMax)));
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class CompareTo
 */
oValidator = ZC.Core.Validator.Create("CompareTo");
oValidator.prototype.sDefaultValidationMessage = _GT("The selection in this field must be greater than the value of the other field.");
oValidator.prototype.Validate = function(Value, oWidget)
{
	if (L.isUndefined(oWidget.aDef.GreaterThanField) && L.isUndefined(oWidget.aDef.LessThanField))
		return true;

	var aMessages = [];
	var bValid = true;
	if (oWidget.aDef.GreaterThanField)
	{
		var aWidgets = oWidget.aDef.GreaterThanField;
		if (L.isString(aWidgets))
			aWidgets = [aWidgets];

		ZC.Util.ForEach(aWidgets, function(sGreaterThanWidgetName)
		{
			var oGreaterThanWidget = ZC.JSManager.GetWidget(sGreaterThanWidgetName);

			bValid = bValid && (Value > oGreaterThanWidget.GetValue());
			aMessages.push(sprintf(_GT("greater than the value in %s"), oGreaterThanWidget.GetCaption()));
		});
	}

	if (oWidget.aDef.LessThanField)
	{
		var aWidgets = oWidget.aDef.LessThanField;
		if (L.isString(aWidgets))
			aWidgets = [aWidgets];

		ZC.Util.ForEach(aWidgets, function(sLessThanWidgetName)
		{
			var oLessThanWidget = ZC.JSManager.GetWidget(sLessThanWidgetName);

			bValid = bValid && (Value < oLessThanWidget.GetValue());
			aMessages.push(sprintf(_GT("less than the value in %s"), oGreaterThanWidget.GetCaption()));
		});
	}

	this.sDefaultValidationMessage = sprintf(_GT("The selection must be %s"), aMessages.join(_GT(" and ")));
	return bValid;
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class DomainName
 */
oValidator = ZC.Core.Validator.Create("DomainName");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a valid domain name; e.g. www.google.com:%");
oValidator.prototype.oValidationRegex = /^[0-9a-zA-Z._-]*$/;

var sDisplayNameRegex = "[a-zA-Z\\d!#$%&'*+\\\\/=?^_`{|}~\\s-]+",
	sAddressRegex = "['A-Za-z0-9_.-]+@(['A-Za-z0-9_-]+\\.['A-Za-z0-9_.-]+)",
	sEmailRegex = "((" + sDisplayNameRegex + ")?\\s*<" + sAddressRegex + ">|" + sAddressRegex + ")?";

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class EmailAddress
 */
oValidator = ZC.Core.Validator.Create("EmailAddress");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a valid e-mail address");
oValidator.prototype.oValidationRegex = new RegExp("^(" + sEmailRegex + ")?$");

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class EmailAddresses
 */
oValidator = ZC.Core.Validator.Create("EmailAddresses");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a valid e-mail address[es] (separated with commas)");
oValidator.prototype.oValidationRegex = new RegExp("^((" + sEmailRegex + "\\s*,\\s*)*" + sEmailRegex + ")?$", "m");

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class EmailAddressesNewLineSeperated
 */
oValidator = ZC.Core.Validator.Create("EmailAddressesNewLineSeperated");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a valid e-mail address[es] (one per line)");
oValidator.prototype.oValidationRegex = new RegExp("^((" + sEmailRegex + "\\s*\\n\\s*)*" + sEmailRegex + ")?$", "m");

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class HTMLColour
 */
oValidator = ZC.Core.Validator.Create("HTMLColour");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a valid HTML colour");
oValidator.prototype.oValidationRegex = /^(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow|#[0-9A-F]{6})?$/i;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class Hex6Digits
 */
oValidator = ZC.Core.Validator.Create("Hex6Digits");
oValidator.prototype.sDefaultValidationMessage = _GT("Please ensure the data is of the format ff00aa (6 digits from 0-9 plus a,b,c,d,e and f)");
oValidator.prototype.oValidationRegex = /^([0-9A-F]{6})?$/i;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class HexHTMLColour
 */
oValidator = ZC.Core.Validator.Create("HexHTMLColour");
oValidator.prototype.sDefaultValidationMessage = _GT("Please ensure the data is of the format #ff00aa (a hash followed by 6 digits from 0-9 plus a,b,c,d,e and f)");
oValidator.prototype.oValidationRegex = /^(#[0-9A-F]{6})?$/i;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class IsFutureDate
 */
oValidator = ZC.Core.Validator.Create("IsFutureDate");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a date in the future.");
oValidator.prototype.Validate = function(Value, oWidget)
{
	if (!Value) return true;

	var oToday = new Date();
	if(oWidget.aDef.AllowToday)
	{
		oToday.setHours(0);
		oToday.setMinutes(0);
		oToday.setSeconds(0);
	}
	else
	{
		oToday.setHours(23);
		oToday.setMinutes(59);
		oToday.setSeconds(59);
	}

	return Value > oToday;
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class IsHumanAge
 */
oValidator = ZC.Core.Validator.Create("IsHumanAge");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a whole number less than 100.");
oValidator.prototype.oValidationRegex = /^\d{0,2}$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class IsInteger
 */
oValidator = ZC.Core.Validator.Create("IsInteger");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a whole number.");
oValidator.prototype.oValidationRegex = /^-?\d*$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class IsNaturalNumber
 */
oValidator = ZC.Core.Validator.Create("IsNaturalNumber");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a whole number greater than zero.");
oValidator.prototype.oValidationRegex = /^(\d*[1-9]\d*)*$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class IsNonNegativeInteger
 */
oValidator = ZC.Core.Validator.Create("IsNonNegativeInteger");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a whole non-negative number.");
oValidator.prototype.oValidationRegex = /^\d*$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class IsPositive
 */
oValidator = ZC.Core.Validator.Create("IsPositive");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a positive number.");
oValidator.prototype.oValidationRegex = /^\d*([,.]\d+)?$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class IsYear
 */
oValidator = ZC.Core.Validator.Create("IsYear");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a 4 digit year.");
oValidator.prototype.Validate = function(Value, oWidget)
{
	if(Value == '')
		return true;

	var bValid = Value.match(/^\d{4}$/);
	var iMaxYear = oWidget.aDef.MaxYear;
	var iMinYear = oWidget.aDef.MinYear;
	if (iMinYear && iMaxYear)
	{
		this.sDefaultValidationMessage = sprintf(_GT('Please enter a 4 digit year between %d and %d.'), iMinYear, iMaxYear);
		bValid = bValid && (Value >= iMinYear && Value <= iMaxYear);
	}
	else if (iMaxYear)
	{
		this.sDefaultValidationMessage = sprintf(_GT('Please enter a 4 digit year before %d.'), iMaxYear);
		bValid = bValid && (Value <= iMaxYear);

	}
	else if (iMinYear)
	{
		this.sDefaultValidationMessage = sprintf(_GT('Please enter a 4 digit year after %d.'), iMinYear);
		bValid = bValid && (Value >= iMinYear);
	}

	return bValid;
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class MassMail_AutomaticRecipients
 */
oValidator = ZC.Core.Validator.Create("MassMail_AutomaticRecipients");
oValidator.prototype.sDefaultValidationMessage = _GT('The option you have selected is invalid');
oValidator.prototype.Validate = function (Value, oWidget)
{
	var oMaxRecipientsWidget = oWidget.oForm.GetWidget('MaxRecipients');
	var oRoleWidget = oWidget.oForm.GetWidget('Role');
	var oPresetEmailsWidget = oWidget.oForm.GetWidget('PresetEmailAddresses');
	
	switch(Value)
	{
		case 1:
			if (oMaxRecipientsWidget.GetValue() == -1)
			{
				this.sDefaultValidationMessage = _GT('This option is not valid when there are no manual recipients');
				return false;
			}	
			break;
				
		case 2:
			if (oRoleWidget.GetValue() == '')
			{
				oRoleWidget.SetValid(false, _GT('You must specify a role'));
				this.sDefaultValidationMessage = _GT('You must specify a role below');
				return false;
			}	
			break;
		case 3:			
			if (oPresetEmailsWidget.GetValue() == '')
			{
				oMaxRecipientsWidget.SetValid(false, _GT('You must specify at least one email address'));
				this.sDefaultValidationMessage = _GT('You must specify at least one preset email address below');
				return false;
			}	
			break;
	}		
	return true;
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class MaxDecimalPlaces
 */
oValidator = ZC.Core.Validator.Create("MaxDecimalPlaces");
oValidator.prototype.Validate = function (Value, oWidget)
{
	this.sDefaultValidationMessage = sprintf(_GT('Please specify this value using no more than %d decimal places.'), oWidget.aDef.MaxDP);
	
	var oDPRegex = new RegExp("^\\d*([,.]\\d{1," + oWidget.aDef.MaxDP + "})?$");
	return oDPRegex.test(Value);
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class MaxLength
 */
oValidator = ZC.Core.Validator.Create("MaxLength");
oValidator.prototype.Validate = function (Value, oWidget)
{
	var iMaxLength = oWidget.aDef.MaxLength;
	this.sDefaultValidationMessage = sprintf(_GT('Please reduce this to %d  characters, it is currently at %d.'), iMaxLength, Value.length);
	
	return (Value.length <= iMaxLength);
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class NotBlank
 */
oValidator = ZC.Core.Validator.Create("NotBlank");
oValidator.prototype.sDefaultValidationMessage = _GT("Please complete this.");
oValidator.prototype.Validate = function (Value, oWidget)
{
	if (L.isUndefined(Value))
		return false;

	// if there's a length property (Strings, Arrays) then use that
	if (!L.isUndefined(Value.length))
		return Value.length > 0;

	// otherwise see if we have a toString method
	return (Value.toString) && (Value.toString().length > 0);
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class PositiveCurrency
 */
oValidator = ZC.Core.Validator.Create("PositiveCurrency");
oValidator.prototype.sDefaultValidationMessage = _GT('Please enter a number with no more than two decimal places.');
oValidator.prototype.oValidationRegex = /^\d*([.,]\d{1,2})?$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class RegEx
 */
oValidator = ZC.Core.Validator.Create("RegEx");
oValidator.prototype.sDefaultValidationMessage = _GT("Please enter a valid value");
oValidator.prototype.Validate = function(Value, oWidget)
{
	var oRE, aREMatch;

	if (Value && Value.length)
	{
		// convert to a RegExp object
		aREMatch = oWidget.GetAttrib('ValidationRegEx').match(/^(.)(.*)\1([imsxeADSUXJu]*)$/);
		if (aREMatch)
		{
			// check flags
			if (aREMatch[3].match(/[^gim]/))
			{
				YAHOO.log("Warning: ValidationRegEx on " + oWidget.sName + " contains flags not supported by Javascript; not performing client-side validation", "warn", "Core_Validator_RegEx");
				return true;
			}

			oRE = new RegExp(aREMatch[2], aREMatch[3]);
			if (oRE)
			{
				return oRE.test(Value);
			}
			else
			{
				YAHOO.log("Error instantiating regexp object", "error", "Core_Validator_RegEx");
				return true;
			}
		}
		else
		{
			YAHOO.log("Warning: Could not parse ValidationRegEx on " + oWidget.sName + "; not performing client-side validation", "warn", "Core_Validator_RegEx");
			return true;
		}
	}

	return true;
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class Regex_Multiline
 */
oValidator = ZC.Core.Validator.Create("Regex_Multiline");
oValidator.prototype.sDefaultValidationMessage = _GT("One or more lines are in an invalid format");
oValidator.prototype.Validate = function (Value, oWidget)
{
	var sRegex = oWidget.aDef.Validator_Regex_Multiline;
	if (!sRegex)
		return true;
	var oRegex = new RegExp(sRegex);

	var aLines = Value.split(/\n/);
	return !ZC.Util.Some(aLines, function(sLine)
	{
		sLine = sLine.replace(/(^\s*|\s$)/g, '');
		return (sLine.length && !oRegex.test(sLine));
	});
}

var sSMSNumberRegexp = "[1-9]\\d{8,15}";
/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class SMSNumber
 */
oValidator = ZC.Core.Validator.Create("SMSNumber");
oValidator.prototype.sDefaultValidationMessage = _GT('Please enter a valid international number including the country code.  UK mobile numbers start with 44 and omit the first 0.');
oValidator.prototype.oValidationRegex = new RegExp('^(' + sSMSNumberRegexp + ')?$');

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class SMSNumbers
 */
oValidator = ZC.Core.Validator.Create("SMSNumbers");
oValidator.prototype.sDefaultValidationMessage = _GT('Please enter valid international numbers including the country code, separated by commas or newlines.');
oValidator.prototype.oValidationRegex = new RegExp('^(' + sSMSNumberRegexp + "([,\\n]" + sSMSNumberRegexp + ")*)?$");

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class SelectMinMax
 */
oValidator = ZC.Core.Validator.Create("SelectMinMax");
oValidator.prototype.sDefaultValidationMessage = _GT('You must select at least 1 and no more than 3 counties');
oValidator.prototype.Validate = function (Value, oWidget)
{
	if (Value instanceof Array)
	{
		var iMin = oWidget.aDef.SelectMin || 0;
		var iMax = oWidget.aDef.SelectMax || 0;
		
		if (iMax > 0)
			this.sDefaultValidationMessage = sprintf(_GT("You must select at least %d and no more than %d."), iMin, iMax);
		else
			this.sDefaultValidationMessage = sprintf(_GT("You must select at least %d."), iMin);
	
		if (Value.length >= iMin && (iMax == 0 || Value.length <= iMax))
			return true;
	}
	return false;	
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class TelephoneNumber
 */
oValidator = ZC.Core.Validator.Create("TelephoneNumber");
oValidator.prototype.sDefaultValidationMessage = _GT('Please enter a valid telephone number excluding the country code.');
oValidator.prototype.oValidationRegex = /^[^a-zA-Z]*$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class TwoTokensValidator
 */
oValidator = ZC.Core.Validator.Create("TwoTokensValidator");
oValidator.prototype.sDefaultValidationMessage = _GT('A maximum of two words is allowed');
oValidator.prototype.oValidationRegex = /^(\S+( \S+)?)?$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class UniqueInRecordset
 */
oValidator = ZC.Core.Validator.Create("UniqueInRecordset");
oValidator.prototype.sDefaultValidationMessage = _GT('There is already a record with this value');
oValidator.prototype.Validate = function(Value, oWidget, sValidationMessage)
{
	var aQueryParams, aDef,
		oCallback = {
		success: function(oResult)
		{
			if (oResult.responseText == 'true')
			{
				this.SetValid(true);
			}
			else
			{
				this.SetValid(false, sValidationMessage);
			}
		},
		scope: oWidget
	};

	aDef = oWidget.GetAttrib('UniqueInRecordsetDef');
	aQueryParams = { widgetvalue: Value };
	U.ForEach(aDef.PriKeys || [], function(sFieldName)
	{
		var mPriKeyValue;

		if (oWidget && oWidget.oForm)
		{
			mPriKeyValue = oWidget.oForm.GetWidget(sFieldName)

			if (!mPriKeyValue && oWidget.oForm.HasHiddenField(sFieldName))
			{
				mPriKeyValue = oWidget.oForm.HiddenFieldValue(sFieldName);
			}
		}
		if (!mPriKeyValue)
		{
			mPriKeyValue = ZC.JSManager.GetWidget(sFieldName);
		}
		if (!L.isUndefined(mPriKeyValue))
		{
			aQueryParams[sFieldName] = mPriKeyValue;
		}
	});

	YAHOO.util.Connect.asyncRequest('GET', ZC.JSManager.URL(aQueryParams, aDef.AjaxURL), oCallback);	
}

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class URL
 */
oValidator = ZC.Core.Validator.Create("URL");
oValidator.prototype.sDefaultValidationMessage = _GT('Please enter a valid URL beginning http://');
oValidator.prototype.oValidationRegex = /^(https?:\/\/\S+)?$/;

/**
 * @namespace ZC.Core.Validator
 * @extends ZC.Core.Validator
 * @class WhiteSpaceSeparatedIntegers
 */
oValidator = ZC.Core.Validator.Create("WhiteSpaceSeparatedIntegers");
oValidator.prototype.sDefaultValidationMessage = _GT('Please enter one or more whole numbers separated by a space e.g. "1 2 3".');
oValidator.prototype.oValidationRegex = /^(\d+\s+)*$/;

})();
;(function() {
var L = YAHOO.lang, Dom = YAHOO.util.Dom, Evt = YAHOO.util.Event, U = ZC.Util, _GT = U.GetText, JSManager = ZC.JSManager,
	oEL, ZCEL = ZC.Core.EventListener;

/**
 * Core CMS components
 * @module Core
 */

/**
 * Opens up an alert box. Expects the contents to be in the 'Message' attribute.
 * @namespace ZC.Core.EventListener
 * @class Alert
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('Alert');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	if (this.AttribIsset('Message'))
		JSManager.Alert(this.GetAttrib('Message'));
}


/**
 * Enables or disables a widget.
 *
 * @namespace ZC.Core.EventListener
 * @class EnableDisable
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('EnableDisable');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var bEnable, sEnabledClass, sDisabledClass, sPopulateWhenDisabledFrom, mValueWhenDisabled, oPopulateFromWidget;

	bEnable = this._SearchLists(
		this._GetWidgetValue(oWidget),
		'EnableWhenSrcEquals',
		'EnableWhenSrcNotEquals',
		'EnableWhenSrcContains',
		'EnableWhenSrcNotContains'
	);

	sEnabledClass = this.GetAttribDefault('EnabledClass', '');
	sDisabledClass = this.GetAttribDefault('DisabledClass', 'disabled');

	this.oDestWidget.Enable(bEnable, sEnabledClass, sDisabledClass);
	if (bEnable)
	{
		if (!L.isUndefined(this.ValueWhenEnabled))
		{
			this.oDestWidget.SetValue(this.ValueWhenEnabled);
			delete this.ValueWhenEnabled;
		}
		return;
	}

	if (this.AttribIsset('ValueWhenDisabled'))
	{
		mValueWhenDisabled = this.GetAttrib('ValueWhenDisabled');
	}
	else if (sPopulateWhenDisabledFrom = this.GetAttribDefault('PopulateWhenDisabledFrom')) // assignment
	{
		oPopulateFromWidget = JSManager.GetWidget(sPopulateWhenDisabledFrom);
		if (oPopulateFromWidget)
			mValueWhenDisabled = oPopulateFromWidget.GetValue();
	}	

	// ValueWhenEnabled will not be set if we've just disabled the widget, but
	// will already be present if it's not been enabled since it was first
	// disabled.
	if (L.isUndefined(this.ValueWhenEnabled) && !L.isUndefined(mValueWhenDisabled))
	{
		// save old value, restore if we enable again
		this.ValueWhenEnabled = this.oDestWidget.GetValue();
		this.oDestWidget.SetValue(mValueWhenDisabled);
	}	
}

/**
 * Enables or disables a tab in a Tabview block.
 *
 * @namespace ZC.Core.EventListener
 * @class EnableDisableTab
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('EnableDisableTab');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var oTabView = JSManager.GetWidget(this.GetAttrib('TabViewName')), bEnable;
	if (!L.isUndefined(oTabView))
	{
		bEnable = this._SearchLists(
			this._GetWidgetValue(oWidget), 
			'EnableWhenSrcEquals',
			'EnableWhenSrcNotEquals',
			'EnableWhenSrcContains',
			'EnableWhenSrcNotContains'
		);
		oTabView.EnableTab(this.GetAttrib('TabID'), bEnable);
	}
}

/**
 * Sets the options on a select widget to the contents of a Javascript array.
 * @namespace ZC.Core.EventListener
 * @class SetOptionsFromJSArray
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('SetOptionsFromJSArray');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	if (!L.isUndefined(window[this.aDef.ArrayName]))
	{
		var aNewOptions = window[this.aDef.ArrayName][this._GetWidgetValue(oWidget)];
		this.oDestWidget.SetAttrib('Options', aNewOptions);
	}
}

/**
 * Makes an AJAX request to get the options for a select.
 * @namespace ZC.Core.EventListener
 * @class SetOptionsFromAjax
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('SetOptionsFromAjax');
oEL.prototype.Setup = function()
{
	this.oSendFields = {};

	JSManager.GetEvent('ManagerInit').subscribe(function() {
		U.ForEach(this.GetAttribDefault('SendFields', {}), function(sWidgetName, sQueryParam)
		{
			var oWidget = this.oDestWidget.oForm.GetWidget(sWidgetName) || JSManager.GetWidget(sWidgetName);

			if (L.isNumber(sQueryParam))
				sQueryParam = sWidgetName;

			if (oWidget)
				this.oSendFields[sQueryParam] = oWidget;
		}, this);
	}, this, true);

	return true;
}

oEL.prototype.Destruct = function()
{
	ZCEL.SetOptionsFromAjax.superclass.Destruct.apply(this, arguments);

	this.oSendFields = {};
}

oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var oCallback = {
			customevents: {
				onFailure: 	function()
				{
					JSManager.Alert(_GT("The server was unable to retrieve the data"));
				},
				onSuccess: 	function(sEventType, aArgs)
				{
					var oResponse = YAHOO.lang.JSON.parse(aArgs[0].responseText);
					this.oDestWidget.SetAttrib('Options', oResponse);
				}
			},
			scope: this
		},
		sURL = JSManager.URL(this.oSendFields, this.GetAttrib('AjaxURL'));

	YAHOO.util.Connect.asyncRequest('GET', sURL, oCallback);
}

/**
 * Copies the value of one widget to another
 * @namespace ZC.Core.EventListener
 * @class SetValue_FromSource
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('SetValue_FromSource');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	oDestWidget.SetValue(this._GetWidgetValue(oWidget));
}

/**
 * Show or hide some elements
 *
 * @namespace ZC.Core.EventListener
 * @class ShowHideElements
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('ShowHideElements');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
   	var Elements, 
		bShow = this._SearchLists(
			this._GetWidgetValue(oWidget), 
			'ShowWhenSrcEquals', 
			'ShowWhenSrcNotEquals',
			'ShowWhenSrcContains',
			'ShowWhenSrcNotContains'
		);

	if (this.AttribIsset('ElementClass'))
	{
		Elements = Dom.getElementsByClassName(this.GetAttrib('ElementClass'), this.GetAttribDefault('ElementType', undefined));
	}
	else if (this.AttribIsset('ElementID'))
	{
		Elements = this.GetAttrib('ElementID');
	}
	else
	{
		// ElementType + ElementClass, or ElementID not specified. Default to the destination widget.
		this.oDestWidget.Show(bShow);
		return;
	}

	if (bShow)
	{
		Dom.removeClass(Elements, 'hide');
	}
	else
	{
		Dom.batch(Elements, function(el)
		{
			el.blur();
			Dom.addClass(el, 'hide');
		});
	}
}

/**
 * Calls a javascript function
 *
 * @namespace ZC.Core.EventListener
 * @class CallJSFunction
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('CallJSFunction');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	if (this.aDef.JSArgs)
	{
		eval(this.aDef.JSFunction + '(' + this.aDef.JSArgs + ');');
	}
	else
	{
		// Check with CB
		// before JSFunction could not be a function like ZC.Util.DefaultLoadingIndicator.Show
		// as window['ZC.Util.DefaultLoadingIndicator.Show'] did not exist, 
		// rather the proper path would have been window['ZC']['Util']['DefaultLoadingIndicator']['Show']
		// Supplying a null JSArg would have worked as that just evals the function
		//var fn = window[this.aDef.JSFunction];
		var fn = eval(this.aDef.JSFunction);
		if (L.isFunction(fn))
			fn();
	}
}

/**
 * Calls a method on an object
 *
 * @namespace ZC.Core.EventListener
 * @class CallObjectMethod
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('CallObjectMethod');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var oObject = this.oDestination,
		sWidget = this.GetAttribDefault('Widget'),
		sBlockUID = this.GetAttribDefault('Block'),
		sMethod = this.GetAttrib('Method'),
		aArguments = this.GetAttribDefault('Arguments', []);

	if (sWidget)
		oObject = JSManager.GetWidget(sWidget);
	else if (sBlockUID)
		oObject = JSManager.GetBlockByUniqueID(sBlockUID);

	if (oObject && oObject[sMethod])
	{
		oObject[sMethod].apply(oObject, aArguments);
		if (this.GetAttribDefault('StopEvent'))
		{
			Evt.stopEvent(oSrcEvent);
			return false;
		}
	}
}


/**
 * Sets options for linked select widgets
 *
 * @namespace ZC.Core.EventListener
 * @class SetRelatedOptions_FromSource
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('SetRelatedOptions_FromSource');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var elDest = Dom.get(this.aDef.ID + ".1");
	if (elDest)
		setOptionsRelatedTo(elDest, this._GetWidgetValue(oWidget));
}

/**
 * Sets options for linked select widgets
 *
 * @namespace ZC.Core.EventListener
 * @class SetRelatedOptions_FromInitialValue
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('SetRelatedOptions_FromInitialValue');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var sID = this.aDef.ID,
	    elDest = Dom.get(sID + '.1'),
	    elInitial = Dom.get(sID + '.initial'),
		opt = this._GetWidgetValue(oWidget) + this.oDestination.GetAttrib('Separator') + elInitial.value;

	for (var i = 0, iMax = elDest.length; i < iMax; i++)
	{
		if (elDest.options[i].value == opt)
		{
			elDest.value=(opt);
			return;
		}
	}
}

/**
 * Makes an AJAX request, populating widgets on the form with the result.
 *
 * @namespace ZC.Core.EventListener
 * @class AutoPopulate
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('AutoPopulate');
oEL.prototype.Setup = function()
{
	this.aCache = {};
	this.oLookupFields = {};

	if (this.oDestWidget.oParent != this.oDestWidget.oForm)
		this.oParent = this.oDestWidget.oParent;
	this.oForm = this.oDestWidget.oForm;

	JSManager.GetEvent('ManagerInit').subscribe(function()
	{
		U.ForEach(this.GetAttrib('LookupFields'), function(sField)
		{
			var oWidget = 
				(this.oParent && this.oParent.GetWidget(sField)) 
				|| this.oForm.GetWidget(sField)
				|| JSManager.GetWidget(sField);

			if (oWidget)
				this.oLookupFields[sField] = oWidget;
		}, this);
	}, this, true);

	var sLoadingElementID = this.GetAttribDefault('LoadingElement'), elLoading;
	if (sLoadingElementID && (elLoading = Dom.get(sLoadingElementID))) // assignment
	{
		this.elLoading = elLoading;
	}

	return true;
}
oEL.prototype.Destruct = function()
{
	ZCEL.AutoPopulate.superclass.Destruct.apply(this, arguments);
	this.oForm = null;
	this.aCache = {};
	this.oLookupFields = {};
	this.oParent = null;
}

oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var oCallback = {
		customevents: {
			onStart: 	function() { Dom.addClass(this.elLoading, 'ap-loading'); },
			onComplete: function() { Dom.removeClass(this.elLoading, 'ap-loading'); },
			onFailure: 	function()
			{
				JSManager.Alert(_GT("The server was unable to retrieve the data"));
			},
			onSuccess: 	function(sEventType, aArgs)
			{
				var oResponse;
			   	if (sEventType == 'fromcache')
				{
					oResponse = aArgs;
				}
				else
				{
					oResponse = YAHOO.lang.JSON.parse(aArgs[0].responseText);
					this.aCache[sURL] = oResponse;
				}

				U.ForEach(oResponse, function(sValue, sKey)
				{
					var sFromColour = this.GetAttribDefault('AnimateFromColour', '#0cf'),
						oWidget, oAnim, sBG;

					oWidget = 
						(this.oParent && this.oParent.GetWidget(sKey)) 
						|| this.oForm.GetWidget(sKey)
						|| JSManager.GetWidget(sKey);

					if (sValue && oWidget)
					{
						oWidget.SetValue(sValue);

						if (this.GetAttribDefault('AnimateWhenLoaded', true))
						{
							oAnim = new YAHOO.util.ColorAnim(oWidget._elInput);
							sBG = oAnim.getAttribute('backgroundColor');

							oAnim.attributes = { backgroundColor: { from: sFromColour, to: sBG } };
							oAnim.duration = 0.5;
							oAnim.animate();
						}
					}
				}, this);
			}
		},
		scope: this
	},
		sURL = JSManager.URL(this.oLookupFields, this.GetAttrib('AjaxURL'));

	// this assumes we get the same value each time for the same query
	if (!L.isUndefined(this.aCache[sURL]))
	{
		oCallback.customevents.onSuccess.call(this, 'fromcache', this.aCache[sURL]);
	}
	else
	{
		YAHOO.util.Connect.asyncRequest('GET', sURL, oCallback);
	}
}

/**
 * Sets the value of a widget to a fixed value
 *
 * @namespace ZC.Core.EventListener
 * @class SetValue_ToValue
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('SetValue_ToValue');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	if (!this._SearchLists(
		this._GetWidgetValue(oWidget), 
		'SetWhenSrcEquals', 
		'SetWhenSrcNotEquals',
		'SetWhenSrcContains', 
		'SetWhenSrcNotContains'
	))
		return;

	this.oDestWidget.SetValue(this.GetAttrib('Value'));
}

/**
 * Adds a value to the Group Matrix widget
 *
 * @namespace ZC.Core.EventListener
 * @class AddToGroupMatrix
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('AddToGroupMatrix');
oEL.prototype.Setup = function()
{
	var aSrc = this.GetAttribDefault('SourceFields', []);
	if (!L.isArray(aSrc))
		aSrc = [aSrc];
	this.SetAttrib('SourceFields', aSrc);

	return true;
}
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var oParams = {}, bFoundValue = false,
	   	sURL, 
		oCallback = {
		customevents: {
			onFailure: 	function()
			{
				JSManager.Alert(_GT("The server was unable to retrieve the data"));
			},
			onSuccess: 	function(sEventType, aArgs)
			{
				var oResponse = YAHOO.lang.JSON.parse(aArgs[0].responseText),
				    sTarget = oResponse.Target,
					oTargetWidget = this.oDestWidget,
					oConfig = oResponse.JSConfig,
					sWidgetName;

				if (sTarget && sTarget != oTargetWidget.sName)
				{
					oTargetWidget = 
						(this.oParent && this.oParent.GetWidget(sTarget))
						|| (this.oDestWidget.oForm && this.oDestWidget.oForm.GetWidget(sTarget))
						|| JSManager.GetWidget(sTarget);
				}

				if (oTargetWidget)
				{
					oTargetWidget.AddRow(oResponse.RowID, oResponse.Caption, oResponse.Widgets, oResponse.JSConfig);
				}
			}
		},
		scope: this
	};
   	
	if (this.GetAttribDefault('NoRowValues'))
	{
		oParams.AddValue = (U.Keys(this.oDestWidget.GetValue()).length) || 0;
		bFoundValue = true;
	}
	else
	{
		U.ForEach(this.GetAttrib('SourceFields'), function(sWidgetName, sKey)
		{
			var oSrcWidget = 
				(this.oParent && this.oParent.GetWidget(sWidgetName))
				|| (this.oDestWidget.oForm && this.oDestWidget.oForm.GetWidget(sWidgetName))
				|| JSManager.GetWidget(sWidgetName);

			if (oSrcWidget)
			{
				if (!L.isString(sKey))
					sKey = oSrcWidget.sName;
					
				if (oSrcWidget.GetValue())
				{
					bFoundValue = true;
					oParams[sKey] = oSrcWidget.GetValue();
					oSrcWidget.Clear();
				}
			}
		}, this);
	}

	if (!bFoundValue)
	{
		U.Alert("Please select something to add to the table.");
	}
	else
	{
		sURL = JSManager.URL(oParams, this.GetAttrib('AjaxURL'));

		YAHOO.util.Connect.asyncRequest('GET', sURL, oCallback);
	}
}

/**
 * Removes a value from the Group Matrix widget
 *
 * @namespace ZC.Core.EventListener
 * @class RemoveFromGroupMatrix
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('RemoveFromGroupMatrix');
oEL.prototype.Setup = function()
{
	var sMatrixID = this.GetAttribDefault('MatrixID'),
		sRowID = this.GetAttribDefault('RowID'),
		oWidget = this.oDestWidget;

	if (!sMatrixID)
	{
		while (oWidget && !(oWidget instanceof ZC.Core.Widget.Group_Matrix))
		{
			oWidget = oWidget.oParent;
		}
	}
	else
	{
		oWidget = oWidget.GetWidget(sMatrixID) 
			|| oWidget.oForm.GetWidget(sMatrixID)
			|| JSManager.GetWidget(sMatrixID);
	}
	if (L.isUndefined(oWidget))
		return false;

	if (!sRowID)
	{
		sRowID = oWidget.FindRowIDWithElement(this.oDestWidget._elInput);
		if (sRowID)
			this.SetAttrib('RowID', sRowID);
	}

	this.oMatrix = oWidget;
	return this.AttribIsset('RowID');
}
oEL.prototype.Destruct  = function()
{
	ZCEL.RemoveFromGroupMatrix.superclass.Destruct.apply(this, arguments);
	this.oMatrix = null;
}
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	U.Confirm('Are you sure you want to remove ' + this.oMatrix.GetRowCaption(this.GetAttrib('RowID')) + '?', this, function() 
	{
		this.oMatrix.RemoveRow(this.GetAttrib('RowID'));		
	});
}

/**
 * Sets attribs on a widget to the value of another widget on the form
 *
 * @namespace ZC.Core.EventListener
 * @class SetAttribFromField
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('SetAttribFromField');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	U.ForEach(this.GetAttrib('AttribsToSet'), function(mField, sAttrib)
	{
		if (!L.isArray(mField))
			mField = [mField];

		for (var i = 0, iMax = mField.length; i < iMax; i++)
		{
			var oWidget = this.oDestWidget.oForm.GetWidget(mField[i]) || JSManager.GetWidget(mField[i]);
			
			if (oWidget && oWidget.HasValue())
			{
				this.oDestWidget.SetAttrib(sAttrib, oWidget.GetValue());
				return;
			}
		}
	}, this);
}

/**
 * Sets attribs on a widget to a value looked up in an array
 *
 * @namespace ZC.Core.EventListener
 * @class SetAttribsFromArray
 * @extends ZC.Core.EventListener
 */
oEL = ZCEL.Create('SetAttribsFromArray');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var aAttribs = this.GetAttrib('AttribValues'), TestValue, WidgetValue = this._GetWidgetValue(oWidget), i, iMax;

	for (i = 0, iMax = aAttribs.length; i < iMax; ++i)
	{
		TestValue = aAttribs[i].Value;
		if ((!L.isArray(TestValue) && TestValue == WidgetValue) 
		 || (L.isArray(TestValue) && U.InArray(WidgetValue, TestValue)))
		{
			this.oDestWidget.SetAttribs(aAttribs[i].Attribs);
			return;
		}
	}

	// not found, do we have defaults?
	if (this.AttribIsset('DefaultAttribs'))
	{
		this.oDestWidget.SetAttribs(this.GetAttrib('DefaultAttribs'));
	}
}

/**
 * Makes an Ajax request on a block.
 *
 * @namespace ZC.Core.EventListener
 * @extends ZC.Core.EventListener
 * @class MakeAjaxRequest
 */
oEL = ZCEL.Create('MakeAjaxRequest');
oEL.prototype.HandleEvent = function(oWidget, oEvent, oSrcEvent)
{
	var sBlockUID = this.GetAttribDefault('Block'), oBlock,
		aIncludeWidgetValues = this.GetAttribDefault('IncludeWidgetValues', {}), oGetPostVars = {},
		oExtraRequestParams = this.GetAttribDefault('ExtraRequestParams', {}),
		sTransaction = this.GetAttrib('Transaction'),
		oParams;

	// has event been cancelled by a previous handler? if so, cancel the AJAX request
	if (oSrcEvent && ((oSrcEvent.getPreventDefault && oSrcEvent.getPreventDefault()) || oSrcEvent.returnValue === false))
		return;

	if (this.oDestination instanceof ZC.Core.Block)
	{
		oBlock = this.oDestination;
	}
	else if (sBlockUID)
	{
		oBlock = JSManager.GetBlockByUniqueID(sBlockUID);

		if (!oBlock)
			oBlock = JSManager.GetWidget(sBlockUID);
	}
	else 
	{
		oBlock = this.oDestination.FindContainingBlock();
	}

	if (!oBlock)
	{
		YAHOO.log('Unable to find block' + (sBlockUID ? (' with UniqueID ' + sBlockUID) : ''), 'warn', 'MakeAjaxRequest');
		return;
	}

	if (oSrcEvent)
		Evt.stopEvent(oSrcEvent);

	U.ForEach(aIncludeWidgetValues, function(sWidgetName, sKey)
	{
		var oWidget;
	   	if (this.oDestination.oForm)
		{
			oWidget = this.oDestination.oForm.GetWidget(sWidgetName);
		}
		else
		{
			oWidget = JSManager.GetWidget(sWidgetName);
		}

		if (oWidget)
		{
			if (L.isString(sKey))
				oGetPostVars[sKey] = oWidget;
			else
				oGetPostVars[sWidgetName] = oWidget;
		}
	}, this);

	if (!L.isUndefined(oExtraRequestParams.GetPostVars))
	{
		oGetPostVars = L.merge(oExtraRequestParams.GetPostVars, oGetPostVars);
	}

	oParams = L.merge(
		oExtraRequestParams,
		{ Transaction: sTransaction, GetPostVars: oGetPostVars }
	);
	oBlock.AjaxRequest(oParams);
	return false;
}

})();
;
