UI = {};

/** Helpful Functions **/
UI.doCallback = function( fn, data ) {

	try {
		if ( $type( fn ) == 'string' )
			fn = eval( fn )( data );
		
		if ( $type( fn ) == 'function' )
			return fn( data );
			
		if ( $type( fn ) == 'element' )
			return fn;
	}
	catch(e) {
		$log( 'Error doing callback function: ' + fn, e );
	}

};

/*
Function: UI.loadSparkle

	Quick and easy way to load additional parsed Sparkle from the Resources folder
	
	Path argument maps to /Kaleidoscope/Templatesets/[template]/Includes/Remotes/
	
	You can alternatively make a simple call by passing the path as a string.
	
	UI.loadSparkle( 'Popups/Signup-Business' )
		.then( function( rtn ) { $log( rtn ); } );
	
	UI.loadSparkle( { path: 'Popups/Signup-Business', data: { itemId: 1 } )
		.then( function( rtn ) { $log( rtn ); } );
	
*/
UI.loadSparkle = function( options ) {
	
	if ( $type( options ) == 'string' )
		options = { path: options };
	
	options = $options( options, {
			path: '',		// path to the sparkle file (required)
			data: {}		// data to pass to the sparkle parser (becomes params.*)
		});
	
	$extend( options.data, {
			action: 'sparkle.parse',
			path: options.path
		});
	
	var onCompleteQueue = [],
		onErrorQueue = [];
	
	var proxy = {
		then: function( fn ) { onCompleteQueue.push( fn ) }
	};
	
	new Request.HTML({
			url: '/Remote.cfm',
			onSuccess: function( tree, elements, html, js ) {
			
				// Check for an error first
				var error = ( html && html.clean().slice( 0, 6 ) == 'error:' );
				
				// Possible error
				if ( error )
				{
					// Retrieve id, convert to integer
					var errorId = html.clean().slice( 6 ).toInt();
					
					// Make sure it's really an id
					if ( typeOf( errorId ) == 'number' )
					{
						if ( confirm( "It looks like there was an error while trying to perform this action.\n\nWould you like to open the error in a new window?" ) )
							window.open( '/errors/inRequest.cfm?btrId=' + errorId );
						
						return;
					}
				}
				
				while ( onCompleteQueue.length )
					onCompleteQueue.shift()( html );
			},
			onFailure: function( err ) {
				
				if ( err.status == '500' && options.onError )
				{
					UI.doCallback( options.onError( err ) );
					return;
				}
				
				if ( err.status == '404' )
				{
					alert( 'The Sparkle file specified ("' + options.path + '") could not be found. Please check the path:\n\n/Sites/[siteKey]/Sparkle/Remotes/' + options.path + '.sparkle' );
					return;
				}
				
				$log( err );
				// alert( "There was an error loading the Sparkle file " + options.path + '.\n\nError has been logged to console.' );
				
			}
		})
		.post( options.data );
	
	return proxy;
	
}


/*
Function: UI.handleIframeCallback
	
	Handles the iframe callback for forms.

*/
UI.handleIframeCallback = function( callbackId, actionRtn ) {

	$log( 'Received callback with id: ' + callbackId + ', action returned:', actionRtn );
	
	// Gather details
	var $iframe = $( callbackId ),
		$form = document.body.getElement( '[target=' + callbackId + ']' ),
		form = $form.retrieve( 'form' );
	
	if ( !form )
	{
		alert( 'No form element detected, cannot process return from iframe.' );
		return;
	}
	
	// Give the form the return of the iframe
	form.processResponse( actionRtn );
	
	// Destroy the iframe
	$iframe.destroy();

};


/*
Function: UI.initClasses

	Used to initialise elements that are created after the page is loaded

*/
UI.initClasses = function( $element ) {

	if ( !$element )
		return;
	
	// Placeholders
	$element.getElements( 'input[hint],textarea[hint]' ).each( function( $el ) {
		$el.store( 'placeholder', new UI.Placeholder( $el, $el.getDataFromComment() ) );
	});
	
	// Slides
	$element.getElements( '.ui-slides' ).each( function( $el ) {
		$el.store( 'slides', new UI.Slides( $el, $el.getDataFromComment() ) );
	});
	
	// Buttons
	$element.getElements( '.ui-button' ).each( function( $el ) {
		$el.store( 'button', new UI.Button( $el, $el.getDataFromComment() ) );
	});
	
	// Maps
	if ( UI.Google )
	{
		var maps = $element.getElements( '.ui-map' );
		
		if ( maps )
		{
			UI.Google.load( 'maps', (function() {
				maps.each( function( $el ) {
					$el.store( 'map', new UI.Map( $el, $el.getDataFromComment() ) );
				});
			}.bind( this )));
		}
	}
	
	// Forms
	$element.getElements( '.ui-form' ).each( function( $el ) {
		$el.store( 'form', new UI.Form( $el, $el.getDataFromComment() ) );
	});
	
	// Images
	/*
	$element.getElements( '.ui-images' ).each( function( $el ) {
	
		var options = $el.getDataFromComment() || {};
		
		if ( !options.mode )
			$extend( options, { mode: 'gallery' } );
		
		$el.store( 'image', new UI.Images( $el, options ) );
	
	});
	
	$element.getElements( '.ui-image-popup' ).each( function( $el ) {
		$el.store( 'image', new UI.Images( $el, $el.getDataFromComment() ) );
	});
	*/
	
	// Suggest
	$element.getElements( '.ui-suggest' ).each( function( $el ) {
		$el.store( 'suggest', new UI.Suggest( $el, $el.getDataFromComment() ) );
	});
	
	// FilterField
	$element.getElements( '.ui-filterfield' ).each( function( $el ) {
		$el.store( 'filterfield', new UI.FilterField( $el, $el.getDataFromComment() ) );
	});
	
	// Expander
	$element.getElements( '.ui-expander' ).each( function( $el ) {
		$el.store( 'expander', new UI.Expander( $el, $el.getDataFromComment() ) );
	});
}


/*
Class: UI.Slides
*/
UI.Slides = new Class({
	
	Implements: Options,
	
	options: {
		width: 0,
		height: 0,
		first: 1,
		type: 'immediate', // horizontal, vertical, fade, immediate
		trigger: 'click',
		duration: 500,
		effect: 'sine',
		ease: 'out',
		fadeContent: false,
		containerClass: 'ui-slides-container',
		contentClass: 'ui-slide-content',
		slidesQuery: '.ui-slide',
		navQuery: '.ui-slidesnav',
		navItemQuery: false,
		autoSlide: false,
		animateNav: false
	},
	
	initialize: function( target, options )
	{
		this.setOptions( options );
		
		var $el = this.$el = $( target );
		
		
		// Transition
		this.transition = this.options.effect;
		
		if ( this.options.effect != 'linear' )
			this.transition += ':' + this.options.ease;
		
		
		// Setup nav
		this.$nav = $el.getElement( this.options.navQuery );
		
		if ( this.$nav )
		{
			var goto = this.goto.bind( this );
			
			this.navLinks = ( this.options.navItemQuery ? this.$nav.getElements( this.options.navItemQuery ) : this.$nav.getChildren() );
			
			this.navLinks.each( function( $navItem, i ) {
				
				switch( this.options.trigger )
				{
					case 'hover':
						this.options.trigger = 'mouseenter';
					break;
				}
				
				$navItem.addEvent( this.options.trigger, function() { goto( i ); } );
				
			}.bind( this ));
		}
		
		
		// Get the slides
		var slidesTarget = $el.getElement( this.options.slidesQuery );
		
		var $container = this.$container = $el.getElement( '.' + this.options.containerClass );
		
		if ( !slidesTarget )
		{
			$log( 'Slides incorrectly configured, aborting:', $el );
			return;
		}
		
		if ( !$container )
			$container = this.$container = new Element( 'div', { 'class': this.options.containerClass, styles: { position: 'relative', overflow: 'hidden' } } ).inject( slidesTarget, 'after' );
		
		if ( this.options.width )
			$container.setStyle( 'width', this.options.width );
		
		if ( this.options.height )
			$container.setStyle( 'height', this.options.height );
		
		this.slides = $el.getElements( this.options.slidesQuery );
		
		if ( !this.slides.length )
		{
			alert( 'No slides detected. Each slide must have the class [' + this.options.slidesQuery + ']' );
			return;
		}
		
		
		// Hide all slides and store indexes
		this.slides.each( (function( slide, index ) {
		
			slide.inject( $container );
			
			slide.setStyles({
				display: 'none',
				width: this.options.width || 'auto',
				height: this.options.height || 'auto'
			});
			
			slide.store( 'index', index );
		
		}.bind( this )));
		
		
		// Adjust first requested slide to deal with array
		this.options.first -= 1;
		
		
		// Go to slide
		this.goto( this.options.first, true );
		
		
		// Init auto slide
		if ( this.options.autoSlide )
			this.autoSlide();
	},
	
	autoSlide: function()
	{
		var $el = this.$el,
			slides = this.slides;
		
		var visibleSlide = 0,
			currentlyHovered = false;
		
		// Start auto sliding
		var autoSlide = (function() {
			
			if ( currentlyHovered )
				return;
			
			this.goto( visibleSlide );
			
			visibleSlide++;
			
			if ( visibleSlide >= slides.length )
				visibleSlide = 0;
			
		}.bind( this ));
		
		// Perform the first auto slide
		autoSlide();
		
		(function() {
			
			autoSlide();
			
		}).periodical( this.options.autoSlide );
		
		// Add event to stop auto sliding
		$el.addEvent( 'mouseenter', function() {
		
			currentlyHovered = true;
			
			$clear( autoSlide );
			
			$el.removeEvents( 'mouseenter' );
		
		});
	},
	
	prepare: function( index )
	{
		// Retrieve next slide
		var $nextSlide = this.$nextSlide = this.slides[ index || 0 ];
		
		if ( !$nextSlide )
		{
			alert( 'UI.Slides Error: Slide ' + index + ' could not be found in the Slides array.' );
			return;
		}
		
		if ( this.currentSlide.index == index )
			return this;
		
		// Deal with current slide
		var currentSlide = this.currentSlide;
		
		if ( this.options.fadeContent )
		{
			// Next slide
			var $nsc = $nextSlide.getElement( '.' + this.options.contentClass );
			
			$nsc.setStyle( 'opacity', 0 );
			
			this.show( index );
			
			// TW: This comment removes the fade out effect as requested by Boris
			// Current slide
			/* var $csc = currentSlide.$el.getElement( '.' + this.options.contentClass );
			
			$csc.get( 'tween' ).setOptions({ duration: 250, transition: 'linear' });
			
			$csc.get( 'tween' )
				.start( 'opacity', 0 )
				.chain( function() {
				
					//
				
				}.bind( this ));
			*/
		}
		else
		{
			this.show( index );
		}
	},
	
	show: function( index )
	{
		var currentSlide = this.currentSlide,
			$nextSlide = this.$nextSlide;
		
		// Retrieve data
		var data = $nextSlide.getDataFromComment(),
			rendered = $nextSlide.retrieve( 'rendered' );
		
		// Load from sparkle (if required)
		if ( !rendered && data && data.fromSparkle )
		{
			UI.loadSparkle( data.fromSparkle )
				.then( function( rtn ) {
					
					var html = rtn;
					
					$nextSlide.set( 'html', html );
					
					UI.initClasses( $nextSlide );
					
					$nextSlide.store( 'rendered', true );
					
					// Call render function (if it exists)
					if ( data.onRender )
						UI.doCallback( data.onRender );
					
				});
		}
		else if ( !rendered )
		{
			$nextSlide.store( 'rendered', true );
		}
		
		$nextSlide.store( 'rendered', true );
		
		
		// Calculate current width and height
		var width = this.$el.getSize().x;
		var height = this.$el.getSize().y;
		
		
		// Handle menu
		if ( this.navLinks )
		{
			this.navigation = {
				$previous: this.navLinks[ this.currentSlide.index ],
				$current: this.navLinks[ index ]
			}
			
			if ( this.options.animateNav )
			{
				UI.doCallback( this.options.animateNav, this );
			}
			else
			{
				if ( this.navLinks[ this.currentSlide.index ] )
					this.navLinks[ this.currentSlide.index ].removeClass( 'selected' );
				
				this.navLinks[ index ].addClass( 'selected' );
			}
		}
		
		
		// Store the new slide
		var storeSlide = (function() {
			
			this.currentSlide = {
				$el: $nextSlide,
				index: index
			}
		
		}.bind( this ));
		
		
		// Ensures all other slides are hidden (useful in transitions and the user is making very fast navigation changes and the animations can't keep up)
		var hideSlides = (function( s ) {
		
			this.slides.each( function( slide ) {
			
				if ( s && slide == s.$el )
					return;
				
				slide.hide();
			
			});
		
		}.bind( this ));
		
		
		// Note: It's possible this isn't even used anymore
		if ( !this.currentSlide )
		{
			$log( 'No current slide detected, showing immediately.' );
			
			var styles = { display: 'block' };
			
			switch( this.options.type )
			{
				case 'horizontal': styles.left = 0; break;
				case 'vertical': styles.top = 0; break;
				case 'fade': styles.opacity = 1; break;
			}
			
			$nextSlide.setStyles( styles );
			
			storeSlide();
			
			return;
		}
		
		var currentSlide = this.currentSlide;
		
		
		// Transition current and new slide
		switch( this.options.type )
		{
			case 'horizontal':
			case 'vertical':
			
				var direction = ( currentSlide.index < $nextSlide.retrieve( 'index' ) ) ? -1 : 1;
				
				var nextSlideSize = $nextSlide.getDimensions();
				
				if ( currentSlide.$nav )
					currentSlide.$nav.removeClass( 'current' ).addClass( 'normal' );
				
				currentSlide.$el.setStyles({
					position: 'absolute'
				});
				
				var $currentSlide = this.currentSlide.$el;
				
				var $content = $currentSlide.getElement( '.' + this.options.contentClass );
				
				// If we have a set width and height, give it to the slide
				if ( this.options.width )
					$currentSlide.setStyle( 'width', this.options.width );
				
				if ( this.options.height )
					$currentSlide.setStyle( 'height', this.options.height );
				
				// If we don't, try to calculate it
				if ( !this.options.width && !this.options.height )
				{
					this.$el.setStyle( 'height', nextSlideSize.y );
					$currentSlide.setStyles({ width: nextSlideSize.x, height: nextSlideSize.y })
				}
				
				// Move first slide
				$currentSlide.get( 'tween' ).setOptions({ duration: this.options.duration, transition: this.transition });
				
				switch( this.options.type )
				{
					case 'horizontal':
					
						$currentSlide.get( 'tween' )
							.start( 'left', nextSlideSize.x * direction )
							.chain( function() {
							
								$currentSlide.setStyles({
									display: 'none',
									position: 'relative'
								});
								
								this.finished( $nextSlide );
							
							}.bind( this ));
					
					break;
					
					case 'vertical':
					
						$currentSlide.get( 'tween' )
							.start( 'top', nextSlideSize.y * direction )
							.chain( function() {
							
								$currentSlide.setStyles({
									display: 'none',
									position: 'relative'
								});
								
								this.finished( $nextSlide );
							
							}.bind( this ));
					
					break;
				}
				
				// Move second slide
				$nextSlide.setStyles({ display: 'block', position: 'absolute' });
				
				$nextSlide
					.get( 'tween' )
					.setOptions({
						duration: this.options.duration,
						transition: this.transition
					});
				
				switch( this.options.type )
				{
					case 'horizontal':
						
						$nextSlide.setStyles({
							'left': nextSlideSize.x * direction * - 1
						})
						.get( 'tween' )
						.start( 'left', 0 )
						.chain( (function() {
						
							$nextSlide.setStyle( 'position', 'relative' );
							
							this.finished( $nextSlide );
						
						}.bind( this )));
						
					break;
					
					case 'vertical':
					
						$nextSlide.setStyles({
							'top': nextSlideSize.y * direction * - 1
						})
						.get( 'tween' )
						.start( 'top', 0 )
						.chain( (function() {
						
							$nextSlide.setStyle( 'position', 'relative' );
							
							this.finished( $nextSlide );
						
						}.bind( this )));
					
					break;
				}
				
				storeSlide();
			
			break;
			
			case 'fade':
			
				hideSlides( currentSlide );
				
				if ( currentSlide.$nav )
					currentSlide.$nav.removeClass( 'current' ).addClass( 'normal' );
				
				$nextSlide.show().setStyle( 'height', 'auto' );
				
				var nextSlideSize = $nextSlide.getComputedSize();
				
				$nextSlide.hide();
				
				this.currentSlide.$el.setStyles({ opacity: 1 })
				
				this.currentSlide.$el.get( 'tween' )
					.setOptions({
						duration: this.options.duration,
						transition: this.transition
					})
					.start( 'opacity', 0 )
					.chain( (function() {
					
						hideSlides();
						
						$nextSlide.show();
						
						// TW 2011-04-21: IE doesn't show the next slide properly, it does weird stuff with the opacity, this shows it immediately
						if ( window.document.body.hasClass( 'is_ie' ) )
						{
							$nextSlide.setStyles({
								position: 'relative',
								width: 'auto',
								height: 'auto',
								opacity: 1
							});
							
							this.finished( $nextSlide );
							
							return;
						}
						
						$nextSlide.setStyles({
							width: nextSlideSize.x,
							height: nextSlideSize.y,
							opacity: 0
						})
						.get( 'tween' )
							.setOptions({
								duration: this.options.duration,
								transition: this.transition
							})
							.start( 'opacity', 0, 1 )
							.chain( (function() {
							
								$nextSlide.setStyles({
									position: 'relative',
									width: 'auto',
									height: 'auto'
								});
								
								this.finished( $nextSlide );
							
							}.bind( this )));
					
					}.bind( this )));
				
				storeSlide();
				
			
			break;
			
			case 'immediate':
			
				currentSlide.$el.hide();
					
				$nextSlide.show();
				
				this.finished( $nextSlide );
				
				storeSlide();
			
			break;
		}
		
		return this;
	},
	
	finished: function( $el )
	{
		// Fade content
		if ( this.options.fadeContent )
		{
			var $csc = $el.getElement( '.' + this.options.contentClass );
			
			$csc.get( 'tween' ).setOptions({ duration: 500, transition: 'linear' });
			
			$csc.get( 'tween' )
				.start( 'opacity', 1 )
				.chain( function() {
				
					//
				
				}.bind( this ));
		}
		
		// Maps
		this.checkForMaps( $el );
	},
	
	next: function()
	{
		if ( !this.currentSlide || this.currentSlide.index >= this.slides.length - 1 )
			return this.prepare( 0 );
		else
			return this.prepare( this.currentSlide.index + 1 );
	},
	
	previous: function()
	{
		if ( !this.currentSlide )
			return this.prepare( 0 );
		else if ( this.currentSlide.index == 0 )
			return this.prepare( this.slides.length - 1 )
		else
			return this.prepare( this.currentSlide.index - 1 );
	},
	
	gotoFirst: function()
	{
		this.goto( 0 );	
	},
	
	gotoLast: function()
	{
		var lastSlide = this.slides.length - 1;
		
		this.goto( lastSlide );
	},
	
	goto: function( i, immediate )
	{
		if ( !this.slides[ i ] || this.currentSlide == i )
			return;
		
		if ( !immediate )
		{
			this.prepare( i );
			return;
		}
		
		if ( this.currentSlide && this.currentSlide.index > -1 )
		{
			this.slides[ this.currentSlide.index ].setStyle( 'display', 'none' );
			
			if ( this.navLinks && this.navLinks[ this.currentSlide.index ] )
				this.navLinks[ this.currentSlide.index ].removeClass( 'selected' );
		}
		
		var $el = this.slides[ i ];
		
		this.currentSlide = {
			$el: this.slides[ i ],
			index: i
		};
		
		$el.setStyle( 'display', 'block' );
		
		if ( this.navLinks && this.navLinks[ i ] )
			this.navLinks[ i ].addClass( 'selected' );
	},
	
	checkForMaps: function( $el )
	{
		var maps = $el.getElements( '.ui-map' );
		
		if ( !maps.length )
			return;
		
		// For each map, create it if it has auto create disabled and we haven't created it yet
		maps.each( function( $map ) {
		
			var map = $map.retrieve( 'map' );
			
			if ( !map.options.autoCreate && !map._created )
				map.render();
		
		});
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-slides' ).each( function( $el ) {
		$el.store( 'slides', new UI.Slides( $el, $el.getDataFromComment() ) );
	});

});

/*
Class: UI.FilterField
*/
UI.FilterField = new Class({
	
	Implements: Options,
	
	options: {
		itemsEl: document,
		hideItems: true,
		
		itemsQuery: '.ui-search-item',
		fieldQuery: '.ui-search-field',
		
		activeClass: 'ui-search-item-active',
		inactiveClass: 'ui-search-item-inactive',
		
		clearQuery: '.ui-search-clear',
		
		minLength: 3,
		inputDelay: 500,
		
		onInit: null,
		onSearch: null,
		
		highlightTerms: false,
		highlightQuery: false
	},
	
	initialize: function( target, options )
	{
		var $el = this.$el = $( target );
		
		this.setOptions( options );
		
		this.originalItemsQuery = this.options.itemsQuery;
		
		this._lastSearch = '';
		
		// Set items element
		var itemsEl = this.itemsEl = this.options.itemsEl;
		
		if ( itemsEl != document )
			itemsEl = this.itemsEl = document.getElement( this.options.itemsEl );
		
		// Find input field
		var $input = this.$input = $el;
		
		if ( $el.get( 'tag' ) != 'input' )
			$input = this.$input = $el.getElement( 'input' );
		
		this.initInput();
		
		// Find clear element
		var $clearSearch = this.$clearSearch = $el.getElement( this.options.clearQuery );
		
		this.initClear();
		
		// Gather initial search data
		this.initSearchData();
		
		// Do initial search (hmm, let's leave out for now shall we?)
		// this.doSearch();
		
		if ( !this.options.highlightQuery )
			this.options.highlightQuery = this.options.activeClass;
		
		// Perform onInit function
		if ( this.options.onInit )
			UI.doCallback( this.options.onInit );
	},
	
	initInput: function()
	{
		var $input = this.$input;
		
		$input.addEvents({
		
			'keyup': function() {
			
				var value = $input.value;
				
				if ( !value )
				{
					if ( this.$clearSearch )
						this.$clearSearch.hide();
				}
				
				if ( value && value.length < this.options.minLength )
					return;
				
				clearTimeout( this._inputTimer );
				
				this._inputTimer = (function() {
					this.doSearch( $input.value );
				}.bind( this )).delay( this.options.inputDelay );
				
				if ( value )
					this.$clearSearch.show();
			
			}.bind( this )
		
		});
	},
	
	initClear: function()
	{
		var $clearSearch = this.$clearSearch;
		
		if ( !$clearSearch )
			return;
		
		$clearSearch.hide();
		
		$clearSearch.addEvents({
		
			'click': function() {
			
				this.$input.value = '';
				
				clearTimeout( this._inputTimer );
				
				this.doSearch();
				
				$clearSearch.hide();
				
				$( 'quicksearch-input' ).fireEvent( 'blur' ).focus();
				
				this.clearHighlights();
			
			}.bind( this )
		
		});
	},
	
	setQuery: function( newQuery )
	{
		this.itemsEl.getElements( this.originalItemsQuery ).hide();
		
		if ( !newQuery )
			newQuery = this.originalItemsQuery;
		
		this.options.itemsQuery = newQuery;
		
		this.doSearch( this._lastSearch );
	},
	
	initSearchData: function()
	{
		var searchData = [];
		
		this.itemsEl.getElements( this.options.itemsQuery ).each( (function( $item ) {
			
			var terms = '';
			
			$item.getElements( this.options.fieldQuery ).each( function( $term ) {
				terms += $term.innerHTML + ' |||| ';
			});
			
			searchData.push({ terms: terms, $el: $item });
			
		}.bind( this )));
		
		this._searchData = searchData;
	},
	
	doSearch: function( searchString )
	{
		var searchData = this._searchData;
		
		// Store the last search
		this._lastSearch = searchString;
		
		// Clear highlights (if any)
		this.clearHighlights();
		
		// If the search string is blank and the query is the same as we started, show everything and return
		if ( ( !searchString || !$chk( searchString.trim() ) ) && ( this.originalItemsQuery == this.options.itemsQuery ) )
		{
			searchData.each( function( i ) {
			
				if ( this.options.hideItems )
					i.$el.show();
				
				i.$el.removeClass( this.options.activeClass ).removeClass( this.options.inactiveClass );
			
			}.bind( this ));
			
			// Search callback
			if ( this.options.onSearch )
				UI.doCallback( this.options.onSearch );
			
			return;
		}
		
		// Clean up search string
		if ( searchString )
			searchString = searchString.trim().escapeRegExp();
		
		// Determine class for detection
		var queryClass = this.options.itemsQuery.replace( '.', '', 'g' );
		
		// Loop over the data and show and hide elements
		searchData.each( (function( i ) {
			
			if ( this.options.hideItems )
			{
				if ( i.terms.test( searchString, "i" ) )
					i.$el.show()
				else
					i.$el.hide();
				
			}
			
			if ( i.terms.test( searchString, "i" ) )
				i.$el.addClass( this.options.activeClass ).removeClass( this.options.inactiveClass );
			else
				i.$el.addClass( this.options.inactiveClass ).removeClass( this.options.activeClass );
			
		}.bind( this )));
		
		// Add highlights
		if ( this.options.highlightTerms )
		{
			var regex = new RegExp( '(' + searchString + ')', 'gi' );
			
			this.itemsEl.getElements( this.options.highlightQuery ).each( (function( $el ) {
			
				var text = $el.get( 'text' ),
					parsed = text.replace( regex, '<span class="ui-search-highlight">$1</span>' );
				
				$el.set( 'html', parsed );
			
			}.bind( this )));
		}
		
		// Search callback
		if ( this.options.onSearch )
			UI.doCallback( this.options.onSearch );
	},
	
	clearHighlights: function()
	{
		if ( !this.options.highlightTerms )
			return;
		
		var highlights = this.itemsEl.getElements( '.ui-search-highlight' );
			highlights.removeClass( 'ui-search-highlight' );
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-filterfield' ).each( function( $el ) {
		$el.store( 'filterfield', new UI.FilterField( $el, $el.getDataFromComment() ) );
	});

});

/*
Class: UI.Placeholder
*/
UI.Placeholder = new Class({
	
	Implements: [ Events, Options ],
	
	options: {
		
		placeHolder: '',
		placeHolderCss: ''
		
	},

	initialize: function( target, options )
	{
		this.setOptions( options );
		
		// Determine placeholder text
		this.placeholder = target.get( 'hint' );
		
		if ( this.options.placeholder )
			this.placeholder = this.options.placeholder;
		
		// Setup field
		var textfieldStyles = {
			display: 'inline-block',
			position: 'relative',
			'-moz-box-sizing': 'border-box',
			'-webkit-box-sizing': 'border-box',
			'box-sizing': 'border-box'
		}
		
		this.$el = new Element( 'span', { 'class': 'ui-textfield', styles: textfieldStyles } ).inject( target, 'after' );
		
		var fieldStyles = {
			'-moz-box-sizing': 'border-box',
			'-webkit-box-sizing': 'border-box',
			'box-sizing': 'border-box'
		};
		
		this.$field = target.setStyles( fieldStyles ).inject( this.$el );
		
		// Init elements
		this.initPlaceholder();
		
		// Add events
		this.$field.addEvent( 'focus', (function( event ) {
				
				//
				
			}).bind( this ))
			.addEvent( 'blur', (function( event ) {
				
				this.updatePlaceholder();
				
			}).bind( this ))
			.addEvent( 'keypress', (function( event ) {
				
				this.updatePlaceholder();
				
			}).bind( this ))
			.addEvent( 'keyup', (function( event ) {
				
				this.updatePlaceholder();
				
			}).bind( this ))
			.addEvent( 'change', (function( event ) {
				
				//
				
			}).bind( this ));
	},
	
	initPlaceholder: function()
	{
		if ( this.$placeholder )
			this.$placeholder = this.$placeholder.destroy();
		
		if ( $chk( this.placeholder ) )
		{
			var placeholderStyles = {
				cursor: 'text',
				top: '0px',
				left: '0px',
				position: 'absolute',
				width: '100%',
				overflow: 'hidden',
				color: '#999',
				'text-align': 'left',
				'-moz-box-sizing': 'border-box',
				'-webkit-box-sizing': 'border-box',
				'box-sizing': 'border-box'
			};
			
			this.$placeholder = new Element( 'span', { 'class': 'ui-textfield-placeholder', styles: placeholderStyles, html: this.placeholder } ).inject( this.$el );
			
			if ( $chk( this.placeholderCss ) )
				this.$placeholder.setStyles( this.placeholderCss );
			
			var xOffset = parseInt( this.$field.getStyle( 'border-left-width' ) ) + parseInt( this.$field.getStyle( 'padding-left' ) ) + 1,
				yOffset = parseInt( this.$field.getStyle( 'border-top-width' ) ) + parseInt( this.$field.getStyle( 'padding-top' ) );
			
			this.$placeholder.setStyles({
				fontSize: this.$field.getStyle( 'font-size' ),
				paddingLeft: xOffset,
				paddingTop: yOffset
			});
			
			this.$placeholder.addEvent( 'click', this.focus.bind( this ) );
			
			this.updatePlaceholder();
		}
	},
	
	updatePlaceholder: function()
	{
		if ( !this.$placeholder )
			return;
		
		this.$placeholder.setStyle( 'opacity', ( this.$field.value.length ? 0 : 1 ) );
		
	},
	
	focus: function()
	{
		try { this.$field.focus(); }
		catch(e) {}
		return this;
	},
	
	set: function( what, value )
	{
		switch ( what )
		{
			case 'placeholder':
				this.placeholder = value;
				this.initPlaceholder();
			break;
			
			default:
				this.store[ what ] = value;
		}
		
		return this;
	},
	
	get: function( what )
	{
		switch ( what )
		{
			default:
				return this.store[ what ];
		}
	}
	
});

document.addEvent( 'domready', function() {

	$$( 'input[hint],textarea[hint]' ).each( function( $el ) {  // Combine two tags to detect one instance of a property?
		$el.store( 'placeholder', new UI.Placeholder( $el, $el.getDataFromComment() ) );
	});

});

/*
Class: UI.Masonry

Description: Masonry layout engine (converted from jQuery Masonry)

License: mooMasonry is dual-licensed under GPL and MIT, just like jQuery Masonry itself. You can use it for both personal and commercial applications.

Authors:
	- David DeSandro
	- Olivier Refalo
*/
UI.Masonry = new Class({

	Implements : Options,
	
	options : {
		singleMode : false,
		columnWidth : undefined,
		itemQuery : undefined,
		appendedContent : undefined,
		resizeable : true
	},
	
	element : undefined,
	colW : undefined,
	colCount : undefined,
	lastColCount : undefined,
	colY : undefined,
	lastColY: undefined,
	bound : undefined,
	masoned : undefined,
	bricks : undefined,
	posLeft : undefined,
	brickParent : undefined,
	
	initialize : function( target, options ) {
	
		this.setOptions(options);
		
		this.element = document.id( target );
		
		this.go();
	
	},
	
	go: function()
	{
		var options = this.options;
		
		// if we're dealing with appendedContent
		if (this.masoned && options.appendedContent != undefined)
			this.brickParent = options.appendedContent;
		else
			this.brickParent = this.element;
		
		if (this.brickParent.getChildren().length > 0) {
			
			// call masonry layout
			this.masonrySetup();
			this.masonryArrange();
			
			// binding window resizing
			
			var resizeOn = this.options.resizeable;
			if (resizeOn)
			{
			  if(this.bound == undefined)
			  {
					this.bound = this.masonryResize.bind(this);
					this.attach();
			  }
			}

			if (!resizeOn)
				this.detach();
		}
		
		// TW: Call resize function (Chrome was not respecting the first initalisation, needs to be investigated)
		this.bound();
	},
	
	attach: function()
	{
		window.addEvent('resize', this.bound);
		return this;
	},
	
	detach: function()
	{
		if (this.bound != undefined )
		{
			window.removeEvent('resize', this.bound);
			this.bound = undefined;
		}
		return this;
	},
	
	placeBrick: function(brick, setCount, setY, setSpan)
	{
		var shortCol = 0;
		
		for (var i = 0; i < setCount; i++)
			if (setY[i] < setY[shortCol])
				shortCol = i;
		
		brick.setStyles({
					top : setY[shortCol],
					left : this.colW * shortCol + this.posLeft
		});
		
		var size=brick.getSize().y+brick.getStyle('margin-top').toInt()+brick.getStyle('margin-bottom').toInt();
		
		for (var i = 0; i < setSpan; i++)
			this.colY[shortCol + i] = setY[shortCol] + size;
	},

	masonrySetup: function()
	{
		var s = this.options.itemQuery;
		this.bricks = s == undefined ? this.brickParent.getChildren() : this.brickParent.getElements(s);
		
		if (this.options.columnWidth == undefined)
		{
			var b = this.bricks[0];
			this.colW = b.getSize().x + b.getStyle('margin-left').toInt() + b.getStyle('margin-right').toInt();
		}
		else
			this.colW = this.options.columnWidth;
		
		var size = this.element.getSize().x+this.element.getStyle('margin-left').toInt()+this.element.getStyle('margin-right').toInt();
		this.colCount = Math.floor(size / this.colW);
		this.colCount = Math.max(this.colCount, 1);
		
		return this;
	},
	
	masonryResize: function()
	{
		this.brickParent = this.element;
		this.lastColY=this.colY;
		this.lastColCount = this.colCount;
		
		this.masonrySetup();
		
		if (this.colCount != this.lastColCount)
			this.masonryArrange();
		return this;
	},
	
	masonryArrange: function()
	{
		// if masonry hasn't been called before
		if (!this.masoned) 
			this.element.setStyle('position', 'relative');
		
		if (!this.masoned || this.options.appendedContent != undefined)
			// just the new bricks
			this.bricks.setStyle('position', 'absolute');
		
		// get top left position of where the bricks should be
		var cursor = new Element('div').inject(this.element, 'top');
		
		var pos = cursor.getPosition();
		var epos = this.element.getPosition();
		
		var posTop = pos.y - epos.y;
		this.posLeft = pos.x - epos.x;
		
		cursor.dispose();
		
		// set up column Y array
		if (this.masoned && this.options.appendedContent != undefined) {
			
			// if appendedContent is set, use colY from last call
			if(this.lastColY != undefined)
				this.colY=this.lastColY; 
				
			/*
			 * in the case that the wall is not resizeable, but the colCount has
			 * changed from the previous time masonry has been called
			 */
			for (var i = this.lastColCount; i < this.colCount; i++)
				this.colY[i] = posTop;
				
		} else {
			this.colY = [];
			for (var i = 0; i < this.colCount; i++)
				this.colY[i] = posTop;
		}
		
		// layout logic
		if (this.options.singleMode)
		{
			for (var k = 0; k < this.bricks.length; k++)
			{
				var brick = this.bricks[k];
				this.placeBrick(brick, this.colCount, this.colY, 1);
			}
		}
		else
		{
			for (var k = 0; k < this.bricks.length; k++)
			{
				var brick = this.bricks[k];
				
				// how many columns does this brick span
				var size=brick.getSize().x+brick.getStyle('margin-left').toInt()+brick.getStyle('margin-right').toInt();
				var colSpan = Math.ceil(size / this.colW);
				colSpan = Math.min(colSpan, this.colCount);
				
				if (colSpan == 1)
					// if brick spans only one column, just like singleMode
					this.placeBrick(brick, this.colCount, this.colY, 1);
				else {
					// brick spans more than one column
					// how many different places could this brick fit horizontally
					var groupCount = this.colCount + 1 - colSpan;
					var groupY = [0];
					// for each group potential horizontal position
					for (var i = 0; i < groupCount; i++)
					{
						groupY[i] = 0;
						// for each column in that group
						for (var j = 0; j < colSpan; j++)
							// get the maximum column height in that group
							groupY[i] = Math.max(groupY[i], this.colY[i + j]);
					}
					this.placeBrick(brick, groupCount, groupY, colSpan);
				}
			}
		}
		
		// set the height of the wall to the tallest column
		var wallH = 0;
		
		for (var i = 0; i < this.colCount; i++)
			wallH = Math.max(wallH, this.colY[i]);
		
		this.element.setStyle('height', wallH - posTop);
		
		// let listeners know that we are done
		this.element.fireEvent('masoned', this.element);
		this.masoned = true;
		this.options.appendedContent = undefined;
		
		// set all data so we can retrieve it for appended appendedContent
		// or anyone else's crazy jquery fun
		// this.element.data('masonry', props );
		
		return this;
	}

});

document.addEvent( 'domready', function() {

	$$( '.ui-masonry' ).each( function( $el ) {
		$el.store( 'masonry', new UI.Masonry( $el, $el.getDataFromComment() ) );
	});

});

/*
Class: UI.Popup

	Applies classes:
		ui-popup
		ui-popup-blockout
	
	Options
		modal
		width
		height
		transition		[ fade | immediate ]
		fromElement		(containing element, copies all children into the dialog on open, back on close)
		fromSparkle		(wraps UI.loadSparkle; can be 'path' or { path: '', data: {} )
		loadingHTML		(html to display while the popup is loading)
		onRender
		zIndex
		cssClass
	
	Methods
		close
		
	Events
		ready
		visible
		beforeClose
		close
		destroy
*/

UI.Popup = new Class({
	
	Implements: [ Options, Events ],
	
	options: {
		modal: true,
		width: false,
		height: false,
		transition: 'fade', // not implemented
		fromElement: null, // not implemented
		fromSparkle: null,
		loadingHTML: null, // not implemented
		onRender: null,
		performOnRender: null,
		zIndex: 1000,
		cssClass: null
	},
	
	initialize: function( options )
	{
		this.setOptions( options );
		
		// Create popup container
		var $container = this.$container = new Element( 'div', {
			'class': 'ui-popup',
			style: 'overflow: hidden; position: absolute; z-index: ' + ( this.options.zIndex + 1 ) + ';'
		}).inject( document.body, 'top' ).hide();
		
		
		// Store instance of class with popup
		$container.store( 'popup', this );
		
		// Add CSS class
		if ( this.options.cssClass )
			$container.addClass( this.options.cssClass );
		
		
		// Store the instance of this class to the popup container
		$container.store( 'popup', this );
		
		
		// Create the blockout element (if it doesn't exist yet)
		if ( this.options.modal )
		{
			if ( !UI.$popupBlockout )
			{
				UI.$popupBlockout = new Element( 'div', {
						'class': 'ui-popup-blockout',
						style: 'position: fixed; top: 0px; left: 0px; width: 100%; z-index: ' + this.options.zIndex + ';'
					})
					.setOpacity( 0 )
					.inject( document.body, 'top' );
			}
			else
				UI.$popupBlockout.show();
			
			UI.$popupBlockout.removeEvents( 'click' );
			
			UI.$popupBlockout.addEvent( 'click', (function() {
			
				this.close();
			
			}.bind( this )));
		}
		
		
		// Load sparkle content (if neccesary)
		if ( this.options.fromSparkle )
		{
			var render = (function( rtn ) {
			
				this.content = rtn;
				this.render();
			
			}.bind( this ));
			
			UI.loadSparkle( this.options.fromSparkle ).then( function( rtn ) { render( rtn ) } );
		}
		
		
		// Clone element content (if neccessary)
		if ( this.options.fromElement )
		{
			//
		}
		
	},
	
	render: function()
	{
		this.$container.show().setOpacity( 0 );
		
		
		// Create inner element
		this.$el = new Element( 'div', { style: 'position: absolute; width: 100%; height: 100%;' } )
			.inject( this.$container );
		
		
		// Create content containers
		var $contentContainer = new Element( 'div', { style: 'position: absolute; width: 100%; height: 100%; z-index: ' + ( this.options.zIndex + 2 ) + ';'  } ).inject( this.$el );
		var $contentPadding = new Element( 'div', { style: 'padding: 0px; position: relative;' } ).inject( $contentContainer );
		
		this.$content = new Element( 'div', { style: 'position: relative;' } )
			.inject( $contentPadding );
		
		
		// Inject content
		this.$content.set( 'html', this.content );
		
		
		// If we haven't specified a width and height, calculate it based on the content we inject
		if ( !this.options.width && !this.options.height )
		{
			this.$container.setStyles({ position: 'relative', top: 0, left: 0, overflow: 'visible' });
			
			var $content = this.$content.getChildren()[0];
			
			var cs = $content.getSize();
			
			this.options.width = cs.x;
			this.options.height = cs.y;
		}
		
		
		// Set width and height
		if ( this.options.width )
			this.$container.setStyles({ width: this.options.width });
		
		if ( this.options.height )
			this.$container.setStyles({ height: this.options.height });
		
		
		// Correct centering positioning, should it be on the $el, not the $container? (seems to be only effective on the $el)
		this.$container.setStyles({
			position: 'absolute',
			overflow: 'hidden',
			'margin-left': '50%',
			'left': -( Math.round( this.options.width / 2 ) )
		});
		
		
		// Initalise UI classes
		if ( this.options.fromSparkle )
			UI.initClasses( this.$content );
		
		
		// Make sure its positioned correctly relative to where we have scrolled to	
		this.positionPopup();
		window.addEvent( 'resize', (function() { this.positionPopup() }.bind( this )) );
		
		
		// Resize blockout
		this.resizeBlockout();
		window.addEvent( 'resize', (function() { this.resizeBlockout() }.bind( this )) );
		
		
		// Show contents
		var showContents = (function() {
		
			this.fireEvent( 'ready' );
			
			var onComplete = (function() {
			
				this.fireEvent( 'visible' );
				
				// Call render function (if provided)
				if ( this.options.onRender )
					UI.doCallback( this.options.onRender );
				
				// Call render function (if provided)
				if ( this.options.performOnRender )
					UI.doCallback( this.options.performOnRender );
				
			}.bind( this ));
			
			this.$container.get( 'tween' ).setOptions({ duration: 300 }).start( 'opacity', 1 ).chain( onComplete );
		
		}.bind( this ));
		
		
		// Fade in the blockout, then the content
		if ( this.options.modal )
			UI.$popupBlockout.get( 'tween' ).setOptions({ duration: 1 }).start( 'opacity', 0.5 ).chain( (function() { showContents(); }.bind( this )));
		else
			showContents();
	},
	
	close: function()
	{
		// TODO: If this.options.fromElement, we need to put the content back in its original div
		
		this.fireEvent( 'beforeClose' );
		
		// TODO: Used to be this.$content, maybe add it back so content fades before box does?
		
		this.$container.get( 'tween' ).setOptions({ duration: 200 }).start( 'opacity', 0 ).chain( (function() {
			
			this.fireEvent( 'close' );
			
			if ( this.options.modal )
				UI.$popupBlockout.get( 'tween' ).setOptions({ duration: 100 }).start( 'opacity', 0 ).chain( (function() { this.destroy(); }.bind( this )));
			else
				this.destroy();
			
			// Note: Used to be bound to the popup itself, now its referencing the popup
		
		}.bind( this )));
	
	},
	
	destroy: function( dontHideBlockout )
	{
		// Unbind events
		window.removeEvent( 'resize', this.resizeBlockout );
		
		// Remove elements
		this.$container.destroy();
		
		if ( this.options.modal && !dontHideBlockout )
			UI.$popupBlockout.hide();
		
		this.fireEvent( 'destroy' );
	},
	
	positionPopup: function()
	{
		this.$container.setStyle( 'margin-top', document.getScrollTop() + ( ( window.getSize().y - this.options.height ) / 2 ) );
	},
	
	resizeBlockout: function()
	{
		if ( !this.options.modal )
			return;
		
		var windowHeight = window.getSize().y;
		var documentHeight = document.body.clientHeight;
		
		UI.$popupBlockout.setStyle( 'height', Math.max( windowHeight, documentHeight ) );
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-popup' ).each( function( $el ) {
		$el.store( 'popup', new UI.Popup( $el, $el.getDataFromComment() ) );
	});

});


/*
Class: UI.Expander
*/
UI.Expander = new Class({
	
	Implements: Options,
	
	options: {
		startExpanded: false,
		labelClass: 'ui-expander-label',
		labelClass_close: 'ui-expander-close',
		contentClass: 'ui-expander-content',
		labelClass_expanded: 'ui-expander-label-expanded',
		labelClass_contracted: 'ui-expander-label-contracted',
		contentClass_expanded: 'ui-expander-content-expanded',
		contentClass_contracted: 'ui-expander-content-contracted',
		fx: true
	},
	
	initialize: function( target, options )
	{
		this.setOptions( options );
		this.$el = $( target );

		this.$content = this.$el.getElement( '.' + this.options.contentClass );
		this.$label = this.$el.getElement( '.' + this.options.labelClass );
		this.$close = this.$el.getElement( '.' + this.options.labelClass_close );
		
		this.defaults = {
			overflow: this.$content.getStyle( 'overflow' )
		};
		
		if ( this.options.startExpanded )
		{
			// Should be expanded in CSS, so no need to call expand();
			this.isExpanded = true;
		}
		else
		{
			this.$content.setStyles({ 'display' : 'none' });
			this.isExpanded = false;
		}
			
		// Event bindings
		this.$label.addEvent( 'click', this.toggle.bind( this ) );
		
		if ( this.$close ) {
			this.$close.addEvent( 'click', this.contract.bind( this ) );
		}

	},
	
	toggle: function()
	{
		if ( this.isExpanded )
			this.contract();
		else
			this.expand();
	},
	
	expand: function()
	{
		var defaults = this.defaults;
		
		if ( this.options.fx )
		{
			this.$content.setStyles({
					display: 'block',
					visibility: 'hidden',
					position: 'absolute'
				});
			
			var toHeight = this.$content.getSize().y;
			
			this.$content.setStyles({
					height: 1,
					position: 'relative',
					overflow: 'hidden',
					visibility: 'visible'
				})
				.get('tween')
					.start( 'height',  1, toHeight )
					.chain( function() {
						this.element.setStyles({
								height: '',
								overflow: defaults.overflow
							}); 
					});
		}
		else
		{
			this.$content.setStyles({
				display: 'block',
				visibility: 'visible'
			 });
		}
		
		this.$label.removeClass( this.options.labelClass_contracted ).addClass( this.options.labelClass_expanded );
		this.$content.removeClass( this.options.contentClass_contracted ).addClass( this.options.contentClass_expanded );
		
		this.isExpanded = true;
	},
	
	contract: function()
	{
		if ( this.options.fx )
		{
			this.$content.setStyle( 'overflow', 'hidden' )
				.get('tween')
				.start( 'height',  1 ).chain( function() {
					this.element.setStyles({
							height: '',
							display: 'none'
					}); 
				 });
		}
		else
		{
			this.$content.setStyles({ 'display' : 'none' });
		}
		
		this.$label.removeClass( this.options.labelClass_expanded ).addClass( this.options.labelClass_contracted );
		this.$content.removeClass( this.options.contentClass_expanded ).addClass( this.options.contentClass_contracted );
		
		this.isExpanded = false;
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-expander' ).each( function( $el ) {
		$el.store( 'expander', new UI.Expander( $el, $el.getDataFromComment() ) );
	});

});


/*
Class: UI.Accordion
*/
UI.Accordion = new Class({
	
	Implements: Options,
	
	options: {
		direction: 'vertical', // horizontal
		effect: 'quad',
		ease: 'in:out',
		duration: 500,
		width: null,
		height: null,
		trigger: 'click',
		itemQuery: null
	},
	
	current: null,
	
	initialize: function( target, options )
	{
		var $el = $( target );
		this.setOptions( options );
		
		// Determine effect
		var effect = this.options.effect;
		
		if ( this.options.ease && effect != 'linear' )
			effect += ':' + this.options.ease;
			
		this.transition = effect;
		
		// Determine trigger
		if ( this.options.trigger == 'hover' )
			this.options.trigger = 'mouseenter';
		
		// Bind children
		var items = $el.getChildren();
		
		if ( this.options.itemQuery )
			items = $el.getElements( this.options.itemQuery );
		
		items.each( (function( item, i ) {
			
			var children = item.getChildren();
			
			// We must have at least 2 elements to support an accordian.
			if ( children.length < 2 )
				return;
			
			var $trigger = children[0],
				$element = children[1];
			
			// Check to see if the item is visible or not, if so, make it the current one
			if ( $element.isVisible() )
				this.current = $element;
			
			// Add event to trigger
			$trigger.addEvent( this.options.trigger, (function(e) {
			
				e.stop();
				
				// If we click an item thats already open, close it.
				if ( this.current == $element )
				{
					this.contract( this.current );
					this.current = null;
					return;
				}
				
				// Make some sound
				if ( this.current )
					this.contract( this.current );
				
				this.expand( $element );
			
			}.bind( this )));
			
			
		}.bind( this )));
		
	},
	
	expand: function( $el )
	{
		var expanded = $el.retrieve( 'expanded' );
		
		if ( expanded )
			return;
		
		$el.show().setStyle( 'height', 'auto' );
		
		var elSize = $el.getDimensions();
		
		$el.setStyles({ position: 'relative', overflow: 'hidden' });
		
		$el.get( 'tween' ).setOptions({ duration: this.options.duration, transition: this.transition });
		
		switch( this.options.direction )
		{
			case 'vertical':
			
				$el.setStyle( 'height', 0 ).get( 'tween' ).start( 'height', this.options.height || elSize.y )
					.chain( function() {
					
						// ...
					
					});
			
			break;
		
			case 'horizontal':
				
				$el.setStyle( 'width', 0 ).get( 'tween' ).start( 'width', this.options.width || elSize.x )
					.chain( function() {
					
						// ...
					
					});
				
			break;
		}
		
		$el.store( 'expanded', true );
		
		this.current = $el;
		
	},
	
	contract: function( $el )
	{
		$el.get( 'tween' ).setOptions({ duration: this.options.duration, transition: this.transition });
		
		switch( this.options.direction )
		{
			case 'vertical':
				$el.get( 'tween' ).start( 'height', 0 );
			break;
		
			case 'horizontal':
				$el.get( 'tween' ).start( 'width', 0 );
			break;
		}
		
		$el.store( 'expanded', false );
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-accordion' ).each( function( $el ) {
		$el.store( 'accordion', new UI.Accordion( $el, $el.getDataFromComment() ) );
	});

});


/*
Class: UI.SmoothScroll
*/

UI.SmoothScroll = new Class({

	Implements: Options,
	
	options: {
		linkQuery: 'a',
		offset: { x: 0, y: 0 }
	},
	
	initialize: function( target, options )
	{
		this.setOptions( options );
		
		var links = target.getElements( this.options.linkQuery );
		
		new Fx.SmoothScroll({
			links: links,
			offset: this.options.offset
		});
	}

});

document.addEvent( 'domready', function() {

	$$( '.ui-smoothscroll' ).each( function( $el ) {
		$el.store( 'smoothscroll', new UI.SmoothScroll( $el, $el.getDataFromComment() ) );
	});

});

/*
Class: UI.Button

	Applies classes:
		
		ui-button (used to detect the buttons)
		
		ui-button-hover
		ui-button-press
		ui-button-focus
		ui-button-normal
	
	Available actions:
		
		show element
		hide element
		
		show menu
		
		switch elements
		
		next slide
		previous slide
		first slide
		last slide
		
		show popup
		close popup
		
		(anything else is eval'd)
		
	Effects for actions:
		
		show/hide: fade, slide, instant
		switch elements: fade, slide, sweet, instant
*/

UI.Button = new Class({
	
	Implements: [ Options, Events ],
	
	options: {
	
		action: 'do nothing',
		
		order: 0,
		
		isToggle: false
	
	},
	
	initialize: function( target, options )
	{
		var $el = this.$el = $( target );
		
		this.setOptions( options );
		
		this.bound = {
			maybeHideMenu: this.maybeHideMenu.bind( this )
		};
		
		this.state = {};
		
		// Add events
		this.$el.addEvent( 'focus', (function() {
			
				this.appearFocused();
			
			}).bind( this )).addEvent( 'blur', (function() {
			
				this.appearBlurred();
			
			}).bind( this )).addEvent( 'keydown', (function( event ) {
			
				if ( event.key == 'space' || event.key == 'enter' )
					this.toElement().fireEvent( 'mousedown' );
			
			}).bind( this )).addEvent( 'keyup', (function( event ) {
			
				if ( event.key == 'space' || event.key == 'enter' )
					this.toElement().fireEvent( 'click' );
			
			}).bind( this )).addEvent( 'mouseenter', (function() {
			
				this.state.mouseover = true;
				
				if ( this.state.disabled || this.state.pressed )
					return;
					
				if ( this.options.on == 'hover' )
					this.action();
				
				this.toElement()
					.removeClass( 'ui-button-normal' )
					.removeClass( 'ui-button-focus' )
					.addClass( 'ui-button-hover' );
			
			}).bind( this )).addEvent( 'mouseleave', (function() {
				
				this.state.mouseover = false;
				this.state.mousedown = false;
				
				if ( this.state.disabled || this.state.pressed )
					return;
				
				this.toElement()
					.removeClass( 'ui-button-hover' )
					.removeClass( 'ui-button-press' )
					.addClass( ( this.state.focus ) ? 'ui-button-focus' : 'ui-button-normal' );
			
			}).bind( this )).addEvent( 'mousedown', (function() {
				
				if ( this.state.disabled )
					return;
				
				this.state.mousedown = true;
				
				this.toElement()
					.removeClass( 'ui-button-normal' )
					.removeClass( 'ui-button-focus' )
					.removeClass( 'ui-button-hover' )
					.addClass( 'ui-button-press' );
				
			}).bind( this )).addEvent( 'click', (function(e) {
				
				var elHref = $el.getProperty( 'href' );
				
				if ( e && this.options.action && this.options.action != 'do nothing' && elHref == 'javascript:;' )
					e.stop();
				
				if ( this.state.disabled || !this.state.mousedown )
					return;
				
				if ( !this.options.on || this.options.on != 'hover' )
					this.action();
				
				this.state.mousedown = false;
				
				if ( this.options.isToggle )
				{
					if ( this.state.pressed )
						this.unpress();
					else
						this.makePressed();
					
					return;
				}
				
				if ( this.options.sticky )
					this.state.pressed = true;
				
				if ( !this.options.sticky )
					this.toElement().removeClass( 'ui-button-press' );
				
				if ( this.state.mouseover && !this.state.pressed )
					this.toElement().addClass( 'ui-button-hover' );
				else
					this.toElement().removeClass( 'ui-button-hover' );
				
				if ( !this.state.mouseover && this.state.focus )
					this.toElement().addClass( 'ui-button-focus' );
				else
					this.toElement().removeClass( 'ui-button-focus' );
				
				if ( !this.state.mouseover && !this.state.focus )
					this.toElement().addClass( 'ui-button-normal' );
				else
					this.toElement().removeClass( 'ui-button-normal' );
				
			}).bind( this ));
		
		this.$el.addClass( 'ui-button-normal' );
		
		this.$el.tabIndex = this.options.order;
	},
	
	appearFocused: function()
	{
		if ( !this.state )
			return;
	
		if ( this.state.hover || this.state.pressed )
			return;
		
		this.state.focus = true;
		
		if ( this.state.disabled )
			return;
		
		this.toElement()
			.removeClass( 'ui-button-normal' )
			.addClass( 'ui-button-focus' );
	},
	
	appearBlurred: function()
	{
		if ( !this.state )
			return;
	
		this.state.focus = false;
		
		if ( this.state.disabled || this.state.pressed )
			return;
		
		this.toElement().removeClass( 'ui-button-focus' );
		
		if ( this.state.hover )
			this.toElement().addClass( 'ui-button-hover' );
		else
			this.toElement().addClass( 'ui-button-normal' );
			
	},
	
	focus: function()
	{
		this.toElement().focus();
	},
	
	press: function()
	{
		this.toElement().fireEvent( 'mousedown' );
		
		if ( !this.options.isToggle )
			(function() { this.toElement().fireEvent( 'click' ); }).bind( this ).delay( 100 );
	},
	
	makePressed: function()
	{
		this.state.pressed = true;

		this.toElement().fireEvent( 'mousedown' );
	},
	
	makeHovered: function()
	{
		this.state.hover = true;
		this.toElement().fireEvent( 'mouseenter' );
	},
	
	makeUnpressed: function()
	{
		this.state.pressed = false;
		this.state.hover = false;
		this.state.focus = false;
		
		this.toElement()
			.removeClass( 'ui-button-press' )
			.removeClass( 'ui-button-hover' )
			.removeClass( 'ui-button-focus' );
		
		this.toElement().addClass( 'ui-button-normal' );
	},
	
	unpress: function()
	{
		this.state.pressed = false;
		
		this.toElement().removeClass( 'ui-button-press' );
		
		if ( this.state.hover )
			this.toElement().addClass( 'ui-button-hover' );
		else if ( this.state.focus )
			this.toElement().addClass( 'ui-button-focus' );
		else
			this.toElement().addClass( 'ui-button-normal' );
	},
	
	disable: function()
	{
		if ( this.state.disabled )
			return;
		
		this.state.disabled = true;
		
		(function() {
		
		this.toElement()
			.removeClass( 'ui-button-hover' )
			.removeClass( 'ui-button-press' )
			.removeClass( 'ui-button-focus' )
			.addClass( 'ui-button-normal' )
			.addClass( 'ui-button-disabled' )
			.setStyle( 'opacity', 0.5 );
			
		}).bind( this ).delay( 50 );
		
		return this;
	},
	
	enable: function()
	{
		if ( !this.state.disabled )
			return;
		
		this.state.disabled = false;
		
		if ( this.state.hover )
			this.toElement().removeClass( 'ui-button-normal' ).addClass( 'ui-button-hover' );
		else if ( this.state.focus )
			this.toElement().removeClass( 'ui-button-normal' ).addClass( 'ui-button-focus' );
		
		( function() { 
			this.toElement().removeClass( 'ui-button-disabled' ).setStyle( 'opacity', 1 );
		}.bind( this )).delay( 50 );
		
		return this;
	},
	
	reset: function()
	{
		this.state.pressed = false;
		this.state.hover = false;
		this.state.disabled = false;
		this.state.focus = false;
		this.state.mouseover = false;
		this.state.mousedown = false;
		
		this.toElement()
			.removeClass( 'ui-button-hover' )
			.removeClass( 'ui-button-press' )
			.removeClass( 'ui-button-focus' )
			.removeClass( 'ui-button-disabled' )
			.addClass( 'ui-button-normal' );
	
	},
	
	set: function( what, value )
	{
		switch ( what )
		{
			case 'tabIndex':
				this.$el.tabIndex = value || 0;
			break;
		}
		
		return this;
	},
	
	action: function()
	{
		var $el = this.$el;
		
		var options = this.options;
		
		switch( options.action )
		{
			/** Show Element / Menu / Hide **/
			case 'show':
			case 'show element':
			case 'show menu':
			case 'hide':
			case 'hide element':
				
				/* Defaults */
				var $target = null,
					on = 'click', // click, hover, mouseenter
					transition = 'instant', // fade, slide, instant
					duration = 300,
					effect = 'linear',
					ease = 'out',
					hideSiblings = false;
				
				var anchorFrom = 'top left',
					align = 'top left',
					offsetX = 0,
					offsetY = 0,
					relativeTo = null;
					
				/* Show Menu only */
				var sticky = false,
					focusField = null;
				
				var fnChain = [];
				
				/* Set Options */
				
				// Target
				if ( options.target == 'this' )
					this.$target = $target = $el;
				else
					this.$target = $target = $( options.target );
				
				// If we have no target, do nothing
				if ( !$target )
					return;
				
				// On
				if ( options.on ) on = options.on;
					if ( on == 'hover' ) on = 'mouseenter';
				
				// Transition
				if ( options.transition ) transition = options.transition;
				
				// Duration
				if ( options.duration ) duration = options.duration;
				
				// Effect
				if ( options.effect ) effect = options.effect;
					
				// Ease
				if ( options.ease && effect != 'linear' ) effect += ':' + options.ease;
				
				// Anchor From
				if ( options.anchorFrom ) anchorFrom = options.anchorFrom.replace( ' ', '-' ).camelCase();
				
				// Align
				if ( options.align ) align = options.align.replace( ' ', '-' ).camelCase();
				
				// Left Offset
				if ( options.left ) offsetX = options.left;
				
				// Top Offset
				if ( options.top ) offsetY = options.top;
				
				// Relative To
				relativeTo = options.relativeTo;
				
				if ( relativeTo == 'this' ) relativeTo = $el;
				
				
				/* Show Menu Only */
				
				// Sticky
				if ( options.action == 'show menu' && options.sticky )
					sticky = options.sticky;
				
				// Focus Field
				if ( options.action == 'show menu' && options.focusField )
					focusField = options.focusField;
				
				
				/* Position */
				var positionTarget = (function() {
				
					if ( !relativeTo )
						return;
					
					$target.position({
						position: anchorFrom,
						edge: align,
						relativeTo: relativeTo,
						offset: { x: Number( offsetX ), y: Number( offsetY ) }
					});
					
				});
				
				/* Hide Siblings */
				var hideSiblings = (function() {
				
					try {
						$target.getParent().getChildren().hide();
					}
					catch(e){}
				
				});
				
				
				/* Add events depending on action */
				switch( options.action )
				{
					case 'show':
					case 'show element':
					
						if ( options.hideSiblings )
							fnChain.push( hideSiblings );
					
					break;
				}
				
				/* Transition */
				switch( transition )
				{
					case 'fade':
					
						var showTarget = (function() {
							
							if ( $target.isVisible() )
								return;
							
							positionTarget();
							
							$target.show().setOpacity( 0 );
							
							$target.get( 'tween' )
								.setOptions({
									duration: duration,
									transition: effect
								})
								.start( 'opacity', 1 )
								.chain( function() {
								
									if ( options.onComplete )
										UI.doCallback( options.onComplete );
								
								});
							
						});
						
						var hideTarget = (function() {
						
							if ( !$target.isVisible() )
								return;
							
							$target.get( 'tween' )
								.setOptions({
									duration: duration,
									transition: effect
								})
								.start( 'opacity', 0 )
								.chain( function() {
								
									$target.hide();
									
									if ( options.onComplete )
										UI.doCallback( options.onComplete );
								
								});
						
						});
						
						switch( options.action )
						{
							case 'show':
							case 'show element':
							case 'show menu':
								fnChain.push( showTarget );
							break;
							
							case 'hide':
							case 'hide element':
								fnChain.push( hideTarget );
							break;
						}
					
					break;
					
					case 'slide':
					
						var showTarget = (function() {
						
							if ( $target.isVisible() )
								return;
							
							$target.setStyles({
								overflow: 'hidden'
							});
							
							var fromHeight = 0;
							
							$target.show();
							
							var toHeight = $target.getSize().y;
							
							$target.setStyles({
								height: 0,
								overflow: 'hidden'
							});
							
							$target.get( 'tween' )
								.setOptions({
									duration: duration,
									transition: effect
								})
								.start( 'height', fromHeight, toHeight )
								.chain( function() {
								
									if ( options.onComplete )
										UI.doCallback( options.onComplete );
								
								});
						
						});
						
						var hideTarget = (function() {
						
							if ( !$target.isVisible() )
								return;
							
							$target.setStyles({
								overflow: 'hidden'
							});
							
							$target.get( 'tween' )
								.setOptions({
									duration: duration,
									transition: effect
								})
								.start( 'height', 0 )
								.chain( function() {
								
									if ( options.onComplete )
										UI.doCallback( options.onComplete );
								
								});
						
						});
						
						switch( options.action )
						{
							case 'show':
							case 'show element':
							case 'show menu':
								fnChain.push( showTarget );
							break;
							
							case 'hide':
							case 'hide element':
								fnChain.push( hideTarget );
							break;
						}
					
					break;
					
					default: // instant
					
						var showTarget = (function() {
						
							if ( $target.isVisible() )
								return;
							
							$target.show();
							
							positionTarget();
							
							if ( options.onComplete )
								UI.doCallback( options.onComplete );
						
						});
						
						var hideTarget = (function() {
						
							if ( !$target.isVisible() )
								return;
							
							$target.hide();
							
							if ( options.onComplete )
								UI.doCallback( options.onComplete );
						
						});
						
						switch( options.action )
						{
							case 'show':
							case 'show element':
							case 'show menu':
								fnChain.push( showTarget );
							break;
							
							case 'hide':
							case 'hide element':
								fnChain.push( hideTarget );
							break;
						}
					
				}
				
				
				// Call functions
				switch( options.action )
				{
					case 'show':
					case 'show element':
					case 'hide':
					case 'hide element':
					
						fnChain.each( function(f) { f() } );
						
						if ( on == 'mouseenter' )
						{
							$el.addEvent( 'mouseleave', function() {
							
								$target.hide();
							
							});
						}
					
					break;
					
					case 'show menu':
					
						fnChain.each( function(f) { f() } );
					
					break;
				}
				
				// Handle show menu
				if ( options.action == 'show menu' )
				{
					if ( this.options.sticky && this._menuIsOpen )
					{
						this.hideMenu();
					}
					else if ( !this._menuIsOpen )
					{
						if ( UI.$currentButtonMenu && UI.$currentButtonMenu._menuIsOpen )
							UI.$currentButtonMenu.hideMenu( true );
						
						UI.$currentButtonMenu = this;
						
						// Detect width and apply (if required)
						if ( this.options.relativeWidth )
						{
							var elSize = this.$el.getSize();
							
							$target.setStyle( 'width', elSize.x );
						}
						
						// Elevate z-index for buttons and the target
						var bIndex = $el.getStyle( 'z-index' ),
							tIndex = $target.getStyle( 'z-index' );
						
						if ( bIndex != 'auto' )
						{
							// Button
							var bInt = bIndex.toInt();
							
							this._buttonIndex = bInt;
							
							$el.setStyle( 'z-index', ( bInt + 10 ) );
						}
						
						if ( tIndex != 'auto' )
						{
							// Target
							var tInt = tIndex.toInt();
							
							this._targetIndex = tInt;
							
							$target.setStyle( 'z-index', ( tInt + 10 ) );
						}
						
						// Focus Field
						if ( this.options.focusField )
						{
							var field = $( this.options.focusField );
							
							if ( field )
								field.focus();
						}
						
						this._menuIsOpen = true;
						
						if ( options.sticky )
						{
							document.body.addEvent( 'click', this.bound.maybeHideMenu );
							document.body.addEvent( 'focus', this.bound.maybeHideMenu );
						}
						else
						{
							this.$el.addEvents({
							
								'mouseenter': (function() {
								
									$clear( this._closeTimer );
								
								}.bind( this )),
								
								'mouseleave': (function(e) {
								
									this.bound.maybeHideMenu(e);
								
								}.bind( this ))
							
							});
							
							this.$target.addEvents({
							
								'mouseenter': (function() {
								
									$clear( this._closeTimer );
								
								}.bind( this )),
								
								'mouseleave': (function(e) {
								
									this.bound.maybeHideMenu(e);
								
								}.bind( this ))
							
							});
						}
					}
				}
			
			break;
			
			case 'switch elements':
			
				/* Defaults */
				var $target = null,
					$show = null,
					$hide = null,
					transition = 'instant',
					duration = 300,
					effect = 'linear',
					ease = 'out';
				
				
				/* Set Options */
				$show = $( options.show );
				$hide = $( options.hide );
				
				// Transition
				if ( options.transition )
					transition = options.transition;
				
				// Duration
				if ( options.duration )
					duration = options.duration;
				
				// Effect
				if ( options.effect )
					effect = options.effect;
				
				// Ease
				if ( options.ease && effect != 'linear' )
					effect += ':' + options.ease;
				
				
				/* Transition */
				switch( transition )
				{
					case 'fade':
					
						$hide.get( 'tween' )
							.setOptions({
								duration: duration,
								transition: effect
							})
							.start( 'opacity', 0 )
							.chain( function() {
							
								$hide.hide();
								
								$show.show().setOpacity(0);
								
								$show.get( 'tween' )
									.setOptions({
										duration: duration / 2,
										transition: effect
									})
									.start( 'opacity', 1 )
									.chain( function() {
									
										if ( options.onComplete )
											UI.doCallback( options.onComplete );
									
									});
								
							});
					
					break;
					
					case 'slide':
					
						var fromHeight = $hide.getSize().y;
						
						$show.show();
						
						var toHeight = $show.getSize().y;
						
						$show.hide();
						
						$hide.setStyle( 'overflow', 'hidden' );
						
						$hide.get( 'tween' )
							.setOptions({
								duration: duration / 2,
								transition: effect
							})
							.start( 'height', fromHeight, toHeight )
							.chain( function() {
							
								$hide.setStyle( 'height', 'auto' );
								$hide.hide();
								
								$show.show();
								
								$show.get( 'tween' )
									.setOptions({
										duration: duration / 2,
										transition: effect
									})
									.start( 'height', toHeight )
									.chain( function() {
									
										$show.setStyle( 'height', 'auto' );
										
										if ( options.onComplete )
											UI.doCallback( options.onComplete );
									
									});
								
							});
					
					break;
					
					case 'sweet':
					
						var fromHeight = $hide.getSize().y;
						
						$show.show();
						
						var toHeight = $show.getSize().y;
						
						$show.hide();
						
						$hide.get( 'morph' )
							.setOptions({
								duration: duration / 2,
								transition: effect
							})
							.start({ 'height': [ fromHeight, toHeight ], opacity: 0 })
							.chain( function() {
							
								$hide.setStyle( 'height', 'auto' );
								$hide.hide();
								
								$show.show().setOpacity(0);
								
								$show.get( 'morph' )
									.setOptions({
										duration: duration / 2,
										transition: effect
									})
									.start({ 'height': toHeight, opacity: 1 })
									.chain( function() {
									
										$show.setStyle( 'height', 'auto' );
										
										if ( options.onComplete )
											UI.doCallback( options.onComplete );
									
									});
								
							});
					
					break;
					
					default: // instant
					
						$show.show();
						$hide.hide();
						
						if ( options.onComplete )
							UI.doCallback( options.onComplete );
				}
			
			break;
			
			case 'next slide':
			case 'previous slide':
			case 'first slide':
			case 'last slide':
			
				var slides = null,
					$curEl = $el;
				
				while ( !slides )
				{
					$curEl = $curEl.getParent();
					
					if ( $curEl )
						if ( $curEl.hasClass( 'ui-slides' ) )
							if ( $curEl.retrieve( 'slides' ) )
								slides = $curEl.retrieve( 'slides' );
				}
				
				if ( !slides ) {
					$log( 'Slides store not found for button:', $el, 'With action: ' + options.action );
					return;
				}
				
				switch( options.action )
				{
					case 'next slide': slides.next(); return;
					case 'previous slide': slides.previous(); return;
					case 'first slide': slides.gotoFirst(); return;
					case 'last slide': slides.gotoLast(); return;
				}
			
			break;
			
			case 'switch class':
			
				$el.removeClass( options.remove ).addClass( options.add );
			
			break;
			
			case 'show popup':
			case 'open popup':
			
				new UI.Popup( options );
			
			break;
			
			case 'close popup':
			case 'close then open popup':
			case 'close popup then reload':
			
				var popup = null,
					$curEl = $el;
				
				while ( !popup )
				{
					$curEl = $curEl.getParent();
					
					if ( $curEl )
					{
						if ( $curEl.hasClass( 'ui-popup' ) )
						{
							window.theElement = $curEl;
						
							if ( $curEl.retrieve( 'popup' ) )
							{
								popup = $curEl.retrieve( 'popup' );
							}
						}
					}
				}
				
				if ( !popup )
				{
					$log( 'Popup store not found for button:', $el, 'With action: ' + options.action );
					return;
				}
				
				if ( options.action == 'close then open popup' )
				{
					popup.destroy( true );
					UI.$popupBlockout.show();
					new UI.Popup( options );
				}
				else
				{
					popup.close();
					
					if ( options.action == 'close then reload' )
					{
						(function() {
							top.location.reload();
						}).delay( 200 );
					}
				}
				
			break;
			
			case 'do nothing':
			
				// *crickets*
			
			break;
			
			default:
			
				UI.doCallback( options.action, $el );
		
		}
	},
	
	toElement: function()
	{
		return this.$el;
	},
	
	/** Behaviour Specific Methods **/
	
	maybeHideMenu: function( e )
	{
		if ( this.options.on == 'hover' )
			this.makeHovered();
		else
			this.makePressed();
		
		// Hides the menu ($target) if the event didn't originate within the target or the button elements
		
		var target = e.target,
			relatedTarget = e.relatedTarget;
		
		// Don't hide the target if we are in sticky mode and we specifically click on it or the button element
		if ( this.options.sticky && ( target == this.$target || target == this.$el ) )
			return;
		
		// Don't hide the target if we are mousing to the button itself or the menu
		if ( relatedTarget == this.$el || relatedTarget == this.$target )
			return;
		
		// Make sure the element we clicked doesn't belong to the target (if in sticky mode)
		if ( relatedTarget )
		{
			if ( relatedTarget.getParent )
			{
				var parents = relatedTarget.getParents(); // target
				
				if ( parents.contains( this.$target ) || parents.contains( this.$el ) )
					return;
			}
		}
		else
			return;
		
		// Otherwise hide the menu
		this.hideMenu();
	},
	
	hideMenu: function( force )
	{
		var hide = (function() {
		
			// Hide element
			this.$target.hide();
			
			// Reset button state
			this.reset();
			
			// Set open state
			this._menuIsOpen = false;
			
			// Remove events
			if ( this.options.sticky )
			{
				document.body.removeEvent( 'click', this.bound.maybeHideMenu );
				document.body.removeEvent( 'focus', this.bound.maybeHideMenu );
			}
			else
			{
				this.$el.removeEvent( 'mouseleave', this.bound.maybeHideMenu );
				this.$target.removeEvent( 'mouseleave', this.bound.maybeHideMenu );
			}
			
			// Set back position indexes
			if ( this._buttonIndex )
				this.$el.setStyle( 'z-index', this._buttonIndex );
				
			if ( this._targetIndex )
				this.$target.setStyle( 'z-index', this._targetIndex );
		
		}.bind( this ));
		
		this._closeTimer = (function() {
		
			hide();
		
		}.bind( this )).delay( ( force ? 0 : 300 ) );
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-button' ).each( function( $el ) {
		$el.store( 'button', new UI.Button( $el, $el.getDataFromComment() ) );
	});

});


/*
Class: UI.Suggest
*/
UI.Suggest = new Class({
	
	Implements: Options,
	
	options: {
		
		target: null,
		
		parameter: null,
		
		field: null,
		
		itemQuery: 'li',
		
		minLength: 3,
		
		populateField: false,
		
		populateLabelField: false,
		populateValueField: false,
		
		highlightTerms: true,
		highlightQuery: 'a',
		
		gotoLink: false,
		
		queryPage: null,
		
		disableBlur: false,
		
		extendOnSelect: false,
		
		resultsClass: null,
		
		left: 0,
		top: 0,
		
		onRender: null,
		
		useFirstResult: false,
		
		positionOutside: false,
		
		onBlank: null,
		
		anchorFrom: 'bottomLeft',
		align: 'topLeft',
		
		autoHeight: false,
		heightAdjustment: 0
		
	},
	
	initialize: function( target, options )
	{
		var $el = this.$el = $( target );
		
		this.setOptions( options );
		
		// Find the field
		var $field = this.$field = $el.getElement( 'input' );
		
		if ( this.options.field )
			$field = $( this.options.field );
		
		if ( !$field )
		{
			$log( 'No valid field was found for UI.Suggest, you must use a standard input field inside the container or supply an id as [field].' );
			return;
		}
		
		// Find results (or create it)
		var $results = this.$results = $el.getElement( '.ui-suggest-results' );
		
		if ( !$results )
			$results = this.$results = new Element( 'div', { 'class': 'ui-suggest-results', style: 'position: absolute;' } );
		
		var body = window.document.body;
		
		if ( this.options.positionOutside )
			$results.inject( body );
		else
			$results.inject( $field, 'after' );
		
		$results.hide();
		
		// Activity El
		var $activity = this.$activity = $el.getElement( '.ui-suggest-activity' );
		
		// Clear El
		this.$clear = $el.getElement( '.ui-suggest-clear' );
		
		// Add events to field
		$field.addEvents({
			
			'keypress': (function( event ) {
				
				// Specific keys
				if ( event.key == 'esc' )
					return this.hide();
				
				if ( event.key == 'enter' )
				{
					if ( this._lastFocused )
						this._lastFocused.fireEvent( 'mousedown' );
					
					if ( this.options.useFirstResult )
						this._useFirstResult = true;
					
					if ( this.$results.isVisible() )
						this.gotoFirstResult();
					
					this.hide();
					
					if ( this.options.queryPage && !this._keyingThroughItems )
					{
						this._redirecting = true;
						top.location.href = this.options.queryPage + '/' + this.$field.value.trim();
					}
					
					return;
				}
				
				var ci = null;
				
				if ( this.results )
					ci = this.results.indexOf( this._lastFocused );
				
				if ( event.key == 'up' )
				{
					event.stop();
					
					if ( this.results && this.results[ ci - 1 ] )
					{
						this.focusItem( this.results[ ci - 1 ] );
						this._keyingThroughItems = true;
					}
					
					return;
				}
				
				if ( event.key == 'down' )
				{
					event.stop();
					
					if ( !this._lastFocused )
					{
						var firstItem = this.results[0];
					
						this.focusItem( firstItem );
						
						return;
					}
					
					if ( this.results && this.results[ ci + 1 ] )
					{
						this.focusItem( this.results[ ci + 1 ] );
						this._keyingThroughItems = true;
					}
					
					return;
				}
			
			}.bind( this )),
			
			'keyup': (function( event ) {
			
				if ( event.key == 'esc' || event.key == 'enter' )
					return;
				
				// Search
				if ( !$field.value.trim() )
				{
					this.hide();
					
					if ( this.options.onBlank )
						UI.doCallback( this.options.onBlank, this );
					
					return;
				}
				
				if ( this._redirecting )
					return;
				
				var searchStr = $field.value.trim(),
					visible = 0,
					firstVisible = null;
				
				if ( this._timer )
					$clear( this._timer );
				
				this._timer = (function() {
				
					this.search();
				
				}.bind( this )).delay( 250 );
			
			}.bind( this )),
			
			'blur': (function( event ) {
			
				if ( !this.options.disableBlur )
					this.hide();
				
				if ( this.$activity )
				{
					this.$el.removeClass( 'ui-suggest-processing' );
					this.$activity.hide();
				}
			
			}.bind( this ))
		
		});
	
	},
	
	hide: function()
	{
		(function(){
			
			if ( this.$results )
				this.$results.hide(); //.empty(); 14/10/10 - Removed as it was preventing gotoFirstResult from working, might still need it for some reason
			
			this._lastSearch = '';
			
		}).delay( 250, this );
	},
	
	search: function()
	{
		if ( this._redirecting )
			return;
		
		var value = this.$field.value.trim();
		
		if ( value.length < this.options.minLength )
			return;
		
		if ( value && ( this._lastSearch == value ) )
		{
			// $log( 'Search string is the same.' );
			return;
		}
		
		// Reset values
		this._lastSearch = value;
		
		this._lastFocused = false;
		
		this._keyingThroughItems = false;
		
		// Search
		var send = {
			path: '/' + this.options.target,
			data: this.options.data || {}
		}
		
		var parameter = ( this.$field.getProperty( 'name' ) || 'search' );
		
		if ( this.options.parameter )
			parameter = this.options.parameter;
		
		send.data[ parameter ] = value;
		
		// Show activity
		if ( this.$activity )
		{
			this.$el.addClass( 'ui-suggest-processing' );
			this.$activity.show();
		}
		
		// Hide clear
		if ( this.$clear )
			this.$clear.hide();
		
		// Call action
		UI.loadSparkle( send )
			.then( (function( rtn ) {
				
				if ( this.$field.value.trim() != value )
					return;
				
				if ( this._redirecting )
					return;
				
				this.render( rtn );
			
			}.bind( this )));
	},
	
	focusItem: function( newFocus )
	{
		if ( this._lastFocused )
			this._lastFocused.removeClass( 'ui-suggest-focused' );
		
		this._lastFocused = newFocus;
		
		if ( newFocus )
			newFocus.addClass( 'ui-suggest-focused' );
	},
	
	gotoFirstResult: function()
	{
		var $results = this.$results;
		
		$results.hide();
		
		var links = $results.getElements( 'a' );
		
		if ( !links.length )
			return;
		
		var $firstLink = links[0];
		
		if ( this.options.onSelect )
		{
			var value = $firstLink.getElement( '.ui-suggest-value' );
			
			if ( value )
			{
				if ( this.options.extendOnSelect )
					UI.doCallback( this.options.onSelect, [ value, this.$field ] );
				else
					UI.doCallback( this.options.onSelect, value );
			}
		}
		else
		{
			top.location.href = $firstLink.getProperty( 'href' );
		}
		
		this._useFirstResult = false;
		
		this.$results.hide() //.empty(); 14/10/10 - Removed as it was preventing gotoFirstResult from working, might still need it for some reason
		
		return;
	},
	
	render: function( results )
	{
		var $field = this.$field,
			$results = this.$results;
		
		// Hide activity
		if ( this.$activity )
		{
			this.$el.removeClass( 'ui-suggest-processing' );
			this.$activity.hide();
		}
		
		// If we have no results, don't render anything
		if ( !results )
			return;
		
		// Show clear
		if ( this.$clear )
			this.$clear.show();
		
		// Show and position results
		$results.show();
		
		$results.set( 'html', results );
		
		$results.position({
			relativeTo: $field,
			position: this.options.anchorFrom.replace( ' ', '-' ).camelCase(),
			edge: this.options.align.replace( ' ', '-' ).camelCase(),
			offset: { x: this.options.left, y: this.options.top }
		});
		
		// Reset last focused as its a brand new set of elements
		this._lastFocused = false;
		
		// Add events to items
		var results = this.results = $results.getElements( this.options.itemQuery );
		
		results.each( (function( $i ) {
			
			$i.addEvents({
			
				'mouseover': (function() {
				
					// this.focusItem( $i );
				
				}.bind( this )),
				
				'mouseout': (function() {
				
					$i.removeClass( 'ui-suggest-focused' );
					
				}.bind( this )),
				
				'mousedown': (function(e) {
				
					// Populate the label hidden field (if we have one) or just use the field we are binded to
					if ( this.options.populateLabelField )
					{
						var label = $i.getElement( '.ui-suggest-label' ).get( 'text' ),
							$labelField = this.$labelField = this.$el.getElement( '[name=' + this.options.populateLabelField + ']' );
						
						$labelField.value = label;
						
						// Populate field (if label)
						if ( this.options.populateField == 'label' )
							this.$field.value = label.clean();
					}
					
					if ( $type( this.options.populateField ) == 'boolean' && this.options.populateField )
					{
						var label = $i.get( 'text' );
						
						this.$field.value = label.clean();
					}
					
					// Populate the value hidden field (if we have one)
					if ( this.options.populateValueField )
					{
						var value = $i.getElement( '.ui-suggest-value' ).get( 'text' ),
							$valueField = this.$valueField = this.$el.getElement( '[name=' + this.options.populateValueField + ']' );
						
						$valueField.value = value.clean();
						
						// Populate field (if value)
						if ( this.options.populateField == 'value' )
							this.$field.value = value.clean();
					}
					
					// Onselect event
					if ( this.options.onSelect )
					{
						var value = $i.getElement( '.ui-suggest-value' );
						
						if ( value )
						{
							if ( this.options.extendOnSelect )
								UI.doCallback( this.options.onSelect, [ value, this.$field ] );
							else
								UI.doCallback( this.options.onSelect, value );
						}
					}
					
					// Go to the first link we encounter (may want to expand this further in case we have examples with more than one link)
					if ( this.options.gotoLink )
					{
						var $link = $i;
					
						if ( $i.get( 'tag' ) != 'a' )
							$link = $i.getElement( 'a' );
						
						if ( !$link )
							return;
						
						top.location.href = $link.getProperty( 'href' );
					
					}
					
					// Make the sure last search is reflected by the current value (it may have changed)
					this._lastSearch = this.$field.value.trim();
					
					// Hide the results (since we selected something)
					if ( this.options.gotoLink || this.options.populateField || this.options.onSelect )
						if ( !this.options.disableBlur )
							this.hide();
					
				}.bind( this ))
				
			});
			
		}.bind( this )));
		
		// Add highlights
		if ( this.options.highlightTerms )
		{
			var regex = new RegExp( '(' + this._lastSearch + ')', 'gi' );
			
			$results.getElements( this.options.highlightQuery ).each( (function( $el ) {
			
				var text = $el.get( 'text' ),
					parsed = text.replace( regex, '<span class="ui-suggest-highlight">$1</span>' );
				
				$el.set( 'html', parsed );
			
			}.bind( this )));
		}
		
		// Init UI Classes
		UI.initClasses( $results );
		
		// Use first result (enter key was pressed)
		if ( this.options.useFirstResult && this._useFirstResult )
			this.gotoFirstResult();
		
		// Auto Height
		if ( this.options.autoHeight )
		{
			var resultsSize = $results.getSize(),
				windowSize = window.getSize();
			
			var resultsPosition = $results.getPosition();
			
			var adjustedHeight = windowSize.y - resultsPosition.y - this.options.top - this.options.heightAdjustment;
			
			if ( resultsSize.y > windowSize.y )
				$results.getElement( this.options.autoHeight ).setStyle( 'height', adjustedHeight );
		}
		
		// On Render Function
		if ( this.options.onRender )
			UI.doCallback( this.options.onRender, this.$el );
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-suggest' ).each( function( $el ) {
		$el.store( 'suggest', new UI.Suggest( $el, $el.getDataFromComment() ) );
	});

});


/*
Class: UI.Floating
*/
UI.Floating = new Class({
	
	Implements: Options,
	
	options: {
		width: null,
		align: 'right',
		floatingClass: null,
		offset: 0
	},
	
	initialize: function( target, options )
	{
		this.setOptions( options );
		
		if ( window.document.body.hasClass( 'is_ie' ) )
			return;
		
		this.$el = target.getElement( '.ui-floating-box' );
		
		this.$track = target;
		
		this.$track.setStyles({
			height: '100%',
			overflow: 'visible',
			position: 'absolute',
			width: this.options.width
		});
		
		switch( this.options.align )
		{
			case 'left':
				this.$track.setStyle( 'left', 0 );
			break;
			
			case 'right':
				this.$track.setStyle( 'right', 0 );
			break;
		}
		
		this.listen();
	},
	
	listen: function()
	{
		var position = this.$el.getPosition().y,
			trackSize = this.$track.getSize().y;
		
		var move = (function() {
		
			var scroll = $( document.body ).getScroll().y,
				height = this.$el.getDimensions().y,
				offset = this.options.offset;
			
			// Should element be floating?
			var floating = ( scroll > ( position - offset ) );
			
			// Has element reached the bottom?
			var bottom = ( ( scroll - position ) > ( trackSize - height - offset ) );
			
			// Position as needed
			if ( floating && !bottom )
			{
				this.$el.setStyles({
					position: 'fixed',
					top: offset,
					bottom: 'auto'
				});
				
				if ( this.options.floatingClass && !this.$el.hasClass( this.options.floatingClass ) )
					this.$el.addClass( this.options.floatingClass );
			}
			else if ( bottom )
			{
				this.$el.setStyles({
					position: 'absolute',
					top: 'auto',
					bottom: 0
				});
			}
			else
			{
				this.$el.setStyles({
					position: 'absolute',
					top: 'auto'
				});
				
				if ( this.options.floatingClass && this.$el.hasClass( this.options.floatingClass ) )
					this.$el.removeClass( this.options.floatingClass );
			}
		
		}.bind( this ));
		
		// Initial positioning
		move();
		
		// Add scroll event
		window.addEvent( 'scroll', move );
	
	},
	
	resize: function()
	{
		window.addEvents( 'scroll' );
		
		this.listen();
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-floating' ).each( function( $el ) {
		$el.store( 'floating', new UI.Floating( $el, $el.getDataFromComment() ) );
	});

});


/*
Class: UI.Images
*/
UI.Images = new Class({

	Implements: Options,
	
	options: {
	
		mode: 'image',
		
		effect: 'quad',
		ease: 'in:out',
		
		durations: {
			open: 700,
			resize: 400,
			fade: 200,
			caption: 400
		},
		
		explode: false, // to be implemented
		
		padding: 10,
		
		captions: false,
		animateCaption: true,
		
		overlay: true, // to be implemented
		
		imageQuery: 'a',
		
		counter: 'Image {num} of {total}',
		
		mouseWheel: false,
		keyboardShortcuts: false
		
	},
	
	initialize: function( target, options )
	{
		this.setOptions( options );
		
		var $el = this.$el = target;
		
		if ( !options )
			options = {};
		
		
		// Gather images
		this.images = [];
		
		
		// Mode
		switch( this.options.mode )
		{
			case 'gallery':
			
				var images = target.getElements( this.options.imageQuery );
				
				if ( images.length <= 1 )
					this.options.mode = 'image';
				
				images.each( function( $el ) {
				
					var options = $el.getDataFromComment() || {};
				
					var image = new UI.Images.Image( $el, this, options );
				
					this.images.push( image );
				
				}, this );
				
			break;
			
			case 'image':
			
				var image = new UI.Images.Image( $el, this, options );
				
				// Enable captions if its been set for the image and we don't set it specifically
				if ( image.get( 'caption' ) && !options.captions )
					this.options.captions = true;
			
			break;
		}
		
	},
	
	setup: function( image )
	{
		// Overlay
		this.$overlay = new Element( 'div', {
			'class': 'ui-images-overlay',
			events: {
				click: this.close.bindWithEvent( this )
			}
		}).inject( document.body, 'top' );
		
		
		// Box
		this.$box = new Element( 'div', {
			'class': 'ui-images-box',
			styles: {
				width: 0,
				height: 0,
				/* marginLeft: 0, */
				position: 'absolute',
				opacity: 0.2
			}
		}).inject( document.body, 'top' );
		
		
		// Previous / Next
		if ( this.options.mode == 'gallery' )
		{
			this.$prevLink = new Element( 'a', {
				'class': 'ui-images-prev',
				href: 'javascript:;'
			}).inject( this.$box );
			
			this.$prevLink.addEvent( 'click', this.changeImage.bindWithEvent( this, -1 ) );
			
			this.$nextLink = new Element( 'a', {
				'class': 'ui-images-next',
				href: 'javascript:;'
			}).inject( this.$box );
			
			this.$nextLink.addEvent( 'click', this.changeImage.bindWithEvent( this, 1 ) );
		}
		
		
		// Stage
		this.$stage = new Element( 'div', { 'class': 'ui-images-stage' } ).inject( this.$box );
		this.$bottom = new Element( 'div', { 'class': 'ui-images-bottom' } ).inject( this.$box );
		
		
		// Close
		this.$closeButton = new Element( 'div', {
		
			'class': 'ui-images-close',
			
			events: {
				click: this.close.bindWithEvent( this )
			}
			
		}).inject( this.$bottom );
		
		
		// Caption
		if ( this.options.captions )
			this.$caption = new Element( 'div', { 'class': 'ui-images-caption' } ).inject( this.$bottom );
		
		
		// Counter
		if ( this.images.length )
			this.$counter = new Element( 'div', { 'class': 'ui-images-counter' } ).inject( this.$bottom );
			
		
		// Active flag (prevents keyboard and mouse events from firing unnecessarily)
		this._active = false;
		
		
		// Mouse Wheel
		if ( this.options.mouseWheel )
			document.addEvent( 'mousewheel', this.mouseWheelListener.bindWithEvent( this ) );
		
		
		// Keyboard
		if ( this.options.keyboardShortcuts )
			document.addEvent( 'keydown', this.keyboardListener.bindWithEvent( this ) );
		
		
		// Set flag so we don't setup again
		this._setup = true;
		
		
		// Open image
		if ( image )
			this.open( image );
	},
	
	toElement: function()
	{
		return this.$el;
	},
	
	open: function( image )
	{
		if ( !this._setup )
		{
			this.setup( image );
			return;
		}
	
		this._active = true;
		
		var size = window.getSize();
		var scroll = window.getScroll();
		var scrollSize = window.getScrollSize();
		
		// If images are bigger than 768 high, they might get chopped off, so move them to the top
		var offset = Math.round( ( size.y < 768 ) ? size.y / 36 : size.y / 10 );
		
		var top = scroll.y; // + offset;
		
		this.$overlay.setStyles({
			opacity: 0,
			display: 'block',
			width: scrollSize.x,
			height: scrollSize.y
		});
		
		// Code before fancy postioning and sizing
		/* this.image.setStyles({
			display: 'block',
			top: top
		}); */
		
		// Retrieve the original size of the image and set it
		var dimensions = image.get( 'dimensions' );
		
		this.$box.setStyles({
			display: 'block',
			width: dimensions.width,
			height: dimensions.height
		});
		
		// Position
		var $el = image.$el;
		
		if ( image.$el.getElement( 'img' ) )
			$el = image.$el.getElement( 'img' );
		
		this.$box.position({ relativeTo: $el });
		
		// Code before fancy postioning and sizing
		// marginLeft: -( originalSize.x / 2 )
		
		this.$box.setOpacity( 0 );
		
		new Fx.Tween( this.$overlay, {
			property: 'opacity'
		}).start( 0.8 );
		
		this.startLoad( image );
		
		// Resize event
		window.addEvent( 'resize', function() {
		
			this.resize();
		
		}.bind( this ));
	},
	
	resize: function()
	{
		if ( !this._active )
			return;
		
		var scrollSize = window.getScrollSize();
		
		this.$overlay.setStyles({
			width: scrollSize.x,
			height: scrollSize.y
		});
	},
	
	startLoad: function( image, preload )
	{
		if ( !image )
			return;
		
		var $image = new Asset.image( image.get( 'src' ), {
		
			onload: function() {
			
				if ( !preload && this.image == image ) this.nextEffect();
			
			}.bind( this )
		
		});
		
		if ( !preload )
		{
			this.$box.addClass( 'ui-images-loading' );
			this.$stage.setStyle( 'display', 'block' );
			this.$stage.empty();
			
			this.$bottom.setStyle( 'opacity', 0 );
			
			if ( this.options.mode == 'gallery' )
			{
				this.$prevLink.setStyle( 'display', 'none' );
				this.$nextLink.setStyle( 'display', 'none' );
			}
			
			this.image = image;
			this.$image = $image;
			this.caption = image.get( 'caption' );
			this._index = this.images.indexOf( image );
			
			this.step = 1;
		}
	},
	
	keyboardListener: function( event )
	{
		if ( !this._active )
			return;
		
		if ( event.key != 'f5' )
			event.preventDefault();
		
		switch ( event.key )
		{
			case 'esc': case 'x': case 'q': this.close(); break;
			case 'b': case 'p': case 'left': this.changeImage( event, -1 ); break;
			case 'f': case 'n': case 'right': this.changeImage( event, 1 );
		}
	},
	
	mouseWheelListener: function( event )
	{
		if ( !this._active )
			return;
		
		if ( event.wheel > 0 )
			this.changeImage( event, -1 );
		
		if ( event.wheel < 0 )
			this.changeImage( event, 1 );
	},
	
	changeImage: function( event, step )
	{
		this._changed = true;
	
		event.preventDefault();
		
		var index = this._index + step,
			image = this.images[ index ],
			count = this.images.length;
		
		if ( !image )
			index == count ? image = this.images[0] : image = this.images[ count - 1 ];
		
		if ( !image )
			return false;
		
		for ( var f in this.effects )
			this.effects[f].cancel();
		
		this.startLoad( image );
	},
	
	nextEffect: function()
	{
		switch( this.step++ )
		{
			case 1:
			
				// $log( '1) Expand from image' );
			
				var w = this.$image.width + this.options.padding * 2,
					h = this.$image.height + this.options.padding * 2;
				
				if ( this.options.mode == 'gallery' )
				{
					this.$prevLink.setStyle( 'height', h );
					this.$nextLink.setStyle( 'height', h );
				}
				
				// var marginLeft = -( this.image.width / 2 ),
				// marginTop = ( ( window.getSize().y - this.image.height ) / 2 );
				
				var scrollTop = window.getScrollTop();
				
				var marginLeft = ( ( window.getSize().x - this.$image.width ) / 2 ),
					marginTop = ( ( window.getSize().y - this.$image.height ) / 2 ) + scrollTop;
				
				// marginTop: marginTop
				// marginLeft: marginLeft
				
				var effect = this.options.effect;
				
				if ( this.options.ease && effect != 'linear' )
					effect += ':' + this.options.ease;
				
				var duration = this.options.durations.resize;
				
				if ( !this._changed )
					duration = this.options.durations.open;
				
				
				new Fx.Morph( this.$box, {
					duration: duration,
					transition: this.options.effect,
					onComplete: this.nextEffect.bind( this )
				}).start({
					width: w,
					height: h,
					left: marginLeft,
					top: marginTop,
					opacity: 1
				});
				
			break;
				
			case 2:
			
				// $log( '2) Fade in' );
				
				this.$box.removeClass( 'ui-images-loading' );
				this.$stage.setStyle( 'opacity', 0 );
				
				this.$image.setStyle( 'margin', this.options.padding );
				this.$image.inject( this.$stage );
				
				new Fx.Tween( this.$stage, {
					property: 'opacity',
					duration: this.options.durations.fade,
					onComplete: this.nextEffect.bind( this )
				}).start( 1 );
			
			break;
			
			case 3:
				
				// $log( '3) Show next/previous, captions' );
				
				if ( this.options.mode == 'gallery' )
				{
					this.$prevLink.setStyle( 'display', 'block' );
					this.$nextLink.setStyle( 'display', 'block' );
				}
				
				if ( this.options.captions && this.options.animateCaption )
				{
					if ( this.options.mode == 'gallery' && this.options.counter )
					{
						var total = this.images.length;
						var num = this._index + 1;
						var counterText = this.options.counter;
						
						counterText = counterText.replace( /\{num\}/, num );
						counterText = counterText.replace( /\{total\}/, total );
						
						this.$counter.set( 'text', counterText);
					}
					
					if ( this.$caption )
						this.$caption.set( 'text', this.caption );
					
					var height = this.$bottom.getStyle( 'height' ).toInt();
					
					this.$bottom.setStyles({
						opacity: 1,
						top: -( height )
					});
					
					new Fx.Tween( this.$bottom, {
						property: 'top',
						duration: this.options.durations.caption,
						onComplete: this.nextEffect.bind( this )
					}).start( 0 );
				}
				
			break;
			
			case 4:
			
				// $log( '4) Start preloading images' );
				
				this.startLoad( this.images[ this._index -1 ], true );
				this.startLoad( this.images[ this._index +1 ], true );
			
			break;
		}
	},
	
	close: function()
	{
		this.$box.setStyle( 'display', 'none' );
		
		this.$overlay.get( 'tween' )
			.start( 'opacity', 0 );
		
		this._active = false;
		
		this._changed = false;
	}
});

UI.Images.Image = new Class({
	
	Implements: [ Events, Options ],
	
	options: {
	
		src: null,
		
		caption: '',
		
		width: null,
		height: null
	
	},
	
	initialize: function( image, images, options )
	{
		this.setOptions( options );
		
		var $el = this.$el = image;
		
		if ( !this.options.src )
			this.options.src = image.get( 'href' );
		
		if ( !this.options.caption )
			this.options.caption = ( image.get( 'title' ) || ( image.getElement( 'img' ) ? image.getElement( 'img' ).get( 'alt' ) : '' ) );
		
		$el.addEvent( 'click', (function( event ) {
		
			event.stop();
		
			images.open( this );
		
		}.bind( this )));
	},
	
	toElement: function()
	{
		return this.$field;
	},
	
	set: function( what, value )
	{
		switch ( what )
		{
			//
		}
		
		return this;
	},
	
	get: function( what )
	{
		switch ( what )
		{
			case 'src':
				return this.options.src;
				
			case 'caption':
				return this.options.caption;
				
			case 'dimensions':
				
				var $el = this.$el;
				
				if ( this.$el.getElement( 'img' ) )
					$el = this.$el.getElement( 'img' );
				
				return $el.getDimensions({ styles: [ 'padding' ] });
				
			case 'position':
			
				var $el = this.$el;
				
				if ( this.$el.getElement( 'img' ) )
					$el = this.$el.getElement( 'img' );
				
				return $el.getPosition();
		}
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-images' ).each( function( $el ) {
	
		var options = $el.getDataFromComment() || {};
		
		if ( !options.mode )
			$extend( options, { mode: 'gallery' } );
		
		$el.store( 'image', new UI.Images( $el, options ) );
	
	});
	
	$$( '.ui-image-popup' ).each( function( $el ) {
		$el.store( 'image', new UI.Images( $el, $el.getDataFromComment() ) );
	});

});


/*
Class: UI.Form
*/
UI.Form = new Class({
	
	Implements: Options,
	
	options: {
		
		target: null,
		
		fields: [],
		
		submitMethod: null,
		
		submitBtn: null,
		feedbackEl: null,
		activityEl: null,
		messageEl: null,
		
		sections: false,
		
		fadeFeedback: false,
		
		displayFeedback: true,
		
		clearFeedback: true,
		
		submitForm: true,
		
		ignoreFields: false,
		
		detectFields: false,
		
		scrollToError: true,
		
		scrollToFeedback: false,
		
		refreshPage: null,
		
		redirectToPage: null,
		
		keyboardShortcuts: true,
		
		validationMode: 'submit',
		
		hideHints: false,
		hideValidHints: false,
		
		resetFields: false,
		
		focusField: null,
		
		hideMenu: false,
		closePopup: false,
		
		splitMemberNameFrom: null,
		memberNameCheckPopup: null,
		
		invalidMessage: '',
		
		validFieldMessage: '',
		invalidFieldMessage: '',
		validatingFieldMessage: '',
		
		onSuccess: null,
		onFail: null,
		onPreSubmit: null,
		onValidation: null,
		onSubmit: null,
		onTransmission: null
		
	},
	
	initialize: function( target, options )
	{
		var $el = this.$el = $( target );
		
		if ( !options ) {
			$log( 'A form was detected without any valid options.', $el );
			return;
		}
		
		this.setOptions( options );
		
		if ( this.options.target )
			this.options.submitMethod = 'ajax';
		
		this._status = 'waiting';
		this._preSubmitQueue = [];
		
		// Submit Button
		var $submit = this.$submit = [];
		
		if ( this.options.submitBtnQuery )
		{
			var $submit = this.$submit = $el.getElements( this.options.submitBtnQuery );
		}
		else if ( this.options.submitBtn )
		{
			var $submit = this.$submit = [ $( this.options.submitBtn ) ];
			
			if ( !$submit && this.options.submitBtn )
				alert( 'Submit button specified in form options is invalid, unable to find: [#' + this.options.submitBtn + ']' );
		}
		else if ( target.getElement( 'input[type=submit]' ) )
		{
			var detectedSubmitBtn = target.getElement( 'input[type=submit]' );
			
			if ( detectedSubmitBtn )
				$submit = this.$submit = this.options.submitBtn = [ detectedSubmitBtn ];
		}
		else
		{
			var $submit = this.$submit = $el.getElements( '.ui-form-submit' );
		}
		
		// Bind form to submit
		$el.addEvent( 'submit', (function(e) {
		
			e.stop();
			
			this.validateForm();
		
		}.bind( this )));
		
		
		// Feedback Element (optional)
		var $feedback = this.$feedback = this.options.feedbackEl = $( this.options.feedbackEl );
		
		if ( this.options.displayFeedback && !this.options.feedbackEl )
			$feedback = this.$feedback = this.options.feedbackEl = $el;
		
		
		// Activity (optional, tries to find it based on a class)
		var $activity = this.$activity = $el.getElements( '.ui-form-activity' );
		
		if ( this.options.activityEl )
			$activity = this.$activity = [ $( this.options.activityEl ) ];
		
		
		// Message (optional, tries to find it based on a class)
		var $message = this.$message = $el.getElement( '.ui-form-message' );
		
		if ( this.options.messageEl )
			$message = this.$message = $( this.options.messageEl );
		
		
		// Define field map
		var fieldMap = this.fieldMap = new Hash();
		
		
		// Set validation errors
		this._validationErrors = [];
		
		
		// Gather
		var gather = (function() {
		
			/* Method 1: Array (fields array supplied to form) */
			var suppliedFields = this.options.fields;
				suppliedFields.each( (function(f) { this.addField(f) }.bind( this )));
			
			/* Method 2: Class (gather values from fields with class .ui-field) */
			var classFields = this.$el.getElements( '.ui-field' );
				classFields.each( (function(f) { this.addField(f) }.bind( this )));
			
			/* Method 3: Loop all other fields (look for inputs that haven't been detected yet, only if detectFields is true) */
			if ( this.options.detectFields )
			{
				var otherFields = this.$el.getElements( 'input,textarea,select' );
					otherFields.each( (function(f) { this.addField(f) }.bind( this )));
			}
			
			// Store fields
			var fields = this.fields = fieldMap;
			
			// Check to see if we have any file fields, and change the submit method if need be
			fields.each( (function(f) {
			
				// TW: 2011-3-17 - Disabled, no longer support iframes right now
				// if ( f.type == 'file' )
					// this.options.submitMethod = 'iframe';
			
			}.bind( this )));
		
		}.bind( this ));
		
		
		// Gather fields together
		gather();
		
		
		// Look for form sections
		this.initSections();
		
		
		// Look for form repeaters
		this.initRepeaters();
		
		
		// Add event to submit button
		if ( $submit.length )
		{
			$submit.each( (function( $s ) {
			
				$s.addEvent( 'click', (function(e) {
				
					e.stop();
					this.validateForm();
				
				}.bind( this )));
			
			}.bind( this )));
		}
		
		
		// Focus a field
		if ( this.options.focusField && Browser.Engine.name != 'trident' )
		{
			var focusField = this.fields[ this.options.focusField ];
			
			if ( focusField )
				focusField.$field.focus();
		}
		
		
		// Detect pages
		if ( this.options.pages )
		{
			this.initPages();
		}
	
	},
	
	addField: function( f, options )
	{
		var fieldMap = this.fieldMap;
		
		var $el = null,
			key = null;
			
		var options = options || false;
		
		switch( $type(f) )
		{
			case 'object':
			
				$el = this.$el.getElement( 'input[name=' + f.key + ']' );
				
				if ( !options )
					options = f;
				
				$extend( options, { fieldOnly: true } );
				
				key = f.key;
				
			break;
			
			case 'element':
			
				$el = f;
				
				// Simple field
				var isFieldTag = /^(input|select|textarea)$/i.test( $el.get( 'tag' ) );
				
				if ( isFieldTag )
				{
					if ( !options )
						options = {};
					
					$extend( options, { fieldOnly: true } );
					key = f.getProperty( 'name' );
				}
				
				// Complex field
				else
				{
					$el = f;
					
					if ( !options )
						options = $el.getDataFromComment() || {};
					
					if ( options.ignoreField )
						return;
					
					var $fields = $el.getElements( 'textarea,select,input' );
					
					options.fields = $fields;
					
					if ( options.key )
					{
						key = options.key;
					}
					else
					{
						switch( $type( $fields ) )
						{
							case 'array':
								
								var firstField = $fields[0];
								
								if ( !firstField )
								{
									alert( "A 'ui-field' element in this form is missing its related element (input, textarea, select).\n\nScan over the fields to ensure all 'ui-fields' have the required elements.\n\n The form may not work properly without it." );
									return;
								}
								
								key = firstField.getProperty( 'name' ); // Base the key off the first field
							
							break;
							
							default: // Single field
								key = $fields.getProperty( 'name' );
						}
					}
				}
				
			break;
		}
		
		// Create the field (only if it doesn't exist)
		if ( key && !fieldMap[ key ] )
			fieldMap[ key ] = new UI.Form.Field( $el, this, options );
		
		return fieldMap[ key ];
	},
	
	initRepeaters: function()
	{
		var $el = this.$el;
		
		var repeaters = $el.getElements( '.ui-form-repeater' );
		
		this.repeaters = {};
		
		var fields = this.fields;
		
		var processRepeaters = (function( $r ) {
		
			// Retrieve options
			var options = $r.getDataFromComment() || {};
			
			if ( !options )
			{
				$log( 'No repeater options found, they are required for element:', $r );
				return;
			}
			
			// Replace pattern
			var replacePattern = options.replacePattern;
			
			if ( !replacePattern )
			{
				$log( 'No replace pattern found, it is a required option for repeater:', $r );
				return;
			}
			
			// Get the original fields we want to repeat
			var $original = $r.getElement( '.ui-form-repeater-fields' );
			
			if ( !$original )
			{
				$log( 'No fields found, you must supply an element with the class [ui-form-repeater-fields] of fields with the class [ui-repeater-field] to base the repeater off.', $r );
				return;
			}
			
			// Find the toolbar
			var $toolbar = $r.getElement( '.ui-form-repeater-toolbar' );
			
			if ( !$toolbar )
			{
				$log( 'No toolbar found, you must include a toolbar with the class [ui-form-repeater-toolbar] with buttons to control the repeater.', $r );
				return;
			}
			
			// Get the buttons to control the repeater
			var $add = $r.getElement( '.ui-form-repeater-add' ),
				$remove = $r.getElement( '.ui-form-repeater-remove' );
			
			// Set initial count
			var repeatCount = 0;
			
			// Add element to repeater object
			var repeater = this.repeaters[ options.id ] = {
				$el: $r,
				repeats: new Hash()
			};
			
			// Function to add each instance of a repeater
			var addRepeat = (function() {
				
				// Prevent the user from adding more fields than they should
				if ( options.max && repeatCount >= options.max )
					return;
				
				repeatCount++;
				
				// Clone the original set of fields
				var $repeat = $original.clone();
				
				$repeat.removeClass( 'ui-form-repeater-fields' ).addClass( 'ui-form-repeater-repeat' );
				
				$repeat.inject( $toolbar, 'before' ).show();
				
				// Replace placeholder classes on elements
				this.initRepeaterFields( $repeat );
				
				// Add fields in repeat to the main fields array
				var classFields = $repeat.getElements( '.ui-field' );
				
				var detectedFields = [];
				
				// Rename name property with replace pattern
				classFields.each( (function( field ) {
					
					// Fields
					var fields = field.getElements( 'textarea,select,input' );
					
					fields.each( function(f) {
					
						var name = f.getProperty( 'name' ).replace( replacePattern, repeatCount );
						f.setProperty( 'name', name );
					
					});
					
					// Labels
					var labels = field.getElements( 'label' );
					
					labels.each( function(l) {
					
						var name = l.getProperty( 'for' ).replace( replacePattern, repeatCount );
						l.setProperty( 'label', name );
					
					});
					
					// Add the field to the main object
					var theField = this.addField( field );
					
					detectedFields.push( theField.name );
					
				}.bind( this )));
				
				// Make sure our fields object is updated with the latest data
				this.fields = this.fieldMap;
				
				// Push to array
				repeater.repeats[ repeatCount ] = { $el: $repeat, fields: detectedFields };
				
				handleButtons();
				
				// Init any ui classes for the new element
				UI.initClasses( $repeat );
			
			}.bind( this ));
			
			// Function to remove an instance of a repeater
			var removeRepeat = (function() {
			
				// Prevent the user from remove more fields than they should
				if ( options.min && repeatCount <= options.min )
					return;
				
				var lastRepeat = repeater.repeats[ repeatCount ];
				
				if ( !lastRepeat )
					return;
				
				lastRepeat.$el.destroy();
				
				lastRepeat.fields.each( (function(f) {
				
					this.fieldMap.erase( f );
				
				}.bind( this )));
				
				repeater.repeats.erase( repeatCount );
				
				// Make sure our fields object is updated with the latest data
				this.fields = this.fieldMap;
				
				repeatCount--;
				
				handleButtons();
				
			}.bind( this ));
			
			// Function to handle buttons when adding and removing
			var handleButtons = (function() {
			
				// Prevent the user from adding more fields than they should
				if ( $add && options.max && repeatCount >= options.max )
					$add.hide();
				else if ( $add )
					$add.show();
				
				// Prevent the user from adding more fields than they should
				if ( $remove && ( options.min && repeatCount <= options.min || !repeatCount ) )
					$remove.hide();
				else if ( $remove )
					$remove.show();
			
			}.bind( this ));
			
			// Add inital repeat (if desired)
			if ( options.min )
				addRepeat();
			
			// Add button
			if ( $add )
			{
				$add.addEvent( 'click', function() {
					addRepeat();
				});
			}
			
			// Remove button
			if ( $remove )
			{
				$remove.addEvent( 'click', function() {
					removeRepeat();
				});
			}
		
		}.bind( this ));
		
		// Loop over repeaters
		repeaters.each( (function( $r ) {
			
			processRepeaters( $r );
			
		}));
	
	},
	
	initRepeaterFields: function( $repeat )
	{
		var repeaterFields = $repeat.getElements( '.ui-repeater-field' );
			repeaterFields.removeClass( 'ui-repeater-field' ).addClass( 'ui-field' );
		
		var suggestFields = $repeat.getElements( '.ui-repeater-suggest' );
			suggestFields.removeClass( 'ui-repeater-suggest' ).addClass( 'ui-suggest' );
	},
	
	initSections: function()
	{
		var $el = this.$el;
		
		var sections = $el.getElements( '.ui-form-section' );
		
		this.sections = {};
		this.conditions = [];
		
		var fields = this.fields;
		
		var processSection = (function( $s ) {
		
			// Retrieve options
			var options = $s.getDataFromComment() || {};
			
			if ( !options )
			{
				$log( 'No section options found, they are required for element:', $s );
				return;
			}
			
			// Add element to sections object
			var section = this.sections[ options.id ] = {
				$el: $s
			};
			
			// Build up conditions
			var condition = {
				values: options.showIf, // for now we assume its just showIf
				_last_: {}, // add last fields eventually (should map the fields in values struct)
				onChange: function( b, values ) {
				
					$s[ b ? 'show' : 'hide' ]();
				
				}
			};
			
			// Convert to an array if its not
			var wasSingleValue = false;
			
			if ( $type( condition.values ) != 'array' )
			{
				wasSingleValue = true;
				
				condition.values = [ condition.values ];
			}
			
			// Checks the conditions values against the last form values to determine if the condition is valid or not
			var checkCondition = function() {
			
				var checkValues = condition.values;
				
				// If either statements are true
				var oneSetOfConditionsIsValid = false;
				
				checkValues.each( (function( values ) {
					
					var lastValues = condition._last_;
					
					// If all statements are true
					var conditionsAreValid = true;
					
					$H( values ).each( function( requiredValue, key ) {
						
						// Abort if we already have a condition that is invalid
						if ( !conditionsAreValid )
							return;
						
						// Check the last value against the required value
						var lastValue = lastValues[ key ];
						
						if ( lastValue == requiredValue )
						{
							conditionsAreValid = true;
						}
						else
						{
							conditionsAreValid = false;
						}
					
					});
					
					// If at least one set of conditions is valid, we can show it
					if ( conditionsAreValid )
						oneSetOfConditionsIsValid = true;
				
				}));
				
				if ( condition.onChange )
				{
					condition.onChange( oneSetOfConditionsIsValid );
				}
			
			};
			
			// Add section to the fields inside the section, TODO: expand to support non ui fields
			var classFields = $s.getElements( '.ui-field' );
			
			classFields.each( function(f) {
			
				var theField = f.retrieve( 'field' );
				
				if ( !theField )
				{
					$log( 'No field class found for element, could because of duplicated [name] property somewhere else on the form.', f );
					return;
				}
				
				theField.section = options.id;
			
			});
			
			// Add inital values condition and events to the fields
			var values = $H( condition.values );
			
			condition.values.each( (function( values ) {
			
				$H( values ).each( function( v, k ) {
				
					var field = fields[ k ];
					
					if ( !field )
					{
						$log( 'Field inside section not be found!', k );
						return;
					}
					
					// Add events to field elements
					var $fields = field.$fields,
						type = field.type;
						
					if ( !$fields.length )
						$fields = [ field.$field ];
					
					// Don't add the event twice
					if ( $H( condition._last_ ).has( k ) )
						return;
					
					// Store inital value
					condition._last_[ k ] = field.get( 'value' );
					
					$fields.each( function( $f ) {
						
						switch( type )
						{
							case 'checkbox':
							case 'radio':
							
								$f.addEvents({
								
									'click': (function(e) {
									
										// Get the field value
										var fieldValue = field.get( 'value' );
										
										// Set the last value
										condition._last_[ k ] = fieldValue;
										
										// Check all the conditions in case its now true/false
										checkCondition();
									
									}.bind( this ))
								
								});
								
							break;
							
							case 'select-one':
								
								$f.addEvents({
								
									'change': (function(e) {
									
										// Get the field value
										var fieldValue = field.get( 'value' );
										
										// Set the last value
										condition._last_[ k ] = fieldValue;
										
										// Check all the conditions in case its now true/false
										checkCondition();
									
									}.bind( this ))
								
								});
							
							break;
						}
					
					});
				
				}.bind( this ));
			
			}.bind( this )));
			
			// Add conition to form conditions
			this.conditions.push( condition );
			
			// $log( condition );
			// $log( section );
		
		}.bind( this ));
		
		// Loop over sections
		sections.each( (function( $s ) {
			
			processSection( $s );
			
		}));
	
	},
	
	initPages: function()
	{
		var $el = this.$el;
		
		var pages = $el.getElements( '.ui-form-page' ); // Add ability to customise
		
		if ( !pages.length )
		{
			$log( 'No form pages detected.' );
			return;
		}
		
		
		// Set the first page
		this._page = pages[0];
		
		
		// Scrolling function
		var scrollToPage = (function() {
		
			var pageStart = this._page.getPosition();
			
			window.document.body.scrollTo( 0, pageStart.y );
		
		}.bind( this ));
		
		
		// Check to see that the current page is still visible (corrects itself if not, allows us to view other pages with specific buttons)
		var checkCurrentPage = (function() {
		
			if ( !this._page.isVisible() )
			{
				pages.each( function(p) {
				
					if ( p.isVisible() )
						this._page = p;
				
				}.bind( this ));
			}
		
		}.bind( this ));
		
		
		// Fire any custom events
		var fireEvents = (function( options ) {
		
			if ( options && options.onDisplay )
				UI.doCallback( options.onDisplay );
		
		}.bind( this ));
		
		
		// Get the next page
		var setNextPage = (function( backwards, index ) {
		
			checkCurrentPage();
			
			this._page.hide();
			
			( backwards ? index-- : index++ );
			
			if ( pages[ index ] )
				this._page = pages[ index ];
			else
				$log( 'No next/prev page found.' );
			
			// Only show the page if we meet certain conditions, otherwise skip it
			var pageOptions = this._page.getDataFromComment() || {};
			
			if ( pageOptions.skipIf )
			{
				var formData = this.gatherData(),
					values = pageOptions.skipIf,
					skipToNextPage = false;
				
				$H( values ).each( function( v, k ) {
					
					var lv = formData[ k ];
					
					skipToNextPage = ( lv == v );
				
				});
				
				// Increment index by one (skip the page)
				if ( skipToNextPage )
				{
					$log( 'Next page is not applicable due to conditions based on form data, skipping...' );
					setNextPage( backwards, index );
					return;
				}
			}
			
			this._page.show();
			
			scrollToPage();
			
			fireEvents( pageOptions );
		
		}.bind( this ));
		
		
		// Next page
		var nextPage = (function( options, index ) {
		
			var pageIsValid = this.validatePage( this._page );
			
			if ( pageIsValid )
				setNextPage( false, index );
		
		}.bind( this ));
		
		
		// Previous page
		var previousPage = (function( options, index ) {
		
			setNextPage( true, index );
		
		}.bind( this ));
		
		
		// Init the buttons (if any)
		pages.each( function( p, i ) {
		
			var o = p.getDataFromComment() || {};
			
			var $next = p.getElement( '.ui-form-next' ),
				$previous = p.getElement( '.ui-form-previous' );
			
			// Handle next button
			if ( $next )
			{
				$next.addEvent( 'click', function(e) {
					e.stop();
					nextPage( o, i );
				}.bind( this ));
			}
			
			// Handle previous button
			if ( $previous )
			{
				$previous.addEvent( 'click', function(e) {
					e.stop();
					previousPage( o, i );
				}.bind( this ));
			}
		
		}.bind( this ));
	},
	
	validatePage: function( page )
	{
		var form = this.form;
		
		var fields = [];
		
		var options = page.getDataFromComment() || {};
		
		
		var classFields = page.getElements( '.ui-field' );
		
		// TODO: Merge this into the main page validation array so we can support remote validation calls etc
		// TODO: Add support for detecting non ui-fields when validating a page, see normal detection code for more details;
		
		classFields.each( function(f) {
		
			if ( f.retrieve( 'field' ) )
				fields.push( f.retrieve( 'field' ) );
		
		});
		
		// $log( 'Fields on this page:', fields );
		
		if ( !fields.length )
			return true;
		
		this._validationErrors = [];
		
		fields.each( (function(f) {
			
			// We always need to check if a field is valid even if we have validated it before, otherwise the user could have changed the value.
			if ( f.options.validate || f.options.required )
				f.validate();
			
		}.bind( this )));
		
		// Determine whether the page is valid or not
		if ( this._validationErrors.length )
		{
			if ( options.onValidation )
				UI.doCallback( options.onValidation, this._validationErrors );
			
			$log( this._validationErrors );
			
			return false;
		}
		else
		{
			if ( options.onValidation )
				UI.doCallback( options.onValidation, false );
			
			return true;
		}
	
	},
	
	validateForm: function()
	{
		// If using pages, validate the final page first
		if ( this.options.pages )
		{
			var validPage = this.validatePage( this._page );
		
			if ( !validPage )
				return;
		}
		
		if ( this._status == 'validating' || this._status == 'submitting' )
		{
			$log( 'The form is already in a validating or submitting state, please wait...' );
			return;
		}
		
		this._status = 'validating';
		
		this._validationErrors = [];
		
		this.fields.each( (function(f) {
			
			// We always need to check if a field is valid even if we have validated it before, otherwise the user could have changed the value.
			if ( f.options.validate || f.options.required )
				f.validate();
			
		}.bind( this )));
		
		this.preSubmitForm();
	
	},
	
	preSubmitForm: function()
	{
		// $log( 'Pre Submit Form...' );
		
		// Call custom function and pass errors (if it exists)
		if ( this.options.onPreSubmit )
			UI.doCallback( this.options.onPreSubmit );
		
		this.toggleForm();
		
		// Check if theres any actions queued first
		if ( this._preSubmitQueue.length )
		{
			// Show activity
			if ( this.$activity )
			{
				this.$activity.each( function( $a ) {
					$a.show();
				});
			}
			
			$log( 'There are actions still queued, waiting for completion...' );
			
			return;
		}
		
		// Otherwise check if we have any validation errors
		else if ( this._validationErrors.length )
		{
			$log( 'There are validation errors:', this._validationErrors );
			
			// Scroll to first error
			if ( this.options.scrollToError )
			{
				var firstError = this._validationErrors[0],
					$errorEl = firstError.$el;
				
				if ( $errorEl )
					new Fx.Scroll( document.body, {
						offset: { x: 0, y: -80 }
					}).toElement( $errorEl ).chain( function() { firstError.$field.focus() } );
			}
			
			// Show message
			if ( this.$message )
				this.$message.show().set( 'html', ( this.options.invalidMessage ? this.options.invalidMessage : 'Please ensure all fields are completed.' ) );
			
			// Call custom function and pass errors (if it exists)
			if ( this.options.onValidation )
				UI.doCallback( this.options.onValidation, this._validationErrors );
			
			// Hide activity
			if ( this.$activity )
			{
				this.$activity.each( function( $a ) {
					$a.hide();
				});
			}
			
			this.toggleForm( true );
			
			this._status = 'waiting';
			
			// Call custom function (if it exists)
			if ( this.options.onSubmit )
				UI.doCallback( this.options.onSubmit, false );
		}
		else
		{
			// Call custom function (if it exists)
			if ( this.options.onSubmit )
				UI.doCallback( this.options.onSubmit, true );
			
			// Call custom function and pass no errors (if it exists)
			if ( this.options.onValidation )
				UI.doCallback( this.options.onValidation, false );
			
			// Submit form
			if ( this.options.submitForm )
				this.submitForm();
		}
	},
	
	checkSubmitQueue: function( remKey )
	{
		// $log( 'Removing [' + remKey + '] from submit queue.' );
		
		if ( remKey )
			this._preSubmitQueue.erase( remKey );
		
		// If the submit queue is empty, pre submit the form again
		if ( !this._preSubmitQueue.length )
		{
			if ( this._status == 'validating' )
			{
				$log( 'Submit Queue is empty, attempting to submit again...' );
				this.preSubmitForm();
			}
			else
			{
				$log( 'Submit Queue is empty with no form submit detected.' );
			}
		}
	
	},
	
	toggleForm: function( revert )
	{
		if ( revert )
		{
			this.$el.removeClass( 'ui-form-processing' );
			
			if ( this.$activity )
			{
				this.$activity.each( function( $a ) {
					$a.hide();
				});
			}
			
			this.$submit.each( (function( $s ) {
			
				if ( $s.retrieve( 'button' ) )
					$s.retrieve( 'button' ).enable();
			
			}.bind( this )));
		}
		else
		{
			this.$el.addClass( 'ui-form-processing' );
			
			if ( this.$activity )
			{
				this.$activity.each( function( $a ) {
					$a.show();
				});
			}
			
			this.$submit.each( (function( $s ) {
			
				if ( $s.retrieve( 'button' ) )
					$s.retrieve( 'button' ).disable();
			
			}.bind( this )));
		}
	},
	
	submitForm: function()
	{
		var $el = this.$el,
			$activity = this.$activity,
			$message = this.$message,
			$submit = this.$submit,
			$feedback = this.options.feedbackEl;
		
		// $log( 'Submitting form...' );
		
		// Check to see if we have already submitted the form
		if ( this._status == 'submitting' )
		{
			// $log( 'The form is already in a submitting state, please wait...' );
			return;
		}
		
		this._status = 'submitting';
		
		// Call custom function (if it exists)
		if ( this.options.onTransmission )
			UI.doCallback( this.options.onTransmission );
		
		if ( $message )
			$message.hide();
		
		// Gather data
		this.gatherData();
		
		// Debug
		if ( this.options.debug )
		{
			$log( 'Data:', this.data );
		}
		
		// TW: Disabled as it should be optional now we can submit via post
		// If theres no target, don't call any actions
		/* if ( !this.options.target )
		{
			this.formCompleted();
			return;
		}
		*/
		
		// Add some custom duplicates (for now its just file handling)
		this.fields.each( (function(f) {
		
			var type = f.options.type;
			
			switch( type )
			{
				case 'file':
				
					var name = f.name,
						$field = f.$field;
					
					var fieldData = this.data[ name ];
					
					if ( !fieldData )
						return;
					
					this.data[ name + '_fileName' ] = fieldData;
					
					if ( this.options.submitMethod == 'iframe' )
						new Element( 'input', { type: 'hidden', name: name + '_fileName', value: fieldData } ).inject( $el, 'top' );
				
				break;
			}
		
		}.bind( this )));
		
		// Call action
		switch( this.options.submitMethod )
		{
			case 'iframe':
				
				var key = $time();
				
				// Add a store for hidden fields
				this.hidden = new Hash();
				
				// Process some fields so the iframe gets the proccesed data
				this.fields.each( (function(f) {
				
					var type = f.options.type;
					
					switch( type )
					{
						case 'textarea':
						case 'date':
						
							var name = f.name,
								$field = f.$field;
							
							if ( type == 'textarea' && !$field.hasClass( 'ui-wysiwyg' ) )
								return;
							
							var fieldData = this.data[ name ];
							
							if ( !fieldData )
								return;
							
							$field.setProperty( 'name', '_' + name + '_' );
							
							this.hidden[ name ] = {
								clone: new Element( ( type == 'textarea' ? 'textarea' : 'input' ), { type: 'hidden', name: name, value: fieldData } ).inject( $el, 'top' ),
								original: $field
							}
							
						break;
						
						default:
						
						
						break;
					}
				
				}.bind( this )));
				
				// Create temporary iframe
				var $iframe = new Element( 'iframe', {
					id: key,
					name: key,
					src: 'about:blank',
					style: 'display: none;'
				}).inject( $el, 'top' );
				
				// Debug
				if ( this.options.debug )
					$iframe.setStyles({ 'width': '100%', 'height': '400px', 'border': '1px solid #B5B5B5', 'margin-bottom': '20px', 'display': 'block' });
				
				$el.setProperties({
					action: '/Remote.cfm' + '?__iframeCallbackId__=' + key + '&action=sparkle.parse&path=' + this.options.target,
					enctype: 'multipart/form-data',
					method: 'post',
					target: key
				});
				
				$log( 'Processed form data:', this.data );
				
				$el.submit();
			
			break;
			
			case 'ajax':
			
				// $log( 'Submit form with:', data );
				
				switch( this.options.target.toLowerCase() )
				{
					case 'signin':
					
						// The action doesn't return html, so don't show clear anything
						this.options.clearFeedback = false;
						
						UI.callAction({
							action: 'member.signin',
							data: this.data,
							onComplete: (function( rtn ) {
								
								this.processResponse( rtn );
								
							}.bind( this ))
						});
					
					break;
					
					default:
					
						UI.loadSparkle({
							path: this.options.target,
							data: this.data
						}).then( (function( rtn ) {
						
							this.processResponse( rtn );
						
						}.bind( this )));
						
					break;
				}
				
			break;
			
			default:
			
				// $log( 'Submitting form via post...' );
				
				$el.submit();
			
			break;
		}
	},
	
	gatherData: function()
	{
		// Gather values
		var data = this.data = {};
		
		// Ignore fields
		var ignoreFields = [];
		
		if ( this.options.ignoreFields )
			ignoreFields = this.options.ignoreFields.split( ',' );
		
		this.fields.each( (function( f, k ) {
		
			if ( ignoreFields.contains( f.name ) )
				return;
			
			var value = f.get( 'value' );
			
			switch( $type( value ) )
			{
				case 'string':
				case 'boolean':
				
					if ( ( this.options.splitMemberNameFrom && this.options.splitMemberNameFrom == f.name ) || f.options.splitAsMemberName )
					{
						if ( !value )
							return;
						
						var memberName = this.splitMemberName( value );
						
						$H( memberName ).each( function( string, key ) {
						
							if ( f.options.splitAsMemberName )
								data[ f.options.splitAsMemberName + '_' + key ] = string;
							else
								data[ key ] = string;
							
						});
					}
					else
						data[ k ] = value;
				
				break;
				
				case 'array':
					data[ k ] = value.toString();
				break;
				
				case 'object': // e.g group of checkboxes or date
				
					switch( f.options.type )
					{
						case 'dateSelects':
							return data[ k ] = value.cf;
						break;
					}
					
					switch( f.options.validate )
					{
						case 'date':
						case 'dob':
							return data[ k ] = value.cf;
						break;
						
						default:
							// $log( 'Detected object, adding the following group of data:', value );
							$extend( data, value );
					}
				
				break;
				
				default:
					$log( 'Warning: Unsupported data type from field detected: [' + $type( value ) + '], your form may be not be submitting correctly.' );
					return;
			}
		
		}.bind( this )));
		
		return data;
	
	},
	
	processResponse: function( rtn )
	{
		var $el = this.$el,
			$activity = this.$activity,
			$message = this.$message,
			$submit = this.$submit,
			$feedback = this.options.feedbackEl;
		
		if ( this._status != 'submitting' )
		{
			$log( 'Cannot process form, currently in [' + this._status + '] stage.' );
			return;
		}
		
		this._status = 'submitted';
		
		if ( rtn )
		{
			// $log( 'Form submission successful.', rtn );
			
			// Display feedback
			if ( $feedback )
			{
				if ( this.options.fadeFeedback )
				{
					$el.get( 'tween' )
						.setOptions({
							duration: 500,
							transition: 'quad:in:out'
						})
						.start( 'opacity', 0 )
						.chain( (function() {
							
							var formSize = $el.setStyle( 'overflow', 'hidden' ).getDimensions();
							
							$el.hide().setOpacity(1);
							
							if ( this.options.clearFeedback )
							{
								$feedback.empty();
								$feedback.set( 'html', rtn );
							}
							
							var fbSize = $feedback.setStyle( 'overflow', 'hidden' ).getDimensions();
							
							$feedback.setStyles({ height: formSize.y });
							
							$feedback.show().setOpacity(0);
							
							$feedback.get( 'tween' )
								.setOptions({
									duration: 500,
									transition: 'quad:in:out'
								})
								.start( 'height', fbSize.y )
								.chain( (function() {
									
									$feedback.get( 'tween' )
										.start( 'opacity', 1 );
									
									this.formCompleted();
									
								}.bind( this )));
						
						}.bind( this )));
				}
				else
				{
					$el.hide();
					
					if ( this.options.clearFeedback )
					{
						$feedback.empty();
						$feedback.set( 'html', rtn );
					}
					
					$feedback.show();
					
					this.formCompleted();
				}
			}
			else
				this.formCompleted();
			
			// Clean up any hidden fields (if we submitted with an iframe)
			if ( this.options.submitMethod == 'iframe' )
			{
				if ( this.hidden && this.hidden.getLength() )
				{
					this.hidden.each((function( f, k ) {
					
						f.clone.destroy();
						
						f.original.setProperty( 'name', k );
					
					}.bind( this )));
				}
			}
			
			// Refresh page
			if ( this.options.refreshPage )
				top.location.reload();
			
			// Redirect to page
			if ( this.options.redirectToPage )
				top.location.href = this.options.redirectToPage;
			
			// On Success function
			if ( this.options.onSuccess )
				UI.doCallback( this.options.onSuccess, { html: rtn, data: this.data } );
		}
		
		/*
		else
		{
			switch( this.options.target.toLowerCase() )
			{
				case 'signin':
				
					// Usually always going to be the same (for now, maybe unverified later)
					alert( 'Invalid username or password, please try again.' );
					
					this.resetForm();
				
				break;
				
				default:
				
					// $log( 'Form submission failed.' );
					
					if ( $message )
						$message.show().set( 'html', 'The form could not be submitted. ' + rtn.message );
					
					if ( this.options.onFail )
						UI.doCallback( this.options.onFail, rtn );
					
				break;
			}
		}
		*/
	},
	
	formCompleted: function()
	{
		$feedback = this.options.feedbackEl;
	
		this.resetForm();
		
		// Init UI Classes
		UI.initClasses( $feedback );
		
		// Scroll to feedback
		if ( this.options.scrollToFeedback )
		{
			if ( $feedback )
				new Fx.Scroll( document.body ).toElement( $feedback );
		}
		
		if ( this.options.hideMenu )
			this.hideMenu();
			
		if ( this.options.hidePopclosePopupup )
			this.closePopup();
	
	},
	
	resetForm: function()
	{
		var $el = this.$el,
			$activity = this.$activity,
			$message = this.$message,
			$submit = this.$submit,
			$feedback = this.options.feedbackEl;
		
		// Enable submit button (if its a button)
		this.$submit.each( (function( $s ) {
		
			if ( $s.retrieve( 'button' ) )
				$s.retrieve( 'button' ).enable();
		
		}.bind( this )));
		
		// Remove class from form
		$el.removeClass( 'ui-form-processing' );
		
		// Hide activity
		if ( this.$activity )
		{
			this.$activity.each( function( $a ) {
				$a.hide();
			});
		}
		
		// Set status message
		this._status = 'waiting';
		
		// Reset fields
		if ( this.options.resetFields )
			this.resetFields();
	},
	
	resetFields: function()
	{
		this.fields.each( function(f) {
			f.resetField();
		});
		
		this.$el.getElements( 'input[hint],textarea[hint]' ).each( function( $el ) {
			$el.retrieve( 'placeholder' ).updatePlaceholder();
		});
	},
	
	hideMenu: function()
	{
		var $curEl = this.$el,
			button = $( this.options.hideMenu ).retrieve( 'button' );
		
		(function() {
			
			var $menu = button.$target;
			
			$menu.get( 'tween' )
				.setOptions({
					duration: 150,
					transition: 'linear'
				})
				.start( 'opacity', 0 )
				.chain( (function() {
				
					$menu.setOpacity(1).hide();
					
					button.makeUnpressed();
				
				}.bind( this )));
			
			button._menuIsOpen = false;
		
		}).delay( 1500 );
		
	},
	
	closePopup: function()
	{
		(function() {
			
			var parents = this.$el.getParents();
			
			parents.each( function(p) {
			
				if ( p.hasClass( 'ui-popup' ) )
					p.retrieve( 'popup' ).close();
			
			});
		
		}.bind( this )).delay( 2500 );
		
	},
	
	splitMemberName: function( name )
	{
		var rtn = { firstName: '', lastName: '', salutation: '' };
		
		var a = name.split(' ');
		
		for ( var i = 0; i < a.length; i++ )
		{
			if ( !/[a-z]/.test( a[i] ) ) a[i] = a[i].toLowerCase();
			a[i] = a[i].capitalize( true );
		}
		
		if ( a.length == 1 )
		{
			rtn.firstName = a[0];
		}
		else
		{
			var sal = a[0].toLowerCase();
			
			if ( /^(mr|mrs|miss|ms|sir|madam|dr)$/.test( sal ) )
				rtn.salutation = a.shift();
			
			rtn.lastName = a.pop();
			rtn.firstName = a.join(' ');
		}
		
		return rtn;
	},
	
	toElement: function()
	{
		return this.$el;
	},
	
	set: function( what, value )
	{
		switch ( what )
		{
			default:
				this.store[ what ] = value;
		}
		
		return this;
	},
	
	get: function( what )
	{
		switch ( what )
		{
			case 'fields':
				return this.fields;
			
			default:
				return this.store[ what ];
		}
	}
	
});

UI.Form.Field = new Class({
	
	Implements: [ Events, Options ],
	
	options: {
	
		form: null,
		
		type: null, // file, dob
		
		key: null,
		
		required: false,
		
		validate: false,
		
		fieldOnly: false, // internal use only
		
		showHintOnFocus: false,
		showHint: false,
		
		hideHint: false,
		
		submitForm: false,
		
		section: null,
		
		matchField: null,
		
		minLength: 1,
		
		hint: '',
		
		fileType: null,
		fileTypes: null,
		
		minDate: false,
		maxDate: false,
		
		validMessage: '',
		invalidMessage: '',
		
		ignoreField: false
		
	},
	
	initialize: function( field, form, options )
	{
		this.setOptions( options );
		
		this.$el = field;
		
		field.store( 'field', this );
		
		// Define field(s)
		this.$field = null;
		this.$fields = [];
		
		if ( this.options.fieldOnly )
		{
			this.$field = this.$el;
		}
		else
		{
			if ( !options.fields )
				return;
			
			// If the field has multiple elements, it means we have more than one field
			if ( options.fields.length > 1 )
				this.$fields = options.fields;
				
			// Set the field to the first element in the array (so if the array was only equal to 1, handle it like a single field, otherwise
			// if there are several, we use the first field to detect the type of input and other information
			this.$field = options.fields[0];
		}
		
		
		// Hint
		this.$hint = this.$el.getElement( '.ui-field-feedback' );
		
		
		// Form
		this.form =	form;
		this.$form = this.form.toElement();
		
		
		// Tag
		this.tag = this.$field.get( 'tag' );
		this.type = this.$field.getProperty( 'type' );
		
		
		// Name
		this.name = this.$field.getProperty( 'name' );
		
		if ( !this.name )
			$log( 'WARNING! Name not detected for field, form will fail.', this.field );
		
		
		// Type
		if ( !this.options.type && this.type == 'file' )
			this.options.type = 'file';
			
		switch( this.options.type )
		{
			case 'file':
			
				this.$selection = this.$el.getElement( '.ui-file-selection' );
				
				if ( this.$selection )
					this.$selection.addClass( 'ui-file-selection-none' );
				
				this.$browse = this.$el.getElement( '.ui-file-browse' );
			
			break;
			
			case 'date':
				
				new UI.Form.Field.Date( this );
				
				// Enforce date validation
				this.options.validate = 'date';
				
			break;
			
			case 'dateSelects':
				
				var hasSelects = this.$el.getElement( 'select' );
				
				if ( !hasSelects )
				{
					var $input = this.$el.getElement( 'input' );
					
					if ( !$input ) {
						$log( 'Date selects require a standard input field.' );
						return;
					}
					
					$input.hide();
					
					var $selects = new Element( 'div', { 'class': 'ui-date-selects' } );
					
					// Day
					var $day = this.$day = new Element( 'select', { 'class': 'ui-date-day' } ).inject( $selects );
					
					new Element( 'option' ).inject( $day );
					
					for ( var d = 1; d <= 31; d++ )
						new Element( 'option', { html: d, value: d } ).inject( $day );
					
					// Month
					var $month = this.$month = new Element( 'select', { 'class': 'ui-date-month' } ).inject( $selects );
					
					var months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];
					
					new Element( 'option' ).inject( $month );
					
					months.each( function( m, i ) {
						new Element( 'option', { html: m, value: i } ).inject( $month );
					});
					
					// Year
					var $year = this.$year = new Element( 'select', { 'class': 'ui-date-year' } ).inject( $selects );
					
					new Element( 'option' ).inject( $year );
					
					for ( var y = 1900; y <= 2020; y++ )
						new Element( 'option', { html: y, value: y } ).inject( $year );
						
					$selects.inject( $input, 'after' );
				}
				else
				{
					var $day = this.$day = this.$el.getElement( '.ui-date-day' );
					var $month = this.$month = this.$el.getElement( '.ui-date-month' );
					var $year = this.$year = this.$el.getElement( '.ui-date-year' );
				}
				
				this.$fields.push( $day, $month, $year );
				
				// Enforce dateSelects validation
				this.options.validate = 'dateSelects';
				
				// Overide the type in select fields case as we replace hide the original input field
				this.type = 'select-one';
			
			break;
		}
		
		
		// Min/Max Dates
		if ( this.options.minDate )
			this.minDate = new Date().parse( this.options.minDate );
			
		if ( this.options.minDate )
			this.maxDate = new Date().parse( this.options.maxDate );
		
		
		// Misc
		if ( ( this.$hint && this.form.options.hideHints && !this.options.showHint ) || this.options.hideHint )
			this.$hint.hide();
		
		
		if ( this.$hint && this.options.hint )
			this.$hint.set( 'html', this.options.hint );
		
		if ( this.options.required && !this.options.validate )
			this.options.validate = 'length';
		
		if ( !this.options.required && !this.options.validate )
			this.valid = 'yes';
		else
			this.valid = 'no';
		
		
		// Add flag if its an action
		this.customAction = false;
		
		if ( !/^(length|password|match|numeric|date|dob|email|dateSelects)$/.test( this.options.validate ) )
			this.customAction = true;
		
		
		// Init Events
		this.initEvents();
	},
	
	initEvents: function()
	{
		var form = this.form;
		
		// Generic events
		this.$field.addEvents({
		
			'keypress': (function( event ) {
				
				if ( this.tag != 'textarea' && event.key == 'enter' )
				{
					event.stop();
					
					if ( form.options.keyboardShortcuts )
					{
						(function() {
							form.validateForm();
						}).delay( 100 );
					}
				}
			
			}.bind( this ))
		
		});
		
		// Validation Mode
		var mode = form.options.validationMode;
		
		
		// Force length checking for file fields
		if ( this.options.type == 'file' )
			this.options.validate = 'length';
		
		
		// Add validation
		if ( this.options.validate )
		{
			switch( mode )
			{
				case 'instant':
				
					// Execute validation
					var doValidation = (function( event, blur ) {
					
						var delay = ( blur ? 0 : 500 );
						
						if ( /^(length|password|match|numeric|email|dob|phone)$/i.test( this.options.validate ) )
							delay = 250;
						
						if ( this.tag == 'select' )
							delay = 0;
							
						if ( /^(radio|checkbox|file)$/i.test( this.type ) )
							delay = 0;
						
						// If its not required and we don't have a value, we don't need a delay
						if ( !this.options.required && !this.get( 'value' ) )
							delay = 0;
						
						if ( !blur && event.key == 'tab' )
							return;
						
						$clear( this.executeValidation );
						
						this.executeValidation = (function() {
						
							this.validate();
							
						}.bind( this )).delay( delay );
					
					}.bind( this ));
					
					
					// Add events to execute validation
					var addEvents = (function( $field ) {
					
						$field.addEvents({
						
							'keyup': (function(e) {
								
								doValidation(e)
							
							}.bind( this )),
							
							'blur': (function(e) {
							
								if ( this.valid != 'processing' ) // don't do anything if 'processing'
									doValidation( e, true );
								
								// TW: Future enhancement - if ( this.valid == 'no' ) // don't do anything if its 'yes' or 'processing'
							
							}.bind( this ))
						
						});
						
						switch( this.type )
						{
							case 'file': // if ( this.options.type == 'file' )
							
								$field.addEvents({
								
									'change': (function(e) { doValidation(e) }.bind( this )),
									
									'click': (function(e) { doValidation(e) }.bind( this ))
								
								});
							
							break;
							
							case 'checkbox':
							case 'radio':
							case 'select-one': // $field.get( 'tag' ) == 'select';
								
								$field.addEvents({
								
									'click': (function(e) {
									
										// Special case: If its a select field and we haven't registered a click yet, don't do anything (otherwise
										// when the user clicks it the first time, it will display the valid message, as the selectfield might have
										// a predefined value set...)
										if ( this.type == 'select-one' && !this._clicked )
										{
											this._clicked = true;
											return;
										}
										
										doValidation(e);
									
									}.bind( this ))
								
								});
								
							break;
						}
					}.bind( this ));
					
					
					// Add events to each field inside the group or add it to the single field
					if ( this.$fields.length )
					{
						this.$fields.each( function(f) {
							addEvents(f);
						});
					}
					else
					{
						addEvents( this.$field );
					}
					
				break;
			}
		}
		
		// Show Hint on Focus
		if ( this.options.showHintOnFocus )
		{
			this.$field.addEvents({
			
				'focus': (function() {
					
					if ( this.$hint )
						this.$hint.show();
				
				}.bind( this ))
			
			});
		}
	},
	
	clearStatus: function()
	{
		// Remove current status
		this.valid = 'no';
		
		this.$field
			.removeClass( 'ui-field-valid' )
			.removeClass( 'ui-field-invalid' );
			
		this.$el
			.removeClass( 'ui-field-container-valid' )
			.removeClass( 'ui-field-container-invalid' );
		
		if ( this.$hint )
			this.$hint
				.removeClass( 'ui-field-feedback-validating' )
				.removeClass( 'ui-field-feedback-valid' )
				.removeClass( 'ui-field-feedback-invalid' );
	
	},
	
	isValid: function( msg, hide )
	{
		// $log( '--> Field [' + this.name + '] is valid.' );
		
		var form = this.form;
		
		// Valid (ability to provide a custom msg (from an action) as well as hide the
		// hint if need be (field isn't required yet field is valid if nothing is entered))
		
		this.clearStatus();
		
		this.$field.addClass( 'ui-field-valid' ); // Should we do this if field is not required yet field is valid? (already hiding hint)
		this.$el.addClass( 'ui-field-container-valid' );
		
		// Determine message
		var validMessage = this.validMessage = msg;
		
		// Manage hint
		if ( this.$hint )
		{
			switch( this.options.type )
			{
				case 'date':
				
					if ( this.options.dateType != 'tooltip' )
						this.$hint.show().addClass( 'ui-field-feedback-valid' );
				
				break;
				
				default:
				
					if ( hide || form.options.hideValidHints )
					{
						this.$hint.hide();
					}
					else
					{
						if ( !validMessage )
							validMessage = this.validMessage = this.options.validMessage || form.options.validFieldMessage;
						
						this.$hint.show().addClass( 'ui-field-feedback-valid' ).empty();
						this.$hint.set( 'html', validMessage );
					}
			}
		}
		
		// Custom procesing
		switch( this.options.type )
		{
			case 'file':
			
				if ( this.options.validBrowseText )
					if ( this.$browse )
						this.$browse.set( 'text', this.options.validBrowseText );
				
				if ( this.$selection )
				{
					var value = this.get( 'value' );
					
					if ( !value )
						return;
					
					this.$selection
						.set( 'html', value )
						.removeClass( 'ui-file-selection-none' )
						.addClass( 'ui-file-selection-selected' )
						.show();
				}
			
			break;
		}
		
		this.valid = 'yes';
		
		// Submit form (for file fields)
		if ( this.options.submitForm )
		{
			// Don't submit twice (the change event is sometimes fired twice)
			if ( this._file == this.get( 'value' ) )
			{
				$log( 'Change event fired but the same file was selected.' );
				return;
			}
			
			this._file = this.get( 'value' );
			
			form.submitForm();
		}
		
		return 'yes';
	},
	
	isInvalid: function( msg )
	{
		// $log( '--> Field [' + this.name + '] is invalid: ' + msg );
		
		var form = this.form;
		
		this.clearStatus();
		
		this.$field.addClass( 'ui-field-invalid' );
		this.$el.addClass( 'ui-field-container-invalid' );
		
		// Determine message
		var invalidMessage = this.invalidMessage = msg;
		
		// Manage hint
		if ( this.$hint )
		{
			switch( this.options.type )
			{
				case 'date':
				
					if ( this.options.dateType != 'tooltip' )
						this.$hint.show().addClass( 'ui-field-feedback-invalid' );
				
				break;
				
				default:
					
					if ( !invalidMessage )
						invalidMessage = this.invalidMessage = this.options.invalidMessage || form.options.invalidFieldMessage;
					
					this.$hint.show().addClass( 'ui-field-feedback-invalid' ).empty();
					this.$hint.set( 'html', invalidMessage );
			}
		}
		
		// Custom procesing
		switch( this.type )
		{
			case 'file':
			
				if ( this.$selection )
					this.$selection
						.set( 'html', '(please add valid image)' )
						.removeClass( 'ui-file-selection-selected' )
						.addClass( 'ui-file-selection-none' );
			
			break;
		}
		
		this.valid = 'no';
		
		// Add to validation errors array
		form._validationErrors.push( this );
		
		return 'no';
	},
	
	validate: function()
	{
		var form = this.form,
			name = this.name,
			value = this.get( 'value' );
		
		var fields = this.$form.retrieve( 'form' ).get( 'fields' ); // TW: Unable to get directly from form...
		
		// Perform specific validation
		var check = (function( type ) {
		
			// If a field is not required and has no value, its theoretically valid
			if ( !this.options.required && !value.length )
			{
				this.isValid( false, true );
				return;
			}
			
			// If the field is in a section, and the section isn't visible, the field is theoretically valid
			if ( this.section )
			{
				var section = form.sections[ this.section ];
				
				if ( !section )
				{
					$log( 'A section was marked inside a field but could not be found.' )
					return;
				}
				
				if ( !section.$el.isVisible() )
				{
					// $log( 'A field inside a section may have been required but it was on a section that was not visible, skipping validation.' );
					this.isValid( false, true );
					return;
				}
			}
			
			switch( type )
			{
				// Length (plus support for min length)
				case 'length':
				
					switch( this.type )
					{
						case 'file':
							
							if ( !value.length || value.length < this.options.minLength )
								this.isInvalid();
							
							var fileTypes = [];
							
							if ( this.options.fileType )
							{
								switch( this.options.fileType )
								{
									case 'image': fileTypes = [ 'jpg', 'jpeg', 'png' ]; break; // 'gif'
									case 'document': fileTypes = [ 'doc', 'docx', 'pdf', 'pages' ]; break;
									case 'audio': fileTypes = [ 'wav', 'mp3', 'aac' ]; break;
								}
							}
							else if ( this.options.fileTypes )
							{
								fileTypes = this.options.fileTypes.split(',');
							}
							
							if ( !fileTypes.length )
							{
								this.isValid();
								return;
							}
							
							var ext = value.split('.').getLast().toLowerCase();
							
							if ( fileTypes.contains( ext ) )
							{
								this.isValid();
							}
							else
							{
								// alert( 'This file is not supported. Please choose a valid file.' )
								this.isInvalid();
							}
						
						break;
						
						case 'checkbox':
							
							if ( value )
								this.isValid();
							else
								this.isInvalid();
						
						break;
						
						case 'checkboxes':
							
							// Multiple fields (handle the object returned)
							if ( !$H( value ).getLength() )
								this.isInvalid();
							else
								this.isValid();
						
						break;
						
						default:
						
							// Single field
							if ( !value || value.length < this.options.minLength )
								this.isInvalid();
							else
								this.isValid();
					}
					
				break;
				
				// Password (same as length, keeping it seperate for now...)
				case 'password':
				
					if ( value.length < this.options.minLength )
						this.isInvalid();
					else
						this.isValid();
					
				break;
				
				// Match
				case 'match':
				
					var matchField = fields[ this.options.matchField ];
					
					if ( !matchField ) {
						$log( "No matching field could be found for the 'match' field type, check that you have specified the correct field first." );
						return;
					}
					
					var matchValue = matchField.get( 'value' );
					
					if ( !value || value != matchValue )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Numeric (plus support for min length)
				case 'numeric':
				
					value = value.toString().replace( / /g, '' );
					
					if ( !/^ *[0-9]+ *$/.test( value ) || value.length < this.options.minLength )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Money (plus support for min length)
				case 'money':
				
					value = value.toString().replace( / /g, '' );
					
					if ( !/^ *[0-9-.]+ *$/.test( value ) || value.length < this.options.minLength )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Phone (numeric plus support for (, ), -, + and min length)
				case 'phone':
				
					value = value.toString().replace( / /g, '' ).replace( /\(/g, '' ).replace( /\)/g, '' ).replace( /\+/g, '' ).replace( /\-/g, '' );
					
					if ( !/^ *[0-9]+ *$/.test( value ) || value.length < this.options.minLength )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Date
				case 'date':
				
					var valueTime = value.parsed.get( 'time' );
					
					// Check min date
					if ( this.options.minDate )
					{
						minDateTime = this.minDate.get( 'time' );
					
						if ( valueTime < minDateTime )
						{
							this.isInvalid();
							return;
						}
					}
					
					// Check max date
					if ( this.options.maxDate )
					{
						maxDateTime = this.maxDate.get( 'time' );
					
						if ( valueTime > maxDateTime )
						{
							this.isInvalid();
							return;
						}
					}
					
					if ( value.cf )
						this.isValid();
					else
						this.isInvalid();
				
				break;
				
				case 'dateSelects':
				
					// Validate only if we have all 3 values or are now validating the form before submit
					if ( this.form._status != 'validating' )
						if ( !value.day || !value.month || !value.year )
							return;
					
					if ( value.cf )
						this.isValid();
					else
						this.isInvalid();
				
				break;
				
				// DOB (backwards compatibility, no longer used)
				case 'dob':
				
					// Caters for slash, hyphen and period, must be valid date, takes leap years into account)
					if ( !/^((((0?[1-9]|[12]\d|3[01])[\.\-\/](0?[13578]|1[02])[\.\-\/]((1[6-9]|[2-9]\d)?\d{2}))|((0?[1-9]|[12]\d|30)[\.\-\/](0?[13456789]|1[012])[\.\-\/]((1[6-9]|[2-9]\d)?\d{2}))|((0?[1-9]|1\d|2[0-8])[\.\-\/]0?2[\.\-\/]((1[6-9]|[2-9]\d)?\d{2}))|(29[\.\-\/]0?2[\.\-\/]((1[6-9]|[2-9]\d)?(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00)|00)))|(((0[1-9]|[12]\d|3[01])(0[13578]|1[02])((1[6-9]|[2-9]\d)?\d{2}))|((0[1-9]|[12]\d|30)(0[13456789]|1[012])((1[6-9]|[2-9]\d)?\d{2}))|((0[1-9]|1\d|2[0-8])02((1[6-9]|[2-9]\d)?\d{2}))|(2902((1[6-9]|[2-9]\d)?(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00)|00))))$/.test( value.stored ) )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Email
				case 'email':
				
					if ( !/^[\w\-]+(\.[\w\-]+)*@[\w\-]+\.([\w\-]+\.)*[a-z]{2,}$/i.test( value ) )
						this.isInvalid();
					else
						this.isValid();
				
				break;
				
				// Test for Sparkle action (plus support for min length)
				default:
					
					// $log( 'Calling sparkle action...' );
					
					if ( !value.length || value.length < this.options.minLength )
					{
						this.isInvalid();
						return;
					}
					
					form._preSubmitQueue.push( this.name );
					
					this.valid = 'processing';
					
					this.clearStatus();
					
					if ( this.$hint )
					{
						this.$hint.show().addClass( 'ui-field-feedback-validating' );
						this.$hint.set( 'html', this.options.validatingMessage || form.options.validatingFieldMessage );
					}
					
					var process = (function( rtn ) {
					
						if ( !rtn.success )
							$log( 'Sparkle validation failed:', rtn );
							
						var msg = rtn.clean(),
							fail = ( msg.slice( 0, 4 ) == 'fail' ),
							success = ( msg.slice( 0, 7 ) == 'success' );
						
						if ( fail )
						{
							msg = msg.slice( 6 );
							
							this.isInvalid( msg );
						}
						else if ( success )
						{
							msg = msg.slice( 9 );
							
							this.isValid( msg );
						}
						else
						{
							$log( 'Sparkle validation return could not be parsed for success/fail.', rtn );
						}
						
						// Remove action from queue and check if we have any other actions
						form.checkSubmitQueue( name );
						
					}.bind( this ));
					
					// Determine path
					var validatePath = this.options.validate,
						validateData = {};
					
					if ( $type( this.options.validate ) == 'object' )
					{
						validatePath = this.options.validate.path;
						validateData = this.options.validate.data;
					}
					
					// Call action
					var send = {
						path: validatePath,
						data: validateData
					};
					
					var name = this.get( 'name' );
					
					send.data[ name ] = value;
					
					UI.loadSparkle( send )
						.then( function( rtn ) {
						
							// Illusion... (the question is, should there be one?)
							(function() {
								process( rtn )
							}).delay( 500 );
						
						}.bind( this ));
				
			}
		
		}.bind( this ));
		
		
		// Check for a basic value if the field is required, has no value and is a simple field type
		if ( this.options.required && !value && /^(length|password|match|numeric|date|dob|email)$/.test( this.options.type ) )
		{
			this.isInvalid();
		}
		else
		{
			// Validation types
			var types = [];
			
			switch( $type( this.options.validate ) )
			{
				case 'string':
					types = $A( this.options.validate.split(':') );
				break;
				
				case 'object':
					types = [ this.options.validate ];
				break;
			}
			
			// validated = false;
			types.each( function( type ) {
			
				check( type );
			
				// if ( validated && !check( type ) )
					// validated = false;
			
			});
		}
	},
	
	toElement: function()
	{
		return this.$field;
	},
	
	getValue: function()
	{
		// Get field value
		var $form = this.$form,
			key = this.get( 'name' ),
			field = $form[ key ];
		
		// If its a collection of fields (like a radio, just use the first one, we don't need references to the others)
		if ( $type( field ) == 'collection' || $type( field ) == 'object' )
			field = field[0];
		
		// Gather some information
		if ( field )
		{
			var value = field.value,
				tag = field.get( 'tag' ),
				type = field.getProperty( 'type' );
		}
		
		// If we have a type, bypass usual field detection, as we know what field to expect
		if ( this.options.type )
		{
			switch ( this.options.type )
			{
				case 'date':
				
					if ( !value )
						return false;
					
					// Try inserting / instead of - so it recognises it as a standard date
					value = value.toString().replace( /-/g, '/' );
					
					var date = new Date.parse( value );
					
					if ( !date.isValid() )
					{
						$log( 'Date could not be parsed.' );
						return false;
					}
					
					var dateObj = {
						stored: value,
						parsed: date,
						cf: date.format( '%Y-%m-%d' )
					}
					
					return dateObj;
				
				break;
				
				case 'dateSelects':
				
					var day = this.$day.value,
						month = this.$month.value,
						year = this.$year.value;
					
					if ( !day || !month || !year )
						return false;
					
					var value = day + '/' + month + '/' + year,
						date = new Date.parse( value );
					
					if ( !date.isValid() )
						return false;
					
					var dateObj = {
						day: day,
						month: month,
						year: year,
						stored: value,
						parsed: date,
						cf: date.format( '%Y-%m-%d' )
					}
					
					return dateObj;
					
				break;
			}
		}
		
		switch( tag )
		{
			case 'input':
			
				switch( type )
				{
					case 'text':
					case 'password':
					case 'file':
					
						switch( this.options.validate )
						{
							case 'date':
							case 'dob':
							
								if ( !value )
									return false;
								
								// Try inserting / instead of - so it recognises it as a standard date
								value = value.toString().replace( /-/g, '/' );
								
								var date = new Date.parse( value );
								
								if ( !date.isValid() )
								{
									$log( 'Date could not be parsed.' );
									return false;
								}
								
								var dateObj = {
									stored: value,
									parsed: date,
									cf: date.format( '%Y-%m-%d' )
								};
								
								return dateObj;
							
							break;
							
							default:
								return value;
						}
					
					break;
					
					case 'checkbox':
					
						// Single
						if ( !this.$fields.length )
						{
							return field.get( 'checked' );
						}
						
						// Multiple
						var values = {};
						
						// If we have a key, we need to send it all together as an array
						if ( this.options.key )
							values = [];
						
						for ( var i = 0; i <= this.$fields.length - 1; i++ ) {
						
							var $field = this.$fields[ i ];
							
							var checked = $field.checked;
							
							var name = this.options.key || $field.getProperty( 'name' );
							
							// If the checkbox has been checked, send it
							if ( checked )
							{
								switch( $type( values ) )
								{
									case 'object':
										values[ name ] = true;
									break;
									
									case 'array':
									
										var fieldValue = $field.get( 'value' ).toInt();
										
										values.push( fieldValue );
									
									break;
								}
								
							}
						
						};
						
						return values;
					
					break;
					
					case 'radio':
						
						var value = false;
						
						for ( var i = 0; i <= $form[ key ].length - 1; i++ ) {
							
							var checked = $form[ key ][ i ].checked;
							
							if ( checked )
								value = $form[ key ][ i ].value;
							
							// Check to see if its a stringed boolean
							if ( $type( value ) == 'string' )
							{
								if ( value == 'true' || value == 'on' )
									value = true;
								else if ( value == 'false' || value == 'off' )
									value = false;
							}
						};
						
						return value;
						
					break;
					
					default:
					
						return value;
				}
			
			case 'textarea':
			
				// TW: Build in proper WYSIWYG support in the future
				if ( this.$field.hasClass( 'ui-wysiwyg' ) )
				{
					var wysiwygEditor = this.$field.retrieve( 'MooEditable' );
					
					if ( wysiwygEditor )
						return wysiwygEditor.getContent();
					else
						return false;
					
					/* var id = this.get( 'id' );
					
					var wysiwygEditor = tinyMCE.get( id );
					
					if ( wysiwygEditor )
						return wysiwygEditor.getContent();
					else
						return false;
					*/
				}
				else
					return value;
			
			break;
			
			case 'select':
			
				return value;
			
			break;
		}
	
	},
	
	resetField: function()
	{
		var $form = this.$form;
		
		var $field = this.$field,
			$fields = this.$fields;
		
		var name = this.get( 'name' );
		
		var tag = this.tag,
			type = this.type;
		
		// Don't reset hidden fields
		if ( type == 'hidden' )
			return;
		
		var reset = (function( field ) {
			
			switch( tag )
			{
				case 'input':
				
					switch( type )
					{
						case 'text':
						case 'password':
							field.value = '';
						break;
						
						case 'checkbox':
							field.checked = false;
						break;
						
						case 'radio':
								
							var key = this.get( 'name' );
								
							for ( var i = 0; i <= $form[ name ].length - 1; i++ ) {
								$form[ name ][ i ].checked = false;
							};
							
						break;
						
						default:
							field.value = '';
					}
				
				case 'textarea':
					field.value = '';
				break;
				
				case 'select':
					field.selectedIndex = 0;
				break;
			}
		
		}.bind( this ));
		
		// Reset values (still need to handle selects and radios)
		if ( $fields.length )
		{
			$fields.each( function(f) {
				reset(f);
			});
		}
		else
		{
			reset( $field );
		}
		
		// Remove classes
		this.clearStatus();
		
		// Empty and hide the hint element
		if ( this.$hint )
			this.$hint.empty().hide();
			
		// Remove status
		this._file = null;
		this._clicked = null;
	
	},
	
	set: function( what, value )
	{
		switch ( what )
		{
			case 'value':
			
				if ( this.value == value )
					return;
					
				this.value = value;
				
			break;
			
			default:
				this.store[ what ] = value;
		}
		
		return this;
	},
	
	get: function( what )
	{
		switch ( what )
		{
			case 'value':
				return this.getValue();
			
			case 'id':
				return this.$field.getProperty( 'id' ) || false;
			
			case 'name':
				return this.$field.getProperty( 'name' );
			
			default:
				return this.store[ what ];
		}
	}
	
});

UI.Form.Field.Date = new Class({
	
	Extends: UI.Form.Field,
	
	options: {
	
		dateHint: 'eg. today, last monday',
		
		fillField: true,
		
		tooltip: false
		
	},
	
	initialize: function( field, options )
	{
		this.setOptions( options );
		
		this.field = field;
		this.$field = field.$field;
		
		this.form = field.form;
		
		this.$el = field.$hint;
		
		if ( field.options.dateType == 'tooltip' )
		{
			this.options.tooltip = true;
			this.$el = field.$el.getElement( '.ui-date-hint' ).hide();
		}
		
		this.$el.set( 'html', this.options.dateHint );
		
		this.$field.addEvents({
		
			'keyup': (function() {
			
				var showDateHint = (function() {
				
					$clear( this._hintUpdate );
					
					this._hintUpdate = function() { this.parse() }.delay( 200, this );
				
				}.bind( this ));
				
				showDateHint();
				
			}.bind( this )),
			
			'focus': (function() {
			
				if ( this.options.tooltip )
				{
					this.$el.show().set( 'opacity', 0 );
					
					this.$el.get( 'tween' )
						.setOptions({
							duration: 150,
							transition: 'linear'
						})
						.start( 'opacity', 1 );
						
					if ( this.options.tooltip )
					{
						this.$el.position({ relativeTo: field.$field, position: 'bottomLeft', edge: 'topLeft', offset: { x: 0, y: 0 } });
					}
				}
			
			}.bind( this )),
			
			'blur': (function() {
			
				if ( this.options.tooltip )
				{
					this.$el.get( 'tween' )
						.setOptions({
							duration: 150,
							transition: 'linear'
						})
						.start( 'opacity', 0 )
						.chain( (function() {
						
							if ( this.options.fillField && this.parsedDate )
								this.field.$field.value = this.parsedDate.format( '%d/%m/%Y' );
							
							this.$el.hide();
						
						}.bind( this )));
				}
			
			}.bind( this ))
		
		});
		
	},
	
	parse: function()
	{
		var value = this.field.get( 'value' ).stored;
		
		if ( !value )
		{
			this.$el.set( 'html', this.options.dateHint );
			return;
		}
			
		var parsedDate = this.parsedDate = new Date.parse( value );
		
		if ( parsedDate.isValid() )
			this.$el.set( 'html', parsedDate.format( '%A %d%o %B %Y' ) );
		else
			this.$el.set( 'html', this.options.dateHint );
	
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-form' ).each( function( $el ) {
		$el.store( 'form', new UI.Form( $el, $el.getDataFromComment() ) );
	});

});


/*
Class: UI.Carousel
*/
UI.Carousel = new Class({
	
	Implements: Options,
	
	options: {
		gapSize: 50,
		focusedClass: null,
		duration: 250,
		autoTransition: true,
		autoDelay: 5000,
		zIndex: 1
	},
	
	initialize: function( target, options )
	{
		this.setOptions( options );
		
		var $el = this.$el = $( target );
		
		// Get the slides
		var slides = this.slides = $H( $el.getChildren() );
		
		// Abort if no slides
		if ( !slides.length )
		{
			alert( 'No elements inside the carousel detected, you must have children inside the carousel element.' );
			return;
		}
		
		// Set intial index
		this._index = 0;
		
		// Apply select class if it doesn't have it already
		var $slide = this._$slide = slides[0];
		
		if ( this.options.focusedClass && !$slide.hasClass( this.options.focusedClass ) )
			$slide.addClass( this.options.focusedClass );
		
		// Determine the size of the whole element
		var width = $el.getSize().x;
		
		// Add events to stop the automatic switching and to handle sliding
		slides.each( function( $s, index ) {
		
			// Set position
			var totalSlidesWidth = slides.length * this.options.gapSize;
			
			var left = 0;
			
			if ( index > 0 )
				left = ( width - ( -( index * this.options.gapSize ) ) - totalSlidesWidth );
			
			$s.setStyle( 'left', left );
			$s.store( 'left', left );
			
			// Set z-index
			var zIndex = $s.getStyle( 'z-index' );
			
			if ( zIndex != 'auto' )
			{
				var zInt = zIndex.toInt();
				
				$s.setStyle( 'z-index', ( zInt + 10 ) );
			}
			else
			{
				var zIndex = this.options.zIndex;
				
				if ( index > 0 )
					zIndex = 10 + index;
				
				$s.setStyle( 'z-index', zIndex );
			}
			
			// Add mouse events
			$s.addEvent( 'mouseenter', (function() {
			
				$clear( this._switchSlides );
				
				this.switchSlide( index );
			
			}.bind( this )));
		
		}.bind( this ));
		
		// Begin switching automatically
		if ( this.options.autoTransition )
			this.switchSlides();
	},
	
	switchSlide: function( index )
	{
		if ( this._index == index )
			return;
		
		// Store new index
		this._index = index;
		
		// Store old slide
		var $oldSlide = this._$slide;
		
		if ( this.options.focusedClass )
			$oldSlide.removeClass( this.options.focusedClass );
		
		// Handle new slide
		var $newSlide = this._$slide = this.slides[ index ];
		
		if ( this.options.focusedClass )
			$newSlide.addClass( this.options.focusedClass );
		
		// Expand slide
		var expandSlide = function( i ) {
		
			var $slide = this.slides[ i ];
			
			// Set position
			var left = 0;
			
			if ( i > 0 )
				left = i * this.options.gapSize;
			
			$slide.get( 'morph' ).setOptions({ transition: 'quad:in:out', duration: this.options.duration }).start({ 'left': left });
		
		}.bind( this );
		
		// Contract slide
		var contractSlide = function( i ) {
		
			var $slide = this.slides[ i ];
			
			var left = $slide.retrieve( 'left' );
			
			$slide.get( 'morph' ).setOptions({ transition: 'quad:in:out', duration: this.options.duration }).start({ 'left': left });
		
		}.bind( this );
		
		// Figure out which slides to move
		var currentSlide = index,
			numberOfSlides = this.slides.length;
		
		this.slides.each( function( $s, i ) {
		
			if ( i == 0 || i == index )
				return;
			
			if ( i > index )
				contractSlide( i );
			else
				expandSlide( i );
		
		}.bind( this ));
		
		expandSlide( index );
		
		/* Deal with opacity
		$newSlide.show().setOpacity(0);
		$newSlide.get( 'morph' ).setOptions({ transition: 'quad:in:out', duration: 500 }).start({ 'opacity': 1 });
		*/
	},
	
	switchSlides: function()
	{
		this._switchSlides = (function() {
		
			if ( this._stopSlides )
				return;
			
			var numberOfSlides = this.slides.length - 1;
			
			var nextSlide = this._index + 1;
			
			if ( nextSlide > numberOfSlides )
				nextSlide = 0;
			
			this.switchSlide( nextSlide );
		
		}.bind( this )).periodical( this.options.autoDelay );
	}

});

document.addEvent( 'domready', function() {

	$$( '.ui-carousel' ).each( function( $el ) {
		$el.store( 'carousel', new UI.Carousel( $el, $el.getDataFromComment() ) );
	});

});


/*
Class: ChildFader
*/
UI.ChildFader = new Class({
	
	Implements: Options,
	
	options: {
		start: 1000,
		duration: 500,
		interval: 1000,
		pauseOnHover: true
	},
	
	initialize: function( target, options )
	{
		var $el = $( target );
		this.setOptions( options );
		
		var children = $el.getChildren();
		
		children.each( function( $child, i ) {
			
			$child.setStyles({
				position: 'absolute',
				opacity: (i) ? 0 : 1, // leave the first child visible
				display: 'block'
			});
			
		});
		
		var startedFading = false,
			currentlyVisible = 0,
			currentlyHovered = false;
		
		if ( this.options.pauseOnHover )
		{
			$el.addEvent( 'mouseenter', function() { currentlyHovered = true; } );
			$el.addEvent( 'mouseleave', function() { currentlyHovered = false; } );
		}
		
		(function() {
			
			startedFading = true;
			
			if ( currentlyHovered )
				return;
			
			children[ currentlyVisible ].set( 'tween', { duration: this.options.duration } ).tween( 'opacity', 0 );
			
			currentlyVisible++;
			
			if ( currentlyVisible >= children.length )
				currentlyVisible = 0;
			
			children[ currentlyVisible ].set( 'tween', { duration: this.options.duration } ).tween( 'opacity', 1 );
			
		}).periodical( ( startedFading ? options.interval : options.start ), this );
		
	}
	
});

document.addEvent( 'domready', function() {

	$$( '.ui-fadechildren' ).each( function( $el ) {
		$el.store( 'childfader', new UI.ChildFader( $el, $el.getDataFromComment() ) );
	});

});

/*
Object: UI.Google
*/
UI.Google = {

	status: {
		maps: false,
		infoBox: false
	},
	
	load: function( type, callback )
	{
		if ( !window.google )
			return;
		
		switch( type )
		{
			// Maps
			case 'maps':
			
				var performCallback = function()
				{
					if ( !callback || typeOf( callback ) != 'function' )
						return;
					
					callback();
				}
				
				switch( UI.Google.status.maps )
				{
					case 'loaded':
					
						$log( 'Maps already loaded...' );
						
						performCallback();
					
					break;
					
					case 'loading':
					
						UI.Google._check = function() {
						
							if ( UI.Google.status.maps == 'loaded' && UI.Google.status.infoBox == 'loaded' )
							{
								$log( 'Maps finished loading after request was queued...' );
								
								clearTimeout( UI.Google._check );
								
								performCallback();
								
								return;
							}
							
							$log( 'Maps have already been requested, waiting for load...' );
						
						}.periodical( 100 );
					
					break;
					
					default:
					
						var done = function() {
							
							UI.Google.status.maps = 'loaded';
							
							$log( 'Maps finished loading...' );
							
							// TW: TEMPORARY
							performCallback();
							return;
							
							// Load the Info Box script
							Asset.javascript( '/JS/Kaleidoscope/Maps/InfoBox.js', {
								events: {
									load: function() {
									
										UI.Google.status.infoBox = 'loaded';
										
										$log( 'Loaded infoBox...' );
										
										performCallback();
									}
								}
							});
						};
						
						UI.Google.status.maps = 'loading';
						
						$log( 'Maps loading...' );
						
						google.load( 'maps', '3', { 'callback': done, 'other_params': 'sensor=false' });
				}
				
			break;
		}
	}
	
};


/*
Class: UI.Map
*/
UI.Map = new Class({
	
	Implements: [ Events, Options ],
	
	options: {
	
		marker: null,
		markers: [],
		
		markerOptions: {
			draggable: false,
			icon: null,
			shadow: null
		},
		
		zoom: null,
		minZoom: 0,
		
		typeMenu: 'default', // default, dropdown, horizontal
		navMenu: 'default', // default, android, small, zoompan
		
		mapType: 'roadmap', // roadmap, hybrid, satellite, terrain
		
		streetView: false,
		
		draggable: true,
		
		manualMode: false,
		
		keyboardShortcuts: false,
		scrollWheel: false,
		
		bgColor: '#B3B7CF',
		
		clusters: false,
		clusterOptions: {}, // maxZoom, gridSize, styles
		
		autoCreate: true,
		
		// Custom events
		performOnReady: null
		
	},
	
	markers: [],
	markersOnly: [],
	
	initialize: function( target, options )
	{
		this.setOptions( options );
		
		this.$el = target;
		
		this.loaded();
	},
	
	loaded: function()
	{
		this.geocoder = new google.maps.Geocoder();
		
		if ( this.options.autoCreate )
			this.render();
	},
	
	render: function()
	{
		var $map = this.$el,
			markers = this.options.markers;
		
		// If theres only a single marker, push it to the main markers array
		if ( this.options.marker )
			markers = [ this.options.marker ];
		
		// All maps must have a center, here we use the world
		var center = this.center = new google.maps.LatLng( 37.71859, 6.679688 );
		
		// Set the center if one is supplied (usually without a marker)
		if ( this.options.center )
			center = this.center = new google.maps.LatLng( this.options.center.latitude, this.options.center.longitude );
		
		// Set the center if we have a marker but don't specifically specify the center
		if ( this.options.marker && !this.options.center )
			center = this.center = new google.maps.LatLng( this.options.marker.latitude, this.options.marker.longitude );
		
		// Map options
		var options = {
			
			mapTypeId: this.options.mapType,
			
			center: center,
			backgroundColor: this.options.bgColor,
			
			streetViewControl: this.options.streetView,
			
			draggable: this.options.draggable,
			
			scrollwheel: this.options.scrollWheel,
			keyboardShortcuts: this.options.keyboardShortcuts,
			
			mapTypeControl: ( this.options.typeMenu ),
			navigationControl: ( this.options.navMenu )
			
		}
		
		// Map Type
		if ( this.options.typeMenu )
		{
			var mapType = this.options.mapType,
				mapTypeStyles = google.maps.MapTypeId;
			
			var style;
			
			switch( typeMenu )
			{
				case 'hybrid': style = mapTypeStyles.TERRAIN; break;
				case 'satellite': style = mapTypeStyles.SATELLITE; break;
				case 'terrain': style = mapTypeStyles.TERRAIN; break;
				
				default:
					style = mapTypeStyles.ROADMAP;
			}
			
			options.mapTypeId.style = style;
		}
		
		// Type Menu
		if ( this.options.typeMenu )
		{
			var typeMenu = this.options.typeMenu,
				typeMenuStyles = google.maps.MapTypeControlStyle;
			
			options.mapTypeControlOptions = { position: '' };
			
			var style;
			
			switch( typeMenu )
			{
				case 'dropdown': style = typeMenuStyles.DROPDOWN_MENU; break;
				case 'horizontal': style = typeMenuStyles.HORIZONTAL_BAR; break;
				
				default:
					style = typeMenuStyles.DEFAULT;
			}
			
			options.mapTypeControlOptions.style = style;
		}
		
		// Nav Menu
		if ( this.options.navMenu )
		{
			var navMenu = this.options.navMenu,
				navMenuStyles = google.maps.NavigationControlStyle;
				
			options.navigationControlOptions = { position: '' };
			
			var style;
			
			switch( navMenu )
			{
				case 'android': style = navMenuStyles.ANDROID; break;
				case 'small': style = navMenuStyles.SMALL; break;
				case 'zoompan':	style = navMenuStyles.ZOOM_PAN; break;
				
				default:
					style = navMenuStyles.DEFAULT;
			}
			
			options.navigationControlOptions.style = style;
		}
		
		
		// Create the map
		var map = this.map = new google.maps.Map( $map, options );
		
		
		if ( !this.options.manualMode )
		{
			// Add markers
			if ( markers.length || typeOf( markers ) == 'hash' && markers.getLength() )
			{
				markers.each( function(o) {
					this.addMarker(o);
				}, this );
				
				// Create single marker instance if only one
				if ( this.markers.length == 1 )
					this.marker = this.markers[0];
			}
			
			this.markersAdded();
		}
	},
	
	markersAdded: function()
	{
		var map = this.map;
		
		this.fitBounds();
		
		// Events
		google.maps.event.addListener( map, 'tilesloaded', (function() {
			
			// Call performOnReady function
			if ( this.options.performOnReady )
			{
				UI.doCallback( this.options.performOnReady );
				this.options.performOnReady = null;
			}
			
		}.bind( this )));
		
		google.maps.event.addListener( map, 'click', (function() {
		
			if ( this._content && this._content.instance )
				this._content.instance.hide();
		
		}.bind( this )));
		
		// Add marker clusterer
		if ( this.options.clusters )
		{
			var markerCluster = this.markerClusterer = new MarkerClusterer( map, this.markersOnly, this.options.clusterOptions );
		}
	},
	
	addMarker: function( m )
	{
		var map = this.map,
			latLng = false;
		
		if ( m.latitude && m.longitude )
			latLng = new google.maps.LatLng( m.latitude, m.longitude );
		else
			return;
		
		var markerOptions = this.options.markerOptions || {};
		
		var options = {
			position: latLng,
			map: map,
			draggable: ( markerOptions.draggable || false )
		};
		
		var markerImageOptions = false;
		
		// Per map basis
		if ( markerOptions.icon )
		{
			markerImageOptions = {
				icon: markerOptions.icon,
				shadow: markerOptions.shadow
			};
		}
		
		// Per marker basis
		if ( m.icon )
		{
			if ( m.icon )
			{
				markerImageOptions = {
					icon: m.icon,
					shadow: m.shadow
				}
			}
		}
		
		// Build new marker image object from the supplied options
		if ( markerImageOptions )
			Object.append( options, this.getMarkerImage( markerImageOptions ) );
		
		// Create marker
		var marker = new google.maps.Marker( options );
		
		var node = {
			marker: marker,
			data: m
		};
		
		// Add the marker details to the data struct
		if ( markerImageOptions )
			node.markerImageOptions = markerImageOptions;
		
		// Create info box
		if ( this.options.infoBox )
		{
			var opts = {
				data: m
			}
			
			// Add the marker details to the content struct
			if ( markerImageOptions )
				opts.markerImageOptions = markerImageOptions;
			
			// If we have content options, add them
			if ( $H( this.options.infoBox ).getLength() )
				Object.append( opts, this.options.infoBox );
			
			var hover = this.options.infoBox.hover,
				click = this.options.infoBox.click;
			
			var hoverElement = false,
				clickElement = false;
			
			if ( hover && hover.renderFn )
				hoverElement = UI.doCallback( hover.renderFn, opts.data );
			
			if ( click && click.renderFn )
				clickElement = UI.doCallback( click.renderFn, opts.data );
			
			// Add offsets
			var left = 0,
				top = 0;
			
			if ( this.options.infoBox.offset )
			{
				if ( this.options.infoBox.offset.x )
					left = ( this.options.infoBox.offset.x );
				
				if ( this.options.infoBox.offset.y )
					top = ( this.options.infoBox.offset.y );
			}
			
			// Open info box
			var openAs = this.options.infoBox.openAs,
				disableAutoPan = this.options.infoBox.disableAutoPan;
			
			if ( this.options.infoBox.hover )
			{
				var hoverOptions = {
					content: hoverElement,
					infoBoxClearance: new google.maps.Size( 1, 1 ),
					alignBottom: true,
					pixelOffset: new google.maps.Size( -( hover.width / 2 ) + ( left ), top ),
					pane: "floatPane",
					enableEventPropagation: false,
					isHidden: ( openAs ? false : true ),
					disableAutoPan: ( !disableAutoPan ? false : true )
				};
			}
			
			if ( this.options.infoBox.click )
			{
				var clickOptions = {
					content: clickElement,
					infoBoxClearance: new google.maps.Size( 1, 1 ),
					alignBottom: true,
					pixelOffset: new google.maps.Size( -( click.width / 2 ) + ( left ), top ),
					pane: "floatPane",
					enableEventPropagation: false,
					isHidden: ( openAs ? false : true ),
					disableAutoPan: ( !disableAutoPan ? false : true )
				};
			}
			
			var ib = new InfoBox( ( openAs == 'click' ? clickOptions : hoverOptions ) );
			
			// Add info box to map
			ib.open( map, marker );
			
			if ( this.options.infoBox.hover )
				hoverOptions.isHidden = false;
			
			if ( this.options.infoBox.click )
				clickOptions.isHidden = false;
			
			google.maps.event.addListener( marker, "mouseover", function(e) {
			
				// Ignore if theres no hover event
				if ( !this.options.infoBox.hover )
					return;
				
				// $log( 'clicked: ' + isClicked() );
				// $log( 'same: ' + isSame() );
				
				if ( isClicked() || isSame() )
					return;
				
				// ? this.instance.setcontent( 'hover' );
				// ? this.instance.show( 'hover' );
				
				ib.setOptions( hoverOptions );
				ib.show( map, marker );
			
			}.bind( this ));
			
			google.maps.event.addListener( marker, "mouseout", function(e) {
			
				// Ignore if theres no hover event
				if ( !this.options.infoBox.hover )
					return;
				
				// $log( 'clicked: ' + isClicked() );
				// $log( 'same: ' + isSame() );
				
				if ( isClicked() && isSame() )
					return;
				
				// ? if ( this.options.static )
					// ? this.instance.setcontent( 'static' );
				// ? else if ( isSame() )
					// ? this.instance.hide();
				
				ib.hide( map, marker );
			
			}.bind( this ));
			
			google.maps.event.addListener( marker, "click", function(e) {
				
				// Ignore if theres no hover event
				if ( !this.options.infoBox.hover )
					return;
				
				// $log( 'clicked: ' + isClicked() );
				// $log( 'same: ' + isSame() );
				
				if ( isClicked() && isSame() )
				{
					// ? if ( options.returnOnSame )
						// ? return;
					
					this._infoBox.infoBox.hide();
					this._infoBox = false;
					
					return;
				}
				
				// Close any existing infoBoxes
				if ( this._infoBox && this._infoBox.infoBox )
					this._infoBox.infoBox.hide();
				
				ib.setOptions( clickOptions );
				ib.show( map, marker );
				
				// Store as current
				this._infoBox = {
					infoBox: ib,
					clicked: true
				}
				
			}.bind( this ));
			
			node.infoBox = {
				ib: ib,
				events: {
					mouseover: (function() {
						google.maps.event.trigger( marker, "mouseover" );
			 		}),
			 		mouseout: (function() {
			 			google.maps.event.trigger( marker, "mouseout" );
			 		}),
					click: (function() {
						google.maps.event.trigger( marker, "click" );
			 		}),
			 		show: (function() {
			 			ib.show( map, marker );
			 		}),
			 		hide: (function() {
			 			ib.hide( map, marker );
			 		})
				}
			}
			
			// Content set (not currently used)
			/* var contentSet = (function() {
			
				if ( this._infoBox )
					return true;
				else
					return false;
			
			}.bind( this )); */
			
			// Check if a piece of content has been clicked
			var isClicked = (function() {
			
				if ( this._infoBox )
					if ( this._infoBox.clicked )
						return true;
				
				return false;
			
			}.bind( this ));
			
			// Check to see if the current content is the same as the one we clicked
			var isSame = (function() {
			
				if ( this._infoBox )
					if ( this._infoBox.infoBox )
						return ( this._infoBox.infoBox == ib );
				
				return false;
			
			}.bind( this ));
			
			/* Old InfoBox Design */
			// node.content = new UI.Map.Content( marker, this, opts );
		}
		
		// Add to marker set and return
		this.markers.push( node );
		this.markersOnly.push( marker );
		
		return node;
	},
	
	getMarkerImage: function( image )
	{
		var iconImage = null,
			shadowImage = null;
		
		// Icon (always exists)
		var icon = {
			src: '',
			width: 20,
			height: 32,
			anchor: false
		}
		
		switch( typeOf( image.icon ) )
		{
			case 'object':
				Object.append( icon, image.icon );
			break;
			
			case 'string':
				icon.src = image.icon;
			break;
		}
		
		// Create marker image
		iconImage = this.makeMarkerImage({ src: icon.src, width: icon.width, height: icon.height, anchor: icon.anchor });
		
		// Shadow (optional)
		if ( image.shadow )
		{
			var shadow = {
				src: '',
				width: 20,
				height: 32,
				anchor: false
			}
			
			switch( typeOf( image.shadow ) )
			{
				case 'object':
					Object.append( shadow, image.shadow );
				break;
				
				case 'string':
					shadow.src = image.shadow;
				break;
			}
			
			// The shadow image is larger in the horizontal dimension while the position and offset are (usually) the same as the main image
			shadowImage = this.makeMarkerImage({ src: shadow.src, width: shadow.width, height: shadow.height, anchor: shadow.anchor });
		}
		
		return { icon: iconImage, shadow: shadowImage };
	},
	
	makeMarkerImage: function( options )
	{
		var size = new google.maps.Size( options.width, options.height );
		
		var origin = new google.maps.Point( 0, 0 );
		
		// Anchor
		var anchor = new google.maps.Point( 0, options.height );
		
		switch( options.anchor )
		{
			case 'center':
				anchor = new google.maps.Point( options.width / 2, options.height / 2 );
			break;
			
			case 'bottom':
				anchor = new google.maps.Point( options.width / 2, options.height );
			break;
		}
		
		return new google.maps.MarkerImage( options.src, size, origin, anchor );
	},
	
	setMarkerImage: function( marker, image )
	{
		var markerImage = this.getMarkerImage( image );
	
		marker.setIcon( markerImage.icon );
		
		if ( image.shadow )
			marker.setShadow( markerImage.shadow );
	},
	
	clearMarkers: function()
	{
		this.markers.each( function( node ) {
			node.marker.setMap( null );
		});
		
		this.markers.empty();
	},
	
	fitBounds: function( markers )
	{
		var map = this.map,
			bounds = this.calculateBounds( markers );
		
		// Rezoom map after bounds change (this is the most efficient way, adding a listener then removing it)
		var zoomChangeBoundsListener = google.maps.event.addListener( map, 'bounds_changed', (function(event) {
			
			// Get the current zoom
			var currentZoom = this.get( 'zoom' );
			
			// Force zoom (if supplied)
			if ( this.options.zoom && this.markers.length )
			{
				var zoom = this.options.zoom;
				
				if ( zoom < this.options.minZoom )
					map.setZoom( zoom );
			}
			
			// Zoom map if we have no markers with a specific level set
			if ( this.options.noMarkerZoom && !this.markers.length )
			{
				if ( this.options.noMarkerZoom )
					map.setZoom( this.options.noMarkerZoom );
			}
			
			// Adjust zoom (if supplied)
			if ( this.options.zoomAdjustment )
			{
				var newZoom = currentZoom + ( this.options.zoomAdjustment );
				
				if ( newZoom < this.options.minZoom )
					map.setZoom( newZoom );
			}
			
			// Min Zoom
			if ( this.get( 'zoom' ) > this.options.minZoom && this.options.minZoom > 0 )
				map.setZoom( this.options.minZoom );
			
			// Remove bounds listener (listener is outdated now)
			google.maps.event.removeListener( zoomChangeBoundsListener );
		
		}.bind( this )));
		
		// Fit map to bounds
		map.fitBounds( bounds );
	},
	
	calculateBounds: function( markers )
	{
		var bounds = new google.maps.LatLngBounds;
		
		var markers = ( markers || this.markers );
		
		if ( markers.length )
		{
			markers.each( function( node ) {
				bounds.extend( node.marker.position );
			});
		}
		else
			bounds.extend( this.center ); // We must extend the bounds with the center point if we have no markers
		
		return bounds;
	},
	
	resize: function()
	{
		google.maps.event.trigger( this.map, 'resize' );
	},
	
	recenter: function()
	{
		this.map.panTo( this.center );
	},
	
	set: function( what, value )
	{
		switch ( what )
		{
			case 'zoom':
				this.map.setZoom( value );
			break;
			
			case 'center':
				this.map.setCenter( value );
			break;
			
			case 'options':
				this.map.setOptions( value );
			break;
			
			case 'panAndZoom':
			
				if ( !value.position || !value.zoom )
					return;
				
				this.map.setCenter( value.position );
				
				var panListener = google.maps.event.addListener( this.map, 'idle', (function() {
					
					if ( this.get( 'zoom' ) != value.zoom )
						this.set( 'zoom', value.zoom );
					
					google.maps.event.removeListener( panListener );
					
				}.bind( this )));
				
			break;
			
			case 'streetView':
			
				new google.maps.StreetViewPanorama( this.$el, {
					position: new google.maps.LatLng( value.latitude, value.longitude ),
					enableCloseButton: true,
					pov: {
						heading: 0,
						pitch: 0,
						zoom: 0
					}
				});
			
			break;
			
			default:
				this.store[ what ] = value;
		}
		
		return this;
	},
	
	get: function( what )
	{
		switch ( what )
		{
			case 'zoom':
				return this.map.getZoom();
			
			default:
				return this.store[ what ];
		}
	}

});

document.addEvent( 'domready', function() {

	var maps = $$( '.ui-map' );
	
	if ( !maps.length )
		return;
	
	UI.Google.load( 'maps', (function() {
	
		maps.each( function( $el ) {
			$el.store( 'map', new UI.Map( $el, $el.getDataFromComment() ) );
		});
	
	}));

});
