/** 
 * Javascript combo box
 * @author Eric S <esatire at gmail dot com>
 * @version 1.1
 *
 * 1.1 02/04/2008	-	Improved behavior when clicking outside element, or pressing escape;
 * 						Many semantic changes. More readable.
 * 1.02 10/01/2008 - Modified setValue 
 * 1.01 12/11/2007 - Alternate for shin1
 *
 * Known Bugs: In Opera onkeydown does not return a value, so can't be cancelled.
 * This can lead to forms being submitted when Enter is pressed.
 */

/** Constructor
 * 
 * Note: When you collect form data, you want to get the value from the select element, not the input.
 *
 * @param containerId{string} - some html element that will hold this entire thing
 * @param selectName{string} - the name of the select box. Enter null or false to have none.
 * @param itemsArray{array} - an array of options - each item in the array has [0]->value and [1]->text
 * @param showButton{bool} - Wether to show a button that allows opening and closing the list
 */
function Combox(containerId,selectName,itemsArray,showButton)
{
	/* ### Private properties ### */
	var me = this;
	var lastValue; // used to skip searches with non-modifying keys (LIKE F2 or CAPSLOCK)
	var listShown = false; // Keeps track of the state of the select element 	
	/* ### DOM handling part ### */
	this.container = document.getElementById(containerId);
	/* This is the simple way to fill up the combo-box. The more intelligent ones have bugs */
	var html = new Array("<input type='text' />");
	if (showButton) html.push("<button>&gt;</button>");
	html.push("<br/><select>");
	for (var i=0;i<itemsArray.length;i++)
	{
		html.push('<option value="'+itemsArray[i][0]+'">'+itemsArray[i][1]+'</option>');
	}
	html.push('</select>');
	this.container.innerHTML = html.join('');

	this.input = this.container.getElementsByTagName('input')[0];
	this.input.style.width = "200px";
	lastValue = this.input.value;

	this.select = this.container.getElementsByTagName('select')[0];
	this.select.style.width = "205px";
	this.select.style.position = 'absolute';
	this.select.style.visibility = 'hidden';
	if (selectName)	this.select.name = selectName;
	this.select.size = 8;
	this.select.selectedIndex = -1;

	if (showButton)
	{
		this.button = this.container.getElementsByTagName('button')[0];
		this.button.style.width = '30px';
		this.button.style.fontWeight = 'bold';
		this.button.title = 'Click here to bring up a list of options';
		this.button.onclick=toggleSelect;
	}

	this.onChange = null; // attach function to run when value changes

	// IE6 memclean, making sure
	html = undefined; // @deprecated = this.container

	/* ### Methods ### */
	/** Sets the select element value, and input text will be set accordingly */
	this.setValue = function(value)
	{
		me.select.value = value;
		me.input.value = me.select.options[me.select.selectedIndex].text;
		lastValue = me.input.value;
	}
	/** Returns valid value, if one is selected, or null */
	this.getValue = function()
	{
		if (this.select.selectedIndex == -1) return null;
		return this.select.value;
	}
	/** Doesn't change the value. Not sure how useful it is. */
	this.setText = function(text)
	{
		this.input.value = text;
	}
	/** If input text is a valid choice - it is returned. Otherwise '' is returned */ 
	this.getText = function()
	{
		if (this.select.selectedIndex == -1) return '';
		return this.input.value;
	}
	/** Shows the select element, if it is hidden */
	this.showList = function()
	{
		if (listShown) return;

		this.select.style.visibility = 'visible';
		listShown = true;
		// Set document event handler - mouse down closes select element
		document.documentElement.onmousedown = this.docMouseDown;
	}
	/** Hides the select element, if it is shown */
	this.hideList = function()
	{
		if (!listShown) return;

		this.select.style.visibility = 'hidden';
		listShown = false;
		// Clear document event handler
		document.documentElement.onmousedown = null
	}
	/** Performs a search based on prefix, and selects a valid value if one exists */
	this.search = function(needle)
	{
		if (!needle)
		{
			this.select.selectedIndex = -1;
			return;
		}

		var curValue;
		var len = this.select.options.length;
		for (var i=-1;++i<len;)
		{
			curValue = this.select.options[i].text.toLowerCase();
			if (curValue.substr(0,needle.length) == needle)
			{
				// we pick the first item in this particular key
				this.select.selectedIndex = i;
				return;
			}
		}
		// not found
		this.select.selectedIndex = -1;
		return;
	}
	/** Assigns the select value to the text field */
	this.acceptSearch = function()
	{
		if (this.select.selectedIndex != -1)
		{
			this.input.value = this.select.options[this.select.selectedIndex].text;
			lastValue = this.input.value;
		}
		else
		{
			lastValue = this.input.value = '';
		}
		this.hideList();		
		// Run custom function if applicable
		if (this.onChange) this.onChange(this.select.value,this.input.value);
	}
	/** Clears the text field, sets value to null */	
	this.abortSearch = function()
	{
		this.input.value = '';
		lastValue = '';
		this.select.selectedIndex = -1;
		this.hideList();
		// Run custom function if applicable
		if (this.onChange) this.onChange(this.select.value,this.input.value);
	}

	/* ### Event Handlers */

	this.input.onkeydown = function(evt)
	{
		var keynum = null;
		if (window.event) keynum = window.event.keyCode;
		else if (evt.which) keynum = evt.which;
		switch (keynum)
		{
	 		case 38: // Up arrow
	 			me.showList();
				if (me.select.selectedIndex == -1) me.select.selectedIndex = 0;
				else if (me.select.selectedIndex == 0)	me.select.selectedIndex = me.select.options.length-1;
				else me.select.selectedIndex--;
	 		break;
			case 40: // Down arrow
				me.showList();
				if (me.select.selectedIndex == me.select.options.length-1) me.select.selectedIndex = 0;
				else me.select.selectedIndex++;
			break;
			case 9: // Tab - When list is open works like enter. If not, moves to next element
				if (!listShown) return true;
			case 13: // Enter
				if (listShown)	me.acceptSearch();
				return false;
			break;
			case 27: // Esc - processed in onkeyup for firefox, returns false here for IE
				return false; // prevents IE from overriding text in input
			break;
		}
	}
	
	this.input.onkeyup = function(evt)
	{
		(window.event) ? window.event.cancelBubble=true : evt.cancelBubble=true;
		var keyCode = getEventKeyCode(evt);
		// Escape must be processed here or Firefox won't allow input value change
		if (keyCode == 27)
		{
			if (listShown)
			{
				me.abortSearch();
				// Another Firefox hack - without blur, another press on ESC will show our last value
				me.input.blur();
			}
			return false;
		}
		// Perform the search, if value had changed
		var needle = this.value.toLowerCase(); // this = the input
		if (needle == lastValue) return;
		lastValue = needle;
		me.showList();
		me.search(needle);
	}

	this.input.onfocus = function () { this.select(); }
	/**
	 * Closes the search with the currently selected value
	 * This is rather uncomfortable, thanks to IE not seperating between OPTIONs and SELECTs
	 * @param {Object} evt
	 */
	this.select.onclick = function(evt)
	{
		(window.event) ? window.event.cancelBubble=true : evt.cancelBubble=true;
		
		me.acceptSearch();
		me.input.focus();
	}
	/** When button is shown, assigned to it */
	function toggleSelect()
	{
		if (!listShown) me.showList();
		else me.acceptSearch();
	}
	/** Any click outside this object should close it and cancel the search as if we pressed ESC */
	this.docMouseDown = function(evt)
	{
		var target = getEventTarget(evt);
		// Ignore internal elements of the combo box
		if (target == me.input || target == me.select || (target.parentNode && target.parentNode == me.select))
		{
			return;
		}
		
		(window.event) ? window.event.cancelBubble=true : evt.cancelBubble=true;
		me.abortSearch();
	}
}