/**
 * Add a dom ready function to inistantiate the UI controller
 */

var kamUi = null;
document.addEvent('domready', function(){
	kamUi = new kamUiController();
});

/**
 * Kameleon : Public user interface
 * 
 * Provides basic support for Kameleon build public interfaces
 *
 * NOTES:
 * 		- Any methods prefixed with two underscores (__) are intended for internal use and should not be called
 * 		
 * @author James Sanders
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */

var kamUiController = new Class({
	Implements: [Options,Events],
	
	className: 'kamUiController',
	
	elementInitialisedClass: 'kamInitialised',
	
	options: {
		/**
		 * cssClasses
		 * 
		 * A list of all the classes that are applied to DOMElements
		 * 
		 * @var object
		 */
		
		cssClasses:{
			fileLink: 'kamFileLink',
			fileLinkTypePrefix: 'kamFileLink',
			first: 'first',
			last: 'last',
			selectedLink: 'selected',
			elementInitialised: 'kamInitialised'
		},
		
		/**
		 * selectors
		 * 
		 * A list of selectors that are used whilst initialising DOMElements
		 * 
		 * @var object
		 */
		
		selectors: {
			aElements: 'a',
			managedForms: 'form.kamForm',
			countrySelect: 'select.kamCountries',
			firstLastPrefixes: [
		        'ul li',
		        'table tr',
		        'tr td',
				'tr th'
	        ]
		},
		
		/**
		 * omittedFileLinkExtensions
		 * 
		 * A list of file extensions that are ignored when handling file link classes 
		 * 
		 * @var array
		 */
		
		validFileExtensions: ['jpg','jpeg','png','gif','ico','pdf','doc','ppt','xls','zip','rar','docx','docm','dotx','dotm','xlsx','xlsm','xltx','xltm','xlsb','xlam','pptx','pptm','potx','potm','ppam','ppsx','ppsm','vcf','txt','rtf','wri','wav','wma','mp4','flv'],
       	
       	/**
       	 * autoInitialise
       	 * 
       	 * Flags which represent whether various resources should or should not be initialised
       	 * 
       	 * @var object
       	 */
       	
       	autoInitialise: {
       		links: {
       			kamPopUp: true,
       			kamVideoModal: true
       		},
       		kamForm: true
       	},
       	
       	/**
       	 * kamVideoModal
       	 * 
       	 * The options used when initialising instance of kamVideoModal links
       	 * 
       	 * @var object
       	 */
       	
       	kamVideoModal: {
			closeMaskOnClick: true,
			width: null,
			height: null,
			autoPlay: true
		},
		
		/**
		 * kamForm
		 * 
		 * The options used when initialising instances of kamForm
		 * 
		 * @var object
		 */
		
		kamForm: {}
	},
	
	/**
	 * setInitialiseOptions
	 *
	 * Sets options which configure how the UI controller initialises DOMElements
	 * 
	 * @param object options
	 */
	
	setInitialiseOptions: function(options){
		this.setOptions(options);
	},
	
	/**
	 * googleAnalytics
	 * 
	 * Stores state information on Google Analytics such as whether it is enabled on this page and
	 * pre-defined keys for custom events
	 * 
	 * @var object
	 */
	
	googleAnalytics: {
		enabled: false,
		categories:{
			fileLinks: {
				key: 'File links',
				actions: {
					clicked: 'Clicked'
				}
			},
			mailToLinks: {
				key: 'Email-to links',
				actions: {
					clicked: 'Clicked'
				}
			}
		}
	},
	
	/**
	 * useAnimations
	 * 
	 * A flag showing whether animations should be used (Morphs / Tweens etc)
	 * 
	 * @var boolean
	 */	
		
	useAnimations: true,
	
	/**
	 * counter
	 * 
	 * Provides a central counter for distributing "unique" IDs - retrieved via getId 
	 */
	
	counter: 0,
	
	/**
	 * initialize
	 * 
	 * The class initialiser, this method performs early parsing of the DOM and fixes commonly
	 * encountered problems
	 */
		
	initialize: function(){
		// get the extended body
		element = document.id(document.body);
		if ( element ){
			// update the body source
			this.initialiseSource(element);
			
			// add the "domReady" class to allow for CSS filtering (if JS is disabled for any reason)
			element.toggleClass('domReady', true);
		}
		
		// determine whether Google Analytics is enabled
		this.googleAnalytics.enabled = ! (typeof(_gaq) == 'undefined');
		
		// add the custom form validators to the global Form.Validator
		kamForm.__addCustomValidators();
	},
		
	/**
	 * disableAnimations
	 * 
	 * Sets the useAnimations flag to FALSE, either based on a browser filter or just as a blanket disable.
	 *
	 * @param string browser
	 * @param integer maxVersion
	 */
	
	disableAnimations: function(browser, maxVersion){
		if ( browser && maxVersion ){
			if ( Browser.name == browser && Browser.version.toInt() <= maxVersion.toInt()  ) this.useAnimations = false;
		} else if ( browser ){
			if ( Browser.name == browser ) this.useAnimations = false;
		} else {
			this.useAnimations = false;
		}
	},
	
	/**
	 * initialiseSource
	 * 
	 * Initialise an element's source performing such updates as fixing registered Rs
	 * 
	 * @param DOMElement element
	 */
	
	initialiseSource: function(element){
		// prepare the event argument which will return initalised elements to interested parties
		var initialisationDetails = {
			element: element
		};
		
		// fix (r) and TM symbols
		element.innerHTML = element.innerHTML.replace(/®/g, '<span class="superscript">®</span>');
	},
	
	/**
	 * initialise
	 * 
	 * Initialises a DOMElement and implements common fixes and functionality extensions. 
	 * 
	 *  @param DOMElement element (OPTIONAL) the element to parse (defaults to BODY)
	 */
	
	initialise: function(element){
		var initialisedClass = this.elementInitialisedClass;
		
		// prepare the event argument which will return initalised elements to interested parties
		var initialisationDetails = {
			element: element
		};
		
		// check to see whether an element has been supplied. if not, use the body element
		if ( ! element ) element = document.body;
		
		// ensure the element has been extended
		element = document.id(element);
		
		// set "first" and "last" classes on relevant items
		this.options.selectors.firstLastPrefixes.each( function(prefix){
			element.getElements(prefix+":first-child").each( function(elm){
				elm.toggleClass(this.options.cssClasses.first, true);
			}.bind(this));
			element.getElements(prefix+":last-child").each( function(elm){
				elm.toggleClass(this.options.cssClasses.last, true);
			}.bind(this));
		}.bind(this));
		
		// update the a elements
		var currentUri = new URI(window.location);
		currentUri.setData({});
		currentUri = currentUri.toString();
		element.getElements(this.options.selectors.aElements).each( function(elm){
			// check to see whether this element has already been initialised
			if ( ! elm.hasClass(initialisedClass) ){
				// add the initalised class to prevent doubling up
				elm.addClass(initialisedClass);
				
				// check to see whether the A tag has a REL tag and whether it relates to a UI action. if it 
				// does apply the relevant handlers
				var rel = elm.get('rel');
				if( rel ){
					switch ( rel ){
						// set-up safe pop ups
						case 'kamPopUp' :
							if ( this.options.autoInitialise.links.kamPopUp ){
								elm.addEvent('click', function(evt){
									evt.preventDefault();
									window.open(elm.href);
								});
							}
							break;
						
						default: 
							// check for instances which contain dynamic trailing data (i.e. match based on
							// a prefix)
							if ( rel.indexOf('kamVideoModal') >= 0 ){
								if ( this.options.autoInitialise.links.kamVideoModal ){
									elm.addEvent('click', function(evt){
										evt.preventDefault();
										
										var href = elm.get('href');
										if ( href ){
											var modalOptions = Object.clone(this.options.kamVideoModal);
	
											// split down the rel and check to see whether there is a width and a 
											// height set
											var relElements = rel.split('-');
											if ( relElements.length >= 2 ){
												modalOptions.videoWidth = relElements[1];
											}
											if ( relElements.length >= 3 ){
												modalOptions.videoHeight = relElements.videoHeight;
											} 
											
											if ( typeof(modalOptions.title) == 'undefined' ){
												var title = elm.get('title');
												modalOptions.title = ! title ? 'Video' : title; 
											}								
											new kamVideoModal(href, modalOptions).show();
										}
									}.bind(this));
								}
							}
							
							break;
					}
				}
				
				// retrieve the element's HREF and ensure that it has a value - all further actions require
				// the HREF to exist
				var elementHref = elm.get('href');
				if ( elementHref ){
					// compare the href of the link with the current page location, unless the href is '#' which
					// normally denotes that it is 'not a real link', i.e. is purely used as a JavaScript action 
					if ( elementHref != '#' ){
						var hrefUri = new URI(elementHref);
						hrefUri.setData({});
						if ( hrefUri.toString() == currentUri ){
							// it is a match. apply the selected state to the A tag
							elm.toggleClass(this.options.cssClasses.selectedLink,true);
							
							// check to see whether the direct parent is an LI. it is apply the selected
							// class to that as well
							if ( elm.parentNode.tagName == 'LI' ){
								document.id(elm.parentNode).toggleClass(this.options.cssClasses.selectedLink,true);
							}
						}
					}
					
					// check to see whether this A tag links to a file by looking for the last full stop which 
					// will hopefully be the break between a file name and its extension
					var fileExtensionBreak = elementHref.lastIndexOf('.');
					if ( fileExtensionBreak ){
						// we've found a break. now validate the string to make sure it is flat text - if it
						// isn't the chances are that there was a fullstop in the URI path which we should ignore
						var extension = elementHref.substring(fileExtensionBreak+1).toLowerCase();
						if( extension.test(/^[a-z0-9]+$/) ){
							// this appears to be a file but before we get too excited check to see whether it is in the
							// validFileExtensions object
							if ( this.options.validFileExtensions.contains(extension) ){
								// add the default file link class
								elm.toggleClass(this.options.cssClasses.fileLink, true);
								
								// in addition to the default file link class, add an extension specific class
								elm.toggleClass(this.options.cssClasses.fileLinkTypePrefix+extension.substr(0, 1).toUpperCase()+extension.substr(1), true);
								
								// check to see whether Google Analytics is enabled. 
								if ( this.googleAnalytics.enabled ){
									// it is. add a analytics tracking handler to the tag
									elm.addEvent('click', function(){
										var settings = this.googleAnalytics.categories.fileLinks;
										_gaq.push(['_trackEvent', settings.key,settings.actions.clicked, elementHref]);
									}.bind(this));
								}
							}
						}
					}
					
					// check to see whether this is a mailto link (but only if GA is enabled)
					if ( this.googleAnalytics.enabled ){
						if ( elementHref.substring(0, 7).toLowerCase() == 'mailto:' ){
							// it is. add a analytics tracking handler to the tag
							elm.addEvent('click', function(){
								var settings = this.googleAnalytics.categories.mailToLinks;
								_gaq.push(['_trackEvent', settings.key,settings.actions.clicked, elementHref.substring(7)]);
							}.bind(this));
						}
					}
				}
			}
		}.bind(this));
		
		// find any managed forms and extend them with an instance of kamForm
		if ( this.options.autoInitialise.kamForm ){
			initialisationDetails.forms = [];
			element.getElements(this.options.selectors.managedForms).each( function(elm){
				// check to see whether this elm has already been initialised
				if ( ! elm.hasClass(initialisedClass) ){
					// add the initalised class to prevent doubling up
					elm.addClass(initialisedClass);
					
					initialisationDetails.forms.push( new kamForm(elm, Object.clone(this.options.kamForm)) );
				}
			}.bind(this));
		}
		
		// find any country selects and populate them
		if ( this.options.autoInitialise.kamForm ){
			initialisationDetails.countrySelects = [];
			element.getElements(this.options.selectors.countrySelect).each( function(elm){
				// check to see whether this elm has already been initialised
				if ( ! elm.hasClass(initialisedClass) ){
					// add the initalised class to prevent doubling up
					elm.addClass(initialisedClass);
					
					kamForm.populateCountrySelect(elm);
					initialisationDetails.countrySelects.push(elm);
				}
			}.bind(this));
		}

		// fire the initialise event
		this.fireEvent('initialiseElement', element);
		
		// fire the initialised event
		this.fireEvent('elementInitialised', initialisationDetails);
	},

	/**
	 * isGoogleAnalyticsEnabled
	 * 
	 * Returns a flag showing whether Google Analytics is enabled on this page
	 * 
	 * @returns boolean
	 */
	isGoogleAnalyticsEnabled: function(){
		return this.googleAnalytics.enabled;
	},
	
	/**
	 * getId
	 * 
	 * Retrieves the next "unique" integer ID. 
	 * 
	 * NOTE: The controller maintains an integer counter for assigning IDs. You must ensure that you prefix
	 * the ID with an appropriate string to maintain validity and uniqueness 
	 * 
	 * @param string prefix (OPTIONAL) the prefix to add to the ID
	 * @return integer||string
	 */
	
	getId: function(prefix){
		return (prefix) ? prefix + ++this.counter : ++this.counter; 
	},
	
	/**
	 * formatSecondsToHumanReadable
	 * 
	 * Converts a integer representing seconds into a human readable format
	 * 
	 * @param integer timeInSeconds
	 * @return string
	 */
	
	formatSecondsToHumanReadable: function (timeInSeconds){
		var minutes = (timeInSeconds / 60).toInt();
		timeInSeconds = (timeInSeconds - (minutes * 60)).toInt();
		if ( timeInSeconds < 10 ){
			timeInSeconds = '0'+timeInSeconds;
		}
		return minutes+':'+timeInSeconds;
	},
	
	/**
	 * getElementDimensions
	 * 
	 * Retreives the X and Y dimensions of an element by checking its CSS values and failing that
	 * measuring it. Returns an object wiith 'x' and 'y' properties 
	 * 
	 * @return object
	 */
	getElementDimensions: function(element){
		var elementMeasurements = null;
		var getElementMeasurements = function(){
			if ( ! elementMeasurements ){
				// there is no valid computed dimension. use the measured size
				elementMeasurements = element.measure(function(){
					return this.getSize();
				});	
			}
			return elementMeasurements;
		};
		
		// try to get the computed dimension. NOTE: IE sometimes returns a dimension of 0px when no value
		// is applied (thanks to its internal currentStyle object rather than the computeStyle of
		// other browesrs) meaning that the value is useless. if the style is returned but has a value 
		// of "0px" then ignore it (as long as it's IE - we trust real browsers)
		var dimensionHandler = function(styleKey){
			var dimension = element.getStyle(styleKey);
			if ( dimension && (Browser.ie === false || dimension != '0px') ){
				// get the integer value of the dimension and check to see whether it is valid (it may be set
				// to 'auto' or similar)
				var dimension = dimension.replace('px','');
				if ( isNaN(dimension) == false ){
					// the computed dimension is valid
					return dimension.toInt();
				} else {
					// there is no valid computed dimension. use the measured size
					return (styleKey == 'width') ? getElementMeasurements().x : getElementMeasurements().y;
				}
			} else {
				// there is no computed dimension. use the measured size
				return (styleKey == 'width') ? getElementMeasurements().x : getElementMeasurements().y;
			}
		}
		
		return {
			x: dimensionHandler('width'),
			y: dimensionHandler('height')
		};
	},
	
	/**
	 * parseCssString
	 * 
	 * Converts a CSS style string into an object
	 * 
	 * @param string input
	 * @return object
	 */
	
	parseCssString: function(input){
		var output = {};
		input.split( ';' ).each( function(part){
			var elementParts = part.split( ':' );
			if( elementParts.length == 2 ) output[elementParts[0].trim()] = elementParts[1].trim();
		});
		return output;
	},

	getNewClear: function(){
		return new Element('div[class=kamAdminClear][html=&#160;]');
	}
});

var kamGlobalEvents = new Class({
	Extends: Events,

	fireEvent: function(event, owner, parameters){
		if (typeof parameters !== 'object') parameters = {};
		parameters.owner = owner;             
		       
		this.parent(event, parameters);
	}
});
 
var kamUiEvents = new Class({
	Extends: Events,
	
	globalEventsHandler: null,
	
	fireEvent: function(event, parameters){
		// fire the standard event
		this.parent(event, parameters);
		
		// fire the global event
		if ( this.globalEventsHandler ) this.globalEventsHandler.fireEvent(event, this, parameters);
	}
});
 
/**
 * kamBrowserCommands
 * 
 * A class which provides the ability for the server to return instructions which will be executed 
 * within the scope of a user's browser. This allows for complex interaction outside the realm of 
 * normal HTTP responses such as opening / closing modals, displaying notifications or providing
 * a goat to the clan. And there was much celebration. 
 * 
 * 
 * CHANGE LOG:
 * ===================
 * 
 * 17/06/2011 (Version 1.0.0)
 * --------------------------
 * 
 * @author James Sanders
 * @version 1.0.0
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */

//var kamBrowserCommands = new Class({
//	/**
//	 * config
//	 * 
//	 * Stores the configuration of this command set
//	 * 
//	 * @var object
//	 */
//	
//	config: null,
//	
//	/**
//	 * initialize
//	 * 
//	 * Class constructor
//	 * 
//	 * @param object config
//	 */
//	
//	initialize: function(config){
//		this.config = config;
//	}, 
//	
//	/**
//	 * process
//	 * 
//	 * Parses the supplied configuration and executes the commands using the defined commands
//	 */
//	
//	process: function(){
//		// check basic instructions
//		var config = this.config;
//		if ( config.commands ){
//			config.commands.each( function(command){
//				var key = command.key;
//				
//				// check to see whether this instruction is supported
//				if ( kamBrowserCommands.commandTypes[key] ){
//					// it is. fire the handler
//					kamBrowserCommands.commandTypes[key](command);
//				}
//			});
//		}
//	}
//});

/**
 * kamBrowserCommands.commandTypes
 *
 * Stores the defined command types
 * 
 * @var object
 */

var kamBrowserCommands = {};


/**
 * kamBrowserCommands.process
 * 
 * Checks the passed response and, if a valid string, evaluates it as a set of browser instructions
 * and processes them. 
 * 
 * @param string response a flat copy of kamBrowserCommands
 * @returns boolean TRUE if the parameter contains a valid instruction set, FALSE if not 
 */

kamBrowserCommands.process = function(config){
	// exexute the supplied commands
	if ( config.commands ){
		config.commands.each( function(command){
			var key = command.key;
			if ( key ){
				// check to see whether this instruction is supported
				if ( kamBrowserCommands.commandTypes[key] ){
					// it is. fire the handler
					kamBrowserCommands.commandTypes[key](command);
				}
			}
		});
	}
};

kamBrowserCommands.commandTypes = {};

/**
 * kamBrowserCommands.defineCommand
 *
 * Adds a command type to global handler
 * 
 * @param string key the unique key of the command type
 * @param function handler the handler of the command
 */

kamBrowserCommands.defineCommand = function(key, handler){
	kamBrowserCommands.commandTypes[key] = handler;
};

/**
 * add the redirectBrowser command to the handler
 */

kamBrowserCommands.defineCommand('redirectBrowser', function(config){
	if ( config.url ) new URI(config.url).go();
});

/**
 * add the setContent command to the handler
 */

kamBrowserCommands.defineCommand('setContent', function(config){
	// get the target container
	var element = document.id(config.targetId);
	if ( element ) kamFx.updateContent(element, config.content);
});

/**
 * Kameleon : Content request
 * 
 * Retrieves external content via an AJAX lookup and returns either the whole response or parts of
 * it based on the specified options
 * 
 * Options:
 * 		useUrlTimestamping (boolean): A flag showing whether a time stamp will be added to the request URL to avoid caching
 * 		onSuccess (function): A event handler for the "onSuccess" event
 * 		onFailure (function): A event handler for the "onFailure" event
 * 		retrieve (null,string or array): the elements to retrieve from the external page. The valid options are:
 * 			null: the entire page body will be returned
 * 			string: the DOMElement with an id of the supplied value will be returned
 * 			array: an object containing all elements whose id was within the array
 * 
 * Events:
 * 	onSuccess
 * 		Fired once the content has been loaded and parsed
 * 		Signature: function(response, responseTree, responseElements, responseHtml, responseJavaScript)
 * 		Parameters:
 * 			response (string): element or array of elements based on the "retrieve" option
 * 			responseTree (nodelist): nodelist
 * 			responseElements (array): array of elements
 * 			responseHtml (string): the full response HTML
 * 			responseJavaScript (string): the full javascript 
 * 
 * 	onFailure
 * 		Fired is the request fails
 * 		Signature: function() 
 * 
 * 	requestInitialise
 * 		Fired when the request object is initialised but before it is executed
 * 		Signature: function(request)
 * 		Parameters:
 * 			request (Request.HTML): the request object
 * 			
 * 
 * CHANGE LOG:
 * ===================
 * 
 * 15/02/2011 (Version 1.0.0)
 * --------------------------
 * 
 * 24/03/2011 (Version 1.0.1)
 * --------------------------
 * Method: execute
 * 		Updated to notify Google Analytics of page requests, if GA is enabled
 * 
 * @author James Sanders
 * @version 1.0.1
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */

var kamContentRequest = new Class({
	Implements: [Events],
	
	className: 'kamContentRequests',
	
	options: {
		useUrlTimestamping: true,
		onSuccess: null,
		onFailure: null,
		retrieve: null,
		evalScripts: false,
		method: 'get',
		data: null,
		autoHandleJavascriptResponses: true
	},
	
	/**
	 * url
	 * 
	 * The url to access on execution
	 */
	
	url: null,
	
	/**
	 * initialize
	 * 
	 * Class constructor
	 * 
	 * @param string the url to load
	 * @param object options the options to load
	 */
	
	initialize: function(url, options){
		// ensure the URL is a string
		this.url = (typeof url == 'string') ? url : url.toString();
		if ( typeof(this.url) == 'string' ){
			this.setOptions(options);
			options = this.options;
			
			// check to see whether any quick event handlers have been set
			if ( options.onSuccess ) this.addEvent('onSuccess', this.options.onSuccess);
			if ( options.onJavascriptHandled ) this.addEvent('onJavascriptHandled', this.options.onJavascriptHandled);
			if ( options.onFailure ) this.addEvent('onFailure', this.options.onFailure);
		} else {
			alert("ERROR > kamContentRequest > initialize > url is not a string")
		}
	},
	
	/**
	 * setOptions
	 * 
	 * Merge the supplied options object with the existing options
	 * 
	 * @param object options the options to merge
	 */
	
	setOptions: function(options){
		this.options = Object.merge(this.options, options);
	},
	
	/**
	 * execute
	 * 
	 * Begins the request process 
	 */
	
	execute: function(){	
		// check to see whether URL time stamping is enabled
		var options = this.options;
		var url = this.url;
		if ( options.useUrlTimestamping ){
			// update the url to include the current timestamp to get around page caching
			var urlObject = new URI(url);
			urlObject.setData({
				'kamReqTime': new Date().getTime()
			},true);
			url = urlObject.toString();
		}

		// build the request and execute
		var request = new Request.HTML({
			method: options.method,
			url: url,
			data: options.data,
			evalScripts: options.evalScripts,
			onSuccess: function(responseTree, responseElements, responseHtml, responseJavaScript){				
				// check to see whether the response is Javascript (and whether we can auto handle it)
				var responseHandled = false;	
				if ( options.autoHandleJavascriptResponses && request.getHeader('Content-type').contains('javascript') ){
					// it is. we can only handle JSON responses so make a best guess as to whether its
					// JSON by checking to see whether the character is an object bracket ({)
					if ( responseHtml.substr(0,1) === '{' ){
						// it is. we will assume that it's JSON so try to decode it
						var data = JSON.decode(responseHtml);
						
						// check to see whether the response correctly supplies a type key
						if ( data.type ){
							// it does. execute the relevant handler
							switch ( data.type ){
								case 'commands':
									responseHandled = true;
									kamBrowserCommands.process(data);
									break;
							}
						}
						
						// fire the Javascript handled event
						if ( responseHandled ) this.fireEvent('onJavascriptHandled');
					}
				} 

				// check to see whether the response was automatically handled. if it wasn't perform
				// the default behaviour
				if ( responseHandled === false ){
					// set the default response to FALSE
					var response = false;
					
					// check to see whether we need to pull out specific elements from the response
					var retrieve = options.retrieve;
					if ( retrieve ){
						// yes. determine whether it is a string or an array (are we after a single item or multiple?
						switch ( typeof(retrieve) ){
							case 'string': 
								// search the tree for the element ID
								responseElements.each( function(element){
									// pull the ID (if available) from the element
									if ( element.get('id') == retrieve ){
										response = element;
									}
								});
								break;
							case 'object': 
								// we need to pull multiple items. prep the response and then run through the elements
								// looking for the keys
								response = {};
								responseElements.each( function(element){
									// pull the ID (if available) from the element
									var elementId = element.get('id');
									if ( elementId ){
										retrieve.each( function(id){
											if ( elementId === id ){
												response[id] = element;
											}
										});
									}
								});
								break;
						}
					} else {
						// nope. just return the whole response
						var response = responseHtml;
					}
					
					// return the result via an event
					this.fireEvent('onSuccess', [response, responseTree, responseElements, responseHtml, responseJavaScript]);
	
					// if Google Analytics is enabled, call the trackPageView method for the page we just
					// loaded to preserve accurate tracking stats
	  				if ( kamUi.isGoogleAnalyticsEnabled() ){
	  					// NOTE: use the url property, rather than the url variable, because we don't want
	  					// the kamRequestTime attribute
	  					var uri = new URI(this.url);
	  					_gaq.push(['_trackPageview', uri.toAbsolute()]);	
	  				}
				}
			}.bind(this),
			onFailure: function(){		
				// something went wrong, fire a failure event
				this.fireEvent('onFailure');
			}.bind(this)
		});
		
		// fire the requestInitialise event
		this.fireEvent('requestInitialise', request);
		
		// make the request
		request.send();
	}
});

/** 
 * Kameleon : Content changer base
 * 
 * A base for content changer classes that use one or more content cells
 * 
 * Options (all options of kamContentChangerBase are also valid):
 * 		cellsElement (DOMElement): A container that holds one or more content cells to load
 * 		contentCellSelector (string): The selector to use when searching for content cells within the cellsElement
 * 		linksElement (DOMElement): A container that holds one or more A tags to which represent remote content cells
 * 		linksElementIsNavigation (boolean): A flag showing whether the links in the links element should be managed as a navigation (defaults to FALSE)
 * 		linkSelector (string): The selector to use when searching for content A tags within the linksElement (only A tags will be accepted)
 * 		navigationWrapperElement (DOMElement): The DOMElement to inject a UL navigation into
 * 		navigationSelectedClass (string): The class to apply to the currently displayed navigation item (defaults to 'active')
 * 		onCellAdd (function): Handler for the cellAdded event
 * 		onCellLoad (function): Handler for the cellLoaded event
 * 		cellStoreParent (DOMElement): The DOMElement which the cell store will be adopted by (if not set BODY will be used)
 * 
 * Events:
 * 		cellAdded
 * 			Fired when a cell is added to the content cell collection
 * 			Signature: function(cell)
 * 			Parameters:
 * 				cell (cell object - see appendix): the cell that was added
 * 
 * 		cellLoaded
 * 			Fired when an external content cell is loaded
 * 			Signature: function(cell)
 * 			Parameters:
 * 				cell (cell object - see appendix): the cell that was loaded
 * 
 * Appendix:
 * 		cell object
 * 			The JSON object that stores the configuration of a cell
 * 			Properties:
 * 				element (DOMElement): The element of the cell (only available once loaded)
 * 				isLoaded (boolean): A flag showing whether the cell is loaded
 * 				linkElement (DOMElement): The link element that an external content cell was created from (only set if this is an external content cell) 
 * 				elementId (string): The element ID to retrieve during an external load (only set if this is an external content cell)
 * 				index (integer): The cell's index in the internal array
 * 
 * 		links cells
 *			Cells that are added via links (A tags) need to have two attributes set - href and rel. Href defines
 *			the URL that content should be retrieved from and rel defines the ID of an element in the URL that should
 *			be retrieved and used as the cell content 		
 * 
 * NOTES:
 * 		- Any methods prefixed with two underscores (__) are intended for internal use and should not be called
 * 
 * 
 * CHANGE LOG:
 * ===================
 * 
 * 15/04/2011 (Version 1.0.1)
 * --------------------------
 * Option: cellStoreParent
 * 		New option which allows the user to define the parent DOM element for the cell store
 * 
 * Method: __createBaseComponents
 * 		Updated to handle the cellStoreParent option
 * 
 * @author James Sanders
 * @version 1.0.1
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */

var kamContentChangerBase = new Class({
	Implements: [Events],
	options: {
		cellsElement: null,
		contentCellSelector: '.cell',
		linksElement: null,
		linksElementIsNavigation: false,
		linkSelector: 'a',
		onCellAdd: null,
		onCellLoad: null,
		navigationWrapperElement: null,
		navigationSelectedClass: 'active',
		cellStoreParent: null
	},
	
	/**
	 * storeElement
	 * 
	 * A container element which is rendered off screen and contains all currently unused content cells
	 * 
	 * @var DOMElement
	 */
	
	storeElement: null,
	
	/**
	 * storeElementClass
	 * 
	 * The class name applied to the storeElement
	 * 
	 * @var string
	 */
	
	storeElementClass: 'kamContentChangerStore',
	
	/**
	 * naviationElement
	 * 
	 * The navigation UL element (if enabled)
	 * 
	 * @var DOMElement
	 */
	
	navigationElement: null,
	
	/**
	 * cells
	 * 
	 * An array of content cells
	 * 
	 *  @var array
	 */
	
	cells: [],
	
	/**
	 * pointer
	 * 
	 * The current cell pointer
	 * 
	 * @var integer
	 */
	
	pointer: null,
	
	/**
	 * previousPointer
	 * 
	 * Stores the index of the previously displayed cell 
	 */
	
	previousPointer: null,
	
	/**
	 * storeKeyLinkCellIndex
	 * 
	 * The key that is used when storing a cell index on a manged navigation A tag
	 * 
	 * @var string
	 */
	
	storeKeyLinkCellIndex: 'kamContentChangerBaseCellIndex',
	
	/**
	 * initialize
	 * 
	 * Class constructor
	 * 
	 * @param object options (OPTIONAL) the options of the class 
	 */
	
	initialize: function(options){
		this.setOptions(options);

		// check to see whether any quick event handlers have been set
		if ( this.options.onCellAdd ){
			this.addEvent('cellAdded', this.options.onCellAdd);
		}
		if ( this.options.onCellLoad ){
			this.addEvent('cellLoaded', this.options.onCellLoad);
		} 
		
		// before we create the components check to see whether there is a links element and if there is
		// extend it (we may need it for the nav build)
		var linksElement = null;
		if ( this.options.linksElement ){
			// roll through the links element and add all the cells to the internal collection
			var linksElement = document.id(this.options.linksElement);
			if ( linksElement){
				this.options.linksElement = linksElement;
			} else {
				this.options.linksElement = null;
			}
		}
		
		// create the base component
		this.__createBaseComponents();
		
		// check to see whether a cells element has been defined
		if ( this.options.cellsElement ){
			// roll through the cells element and add all the cells to the internal collection
			var cellsElement = document.id(this.options.cellsElement);
			if ( cellsElement ){
				cellsElement.getElements(this.options.contentCellSelector).each( function(element){
					this.addCellElement(element)
				}.bind(this));
			}
		}
		// second, check to see whether a link list has been supplied
		if ( linksElement ){
			// roll through the links element and add all the cells to the internal collection
			if ( linksElement ){
				linksElement.getElements(this.options.linkSelector).each( function(element){
					// ensure the matching element is actually an A
					if ( element.tagName == 'A' ){
						// grab the hred and the rel attributes. both are need - the rel tag defines the element
						// to retrieve
						var href = element.get('href');
						var elementId = element.get('rel');
						if ( href && elementId ){
							this.addCellLink(href, elementId, element);
						}
					}
				}.bind(this));
			}
		}
	},
		
	/**
	 * setOptions
	 * 
	 * Merge the supplied options object with the existing options (this.options)
	 * 
	 * @param object options the options to merge
	 */
	
	setOptions: function(options){
		this.options = Object.merge(this.options, options);
	},
	
	/**
	 * __createStoreComponents
	 * 
	 * Creates the store DOMElement
	 */
	
	__createBaseComponents: function(){
		this.storeElement = new Element('div', {
			'class': this.storeElementClass,
			styles: {
				position: 'absolute',
				left: '-100000px',
				top: '-100000px'
			}
		});
		
		// check to see whether a parent has been supplied for the store. if not, inject it into the body
		var storeParent = document.id( this.options.cellStoreParent ? this.options.cellStoreParent : document.body );
		storeParent.grab(this.storeElement);

		// check to see whether a navigation wrapper has been supplied. if not, check to see whether
		// a links element was supplied and if it was whether it should be the nav
		if ( this.options.navigationWrapperElement ){
			var navigationWrapper = document.id(this.options.navigationWrapperElement);
			if ( navigationWrapper ){
				// one has. create a ul and inject it into the wrapper
				this.navigationElement = new Element('ul');
				navigationWrapper.set('html', '');
				navigationWrapper.grab(this.navigationElement);
			}
		} else if ( this.options.linksElementIsNavigation && this.options.linksElement ){
			// the links should be the nav. set the links element to be the nav
			this.navigationElement = this.options.linksElement;
			
			// roll through the a tags within the links element and update
			this.navigationElement.getElements('a').each( function(aElement, index){
				// add a click handler
				aElement.addEvent('click', function(event){
					// cancel the event and initiate a slide
					event.preventDefault();
					this.skipTo(index);
				}.bind(this));
				
				// store the incremented index on this a tag for later retrieval if needed
				aElement.store(this.storeKeyLinkCellIndex, index);
			}.bind(this));
		}
	},

	/**
	 * updateNavigation
	 * 
	 * Updates the navigation (if enabled) to reflect the currently selected item
	 */
	
	__updateNavigation: function(){
		// check to see whether we are managing a navigation list
		var navigationElement = this.navigationElement;
		if ( navigationElement ){
			// check to see whether the links element is a UL
			var tagSelector = 'li';
			if ( navigationElement.tagName != 'UL' ){
				// its not a UL which means we are searching for A tags
				tagSelector = 'a';
			}

			// update the selected class on the relevant items
			var currentPointer = this.pointer;
			var selectedClass = this.options.navigationSelectedClass;
			this.navigationElement.getElements(tagSelector).each(function(element, index){
				var isSelected = index == currentPointer;
				element.toggleClass(selectedClass, isSelected);
			});
		}
	},

	/**
	 * addCellElement
	 * 
	 * Adds an element from the current page as a content cell
	 * 
	 * @param DOMElement element
	 */
	
	addCellElement: function(element){
		// pull the element out of its existing position and insert it into the store
		this.storeElement.adopt(element);
		
		// add the cell config to the array
		this.__addCellObject({
			element: element,
			isLoaded: true,
			linkElement: null,
			elementId: null
		});
	},
	
	/**
	 * addCellLink
	 * 
	 * Adds a remote URL as a content cell
	 * 
	 * NOTE: If the supplied URL is the same as the current page URL and the element can be found
	 * it will be treated as a local cell (addCellElement) and not a remote cell
	 * 
	 * @param string url the URL to retrieve the content from
	 * @param string elementId the ID of the element to extract from the remote page
	 * @param DOMElement aElement (optional) the A tag associated with the link
	 */
	
	addCellLink: function(url, elementId, aElement){
		// ensure that the URL and element ID have been set
		if ( url && elementId ){
			// compare the URL to the current page to see if they are the same, after normalising them
			var urlObject = new URI(url);
			url = urlObject.toString();
			urlObject = new URI(window.location);
			var windowUrl = urlObject.toString();
			var isLocalCell = false;
			if ( url == urlObject ){
				// we are already on the link url. check to see whether we can find the element that would
				// normally be retrieved
				var element = document.id(elementId);
				if ( element ){
					// we've found the element so treat this cell as a local cell rather than a remote
					this.addCellElement(element);
					isLocalCell = true;
				}
			}
			
			// if this request wasnt converted to a local cell add a link cell to the array
			if ( ! isLocalCell ){
				this.__addCellObject({
					element: null,
					isLoaded: false,
					url: url,
					elementId: elementId,
					aElement: aElement
				});
			}
		} else {
			alert("ERROR > kamContentSlider > addCellLink > either the URL or elementId is missing")
		}
	},
	
	/**
	 * __addCellObject
	 * 
	 * Adds a cell object to the cell array
	 * 
	 * @param object cell
	 */
	
	__addCellObject: function(cell){
		// add the index to the cell
		cell.index = this.cells.length; 
		
		// push the cell into the array
		this.cells.push(cell);
		
		// check to see whether we are managing a navigation
		var cellCount = this.cells.length;
		if ( this.navigationElement ){
			// check to see whether the links element is the nav. if not, then process
			if ( ! this.options.linksElementIsNavigation ){
				// no, create a new li for this item
				var liElement = new Element('li');
				
				// add an A tag to the li
				var aElement = new Element('a', {
					href: '#',
					html: cellCount
				});
				liElement.grab( aElement );
				
				// add a click handler to the A element
				aElement.addEvent('click', function(event){
					// cancel the event and initiate a slide
					event.preventDefault();
					this.skipTo(cell.index);
				}.bind(this));

				// store the incremented index on this a tag for later retrieval if needed
				aElement.store(this.storeKeyLinkCellIndex, cell.index);
				
				// check to see whether this is the first cell
				if ( cellCount == 1 ){
					liElement.addClass(this.options.navigationSelectedClass);
				}
				
				// add the li to the navigation
				this.navigationElement.grab(liElement);
			}
		}
		
		// fire the cellAdded event
		this.fireEvent('cellAdded', cell);
	},
	
	/**
	 * __getIndexFromNavigationLink
	 * 
	 * Retrieves the index of the cell that a navigation link applies to
	 * 
	 * @param DOMElement aElement a A element
	 */
	
	__getIndexFromNavigationLink: function(aElement){
		if ( aElement ){
			return aElement.retrieve(this.storeKeyLinkCellIndex);
		}
		return false;
	},
	
	/**
	 * __loadCell
	 * 
	 * Loads an external content cell
	 * 
	 * @param object cell the cell to load
	 * @param function onCompleteHandler (OPTIONAL) a handler which will be called once the cell has been loaded   
	 */
	
	__loadCell: function(cell, onCompleteHandler){
		new kamContentRequest(cell.url, {
			retrieve: cell.elementId,
			onSuccess: function(response){
				// strip off the ID of the element because otherwise the chances are we will create an 
				// invalid document
				if ( response ){
					response.erase('id');
					
					
					
					// store the element, set the loaded flag and set the content
					cell.element = response;
					this.storeElement.adopt(cell.element);
					cell.isLoaded = true;
					
					// initalise the element
					kamUi.initialiseSource(cell.element);
					kamUi.initialise(cell.element);
					
					// if a onCompleteHandler has been supplied, fire it now
					if ( typeof(onCompleteHandler) == 'function' ){
						onCompleteHandler();
					}
	
					// fire the cellLoaded event
					this.fireEvent('cellLoaded', cell);
				}
			}.bind(this)
		}).execute();
	}
});

/**
 * Kameleon : Content changer
 * 
 * Allows multiple content cells to be loaded and then exchanged via a wrapper DOMElement
 * 
 * Options (all options of kamContentChangerBase are also valid):
 * 		maskClass (string): The class applied to the exchanger mask element (defaults to 'kamContentChangerMask')		
 * 		maskWidth (integer): The width of the exchanging mask. If not set the width of the wrapper will be used		
 * 		maskHeight (integer): The height of the exchanging mask. If not set the height of the wrapper will be used
 * 		maskZIndex (integer): The Z index of the mask element (defaults to '10000')
 * 		autoChange (boolean): Whether auto changing should be enabled
 * 		fade (boolean: A flag showing whether content should be exchanged via a fade (defaults to TRUE)		
 * 		changeInterval (integer): How frequently in milliseconds a change should occur (when autoChange is enabled. defaults to 5000ms)
 * 		changeDuration (integer): How long a change fade should take if fade is enabled (defaults to 1000ms)
 * 		startingIndex (integer||string[random]): The index to start at. Can be set to be a string of 'random' to start at a random index. If the value is not set either the navigation element with a "active" class will be used (if navigation is being internally managed) or the first cell will
 * 		fadeInitialiseCell (boolean): Whether to fade the item that is loaded during initialise (defaults to FALSE)
 * 		onBeginChange (function): A event handler for the "beginChange" event
 * 		onChangeComplete (function): A event handler for the "changeComplete" event
 * 		onChangeCancalled (function): A event handler for the "changeCancelled" event
 * 
 * Events:
 * 		beginChange
 * 			Fired when a change request begins but before any DOM modifications have taken place
 * 			Signature: function(nextCell)
 * 			Parameters:
 * 				nextCell (cell object - see kamContentChangerBase appendix): The cell that will be displayed
 * 
 * 		changeComplete
 * 			Fired when a change completes
 * 			Signature: function(cell)
 * 			Parameters:
 * 				cell (cell object - see kamContentChangerBase appendix): The cell that now has focus
 * 		
 * 		morphInitialise
 * 			Fired when a fade change morph is created but before it begins its transition
 * 			Signature: function(morph, morphParameters)
 * 			Parameters:
 * 				morph (FX.Morph): the new morph
 * 				morphParameters (object): The parameters that will be used during the morph
 * 
 * 		changeCancelled
 * 			Fired when a change request is cancelled due to the new index being invalid, the same as the current item or if a change is already firing 
 * 			Signature: function()
 * 			Parameters:
 * 				No paramaters
 * 
 * NOTES:
 * 		- Any methods prefixed with two underscores (__) are intended for internal use and should not be called
 *  
 * CHANGE LOG:
 * ===================
 * 
 * 07/03/2011 (Version 1.0.1)
 * --------------------------
 * Method: __prepareMask
 * 		Added fix which nudges the mask 1px to the right to prevent the content skipping in FireFox
 * 
 * 17/03/2011 (Version 1.0.2)
 * --------------------------
 * Property: initialised
 * 		Flag which shows whether the initalize method has completed
 * 
 * Method: initialize
 * 		Updates the initialised property on completion
 * 
 * Method: __change
 * 		If there is only one content cell, this method now allows the change if the initialised property
 * 		is still set to FALSE, allowing the first cell to be set back in place on startup
 * 
 * 27/04/2011 (Version 1.0.3)
 * --------------------------
 * Method: initialize
 * 		Updated to handle the "random" option of the "startingIndex"
 * 
 * @author James Sanders
 * @version 1.0.3
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */

var kamContentChanger = new Class({
	Extends: kamContentChangerBase,
	
	className: 'kamContentChanger',
	
	/**
	 * autoChangeHandler
	 * 
	 * Stores the auto change function while auto changing is enabled
	 * 
	 * @var function
	 */
	
	autoChangeHandler: null,
	
	/**
	 * changing
	 * 
	 * A boolean flag showing whether a change action is currently executing
	 * 
	 * @var boolean
	 */
	
	changing: false,
	
	/**
	 * initialised 
	 * 
	 * A boolean flag showing whether the intialisation process has completed.
	 * 
	 * @var boolean
	 */
	
	initialised: false,
	
	/**
	 * wrapperElement
	 * 
	 * The DOMElement whose content will be exchanged
	 * 
	 * @var DOMElement
	 */
	
	wrapperElement: null,
	
	/**
	 * changerElements
	 * 
	 * The main DOM elements that make up the changer control
	 * 
	 * @var object
	 */
	
	changerElements:{ 
		mask: null
	},
	
	/**
	 * initialize
	 * 
	 * Class constructor
	 * 
	 * @param DOMElement||string wrapperElement the container that will have its contents exchanged
	 * @param object options (OPTIONAL) the options of the class 
	 */
	
	initialize: function(wrapperElement, options){
		wrapperElement = document.id(wrapperElement);
		if ( wrapperElement ){
			// store the config
			this.wrapperElement = wrapperElement;
			
			// set default options
			this.setOptions({
				maskClass: 'kamContentChangerMask',
				maskWidth: null,
				maskHeight: null,
				maskZIndex: 10000,
				autoChange: false,
				fade: true,
				changeInterval: 5000,
				changeDuration: 1000,
				startingIndex: null,
				fadeInitialiseCell: false,
				onBeginChange: null,
				onChangeComplete: null,
				onChangeCancelled: null,
				onChangeCancelled: null
			});

			// execute the parent constructor
			this.parent(options);
			options = options;
			
			// build the changer components
			this.__buildChangerComponents();

			// check to see whether any quick event handlers have been set
			if ( options.onBeginChange ) this.addEvent('beginChange', options.onBeginChange);
			if ( options.onChangeComplete )this.addEvent('changeComplete', options.onChangeComplete);
			if ( options.onChangeCancelled ) this.addEvent('changeCancelled', options.onChangeCancelled);
			
			// disable fade for setting the first item
			var previousFade = options.fade;
			options.fade = options.fadeInitialiseCell;
			
			// check to see whether a starting index has been supplied
			var firstIndex = 0;
			if ( options.startingIndex ){
				// check to see whether the startingIndex indicates a random index
				if ( options.startingIndex == 'random' ){
					firstIndex = Number.random(0, (this.cells.length - 1));
				} else {
					firstIndex = options.startingIndex;	
				}
			} else {
				// nope. check to see whether we are managing our own nav
				var navigationElement = this.navigationElement;
				if ( navigationElement ){
					// check to see whether the nav element is a UL
					if ( navigationElement.tagName != 'UL' ){
						// its not a UL so look directly for the A tag
						var selectedAElement = navigationElement.getElement('a.'+options.navigationSelectedClass);
					} else {
						var selectedAElement = navigationElement.getElement('li.'+options.navigationSelectedClass+' a');
					}
					
					// check to see whether we found the selected A tag
					if ( selectedAElement ){
						// we did. retrieve its index
						var cellIndex = this.__getIndexFromNavigationLink(selectedAElement);
						if ( cellIndex ){
							// skip to the retrieved index
							firstIndex = cellIndex;
						}
					}
				}
			}
			
			// skip to the first index
			this.skipTo(firstIndex);
			
			// set the value of fade back to its previous state
			options.fade = previousFade;
			
			// if autoChange is TRUE toggle it on
			if ( options.autoChange ) this.toggleAutoChange(true);
			
			// mark that the changer is fully initialised
			this.initialised = true;
		} else {
			alert("ERROR > kamContentChanger > unable to find the wrapper element in the DOM");
		}
	},
	
	/**
	 * __buildChangerComponents
	 * 
	 * Constructs the DOM elements for use with the changer
	 */
	
	__buildChangerComponents: function(){
		// inject a masking element into the DOM which is the same size as the wrapper
		var mask = new Element('div',{
			'class': this.options.maskClass,
			styles: {
				position: 'absolute',
				display: 'none',
				zIndex: this.options.maskZIndex
			}
		});
		this.changerElements.mask = mask;
		document.body.grab(mask);
	},

	/**
	 * toggleAutoChange
	 * 
	 * Toggles the auto change flag
	 * 
	 * @param boolean force (optional) forces the new value to either TRUE or FALSE
	 */
	
	toggleAutoChange: function(force){
		// check to see whether the force flag is set. if it is set to the relevant value, otherwise toggle
		if ( force == true ){
			this.options.autoChange = true;
		} else if ( force == false ){
			this.options.autoChange = false;
		} else {
			this.options.autoChange = ! this.options.autoChange;
		}

		// if auto change is enabled create the timer, otherwise ensure it is disabled
		if ( this.options.autoChange ){
			// only process if there isn't already one setup
			if ( ! this.autoChangeHandler ){
				this.autoChangeHandler = function(){
					this.next();
				}.periodical(this.options.changeInterval, this);
			}
		} else {
			// only process if there is a auto handler set
			if ( this.autoChangeHandler ){
				clearInterval(this.autoChangeHandler);
				this.autoChangeHandler = null;
			}
		}
	},
	
	/**
	 * isChanging
	 *
	 * Returns a boolean flag indiciating whether a change is in progress
	 * 
	 * @return boolean
	 */
	
	isChanging: function(){
		return this.changing;
	},
	
	/**
	 * next
	 * 
	 * Slides to the next cell
	 */
	
	next: function(){
		this.__change(true);
	},
	
	/**
	 * previous
	 * 
	 * Slides to the previous cell
	 */
	
	previous: function(){
		this.__change(false);	
	},
	
	/**
	 * skipTo
	 * 
	 * Slides to a specific cell
	 * 
	 * @param integer index the index of the cell to change to
	 */
	
	skipTo: function(index){
		if ( isNaN(index) == false ){
			// ensure the index isn't less than 0 and not more than the cell count
			if ( index >= 0 && index < (this.cells.length) ){
				// ensure the new index isnt the same as the current index
				if ( index != this.pointer ){
					// determine whether the new index is more or less than the current pointer
					var changeNext = (index > this.pointer); 
					
					// execute a change
					this.__change(changeNext, index);
				}  else {
					// the requested index is the current index so fire a cancel event
					this.fireEvent('changeCancelled');
				}
			} else {
				// the requested index isnt valid so fire a cancel event
				this.fireEvent('changeCancelled');
			}
		}
	},
	
	/**
	 * __change
	 * 
	 * Perform a change action
	 * 
	 * @param boolean changeNext a flag showing whether the change should be to the next or previous cell
	 * @param integer newPointer (optional) the index of the cell to slide to
	 */
	
	__change: function(changeNext, newPointer){
		// ensure we aren't already changing
		if ( ! this.changing ){
			// ensure there is more than one item in the cell list (unless we have not yet completed
			// initalisation in which case we need to set the first cell back in place)
			var cellCount = this.cells.length;
			if ( cellCount > 1 || this.initialised == false ){
				// set the changing flag to block and actions during the transition
				this.changing = true;
				
				// disable the auto change
				var previousAutoChange = this.options.autoChange;
				this.toggleAutoChange(false);
				
				// store the current pointer to enable "back" actions
				this.previousPointer = ( typeof this.pointer == 'null' ) ? 0 : this.pointer;
				
				// either increment or decrement the pointer based on the changeNext param, set the next cell and
				// calculate the next left margin value of the changer element
				if ( changeNext ){
					// increment and the new value is valid. if not set it the first item
					this.pointer++;
					if ( this.pointer > (cellCount - 1) ){
						this.pointer = 0;
					}
				} else {					
					// decrement and the new value is valid. if not set it the last item in the cell list
					this.pointer--;
					if ( this.pointer < 0 ){
						this.pointer = (cellCount - 1);
					}
				}

				// if a newPointer has been supplied override whatever the current value is
				if ( isNaN(newPointer) == false ){
					this.pointer = newPointer;
				}
				
				// get the next cell to load
				var nextCell = this.cells[this.pointer];
								
				// create the complete change handler
				var completeChangeHandler = function(){
					// swap the contents of current cell with the next cell
					this.__populateWrapper(nextCell);
					
					// update the navigation
					this.__updateNavigation();
					
					// hide the mask
					this.changerElements.mask.setStyle('display', 'none');
					
					// set the changing flag back to FALSE
					this.changing = false;
					
					// fire a changeComplete event
					this.fireEvent('changeComplete', nextCell);
					
					// reenable the previous state of the auto change (if necessary)
					if ( previousAutoChange == true ){
						this.toggleAutoChange(true);
					}
				}.bind(this);
				
				// check to see whether we want to fade
				var fadeHandler = null;
				if ( kamUi.useAnimations && this.options.fade ){
					// we do so create the morph 
					var fadeHandler = function(){
						var morph = new Fx.Morph(this.changerElements.mask, {
							duration: this.options.changeDuration,								
							onComplete: function(){
								completeChangeHandler();
							}.bind(this)
						})
						
						// prepare the morph parameters
						var morphParameters = {
							'opacity': 1
						};
						
						// fix firefox pixel shift
						if( Browser.firefox ){
							this.changerElements.mask.setStyle("margin-left","1px");	
						}
						
						// fire the morphInitialise event
						this.fireEvent('morphInitialise', [morph, morphParameters]);
						
						// start the morph
						morph.start(morphParameters);
					}.bind(this);
				}
				
				// check to see whether the cell is loaded
				if ( nextCell.isLoaded ){
					// load the contents of the next cell into the next cell
					this.__prepareMask(nextCell);
					
					// fire a beginChange event
					this.fireEvent('beginChange', nextCell);
					
					// it is. execute the change now
					if ( fadeHandler ){
						fadeHandler();
					} else {
						completeChangeHandler();
					}
				} else {
					// the cell isn't loaded. we need to retrieve the content
					this.__loadCell(nextCell, function(){
						// load the contents of the next cell into the next cell
						this.__prepareMask(nextCell);
						
						// fire a beginChange event
						this.fireEvent('beginChange', nextCell);
						
						// it is. execute the change now
						if ( fadeHandler ){
							fadeHandler();
						} else {
							completeChangeHandler();
						}
					}.bind(this));
				}
			}
		} else {
			// since we are already changing this request will be ignored. fire an event just in case someone
			// has performed actions that need to be cancelled, such as displaying a mask on a nav click...TOM!
			this.fireEvent('changeCancelled');
		}
	},
	
	/**
	 * __populateWrapper
	 * 
	 * Sets the content of the wrapper
	 * 
	 * @param object cell the content cell to move into the slider cell
	 */
	
	__populateWrapper: function(cell){
		// check to see whether this wrapper cell already has content
		var wrapper = this.wrapperElement;
		var currentCell = wrapper.retrieve('kamContentChangerCurrentCell');
		if ( currentCell ){
			// it does. move the element back to the store
			this.storeElement.adopt(currentCell.element);
		}
		
		// move the cell's element into the wrapper cell 
		wrapper.adopt(cell.element);
		
		// store the new cell as the current content cell of the wrapper cell...yes
		wrapper.store('kamContentChangerCurrentCell', cell);
	},

	/**
	 * __prepareMask
	 * 
	 * Sets the content of the mask and prepares it for display
	 * 
	 * @param object cell the content cell to move into the wrapper element
	 */
	
	__prepareMask: function(cell){
		// move the cell's element into the wrapper cell 
		this.changerElements.mask.adopt(cell.element);
		
		// prepare the mask for view
		var mask = this.changerElements.mask;
		mask.setStyle('opacity', 0);
		mask.setStyle('display', 'block');
	
		// position the mask
		var wrapperPosition = this.wrapperElement.getPosition();
		mask.setStyles({
			left: wrapperPosition.x,
			top: wrapperPosition.y
		});
		
		// set the dimensions of the mask
		var cellSize = this.wrapperElement.measure(function(){
			return this.getSize(); 
		});
		if ( ! this.options.maskWidth ){
			mask.setStyle('width', cellSize.x+'px');
		} else {
			mask.setStyle('width', this.options.maskWidth+'px');	
		}
		if ( ! this.options.maskHeight ){
			mask.setStyle('height', cellSize.y+'px');
		} else {
			mask.setStyle('height', this.options.maskHeight+'px');	
		}
	},

destroy: function(){
	// stop the auto change
	this.toggleAutoChange(false);
	
	// wrap the destroy action in a function so we can act on our state
	var destroyHandler = function(){
		// fire the destroy event in case anyone wants to know 
		this.fireEvent('destroy');
		
		// destroy the store element
		this.storeElement.destroy();
		
		// destroy the nav element (if available)
		if ( this.navigationElement ) this.navigationElement.destroy();
		
		// destroy the mask
		this.changerElements.mask.destroy();
	}.bind(this);
	
	// check to see whether we are in the process of changing
	this.changing ? this.addEvent('changeComplete', destroyHandler) : destroyHandler(); 
}
});

/**
 * Kameleon : Content slider
 * 
 * Allows multiple content cells to be loaded and then viewed as a slider control
 * 
 * Options (all options of kamContentChangerBase are also valid):
 * 		sliderClass (string): The class applied to the slider DIV (defaults to 'kamSlider')
 * 		sliderCellClass (string): The class applied to a slider control cell (defaults to 'cell')
 * 		sliderCellWidth (integer): The width of a slider control cell. If not set the width of the first content cell will be used
 * 		autoSlide (boolean): Whether auto sliding should be enabled
 * 		slideInterval (integer): How frequently in milliseconds a slide should occur (when autoslide is enabled. defaults to 5000ms)
 * 		slideDuration (integer): How long a slide action should take (defaults to 1000ms)
 * 		onBeginSlide (function): A event handler for the "beginSlide" event
 * 		onSlideComplete (function): A event handler for the "slideComplete" event
 * 		onChangeCancalled (function): A event handler for the "changeCancelled" event
 * 		vertical (boolean): A flag showing whether the slider should slide horizontally or vertically (defaults to FALSE - horizontal slide)
 * 
 * Events:
 * 		beginSlide
 * 			Fired when a slide request begins but before any DOM modifications have taken place
 * 			Signature: function(nextCell)
 * 			Parameters:
 * 				nextCell (cell object - see kamContentChangerBase appendix): The cell that will be slid into focus
 * 
 * 		slideComplete
 * 			Fired when a slide completes
 * 			Signature: function(cell)
 * 			Parameters:
 * 				cell (cell object - see kamContentChangerBase appendix): The cell that now has focus
 * 		
 * 		morphInitialise
 * 			Fired when a slide morph is created but before it begins its transition
 * 			Signature: function(morph, morphParameters)
 * 			Parameters:
 * 				morph (FX.Morph): the new morph
 * 				morphParameters (object): The parameters that will be used during the morph
 * 
 * 		changeCancelled
 * 			Fired when a change request is cancelled thanks to the new index being invalid, the same as the current one or a change being in action
 * 			Signature: function()
 * 			Parameters:
 * 				No paramaters
 * 
 * NOTES:
 * 		- Any methods prefixed with two underscores (__) are intended for internal use and should not be called
 *  
 *  
 * 14/04/2011 (Version 1.0.1)
 * --------------------------
 * Option: vertical
 * 		New option for flagging vertical sliders
 * 
 * Method: __buildSliderComponents
 * 		Updated measuring code to use kamUi.getElementDimensions
 * 
 * Method: __slide
 * 		Updated to handle vertical sliding
 * 
 * Method: setCellDimensions
 * 		Updated to also set height dimensions as well as width
 *  
 * @author James Sanders
 * @version 1.0.1
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */

var kamContentSlider = new Class({
	Extends: kamContentChangerBase,
	
	className: 'kamContentSlider',
	
	/**
	 * autoSlideHandler
	 * 
	 * Stores the auto slide function while auto sliding is enabled
	 * 
	 * @var function
	 */
	
	autoSlideHandler: null,
	
	/**
	 * sliding
	 * 
	 * A boolean flag showing whether a slide action is currently executing
	 * 
	 * @var boolean
	 */
	
	sliding: false,
	
	/**
	 * wrapperElement
	 * 
	 * The wrapper DOMElement for the slider
	 * 
	 * @var DOMElement
	 */
	
	wrapperElement: null,
	
	/**
	 * sliderElements
	 * 
	 * The main DOM elements that make up the slider control
	 * 
	 * @var object
	 */
	
	sliderElements:{ 
		slider: null,
		previousCell: null,
		currentCell: null,
		nextCell: null
	},
	
	/**
	 * initialize
	 * 
	 * Class constructor
	 * 
	 * @param DOMElement||string wrapperElement the container to inject the slider into
	 * @param object options (OPTIONAL) the options of the class 
	 */
	
	initialize: function(wrapperElement, options){
		wrapperElement = document.id(wrapperElement);
		if ( wrapperElement ){
			// store the config
			this.wrapperElement = wrapperElement;
			
			// set default options and then apply the provided ones
			this.setOptions({
				sliderClass: 'kamSlider',
				sliderCellClass: 'cell',
				sliderCellWidth: null,
				sliderCellHeight: null,
				autoSlide: true,
				slideInterval: 5000,
				slideDuration: 1000,
				onBeginSlide: null,
				onSlideComplete: null,
				vertical: false
			});
			this.setOptions(options);
			
			// build the slider components
			this.__buildSliderComponents();
			
			// execute the parent constructor
			this.parent(options);

			// check to see whether any quick event handlers have been set
			if ( this.options.onBeginSlide ){
				this.addEvent('beginSlide', this.options.onBeginSlide);
			}
			if ( this.options.onSlideComplete ){
				this.addEvent('slideComplete', this.options.onSlideComplete);
			}
			if ( this.options.onChangeCancelled ){
				this.addEvent('changeCancelled', this.options.onChangeCancelled);
			}

			// check to see whether a starting index has been supplied
			var firstIndex = 0;
			if ( options.startingIndex ){
				// check to see whether the startingIndex indicates a random index
				if ( options.startingIndex == 'random' ){
					firstIndex = Number.random(0, (this.cells.length - 1));
				} else {
					firstIndex = options.startingIndex;	
				}
			} else {
				// nope. check to see whether we are managing our own nav
				var navigationElement = this.navigationElement;
				if ( navigationElement ){
					// check to see whether the nav element is a UL
					if ( navigationElement.tagName != 'UL' ){
						// its not a UL so look directly for the A tag
						var selectedAElement = navigationElement.getElement('a.'+options.navigationSelectedClass);
					} else {
						var selectedAElement = navigationElement.getElement('li.'+options.navigationSelectedClass+' a');
					}
					
					// check to see whether we found the selected A tag
					if ( selectedAElement ){
						// we did. retrieve its index
						var cellIndex = this.__getIndexFromNavigationLink(selectedAElement);
						if ( cellIndex ){
							// skip to the retrieved index
							firstIndex = cellIndex;
						}
					}
				}
			}
			
			// check to see whether this is the first cell
			if ( firstIndex >= 0 && firstIndex < this.cells.length ){
				// check to see whether the cell is loaded
				this.pointer = firstIndex;
				var cell = this.cells[firstIndex];
				if ( cell.isLoaded ){
					// it is. add the contents to the current slider cell
					this.__populateSliderCell(this.sliderElements.currentCell, cell, true);

					// if autoslide is TRUE toggle it on
					if ( this.options.autoSlide ) this.toggleAutoSlide(true);
				} else {
					this.__loadCell(cell, function(){
						this.__populateSliderCell(this.sliderElements.currentCell, cell, true);

						// if autoslide is TRUE toggle it on
						if ( this.options.autoSlide ) this.toggleAutoSlide(true);
					}.bind(this));
				}
			} else {
				// if autoslide is TRUE toggle it on
				if ( this.options.autoSlide ) this.toggleAutoSlide(true);
			}
			
		} else {
			alert("ERROR > kamContentSlider > unable to find the slider wrapper element in the DOM");
		}
	},
	
	/**
	 * __buildSliderComponents
	 * 
	 * Constructs the DOM elements for use with the slider
	 */
	
	__buildSliderComponents: function(){
		// inject three cells into the wrapper
		var wrapperElement = this.wrapperElement;
		var slider = new Element('div[class='+this.options.sliderClass+']');
		wrapperElement.grab(slider);
		this.sliderElements.slider = slider;
		this.sliderElements.previousCell = new Element('div[class='+this.options.sliderCellClass+']');
		this.sliderElements.currentCell = new Element('div[class='+this.options.sliderCellClass+']');
		this.sliderElements.nextCell = new Element('div[class='+this.options.sliderCellClass+']');
		slider.adopt([this.sliderElements.previousCell, this.sliderElements.currentCell, this.sliderElements.nextCell]);
		
		// if either the cell width or height is not defined determine it using the first cell
		var firstCell = slider.getElement('.'+this.options.sliderCellClass);
		var elementDimensions = kamUi.getElementDimensions(firstCell);
		if ( ! this.options.sliderCellWidth ){
			this.options.sliderCellWidth = elementDimensions.x;
		}
		if ( ! this.options.sliderCellHeight ){
			this.options.sliderCellHeight = elementDimensions.y;
		}
		
		this.setCellDimensions(this.options.sliderCellWidth, this.options.sliderCellHeight);
	},

	/**
	 * __addCellObject
	 * 
	 * Adds a cell object to the cell array
	 * 
	 * @param object cell
	 */
	
	__addCellObject: function(cell){
		this.parent(cell);
		
		// check to see whether this is the first cell
		if ( this.cells.length == 1 ){
			// check to see whether the cell is loaded
			if ( cell.isLoaded ){
				// it is. add the contents to the current slider cell
				this.__populateSliderCell(this.sliderElements.currentCell, cell, true);
			} else {
				this.__loadCell(cell, function(){
					this.__populateSliderCell(this.sliderElements.currentCell, cell, true);
				}.bind(this));
			}
		}
	},
	
	/**
	 * toggleAutoSlide
	 * 
	 * Toggles the auto slide flag
	 * 
	 * @param boolean force (optional) forces the new value to either TRUE or FALSE
	 */
	
	toggleAutoSlide: function(force){
		// check to see whether the force flag is set. if it is set to the relevant value, otherwise toggle
		if ( force == true ){
			this.options.autoSlide = true;
		} else if ( force == false ){
			this.options.autoSlide = false;
		} else {
			this.options.autoSlide = ! this.options.autoSlide;
		}

		// if auto slide is enabled create the timer, otherwise ensure it is disabled
		if ( this.options.autoSlide ){
			// only process if there isn't already one setup
			if ( ! this.autoSlideHandler ){
				this.autoSlideHandler = function(){
					this.next();
				}.periodical(this.options.slideInterval, this);
			}
		} else {
			// only process if there is a auto handler set
			if ( this.autoSlideHandler ){
				clearInterval(this.autoSlideHandler);
				this.autoSlideHandler = null;
			}
		}
	},
	
	/**
	 * isSliding
	 *
	 * Returns a boolean flag indiciating whether a slide is in progress
	 * 
	 * @return boolean
	 */
	
	isSliding: function(){
		return this.sliding;
	},
	
	/**
	 * next
	 * 
	 * Slides to the next cell
	 */
	
	next: function(){
		this.__slide(true);
	},
	
	/**
	 * previous
	 * 
	 * Slides to the previous cell
	 */
	
	previous: function(){
		this.__slide(false);	
	},
	
	/**
	 * skipTo
	 * 
	 * Slides to a specific cell
	 * 
	 * @param integer index the index of the cell to slide to
	 * @param string the direction in which to slide. values 'previous'||'next'
	 */
	
	skipTo: function(index, slideDirection){
		if ( isNaN(index) == false ){
			// ensure the index isn't less than 0 and not more than the cell count
			if ( index >= 0 && index < (this.cells.length) ){
				// ensure the new index isnt the same as the current index
				if ( index != this.pointer ){
					// determine the direction we should slide in based on the current pos
					// and the next pos
					var slideNext = (index > this.pointer);
					
					// check to see whether a slide direction has been specified
					if ( slideDirection ){
						// it has. work out which direction we want to move in 
						switch ( slideDirection ){
							case 'previous' : 
								slideNext = false;
								break;
							case 'next' : 
								slideNext = true;
								break;
						}
					}
					
					// execute a slide
					this.__slide(slideNext, index);
				} else {
					// the requested index is the same as the current index so fire a cancel event
					this.fireEvent('changeCancelled');
				}
			}  else {
				// the requested index isnt valid so fire a cancel event
				this.fireEvent('changeCancelled');
			}
		}
	},
	
	/**
	 * __slide
	 * 
	 * Perform a slide action
	 * 
	 * @param boolean slideNext a flag showing whether the slide should animate in a "next" or "previous" motion
	 * @param integer newPointer (optional) the index of the cell to slide to
	 */
	
	__slide: function(slideNext, newPointer){
		// ensure we aren't already sliding
		if ( ! this.sliding ){
			// 28/06/2011 - before proceeding check to see whether the slider implements the 
			// getStyle method. sometimes, when performing multiple heavy AJAX loads/wipes, the element
			// can start to be destroyed during a transition causing a failure
			if ( this.sliderElements.slider.getStyle ){
				// ensure there is more than one item in the cell list
				var cellCount = this.cells.length;
				if ( cellCount > 1 ){
					// set the sliding flag to block and actions during the transition
					this.sliding = true;
					
					// disable the auto slide
					var previousAutoSlide = this.options.autoSlide;
					this.toggleAutoSlide(false);
					
					// get the current maring
					var vertical = this.options.vertical;
					if ( vertical ){
						var styleKey = 'margin-top';
						var cellMargin = this.options.sliderCellHeight;
					} else {
						var styleKey = 'margin-left';
						var cellMargin = this.options.sliderCellWidth;
					}
	
					var currentMargin = parseFloat(this.sliderElements.slider.getStyle(styleKey));

					// store the current pointer to enable "back" actions
					this.previousPointer = ( typeof this.pointer == 'null' ) ? 0 : this.pointer;
					
					// either increment or decrement the pointer based on the slideNext param, set the next cell and
					// calculate the next left margin value of the slider element
					if ( slideNext ){
						// increment and chck the new value is valid. if not set it to the first item
						this.pointer++;
						if ( this.pointer > (cellCount - 1) ){
							this.pointer = 0;
						}
						
						var nextSliderCell = this.sliderElements.nextCell;
						var newMargin = currentMargin - cellMargin;
					} else {					
						// decrement and check the new value is valid. if not set it the last item in the cell list
						this.pointer--;
						if ( this.pointer < 0 ){
							this.pointer = (cellCount - 1);
						}
						
						var nextSliderCell = this.sliderElements.previousCell;
						var newMargin = currentMargin + cellMargin;
					}
	
					// if a newPointer has been supplied override whatever the current value is
					if ( isNaN(newPointer) == false ){
						this.pointer = newPointer;
					}
								
					// get the next cell to load
					var nextCell = this.cells[this.pointer];
					
					// create and initialise the slide morph
					var morphHandler = function(){
						// load the contents of the next content cell into the next slider cell
						this.__populateSliderCell(nextSliderCell, nextCell);
						
						// fire a beginSlide event
						this.fireEvent('beginSlide', nextCell);
						
						var completeHandler = function(){
							// swap the contents of current cell with the next cell
							this.__populateSliderCell(this.sliderElements.currentCell, nextCell, true);
							
							// update the navigation
							this.__updateNavigation();
							
							// reset the position of the slider object
							this.sliderElements.slider.setStyle(styleKey, '-'+cellMargin+'px');
							
							// set the sliding flag back to FALSE
							this.sliding = false;
							
							// fire a slideComplete event
							this.fireEvent('slideComplete', nextCell);
							
							// reenable the previous state of the autoslide (if necessary)
							if ( previousAutoSlide == true ){
								this.toggleAutoSlide(true);
							}
						}.bind(this)
						
						// check to see whether animations are enabled
						if( kamUi.useAnimations ){
							// create the morph
							var morph = new Fx.Morph(this.sliderElements.slider, {
								duration: this.options.slideDuration,								
								onComplete: completeHandler
							});
							
							// prepare the morph parameters
							var morphParameters = {}
							morphParameters[styleKey] = newMargin;
							
							// fire the morphInitialise event
							this.fireEvent('morphInitialise', [morph, morphParameters]);
							
							// start the morph
							morph.start(morphParameters);
						} else {
							completeHandler();
						}
					}.bind(this);
					
					// check to see whether the cell is loaded
					if ( nextCell.isLoaded ){
						// it is. execute the slide now
						morphHandler();
					} else {
						// the cell isn't loaded. we need to retrieve the content
						this.__loadCell(nextCell, function(){
							// it is. execute the slide now
							morphHandler();
						}.bind(this));
					}
				}
			}
		} else {
			// since we are already changing this request will be ignored. fire an event just in case someone
			// has performed actions that need to be cancelled, such as displaying a mask on a nav click...TOM!
			this.fireEvent('changeCancelled');
		}
	},
	
	/**
	 * __populateSliderCell
	 * 
	 * Sets the content of a slider cell
	 * 
	 * @param DOMElement sliderCell a slider cell element
	 * @param object cell the content cell to move into the slider cell
	 * @param boolean isCurrentSliderCell a flag showing whether the slider cell is the "current" cell
	 */
	
	__populateSliderCell: function(sliderCell, cell, isCurrentSliderCell){
		// check to see whether this is the "current" slider cell
		if ( isCurrentSliderCell ){
			// check to see whether this slider cell already has content
			var currentCell = sliderCell.retrieve('kamSliderCurrentCell');
			if ( currentCell ){
				// it does. move the element back to the store
				this.storeElement.adopt(currentCell.element);
			}
		}
		
		// move the cell's element into the slider cell 
		sliderCell.adopt(cell.element);
		
		// store the new cell as the current content cell of the slider cell...yes
		sliderCell.store('kamSliderCurrentCell', cell);
	},
	
	/**
	 * setCellDimensions
	 * 
	 * Sets the size of the slider cells
	 * 
	 * @param integer width the new width of the slider cells
	 */
	
	setCellDimensions: function(width, height){
		// update the slider cells width
		if ( width ){
			this.options.sliderCellWidth = width;
			this.sliderElements.previousCell.setStyle('width', this.options.sliderCellWidth+'px');
			this.sliderElements.currentCell.setStyle('width', this.options.sliderCellWidth+'px');
			this.sliderElements.nextCell.setStyle('width', this.options.sliderCellWidth+'px');
		}
		
		// update the slider cells height
		if ( height ){
			this.options.sliderCellHeight = height
			this.sliderElements.previousCell.setStyle('height', this.options.sliderCellHeight+'px');
			this.sliderElements.currentCell.setStyle('height', this.options.sliderCellHeight+'px');
			this.sliderElements.nextCell.setStyle('height', this.options.sliderCellHeight+'px');
		}
		
		// position the slider so that the middle cell is visible
		if ( this.options.vertical ){
			// push the slider up by the height of one cell
			this.sliderElements.slider.setStyle('margin-top', '-'+this.options.sliderCellHeight+'px');
			
			// set the height of the slider to be three times the height of a cell
			this.sliderElements.slider.setStyle('height', ((this.options.sliderCellHeight * 3) + 20) +'px');
		} else {
			// push the slider left by the height of one cell
			this.sliderElements.slider.setStyle('margin-left', '-'+this.options.sliderCellWidth+'px');

			// set the width of the slider to be three times the width of a cell
			this.sliderElements.slider.setStyle('width', ((this.options.sliderCellWidth * 3) + 20) +'px');			
		}
	},
	


destroy: function(){
	// stop the auto change
	this.toggleAutoSlide(false);
	
	// wrap the destroy action in a function so we can act on our state
	var destroyHandler = function(){
		// fire the destroy event in case anyone wants to know 
		this.fireEvent('destroy');
		
		// destroy the store element
		this.storeElement.destroy();
		
		// destroy the nav element (if available)
		if ( this.navigationElement ) this.navigationElement.destroy();
		
		// destroy out elements
		this.sliderElements.slider.destroy();
	}.bind(this);
	
	// check to see whether we are in the process of changing
	this.sliding ? this.addEvent('slideComplete', destroyHandler) : destroyHandler(); 
}
});

/**
 * Kameleon : Form
 * 
 * Extends a form in order to provide validation
 *
 * Options:
 * 		submitHandler (function): A custom submission handler. If supplied default submission behavour will be skipped. The handler will be passed one argument which is the FORM DOMElement.
 *		errorReporting (string): The manner in which errors should be reported. Can be either ('none', 'alert' or 'modal' - defaults to 'none')
 *		errorModalOptions (object): The options used for error reporting modals
 *		errorModalIntroduction (string||DOMElement): Text or elements to inject into the modal content before the error DD
 *		submitViaAjax (boolean): whether this form should be submitted via AJAX (defaults to TRUE). See appendix: AJAX posting for details on how this is handled
 *		ajaxSpinnerOptions (object): the options used when displaying a Spinner during an AJAX POST
 *		ajaxSpinnerOpacity (float): the opacity to morph the AJAX POST spinner to and from (defaults to 0.8
 *		onInvalidForm (function): A handler for the invalidForm event
 *		ajaxCompleteHandler (function): A handler for managing the AJAX onComplete event when posting via AJAX
 *
 * Events:
 * 		invalidForm
 * 			Fired if a form submit action is requested but the form is invalid
 * 			Signature: function(details)
 * 			Parameters:
 * 				details (object): details of the failed inputs. See appendix: invalid form details 
 * 
 * Appendix:
 * 		invalid form details
 * 			The JSON object that contains the details of the failed inputs
 * 			Properties:
 * 				inputs (array): An array of failed inputs. See appendix for failed inputs
 * 				
 * 		failed inputs
 * 			The JSON object that contains details of a failed input
 * 			Properties:			
 * 				element (DOMElement): The form input that failed
 *				label (string): The label of the form
 *				labelElement (DOMElement): The label element of the failed input (if available)
 *				failedValidators (array): An array of the failed validators. See appendix for failed validators
 * 
 * 		failed validators
 * 			The JSON object that contains details of a failed input validator
 * 			Properties:
 * 				type (string): the validator type
 * 				validator (string): the entire validator string (as set in the input's class)
 * 				message (string): the fail message associated with the validator	
 * 
 * 		AJAX POSTing
 * 			By default forms will send their data to the server via an AJAX POST. The following behaviour is used during this process
 * 				- On POST a Mootools Spinner will be displayed over the FORM element
 * 				- Once the data has been succesfully sent the response code will be parsed
 * 					- The component will search the response looking for a DOMElement with the same ID attribute as the form - this should be the confirmation message
 * 					- Once the component is found the following actions occur
 * 						- the Spinner is destroyed
 * 						- the new element is exchanged for the FORM but retains the height of the FORM element
 * 						- the new element is shrunk / stretched (via a Morph) to its correct size
 * 
 * NOTES:
 * 		- Any methods prefixed with two underscores (__) are intended for internal use and should not be called
 * 		- Forms with a class of "kamForm" are automatically extended with this form (see kamUiController)
 * 			
 * CHANGE LOG:
 * ===================
 * 
 * 07/03/2011 (Version 1.0.0)
 * --------------------------
 * 
 * 14/03/2011 (Version 1.0.1)
 * --------------------------
 * 
 * Static method: __addCustomValidators
 * 		New method for setting up custom validators to the Form.Validator global (called by kamUiController)
 * 
 * Property: validatorErrorMessages
 * 		Binned in favour of using the validator stock behaviour
 * 
 * Method: __fireInvalidFormEvent
 * 		Switched error message handling to use the validators' inbuild generators again
 * 
 * Option: submitHandler
 * 		Ability to supply a submitHandler function which handles the actual submission of the form
 * 
 * Method: __transmitForm
 * 		Updated to take into account the submitHandler option. If no option has been supplied stock
 * 		submission behavioud will be carried out
 * 
 * 
 * 14/03/2011 (Version 1.0.2)
 * --------------------------
 * 
 * Property: errorReporting
 * 		New property which manages how errors are reported
 * 
 * Property: errorModalOptions
 * 		New property which stores the options for error reporting kamModals
 * 
 * Method: __reportErrors
 * 		New method to handle the automatic display of errors
 * 
 * Method: __fireInvalidFormEvent
 * 		Updated to call __reportErrors
 * 
 * 22/03/2011 (Version 1.0.3)
 * --------------------------
 * 
 * Option: submitViaAjax (defaults to TRUE)
 * 		New option to manage whether a FORM is POSTed via AJAX or not
 *  
 * Option: ajaxSpinnerOptions
 * 		New option containing the options that are passed to the "sending" AJAX spinner
 * 
 * Option: ajaxSpinnerOpacity (defaults to 0.8)
 * 		The opacity to morph the AJAX "sending" Spinner to and from
 * 
 * Method : __transmitForm
 * 		Modified to determine whether the FORM should be sent via AJAX
 * 
 * Method: __transmitFormViaAjax
 * 		New method to handle sending FORMs via AJAX
 * 
 * 02/04/2011 (Version 1.0.4)
 * --------------------------
 * 
 * Method: __setupValidation
 * 		Updated FAIL and PASS listeners to check fix the problem of mootools returning OPTION elements
 * 		in IE7
 * 
 * Method: submitForm
 * 		Updated to reset the failingInputs property because mootools cant be trusted to notify us of
 * 		all PASSES
 * 
 * 11/04/2011 (Version 1.0.5)
 * --------------------------
 * OPTION: ajaxCompleteHandler
 * 		New option for managing AJAX onComplete events
 * 
 * @author James Sanders
 * @version 1.0.4
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */

var kamForm = new Class({
	Extends: Events,
	
	className: 'kamForm',
	
	/**
	 * class options 
	 * 
	 * @var object
	 */
	
	options: {
		submitHandler: null,
		errorReporting: 'none',
		errorModalIntroduction: null,
		errorModalOptions: {
			closeMaskOnClick: true
		},
		submitViaAjax: true,
		ajaxCompleteHandler: null,
		ajaxSpinnerOptions: {
			message: 'Sending...',
			'class': 'kamFormSendingMask'
		},
		ajaxSpinnerOpacity: 0.8,
		onInvalidForm: null,
		autoHandleJavascriptResponses: true
	},
	
cssClasses: {
	blockAjax: 'kamNoAjax'
},
	
	/**
	 * storeKey
	 * 
	 * The storage key that is used when storing this class on the form DOMElement
	 * 
	 * @var string
	 */
	
	//storeKey: 'kamForm',
		
	/**
	 * visibleControl
	 * 
	 * The form DOMElement that this class is extending
	 * 
	 * @var DOMElement
	 */
	
	visibleControl: null,
	
	/**
	 * failingInputs
	 * 
	 * An object containing all the form inputs that are currently failing. Stored using the element's
	 * ID as the key and a list of the failed validators as the value
	 * 
	 * @var object
	 */
	
	failingInputs: {},
	
	/**
	 * validator
	 * 
	 * The instance of mootool's Form.Validator that is applied to this form 
	 * 
	 * @var Form.Validator
	 */
	
	validator: null,
		
	/**
	 * initialize
	 * 
	 * Class constructor
	 * 
	 * @param DOMFormElement formElement the form element to extend
	 */
	
	initialize: function(formElement, options){
		// merge the options
		this.setOptions(options);
		
		// check to see whether we should submit via AJAX and, if so, ensure an ID is set on the element
		if ( this.options.submitViaAjax === false || formElement.get('id') ){
			// store the element
			this.visibleControl = formElement;
			
			// store this class on the element itself
			formElement.store( kamForm.storeKey, this );
			
			// initialise the form validation
			this.__setupValidation();
	
			// setup the event handlers (if present)
			if ( this.options.onInvalidForm ){
				this.addEvent('invalidForm', this.options.onInvalidForm);
			}
			
			// finally, flag any labels whose inputs have the "required" class. this duplicaton
			// is needed to visually display required inputs in an easy way
			formElement.getElements('[class*=required]').each( function(element){
				var id = element.get('id');
				if( id ){
					var label = formElement.getElement('label[for='+id+']');
					if ( label ) label.addClass('required');
				}
			});
			
		} else {
			alert("ERROR > kamForm > AJAX submission cannot be used because the form has no ID attribute");
		}
	},
	
	/**
	 * setOptions
	 * 
	 * Merges the supplied options object with the existing options
	 * 
	 * @param object options the options to merge
	 */
	
	setOptions: function(options){
		this.options = Object.merge(this.options, options);
	},
	
	/**
	 * __setupValidation
	 * 
	 * Initialises the form validator
	 */
	
	__setupValidation: function(){
		// create a validator for this form
		this.validator = validator = new Form.Validator(this.visibleControl, {
			evaluateOnSubmit : false,
			evaluateFieldsOnBlur: false,
			evaluateFieldsOnChange: false,
			ignoreHidden: false,
			serial: false,
			onElementFail : function(element, failedValidators){
				// ensure the element is valid - there is a bug in mootools 1.3.1.1 which causes the
				// passed parameter to be "undefined" on rare occasions. hopefully will be fixed in 
				// 1.3.1.2.....
				if ( element ){
					element = document.id(element);

					// check to see whether the returned element is an option. 
					if ( element.tagName == 'OPTION' ){
						// WHY mootools returns option elements in IE7 rather than the select no one knows,
						// and frankly I dont have time to consult the ancient mystics of mount badger. 
						// so, as a fix, try to get the parent select input
						element = element.getElement('! select');
						
						// check to see whether the element is still valid
						if( ! element ){
							return true;
						}
					}
					
					var elementName = element.get('name');
					this.failingInputs[elementName] = {
						element: element,
						failedValidators: failedValidators
					}
				}
			}.bind(this),
			onElementPass : function(element){
				// ensure the element is valid - there is a bug in mootools 1.3.1.1 which causes the
				// passed parameter to be "undefined" on rare occasions. hopefully will be fixed in 
				// 1.3.1.2.....
				if ( element ){
					element = document.id(element);

					// check to see whether the returned element is an option. 
					if ( element.tagName == 'OPTION' ){
						// WHY mootools returns option elements in IE7 rather than the select no one knows,
						// and frankly I dont have time to consult the ancient mystics of mount badger. 
						// so, as a fix, try to get the parent select input
						element = element.getElement('! select');
						
						// check to see whether the element is still valid
						if( ! element ){
							return true;
						}
					}

					var elementName = element.get('name');
					if ( this.failingInputs[elementName] ){
						delete(this.failingInputs[elementName]);
					}
				}
			}.bind(this)
		});
		
		// create a submission handler
		this.visibleControl.addEvent('submit', function(event){
			event.preventDefault();
			this.submitForm();
		}.bind(this));
		
	},
	
	/**
	 * submitForm
	 * 
	 * Begins the submission process performing validation before hand
	 */
	submitForm: function(){		
		var response = {
			cancel: false
		};
		this.fireEvent('submitting', response);
	
		if ( ! response.cancel ){
			// reset the failed validators
			this.failingInputs = {};
			
			// validate the form
			if ( this.validator.validate() ){
				// everything is ok - transmit the data
				this.__transmitForm();
			}
			else{ 
				// this form is invalid. generate the details of the failed input and fire the event
				this.__fireInvalidFormEvent();
			}
		}
	},
	
	/**
	 * __fireInvalidFormEvent
	 *
	 * Builds an object which contains all the failed inputs and their failed validators and
	 * fires the invalidForm event
	 */
	
	__fireInvalidFormEvent: function(){
		// roll through the failing inputs
		var failedInputs = [];
		Object.each( this.failingInputs, function(inputDetails, elementName){
			// try to recover the label for the element
			var elementId = inputDetails.element.get('id');
			var label = false;
			var labelElement = null;
			if ( elementId ){
				labelElement = this.visibleControl.getElement('label[for='+elementId+']')
				if ( labelElement ){
					label = labelElement.get('html');
				}
			}
			if ( label == false ){
				// we couldn't find a label element so just use the name
				label = elementName;
			}
			
			// roll through the failed validators
			var validators = [];
			inputDetails.failedValidators.each( function(failedValidator){
				// break down the validator into key and parameters
				var breakIndex = failedValidator.indexOf(':');
				var hasParams = false;
				if ( breakIndex && breakIndex > 0 ){
					validatorKey = failedValidator.substr(0, breakIndex);
					hasParams = true;
				} else {
					var validatorKey = failedValidator;
				}
				
				// deceode the validator to build the properties (if required)
				validatorProperties = hasParams ? JSON.decode('{'+failedValidator+'}') : {};
				
				// retrieve the validator object and retrieve the message
				var validator = Form.Validator.adders.getValidator(validatorKey);
				var errorMessage = validator.getError(inputDetails.element, validatorProperties);

				// push the validator details into the validators array
				validators.push({
					type: validatorKey,
					validator: failedValidator,
					message: errorMessage
				});
			}.bind(this));
			
			// push the input details into the failed inputs array
			failedInputs.push({
				element: inputDetails.element,
				label: label,
				labelElement: labelElement,
				failedValidators: validators
			});
		}.bind(this));

		// fire the invalid form event
		this.fireEvent('invalidForm',{
			inputs: failedInputs
		});
		
		// report the errors
		this.__reportErrors(failedInputs);
	},
	
	/**
	 * __transmitForm
	 * 
	 * Transmits the form 
	 */
	
	__transmitForm: function(){
		// check to see whether there is a custom submit handler
		if ( this.options.submitHandler ){
			// there is a custom handler. call the handler passing the form element as an argument
			this.options.submitHandler(this.visibleControl);			
		} else {
			// no custom handler, check to see whether we should be submitting via AJAX
			if ( this.options.submitViaAjax && ! this.visibleControl.hasClass(this.cssClasses.blockAjax) ){
				// yes. vall the handler
				this.__transmitFormViaAjax();
			} else {
				// no. just submit the old fashioned way
				this.visibleControl.submit();
			}
		}
	},
	
	/**
	 * __transmitFormViaAjax
	 * 
	 * Transmits the form via AJAX 
	 */
	
	__transmitFormViaAjax: function(){
		// yes. we want to submit via ajax. show a spinner over the top of the form element
		var useAnimations = kamUi.useAnimations;
		var spinner = new Spinner(this.visibleControl, this.options.ajaxSpinnerOptions);
		var formElement = this.visibleControl;
		var formId = formElement.get('id');
		spinner.show();
		
		var spinnerVisibleHandler = function(){
			
			new kamContentRequest(formElement.get('action'),{
				method: 'post', 
				data: this.visibleControl,
				autoHandleJavascriptResponses: this.options.autoHandleJavascriptResponses,
				onSuccess: function(response, responseTree, responseElements, responseHTML, responseJavaScript){
					// check to see whether a custom AJAX complete handler has been supplied
					if ( this.options.ajaxCompleteHandler ){
						this.options.ajaxCompleteHandler(response, responseTree, responseElements, responseHTML, responseJavaScript);
					} else {
						// nope. loop through the elements and look for the relevant item
						responseElements.each( function(element){
							// grab the ID of the element and check to see whether it's the one that we're 
							// after
							var id = element.get('id');
							if ( id && id == formId ){
								// get the height of the form element
								var formHeight = formElement.measure( function(){ 
									return this.getSize().y;
								});
								
								// wipe the ID attribute from the form to prevent temporarily 
								// invalidating the page
								formElement.erase('id');
								
								// inject the new element into the DOM, but in a hidden manner,
								// so we can perform a measure action on it
								element.setStyle('display', 'none');
								element.inject( document.body );

								// get the height of the new element
								var elementHeight = element.measure( function(){ 
									return this.getSize().y;
								});

								var completeHandler = function(){
									// the spinner is no good to us any more, whack it
									if ( useAnimations ){
										new Fx.Morph(spinner, {
											onComplete: function(){
												spinner.destroy();		
											}
										}).start({
											opacity: 0
										});
									} else {
										spinner.destroy();
									}
									
									// replace the form with the new element, set it to visible, albeit
									// with an opacity of 0, and its height to that of the form element
									// that it is replacing
									element.replaces(formElement);
									element.setStyles({
										opacity: 0,
										display: 'block',
										height: formHeight+'px'
									});
									
									// now fade in the new content and at the same time resize it to its
									// true height
									if ( useAnimations ){
										new Fx.Morph(element).start({
											opacity: 1,
											height: elementHeight,
											onComplete: function(){
												new Fx.Scroll(window).toElement(element);
											}
										});
									} else {
										element.setStyles({
											opacity: 1,
											height: elementHeight
										});
										new Fx.Scroll(window).toElement(element);
									}
								};
								
								// fade out the existing content
								if ( useAnimations ){
									new Fx.Morph(formElement, {
										onComplete: completeHandler
									}).start({
										opacity: 0
									});
								} else {
									completeHandler();
								}
							}
						});
					}
					
					// hide the spinner
					spinner.hide();
				}.bind(this),
				onJavascriptHandled: function(){
					// hide the spinner
					spinner.hide();
				}
			}).execute();
		}.bind(this)
		
		// check to see whether animations are enabled
		if ( useAnimations ){
			new Fx.Morph(spinner, {
				onComplete: spinnerVisibleHandler
			}).start({
				opacity: [0, this.options.ajaxSpinnerOpacity]
			});
		} else {
			document.id(spinner).setStyle('opacity', this.options.ajaxSpinnerOpacity);
			spinnerVisibleHandler();
		}
	},
	
	/**
	 * __reportErrors
	 *
	 * Reports the errors of an invalid form based on the configured options
	 */
	
	__reportErrors: function(failedInputs){
		var errorReporting = this.options.errorReporting;
		if ( errorReporting && errorReporting != 'none' ){
			switch ( errorReporting ){
				case 'alert':
					// roll through the failed input data to build the contents for the modal
					var message = 'There are errors with your form:'+"\n";
					failedInputs.each( function(input){
						message += "\n"+input.label+":\n";
						input.failedValidators.each( function(validator){
							message += "\t-"+validator.message+"\n";	
						});
					});
					
					alert(message);
					break;
					
				case 'modal':
					// roll through the failed input data to build the contents for the modal
					var errors = new Element('dl');
					failedInputs.each( function(input){
						new Element('dt', {
							html: input.label
						}).inject(errors);
						
						var ddElement = new Element('dd').inject(errors);
						var ulElement = new Element('ul').inject(ddElement);
						
						input.failedValidators.each( function(validator){
							new Element('li', {
								html: validator.message
							}).inject(ulElement);
						});
					});

					// get the modal options and set the title (unless already supplied) and content
					var modalOptions = this.options.errorModalOptions;
					if ( ! modalOptions.title ){
						modalOptions.title = 'There are errors with your form';
					}

					// check to see whether a intro has been supplied
					var clearElement = new Element('div', {
						'class': 'clear',
						html: '&#160'
					});
					var modalIntro = this.options.errorModalIntroduction;
					if ( modalIntro ){
						// check to see whether the intro is a string
						if ( typeof(modalIntro) == 'string' ){
							// it is. wrap it in a P tag, otherwise the modal's "adopt" call tends
							// to ignore it
							modalIntro = new Element('p',{
								html: modalIntro
							});
						}
						
						// set the content
						modalOptions.content = [modalIntro,errors,clearElement];
					} else {
						// set the content
						modalOptions.content = [errors, clearElement];
					}
					
					// display the errors in a modal
					new kamModal(modalOptions).show();
					
					break;
			}
		}
	}
});
kamForm.storeKey = 'kamForm';

kamForm.getFormInstance = function(element){
	element = document.id(element);
	if ( element ){
		return element.retrieve(kamForm.storeKey);
	}
	return false; 
}

kamForm.__addCustomValidators = function(){
	// add a mustMatch validator
	Form.Validator.add('mustMatch', {
	    errorMsg: function(element, props){
	    	if ( props.mustMatch ){
	    		// try to retrieve the indicated input
	    		var masterInput = document.id(props.mustMatch);
	    		if ( masterInput ){
	    			// try to retrieve the label of the master input
	    			var label = document.getElement('label[for='+props.mustMatch+']');
	    			return 'The value must match that of "'+(label ? label.get('html'): materInput.get('name') )+'"';	
	    		}
	    	}
			return '';
	    },
	    test: function(element, props){
	    	if ( props.mustMatch ){
	    		// try to retrieve the indicated input
	    		var masterInput = document.id(props.mustMatch);
	    		if ( masterInput ){
	    			// compare the values and return the result
	    			return masterInput.value == element.value;
	    		}
	    	}
			return false;
		}
	});
	
	// add a ajax validator
	Form.Validator.add('ajax', {
	    errorMsg: function(element, props){
	    	// try to retrieve the stored ajax result
	    	var ajaxResult = element.retrieve('kamAjaxValidationResult');
	    	if ( ajaxResult ){
	    		// there is one. return the message if available
	    		if ( ajaxResult.message ){
	    			return ajaxResult.message;
	    		}
	    	}
	    	return '';
	    },
	    test: function(element, props){
	    	var result = false;
	    	if ( props.ajax ){
				var uri = new URI(props.ajax);
				uri.setData({
					'kamReqTime': new Date().getTime()
				}, true);

				new Request({
					url: uri.toString(),
					method: 'post',
					data: {
						value: element.get('value')
					},
					async: false,
					onSuccess: function(responseText){
						var ajaxResponse = null;
						eval(responseText);
						if ( ajaxResponse ){
							// check to see whether it was a success
							if ( ajaxResponse.result ){
								// it passed. if there is already a validation result stored on the element
								// wipe if 
								element.eliminate('kamAjaxValidationResult');
								
								// return true
								result = true;
							} else {
								// it wasnt. store the response on the element (for the generation of messages)
								element.store('kamAjaxValidationResult', ajaxResponse);
							}
						}
					}
				}).send();	    		
	    	}
	    	return result;
		}
	});
	
	// add a validate-oneInFieldOptions validator
	Form.Validator.add('validate-oneInFieldOptions', {
	    errorMsg: function(element, props){
	    	return 'At least one item must be selected';
	    },
	    test: function(element, props){
	    	// find the parent container
	    	var itemSet = false;
	    	var containerElement = element.getElement('! .kamFormFieldOptions');
	    	if ( containerElement ){
	    		// retrieve all the inputs of the same type
	    		var elementType = element.get('type');
	    		if ( elementType ){
	    			
	    			containerElement.getElements('input[type='+elementType+']').each( function(item){
	    				if ( item.get('checked') ) itemSet = true;
	    			});
	    		}		
	    	}
	    	return itemSet;
		}
	});
};

kamForm.populateCountrySelect = function(element, currentValue){
	var countries = [   
		{
			name: 'United Kingdom',
			iso: 'GB'
		},
		{
			name: 'Afghanistan',
			iso: 'AF'
		},
		{
			name: 'Åland Islands',
			iso: 'AX'
		},
		{
			name: 'Albania',
			iso: 'AL'
		},
		{
			name: 'Algeria',
			iso: 'DZ'
		},
		{
			name: 'American Samoa',
			iso: 'AS'
		},
		{
			name: 'Andorra',
			iso: 'AD'
		},
		{
			name: 'Angola',
			iso: 'AO'
		},
		{
			name: 'Anguilla',
			iso: 'AI'
		},
		{
			name: 'Antarctica',
			iso: 'AQ'
		},
		{
			name: 'Antigua and Barbuda',
			iso: 'AG'
		},
		{
			name: 'Argentina',
			iso: 'AR'
		},
		{
			name: 'Armenia',
			iso: 'AM'
		},
		{
			name: 'Aruba',
			iso: 'AW'
		},
		{
			name: 'Australia',
			iso: 'AU'
		},
		{
			name: 'Austria',
			iso: 'AT'
		},
		{
			name: 'Azerbaijan',
			iso: 'AZ'
		},
		{
			name: 'Bahamas',
			iso: 'BS'
		},
		{
			name: 'Bahrain',
			iso: 'BH'
		},
		{
			name: 'Bangladesh',
			iso: 'BD'
		},
		{
			name: 'Barbados',
			iso: 'BB'
		},
		{
			name: 'Belarus',
			iso: 'BY'
		},
		{
			name: 'Belgium',
			iso: 'BE'
		},
		{
			name: 'Belize',
			iso: 'BZ'
		},
		{
			name: 'Benin',
			iso: 'BJ'
		},
		{
			name: 'Bermuda',
			iso: 'BM'
		},
		{
			name: 'Bhutan',
			iso: 'BT'
		},
		{
			name: 'Bolivia',
			iso: 'BO'
		},
		{
			name: 'Bosnia and Herzegovina',
			iso: 'BA'
		},
		{
			name: 'Botswana',
			iso: 'BW'
		},
		{
			name: 'Bouvet Island',
			iso: 'BV'
		},
		{
			name: 'Brazil',
			iso: 'BR'
		},
		{
			name: 'British Indian Ocean Territory',
			iso: 'IO'
		},
		{
			name: 'Brunei Darussalam',
			iso: 'BN'
		},
		{
			name: 'Bulgaria',
			iso: 'BG'
		},
		{
			name: 'Burkina Faso',
			iso: 'BF'
		},
		{
			name: 'Burundi',
			iso: 'BI'
		},
		{
			name: 'Cambodia',
			iso: 'KH'
		},
		{
			name: 'Cameroon',
			iso: 'CM'
		},
		{
			name: 'Canada',
			iso: 'CA'
		},
		{
			name: 'Cape Verde',
			iso: 'CV'
		},
		{
			name: 'Cayman Islands',
			iso: 'KY'
		},
		{
			name: 'Central African Republic',
			iso: 'CF'
		},
		{
			name: 'Chad',
			iso: 'TD'
		},
		{
			name: 'Chile',
			iso: 'CL'
		},
		{
			name: 'China',
			iso: 'CN'
		},
		{
			name: 'Christmas Island',
			iso: 'CX'
		},
		{
			name: 'Cocos (Keeling) Islands',
			iso: 'CC'
		},
		{
			name: 'Colombia',
			iso: 'CO'
		},
		{
			name: 'Comoros',
			iso: 'KM'
		},
		{
			name: 'Congo',
			iso: 'CG'
		},
		{
			name: 'Congo, The Democratic Republic of The',
			iso: 'CD'
		},
		{
			name: 'Cook Islands',
			iso: 'CK'
		},
		{
			name: 'Costa Rica',
			iso: 'CR'
		},
		{
			name: 'Cote D\'ivoire',
			iso: 'CI'
		},
		{
			name: 'Croatia',
			iso: 'HR'
		},
		{
			name: 'Cuba',
			iso: 'CU'
		},
		{
			name: 'Cyprus',
			iso: 'CY'
		},
		{
			name: 'Czech Republic',
			iso: 'CZ'
		},
		{
			name: 'Denmark',
			iso: 'DK'
		},
		{
			name: 'Djibouti',
			iso: 'DJ'
		},
		{
			name: 'Dominica',
			iso: 'DM'
		},
		{
			name: 'Dominican Republic',
			iso: 'DO'
		},
		{
			name: 'Ecuador',
			iso: 'EC'
		},
		{
			name: 'Egypt',
			iso: 'EG'
		},
		{
			name: 'El Salvador',
			iso: 'SV'
		},
		{
			name: 'Equatorial Guinea',
			iso: 'GQ'
		},
		{
			name: 'Eritrea',
			iso: 'ER'
		},
		{
			name: 'Estonia',
			iso: 'EE'
		},
		{
			name: 'Ethiopia',
			iso: 'ET'
		},
		{
			name: 'Falkland Islands (Malvinas)',
			iso: 'FK'
		},
		{
			name: 'Faroe Islands',
			iso: 'FO'
		},
		{
			name: 'Fiji',
			iso: 'FJ'
		},
		{
			name: 'Finland',
			iso: 'FI'
		},
		{
			name: 'France',
			iso: 'FR'
		},
		{
			name: 'French Guiana',
			iso: 'GF'
		},
		{
			name: 'French Polynesia',
			iso: 'PF'
		},
		{
			name: 'French Southern Territories',
			iso: 'TF'
		},
		{
			name: 'Gabon',
			iso: 'GA'
		},
		{
			name: 'Gambia',
			iso: 'GM'
		},
		{
			name: 'Georgia',
			iso: 'GE'
		},
		{
			name: 'Germany',
			iso: 'DE'
		},
		{
			name: 'Ghana',
			iso: 'GH'
		},
		{
			name: 'Gibraltar',
			iso: 'GI'
		},
		{
			name: 'Greece',
			iso: 'GR'
		},
		{
			name: 'Greenland',
			iso: 'GL'
		},
		{
			name: 'Grenada',
			iso: 'GD'
		},
		{
			name: 'Guadeloupe',
			iso: 'GP'
		},
		{
			name: 'Guam',
			iso: 'GU'
		},
		{
			name: 'Guatemala',
			iso: 'GT'
		},
		{
			name: 'Guernsey',
			iso: 'GG'
		},
		{
			name: 'Guinea',
			iso: 'GN'
		},
		{
			name: 'Guinea-bissau',
			iso: 'GW'
		},
		{
			name: 'Guyana',
			iso: 'GY'
		},
		{
			name: 'Haiti',
			iso: 'HT'
		},
		{
			name: 'Heard Island and Mcdonald Islands',
			iso: 'HM'
		},
		{
			name: 'Holy See (Vatican City State)',
			iso: 'VA'
		},
		{
			name: 'Honduras',
			iso: 'HN'
		},
		{
			name: 'Hong Kong',
			iso: 'HK'
		},
		{
			name: 'Hungary',
			iso: 'HU'
		},
		{
			name: 'Iceland',
			iso: 'IS'
		},
		{
			name: 'India',
			iso: 'IN'
		},
		{
			name: 'Indonesia',
			iso: 'ID'
		},
		{
			name: 'Iran, Islamic Republic of',
			iso: 'IR'
		},
		{
			name: 'Iraq',
			iso: 'IQ'
		},
		{
			name: 'Ireland',
			iso: 'IE'
		},
		{
			name: 'Isle of Man',
			iso: 'IM'
		},
		{
			name: 'Israel',
			iso: 'IL'
		},
		{
			name: 'Italy',
			iso: 'IT'
		},
		{
			name: 'Jamaica',
			iso: 'JM'
		},
		{
			name: 'Japan',
			iso: 'JP'
		},
		{
			name: 'Jersey',
			iso: 'JE'
		},
		{
			name: 'Jordan',
			iso: 'JO'
		},
		{
			name: 'Kazakhstan',
			iso: 'KZ'
		},
		{
			name: 'Kenya',
			iso: 'KE'
		},
		{
			name: 'Kiribati',
			iso: 'KI'
		},
		{
			name: 'Korea, Democratic People\'s Republic of',
			iso: 'KP'
		},
		{
			name: 'Korea, Republic of',
			iso: 'KR'
		},
		{
			name: 'Kuwait',
			iso: 'KW'
		},
		{
			name: 'Kyrgyzstan',
			iso: 'KG'
		},
		{
			name: 'Lao People\'s Democratic Republic',
			iso: 'LA'
		},
		{
			name: 'Latvia',
			iso: 'LV'
		},
		{
			name: 'Lebanon',
			iso: 'LB'
		},
		{
			name: 'Lesotho',
			iso: 'LS'
		},
		{
			name: 'Liberia',
			iso: 'LR'
		},
		{
			name: 'Libyan Arab Jamahiriya',
			iso: 'LY'
		},
		{
			name: 'Liechtenstein',
			iso: 'LI'
		},
		{
			name: 'Lithuania',
			iso: 'LT'
		},
		{
			name: 'Luxembourg',
			iso: 'LU'
		},
		{
			name: 'Macao',
			iso: 'MO'
		},
		{
			name: 'Macedonia, The Former Yugoslav Republic of',
			iso: 'MK'
		},
		{
			name: 'Madagascar',
			iso: 'MG'
		},
		{
			name: 'Malawi',
			iso: 'MW'
		},
		{
			name: 'Malaysia',
			iso: 'MY'
		},
		{
			name: 'Maldives',
			iso: 'MV'
		},
		{
			name: 'Mali',
			iso: 'ML'
		},
		{
			name: 'Malta',
			iso: 'MT'
		},
		{
			name: 'Marshall Islands',
			iso: 'MH'
		},
		{
			name: 'Martinique',
			iso: 'MQ'
		},
		{
			name: 'Mauritania',
			iso: 'MR'
		},
		{
			name: 'Mauritius',
			iso: 'MU'
		},
		{
			name: 'Mayotte',
			iso: 'YT'
		},
		{
			name: 'Mexico',
			iso: 'MX'
		},
		{
			name: 'Micronesia, Federated States of',
			iso: 'FM'
		},
		{
			name: 'Moldova, Republic of',
			iso: 'MD'
		},
		{
			name: 'Monaco',
			iso: 'MC'
		},
		{
			name: 'Mongolia',
			iso: 'MN'
		},
		{
			name: 'Montenegro',
			iso: 'ME'
		},
		{
			name: 'Montserrat',
			iso: 'MS'
		},
		{
			name: 'Morocco',
			iso: 'MA'
		},
		{
			name: 'Mozambique',
			iso: 'MZ'
		},
		{
			name: 'Myanmar',
			iso: 'MM'
		},
		{
			name: 'Namibia',
			iso: 'NA'
		},
		{
			name: 'Nauru',
			iso: 'NR'
		},
		{
			name: 'Nepal',
			iso: 'NP'
		},
		{
			name: 'Netherlands',
			iso: 'NL'
		},
		{
			name: 'Netherlands Antilles',
			iso: 'AN'
		},
		{
			name: 'New Caledonia',
			iso: 'NC'
		},
		{
			name: 'New Zealand',
			iso: 'NZ'
		},
		{
			name: 'Nicaragua',
			iso: 'NI'
		},
		{
			name: 'Niger',
			iso: 'NE'
		},
		{
			name: 'Nigeria',
			iso: 'NG'
		},
		{
			name: 'Niue',
			iso: 'NU'
		},
		{
			name: 'Norfolk Island',
			iso: 'NF'
		},
		{
			name: 'Northern Mariana Islands',
			iso: 'MP'
		},
		{
			name: 'Norway',
			iso: 'NO'
		},
		{
			name: 'Oman',
			iso: 'OM'
		},
		{
			name: 'Pakistan',
			iso: 'PK'
		},
		{
			name: 'Palau',
			iso: 'PW'
		},
		{
			name: 'Palestinian Territory, Occupied',
			iso: 'PS'
		},
		{
			name: 'Panama',
			iso: 'PA'
		},
		{
			name: 'Papua New Guinea',
			iso: 'PG'
		},
		{
			name: 'Paraguay',
			iso: 'PY'
		},
		{
			name: 'Peru',
			iso: 'PE'
		},
		{
			name: 'Philippines',
			iso: 'PH'
		},
		{
			name: 'Pitcairn',
			iso: 'PN'
		},
		{
			name: 'Poland',
			iso: 'PL'
		},
		{
			name: 'Portugal',
			iso: 'PT'
		},
		{
			name: 'Puerto Rico',
			iso: 'PR'
		},
		{
			name: 'Qatar',
			iso: 'QA'
		},
		{
			name: 'Reunion',
			iso: 'RE'
		},
		{
			name: 'Romania',
			iso: 'RO'
		},
		{
			name: 'Russian Federation',
			iso: 'RU'
		},
		{
			name: 'Rwanda',
			iso: 'RW'
		},
		{
			name: 'Saint Helena',
			iso: 'SH'
		},
		{
			name: 'Saint Kitts and Nevis',
			iso: 'KN'
		},
		{
			name: 'Saint Lucia',
			iso: 'LC'
		},
		{
			name: 'Saint Pierre and Miquelon',
			iso: 'PM'
		},
		{
			name: 'Saint Vincent and The Grenadines',
			iso: 'VC'
		},
		{
			name: 'Samoa',
			iso: 'WS'
		},
		{
			name: 'San Marino',
			iso: 'SM'
		},
		{
			name: 'Sao Tome and Principe',
			iso: 'ST'
		},
		{
			name: 'Saudi Arabia',
			iso: 'SA'
		},
		{
			name: 'Senegal',
			iso: 'SN'
		},
		{
			name: 'Serbia',
			iso: 'RS'
		},
		{
			name: 'Seychelles',
			iso: 'SC'
		},
		{
			name: 'Sierra Leone',
			iso: 'SL'
		},
		{
			name: 'Singapore',
			iso: 'SG'
		},
		{
			name: 'Slovakia',
			iso: 'SK'
		},
		{
			name: 'Slovenia',
			iso: 'SI'
		},
		{
			name: 'Solomon Islands',
			iso: 'SB'
		},
		{
			name: 'Somalia',
			iso: 'SO'
		},
		{
			name: 'South Africa',
			iso: 'ZA'
		},
		{
			name: 'South Georgia and The South Sandwich Islands',
			iso: 'GS'
		},
		{
			name: 'Spain',
			iso: 'ES'
		},
		{
			name: 'Sri Lanka',
			iso: 'LK'
		},
		{
			name: 'Sudan',
			iso: 'SD'
		},
		{
			name: 'Suriname',
			iso: 'SR'
		},
		{
			name: 'Svalbard and Jan Mayen',
			iso: 'SJ'
		},
		{
			name: 'Swaziland',
			iso: 'SZ'
		},
		{
			name: 'Sweden',
			iso: 'SE'
		},
		{
			name: 'Switzerland',
			iso: 'CH'
		},
		{
			name: 'Syrian Arab Republic',
			iso: 'SY'
		},
		{
			name: 'Taiwan, Province of China',
			iso: 'TW'
		},
		{
			name: 'Tajikistan',
			iso: 'TJ'
		},
		{
			name: 'Tanzania, United Republic of',
			iso: 'TZ'
		},
		{
			name: 'Thailand',
			iso: 'TH'
		},
		{
			name: 'Timor-leste',
			iso: 'TL'
		},
		{
			name: 'Togo',
			iso: 'TG'
		},
		{
			name: 'Tokelau',
			iso: 'TK'
		},
		{
			name: 'Tonga',
			iso: 'TO'
		},
		{
			name: 'Trinidad and Tobago',
			iso: 'TT'
		},
		{
			name: 'Tunisia',
			iso: 'TN'
		},
		{
			name: 'Turkey',
			iso: 'TR'
		},
		{
			name: 'Turkmenistan',
			iso: 'TM'
		},
		{
			name: 'Turks and Caicos Islands',
			iso: 'TC'
		},
		{
			name: 'Tuvalu',
			iso: 'TV'
		},
		{
			name: 'Uganda',
			iso: 'UG'
		},
		{
			name: 'Ukraine',
			iso: 'UA'
		},
		{
			name: 'United Arab Emirates',
			iso: 'AE'
		},
		{
			name: 'United States',
			iso: 'US'
		},
		{
			name: 'United States Minor Outlying Islands',
			iso: 'UM'
		},
		{
			name: 'Uruguay',
			iso: 'UY'
		},
		{
			name: 'Uzbekistan',
			iso: 'UZ'
		},
		{
			name: 'Vanuatu',
			iso: 'VU'
		},
		{
			name: 'Venezuela',
			iso: 'VE'
		},
		{
			name: 'Viet Nam',
			iso: 'VN'
		},
		{
			name: 'Virgin Islands, British',
			iso: 'VG'
		},
		{
			name: 'Virgin Islands, U.S.',
			iso: 'VI'
		},
		{
			name: 'Wallis and Futuna',
			iso: 'WF'
		},
		{
			name: 'Western Sahara',
			iso: 'EH'
		},
		{
			name: 'Yemen',
			iso: 'YE'
		},
		{
			name: 'Zambia',
			iso: 'ZM'
		},
		{
			name: 'Zimbabwe',
			iso: 'ZW'
		}
	];
	
	// if no current value has been specified, check to see whether a value has been embedded
	// into the title attribute
	if ( ! currentValue ) currentValue = element.get('title');
	
	// populate the country
	element.empty();
	countries.each( function(country){
		var option = new Element('option', {
			html: country.name,
			value: country.iso
		});
		if ( country.iso == currentValue ) option.set('selected', 'selected');
		option.inject(element);
	});
}

/**
 * Kameleon : Modal window
 * 
 * Provides functionality to display modal windows. 
 *
 * Options 
 * 		closeMaskOnClick (boolean): Whether clicking on the mask should close the modal (defaults to FALSE)
 * 		width (integer): the width of the modal
 * 		height (integer): the height of the modal
 * 		id (string): the ID to apply to the modal
 * 		title (string): the title of the modal (defaults to '')
 * 		content (string): the content of the modal (defaults to '')
 * 		zIndex (integer): the zIndex of the modal (defaults to 999999)
 * 		useAnimations (boolean): a flag showing whether animations should be used when showing or hiding the modal (defaults to TRUE)
 * 		maskOpacity (decimal): the opacity of the mask (defaults to 0.8)
 * 		expandTransition (mootools transition): the transition to use when expanding the modal. only used if useAnimations is TRUE (defaults to Fx.Transitions.Quad.easeOut)
 * 		expandDuration (integer): how long in milliseconds the expand transition should take. only used if useAnimations is TRUE (defaults to 250)
 *		onTitleSet (function): a handler for the titleSet event
 *		onContentSet (function): a handler for the contentSet event
 *		resizeToContent (boolean: a flag showing whether the window should be resized when content is set (defaults to FALSE)
 *		onShow (function): a handler for the show event
 *		onClose (function): a handler for the close event
 *		onDestroy (function): a handler for the destroy event
 *		position (object): An object, which can optionally contain an 'x' and a 'y' property, which sets the 'left' and 'top' styles of the modal respectively. Each value can either by an integer or a function which returns an integer (by default the modal is centered on each axis)
 *		useMask (boolean): A flag showing whether the page should be masked off when displaying the modal (defaults to TRUE)
 *		disablePageScrolling (boolean): A flag showing whether page scrolling should be disabled when the modal is visible (defaults to TRUE) 
 *
 * NOTES:
 * 		- Any methods prefixed with two underscores (__) are intended for internal use and should not be called
 * 			
 * Events:
 * 		titleSet
 * 			Fired when the modal title has been set
 * 			Signature: function(titleElement)
 * 			Parameters:
 * 				titleElement (DOMElement): the title element	
 * 
 * 		contentSet
 * 			Fired when the modal content has been set
 * 			Signature: function(contentElement)
 * 			Parameters:
 * 				contentElement (DOMElement): the content element	
 * 
 * 		show
 * 			Fired when the modal window is shown
 * 			Signature: function()
 * 			
 * 		close
 * 			Fired when a modal window close action begins but before resources are destroyed
 * 			Signature: function()
 * 
 * 		destroy
 * 			Fired when a modal window close action has completed and resources have been destroyed
 * 			Signature: function()
 * 
 * CHANGE LOG:
 * ===================
 * 
 * 05/03/2011 (Version 1.0.0)
 * --------------------------
 * 
 * 15/03/2011 (Version 1.0.1)
 * --------------------------
 * Option: height
 * 		Changed default to NULL to indicate auto-sizing
 * 
 * Method: __createComponents
 * 		- Updated to ensure that the width and height of the window is not greater than the viewport 
 * 		- Updated to automatically calculate the height of the modal, based on its contents, if no height option was supplied
 * 
 * 06/04/2011 (Version 1.0.2)
 * --------------------------
 * Option: width
 * 		Changed default to NULL to indicate auto-sizing
 * 
 * Method: __createComponents
 * 		- Updated to ensure that the width and height are automatically calculated if no values are supplied 
 * 		
 * 15/04/2011 (Version 1.0.3)
 * --------------------------
 * Option: position
 * 		New option which allows the user to override the centering behaviour of the modal
 * 
 * Option: useMask
 * 		New option which allows the user to disable the mask
 * 
 * Method: __getPositionOverrides
 * 		New method which returns any supplied position overrides
 * 
 * Property: resizeHandler
 * 		Updated to handle the 'position' option
 * 
 * Method: resize
 * 		Updated to handle the 'position' option
 * 
 * Method: showModal
 * 		Updated to handle the 'position' option
 * 
 * Method: __createControls
 * 		Updated to handle the 'useMask' option
 * 
 * Method: __showMask
 * 		Updated to handle the 'useMask' option
 * 
 * Method: close
 * 		Updated to handle the 'useMask' option
 * 
 * 
 * 10/05/2011 (Version 1.0.4)
 * --------------------------
 * Event: destroy
 * 		New event fired once all resources have been destroyed
 * 
 * Option: onDestroy
 * 		Handler for destroy event
 * 
 * Options: disablePageScrolling
 * 		Flag to enable/disable page scrolling on show
 * 
 * Method: initalize
 * 		Updated to handle new options
 * 
 * Method: __showModal
 * 		Updated to handle new options
 * 
 * Method: __destruct
 * 		Updated to handle new options
 * 
 * @author James Sanders
 * @version 1.0.4
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */

var kamModal = new Class({
	Extends: kamUiEvents,
	
	className: 'kamModal',
	
	/**
	 * options
	 * 
	 * Class default options
	 * 
	 * @var object
	 */
	
	options: {
		closeMaskOnClick: false,
		width: null,
		height: null, 
		id: null,
		modalClass: 'kamModal',
		useMask: true,
		maskClass: 'kamModalMask',
		title: '',
		content: '',
		zIndex: 999999,
		useAnimations: true,
		maskOpacity: 0.8,
		expandTransition: Fx.Transitions.Quad.easeOut,
		expandDuration: 250,
		onTitleSet: null,
		onContentSet: null,
		position: null,
		onShow: null,
		onClose: null,
		onDestroy: null,
		disablePageScrolling: true,
		resizeToContent: false
	},
	
	/**
	 * cssClasses
	 * 
	 * CSS classes that are automatically applied to DOMElements
	 * 
	 * @var object
	 */
	
	cssClasses: {
		outer: 'outer',
		header: 'header',
		title: 'title',
		closeButton: 'close',
		content: 'content'
	},
	
	/**
	 * elements
	 * 
	 * Important DOMElements that are accessed throught the class's lifespan
	 * 
	 * @var object
	 */
	
	elements: {
		outer: null,
		title: null,
		content: null
	},
	
	/**
	 * mask
	 * 
	 * The Mask instance used when displaying the modal window
	 * 
	 * @var Mask
	 */
	
	mask: null,
	
	/**
	 * maskElement
	 * 
	 * The DOMElement that forms the mask
	 * 
	 * @var DOMElement
	 */
	
	maskElement: null,
	
	/**
	 * visibleControl
	 * 
	 * The modal DOMElement
	 * 
	 * @var DOMElement
	 */
	
	visibleControl: null,
	
	/**
	 * modalVisible
	 * 
	 * A flag showing whether the modal is currently visible
	 * 
	 * @var boolean
	 */
	
	modalVisible: false,
	
	/**
	 * resizeHandler
	 * 
	 * The resize / re-position handler to manage updating the modal's location
	 */
	
	resizeHandler: function(){
		// reposition the modal window ( if visible )
		if ( this.modalVisible ){
			// center the modal
			this.visibleControl.position('center');
			
			// apply any position overrides
			var positionOverrides = this.__getPositionOverrides();
			if ( positionOverrides.x ){
				this.visibleControl.setStyle('left', positionOverrides.x );
			}
			if ( positionOverrides.y ){
				this.visibleControl.setStyle('top', positionOverrides.y );
			}
			
			// resize the mask
			if ( this.options.useMask ){
				this.mask.resize();
			}
		}
	},
	
	/**
	 * resizeListenerHandle
	 * 
	 * The handle used whilst listening for window resize events
	 */
	
	resizeListenerHandle: null,
	
	/**
	 * scrollListenerHandle
	 * 
	 * The handle used whilst listening for window scroll events
	 */
	
	scrollListenerHandle: null,
	
	/**
	 * setOptions
	 * 
	 * Merges the supplied options object with the existing options
	 * 
	 * @param object options the options to merge
	 */
	
	setOptions: function(options){
		this.options = Object.merge(this.options, options);
	},
	
	/**
	 * initialize
	 * 
	 * Class constructor
	 * 
	 * @param object (OPTIONAL) options to apply to the class
	 */
	
	initialize: function(options){
		this.globalEventsHandler = kamModal.events;
		this.setOptions(options);
		this.__createControls();
		this.__addTrackingEvents();
		
		// add the event handlers
		var options = this.options;
		if ( options.onTitleSet ) this.addEvent('titleSet', options.onTitleSet );
		if ( options.onContentSet ) this.addEvent('contentSet', options.onContentSet );
		if ( options.onShow ) this.addEvent('show', options.onShow );
		if ( options.onClose ) this.addEvent('close', options.onClose );
		if ( options.onDestroy ) this.addEvent('destroy', options.onDestroy );
	},
	
	/**
	 * __createControls
	 * 
	 * Creates the modal controls
	 */
	
	__createControls: function(){
		// create a mask
		if ( this.options.useMask ){
			var maskId = this.options.maskClass+kamUi.getId();
			this.mask = new Mask(document.body, {
				hideOnClick: false,
				destroyOnClick: false,
				'class': this.options.maskClass,
				id: maskId,
				style: {
					opacity: this.options.maskOpacity
				}
			});
			this.maskElement = document.id(maskId);
			this.mask.addEvent('click', function(){
				// only process the click if the modal is visible
				if ( this.modalVisible ){
					if ( this.options.closeMaskOnClick ){
						this.close();
					}
				}
			}.bind(this));
		};
		
		// create the modal window
		this.visibleControl = new Element('div', {
			'class': this.options.modalClass,
			styles:{
				position: 'absolute',
				zIndex: this.options.zIndex,
				display: 'none'
			}
		}).inject(document.body);
		
		// if an ID has been supplied, set it on the modal
		if ( this.options.id ){
			this.visibleControl.set('id', this.options.id);
		}
		
		// create the outer
		this.elements.outer = new Element('div', {
			'class': this.cssClasses.outer
		}).inject(this.visibleControl);
		
		// create the header bar (including title and close button)
		this.elements.header = new Element('div', {
			'class': this.cssClasses.header
		}).inject(this.elements.outer);
		
		this.elements.title = new Element('p', {
			'class': this.cssClasses.title,
			html: this.options.title
		}).inject( this.elements.header );
		
		var closeButton = new Element('a', {
			'class': this.cssClasses.closeButton,
			href: '#',
			html: 'close'
		}).inject( this.elements.header );
		closeButton.addEvent('click', function(event){
			event.preventDefault();
			this.close();
		}.bind(this));
		
		// insert a clear into the header
		new Element('div', {
			html: '&#160;',
			styles: {
				display: 'block',
				clear: 'both'
			}
		}).inject(this.elements.header);
		
		// create the inner (content) 
		this.elements.content = new Element('div', {
			'class': this.cssClasses.content
		}).inject(this.elements.outer);	
		
		// set the content
		this.setContent( this.options.content );

		// if either the width or height of the modal hasn't been supplied, determine them automatically
		var elementDimensions = kamUi.getElementDimensions(this.visibleControl);
		if ( ! this.options.width ){	
			this.options.width = elementDimensions.x;	
		}
		if ( ! this.options.height ){	
			this.options.height = elementDimensions.y;	
		}
		
		// ensure the width is not larger than the viewport
		var windowSize = window.getSize();
		if ( this.options.width > windowSize.x ){
			// the set width is too large. update it to the max size of the window -50 (to allow 
			// for a bit of a margin)
			this.options.width = (windowSize.x - 50);
		}
		
		// ensure the height is not larger than the viewport
		if ( this.options.height > windowSize.y ){
			// the set height is too large. update it to the max size of the window -50 (to allow 
			// for a bit of a margin)
			this.options.height = (windowSize.y - 50);
		}

		// apply the default width and height to the modal
		this.visibleControl.setStyles({
			width: this.options.width,
			height: this.options.height
		});
		
		// store the class on the visible control
		this.visibleControl.store(kamModal.storeKey, this);
	},
	
	/**
	 * resize
	 * 
	 * Resizes the modal to the supplied dimensions
	 * 
	 * @param integer width the new width
	 * @param integer height the new height
	 */
	
	resize: function(width, height){
		// ensure the width is not larger than the viewport
		var windowSize = window.getSize();
		if ( width > windowSize.x ){
			// the set width is too large. update it to the max size of the window -50 (to allow 
			// for a bit of a margin)
			width = (windowSize.x - 50);
		}
		
		// ensure the height is not larger than the viewport
		var yAxisShrunk = false;
		if ( height > windowSize.y ){
			// the set height is too large. update it to the max size of the window -50 (to allow 
			// for a bit of a margin)
			var yAxisShrunk = true;
			height = (windowSize.y - 50);
		}
		
		// update the width and height values
		var currentWidth = this.options.width;
		var currentHeight = this.options.height;
		this.options.width = width;
		this.options.height = height;
		
		// set the width, height and overflow of the content 
		var headerDimensions = kamUi.getElementDimensions(this.elements.header);
		var contentHeight = height - headerDimensions.y;
		
		var overflowValue = 'visible';
		if ( yAxisShrunk ) overflowValue = 'auto';
		this.elements.content.setStyles({
			width: width,
			height: contentHeight,
			'overflow-y': overflowValue
		});
		
		// if the modal is visible, resize and position it
		if ( this.modalVisible ){
			var currentCoords = this.visibleControl.getCoordinates();
			var currentLeft = currentCoords.left;
			var currentTop = currentCoords.top;
			
			// get the postition overrides
			var positionOverrides = this.__getPositionOverrides();

			// if there is a position override for left, don't allow it to change. otherwise, calculate
			// the new pos
			if ( positionOverrides.x ){
				var newLeft = positionOverrides.x; 
			} else {
				if ( width > currentWidth ){
					var newLeft = currentLeft - ((width - currentWidth) / 2);
				} else {
					var newLeft = currentLeft + ((currentWidth - width) / 2);
				}
			}
	
			// if there is a position override for top, don't allow it to change. otherwise, calculate
			// the new pos
			if ( positionOverrides.y ){
				var newTop = positionOverrides.y; 
			} else {
				if ( height > currentHeight ){
					var newTop = currentTop - ((height - currentHeight) / 2);
				} else {
					var newTop = currentTop + ((currentHeight - height) / 2);
				}
			}
			
			// resize and reposition the modal
			if ( kamUi.useAnimations && this.options.useAnimations ){
				new Fx.Morph(this.visibleControl).start({
					left: [currentLeft, newLeft],
					top: [currentTop, newTop],
					width: [currentWidth, this.options.width],
					height: [currentHeight, this.options.height]
				})
			} else {
				this.visibleControl.setStyles({
					left: newLeft,
					top: newTop,
					width: this.options.width,
					height: this.options.height
				});
			}
		}
	},
	
	/**
	 * resizeToContent
	 * 
	 * Resize the modal's height to that of its content
	 */
	
	resizeToContent: function(){
		// grab the current height of the content element and then remove any restriction (otherwise
		// the measure will return the fixed height rather than the real height)
		var currentOuterHeight = this.elements.content.getStyle('height').replace('px','');
		this.elements.content.setStyles({
			height:'',
			'overflow': 'visible',
			'overflow-y': 'visible'
		});

		// measure the outer element to determine the full height
		var newHeight = kamUi.getElementDimensions(this.elements.outer).y;

		// reapply the previous height style on the outer
		this.elements.content.setStyle('height',currentOuterHeight);
		
		// initiate a resize based on the new content height
		this.resize(this.options.width, newHeight);
	},
	
	/**
	 * __addTrackingEvents
	 * 
	 * Adds tracking events to the window to perform resizing and repositioning actions to the modal 
	 */
	
	__addTrackingEvents: function(){
		// create the resize and scroll listener handles and add them to the window 
		this.resizeListenerHandle = this.resizeHandler.bind(this);
		window.addEvent('resize', this.resizeListenerHandle);
		this.scrollListenerHandle = this.resizeHandler.bind(this);
		window.addEvent('scroll', this.scrollListenerHandle);
	},
	
	/**
	 * setTitle
	 * 
	 * Sets the title of the modal
	 * 
	 * @param string title the title to apply
	 */
	
	setTitle: function(title){
		if ( typeof(title) == 'string' ){
			this.elements.title.set('html', title);
		} else {
			this.elements.title.set('html', '');
			this.elements.title.adopt(title);
		}
		
		this.fireEvent('titleSet', this.elements.title);
	},

	/**
	 * setContent
	 * 
	 * Sets the content of the modal
	 * 
	 * @param string content the content to apply
	 */
	
	setContent: function(content){
		if ( typeof(content) == 'string' ){
			this.elements.content.set('html', content);
		} else {
			this.elements.content.set('html', '');
			this.elements.content.adopt(content);
		}
		
		this.fireEvent('contentSet', this.elements.content);
		
		// if enabled, resize the window to the new content
		if ( this.options.resizeToContent ) this.resizeToContent();
	},
	
	/**
	 * show
	 * 
	 * Displays the modal window
	 */
	
	show: function(){
		this.__showMask(true);
	},
	
	/**
	 * __showMask
	 * 
	 * Displays the mask
	 * 
	 * @param boolean chainShowModal if TRUE the modal will be displayed once the mask is visible
	 */
	
	__showMask: function(chainShowModal){
		if ( this.options.useMask ){
			if ( kamUi.useAnimations && this.options.useAnimations ){
				// set the body tags overflow to hidden to prevent badgered scrolling
				if ( this.options.disablePageScrolling ) document.id(document.body).setStyle('overflow', 'hidden');
				
				// show the mask 
				this.maskElement.setStyle('opacity', 0);
				this.mask.show();
				
				new Fx.Morph(this.maskElement, {
					onComplete: function(){
						if ( chainShowModal ) this.__showModal();
					}.bind(this)
				}).start({
					'opacity': this.options.maskOpacity
				});	
			} else {
				this.mask.show();
				this.maskElement.setStyle('opacity', this.options.maskOpacity);
				if ( chainShowModal ) this.__showModal();
			}
		} else {
			if ( chainShowModal ) this.__showModal();
		}
	},
	
	/**
	 * __getPositionOverrides
	 * 
	 * Retrieves any valid position overrides that have been supplied
	 * 
	 * @return object
	 */
	
	__getPositionOverrides: function(){
		// check to see whether any positioning parameters have been supplied
		var position = this.options.position;
		var overrides = {};
		if ( position ){
			// check for x - left
			if ( position.x ){
				// get the newLeft value (either directly or by calling the supplied function)
				var newLeft = typeof(position.x) == 'function' ? position.x() : position.x;
				
				// set the value (if the value is numeric)
				if ( isNaN(newLeft) === false ){
					overrides.x = newLeft;
				}
			}
			
			// check for y - top
			if ( position.y ){
				// get the newLeft value (either directly or by calling the supplied function)
				var newTop = typeof(position.y) == 'function' ? position.y() : position.y;
				
				// set the value (if the value is numeric)
				if ( isNaN(newTop) === false ){
					overrides.y = newTop;
				}
			}
		}
		return overrides;
	},
	
	/**
	 * __showModal
	 * 
	 * Displays the modal
	 */
	
	__showModal: function(){
		if ( ! this.modalVisible ){
			// get the position overrides
			var positionOverrides = this.__getPositionOverrides();
			
			// fire the preShow event
			this.fireEvent('preShow');
			
			// display the modal (animating if allowed)
			if ( kamUi.useAnimations && this.options.useAnimations ){
				this.visibleControl.setStyles({
					width: '1px',
					height: '1px',
					display: 'block',
					overflow: 'hidden'
				});
				
				// center the modal
				this.visibleControl.position('center');
				
				// calculate the half width and height of the modal
				var halfWidth = this.options.width / 2;
				var halfHeight = this.options.height / 2;
				
				// apply any position overrides
				if ( positionOverrides.x ){
					// NOTE: ensure that the halfWidth is added otherwise the margin will be
					// negative by the half
					this.visibleControl.setStyle('left', (positionOverrides.x + halfWidth) );
				}
				if ( positionOverrides.y ){
					// NOTE: ensure that the halfWidth is added otherwise the margin will be
					// negative by the half
					this.visibleControl.setStyle('top', (positionOverrides.y + halfHeight) );
				}
				
				var currentPosition = this.visibleControl.getCoordinates();
				
				this.elements.outer.setStyle('opacity', '0');
				
				new Fx.Morph(this.visibleControl, {
					transition: this.options.expandTransition,
					duration: this.options.expandDuration,
					onComplete: function(){
						new Fx.Morph(this.elements.outer, {
							onComplete: function(){
								this.modalVisible = true;
								
								// fire a show event
								this.fireEvent('show');
							}.bind(this)
						}).start({
							'opacity': 1
						})
					}.bind(this)
				}).start({
					left: currentPosition.left - halfWidth,
					top: currentPosition.top - halfHeight,
					width: this.options.width,
					height: this.options.height
				});
			} else {
				// apply any position overrides
				if ( positionOverrides.x ){
					this.visibleControl.setStyle('left', positionOverrides.x );
				}
				if ( positionOverrides.y ){
					this.visibleControl.setStyle('top', positionOverrides.y );
				}
				
				this.visibleControl.setStyles({
					width: this.options.width,
					height: this.options.height,
					display: 'block'
				});
				
				this.visibleControl.position('center');
				
				this.elements.outer.setStyles({
					display: 'block',
					opacity: 1
				});
				
				this.modalVisible = true;
				
				// fire a show event
				this.fireEvent('show');
			}
		}
	},
	
	/**
	 * close
	 * 
	 * Hides and destroys the modal window
	 */
	
	close: function(){
		var closeHandler = function(){
			// fire a close event
			this.fireEvent('close');
			this.__destruct();
			
			// set the body tag's overflow back to auto to allow for scrolling again
			if ( this.options.disablePageScrolling ) document.id(document.body).setStyle('overflow', 'auto');
		}.bind(this);
		
		if ( this.modalVisible && kamUi.useAnimations && this.options.useAnimations ){
			new Fx.Morph(this.visibleControl, {
				duration: 250,
				onComplete: function(){
					if ( this.options.useMask ){
						new Fx.Morph(this.maskElement, {
							duration: 250,
							onComplete: function(){
								closeHandler();
							}.bind(this)
						}).start({
							'opacity': 0
						});
					} else {
						closeHandler();
					}
				}.bind(this)
			}).start({
				'opacity': 0
			});
		} else {
			closeHandler();
		}
	},
	
	/**
	 * __destruct
	 * 
	 * Manages the destruction of all DOM elements and initialised resources
	 */
	
	__destruct: function(){
		// remove the tracking events
		window.removeEvent('resize', this.resizeListenerHandle);
		window.removeEvent('scroll', this.scrollListenerHandle);
		
		// destroy the mask and the modal
		this.visibleControl.destroy();
		if ( this.options.useMask ) this.mask.destroy();
		
		// fire the destroy event
		this.fireEvent('destroy');
	}
});

// add the global events handler
kamModal.events = new kamGlobalEvents();

// add the store key to the kamModal (the key under which the modal instance is stored on the DOMElement)
kamModal.storeKey = 'kamModal';

// add a static method for retrieving the modal instance from the DOM via its ID or from the element
kamModal.getModalInstance = function(element){
	// as long as we have an element, try to pull the modal instance from it
	element = document.id(element);
	if ( element ){
		var modal = element.retrieve(kamModal.storeKey);
		if ( modal ) return modal;
	}
	return false;
};

/**
 * Kameleon : Video modal window
 * 
 * Displays a video player in a modal player
 * 
 * EXTENDS: kamModal
 *
 * Options 
 * 		autoPlay (boolean): a flag showing whether the video should start playing when the modal is displayed (defaults to FALSE)
 * 		playerWidth (integer): the desired width of the player (by default it will be the full width of the modal's content element)  
 * 		playerHeight (integer): the desired height of the player (by default it will be automatically calculated based on the player's width and a 16:9 viewport)
 * 		controlBarRenderHandler (function): a custom render handler which allows the modification or replacement of the video modal's control bar. Two arguments are supplied 
 * 			to the function: buildElements (object) - an object containing all the major control elements; controlBar (DOMElement) - the structured default control 
 * 			bar element. The returned results of the handler will be added to the modal
 * 		videoWidth (integer): The width of the video. If supplied the value will be taken into account whilst determining whether to stretch videos or allow them to maintain their original size
 * 		videoHeight (integer): The height of the video. If supplied the value will be taken into account whilst determining whether to stretch videos or allow them to maintain their original size  
 *
 * NOTES:
 * 		- Any methods prefixed with two underscores (__) are intended for internal use and should not be called
 * 			
 * Events:
 * 		play
 * 			Fired when player begins to play
 * 			Signature: function()
 * 						
 * 		pause
 * 			Fired when the modal window is paused
 * 			Signature: function()
 * 
 * CHANGE LOG:
 * ===================
 * 
 * 10/03/2011 (Version 1.0.0)
 * --------------------------
 * 
 * 15/03/2011 (Version 1.0.1)
 * --------------------------
 * Property: jwPlayerResources
 * 		Updated to point at version 5.5 rather than 5.4
 * 
 * Method: __createControls
 * 		Updated to include "download" mode on JwPlayer for mobile support
 * 
 * 23/03/2011 (Version 1.0.2)
 * --------------------------
 * Property: googleAnalyticsCategoryKey
 * 		New property containing the key for custom video Google Analytic events
 * 
 * Property: googleAnalyticsActions
 * 		New property containing the action keys for custom video Google Analytic events
 * 
 * Method: __createControls
 * 		Updated to push custom Google Analytic events
 * 
 * 06/04/2011 (Version 1.0.3)
 * --------------------------
 * Property: googleAnalyticsActions
 * 		New properties for share links
 * 
 * Event: time
 * 		New event which fires when the video player's position changes
 * 
 * Event: complete
 * 		New event which fires when the video player finishes playing back a video
 * 
 * Event: bufferChange
 * 		New event which fires when the video player's buffer value changes
 * 
 * Option: onTime
 * 		Event handler for the "time" event
 * 
 * Option: onComplete
 * 		Event handler for the "complete" event
 * 
 * Option: onBufferChange
 * 		Event handler for the "bufferChange" event
 * 
 * Options: includeShareLinks
 * 		New option flag which sets whether share links should be included on the control bar
 * 
 * Options: controlBarRenderHandler
 * 		New option to allow for a custom control bar rendering handler
 * 
 * Method: __createControls
 * 		- Updated to handle the controlBarRenderHandler option
 * 		- Altered to handle share links 
 * 
 * Method: initialise
 * 		Altered to correctly handle new options
 * 
 * 
 * 21/04/2011 (Version 1.0.4)
 * --------------------------
 * Option: videoWidth
 * 		New option to help determine whether videos should be stretched
 * 
 * Option: videoHeight
 * 		New option to help determine whether videos should be stretched
 * 
 * Method: __createControls
 * 		Updated to handle the videoWidth / videoHeight options and determine whether the video should be stretched
 * 
 * @author James Sanders
 * @version 1.0.4
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */

var kamVideoModal = new Class({
	Extends: kamModal,
	
	className: 'kamVideoModal',
	
	/**
	 * jwPlayer
	 * 
	 * The instance of JW player that this modal maintains
	 * 
	 * @var JW Player
	 */
	
	jwPlayer: null,
	
	/**
	 * videoUrl
	 * 
	 * The URL of the video to play
	 * 
	 * @var string
	 */
	
	videoUrl: null,
	
	/**
	 * videoDuration
	 * 
	 * The duration of the video in seconds. This value is only populated once the video begins to play
	 * 
	 * @var integer
	 */
	
	videoDuration: null,
	
	/**
	 * jwPlayerResources
	 * 
	 * URL information on where the JW Player resources reside
	 * 
	 * @var object
	 */
	
	jwPlayerResources: {
		baseUrl: 'http://resources.kameleondigital.com/jwPlayer/5.5/',
		flashPlayer: 'player.swf'
	},
	
	/**
	 * googleAnalyticsCategoryKey
	 * 
	 * The category key to be used when pushing custom events to GA
	 */
	
	googleAnalyticsCategoryKey: 'Video links',
	
	/**
	 * googleAnalyticsActions
	 * 
	 * Stores the pre-defined keys to be used when pushing custom events to Google Analytics
	 * 
	 * @var object
	 */
	
	googleAnalyticsActions: {
		played: 'Played',
		paused: 'Paused',
		progressPrefix: 'Progress - ',
		sharedViaEmail: 'Shared via mail',
		sharedViaFacebook: 'Shared via Facebook',
		sharedViaTwitter: 'Shared via Twitter'
	},
	
	/**
	 * initialize
	 * 
	 * Class constructor
	 * 
	 * @param string videoUrl the URL of the file to play
	 * @param object options (OPTIONAL) the options to apply to this class
	 */
	
	initialize: function(videoUrl, options){
		// merge the video cssClasses with the modal
		this.cssClasses = Object.merge( this.cssClasses, {
			modal: 'kamVideoModal',
			player: 'player',
			controlBar: 'controlBar',
			playButton: 'play',
			pauseButton: 'pause',
			timeDetails: 'timeDetails',
			timeBar: 'timeBar',
			totalTimeBar: 'totalTimeBar',
			bufferTimeBar: 'bufferTimeBar',
			currentTimeBar: 'currentTimeBar',
			currentTime: 'currentTime',
			totalTime: 'totalTime',
			shareLinks: 'shareLinks',
			shareByEmail: 'shareByEmail',
			shareByFacebook: 'shareByFacebook',
			shareByTwitter: 'shareByTwitter'
		});
		
		// ensure the a file has been specified
		this.setOptions(options);
		if ( videoUrl && typeof(videoUrl) == 'string' ){
			// store the video URL
			this.videoUrl = videoUrl;
			
			// fire the parent constructor
			this.parent(options);
			
			// add the event listeners defined in the options
			if ( this.options.onTime ){
				this.addEvent('time', this.options.onTime);
			}
			if ( this.options.onComplete ){
				this.addEvent('complete', this.options.onComplete);
			}
			if ( this.options.onBufferChange ){
				this.addEvent('bufferChange', this.options.onBufferChange);
			}
			
			// add a show event listener ( so we can begin autoPlay if enabled ). The reason that we dont want
			// to override the show method is that if animations are enabled the video may start playing before
			// the content is actually visible
			this.addEvent('show', function(){
				// check to see whether we should automatically begin playing the video
				if ( this.options.autoPlay ){
					this.play();
				}
			}.bind(this));
		} else {
			alert("ERROR > kamVideoModal > no 'videoUrl' has been supplied")
		}
	},
	
	/**
	 * __createControls
	 * 
	 * Creates the player and the control bar elements
	 */
	
	__createControls: function(){
		var options = this.options;
		this.parent();
		
		// determine whether GA is enabled for the various click handlers
		var isGoogleAnalyticsEnabled = kamUi.isGoogleAnalyticsEnabled();
		
		// add the video modal class to the visible control
		this.visibleControl.toggleClass(this.cssClasses.modal);
		
		// inject a video place holder into the content
		var contentElement = this.elements.content;
		var player = new Element('div', {
			'class': this.cssClasses.player
		}).inject(contentElement);
		var containerId = kamUi.getId('kamVideoPlayer');
		new Element('div', {
			html: '&#160',
			id: containerId
		}).inject(player);
		
		// inject the control bar
		var buildElements = {};
		var controlBar = new Element('div', {
			'class': this.cssClasses.controlBar
		});
		buildElements.controlBar = controlBar;
		
		// inject the play/pause button
		this.elements.playButton = new Element('a', {
			href: '#',
			html: 'play/pause',
			'class': this.cssClasses.playButton
		}).inject(controlBar);
		buildElements.playButton = this.elements.playButton;
		
		this.elements.playButton.addEvent('click', function(event){
			event.preventDefault();
			
			var currentState = this.jwPlayer.getState();
			if ( currentState == 'PAUSED' || currentState == 'IDLE' ){
				this.play();
			} else {
				this.pause();
			}
		}.bind(this));

		// inject the time bar, the buffer time bar and the current time bar along with the labels
		var timeDetails = new Element('div', {
			'class': this.cssClasses.timeDetails
		}).inject(controlBar);
		this.elements.currentTime = new Element('p', {
			'class': this.cssClasses.currentTime
		}).inject(timeDetails);
		this.elements.timeBar = new Element('div', {
			'class': this.cssClasses.timeBar
		}).inject(timeDetails);
		this.elements.bufferTimeBar = new Element('div', {
			'class': this.cssClasses.bufferTimeBar
		}).inject(this.elements.timeBar);
		this.elements.currentTimeBar = new Element('div', {
			'class': this.cssClasses.currentTimeBar
		}).inject(this.elements.timeBar);
		this.elements.totalTime = new Element('p', {
			'class': this.cssClasses.totalTime
		}).inject(timeDetails);
		
		buildElements.timeDetails = timeDetails;
		buildElements.currentTime = this.elements.currentTime;
		buildElements.totalTime = this.elements.totalTime;
		buildElements.timeBar = this.elements.timeBar;
		buildElements.bufferTimeBar = this.elements.bufferTimeBar;
		buildElements.currentTimeBar = this.elements.currentTimeBar;
		
		// add a click handler to the time bar which will allow you to seek once the video has buffered
		this.elements.timeBar.addEvent('click', function(event){
			if ( this.videoDuration ){
				var timeBarCoords = this.elements.timeBar.getCoordinates();
				var percentageClick = (event.page.x - timeBarCoords.left) / timeBarCoords.width;
				var newPosition = this.videoDuration * percentageClick;
			
				this.jwPlayer.seek( (this.videoDuration * percentageClick).toInt() );
			}
		}.bind(this));
		
		// get the fully qualified URL of the current page for the share links
		var shareUri = new URI(window.location);
		shareUri = encodeURIComponent(shareUri.toString());

		// build the share components
		var shareLinks = new Element('div', {
			'class': this.cssClasses.shareLinks
		});
		buildElements.shareLinks = shareLinks;
		buildElements.shareByEmailLink = new Element('a', {
			'class': this.cssClasses.shareByEmail,
			html: '&#160;',
			href: 'mailto:?body='+shareUri
		}).inject(shareLinks);
		buildElements.shareByFacebookLink = new Element('a', {
			'class': this.cssClasses.shareByFacebook,
			html: '&#160;',
			href: 'http://www.facebook.com/sharer.php?u='+shareUri
		}).inject(shareLinks);
		buildElements.shareByTwitterLink = new Element('a', {
			'class': this.cssClasses.shareByTwitter,
			html: '&#160;',
			href: 'http://twitter.com/home?status='+shareUri
		}).inject(shareLinks);

		// add the click handlers to the share links
		buildElements.shareByEmailLink.addEvent('click', function(event){
			event.preventDefault();			
			window.open(buildElements.shareByEmailLink.get('href'));

			// push a custom event to Google Analytics, if enabled
			if ( isGoogleAnalyticsEnabled ){
				_gaq.push(['_trackEvent', this.googleAnalyticsCategoryKey, this.googleAnalyticsActions.sharedViaEmail, this.videoUrl]);	
			}
		});
		buildElements.shareByFacebookLink.addEvent('click', function(event){
			event.preventDefault();			
			window.open(buildElements.shareByFacebookLink.get('href'));

			// push a custom event to Google Analytics, if enabled
			if ( isGoogleAnalyticsEnabled ){
				_gaq.push(['_trackEvent', this.googleAnalyticsCategoryKey, this.googleAnalyticsActions.sharedViaFacebook, this.videoUrl]);	
			}
		});
		buildElements.shareByTwitterLink.addEvent('click', function(event){
			event.preventDefault();			
			window.open(buildElements.shareByTwitterLink.get('href'));

			// push a custom event to Google Analytics, if enabled
			if ( isGoogleAnalyticsEnabled ){
				_gaq.push(['_trackEvent', this.googleAnalyticsCategoryKey, this.googleAnalyticsActions.sharedViaTwitter, this.videoUrl]);	
			}
		});

		// check to see whether the share components should be automatically appended to the control bar
		if( options.includeShareLinks ){
			controlBar.grab(shareLinks);
		}
		
		// check to see whether a video width has been supplied
		var playerWidth = options.playerWidth;
		if ( ! playerWidth ){
			// no. set it to be the size of the content element
			var contentDimensions = contentElement.measure( function(){ 
				return this.getSize(); 
			});
		
			playerWidth = contentDimensions.x;
		}
		
		// check to see whether a video height has been supplied
		var playerHeight = options.playerHeight;
		if ( ! playerHeight ){
			// no. automatically set it based on a 16:9 resolution (using the width)
			var playerHeight = playerWidth * 0.5625;
		}
				
		// check to see whether a render handler has been supplied
		if ( options.controlBarRenderHandler ){
			// a custom handler has been supplied. call the handler, passing the default elements as 
			// an argument, and add the response to the content
			contentElement.adopt(options.controlBarRenderHandler(controlBar, buildElements));
		} else {
			// no custom handler has been supplied, just add the constructed components to the content
			contentElement.adopt(controlBar);	
		}
		
		// prepare the progress reporting settings
		var previousReportedPoint = 0;
		var reportPoints = [
        	25,
        	50,
        	75
        ];
		var reportPointLength = reportPoints.length;
		
		// determine whether the video will be stretched or allowed to maintain its original size by
		// comparing the video dimensions to that of the player (if the video is larger stretch down 
		// otherwise maintain)
		var stretching = 'none';
		if ( options.videoWidth && options.videoWidth > playerWidth ) stretching = 'uniform';
		if ( options.videoHeight && options.videoHeight > playerHeight ) stretching = 'uniform';
		
		// initialise the JW Player
	    this.jwPlayer = jwplayer(containerId).setup({
	        file: this.videoUrl,
	        width: playerWidth,
	        height: playerHeight,
	        controlbar: 'none',
	        stretching: stretching,
	        players: [
	          	{type: 'flash', src:this.jwPlayerResources.baseUrl+this.jwPlayerResources.flashPlayer},
	      		{type: 'html5'},
	      		{type: 'download'}
      		],
      		events: {
      			onPlay: function(){
  					// swap the play/pause class on the play button
      				this.elements.playButton.removeClass(this.cssClasses.playButton);
      				this.elements.playButton.addClass(this.cssClasses.pauseButton);
      				 
      				this.fireEvent('play');
      				
      				// push a custom event to Google Analytics, if enabled
      				if ( isGoogleAnalyticsEnabled ){
      					_gaq.push(['_trackEvent', this.googleAnalyticsCategoryKey, this.googleAnalyticsActions.played, this.videoUrl]);	
      				}
      			}.bind(this),
      			onPause: function(){
  					// swap the play/pause class on the play button
      				this.elements.playButton.addClass(this.cssClasses.playButton);
      				this.elements.playButton.removeClass(this.cssClasses.pauseButton);
      				
      				this.fireEvent('pause');

      				// push a custom event to Google Analytics, if enabled
      				if ( isGoogleAnalyticsEnabled ){
      					_gaq.push(['_trackEvent', this.googleAnalyticsCategoryKey, this.googleAnalyticsActions.paused, this.videoUrl]);	
      				}
      			}.bind(this),
      			onTime: function(details){
      				// store the duration of the video (so we can perform seeking) and set the total time
      				if ( ! this.videoDuration ){
      					this.videoDuration = details.duration;
      					this.elements.totalTime.set('html', kamUi.formatSecondsToHumanReadable(details.duration) );
      				}
      				
      				// update the current time bar
      				this.elements.currentTimeBar.setStyle('width', ( this.elements.timeBar.getSize().x * (details.position / details.duration))+'px');
      				
      				// update the current time div
      				this.elements.currentTime.set('html', kamUi.formatSecondsToHumanReadable(details.position) );
      				
      				// determine what percentage of the video has been watched
      				if ( isGoogleAnalyticsEnabled ){
	      				var watchedPercentage = ((details.position / details.duration) * 100).toInt();
	  					for ( var x = 0; x < reportPointLength; x++ ){
	  						var pointValue = reportPoints[x];
	  						if ( previousReportedPoint < pointValue && watchedPercentage >= pointValue ){
	  							previousReportedPoint = pointValue;
	  							_gaq.push(['_trackEvent', this.googleAnalyticsCategoryKey, this.googleAnalyticsActions.progressPrefix+pointValue+'% viewed', this.videoUrl]);
								break;
	  						}
	  					}
      				}

      				this.fireEvent('time', details);
      			}.bind(this),
      			onComplete: function(details){
      				// send the 100% progress event, if GA is enabled
      				if ( isGoogleAnalyticsEnabled ){
      					_gaq.push(['_trackEvent', this.googleAnalyticsCategoryKey, this.googleAnalyticsActions.progressPrefix+'100% viewed', this.videoUrl]);
      				}
      				
      				this.fireEvent('complete');
      			}.bind(this),
      			onBufferChange: function(details){
      				// update the current time bar
      				this.elements.bufferTimeBar.setStyle('width', ( (this.elements.timeBar.getSize().x / 100) * details.bufferPercent)+'px');
      				
      				this.fireEvent('bufferChange', details);
      			}.bind(this)
      		}
	    });
	},
	
	/**
	 * __destruct
	 * 
	 * Destroys the player
	 */
	
	__destruct: function(){
		// remove the jwplayer from memory
		this.jwPlayer.remove();
		
		this.parent();
	},
	
	/**
	 * play
	 * 
	 * Plays the video player
	 */
	
	play: function(){
		this.jwPlayer.play();
	},

	/**
	 * pause
	 * 
	 * Pauses the video player
	 */
	
	pause: function(){
		this.jwPlayer.pause();
	}
});

/**
 * Kameleon : AJAX content modal
 * 
 * Retrieves remote content and displays it in a modal window
 * 
 * EXTENDS: kamModal
 *
 * Options 
 * 		contentRequestOptions (object): The options to use when requesting remote data. NOTE: the ajaxContentModal expects the request to only return a single DOMElement
 * 
 * CHANGE LOG:
 * ===================
 * 
 * 10/03/2011 (Version 1.0.0)
 * --------------------------
 * 
 * @author James Sanders
 * @version 1.0.0
 * @copyright Kameleon Digital ( http://www.kameleondigital.com )
 */


var kamAjaxContentModal = new Class({
	Extends: kamModal,
	
	className: 'kamAjaxContentModal',
	
	/**
	 * contentUrl
	 * 
	 * The URL that the content of the modal was loaded from
	 * 
	 * @var string
	 */
	
	contentUrl: null,
		
	/**
	 * initialize
	 * 
	 * Class constructor
	 * 
	 * @param string contentUrl the URL of the page to load into the modal
	 * @param object options (OPTIONAL) the options to apply to this class
	 */
	
	initialize: function(contentUrl, options){
		// ensure the a file has been specified
		this.setOptions(options);
		if ( contentUrl && typeof(contentUrl) == 'string' ){
			// add the additional default options
			this.setOptions({
				contentRequestOptions: {},
				height: 400
			});
			
			// store the video URL
			this.contentUrl = contentUrl;
			
			// fire the parent constructor
			this.parent(options);
		} else {
			alert("ERROR > kamAjaxContentModal > no 'contentUrl' has been supplied")
		}
	},
	
	show: function(){
		this.__showMask();
		this.reload();
	},
	
	reload: function(){
		this.load(this.contentUrl);
	},
	
	load: function(url){
		// store the video URL
		this.contentUrl = url;
		
		var request = new kamContentRequest(url, this.options.contentRequestOptions);
		request.addEvent('onSuccess', function(content){
			this.setContent(content);
			
			// initalise the new content
			kamUi.initialise(this.elements.content);
			
			// show the modal
			this.__showModal();
		}.bind(this)).execute();
	}
});

/**
 * prepare the kamFX collection
 */

var kamFx = {};

kamFx.rotateElement = function(element, degrees, duration){
	var cssPrefix = false;
	switch(Browser.name) {
	  case 'safari':
	    cssPrefix = 'webkit';
	    break;
	  case 'chrome':
	    cssPrefix = 'webkit';
	    break;
	  case 'firefox':
	    cssPrefix = 'moz';
	    break;
	  case 'opera':
	    cssPrefix = 'o';
	    break;
	  case 'ie':
	    cssPrefix = 'ms';
	    break;
	}

	// if a duration has been supplied, set it now. otherwise ensure none is set
	if ( duration ){
		element.setStyle('-'+cssPrefix+'-transition-duration', duration+'s');
		element.setStyle('transition-duration', duration+'s');
	} else {
		element.setStyle('-'+cssPrefix+'-transition-duration', '');
		element.setStyle('transition-duration', '');
	}
	
	// set the rotation
	element.setStyle('-'+cssPrefix+'-transform', 'rotate('+degrees+'deg)');	
};

kamFx.updateContent = function(element, newContent){
	element = document.id(element);
	var updateContent = function(){
		// update the content 
		if ( typeof newContent == 'string' ){
			element.set('html', newContent);
		} else {
			element.empty();
			element.adopt( newContent );
		}
	};
	
	// determine whether we should use animations
	if ( kamUi.useAnimations ){
		// set the opacity of the container to transparent to avoid any flickering from the 
		// content being swapped
		element.setStyle('opacity', 0.01);
		
		// get the existing height of the container
		var height = element.getStyle('height');
		
		// update the content 
		updateContent();
		
		// disable the height style to allow for a measure
		element.setStyle('height', '');
		var newHeight = element.measure( function(){ return this.getSize().y; });
		element.setStyle('height', height);
		
		// morph the container to the new size
		new Fx.Morph(element, {
			onComplete: function(){
				// strip the style restrictions from the container
				element.setStyles({
					overflow: '',
					height: ''
				});
			}
		}).start({
			height: [height, newHeight],
			opacity: [0.01, 1]
		});
	} else {
		// nope. just update the content
		updateContent();
		
		// strip off the overflow and height which will snap the page to the correct height
		element.setStyles({
			overflow: '',
			height: ''
		});
	}
	
	// initialise the element
	kamUi.initialise(element);
}
