/*-----------------------------------------------------------------------------------------------------
 * GLOBALS
 * Global variables
 *-----------------------------------------------------------------------------------------------------*/

var YES = true;
var NO = false;

// Sort directions
var UP = "asc";
var DOWN = "desc";




/* The buttons and icons are DEPRECATED */

var BUTTON = {
		DIMENSIONS: { WIDTH: 20, HEIGHT: 20 },
		CLOSER: { TITLE: "Close Window", ID: 0 },
		ADD: { TITLE: "Add Item", ID: 1 },
		REMOVE: { TITLE: "Remove Item", ID: 2 },
		EDIT: { TITLE: "Edit Item", ID: 3 },
		HISTORY: { TITLE: "View History of Item", ID: 4 },
		TAG: { TITLE: "Tags and Keywords", ID: 5 },
		USER: { TITLE: "User Access", ID: 6 },
		SECURITY: { TITLE: "Security", ID: 7 },
		ACTION: { TITLE: "Actions", ID: 8 }
	};

var ICONS = {
		TRASH: "UICore/Trash.png"
	};
		

var DEFERRED_CALLS = [];

/*-----------------------------------------------------------------------------------------------------
 * CACHES
 * Global caches for UI elements
 *-----------------------------------------------------------------------------------------------------*/

var UICaches = {

		PopOver: [],
		Gallery: [],
		Rotary: [],
		Dialog: [],
		Editor: []
	
	};


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * Set up an alternative debug console if it isn't supported by the browser...
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

	
function SetUpConsole() {
	if (console == undefined) {
		document.body.appendChild(new Element("div", {id: "debug-console"}));
		window.console = {
			log: function(message) {
				var debugConsole = $("debug-console");
				if (debugConsole != undefined) {
					var htmlMessage = message + "<br />";
					debugConsole.insert(message);
					}
				}
			};
		}
	}
	

AddFunctionToDeferredCalls(SetUpConsole);


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * LOCALISATIONS
 * Localisation strings. These could be stored as a separate file and the relevant language
 * injected into the page as necessary.
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

var Localisations = {
	DateAndTime: {
		Days: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ],
		Months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ],
		TimeSuffix: { AM: "am", PM: "pm" },
		Ordinals: { ST: "st", ND: "nd", RD: "rd", TH: "th" }
		}
	};
	
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * DATE FORMATS
 * Predefined date/time formatters.
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

var DateFormats = {
	ATOM: "Y-m-d\\TH:i:sP",
	COOKIE: "l, d-M-y H:i:s T",
	ISO8601: "Y-m-d\\TH:i:sO",
	RFC822: "D, d M y H:i:s O",
	RFC850: "l, d-M-y H:i:s T",
	RFC1036: "D, d M y H:i:s O",
	RFC1123: "D, d M Y H:i:s O",
	RFC2822: "D, d M Y H:i:s O",
	RFC3339: "Y-m-d\\TH:i:sP",
	RSS: "D, d M Y H:i:s O",
	W3C: "Y-m-d\\TH:i:sP"
	};
	
	
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * UI UTILS
 * Utility code (stored in the UIUtils namespace)
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

var UIUtils = {

	FILES_NOT_LOADED: [],
	FILES_LOADED: NO,
	FILES_TIMER: null,
	JavascriptCode: "",
	NOTIFICATIONS: [],
	NOTIFICATIONS_TIMER: null,
	NOTIFICATION_TIME: 1000,
	
	GetLinkElements: function(relAttributeName) {

		var theLinks = [];
		var linksInDocument = document.getElementsByTagName("a");
		var relTagPattern = new RegExp('^\\b' + relAttributeName + '\\b');
		for (var i = 0; i < linksInDocument.length; i++) {
			var obj = linksInDocument[i];
			if (relTagPattern.test(linksInDocument[i].rel)) {
				theLinks.push(obj);
				}
			}	
		return theLinks;
		},

	/*--------------------------------------------------------------------------------------------------
	 * INSERT ELEMENTS
	 * Inserts a number of elements into a parent element (extends Prototype's Element.insert method)
	 * First parameter is the element which is to have the items inserted
	 *--------------------------------------------------------------------------------------------------*/

	InsertElements: function() {
		var el = (arguments[0] != undefined) ? arguments[0] : null;
		if (el == null) { return };
		var elementCount = arguments.length - 1;
		for (var i = 0; i < elementCount; i++) {
			Element.insert(el, arguments[i+1]);
			}
		},

	/*--------------------------------------------------------------------------------------------------
	 * INJECT JAVASCRIPT FILES
	 * Injects the supplied list of files into the page (but only those which haven't already been added)
	 *--------------------------------------------------------------------------------------------------*/
	
	InjectJavascriptFiles: function(files) {
		if (files == undefined)  { return; }
		var fileList = files.split(",");	
		// Get the list of scripts which have already been loaded
		var scriptTags = document.getElementsByTagName('head')[0].getElementsByTagName('script');
		var alreadyLoaded = new Array();
		for (var i = 0; i < scriptTags.length; i++) {
			alreadyLoaded[i] = scriptTags[i].src;
			}
		// Loop through the file list...
		for (var i = 0; i < fileList.length; i++) {
			if (fileList[i] != "") {
				var fileFound = NO;
				for (var j = 0; j < alreadyLoaded.length; j++) {
					if (alreadyLoaded[j].endsWith(fileList[i])) {
						fileFound = YES;
						}
					}
				if (!fileFound) {
					UIUtils.FILES_NOT_LOADED.push(fileList[i]);
					UIUtils.FILES_LOADED = NO;
					var fileReference = document.createElement('script');
					fileReference.setAttribute('type', 'text/javascript');
					fileReference.setAttribute('src', fileList[i]);
					var thisFile = fileList[i];
					fileReference.onreadystatechange= function () {
						if (this.readyState == "loaded" || this.readyState == "complete") {
							var fileLoaded = Event.findElement(event);
							var index = -1;
							for (var k = 0; k < UIUtils.FILES_NOT_LOADED.length; k++) {
								if (fileLoaded.src.endsWith(UIUtils.FILES_NOT_LOADED[k])) {
									index = k;
									}
								}
							if (index > -1) {
								UIUtils.FILES_NOT_LOADED.splice(index, 1);
								}
							}
					   };
					fileReference.onload = function(event) {
						var fileLoaded = Event.findElement(event);
						var index = -1;
						for (var k = 0; k < UIUtils.FILES_NOT_LOADED.length; k++) {
							if (fileLoaded.src.endsWith(UIUtils.FILES_NOT_LOADED[k])) {
								index = k;
								}
							}
						if (index > -1) {
							UIUtils.FILES_NOT_LOADED.splice(index, 1);
							}
						
						};
					document.getElementsByTagName('head')[0].appendChild(fileReference);
					
					}
				}
			}
		UIUtils.CheckFilesLoaded();
		},

	CheckFilesLoaded: function() {
		var filesLeftToLoad = UIUtils.FILES_NOT_LOADED.length;
		if (filesLeftToLoad > 0) {
			UIUtils.FILES_TIMER = setTimeout(UIUtils.CheckFilesLoaded, 100);
			}
		else {
			UIUtils.FILES_LOADED = YES;
			}
		},
		
	/*--------------------------------------------------------------------------------------------------
	 * INJECT CSS FILES
	 * Injects the supplied list of files into the page
	 *--------------------------------------------------------------------------------------------------*/

	InjectCSSFiles: function(files) {
		if (files == undefined) return;
		fileList = files.split(",");
		var linkTags = document.getElementsByTagName('head')[0].getElementsByTagName('link');
		var alreadyLoaded = new Array();
		for (var i = 0; i < linkTags.length; i++) {
			alreadyLoaded[i] = linkTags[i].href;
			}		
		
		for (i = 0; i < fileList.length; i++) {
			if (fileList[i] != "") {
				var fileFound = NO;
				for (var j = 0; j < alreadyLoaded.length; j++) {
					if (alreadyLoaded[j].endsWith(fileList[i])) {
						fileFound = YES;
						}
					}
				if (!fileFound) {
					var theSheet = document.createElement('link');
					theSheet.setAttribute('type', 'text/css');
					theSheet.setAttribute('rel', 'stylesheet');
					theSheet.setAttribute('href', fileList[i]);
					document.getElementsByTagName('head')[0].appendChild(theSheet);
					}
				}
			}
		},
		
	/*--------------------------------------------------------------------------------------------------
	 * SET ATTRIBUTES
	 * Checks to see which attributes the passed object has and if any of these are mentioned
	 * in the passed attributes, set the object attributes
	 *--------------------------------------------------------------------------------------------------*/

	SetAttributes: function(obj, attrs) {
	
		var objectSettings = Object.keys(obj);
		var attributes = $H(attrs);
		attributes.each(function(item) {
			if (objectSettings.indexOf(item.key) >=0) {
				obj[item.key] = item.value;
				}
			});
		},


	GetDocumentSize: function() {
		var docBody = document.body;
		var docElement = document.documentElement;

		var width = Math.max(	docBody.scrollWidth, 
								docElement.scrollWidth,
								docBody.offsetWidth, 
								docElement.offsetWidth, 
								docBody.clientWidth, 
								docElement.clientWidth
								);
								
		var height = Math.max(	docBody.scrollHeight, 
								docElement.scrollHeight,
								docBody.offsetHeight, 
								docElement.offsetHeight, 
								docBody.clientHeight, 
								docElement.clientHeight
								);
	
		return {width: width, height: height};
		},
		

	/*-----------------------------------------------------------------------------------------------------
	 * CONVERT TEXT LIST TO ARRAY
	 * Content can be stored for a form element in the form: 
	 * FormPopUpMenuList:Deleted = -1,Draft = 1,PendingReview = 2,Published = 4
	 * This method extracts the content from the string and converts to an array of items, with a text
	 * and value for each, suitable for passing to a Select form method
	 *-----------------------------------------------------------------------------------------------------*/

	ConvertTextListToArray: function(listText) {
			var items = new Array();
			var bits = listText.split(":");
			var elements = bits[1].split(",");
			for (var i = 0; i < elements.length; i++) {
				var item = elements[i].split("=");
				items[i] = { text: item[0].strip().PrettifyCamelCase(), value: item[1].strip() };
				}	
			return items;
		},

		
 	/*-----------------------------------------------------------------------------------------------------
	 * DETECT ID
	 * Detect the ID of the element that triggered the passed event. htmlElementType should be set to the
	 * 'parent' container, e.g. with a list item which contains IMG and SPAN tags, use LI.
	 * The idPrefix should be set to any extraneous information to be removed from the ID, e.g.
	 * if the ID is "container-form-element-name", and just 'name' should be extracted,
	 * provide "container-form-element-". The method also stops event propagation.
	 *-----------------------------------------------------------------------------------------------------*/
		
	DetectID: function(event, htmlElementType, idPrefix) {
		event.stop();
		htmlElementType = (htmlElementType == undefined) ? "div" : htmlElementType;
		idPrefix = (idPrefix == undefined) ? "" : idPrefix;
		var element = Event.findElement(event, htmlElementType);
		var id = element.id.substring(idPrefix.length);
		return id;
		},
		
	/*--------------------------------------------------------------------------------------------------
	 * CREATE CLICKABLE BUTTON
	 * Creates a clickable button (using the button sprites set up in UICore/Buttons.png) and
	 * assigns the supplied clickFunction callback to this button.
	 *--------------------------------------------------------------------------------------------------*/

	CreateClickableButton: function(buttonType, cssClass, clickFunction) {
		var theButton = new Element('div', {'class': cssClass, 'title': buttonType.TITLE});
		theButton.setStyle({ backgroundPosition: -(buttonType.ID * BUTTON.DIMENSIONS.WIDTH) + "px 0px" });
		theButton.observe("click", clickFunction);
		return theButton;
		},

	/*--------------------------------------------------------------------------------------------------
	 * DISPLAY DIALOG
	 * Display a dialog box with the specified actions.
	 *--------------------------------------------------------------------------------------------------*/

	DisplayDialog: function(attributes) {
		var obj = this;
		this.dialogPopover = null;
		this.dialog = new Element("div", {'class': "form-block"});		
		this.message = new Element("div", {'class': "popover-form extra-padding-small dialog"});		
		this.message.insert(new Element("img", {src: attributes.icon}));
		this.message.insert(new Element("p").update(attributes.text));
		if (attributes.details != undefined) {
			this.message.insert(new Element("p", {'class': "dialog-detail-text"}).update(attributes.details));	
			}
		this.message.insert(new Element("div", {'class': "clear"}));	
		this.dialog.insert(this.message);
		var formElement = new Element('div', {'class': "form-element"});
		for (var i = 0; i < attributes.actions.length; i++) {
			var id = "dialog-button-" + i;
			var button = new Element('input', {type: "submit", name: id, id: id, value: attributes.actions[i].label});
			button.callback = attributes.actions[i].callback;
			
			button.observe("click", function(event) {
				if (event != null) {
					var el = Event.element(event);
					if (el.callback != null) {
						el.callback();
						}
					obj.dialogPopover.Close();
					
					}
				});
			formElement.insert(button);
			}
		this.dialog.insert(formElement);		
		this.dialogPopover = new Popover("", { width: 400, height: 165, content: this.dialog, title: attributes.title, hasCloser: NO, useScrollOffset: NO }, null, YES);
		},			

	MailContentsOfPage: function(URI) {
		var obj = this;
		this.mailPopover = null;
		this.mailAddress = new Element("div", {'class': "form-block"});		
		
		this.mailPopover = new Popover("", { width: 400, height: 165, content: this.mailAddress, title: "Enter Email Address", hasCloser: NO, useScrollOffset: NO }, null, YES);
		
		},
		
	/*--------------------------------------------------------------------------------------------------
	 * ADD NOTIFICATION
	 * Add a notification to the notifications queue. 
	 *--------------------------------------------------------------------------------------------------*/

	AddNotification: function(message) {
		UIUtils.NOTIFICATIONS.push(message);
		if (UIUtils.NOTIFICATIONS_TIMER == null) {
			UIUtils.DisplayNotification();
			}	
		},
		
		
	DisplayNotification: function() {
		if (UIUtils.NOTIFICATIONS.length > 0) {
			var message = this.NOTIFICATIONS.shift();
			if (UIUtils.note != undefined && UIUtils.note != null) {
				UIUtils.note.remove();
				}
			UIUtils.note = new Element("div", {id: "ui-utils-notification", 'class': "notification"}).update(message);
			document.body.insert({after: UIUtils.note}); 
			UIUtils.NOTIFICATIONS_TIMER = setTimeout(UIUtils.NextNotification, UIUtils.NOTIFICATION_TIME);
			
			}
		else {
			UIUtils.NOTIFICATIONS_TIMER = null;
			}
	
		},
		
	NextNotification: function() {
		UIUtils.note.setStyle({opacity: 0, top: "-50px"});
		
		UIUtils.NOTIFICATIONS_TIMER = setTimeout(UIUtils.DisplayNotification, 1000);
		},
		
	/*--------------------------------------------------------------------------------------------------
	 * GET ELEMENT CSS
	 * Get a complete list of the element's CSS style declarations
	 *--------------------------------------------------------------------------------------------------*/
	
	GetElementCSS: function(element) {
		var styles = new Hash();
		if ('getComputedStyle' in window) {
			var cs = getComputedStyle(element, '');
			if (cs.length != 0) {
				for (var i = 0; i < cs.length; i++) {
					styles.set(cs.item(i), cs.getPropertyValue(cs.item(i)));
					}
				}
			else {
				for (var i in cs) {
					if (cs.hasOwnProperty(i)) {
						styles.set(i, cs[i]);
						}
					}
				}
			}
		else if ('currentStyle' in element) {
			var cs = element.currentStyle;
			for (var k in cs) {
				styles.set(i, cs[k]);
				}
			}
		return styles;
		},
		
	DegreesToRadians: function(degrees) {
		return (Math.PI/180)*degrees;
		},
		
	d2h: function(d) {
		var myhex = parseInt(d).toString(16);
		if (myhex.length == 1)
			myhex = '0' + myhex;
		return myhex;
		},

	h2d: function(h) {
		return parseInt(h,16);
		},

	/*--------------------------------------------------------------------------------------------------
	 * COLOR CONVERSION UTILITIES
	 * Provide color conversion formulae
	 *--------------------------------------------------------------------------------------------------*/

	ConvertHexToRGB: function(hex) {
		// convert the hex colour triad to RGB
		if (hex.substring(0,1) == "#") {
			hex = hex.substring(1);
			}
		var r = this.h2d(hex.substring(0,2));
		var g = this.h2d(hex.substring(2,4));
		var b = this.h2d(hex.substring(4,6));
		return [r, g, b];
		},
	
	ConvertRGBToHex: function(r, g, b) {
		var hex = this.d2h(r) + this.d2h(g) + this.d2h(b);
		return hex;
		},
	
	ConvertRGBToHSV: function(r, g, b) {
		// conversion algorithm from wikipedia.org
		// set the rgb values to 0-1
		r = r / 255; g = g / 255; b = b / 255;
		// max is greatest value
		if (r > g && r > b) max = r; else if (g > b) max = g; else max = b;
		// min is  lowest value
		if (r < g && r < b) min = r; else if (g < b) min = g; else min = b;
		// calculate the hue
		if (max == min)
			h = 0;
		else if (max == r && g >= b)
			h = 60*((g-b)/(max-min));
		else if (max == r && g < b)
			h = 60*((g-b)/(max-min))+360;
		else if (max == g)
			h = 60*((b-r)/(max-min))+120;
		else if (max == b)
			h = 60*((r-g)/(max-min))+240;
		h = h % 360;
		// calculate the saturation
		if (max == 0)
			s = 0;
		else
			s = 1-(min/max);
		// calculate the value
		v = max;
		return [h, s, v];
		},
	
	ConvertHSVToRGB: function(h, s, v) {
		// conversion algorithm from wikipedia.org
		r = g = b = 0;
		if (s == 0) { // saturation is zero
			if (v == 0) { // black
				r = g = b = 0;
				} 
			else { // greyscale
				r = g = b = parseInt(v * 255);
				}
			} 
		else { // saturation isn't zero
			if (h == 360) {
				h = 0;
				}
			h = h/60;
			hi = Math.floor(h); // there are six cases, depending on the hue (0-59 degrees, 60-119, etc)
			f = h-hi;
			p = v*(1-s);
			q = v*(1-s*f);
			t = v*(1-(1-f)*s);
			switch (hi) {
				case 0: r = v; g = t; b = p; break;
				case 1: r = q; g = v; b = p; break;
				case 2: r = p; g = v; b = t; break;
				case 3: r = p; g = q; b = v; break;
				case 4: r = t; g = p; b = v; break;
				case 5: r = v; g = p; b = q; break;
				}
			r = Math.round(r * 255);
			g = Math.round(g * 255);
			b = Math.round(b * 255);
			}
		hexr = this.d2h(r);
		hexg = this.d2h(g);
		hexb = this.d2h(b);
		myhexcolor = hexr + hexg + hexb ;
		return myhexcolor.toUpperCase();
		},

	ConvertCMYKToRGB: function(c, m, y, k) {
		var ck = c*(1-k)+1*k;
		if (ck > 1) ck = 1;
		var mk = m*(1-k)+1*k;
		if (mk > 1) mk = 1;
		var yk = y*(1-k)+1*k;
		if (yk > 1) yk = 1;
		var r = Math.round((1-ck)*255);
		var g = Math.round((1-mk)*255);
		var b = Math.round((1-yk)*255);
		return [r, g, b];
		},
 	
 	ConvertRGBToCMYK: function(r, g, b) {
 		var c = m = y = k = 0;
 		if (r == 0 && g == 0 && b == 0) {
 			k = 1;
 			}
 		else {
 			c = 1 - (r/255);
 			m = 1 - (g/255);
 			y = 1 - (b/255);
 			var min = Math.min(c, Math.min(m, y));
 			c = (c - min) / (1 - min);
 			m = (m - min) / (1 - min);
 			y = (y - min) / (1 - min);
 			k = min;
 			}
 		return [c, m, y, k];
 		},
 		
	GetColorFromText: function(color) {
		var parts = color.split(',');
		var redbit = parts[0].split('(');
		var bluebit = parts[2].split(')');
		var r = redbit[1];
		var b = bluebit[0];
		var g = parts[1];
		return [r, g, b];
		}
	
	};



/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * FORMAT NUMBER
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

/* Taken from 
	http://stackoverflow.com/questions/149055/how-can-i-format-numbers-as-money-in-javascript
*/

function FormatNumber(n, decimals, decimal_sep, thousands_sep) { 
   c = isNaN(decimals) ? 0 : Math.abs(decimals);
   d = decimal_sep || '.';

   /*
   according to [http://stackoverflow.com/questions/411352/how-best-to-determine-if-an-argument-is-not-sent-to-the-javascript-function]
   the fastest way to check for not defined parameter is to use typeof value === 'undefined' 
   rather than doing value === undefined.
   */   
   t = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep; //if you don't want to use a thousands separator you can pass empty string as thousands_sep value

   sign = (n < 0) ? '-' : '';

   //extracting the absolute value of the integer part of the number and converting to string
   i = parseInt(n = Math.abs(n).toFixed(c)) + ''; 

   j = ((j = i.length) > 3) ? j % 3 : 0; 
   return sign + (j ? i.substr(0, j) + t : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : ''); 
	}



/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * EXTEND THE NUMBER OBJECT
 * Add utility functions
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

Object.extend(Number.prototype, {

	/*-----------------------------------------------------------------------------------------------------
	 * TO PADDED STRING
	 * Left pad a number with zeros, optionally supply a base for the result
	 *-----------------------------------------------------------------------------------------------------*/
	
	ToPaddedString: function() {
		var length = (typeof(arguments[0]) != "undefined") ? parseInt(arguments[0]) : 2;
		var base = (typeof(arguments[1]) != "undefined") ? parseInt(arguments[1]) : 10;
		var string = this.toString(base);
		return (length > string.length) ? Array(length + 1 - string.length).join("0") + string : string;	
		},


	/*-----------------------------------------------------------------------------------------------------
	 * TO COLOR PART
	 * Convert an RGB colour part to hex
	 *-----------------------------------------------------------------------------------------------------*/
		
	ToColourPart: function() {
		return this.ToPaddedString(2, 16);
		}

	});


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * EXTEND THE STRING OBJECT
 * Add utility functions
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

Object.extend(String.prototype, {

	Reverse: function() {
		return this.split('').reverse().join('');
		},


	/*-----------------------------------------------------------------------------------------------------
	 * TO URI
	 * Convert a string to its URI equivalent, e.g. "This is a string!" -> "this-is-a-string".
	 *-----------------------------------------------------------------------------------------------------*/
	
	ToURI: function() {
    //	var accents = "ÀÁÂÃÄÅĀĄĂÆÇĆČĈĊĎĐÈÉÊËĒĘĚĔĖĜĞĠĢĤĦÌÍÎÏĪĨĬĮİĲĴĶŁĽĹĻĿÑŃŇŅŊÒÓÔÕÖØŌŐŎŒŔŘŖŚŠŞŜȘŤŢŦȚÙÚÛÜŪŮŰŬŨŲŴÝŶŸŹŽŻàáâãäåāąăæçćčĉċďđèéêëēęěĕėƒĝğġģĥħìíîïīĩĭįıĳĵķĸłľĺļŀñńňņŉŋòóôõöøōőŏœŕřŗśšşŝșťţŧțùúûüūůűŭũųŵýÿŷžżźÞþßſÐð";
	//	var ascii	= "AAAAAAAAAACCCCCDDEEEEEEEEEGGGGHHIIIIIIIIIJJKLLLLLNNNNNOOOOOOOOOORRRSSSSSTTTTUUUUUUUUUUWYYYZZZaaaaaaaaaacccccddeeeeeeeeefgggghhiiiiiiiiijjkklllllnnnnnnoooooooooorrrsssssttttuuuuuuuuuuwyyyzzzppsrdo";
		return this.trim().replace(/[^a-zA-Z \-0-9]+/g,'').toLowerCase().replace(/\s/g, "-");
		}
		
	});
	
	
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * EXTEND THE DATE OBJECT
 * Add utility functions
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

Object.extend(Date.prototype, {

	/*-----------------------------------------------------------------------------------------------------
	 * SET FROM TIMESTAMP
	 * Sets a date from the passed timestamp ("YYYY-MM-DD HH:MM:SS). If none is passed, the date object isn't set
	 * (as it will have been initialised to today's date anyway, we leave it at that)
	 *-----------------------------------------------------------------------------------------------------*/

	SetFromTimestamp: function() {
		if (arguments[0] != "undefined") {
			var t = arguments[0];
			var parts = t.split(" ");
			var dateParts = parts[0].split("-");
			var timeParts = parts[1].split(":");
			this.setFullYear(dateParts[0]);
			this.setMonth(parseInt(dateParts[1])-1); // bloody dates in JS run from 0-11!!!
			this.setDate(dateParts[2]);
			this.setHours(timeParts[0]);
			this.setMinutes(timeParts[1]);
			this.setSeconds(timeParts[2]);
			}
		},
		
	SetFromDateString: function() {
		if (arguments[0] != "undefined") {
			var t = arguments[0];
			if (t != "") {
				var dateParts = t.split("-");
			
				var month = (dateParts[1].substring(0,1) == "0") ? dateParts[1].substring(1) : dateParts[1];
				var day = (dateParts[2].substring(0,1) == "0") ? dateParts[2].substring(1) : dateParts[2];
				this.setFullYear(parseInt(dateParts[0]), month-1, day); // bloody months in JS run from 0-11!!!
				}
			}
		},
		
	SetFromTimeString: function() {
		if (arguments[0] != "undefined") {
			var t = arguments[0];
			if (t != "") {
				var timeParts = t.split(":");
				this.setHours(timeParts[0]);
				this.setMinutes(timeParts[1]);
				this.setSeconds(timeParts[2]);
				}
			}
		},

	/*-----------------------------------------------------------------------------------------------------
	 * DAYS IN MONTH
	 * Return the number of days in the month, accounting for leap years
	 *-----------------------------------------------------------------------------------------------------*/
		
	DaysInMonth: function() {
		var days = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
		var m = this.getMonth();
		return (this.IsLeapYear() && m == 1) ? 29 : days[m];
		},
	
	MonthName: function() {
		var m = this.getMonth();
		return Localisations.DateAndTime.Months[m];
		},

	/*-----------------------------------------------------------------------------------------------------
	 * IS LEAP YEAR
	 * Return whether or not the year is a leap year or not
	 *-----------------------------------------------------------------------------------------------------*/

	IsLeapYear: function() {
		var y = this.getFullYear();
		return (y % 4 == 0) && ((y % 400 == 0) || (y % 100 != 0));
		},

	/*-----------------------------------------------------------------------------------------------------
	 * _TIME SUFFIX
	 * Internal function which returns the time suffix (e.g. AM or PM) for the date object
	 *-----------------------------------------------------------------------------------------------------*/

	_TimeSuffix: function() {
		return (this.getHours() > 11) ? suffix = Localisations.DateAndTime.TimeSuffix.PM : Localisations.DateAndTime.TimeSuffix.AM;
		},

	/*-----------------------------------------------------------------------------------------------------
	 * _HOURS12
	 * Internal function which returns the hours in 12 hour clock format
	 *-----------------------------------------------------------------------------------------------------*/
	
	_Hours12: function() {
		var h = this.getHours();
		if (h > 11) {
			// pm
			h = h - 12;
			}
		if (h == 0) {
			h = 12;
			}
		return h;
		},

	/*-----------------------------------------------------------------------------------------------------
	 * FORMAT
	 * General purpose date and time formatter. This uses an identical format to PHP's date() command.
	 * Note that there are a few options which aren't implemented at present - z, W, t
	 * 
	 * Day:
	 *			d		Day of the month, 2 digits with leading zeros
	 *			D		A textual representation of the day of the week, three letters
	 *			j		Day of the month, without leading zeros
	 *			l		A full textual representation of the day of the week
	 *			N		ISO-8601 numeric representation of the day of the week (Monday = 1 ... Sunday = 7)
	 *			S		Ordinal suffix for the day of the month, 2 characters
	 *			w		Numeric representation of the day of the week (Sunday = 0 ... Saturday = 6)
	 * Month:
	 *			F		Full textual representation of a month
	 *			m		Numeric representation of a month, with leading zeros
	 *			M		A short textual representation of a month, three letters
	 *			n		Numeric representation of a month, without leading zeros
	 *			t		Number of days in the given month
	 * Year:
	 *			L 		Whether it's a leap year (1 or 0)
	 *			Y		A full numeric representation of the year, 4 digits
	 *			y		A two digit representation of the year
	 * Time:
	 *			a		Lowercase ante-meridiem and post-meridiem
	 *			A		Uppercase ante-meridiem and post-meridiem
	 *			g		12-hour format of an hour without leading zeros
	 *			G		24-hour format of an hour without leading zeros
	 *			h		12-hour format of an hour with leading zeros
	 *			H		24-hour format of an hour with leading zeros
	 *			i		Minutes with leading zeros
	 *			s		Seconds with leading zeros
	 * Timezone:
	 *			e		Timezone identifier
	 *			I		Daylight savings time
	 *			O		Difference to GMT in hours and minutes
	 *			P		Different to GMT with colon between hours and minutes
	 *			T		Timezone abbreviation
	 *			Z		Timezone offset in seconds
	 *-----------------------------------------------------------------------------------------------------*/

	Format: function() {
		// This is a bit complicated, as we require regex look-behinds which aren't available
		// in Javascript (as we don't want to convert escaped characters (or in this case, double-escaped)).
		// So we need to reverse the pattern (and the match results) and
		// use a positive lookahead instead!
		// Concept from http://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript
		
		var formatter = (typeof(arguments[0]) != "undefined") ? arguments[0] : "";
		var replacements = function(key, obj) {
			var output = "";
			switch (key) {
				// Day
				case "d":	output = obj.getDate().ToPaddedString(2); break;
				case "D":	output = Localisations.DateAndTime.Days[obj.getDay()].substr(0,3); break;
				case "j":	output = String(obj.getDate()); break;
				case "l":	output = Localisations.DateAndTime.Days[obj.getDay()]; break;
				case "N":	var d = obj.getDay(); output = (d == 0) ? "7" : String(d); break;
				case "S":	var d = obj.getDate(); 
							output = Localisations.DateAndTime.Ordinals.TH;
							if (d == 1 || d == 21 || d == 31) {
								output = Localisations.DateAndTime.Ordinals.ST;
								}
							else if (d == 2 || d == 22) {
								output = Localisations.DateAndTime.Ordinals.ND;
								}
							else if (d == 3 || d == 23) {
								output = Localisations.DateAndTime.Ordinals.RD;
								}
							break;
				case "w":	output = String(obj.getDay()); break;
				case "z":	output = ""; break;	// the day of the year (0-365)
				
				// Week
				case "W":	output = ""; break; // week number of year
				
				// Month
				case "F":	output = Localisations.DateAndTime.Months[obj.getMonth()]; break;
				case "m":	output = (obj.getMonth() + 1).ToPaddedString(2); break;
				case "M":	output = Localisations.DateAndTime.Months[obj.getMonth()].substr(0,3); break;
				case "n":	output = String(obj.getMonth()+1); break;
				case "t":	output = String(obj.DaysInMonth()); break;
				
				// Year
				case "L":	output = (obj.IsLeapYear()) ? "1" : "0"; break;
				case "o":	output = ""; break; // ISO-8601 year number
				case "Y":	output = String(obj.getFullYear()); break;
				case "y":	output = String(obj.getFullYear()).substr(2,2); break;
				
				// Time
				case "a":	output = obj._TimeSuffix().toLowerCase(); break;
				case "A":	output = obj._TimeSuffix().toUpperCase(); break;
				case "B": 	output = ""; break; // Swatch Internet time
				case "g":	output = String(obj._Hours12()); break;
				case "G":	output = String(obj.getHours()); break;
				case "h":	output = obj._Hours12().ToPaddedString(2); break;
				case "H":	output = obj.getHours().ToPaddedString(2); break;
				case "i":	output = obj.getMinutes().ToPaddedString(2); break;
				case "s":	output = obj.getSeconds().ToPaddedString(2); break;
				case "u":	output = ""; break; // Microseconds
				
				// Timezone
				case "e":	output = ""; break; // Timezone identifier
				case "I": 	output = ""; break;
				case "O":	var timeOffset = obj.getTimezoneOffset();
							var sign = (timeOffset < 0) ? "+" : "-";
							var GMTHours = - timeOffset / 60;
							var GMTMinutes = Math.abs(timeOffset % 60);
							GMTHours = GMTHours.ToPaddedString(2);
							GMTMinutes = GMTMinutes.ToPaddedString(2);
							output = sign + GMTHours + GMTMinutes;
							break;
				case "P":	var timeOffset = obj.getTimezoneOffset();
							var sign = (timeOffset < 0) ? "+" : "-";
							var GMTHours = - timeOffset / 60;
							var GMTMinutes = Math.abs(timeOffset % 60);
							GMTHours = GMTHours.ToPaddedString(2);
							GMTMinutes = GMTMinutes.ToPaddedString(2);
							output = sign + GMTHours + ":" + GMTMinutes;
							break;
				case "T":	output = "UTC"; break; // THIS NEEDS FIXED!!!
				case "Z":	output = ""; break;
				
				}
			return output.Reverse();
			};
		var obj = this;
		var result = formatter.Reverse().replace(/[a-z](?!\\)/gi, function(match) { return replacements(match, obj); }).Reverse();
		return result.replace(/\\/gi, "");
		}
		
	});


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * EXTEND THE STRING OBJECT
 * Add utility functions
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

Object.extend(String.prototype, {

	/*-----------------------------------------------------------------------------------------------------
	 * PRETTIFY CAMEL CASE
	 * Converts a fieldname into a caption, e.g. FirstName -> "First Name" (adds spaces at capital letters)
	 *-----------------------------------------------------------------------------------------------------*/

	PrettifyCamelCase: function() {
		return this.replace(/[A-Z]/g, function(match, char) { return (char != 0) ? " " + match : match; });
		},

	/*-----------------------------------------------------------------------------------------------------
	 * ATTRIBUTISE
	 * Converts text to a pseudo-variable name, e.g. "Align Right" -> "align-right"
	 *-----------------------------------------------------------------------------------------------------*/

	Attributise: function() {
		return this.toLowerCase().replace(/\s/g, "-");
		},
		
	/*-----------------------------------------------------------------------------------------------------
	 * TITLE CASE
	 * Converts the string to title case, e.g. "The Quick Brown Fox..." Allows for lower case letters
	 * immediately following punctuation (where there would normally be a space)
	 *-----------------------------------------------------------------------------------------------------*/

	TitleCase: function() {
		return this.toLowerCase().replace(/^[a-z]|[\s,!,\,,\?,\',\",\.][a-z]/g, function(match, char) { return match.toUpperCase(); });
		},

	/*-----------------------------------------------------------------------------------------------------
	 * SENTENCE CASE
	 * Converts the string to sentence case, e.g. "The quick brown fox..."
	 *-----------------------------------------------------------------------------------------------------*/

	SentenceCase: function() {
		return this.toLowerCase().replace(/^[a-z]|[!,\?,\',\",\.][a-z]/gi, function(match, char) { return match.toUpperCase(); });
		}
		
	});
	

/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * DEFERRED CALLS
 * Some scripts (e.g. Google Maps) need to be called when the body.onload event is triggered.
 * Rather than have multiple modules fighting over the event, the Framework holds a list of
 * calls which should be run when the event is triggered. Modules and tags simply
 * add their call using the function 'AddFunctionToDeferredCalls'.
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

function RunDeferredCalls() {
	var deferredCallCount = DEFERRED_CALLS.length;
	for (var i = 0; i < deferredCallCount; i++) {
		call = DEFERRED_CALLS[i];
		call();
		}
	}

function AddFunctionToDeferredCalls(call) {
	DEFERRED_CALLS.push(call);
	}

/*-----------------------------------------------------------------------------------------------------
 *Set up the window onload event to run the deferred call list
 *-----------------------------------------------------------------------------------------------------*/

window.onload = function() { RunDeferredCalls(); };

var imageUpload = null;




/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * UPDATE URI
 * Function to update the value of the URI based on the card Title field
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

function UpdateURI(formName) {
	var title = $(formName + ".Title").getValue();
	var baseURI = $('base-uri').getValue();
	var includeTitleInURI = $('uri-uses-title').getValue();
	if (includeTitleInURI != "") {
		var niceTitle = title.ToURI();
		}
	else {
		niceTitle = "";
		}
	$(formName + ".URI").setValue(baseURI + niceTitle);
	}
	

/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * MODULE
 * Basic module functionality (used to load any configuration settings)
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

var Module = Class.create({ 

	_name: "ModuleName",
	MODULE_PATH: "framework/modules/",
	
	_hasConfigurationSettings: YES,

	/*--------------------------------------------------------------------------------------------------
	 * CLEAR SETTINGS
	 * Clear the containers and other settings which are stored by the module - this method
	 * should be called before DefaultSettings method is called.
	 *--------------------------------------------------------------------------------------------------*/
	
	ClearSettings: function() { /* Override */ },
		
	/*--------------------------------------------------------------------------------------------------
	 * DEFAULT SETTINGS
	 * Default settings for this module - the SetAttributes method is used to override the attributes
	 * set here. Alternatively, just set the settings individually.
	 *--------------------------------------------------------------------------------------------------*/
	
	DefaultSettings: function(settings) {
		UIUtils.SetAttributes(this, settings);
		},
		
	/*--------------------------------------------------------------------------------------------------
	 * INITIALIZE
	 * Load the settings for this module then call the main initialise method.
	 *--------------------------------------------------------------------------------------------------*/

	initialize: function(el, attributes) {
		var obj = this;
		this.ClearSettings();
		if (this._hasConfigurationSettings) {
			this.AjaxJSONRequest("settings", {}, 
				function(response) { 
					obj.DefaultSettings(response.responseJSON);
					obj.Initialise(el, attributes);
					}
				);	
			}
		else {
			this.DefaultSettings();
			this.Initialise(el, attributes);
			}
		},


 	/*-----------------------------------------------------------------------------------------------------
	 * AJAX HTML REQUEST
	 * Helper to simplify Ajax requests (HTML format)
	 *-----------------------------------------------------------------------------------------------------*/
	
	AjaxHTMLRequest: function(call, params, callback) {
		var obj = this;
		new Ajax.Request("/index.php", {
				method: "post",
				parameters: params,
				requestHeaders: { Request: "module-call", Module: obj._name, moduleQuery: call, outputType: "HTML" },
				onSuccess: function(response) { 
					callback(response.responseText); 
					},
				onFailure: function(response) {
					callback();
					}
				});		
		},
		
 	/*-----------------------------------------------------------------------------------------------------
	 * AJAX JSON REQUEST
	 * Helper to simplify Ajax requests (JSON format)
	 *-----------------------------------------------------------------------------------------------------*/
	
	AjaxJSONRequest: function(call, params, callback) {
		var obj = this;
		new Ajax.Request("/index.php", {
				method: "post",
				parameters: params,
				requestHeaders: { Request: "module-call", Module: obj._name, moduleQuery: call, outputType: "JSON" },
				onSuccess: function(response) {
					callback(response.responseJSON); 
					},
				onFailure: function(response) {
					callback();
					}
				});		
		},
		
		
	/*--------------------------------------------------------------------------------------------------
	 * INITIALISE
	 * Main module initialisation method. Called after any configuration settings have been read.
	 *--------------------------------------------------------------------------------------------------*/
		
	Initialise: function(el) { /* Override */ }

	/*--------------------------------------------------------------------------------------------------
	 * END OF MODULE
	 *--------------------------------------------------------------------------------------------------*/
	
	});
	
		
		

/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * DRAW
 * Canvas drawing code (stored in the Draw namespace)
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

var YES = true;
var NO = false;

var DEFAULT_SHADOW_OPACITY = 0.5;
var DEFAULT_COLOR = "rgb(0,0,0)";
var DEFAULT_LINE_WIDTH = 1.0;
var DEFAULT_LINE_CAP = "butt";
var DEFAULT_LINE_JOIN = "miter";
var DEFAULT_LINE_MITER_LIMIT = 10.0;


var Draw = {

	/*--------------------------------------------------------------------------------------------------
	 * ROUNDED RECTANGLE
	 * Draws the path only for a rounded cornered rectangle
	 *--------------------------------------------------------------------------------------------------*/

	RoundedRectangle: function(context, x, y, width, height, radius) {
		context.beginPath();
		context.moveTo(x+radius,y);
		context.lineTo(x+width-radius,y);
		context.quadraticCurveTo(x+width,y,x+width,y+radius);
		context.lineTo(x+width,y+height-radius);
		context.quadraticCurveTo(x+width, y+height, x+width-radius, y+height);
		context.lineTo(x+radius,y+height);
		context.quadraticCurveTo(x, y+height, x, y+height-radius);
		context.lineTo(x, y+radius);
		context.quadraticCurveTo(x, y, x+radius, y);
		context.closePath();
		},

	/*--------------------------------------------------------------------------------------------------
	 * ROUNDED RECTANGLE
	 * Draws the path only for a rounded cornered rectangle
	 *--------------------------------------------------------------------------------------------------*/

	PartlyRoundedRectangle: function(context, x, y, width, height, radius, corners) {
		context.beginPath();
		context.moveTo(x+(radius * (corners && 1 == 1)),y);
		context.lineTo(x+width-(radius * (corners && 2 == 2)),y);
		if (corners && 2 == 2) {
			context.quadraticCurveTo(x+width,y,x+width,y+radius);
			}
		context.lineTo(x+width,y+height-(radius * (corners && 4 == 4)));
		if (corners && 4 == 4) {
			context.quadraticCurveTo(x+width, y+height, x+width-radius, y+height);
			}
		context.lineTo(x+(radius * (corners && 8 == 8)),y+height);
		if (corners && 8 == 8) {
			context.quadraticCurveTo(x, y+height, x, y+height-radius);
			}
		context.lineTo(x, y+(radius * (corners && 1 == 1)));
		if (corners && 1 == 1) {
			context.quadraticCurveTo(x, y, x+radius, y);
			}
		context.closePath();
		},
		
	/*--------------------------------------------------------------------------------------------------
	 * DRAW WEDGE
	 * Draws a wedge for an element of a pie chart
	 *--------------------------------------------------------------------------------------------------*/

	Wedge: function(context, settings) {
		var xOrigin = (settings.x != undefined) ? settings.x : DEFAULT_RADIUS;
		var yOrigin = (settings.y != undefined) ? settings.y : DEFAULT_RADIUS;
		var radius = (settings.radius != undefined) ? settings.radius : DEFAULT_RADIUS;
		var startAngle = (settings.startAngle != undefined) ? settings.startAngle : 0;
		var endAngle = (settings.endAngle != undefined) ? settings.endAngle : 360;
		
		context.beginPath();
		context.moveTo(xOrigin, yOrigin);
		context.arc(xOrigin, yOrigin, radius, UIUtils.DegreesToRadians(startAngle), UIUtils.DegreesToRadians(endAngle), NO);
		context.lineTo(xOrigin, yOrigin);
		context.closePath();
		},
		
	/*--------------------------------------------------------------------------------------------------
	 * LINEAR GRADIENT
	 * Creates a linear gradient based on the set of coordinates (x, y, width, height) and
	 * an array of color stops and color value pairs
	 *--------------------------------------------------------------------------------------------------*/

	LinearGradient: function(context, coords, colorStops) {
		if (colorStops != undefined && colorStops.length > 0 && colorStops.length % 2 == 0) {
			var gradient = context.createLinearGradient(
			(coords.x != undefined) ? coords.x : 0,
			(coords.y != undefined) ? coords.y : 0,
			(coords.width != undefined) ? coords.width : 0,
			(coords.height != undefined) ? coords.height : 0);
			for (var i = 0; i < (colorStops.length / 2); i++) {
				gradient.addColorStop(colorStops[i*2], colorStops[i*2+1]);
				}
			}
		else {
			var gradient = DEFAULT_COLOR;
			}
		return gradient;
		},

	/*--------------------------------------------------------------------------------------------------
	 * SHADOW
	 * Sets the drop shadow attributes based on the settings attributes (x, y, blur, color)
	 *--------------------------------------------------------------------------------------------------*/
		
	Shadow: function(context, settings) {
		context.shadowOffsetX = (settings.xOffset != undefined) ? settings.xOffset : 0;
		context.shadowOffsetY = (settings.yOffset != undefined) ? settings.yOffset : 0;
		context.shadowBlur = (settings.blur != undefined) ? settings.blur : 0;
		context.shadowColor = (settings.color != undefined) ? settings.color : "rgba(0,0,0," + DEFAULT_SHADOW_OPACITY + ")";
		},

	/*--------------------------------------------------------------------------------------------------
	 * CLEAR SHADOW
	 * Clears any previously set drop shadow
	 *--------------------------------------------------------------------------------------------------*/
	
	ClearShadow: function(context) {
		context.shadowOffsetX = 0;
		context.shadowOffsetY = 0;
		context.shadowBlur = 0;
		context.shadowColor = "rgba(0,0,0," + DEFAULT_SHADOW_OPACITY + ")";
		},

	/*--------------------------------------------------------------------------------------------------
	 * FILL
	 * Fills (and therefore closes) the current path using the attributes passed (color only at present)
	 *--------------------------------------------------------------------------------------------------*/
		
	Fill: function(context, settings) {
		context.fillStyle = (settings.color != undefined) ? settings.color : DEFAULT_COLOR;
		context.fill();
		},

	/*--------------------------------------------------------------------------------------------------
	 * STROKE
	 * Strokes (and therefore closes) the current path using the attributes passed 
	 * (color, width, cap, join, miterLimit) - any values not supplied are reset to their default values
	 *--------------------------------------------------------------------------------------------------*/

	Stroke: function(context, settings) {
		context.strokeStyle = (settings.color != undefined) ? settings.color : DEFAULT_COLOR;
		context.lineWidth = (settings.width != undefined) ? settings.width : DEFAULT_LINE_WIDTH;
		context.lineCap = (settings.cap != undefined) ? settings.cap : DEFAULT_LINE_CAP;
		context.lineJoin = (settings.join != undefined) ? settings.join : DEFAULT_LINE_JOIN;
		context.miterLimit = (settings.miterLimit != undefined) ? settings.miterLimit : DEFAULT_LINE_MITER_LIMIT;
		context.stroke();
		},
		
	PopOverContainer: function(context, x, y, width, height, radius, topIsFlat) {
		context.beginPath();
		if (topIsFlat) {
			context.moveTo(x,y);
			context.lineTo(x+width,y);
			context.lineTo(x+width,y+height-radius);
			}
		else {
			context.moveTo(x+radius,y);
			context.lineTo(x+width-radius,y);
			context.quadraticCurveTo(x+width,y,x+width,y+radius);
			context.lineTo(x+width,y+height-radius);
			}
		context.quadraticCurveTo(x+width, y+height, x+width-radius, y+height);
		context.lineTo(x+width/2+radius,y+height);
		context.lineTo(x+width/2,y+height+radius);
		context.lineTo(x+width/2-radius,y+height);
		context.lineTo(x+radius,y+height);
		context.quadraticCurveTo(x, y+height, x, y+height-radius);
		if (topIsFlat) {
			context.lineTo(x, y);
			}
		else {
			context.lineTo(x, y+radius);
			context.quadraticCurveTo(x, y, x+radius, y);
			}
		context.closePath();
		},
		
	
	PopOver: function(context, x, y, width, height, radius) {
		context.save();
		Draw.Shadow(context, { xOffset: 0, yOffset: 4, blur: 4 } );
		Draw.PopOverContainer(context, x, y, width, height, radius, false);
		Draw.Fill(context, {color: "rgb(255,255,255)"});
		context.restore();
		context.save();
		Draw.PopOverContainer(context, x, y, width, height, radius, false);
		Draw.Fill(context, {color: Draw.LinearGradient(context, { "height": height}, [0, "rgb(0,0,0)", 0.23, "rgb(48,48,48)", 0.7, "rgb(0,0,0)", 1, "rgb(0,0,0)"])});
		Draw.Stroke(context, {color: "rgba(0,0,0,0.6)"});	
		context.restore();
		Draw.PopOverContainer(context, x, y + 25, width, height - 25, radius, true);
		Draw.Fill(context, {color: "rgb(255,255,255)"});
		}		

	};
	
	
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * CHARTS
 * Chart drawing code (stored in the Charts namespace)
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

var DEFAULT_PIE_CHART_RADIUS = 50;
var DEFAULT_PIE_CHART_EXPLODED_VALUE = "none"; // No exploded value (can be "none", "all", or "0"..."<num-segments>")
var DEFAULT_PIE_CHART_EXPLODED_OFFSET = 0.2; // Percentage of pie chart radius

var Charts = {

// Charts.PieChart(context, { values: [ 50, 30, 10, 8, 14 ], colors: [ "rgb(255,0,0)", "rgb(255,128,0)", "rgb(128,255,0)", "rgb(0,255,0)", "rgb(0,255,128)"] } );
	PieChart: function(context, attributes) {
		var valueCount = (attributes.values != undefined) ? attributes.values.length : 0;
		var xOrigin = (attributes.x != undefined) ? attributes.x : DEFAULT_PIE_CHART_RADIUS;
		var yOrigin = (attributes.y != undefined) ? attributes.y : DEFAULT_PIE_CHART_RADIUS;
		var chartRadius = (attributes.radius != undefined) ? attributes.radius : DEFAULT_PIE_CHART_RADIUS;
		var explodedSegment = (attributes.explodedSegment != undefined) ? attributes.explodedSegment : DEFAULT_PIE_CHART_EXPLODED_VALUE;
		var explodedOffset = (attributes.explodedOffset != undefined) ? attributes.explodedOffset : DEFAULT_PIE_CHART_EXPLODED_OFFSET;
		
		if (valueCount > 0) {
			var totalValue = 0;
			for (i = 0; i < valueCount; i++) {
				totalValue = totalValue + attributes.values[i];
				}
			var angle = 360 / totalValue;
			var startingAngle = 0;
			for (i = 0; i < valueCount; i++) {
				Draw.Wedge(context, {x: xOrigin, y: yOrigin, radius: chartRadius, startAngle: startingAngle, endAngle: startingAngle + attributes.values[i] * angle });
				Draw.Fill(context, {color: (attributes.colors[i] != undefined) ? attributes.colors[i] : DEFAULT_COLOR });
				
				startingAngle += attributes.values[i] * angle;
				}
		
			}
	
		}
		
	};
	

/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * POPOVER
 * Popover / popup windows
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

var Popover = Class.create( {

	initialize: function(el, settings, callback, isPopUpWindow) {
		this.isPopUpWindow = (isPopUpWindow == undefined) ? NO : isPopUpWindow;
		this.useScrollOffset = (settings.useScrollOffset != undefined) ? settings.useScrollOffset : YES;
		var hasTitleBar = (settings.hasTitleBar != undefined) ? settings.hasTitleBar : YES;
		var hasCloser = (settings.hasCloser != undefined) ? settings.hasCloser : (hasTitleBar ? YES : NO);
		this.hasCanvas = NO;
		this.DefaultSettings();
		this.id = el;
		this.width = settings.width;
		this.height = settings.height;
		var viewport = document.viewport.getDimensions();
		this.left = (settings.left == undefined) ? (viewport.width - settings.width) / 2 : settings.left;
		this.top = (settings.top == undefined) ? (viewport.height - settings.height) / 2 : settings.top;
		this.arrowAtSide = (settings.sideArrow != undefined) ? YES : NO;
		this.highlightClass = (settings.highlightClass != undefined) ? settings.highlightClass : "editor-button-selected";
		this.popoverStyle = (settings.popoverClass != undefined) ? settings.popoverClass : "popover";
		this.callback = callback;
		var radius = (this.isPopUpWindow) ? 10 : 6;
		if (!this.isPopUpWindow && this.highlightClass != undefined && el != undefined) {
			$(el).addClassName(this.highlightClass);
			}
		if (!UICaches.PopOver.include(el)) {
			UICaches.PopOver.push(el);
			
			if (this.isPopUpWindow) {
				this.popOverContainer = new Element('div', {id: "popover-" + this.id, 'class': this.popoverStyle });
				}
			else {
				var parentPosition = $(el).viewportOffset();
				var parentLayout = new Element.Layout($(el));
				var parentWidth = parentLayout.get("width");
				var parentHeight = parentLayout.get("height");
				if (this.arrowAtSide) {
					var canvasOriginX = parentPosition.left + (parentWidth / 2) ;
					var canvasOriginY = parentPosition.top + (parentHeight / 2) - (settings.height / 2) - 4;
					}
				else {
					var canvasOriginX = parentPosition.left + (parentWidth / 2) - (settings.width / 2) - 4;
					var canvasOriginY = parentPosition.top + (parentHeight/2);
					}
				this.popOverCanvas = new Element('canvas', {id: "popover-canvas-" + this.id, width: settings.width + 10, height: settings.height + 30, 'class': "popover-canvas" });
				document.body.appendChild(this.popOverCanvas);
				this.hasCanvas = (this.popOverCanvas.getContext);
				
				if (this.hasCanvas) {
					this.popOverCanvas.setStyle({width: (settings.width + 10) + 'px', height: (settings.height + 25) + 'px', left: canvasOriginX + 'px', top: canvasOriginY + 'px'});
					var context = this.popOverCanvas.getContext('2d');
					this.DrawPopOver(context, settings.width + 10, settings.height + 25, this.arrowAtSide, radius);
					this.popOverContainer = new Element('div', {id: "popover-" + this.id, 'class': "popover-for-canvas" });
					}
				else {
					//console.log("no canvas support");
					this.popOverContainer = new Element('div', {id: "popover-" + this.id, 'class': "popover" });
					}
				}
			var popOverBody = new Element("div", {id: "popover-body-" + this.id, 'class': "popover-content" });
			popOverBody.insert(settings.content);
			
			if (hasTitleBar) {
				var titleClass = (this.isPopUpWindow) ? "popover-title-window" : "popover-title";
				var popOverTitle = new Element("div", {'class': titleClass }).update(settings.title);
				
				if (hasCloser) {
					var popOverCloser = new Element("div", {id: "popover-closer-" + this.id, 'class': "popover-closer" });
					popOverCloser.observe("click", function() { obj.Close(); });
					popOverCloser.setStyle({left: (settings.width - 24) + "px"});
					popOverTitle.appendChild(popOverCloser);
					}
				this.popOverContainer.appendChild(popOverTitle);
				}
				
			this.popOverContainer.appendChild(popOverBody);
			
			
			if (this.isPopUpWindow) {
				this.popOverBackground = new Element("div", {id: "popover-background"});
				}
			else {
				this.popOverBackground = new Element("div", {id: "popover-invisible-background"});
				}
			document.body.appendChild(this.popOverBackground);
		
			
			document.body.appendChild(this.popOverContainer);

			var viewport = document.viewport.getDimensions();
			var scroll = document.viewport.getScrollOffsets();
			if (this.useScrollOffset) {
				this.popOverBackground.setStyle("width:" + viewport.width + "px; height:" + viewport.height + "px; left:" + scroll.left + "px top:" + scroll.top + "px");
				this.popOverContainer.setStyle("width:" + (settings.width - 5) + "px; height:" + settings.height + "px; left:" + ((viewport.width - settings.width) / 2 + scroll.left) + "px; top:" + ((viewport.height - settings.height) / 2 + scroll.top) + "px;");
				}
			else {
				this.popOverBackground.setStyle("width: " +viewport.width + "px; height:" + viewport.height + "px; left: 0; top: 0;");
				this.popOverContainer.setStyle("width:" + settings.width + "px; height:" + settings.height + "px; left:" + this.left + "px; top:" + this.top + "px;");
				
				}

			if (this.arrowAtSide) {
				this.popOverContainer.setStyle({width: (settings.width - 10) + 'px', height: settings.height + 'px', left: (canvasOriginX + 10) + 'px', top: (canvasOriginY + 10) + 'px'});
				}
			else {
				this.popOverContainer.setStyle("width:" + (settings.width - 5) + "px; height: " + settings.height + "px; left:" +  (canvasOriginX + 5) + "px; top:" +  (canvasOriginY + 10) + "px;");
				}

			this.popOverContainer.show();
			var obj = this;
			if ((settings.clickOutsideCloses != undefined && settings.clickOutsideCloses == YES)) { // was  || hasTitleBar == NO
				this.clickMethod = function(event) { obj.CheckForClickOutside(event); };
				Event.observe(document, "click", this.clickMethod);
				}
			if (this.isPopUpWindow) {
				this.resizeMethod = function(event) { obj.WindowResizedOrScrolled(event); };
				Event.observe(window, "resize", this.resizeMethod);
				if (this.useScrollOffset) {
					Event.observe(document, "scroll", this.resizeMethod);
					}
				}
					
		
			}
		},
	
		Update: function(content) {
			$("popover-body-" + this.id).insert(content);
			},
		
		CheckForClickOutside: function(event) {
			event.stop();
			var el = Event.element(event);
			var clickInsidePopover = NO;
			while (el.parentNode) {
				var id = el.id;
				if (id == "popover-" + this.id)	{
					clickInsidePopover = YES;
					}
				el = el.parentNode;
				}
			if (clickInsidePopover == NO) {
				this.Close();
				}
			},
		
		WindowResizedOrScrolled: function(event) {
			Event.stop(event);
			var viewport = document.viewport.getDimensions();
			var scroll = document.viewport.getScrollOffsets();
			this.left = (viewport.width - this.width) / 2;
			this.top = (viewport.height - this.height) / 2;
			this.popOverBackground.setStyle({width: viewport.width + "px", height: viewport.height + "px"});
			if (this.useScrollOffset) {
				this.popOverContainer.setStyle({left: (this.left + scroll.left) + 'px', top: (this.top + scroll.top) + 'px'});
				}
			else {
				this.popOverContainer.setStyle({left: this.left + 'px', top: this.top + 'px'});
				}
			},
			
		DrawPopOver: function(context, width, height, arrowAtSide, radius) {
			context.save();
			Draw.Shadow(context, { xOffset: 0, yOffset: 1, blur: 12, color: "rgba(0,0,0,0.9)" } );
			var centre = parseInt(width / 2);
			var centreH = parseInt(height / 2);
			context.beginPath();
			if (arrowAtSide) {
				context.moveTo(10 + radius,10);
				context.quadraticCurveTo(10, 10, 10, 10 + radius);
				context.lineTo(10, centreH - 8);
				context.lineTo(2, centreH);
				context.lineTo(10, centreH+8);
				context.lineTo(10,height - 15 - radius);
				context.quadraticCurveTo(10, height - 15, 10 + radius, height - 15);
				context.lineTo(width - 10 - radius, height - 15);
				context.quadraticCurveTo(width - 10, height - 15, width - 10, height - 15 - radius);
				context.lineTo(width - 10, 10 + radius);
				context.quadraticCurveTo(width - 10, 10, width - 10 - radius, 10);
				context.lineTo(10 + radius,10);
				}
			else {
				context.moveTo(5, 10 + radius);
				context.quadraticCurveTo(5, 10, 5 + radius, 10);
				context.lineTo(centre-8,10);
				context.lineTo(centre,2);
				context.lineTo(centre+8,10);
				context.lineTo(width - 10 - radius,10);
				context.quadraticCurveTo(width - 10, 10, width - 10, 10 + radius);
				context.lineTo(width - 10,height - 15 - radius);
				context.quadraticCurveTo(width - 10, height - 15, width - 10 - radius, height - 15);
				context.lineTo(5 + radius, height - 15);
				context.quadraticCurveTo(5, height - 15, 5, height - 15 - radius);
				context.lineTo(5, 10 + radius);
				}
			
			context.closePath();
			Draw.Fill(context, {color: "rgb(255,255,255)"});
			Draw.ClearShadow(context);
			context.restore();
			
			if (!arrowAtSide) {
				context.beginPath();
				context.moveTo(centre-8,12);
				context.lineTo(centre-8,10);
				context.lineTo(centre,2)
				context.lineTo(centre+8,10);
				context.lineTo(centre+8,12);
				context.closePath();
				Draw.Fill(context, {color: "rgb(220,220,220)"});
				}		
			
			},
		
		Show: function() {
			if (this.popOverContainer != null) {
				this.popOverContainer.show(); 
				if (this.hasCanvas) {
					this.popOverCanvas.show();
					}
				if (this.isPopUpWindow) {
					this.popOverBackground.show();
					}
				}
			},
			
		Hide: function() {
			if (this.popOverContainer != null) {
				this.popOverContainer.hide(); 
				if (this.hasCanvas) {
					this.popOverCanvas.hide();
					}
				if (this.isPopUpWindow) {
					this.popOverBackground.hide();
					}
				}
			},
			
		Close: function() {
			var obj = this;
			if (!this.isPopUpWindow) {
				if (this.id != undefined) {
					$(this.id).removeClassName(this.highlightClass);
					}
				}
			if (this.popOverContainer != null) {
				this.popOverContainer.stopObserving(); 
				document.body.removeChild(this.popOverContainer);
				if (this.hasCanvas) {
					document.body.removeChild(this.popOverCanvas);
					}
			//	if (this.isPopUpWindow) {
					document.body.removeChild(this.popOverBackground);
			//		}
				}
			// Remove any events...
			Event.stopObserving(document, "click", this.clickMethod);
			Event.stopObserving(window, "resize", this.resizeMethod);
			Event.stopObserving(document, "scroll", this.resizeMethod);
			
			this.popOverContainer = null;
			this.popOverCanvas = null;
			UICaches.PopOver = UICaches.PopOver.without(this.id);
			if (arguments[0] == undefined && this.callback != null) {
				this.callback(null);
				}
			},
			
		DefaultSettings: function() {
		
		}
	});


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * POPOVER FORM
 * Form helper functions for popovers
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

var PopoverForm = {

	Input: function(attributes) {
		var largeLabel = (attributes.largeLabel != undefined & attributes.largeLabel == YES) ? "form-element-large-label" : "";
		var formLabel = new Element('label', {'for': attributes.name, 'class': largeLabel}).update(attributes.label);
		
		var size = (attributes.size != undefined) ? attributes.size : 30;
		var maxlength = (attributes.length != undefined) ? attributes.length : 100;
		var placeholder = (attributes.placeholder != undefined) ? attributes.placeholder : "";
		var formField = new Element('input', {type: attributes.inputType, 'name': attributes.name, size: size, maxlength: maxlength, placeholder: placeholder, id: attributes.name, value: attributes.value});
		
		
		var formElement = new Element('div', {'class': "form-element"});
		formElement.insert(formLabel);
		formElement.insert(formField);
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		formElement.insert(new Element('div', {'class': "clear"}));
		return formElement;
		},


	YesNoSwitch: function(attributes) {
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formField = new Element('input', {type: "hidden", 'name': attributes.name, id: attributes.name, value: attributes.value});
		
		
		var formElement = new Element('div', {'class': "form-element"});
		formElement.insert(formLabel);
		formElement.insert(formField);
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		formElement.insert(new Element('div', {'class': "clear"}));
		return formElement;
		},
		
	ImageScaling: function(attributes) {
		var formLabel = new Element('label', {'for': attributes.name}).update("Maximum size");
		var formControls = new Element('div', {'class': "form-input-small"});
		var formWidth = new Element('input', {type: 'text', 'name': attributes.name + "-width", id: attributes.name + "-width", value: attributes.width});
		var formWidthCaption = new Element('span').update("pixels wide");
		var formHeight = new Element('input', {type: 'text', 'name': attributes.name + "-height", id: attributes.name + "-height", value: attributes.height});
		var formHeightCaption = new Element('span').update("pixels high");
		
		formControls.insert(formWidth);
		formControls.insert(formWidthCaption);
		formControls.insert(formHeight);
		formControls.insert(formHeightCaption);
		
		var formElement = new Element('div', {'class': "form-element"});
		formElement.insert(formLabel);
		formElement.insert(formControls);
		formElement.insert(new Element('div', {'class': "form-help"}).update("Enter the maximum size the image should be."));
		
		formElement.insert(PopoverForm.Buttons({name: attributes.name + "-cropping", label: 'Scaling method', 
													buttons: [	"Scale image to fit within the specified size", "Scale and crop image to exactly the specified size", "Crop the image to exactly the specified size", "Scale small images up to fit within specified size" ], 
													buttonImages: attributes.image, numberOfButtons: 4, selected: attributes.crop, helpText: "Select how the image should be cropped and scaled. Images smaller than the specified size can be scaled up, but may appear pixellated."}));
													
		formElement.insert(new Element('div', {'class': "clear"}));
		return formElement;
		},

	List: function(attributes) {
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formField = new Element('input', {type: "hidden", name: attributes.name, id: attributes.name, value: attributes.chosen});
		var formList = new Element("ul", {'class': "form-list"});
		var items = attributes.items;
		for (i = 0; i < items.length; i++) {
			var value = (items[i].value != undefined) ? items[i].value : i;
			var text = (items[i].text != undefined) ? items[i].text : items[i];
			
			var thisItem = new Element('li').update(text);
			if (i == attributes.chosen) {
				thisItem.addClassName("form-list-selected");
				}
			thisItem.itemValue = value;
			// add an onClick function here
			formList.insert(thisItem);
			}
		var formElement = new Element('div', {'class': "form-element"});
		formElement.insert(formLabel);
		formElement.insert(formField);
		formElement.insert(formList);
		formElement.insert(new Element('div', {'class': "clear"}));
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		return formElement;
		},

	Select: function(attributes) {
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formSelect = new Element('select', {'name': attributes.name, id: attributes.name});
		var items = attributes.items;
		for (i = 0; i < items.length; i++) {
			var value = (items[i].value != undefined) ? items[i].value : i;
			var text = (items[i].text != undefined) ? items[i].text : items[i];
			var isSelected = (items[i].value != undefined) ? (value == attributes.chosen) : (i == attributes.chosen);
			var thisItem = new Element('option', {value: value, selected: isSelected }).update(text);
			formSelect.insert(thisItem);
			}
		var formElement = new Element('div', {'class': "form-element"});
		formElement.insert(formLabel);
		formElement.insert(formSelect);
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		formElement.insert(new Element('div', {'class': "clear"}));
		return formElement;
		},
	
	SelectText: function(attributes) {
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formSelect = new Element('select', {'name': attributes.name, id: attributes.name});
		var items = attributes.items;
		for (i = 0; i < items.length; i++) {
			var text = items[i];
			var value = text;
			var isSelected = (value == attributes.chosen);
			var thisItem = new Element('option', {value: value, selected: isSelected }).update(text);
			formSelect.insert(thisItem);
			}
		var formElement = new Element('div', {'class': "form-element"});
		formElement.insert(formLabel);
		formElement.insert(formSelect);
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		formElement.insert(new Element('div', {'class': "clear"}));
		return formElement;
		},
		
	Hidden: function(attributes) {
		return new Element('input', {type: "hidden", name: attributes.name, id: attributes.name, value: attributes.value});
		},
		
	File: function(attributes) {
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formField = new Element('input', {type: "file", name: attributes.name, id: attributes.name });
		var uploadButton = new Element('input', {type: "submit", 'class': "file-upload-button", value: "Upload"});
		uploadButton.observe("click", attributes.callback);
		var formElement = new Element('div', {'class': "form-element"});
		formElement.insert(uploadButton);
		formElement.insert(formLabel);
		formElement.insert(formField);
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		formElement.insert(new Element('div', {'class': "clear"}));
		
		return formElement;
		},

	FileBrowse: function(attributes) {
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formField = new Element('input', {type: "file", name: attributes.name, id: attributes.name });
		var extraClass = (attributes.style != undefined) ? attributes.style : "";
		var formElement = new Element('div', {'class': "form-element " + extraClass});
		formElement.insert(formLabel);
		formElement.insert(formField);
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		formElement.insert(new Element('div', {'class': "clear"}));
		return formElement;
		},
		
	Buttons: function(attributes) {
		// {name: 'image-alignment', label: 'Alignment', buttonImages: '/framework/ui/editor/image-alignment.png', numberOfButtons: 4, selected: 0}
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formContent = new Element('ul', {'class': "form-buttons"});
		for (i = 0; i < attributes.numberOfButtons; i++) {
			var thisButtonClass = (i == attributes.selected) ? "selected" : "";
			var thisButtonImage = new Element('span', {title: attributes.buttons[i], style: "background: url(" + attributes.buttonImages + ") no-repeat -" + i * 31 + "px 0px;"});
			var thisButton = new Element('li', {id: attributes.name + "-" + i, 'class': thisButtonClass, title: attributes.buttons[i]});
			thisButton.observe("click", function(event) { PopoverForm.ButtonPressed(event, attributes.name); });
			thisButton.insert(thisButtonImage);
			formContent.insert(thisButton);
			}			
		var formChosenButton = new Element('input', {type: "hidden", name: attributes.name, id: attributes.name, value: attributes.selected});	
		var formCurrentButtonText = new Element('span', {id: attributes.name + "-current", 'class': "form-buttons-selected"}).update(attributes.buttons[attributes.selected]);
		
		var formElement = new Element('div', {'class': "form-element no-bottom-rule"});
		formElement.insert(formLabel);
		formElement.insert(formContent);
		formElement.insert(formCurrentButtonText);
		formElement.insert(formChosenButton);
		formElement.insert(new Element('div', {'class': "clear"}));
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		return formElement;
		},
	
	ButtonPressed: function(event, name) {
		// We might need to get the parent node here (<li>) as the button image is held within an inner <span> tag
		var el = Event.element(event);
		if (el.tagName.toLowerCase() != "li") {
			var el = el.parentNode;
			}
		// Toggle the previous button selected state
		$(name + "-" + $(name).getValue()).toggleClassName("selected");
		// Store the new button number and toggle its state
		var newButton = el.id.substring(name.length + 1);
		$(name + "-" + newButton).toggleClassName("selected");
		$(name).setValue(newButton);
		
		$(name + "-current").update($(name + "-" + newButton).title);
		},
	
	TaskbarButtons: function(attributes) {
		var formContent = (attributes.style != undefined) ? new Element('ul', {'class': "form-taskbar-buttons " + attributes.style}) : new Element('ul', {'class': "form-taskbar-buttons"});
		for (i = 0; i < attributes.buttons.length; i++) {
			var id = (attributes.buttons[i].id != undefined) ? attributes.buttons[i].id : "";
			var thisButtonImage = new Element('span', {title: attributes.buttons[i].caption, style: "background: url(" + attributes.buttonImages + ") no-repeat -" + i * 36 + "px 0px;"});
			var thisButton = new Element("li", {id: id});
			thisButton.insert(thisButtonImage);
			thisButton.observe("click", attributes.buttons[i].action);
			formContent.insert(thisButton);
			}
		return formContent;
		},
		

	Tabs: function(attributes) {
		var tabsContainer = new Element("div", {'class': "tab-bar"});
		var tabsList = new Element("ul");
		for (i = 0; i < attributes.tabs.length; i++) {
			var thisTabClass = (i == attributes.selected) ? "tab-selected" : "";
			var content = "<span style='background-position: -" + attributes.tabs[i].icon * 38 + "px 0px;'></span>"+ attributes.tabs[i].caption;
			var thisTab = new Element("li", {id: "popover-tab-" + i, 'class': thisTabClass}).update(content);
			thisTab.observe("click", function(event) { PopoverForm.TabSelected(event, attributes.name); });
			tabsList.insert(thisTab);
			}
		tabsContainer.insert(tabsList);
		var tabSelectedButton = new Element('input', {type: "hidden", name: attributes.name, id: attributes.name, value: attributes.selected});	
		tabsContainer.insert(tabSelectedButton);
		
		return tabsContainer;
		},
		
	TabSelected: function(event, name) {
		var el = Event.findElement(event, 'li');
		var currentTab = $(name).getValue();
		var newTab = el.id.substring(12);
		$(name + "-" + currentTab).toggleClassName("visible");
		$(name + "-" + currentTab).toggleClassName("hidden");
		$(name + "-" + newTab).toggleClassName("hidden");
		$(name + "-" + newTab).toggleClassName("visible");
		$("popover-tab-" + currentTab).toggleClassName("tab-selected");
		$("popover-tab-" + newTab).toggleClassName("tab-selected");
		$(name).setValue(newTab);
		},
		
	Submit: function(attributes, callback) {
		var formElement = new Element('div', {'class': "form-element"});
		if (attributes.status != undefined) {
			var formStatus = new Element("div", {'class': "form-status-area"});
			formStatus.insert(new Element("div", {id: attributes.status + "-rotary", 'class': "form-status-rotary"}));
			formStatus.insert(new Element("div", {id: attributes.status + "-message", 'class': "form-status-message"}));
			formElement.insert(formStatus);
			}
		var submitButton = new Element('input', {type: "submit", name: attributes.name, id: attributes.name, value: attributes.label});
		submitButton.observe("click", callback);
		formElement.insert(submitButton);
		return formElement;
		},

 	/*-----------------------------------------------------------------------------------------------------
	 * ACCESS GROUPS
	 * Display the list of groups which can access content and provide a toggle action to modify
	 *-----------------------------------------------------------------------------------------------------*/

	AccessGroups: function(attributes) {
		var obj = this;
		var formElement = new Element('div', {'class': "form-element"});
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formField = new Element('input', {type: "hidden", name: attributes.name, id: attributes.name, value: attributes.value});
		var accessGroups = attributes.value.split(";");
		var allGroups = ACCESS_GROUPS.split(";");
		formElement.insert(formLabel);
		formElement.insert(formField);
		var groupCount = allGroups.length;
		var groupsList = new Element("ul", {'class': "form-element-status-light"});
		for (var i = 0; i < groupCount; i++) {
			var group = new Element("li", {id: attributes.name + "-group-" + i}).update(allGroups[i]);
			var groupActive = NO;
			for (var j = 0; j < accessGroups.length; j++) {
				if (accessGroups[j] == allGroups[i]) {
					groupActive = YES;
					break;
					}
				}
			if (groupActive) {
				group.setStyle({backgroundPosition: "0px -20px"});
				}
			group.observe("click", function(event) { obj.ToggleAccessGroup(event, attributes.name); });
			groupsList.insert(group);
			}
		// Add in the button for configuring the groups...
		var configureGroups = new Element("li", {'class': "form-element-configure-groups"}).update("Configure groups"); 
		groupsList.insert(configureGroups);
		formElement.insert(groupsList);
		
		formElement.insert(new Element('div', {'class': "clear"}));
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		formElement.insert(new Element('div', {'class': "clear"}));
		return formElement;	
		},

 	/*-----------------------------------------------------------------------------------------------------
	 * TOGGLE ACCESS GROUP
	 * Toggle the value of the selected access group and update the field value
	 *-----------------------------------------------------------------------------------------------------*/
	
	ToggleAccessGroup: function(event, fieldname) {
		var id = UIUtils.DetectID(event, "li", fieldname + "-group-");
		var allGroups = ACCESS_GROUPS.split(";");
		var fieldGroups = $(fieldname).getValue().split(";");
		var groupToToggle = allGroups[id];
		var imageOffset = 0;
		if (fieldGroups.indexOf(groupToToggle) >= 0) {
			// Group exists in list, remove
			fieldGroups = fieldGroups.without(groupToToggle);
			}
		else {
			// Group doesn't exist in list, add
			fieldGroups.push(groupToToggle);
			imageOffset = -20;
			}
		// Update the 'led' graphic
		$(fieldname + "-group-" + id).setStyle({backgroundPosition: "0 " + imageOffset + "px"});
		// Resave the groups list
		$(fieldname).setValue(fieldGroups.join(";"));
		},

 	/*-----------------------------------------------------------------------------------------------------
	 * DETECT ID
	 * Get the ID of the event (and trim any prefix from the ID)
	 *-----------------------------------------------------------------------------------------------------*/
		
	DetectID: function(event, htmlElementType, idPrefix) {
		event.stop();
		htmlElementType = (htmlElementType == undefined) ? "div" : htmlElementType;
		idPrefix = (idPrefix == undefined) ? "" : idPrefix;
		var element = Event.findElement(event, htmlElementType);
		var id = element.id.substring(idPrefix.length);
		return id;
		},
		
 	/*-----------------------------------------------------------------------------------------------------
	 * PASSED ITEM
	 * Helper to simply insert the already defined form item(s) into the standard form element set-up
	 * Define the relevant form field(s) and store in 'item' in the attributes
	 *-----------------------------------------------------------------------------------------------------*/

	PassedItem: function(attributes) {
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formField = attributes.item;
		var formElement = new Element('div', {'class': "form-element"});
		formElement.insert(formLabel);
		formElement.insert(formField);
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		formElement.insert(new Element('div', {'class': "clear"}));
		return formElement;
		},
		
	Icon: function(attributes) {
		var obj = this;
		var formLabel = new Element('label', {'for': attributes.name}).update(attributes.label);
		var formField = new Element('input', {type: "hidden", name: attributes.name, id: attributes.name, value: attributes.value});
		var formElement = new Element('div', {'class': "form-element"});
		containerIcon = new Element("img", {	id: attributes.name + "-image",
												src: attributes.path + attributes.value + ".png",
												alt: attributes.value, 
												title: attributes.value,
												'class': "form-element-image"
									});
		containerIcon.observe("click", function(event) { obj.PopoverIconList(event, attributes); });
		formElement.insert(formLabel);
		formElement.insert(formField);
		formElement.insert(containerIcon);
		if (attributes.helpText) {
			formElement.insert(new Element('div', {'class': "form-help"}).update(attributes.helpText));
			}
		formElement.insert(new Element('div', {'class': "clear"}));
		return formElement;
		},
		
	PopoverIconList: function(event, attributes) {
		var obj = this;
		var id = UIUtils.DetectID(event, "img");
		var field = $(attributes.name);
		var value = field.getValue();
		var iconList = attributes.items;
		this.attributes = attributes;
		// Close any current popover
		this.PopoverClose();

		// Generate the list of icons
		var containerIconsWrapper = new Element("div", {id: attributes.name + "-icon-list", 'class': "form-element-icons"});
		var containerIcons = new Element("div");
		var iconCount = iconList.length;
		var scrollPosition = 0;
		containerIconsWrapper.setStyle({width: "500px", margin: "5px"});
		containerIcons.setStyle({width: (iconCount * (attributes.width + 15)) + "px"});
		
		for (var i = 0; i < iconCount; i++) {
			var thisIcon = new Element("img", {id: "icon-id-" + i, src: attributes.path + iconList[i] + ".png", alt: iconList[i], title: iconList[i]});
			containerIcons.insert(thisIcon);
			if (iconList[i] == value) {
				thisIcon.addClassName("form-element-icons-selected");
				scrollPosition = i * (attributes.width + 15);
				}
			thisIcon.observe("click", function(event) { obj.HidePopoverIconList(event); });
			}
		containerIconsWrapper.insert(containerIcons);
		
		// Now create a popover to allow the user to select from the set of icons
		this.popover = new Popover(id, { width: 520, height: attributes.height + 15 + 20 + 23 + 20, title: "Select an icon", content: containerIconsWrapper, sideArrow: YES, clickOutsideCloses: YES }, function(event) { obj.HidePopoverIconList(event); });
		
		// Set the scroll position (must be done after display in Safari!)
		containerIconsWrapper.scrollLeft = scrollPosition;	
		},
	
	HidePopoverIconList: function(event) {
		if (event != null) {
			// If the event isn't null, the popover hasn't been dismissed by itself
			var id = UIUtils.DetectID(event, "img", "icon-id-");
			$(this.attributes.name).setValue(this.attributes.items[id]);
			$(this.attributes.name + "-image").src = this.attributes.path + this.attributes.items[id] + ".png",
			this.popover.Close();
			}
		},
	
	PopoverClose: function() {
		if (this.popover != undefined & this.popover != null) {
			this.popover = null;
			}	
		}
		
	};



/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 * ROTARY
 * Progress indicator
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

var Rotary = Class.create( {

	_ROTARY_IMAGES: 12,
	
	initialize: function(el, settings) {
		this.DefaultSettings();
		this.timer = null;
		this.rotary = null;
		this.container = null;
		this.id = el;
		UIUtils.SetAttributes(this, settings);
		if (!UICaches.Rotary.include(this.id)) {
			UICaches.Rotary.push(this.id);
			
			this.type = this.type.underscore().dasherize();
			this.container = new Element("div", { id: this.id + "-rotary", "class": "rotary" });
			
			this.rotary = new Element("img", { id: this.id + "-rotary-image", src: "/framework/ui/rotary/" + this.type + "/" + this.type + "-" + this.size + ".png"});
			this.container.insert(this.rotary);
			this.container.setStyle({width: this.size + "px", height: this.size + "px" });
			$(el).insert(this.container);
			var containerDimensions = $(el).getDimensions();
			var top = (containerDimensions.height - this.size) / 2;
			var left = (containerDimensions.width - this.size) / 2;
			this.container.setStyle({left: left + "px", top: top + "px"});
			this.Start();
			}
		},
		
	Start: function() {
		var obj = this;
		var rotaryWidth = $(this.id + "-rotary-image").getWidth();
		this.currentRotaryImage = 0;
		this.timer = setInterval( function() { obj.NextRotaryImage(); }, this.speed);	
		},
	
	NextRotaryImage: function(obj) {
		this.currentRotaryImage = (this.currentRotaryImage + 1) % this._ROTARY_IMAGES;
		var offset = this.currentRotaryImage * this.size;
		if (this.rotary != undefined) {
			this.rotary.setStyle({left: -offset + "px"});
			}
		},
		
	Pause: function() {
		clearInterval(this.timer);
		this.rotary.hide();
		},
		
	Resume: function() {
		this.rotary.show();
		this.Start();
		},
		
	Stop: function() {
		clearInterval(this.timer);
		Element.remove(this.rotary);
		Element.remove(this.container);
		this.container = null;
		this.rotary = null;
		UICaches.Rotary = UICaches.Rotary.without(this.id);		
		},
		
	DefaultSettings: function() {
		this.type = "rotary";
		this.size = 20;
		this.speed = 50;
		this.imageCount = 12;
		}
	});
	

	


