/**
 * 
 * 
 * ---------------------------------------------------------------------------
 * 
 * Copyright (C) 2009 Omnium Research Group
 * 
 * ---------------------------------------------------------------------------
 * 
 * LICENSE:
 * 
 * This file is part of Omnium(R) Software.
 * 
 * Omnium(R) Software is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * Omnium(R) Software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Omnium(R) Software; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * ---------------------------------------------------------------------------
 * 
 * @author    Sam Bauers <sam@omnium.net.au>
 * @copyright 2009 Omnium Research Group
 * @license   http://www.gnu.org/licenses/gpl.txt GNU GPL v2
 * @link      http://open.omnium.net.au Omnium Open
 **/



/**
 * omFormWidgets class for creation of custom form/navigation widgets
 *
 * @author  Sam Bauers
 **/
var omFormWidgets = {
	// The current version
	Version: '0.0.10'
}


/**
 * Creates a "chooser" element which behaves much like a non-multiple select element.
 *
 * The class needs to be invoked on page load with the following syntax
 *
 * new omFormWidgets.Chooser(
 *    buildDiv,                 <-- The div HTML object or ID (needs quotes) where the chooser will be built
 *    0,                        <-- The current position of the default choice
 *    [                         <-- The array of choices
 *       [
 *          'value',                                <-- The value
 *          'Value',                                <-- The displayed text
 *          "callbackFunction('someVariable');",    <-- The javascript to run on selection of this choice - run using "enum" function
 *          'newUrl.php?blah=1&blah=2',             <-- URL to redirect to when the choice is selected in addition to running the callback
 *          'choiceClass'                           <-- Any custom class to apply to this choice only
 *          false                                   <-- Boolean - if true, will *not* set the current position to this choice when selected
 *       ]
 *    ],
 *    'chooserPopupCSSclass'    <-- The CSS class applied to the chooser "pop-up"
 * );
 *
 * The chooser inherits most styles from the build div e.g. font, color, line-height
 *
 * @author  Sam Bauers
 **/
omFormWidgets.Chooser = Class.create();
omFormWidgets.Chooser.prototype = {
	initialize: function(initBuildDiv, initInitialPosition, initChoices, initChooserClass)
	{
		// The target input div
		this.buildDiv = $(initBuildDiv);
		
		// The initial position
		this.initialPosition = initInitialPosition;
		
		// The array of values and display values
		this.choices = initChoices;
		
		// The CSS class for the chooser
		this.chooserClass = initChooserClass;
		
		// Build the base objects for the chooser
		this.buildTarget();
		this.buildOnblur();
		this.buildSlider();
		this.buildChooser();
	},
	
	buildTarget: function()
	{
		// Insert the value of the initial position in to the target
		this.buildDiv.update(this.choices[this.initialPosition][1]);
		
		// Add an additional class to the target div - adds some special CSS - see omForm.css
		this.buildDiv.className += ' ' + this.chooserClass + 'Target';
		if (this.choices[this.initialPosition][4]) {
			this.buildDiv.className += ' ' + this.choices[this.initialPosition][4];
		}
		
		// Watch for mousedown event on the target div
		Event.observe(this.buildDiv, 'mousedown', this.open.bind(this));
	},
	
	buildOnblur: function()
	{
		// Build the onblur capture layer
		this.onblurHTML = new Element(
			'div',
			{
				style: 'position:absolute; width:100%; height:100%; z-index:999; display:none; cursor:default;'
			}
		);
		
		// IE 6 doesn't work without this
		if (Prototype.Browser.IE) {
			Element.setStyle(
				this.onblurHTML,
				{
					backgroundColor: 'rgb(0, 0, 0)',
					filter: 'alpha(opacity=1)'
				}
			);
		}
		
		// Watch for click events and close the chooser when observed
		Event.observe(this.onblurHTML, 'click', this.close.bind(this));
		
		// Append it to the body
		document.body.appendChild(this.onblurHTML);
	},
	
	buildSlider: function()
	{
		// Build a slider div - this will eventually handle the sliding up and down of long choosers
		this.sliderHTML = new Element(
			'div',
			{
				style: 'position:absolute; z-index:1000; height:100%; top:0; display:none;'
			}
		);
		
		// Append it to the body
		document.body.appendChild(this.sliderHTML);
		
		// Build the top scrolling control
		this.scrollTopHTML = new Element(
			'div',
			{
				style: 'z-index:1;',
				className: this.chooserClass + 'ScrollUp'
			}
		).insert(new Element('div'));
		
		// Observe events on the top scrolling control
		Event.observe(
			this.scrollTopHTML,
			'mouseover',
			this.scrollStart.bind(this, 'up', true)
		);
		Event.observe(
			this.scrollTopHTML,
			'mouseout',
			this.scrollStop.bind(this)
		);
		
		// Append it to the slider and hide it
		this.sliderHTML.appendChild(this.scrollTopHTML);
		this.scrollTopHTML.hide();
		
		// Build the bottom scrolling control
		this.scrollBottomHTML = new Element(
			'div',
			{
				style: 'z-index:1;',
				className: this.chooserClass + 'ScrollDown'
			}
		).insert(new Element('div'));
		
		// Observe events on the bottom scrolling control
		Event.observe(
			this.scrollBottomHTML,
			'mouseover',
			this.scrollStart.bind(this, 'down', true)
		);
		Event.observe(
			this.scrollBottomHTML,
			'mouseout',
			this.scrollStop.bind(this)
		);
		
		// Append it to the slider and hide it
		this.sliderHTML.appendChild(this.scrollBottomHTML);
		this.scrollBottomHTML.hide();
	},
	
	buildChooser: function()
	{
		// Build the chooser popup div
		this.chooserHTML = new Element(
			'div',
			{
				className: this.chooserClass,
				style: 'position:absolute; left:0; z-index:0;'
			}
		);
		
		// Create an array for the choices in the chooser
		this.choiceHTML = new Array();
		
		var choiceOptions;
		
		// Loop through the choices and build divs for each
		for (var i = 0, cLen = this.choices.length; i < cLen; ++i) {
			if (this.choices[i][4]) {
				choiceOptions = {
					onmouseover: "this.className = 'over " + this.choices[i][4] + "';",
					onmouseout: "this.className = '" + this.choices[i][4] + "';",
					style: 'cursor:default; height:auto; white-space:nowrap; -moz-user-select:none; -khtml-user-select:none;',
					className: this.choices[i][4]
				}
			} else {
				choiceOptions = {
					onmouseover: "this.className = 'over';",
					onmouseout: "this.className = '';",
					style: 'cursor:default; height:auto; white-space:nowrap; -moz-user-select:none; -khtml-user-select:none;'
				}
			}
			
			this.choiceHTML[i] = new Element(
				'div',
				choiceOptions
			).update(this.choices[i][1]);
			
			// Watch for events
			Event.observe(
				this.choiceHTML[i],
				'mouseout',
				this.out.bind(this)
			);
			Event.observe(
				this.choiceHTML[i],
				'mouseup',
				this.run.bind(this, i)
			);
			
			// Append it to the chooser popup
			this.chooserHTML.appendChild(this.choiceHTML[i]);
		}
		
		// Append it to the slider
		this.sliderHTML.appendChild(this.chooserHTML);
	},
	
	setChooserStyle: function()
	{
		// Get the height of the target div
		var choiceHeight = Element.getDimensions(this.buildDiv).height;
		
		// Get the width of the target div
		var choiceWidth = Element.getDimensions(this.buildDiv).width;
		
		if (Prototype.Browser.IE) {
			var choiceTopPadding = Number(Element.getStyle(this.buildDiv, 'padding-top').replace(/px/, ''));
			var choiceRightPadding = Number(Element.getStyle(this.buildDiv, 'padding-right').replace(/px/, ''));
			var choiceBottomPadding = Number(Element.getStyle(this.buildDiv, 'padding-bottom').replace(/px/, ''));
			var choiceLeftPadding = Number(Element.getStyle(this.buildDiv, 'padding-left').replace(/px/, ''));
			choiceHeight += choiceTopPadding + choiceBottomPadding;
			choiceWidth += choiceRightPadding + choiceLeftPadding;
		}
		
		choiceHeight += 'px';
		choiceWidth += 'px';
		
		// Copy selected styles from the target div to the chooser
		Element.setStyle(
			this.chooserHTML,
			{
				borderWidth: Element.getStyle(this.buildDiv, 'border-top-width'),
				borderStyle: 'solid',
				borderColor: Element.getStyle(this.buildDiv, 'border-top-color'),
				color: Element.getStyle(this.buildDiv, 'color'),
				backgroundColor: Element.getStyle(this.buildDiv, 'background-color'),
				fontFamily: Element.getStyle(this.buildDiv, 'font-family'),
				fontSize: Element.getStyle(this.buildDiv, 'font-size'),
				lineHeight: choiceHeight,
				minWidth: choiceWidth
			}
		);
		
		if (Prototype.Browser.IE) {
			Element.setStyle(this.chooserHTML, {width: choiceWidth});
		}
		
		// Copy selected styles from the target div to the choices within the chooser
		for (c = 0; c < this.choiceHTML.length; c++) {
			Element.setStyle(
				this.choiceHTML[c],
				{
					paddingTop: Element.getStyle(this.buildDiv, 'padding-top'),
					paddingRight: Element.getStyle(this.buildDiv, 'padding-right'),
					paddingBottom: Element.getStyle(this.buildDiv, 'padding-bottom'),
					paddingLeft: Element.getStyle(this.buildDiv, 'padding-left'),
					height: choiceHeight,
					lineHeight: choiceHeight
				}
			);
			
			if (Prototype.Browser.IE) {
				Element.setStyle(this.choiceHTML[c], {width: 'auto'});
			}
		}
	},
	
	// Wrapper function to manually change the scroller's position
	setPosition: function(position)
	{
		// Set the initialPosition of the chooser, which will then be referenced by everything else
		// If the parameter happens to be empty or invalid, no action will take place
		if (position !== undefined && position >= 0) {
			this.initialPosition = position;
			
			// Set the position of the scroller div
			this.setChooserPosition();
			
			// Update the text in the main select window
			this.buildDiv.update(this.choices[this.initialPosition][1]);
		}
	},
	
	// Set the relative position of the scroller div. Does not include the main select window
	setChooserPosition: function()
	{
		// Get the position of the target div
		var targetOffset = Element.cumulativeOffset(this.buildDiv);
		
		// Offset that position depending on which choice is current
		var choiceOffset = Element.getDimensions(this.buildDiv).height * this.initialPosition;
		
		// Do some adjusting to account for any border on the top of the target div
		var targetTopBorder = Element.getStyle(this.buildDiv, 'border-top-width').replace(/px/, '');
		
		// Adjust the left position of the slider on the page
		Element.setStyle(this.sliderHTML, {left: String(targetOffset[0]) + 'px'});
		
		// Adjust the top position of the chooser in the slider
		Element.setStyle(this.chooserHTML, {top: String(Number(targetOffset[1]) - Number(choiceOffset) - Number(targetTopBorder)) + 'px'});

	},
	
	scrollShowHide: function()
	{
		// Get the current top position of the chooser list
		var cTop = Number(Element.getStyle(this.chooserHTML, 'top').replace(/px/, ''));
		
		// Get the inner height of the window
		if (window.innerHeight) {
			var wHeight = Number(window.innerHeight);
		} else if (document.documentElement && document.documentElement.clientHeight) {
			var wHeight = Number(document.documentElement.clientHeight);
		} else if (document.body) {
			var wHeight = Number(document.body.clientHeight);
		}
		
		// Get the height of the 
		var cHeight = Number(Element.getHeight(this.chooserHTML));
		
		// Get the current bottom position of the chooser list relative to the bottom of the page
		var cBottom = cHeight + cTop - wHeight;
		
		if (cTop < 0) {
			// If the chooser extedns past the top of the window then show the control
			this.scrollTopHTML.show();
		} else {
			// Otherwise hide it
			this.scrollTopHTML.hide();
			if (this.scrollDirection == 'up') {
				// If we are currently scrolling up then stop
				this.scrollStop();
			}
		}
		
		if (cBottom > 0) {
			// If the chooser extends past the bottom of the window then show the control
			this.scrollBottomHTML.show();
		} else {
			// Otherwise hide it
			this.scrollBottomHTML.hide();
			if (this.scrollDirection == 'down') {
				// If we are currently scrolling down then stop
				this.scrollStop();
			}
		}
	},
	
	scrollStart: function(direction, init)
	{
		if (direction) {
			// Set the scroll direction
			this.scrollDirection = direction;
		}
		
		if (init) {
			// We are scrolling until this is changed to false (by scrollStop function)
			this.scroll = true;
		}
		
		// Set the amounts by which we move each step
		if (this.scrollDirection == 'up') {
			var change = -10;
		} else {
			var change = 10;
		}
		
		// Get the current top position of the chooser
		var currentTop = Number(Element.getStyle(this.chooserHTML, 'top').replace(/px/, ''));
		
		// What will the new top position be after this move
		var newTop = currentTop - change;
		
		// Set the style of the chooser to this new top position
		Element.setStyle(this.chooserHTML, {top:newTop + 'px'});
		
		// Work out what scroller controls to show after this update
		this.scrollShowHide();
		
		// If we are still scrolling then do it all again
		if (this.scroll) {
			// Run scrollRepeat function in 10 milliseconds
			this.timer = setTimeout(this.scrollRepeat.bind(this),10);
		}
	},
	
	scrollRepeat: function()
	{
		// Run scrollStart function and pass the direction - has to be done this way to avoid FireFox weirdness
		this.scrollStart(this.scrollDirection);
	},
	
	scrollStop: function()
	{
		// Set the scroll state to false - stops current scrolling loop
		this.scroll = false;
	},
	
	open: function()
	{
		/**
		 * Reset the flag that indicates whether we have moved from the original choice
		 *
		 * This setting helps create the following behavior:
		 *   - Mousedown/mouseup once on the initial choice will open the chooser, mouseup
		 *       once again on the original choice will close the chooser, but not execute it.
		 *   - Mousedown/mouseup once on the initial choice will open the chooser, mouseup on
		 *       any other choice will execute, then close the chooser.
		 *   - Mousedown once on the initial choice will open the chooser, mouseup after
		 *       moving to any other choice will execute then close the chooser.
		 *   - Mousedown once on the initial choice will open the chooser, moving to any
		 *       other choice and then mouseup on the original choice will close the chooser
		 *       but not execute it.
		 **/
		this.movedFromInitial = false;
		
		// Set the styles
		this.setChooserStyle();
		
		// Set the position
		this.setChooserPosition();
		
		// Set the state of the choice under the mouse to "over"
		if (this.choices[this.initialPosition][4]) {
			this.choiceHTML[this.initialPosition].className = 'over ' + this.choices[this.initialPosition][4];
		} else {
			this.choiceHTML[this.initialPosition].className = 'over';
		}
		
		// Show the slider (containing the chooser) and onblur divs
		this.onblurHTML.show();
		this.sliderHTML.show();
		
		// Set the state of the scroll controls
		var cWidth = Number(Element.getDimensions(this.chooserHTML).width);
		Element.setStyle(
			this.scrollTopHTML,
			{
				width:cWidth + 'px'
			}
		);
		Element.setStyle(
			this.scrollBottomHTML,
			{
				width:cWidth + 'px'
			}
		);
		
		this.scrollShowHide();
		
		// Watch for blur event on the window (forget about IE though)
		if (!Prototype.Browser.IE) {
			Event.observe(window, 'blur', this.close.bind(this));
		}
	},
	
	close: function()
	{
		// Hide the slider (containing the chooser) and onblur divs
		this.sliderHTML.hide();
		this.onblurHTML.hide();
	},
	
	out: function()
	{
		// Flag that we are no longer on the original choice
		this.movedFromInitial = true;
	},
	
	run: function(position)
	{
		// If this is not the original choice
		if (position != this.initialPosition) {
			this.choiceHTML[position].className = this.choices[position][4];
			
			// If the choice causes a redirect, then leave the chooser in place
			// - this is to give the correct "back" button behaviour
			if (!this.choices[position][3] && !this.choices[position][5]) {
				// Make this the original choice
				this.initialPosition = position;
				
				// Insert this choice into the target div
				this.buildDiv.update(this.choices[this.initialPosition][1]);
				
				// Move the chooser popup accordingly
				this.setChooserPosition();
			}
			
			// Run any scripts associated with this choice
			if (this.choices[position][2]) {
				eval(this.choices[position][2]);
			}
			
			// Redirect to the requested URL
			if (this.choices[position][3]) {
				window.location = this.choices[position][3];
			}
		}
		
		// Close the chooser if appropriate
		if (this.movedFromInitial) {
			this.close();
		}
		
		// After clicking once on the initial choice it will be closed with another click
		this.movedFromInitial = true;
	}
}


/**
 * Creates a "colour picker" element.
 *
 * The class must be invoked after the target field.
 *
 * new omFormWidgets.ColourPicker(
 *    fieldId,                                <-- The input HTML object or ID (needs quotes)
 *    togglerObj,                             <-- The HTML object to be contained within the toggler link
 *    pickerObj,                              <-- The HTML object housing the picker
 *    "callbackFunction('someVariable');"     <-- The javascript to run when a new colour is set - run using "enum" function
 * );
 *
 * @author  Sam Bauers
 **/
omFormWidgets.ColourPicker = Class.create();
omFormWidgets.ColourPicker.prototype = {
	initialize: function(fieldId, togglerObj, pickerObj, callbackFunc)
	{
		// The HTML object of the field
		this.field = $(fieldId);
		
		// Add the toggler link after the field
		this.toggler = new Element('a', { href:'javascript:void(0);' }).insert(togglerObj);
		this.field.parentNode.appendChild(this.toggler);
		
		// Add the lupe/picker area after the toggler link
		this.picker = pickerObj;
		this.field.parentNode.appendChild(this.picker);
		
		// Set the callback function string
		this.callback = callbackFunc;
		
		this.lupeArea = new Array();
		
		// When the field is manually edited, then update the lupe
		Event.observe(this.field, 'keyup', this.set.bindAsEventListener(this));
		//Event.observe(this.field, 'change', this.set.bindAsEventListener(this));
		
		// When the fields form is reset, then update the lupe
		Event.observe(this.field.form, 'reset', this.set.bindAsEventListener(this));
		
		// When the toggler is clicked the show/hide the lupe/preview
		Event.observe(this.toggler, 'click', this.toggle.bindAsEventListener(this));
	},
	
	// Shows and hides the lupe and picker
	toggle: function()
	{
		this.build();
		
		new Effect.toggle(this.picker, 'blind', { duration:0.2 });
	},
	
	// Builds the lupe and picker
	build: function()
	{
		// Assign the current value of the field to this.colour
		this.colour = this.field.value;
		
		if (this.picker.empty()) {
			// Initialise the lupe area elements. These are the preview areas above the colour picker
			
			// Container
			var L = Builder.node(
				'div',
				{
					style:'height:50px; width:250px; position:relative; margin-bottom:2px; line-height:25px; text-indent:4px;'
				},
				[
					// Black foreground on selected background
					this.lupeArea[0] = new Element(
						'div',
						{
							style:'height:25px; width:125px; position:absolute; top:0px; left:0px; color:rgb(0,0,0); background-color:' + this.field.value + ';'
						}
					),
			
					// White foreground on selected background
					this.lupeArea[1] = new Element(
						'div',
						{
							style:'height:25px; width:125px; position:absolute; top:0px; right:0px; color:rgb(255,255,255);'
						}
					),
			
					// Selected foreground on black background
					this.lupeArea[2] = new Element(
						'div',
						{
							style:'height:25px; width:125px; position:absolute; bottom:0px; left:0px; background-color:rgb(0,0,0);'
						}
					),
			
					// Selected foreground on white background
					this.lupeArea[3] = new Element(
						'div',
						{
							style:'height:25px; width:125px; position:absolute; bottom:0px; right:0px; background-color:rgb(255,255,255);'
						}
					)
				]
			);
			
			// We now build the individual colour picker blocks
			// The 3 colour containers
			var R = new Array();
			var G = new Array();
			var B = new Array();
			// The 3 children values
			var r, g, b;
			// The colour and style values
			var c, s;
			
			// Loop through the colours to build the picker
			for (r = 0; r < 257; r += 64){
				if (r > 255) {
					r = 255;
				}
				
				for (g = 0; g < 257; g += 64) {
					if (g > 255) {
						g = 255;
					}
					
					for (b = 0; b < 257; b += 64) {
						if (b > 255) {
							b = 255;
						}
						
						// Set the color of this block
						c = 'rgb(' + r + ', ' + g + ', ' + b + ');';
						
						// Set the style of this block
						s  = 'cursor:crosshair;';
						s += 'float:left;';
						s += 'height:10px;';
						s += 'width:10px;';
						s += 'background-color:' + c;
						
						// Build the div
						B[b] = new Element('div', { style: s });
						
						// On click, set the colour to the colour of this block
						Event.observe(B[b], 'click', this.set.bindAsEventListener(this));
						
						// On mouseover, preview the colour in the lupe
						Event.observe(B[b], 'mouseover', this.preview.bindAsEventListener(this));
						
					}
					
					// Put that pass together in a div
					G[g] = Builder.node('div', { style:'clear:both' }, B);
				}
				
				// Put that pass together in a div
				R[r] = Builder.node('div', { style:'float:left' }, G);
			}
			
			// Create the picker by combining the lupe (preview) area, and all picker blocks. Also insert a clearing div
			var P = Builder.node(
				'div',
				{
					style:'width:250px;'
				},
				[
					L,
					R,
					new Element('div', { style:'clear:both' })
				]
			);
			
			// Reset the lupe when the colour picker is moused out
			Event.observe(P, 'mouseout', this.reset.bindAsEventListener(this));
			
			// Append the HTML objects to the picker div
			this.picker.appendChild(P);
			
			// Set the lupe to this.colour
			this.lupe();
		}
	},
	
	// Set the current value of the field and set the lupe colour
	set: function(e)
	{
		// Switch between the event types
		switch (e.type) {
			case 'keyup':
				// Manual editing of the field
				this.colour = this.field.value;
				break;
			case 'reset':
				// Resetting of the form
				this.colour = this.field.defaultValue;
				break;
			case 'click':
				// Clicking on a picker color
				this.colour = e.target.style.backgroundColor;
				this.field.value = this.colour;
				break;
		}
		
		// If the lupe/picker is visible then set it's color
		if (!this.picker.empty()) {
			this.lupe();
		}
		
		// If there is a callback available then run it
		if (this.callback) {
			eval(this.callback);
		}
	},
	
	// Reset the colour in the lupe
	reset: function()
	{
		this.lupe();
	},
	
	// Preview the colour in the lupe
	preview: function(e)
	{
		// Get the color from the current events triggering element background
		this.lupe(e.target.style.backgroundColor);
	},
	
	// Set the color of the lupe
	lupe: function(colour)
	{
		var c = colour;
		
		if (!c) {
			c = this.colour
		}
		
		// Set the text in each lupe area
		for (l in this.lupeArea) {
			Element.update(this.lupeArea[l], c);
		}
		
		// Set the colors of each lupe area
		this.lupeArea[0].style.backgroundColor = c;
		this.lupeArea[1].style.backgroundColor = c;
		this.lupeArea[2].style.color = c;
		this.lupeArea[3].style.color = c;
	}
}


/**
 * Creates a "calendar" element.
 *
 * The class must be invoked after the target field.
 *
 * new omFormWidgets.Calendar(
 *    fieldId,                                <-- The input HTML object or ID (needs quotes)
 *    togglerObj,                             <-- The HTML object to be contained within the toggler link
 *    calendarObj,                            <-- The HTML object housing the calendar
 *    setTime                                 <-- [OPTIONAL] A certain time to set the calendar as
 * );
 *
 * @author  Dan Callaghan
 **/
omFormWidgets.Calendar = Class.create();
omFormWidgets.Calendar.prototype = {
	initialize: function(fieldId, togglerObj, calendarObj, setYear, setMonth, setDay)
	{
		
		// If there is a JavaScript input element hide it
		if ($('dateInput')) {
			$('dateInput').hide();
		}
		
		// The HTML object of the field
		this.field = $(fieldId);
		
		// Add the toggler link after the field
		this.toggler = Builder.node('a', {href:'javascript:void(0);'}, [togglerObj]);
		this.field.appendChild(this.toggler);
		
		// Add the calendar after the toggler link
		this.calendar = calendarObj;
		this.field.appendChild(this.calendar);
		
		// When the toggler is clicked the show/hide the calendar
		Event.observe(this.toggler, 'click', this.toggle.bindAsEventListener(this));
		
		// Set the current date
		this.currentDay = setDay;
		this.currentMonth = setMonth;
		this.currentYear = setYear;
		
		// Set the date set as the current date
		this.daySet = this.currentDay;
		this.monthSet = this.currentMonth;
		this.yearSet = this.currentYear;
		
		// Initialise the months array
		this.months = new Array(
			'January',
			'February',
			'March',
			'April',
			'May',
			'June',
			'July',
			'August',
			'September',
			'October',
			'November',
			'December'
		);
		
		// Initialise day array
		this.days = new Array('S', 'M', 'T', 'W', 'Th', 'F', 'Sa');
		
		// Call the method to build the calendar
		this.build();
		
		// Call the method to build the date selects
		this.buildSelects();
		
	},
	
	// Build the calendar
	build: function(update)
	{
		
		// Initialise th days of the week heading
		DH = new Array();
		
		// Traverse through the days of the week headings
		for (var i = 0; i < this.days.length; i++) {
			DH[i] = Builder.node('th', this.days[i]);
		};
		
		// Create left and right actions for changing the month
		leftAction = Builder.node('th', { className: 'monthAction' }, '<');
		rightAction = Builder.node('th', { className: 'monthAction' }, '>');
		
		// On click, set the colour to the colour of this block
		Event.observe(leftAction, 'click', this.changeMonth.bindAsEventListener(this, 'left', true));
		Event.observe(leftAction, 'mouseover', this.hover.bindAsEventListener(this));
		Event.observe(leftAction, 'mouseout', this.hoverOff.bindAsEventListener(this));
		// On click, set the colour to the colour of this block
		Event.observe(rightAction, 'click', this.changeMonth.bindAsEventListener(this, 'right', true));
		Event.observe(rightAction, 'mouseover', this.hover.bindAsEventListener(this));
		Event.observe(rightAction, 'mouseout', this.hoverOff.bindAsEventListener(this));
		
		// Build the header area
		var H = Builder.node(
			'thead',
			[
				Builder.node('tr',
					[
						leftAction,
						Builder.node('th', { colSpan:'5' }, this.months[this.currentMonth] + ' ' + this.currentYear),
						rightAction
					]
				),
				Builder.node('tr', {className: 'days'}, [DH])
			]
		);
		
		// Initialise the days and weeks
		var W = new Array();
		var D = new Array();
		
		// Get the first day of the month
		firstDay = this.firstDayOfMonth(this.currentMonth, this.currentYear);
		// Get the number of days in the month
		numberDays = this.numberDaysOfMonth(this.currentMonth, this.currentYear);
		previousMonthDays = this.numberDaysOfMonth(this.currentMonth - 1, this.currentYear);
		
		// Calculate the number of weeks required
		numberWeeks = 6;
		
		// Initialise the event listeners for date cells
		this.bset = this.set.bindAsEventListener(this);
		this.bsetGhost = this.setGhost.bindAsEventListener(this);
		this.bHover = this.hover.bindAsEventListener(this);
		this.bHoverOff = this.hoverOff.bindAsEventListener(this);
		
		// Traverse through the number of weeks
		for (var w = 0; w < numberWeeks; w++) {
			
			// Traverse through the days of the week
			for (var d = 0; d < 7; d++) {
				
				// Calculate the date of this cell
				var day = (d + 1) + (w * 7) - firstDay;
				
				// Initialise options to be used
				var opt;
				
				// If day isn't in this month
				if (day < 1 || day > numberDays) {
					opt = { className: 'inactive' };
					if (day < 1) {
						day = day + previousMonthDays;
					} else if (day > numberDays) {
						day = day - numberDays;
					}
				} else {
					opt = false;
				}
				
				// Build the day cell element
				dayElement = Builder.node('td', opt, day);
				
				if (day) {
					
					if (opt) {
						
						// Set ghost listener
						Event.observe(dayElement, 'click', this.bsetGhost);
						// On mouse over, hover on
						Event.observe(dayElement, 'mouseover', this.bHover);
						// On mouse out, hover off
						Event.observe(dayElement, 'mouseout', this.bHoverOff);
						
					} else {
						
						// On click, set the colour to the colour of this block
						Event.observe(dayElement, 'click', this.bset);
						
						// Check if this day is the set day
						if (this.daySet == day && this.monthSet == this.currentMonth && this.yearSet == this.currentYear) {
							
							this.autoSet(dayElement);
							
						} else {
							
							// On mouse over, hover on
							Event.observe(dayElement, 'mouseover', this.bHover);
							// On mouse out, hover off
							Event.observe(dayElement, 'mouseout', this.bHoverOff);
							
						}
					}
					
				}
				
				// Store the day element in the day element array
				D[d] = dayElement;
				
			};
			
			// Build a table row for the week
			W[w] = Builder.node('tr', D);
			
		}
		
		// Build the table body area
		var B = Builder.node('tbody', W);
		
		// Build the calendar table
		table = Builder.node('table', [H, B]);
		
		// If this build is an update, remove all elements and reattach new table
		if (update) {
			Element.update(this.calendar);
		}
		
		// Add the calendar to the container
		this.calendar.appendChild(table);
		
	},
	
	// Sets a specific date when the calendar has been clicked
	set: function(e)
	{
		// Define the object
		setObj = e.target;
		
		// Set the elements class as active
		setObj.addClassName('active');
		// Stop observing clicking and hovering for the selected element
		Event.stopObserving(setObj, 'click', this.bset);
		Event.stopObserving(setObj, 'mouseover', this.bHover);
		Event.stopObserving(setObj, 'mouseout', this.bHoverOff);
		
		// If a date is already set
		if (this.dateElement) {
			
			// Start observing clicking and hovering and remove the active class
			Event.observe(this.dateElement, 'click', this.bset);
			Event.observe(this.dateElement, 'mouseover', this.bHover);
			Event.observe(this.dateElement, 'mouseout', this.bHoverOff);
			this.dateElement.removeClassName('active');
			this.dateElement.removeClassName('over');
			
		}
		
		// Set the date
		this.dateElement = setObj;
		this.daySet = parseInt(setObj.innerHTML);
		$('daySelect').selectedIndex = this.daySet - 1;
		
		if (this.monthSet != this.currentMonth) {
			this.updateDaySelect(this.currentMonth);
		}
		
		// Set the month
		this.monthSet = this.currentMonth;
		$('monthSelect').selectedIndex = this.monthSet;
		
		// Set the year
		this.yearSet = this.currentYear;
		// Find the select index that matches the value
		var selectOptions = $('yearSelect').options;
		for (var i = 0; i < selectOptions.length; i++) {
			if (selectOptions[i].text == this.yearSet) {
				$('yearSelect').selectedIndex = i;
			}
		}
	},
	
	// Automatically sets the default date
	autoSet: function(dateElement) {
		// Set the class as active
		dateElement.addClassName('active');
		this.dateElement = dateElement;
		this.monthSet = this.currentMonth;
		this.yearSet = this.currentYear;
	},
	
	// Sets a ghosted date as the current date and changes month accordingly
	setGhost: function(e) {
		if (parseInt(e.target.innerText) > 15) {
			this.changeMonth(false, 'left', false);
		} else {
			this.changeMonth(false, 'right', false);
		}
		this.set(e);
		this.build(true);
	},
	
	// Handler for when a select option is changed
	select: function(e) {
		
		// Get the selected element
		var selectObj = e.target;
		selectValue = selectObj.options[selectObj.options.selectedIndex].value;
		
		if (selectObj.id == 'daySelect') {
			this.daySet = selectValue;
			if (this.currentMonth != this.monthSet) {
				this.currentMonth = this.monthSet;
			}
			if (this.currentYear != this.yearSet) {
				this.currentYear = this.yearSet;
			}
		} else if (selectObj.id == 'monthSelect') {
			this.monthSet = selectValue;
			this.currentMonth = this.monthSet;
			this.updateDaySelect(this.currentMonth);
		} else if (selectObj.id == 'yearSelect') {
			this.yearSet = selectValue;
			this.currentYear = this.yearSet;
		}
		
		this.build(true);
		
	},
	
	hover: function(e)
	{
		e.target.addClassName('over');
	},
	
	hoverOff: function(e)
	{
		e.target.removeClassName('over');
	},
	
	// Increments or decrements the month in a certain direction
	changeMonth: function(event, direction, update) {
		
		if (direction == 'left') {
			this.currentMonth--;
		} else {
			this.currentMonth++;
		}
		
		// Check to see if the year has changed
		if (this.currentMonth > 11) {
			this.currentMonth = 0;
			this.currentYear++;
		} else if (this.currentMonth < 0) {
			this.currentMonth = 11;
			this.currentYear--;
		}
		
		if (update) {
			// Update the calendar
			this.build(true);
		}
		
	},
	
	// Shows and hides the calendar chooser
	toggle: function()
	{
		new Effect.toggle(this.calendar, 'blind', {duration:0.2});
	},
	
	buildSelects: function()
	{
		// Get the first day of the month
		firstDay = this.firstDayOfMonth(this.currentMonth, this.currentYear);
		// Get the number of days in the month
		numberDays = this.numberDaysOfMonth(this.currentMonth, this.currentYear);
		
		// Initialise the option element arrays
		var D = new Array();
		var M = new Array();
		var Y = new Array();
		
		// Traverse through the number of days in the current month
		for (var d = 1; d <= numberDays; d++) {
			
			// Build the day form option element
			D[d] = Builder.node('option', { value: d }, d);
			
		};
		
		// Traverse through the months
		for (var m = 0; m < this.months.length; m++) {
			
			// Build the month form option element
			M[m] = Builder.node('option', { value: m }, this.months[m]);
			
		};
		
		// Traverse through the next few years
		for (var y = 0; y < 3; y++) {
			
			// Build the year form option element
			Y[y] = Builder.node('option', { value: (y + this.currentYear - 1) }, (y + this.currentYear - 1));
			
		};
		
		// Build the day select
		DS = Builder.node(
			'select',
			{
				name: 'daySelect',
				id:   'daySelect',
				title:'Select the day for activation'
			},
			[D]
		);
		
		// Add an event listener for the day select
		Event.observe(DS, 'change', this.select.bindAsEventListener(this));
		
		// Build the month select
		MS = Builder.node(
			'select',
			{
				name: 'monthSelect',
				id:   'monthSelect',
				title:'Select the month for activation'
			},
			[M]
		);
		
		
		// Add an event listener for the day select
		Event.observe(MS, 'change', this.select.bindAsEventListener(this));
		
		// Build the year select
		YS = Builder.node(
			'select',
			{
				name: 'yearSelect',
				id:   'yearSelect',
				title:'Select the year for activation'
			},
			[Y]
		);
		
		// Add an event listener for the day select
		Event.observe(YS, 'change', this.select.bindAsEventListener(this));
		
		DS.selectedIndex = this.currentDay - 1;
		MS.selectedIndex = this.currentMonth;
		YS.selectedIndex = 1;
		
		// Append the selects to the element
		$('activationSelect').appendChild(DS);
		$('activationSelect').appendChild(MS);
		$('activationSelect').appendChild(YS);
		
	},
	
	updateDaySelect: function(month)
	{
		
		// Remove all current days
		$('daySelect').update();
		
		// Get the first day of the month
		firstDay = this.firstDayOfMonth(month, this.currentYear);
		// Get the number of days in the month
		numberDays = this.numberDaysOfMonth(month, this.currentYear);
		
		// Check to see if the day set is within the number of days of the new month
		if (this.daySet > numberDays) {
			this.daySet = numberDays;
		}
		
		// Traverse through the number of days in the current month
		for (var d = 1; d <= numberDays; d++) {
			
			if (d == this.daySet) {
				opt = {
					value: d,
					selected: 'true'
				}
			} else {
				opt = { value: d }
			}
			
			// Build the day form option element
			D = Builder.node('option', opt, d);
			
			// Append the current day to the day select
			$('daySelect').appendChild(D);
			
		};
		
		// Set the corresponding day in that month
		$('daySelect').selectedIndex = this.daySet - 1;
		
	},
	
	// Gets the starting day of a month
	firstDayOfMonth: function(month, year)
	{
		// Get the day the month starts on
		monthStart = new Date();
		monthStart.setMonth(month);
		monthStart.setYear(year);
		monthStart.setDate(1);
		// Return the first day
		return monthStart.getDay();
	},
	
	// Gets the number of days of a month
	numberDaysOfMonth: function(month, year)
	{
		// Get the day the month starts on
		monthNum = new Date();
		monthNum.setMonth(month);
		monthNum.setYear(year);
		monthNum.setDate(32);
		numberOfDays = 32 - monthNum.getDate();
		// Return the number of days
		return numberOfDays;
	}
	
}


/**
 * Creates a "form box" element.
 *
 * The class must be invoked after the target field.
 *
 * new omFormWidgets.FormBox(
 *    buildUL,                  <-- The unordered list ID (needs quotes) where the tabs will be built
 *    0,                        <-- The current position of the default choice
 *    [                         <-- The array of choices
 *       [
 *          'id',                                   <-- The id of the associated element
 *          'Value'                                 <-- The tab displayed text
 *       ]
 *    ]
 * );
 *
 *
 * @author  Dan Callaghan
 **/
omFormWidgets.FormBox = Class.create();
omFormWidgets.FormBox.prototype = {
	initialize: function(buildUL, defaultChoice, initTabs, callbackFunc)
	{
		
		// Initialise the build div
		this.buildUL = $(buildUL);
		
		// Initialise the default tab position
		this.defaultChoice = defaultChoice;
		
		// Initialise the tabs
		this.tabs = initTabs;
		
		// Initialise callback function
		this.callbackFunc = callbackFunc;
		
		// Initialise Event binding to be used
		this.bHover = this.hover.bindAsEventListener(this);
		this.bHoverOff = this.hoverOff.bindAsEventListener(this);
		
		this.buildTabs();
		
	},
	
	// Builds the tab for the form box element
	buildTabs: function()
	{
		
		// Make sure the build UL is clean
		this.buildUL.update('');
		
		// Traverse through the tabs
		for (var t = 0; t < this.tabs.length; t++) {
			
			// Initialise the options for this tab
			var opt;
			
			// Build the tab list element
			T = Builder.node('li', this.calculatePosition(t), this.tabs[t][1]);
			
			Event.observe(T, 'click', this.set.bindAsEventListener(this, t));
			
			if (this.tabs[t][2]) {
				T.addClassName('error');
			}
			
			// Add event listeners to the inactive tabs
			if (t != this.defaultChoice) {
				Event.observe(T, 'mouseover', this.bHover);
				Event.observe(T, 'mouseout', this.bHoverOff);
				$(this.tabs[t][0]).hide();
			} else {
				this.tabSet = T;
				this.tabActive = t;
				T.addClassName('on');
				$(this.tabs[t][0]).show();
			}
			
			// Append the tabs to the containing div
			this.buildUL.appendChild(T);
			
		};
		
	},
	
	set: function(e, tabNumber)
	{
		
		if (this.tabSet) {
			this.tabSet.removeClassName('on');
			Event.observe(this.tabSet, 'mouseover', this.bHover);
			Event.observe(this.tabSet, 'mouseout', this.bHoverOff);
		}
		
		// Show and hide containers
		$(this.tabs[this.tabActive][0]).hide();
		$(this.tabs[tabNumber][0]).show();
		this.tabActive = tabNumber;
		
		// Set the new active tab
		this.tabSet = $('tab_' + tabNumber);
		
		// Set to active and stop observing events on this tab
		this.tabSet.removeClassName('over');
		this.tabSet.addClassName('on');
		Event.stopObserving(this.tabSet, 'mouseover', this.bHover);
		Event.stopObserving(this.tabSet, 'mouseout', this.bHoverOff);
		
	},
	
	hover: function(e)
	{
		e.target.addClassName('over');
	},
	
	hoverOff: function(e)
	{
		e.target.removeClassName('over');
	},
	
	// Calculates the positioning of a tab depending on its place
	calculatePosition: function(number)
	{
		
		// Initialise the two positions
		var left;
		var right;
		
		// Initialise the divider
		var divider;
		
		if (this.tabs.length > 3) {
			divider = this.tabs.length;
		} else {
			divider = 3;
		}
		
		// Calculate the width of each tab
		tabLength = 100 / divider;
		
		// Calculate the left and right positions for the tab
		left = (tabLength * number) + '%';
		right = '';
		width = (tabLength - 1);
		
		// If the first tab
		if (number == 0) {
			left = '0px';
		}
		
		// Create the tab position style element
		tabPosition = { style: 'left: ' + left + ';' + right + ' width:' + width + '%;', id: 'tab_' + number, onclick: this.callbackFunc };
		
		// Return the tab position
		return tabPosition;
		
	},
	
	// Modifies a tab to have different behaviours
	modifyTab: function(tabNumber, tabLink, tabDisplay)
	{
		$('tab_' + tabNumber).update(tabDisplay);
		$(this.tabs[tabNumber][0]).hide();
		this.tabs[tabNumber][0] = tabLink;
		$(tabLink).show();
	}
}



/**
 * Creates a BB Code helper toolbar.
 *
 * @author  Tim Neill
 **/
omFormWidgets.BBHelper = Class.create();
omFormWidgets.BBHelper.prototype = {
	initialize: function(targetID, availableButtons)
	{
		if(!targetID) {
			// If the ID is null, grab the first textarea element instead
			var textareas = $$('textarea');
			if (textareas) {
				this.objTextarea = textareas[0];
			} else {
				return;
			}
		} else {
			this.objTextarea = $(targetID);
		}
		
		// Actions available. Images are the tag name with '.png' extension (i.e. pad.png)
		this.bbcode = (availableButtons || $H({
			'b': 		'[b]%1[/b]',
			'i': 		'[i]%1[/i]',
			'u': 		'[u]%1[/u]',
			's': 		'[s]%1[/s]',
			'img': 		'[img]http://www.example.org/image.jpg[/img]',
			'url': 		'[url=http://www.example.org]%1[/url]',
			'email': 	'[email]%1[/email]',
			'color': 	'[color=gray]%1[/color]',
			'list': 	'[list]\n[*]%1\n[/list]',
			'ulist': 	'[ulist]\n[*]%1\n[/ulist]',
			'quote': 	'[quote=Anonymous]%1[/quote]',
			'pad': 		'[pad=2]',
			'line': 	'[line]',
			'link': 	'[link]http://www.example.org[/link]',
			'youtube': 	'[youtube]http://www.youtube.com/watch?v=[/youtube]'
		}));
		
		// Initialise the BBToolbar element. Will contain all of our BB elements and icons
		this.BBToolbar = new Element('div', { id: 'omBBToolbar', style: 'float:left;width:100%' });
		
		this.build();
	},
	
	// Builds the helper toolbar
	build: function()
	{
		// Loop through all of the commands and generate BB elements and icons
		var theCodes = this.bbcode.keys();
		
		for(var i = 0; i < theCodes.length; i++) {
			var toolbarButton = new Element(
				'a',
				{
					className: 'omBBButton',
					href: 'javascript:void(0);',
					title: theCodes[i].capitalize()
				}
			).update('<img src="' + omVars.get('adminIconDir') + 'bbcode/' + theCodes[i] + '.png" alt="' + theCodes[i].capitalize() + '" />');
			
			// Add an event listener for the button
			Event.observe(toolbarButton, 'click', this.insertTag.bind(this, theCodes[i]));
			
			// Add the button to the growing list of BB options
			this.BBToolbar.insert(toolbarButton);
		}
		
		var previewButton = new Element(
			'a',
			{
				href: 'javascript:void(0);',
				title: 'Preview',
				style: 'float:right;margin-right:12px;'
			}
		).update('Preview');
		
		Event.observe(previewButton, 'click', this.createPreview.bind(this));
		
		this.BBToolbar.insert(previewButton);
		
		// Insert a clearing div
		this.BBToolbar.insert(new Element('div', { className: 'clear' }));
		
		// Insert the compiled toolbar into the DOM
		this.objTextarea.insert({ before: this.BBToolbar });
	},
	
	// Does an insert into the current textarea. Method borrowed from PHPMyAdmin
	insertTag: function(tag)
	{
		if (this.objTextarea) {
			if (Prototype.Browser.IE && document.selection) {
				this.objTextarea.focus();
				sel = document.selection.createRange();
				sel.text = this.bbcode.get(tag).sub('%1', sel.text);
			} else if (this.objTextarea.selectionStart || this.objTextarea.selectionStart == '0') {
				var startPos = this.objTextarea.selectionStart;
				var endPos = this.objTextarea.selectionEnd;
				// Reconstruct the string using everything BEFORE the selection, the new text, then everything AFTER the selection
				this.objTextarea.value = this.objTextarea.value.substring(0, startPos)
									   + this.bbcode.get(tag).sub('%1', this.objTextarea.value.substring(startPos, endPos))
									   + this.objTextarea.value.substring(endPos, this.objTextarea.value.length);
			} else {
				this.objTextarea.value += this.bbcode.get(tag).sub('%1', '');
			}
			
			this.objTextarea.focus();
		}
	},
	
	// Creates a preview window below the text box element
	createPreview: function()
	{
		if (this.objTextarea) {
			var result = '';
			
			new Ajax.Request(
				omVars.get('url') + 'base/preview.ajax.php',
				{
					method: 'post',
					asynchronous: false,
					parameters: {
						content: this.objTextarea.value
					},
					onSuccess: function(t, json)
					{
						result = json.previewContent;
					}
				}
			)
			
			if (result || result === '') {
				
				// If the preview pane doesnt exist, we need to create it first
				if (!this.previewPane) {
					
					// Cache the two preview objects we need to attach events to
					var previewContent, previewAction;
					
					// Create the preview pane object and its children
					this.previewPane = Builder.node(
						'div',
						{
							className: 'omBBPreviewWrapper'
						},
						[
							new Element('span', { className: 'previewTitle' }).update('Preview'),
							previewAction  = new Element('a', { className: 'previewAction', href: 'javascript:void(0);' }).update('Close Preview'),
							previewContent = new Element('div', { className: 'omBBPreviewArea' })
						]
					);
					
					// Create event listeners for the preview pane. Cache the preview pane object to maintain scope
					previewPane = this.previewPane;
					Event.observe(previewAction, 'click', function() { previewPane.hide(); });
					
					// Set the class's previewContent object to the one we just created
					this.previewContent = previewContent;
					
					// Insert the preview pane object into the space after the textarea
					this.objTextarea.insert({ after: this.previewPane });
				}
				
				// Insert the new content
				this.previewContent.update(result);
				
				// Show the preview pane if it is otherwise invisible
				if (!this.previewPane.visible()) {
					this.previewPane.show();
				}
				
			}
		}
	}
};
