/*
@Description:	ComboBox class to create dynamic dropdown based on normal 'select' element
@Author:		Daniel Oosterhuis for Bean IT <daniel@bean-it.nl>
@Author:		Arnold Daniels for Bean IT <arnold@bean-it.nl>
@Author:		Ralph Hoedeman for Bean IT <ralph@bean-it.nl>
@Created:		August 2005
@Version:		v1.4

@Todo:			- If you scroll down with the mouse and then try to scroll back up with the arrow key, it won't go
				- If you scroll or press the button and then lose focus the combobox is not blurred
				- Loose the use of global var with eval (still in event handlers)
				- Fix prevent submit on enter (logicaly)
				- If you type fast, the first value is selected and fully when only some chars are typed. Further chars are placed after
				- A disabled option can be selected with keyboard

@Changes:		v1.0 First release.
				v1.1 Function convertToCombobox() now only converts the select into cbo without options. The options are added separately by setOptions()
				v1.2 Use function(){} instead of new Function() and eval wherever possible.
				v1.3 Use z-index only on list and scrollbuttons instead on container
				v1.4 Reder disabled option as disabled: gray and no events.
*/

MAX_SUGGESTIONS 	= 10; 
CONTAINER_CLASS		= 'combobox';
TEXTFIELD_CLASS 	= 'text';
LIST_CLASS 			= 'list';
OPTION_CLASS 		= 'option';
OPTION_HIGHLIGHT_CLASS 	= 'highlight';
BUTTON_CLASS 		= 'button';
SCROLLDIV_CLASS		= 'scrolldiv';
SCROLLUP_CLASS		= 'scrollup';			
SCROLLDOWN_CLASS	= 'scrolldown';
SCROLLDELAY			= 100; // milliseconds for mousedown
IMG_PATH			= "icn/";
z = 100;

/*
	COMBOBOX CLASS
*/
function ComboBox (element, z) // ComboBox class
{
	this.src = element;
	this.literalname = element.name;
	this.name = element.name.replace(/\[|\]/g, '__');	// the safe name of the original element
	this.allOptions = new Array(); 			// the options for this combobox
	this.relevantOptions = new Array(); 	// the options that match the current search string
	this.textBox = null; 					// the textfield
	this.button = null; 					// the button
	this.searchStr = "";					// the string that the user typed
	this.highlighted = null; 				// the option that is currently highlighted
	this.highlightedText = ""; 				// the text belonging to this option
	this.showfrom = 0;						// this value will change if the user scrolls through the list
	this.onlist = false;					// is the mouse on the list?
	this.hotbutton = 0;						// keycode of the button that is currently pressed, or, in case of mouseclick, 10 -> reverts to 0 on keyup or mouseup
	this.src;								// reference to the original <select> element on which this CB is built
	this.x = 0;
	/*
		replace element with this combobox and return a reference to element
	*/
	this.placeCB = function()
	{ 
		this.src.parentNode.replaceChild(this.container, this.src); // place it in DOM
	}
	
	/* 
		create the ComboBox
	*/
	this.createField = function()
	{
		var obj = this;
		
		// the container for this ComboBox
		this.container = document.createElement("DIV");
		this.container.className = CONTAINER_CLASS;
		this.container.style.zIndex = z < 1 ? 1 : z;
		this.container.onmouseover = function () {obj.onlist = true;}
		this.container.onmouseout = function () {obj.onlist = false;}

		// create the textBox that replaces the dropdown
		this.textBox = document.createElement("INPUT");
		this.textBox.type = "text";
		this.textBox.name = this.name + "_str";
		if (this.src.id) this.textBox.id = this.src.id;
		this.textBox.className = TEXTFIELD_CLASS;
		this.textBox.setAttribute('autocomplete','off');
		this.container.appendChild(this.textBox);
		
		this.textBox.onfocus = function () {active_combobox = obj;};
		this.textBox.onblur = function () {if (!obj.onlist) {obj.removeOptions(); obj.blur();}};
		
		// add the button
		this.button = this.createButton('dropit', BUTTON_CLASS, 'down', function () {obj.toggleShowOptions();});

		// quickfix for FF positioning
		if (!navigator.appName.match(/microsoft/i) ) {
			this.button.style.verticalAlign = 'bottom';
			this.textBox.style.verticalAlign = 'top';
		}
		this.container.appendChild(this.button);

		// create a hidden field to keep track of the actual value
		this.passValue = document.createElement("INPUT");
		this.passValue.type = "hidden";
		this.passValue.name = this.literalname;
		this.passValue.id = this.literalname;
		this.container.appendChild(this.passValue)
		
		this.setRecalc();
	}
	
	/*
		Set recalc event for source element (select)
	*/
	this.setRecalc = function()
	{
		var obj = this;
		var fn = function () {
			obj.setOptions();
		}
		if (typeof(this.src.onrecalc) == 'function') {
			var old_onrecalc = this.src.onrecalc;
			this.src.onrecalc = function () {
				fn();
				old_onrecalc();
			}
		} else {
			this.src.onrecalc = fn;
		}
	}
	
	/*
		Add an option to the ComboBox
	*/
	this.addOption = function(val, desc, disabled)
	{ 	
		this.allOptions[this.allOptions.length] = new Array(val, desc, disabled);
	}

		
	/*
		Delete all options
	*/
	this.deleteOptions = function()
	{
		//this.textBox.value = "";
		this.passValue.value = "";
		this.allOptions = new Array();
		this.relevantOptions = new Array();
		//this.removeOptions();
	}

	
	this.selectContents = function()
	{
		selectRange(this.textBox, 0, this.textBox.value.length);
	}
	
	this.toggleShowOptions = function()
	{
		if (!this.removeOptions()){
			this.relevantOptions = this.allOptions;
			this.renderOptions(); 
			this.selectContents();
		}
	}
	
	/*
		Hide the optionlist if it is visible
	*/
	this.removeOptions = function()
	{
		this.showfrom = 0;
		if (sc = document.getElementById(this.name + 'sc')) this.container.removeChild(sc);
		if (opts = document.getElementById('optionlist_'+this.name)){
			this.container.removeChild(opts);
			this.lowlightOption();
			return true;
		}
		return false;
	}
	
	
	/*
		@param	li opt
	*/
	this.setSelected = function(opt, setval)
	{
		this.highlighted = opt;
		opt.className = OPTION_HIGHLIGHT_CLASS;
		if (setval) {
			var txt = opt.childNodes.length && opt.id ? opt.childNodes.item(0).nodeValue : "";
			this.passValue.value = opt.id;
			this.textBox.value = txt;
		}
	}
	
	/*
		Find option with value val and set it
	*/
	this.setValue = function(val)
	{
		var result;
		for (var i=0; i<this.allOptions.length; i++){
			if (this.allOptions[i][0] == val){
				result = this.allOptions[i];
				break;
			}	
		}
		if (result){
			opt = this.createOption(result);
			var txt = opt.childNodes.length && opt.id ? opt.childNodes.item(0).nodeValue : "";
			this.relevantOptions = new Array(new Array(opt.id, txt));
			this.passValue.value = opt.id;
			this.textBox.value = txt;
		}
	}
	
	
	/*
		Select the options that match the string str
		
		@param	string str
		@param	string direction - this is the direction in which the person is typing (forward/backward)
	*/
	this.matchOptions = function(str, direction)
	{
		this.passValue.value = "";
		this.searchStr = str;
		this.removeOptions();
		this.relevantOptions = new Array();
		
		if (str != "") {
			pattern = str.replace(/(\W)/g, "\\$1"); // escape all alphanumeric characters to ensure valid regex
			var re = new RegExp("^" + pattern, "i");
			var test = false;
			for (i=0; i<this.allOptions.length; i++) {
				if (test || re.test(this.allOptions[i][1])) {
					test = true;
					this.relevantOptions[this.relevantOptions.length] = this.allOptions[i];
				}
			}
			
			// auto complete the textbox with the top suggestion 
			if ( this.relevantOptions.length > 0 && direction == "forward"){
				if (this.textBox.createTextRange || this.textBox.setSelectionRange) {
					var iLen = this.searchStr.length; 
					this.textBox.value = this.relevantOptions[0][1];
					selectRange(this.textBox, iLen, this.relevantOptions[0][1].length);
				}
			}
		} else {
			this.relevantOptions = this.allOptions;
		}
		this.showfrom = 0;
		this.renderOptions();
		this.selectFirst();	// select the top option
	}
	

	/*
		select the top option
	*/
	this.selectFirst = function(setval) {
		if (list = document.getElementById('optionlist_'+this.name)){
			var opt = list.firstChild;
			this.setSelected(opt, setval); 
			return true;
		}
		return false;
	}
	
	
	/*
		handle the down-arrow
	*/
	this.selectNext = function()
	{
		if (!document.getElementById('optionlist_'+this.name)) this.matchOptions(this.searchStr, "");
	
		if (this.relevantOptions.length > 0){	
			if (!this.highlighted) {
				if (this.selectFirst(true))	return;
			}

			next = this.highlighted.nextSibling;
			if (next) {
				this.highlighted.className = OPTION_CLASS;
				this.setSelected(next, true);
				//fixed last option not shown. Removed +1 after MAX_SUGGESTIONS
			} else if (this.showfrom + MAX_SUGGESTIONS  < this.relevantOptions.length){ // er moet omlaag gescrolld worden
				if (this.highlighted){
					this.highlighted.className = OPTION_CLASS;
					this.highlighted = null;
				}
				this.scrollDown('key'); 
			}
		}
	}
	
	/* 
		handle the up-arrow
	*/
	this.selectPrevious = function()
	{
		if (!this.highlighted) return;
		this.highlighted.className = OPTION_CLASS;
		prev = this.highlighted.previousSibling;
		if (prev){
			this.setSelected(prev, true);
		} else {
			if (this.showfrom > 0) this.scrollUp('key');
		}
	}
	
	
	/*
		Do something when the user types in the box
	*/
	this.handleAction = function(event)
	{
		var type_direction = "";
		
		if (event){
			var typed = event.keyCode; // figure out which key was pressed
			switch (typed){
				case 8: // backspace
				case 46: // delete
					this.passValue.value = "";
					type_direction = "delete";
					break;
				case 37: // arrow left
					type_direction = "backward";
					break;
				case 13: // enter
					if (this.highlighted && this.highlighted.id) this.selectOption(this.highlighted.id, this.highlighted.innerText); 
					return;
				case 38: // arrow up
					this.selectPrevious();
					break;
				case 40: // arrow down
					this.selectNext();
					break;
				default:
					// Only visible characters
					if (typed == 32 || typed >= 48) type_direction = "forward";
					break;
			}
		}
		if (type_direction != "") {
			this.matchOptions(this.textBox.value, type_direction);
		}

		return true;
	}
	
	this.lowlightOption = function()
	{
		if (this.highlighted) {
			this.highlighted.className = OPTION_CLASS;
			this.highlighted = null;
		}
	}
	
	this.highlightOption = function(opt)
	{
		this.lowlightOption();
		this.setSelected(opt, false);
		//opt.id ? this.setSelected(opt) : this.highlighted = null;
	}

	this.createButton = function(id, css, direction, action)
	{
		var select = element;
		var bt = document.createElement('button');
		bt.className = css;
		bt.id = this.name + id;
		bt.setAttribute("type", "button");
		bt.setAttribute("tabIndex", -1);

		var obj = this;
		bt.onmousedown = function () {
			active_combobox = obj;
			obj.textBox.focus();
			obj.onlist = true;
			if (typeof(action) == 'function') action();
		};
		bt.onblur = function () {if (!obj.onlist) obj.removeOptions();};
		
		// the arrow in the button
		var img = document.createElement("IMG");
		img.src = IMG_PATH + "arrow_"+direction+".gif";
		bt.appendChild(img);
		
		return bt;
	}
	
	this.blur = function()
	{
		if (active_combobox === this) {
			if (this.passValue.value == "" && this.textBox.value != "") {
				window.alert("De optie '" + this.textBox.value + "' komt niet voor in de lijst.");
				this.textBox.value = "";
				
			}
			active_combobox = null;
		}
	}
	
	/*
		Check if the list requires scrollbuttons, and, if so, create the scrollbar and the appropriate button(s)
	*/
	this.makeScrollButtons = function()
	{
		scrollhold = document.getElementById(this.name + 'sc');
		if (this.relevantOptions.length > MAX_SUGGESTIONS){
			var mustAppend = false;
			if (!scrollhold){
				scrollhold = document.createElement('DIV');
				scrollhold.className = SCROLLDIV_CLASS;
				scrollhold.id = this.name + 'sc';
				scrollhold.style.zIndex = z < 1 ? 1 : z;
				this.container.appendChild(scrollhold);
			}
			while (scrollhold.firstChild) scrollhold.removeChild(scrollhold.firstChild); // remove scrollbuttons
			
			if (this.showfrom > 0){ // need scrollup button
				scrollbutton = this.createButton('_scrup', SCROLLUP_CLASS, 'up');
				scrollbutton.style.zIndex = z < 1 ? 1 : z;
				scrollhold.appendChild(scrollbutton);
				mustAppend = true;
			}

			if (this.showfrom + MAX_SUGGESTIONS + 1 < this.relevantOptions.length){ // scrolldown too
				scrollbutton = this.createButton('_scrdw', SCROLLDOWN_CLASS, 'down');
				scrollbutton.style.zIndex = z < 1 ? 1 : z;
				scrollhold.appendChild(scrollbutton);
				mustAppend = true;
			}
			
			if (!mustAppend) this.container.removeChild(scrollhold);
		} else {
			if (scrollhold) this.container.removeChild(scrollhold);
		}
	}

	
	/*
		To be called privately
	*/
	this._scrollDown = function()
	{
		var list = document.getElementById('optionlist_'+this.name);
		var lastOption = list.lastChild;

		if (this.relevantOptions.length > (MAX_SUGGESTIONS + this.showfrom) && lastOption){
			this.lowlightOption();
			this.showfrom++;

			list.removeChild(list.firstChild);
			var next = this.createOption(this.relevantOptions[MAX_SUGGESTIONS+this.showfrom-1]);
			list.appendChild(next);
			this.setSelected(next, false);
		}
		this.makeScrollButtons();
	}
	
	/*
		To be called privately
	*/
	this._scrollUp = function()
	{
		if (this.showfrom == 0) return;
		var list = document.getElementById('optionlist_'+this.name);
		var firstOption = list.firstChild;
		if (this.showfrom > 0 && firstOption){
			this.lowlightOption();
			this.showfrom--;
			list.removeChild(list.lastChild);
			var prev = this.createOption(this.relevantOptions[this.showfrom]);
			list.insertBefore(prev, list.firstChild);
			this.setSelected(prev, false);
		}
		this.makeScrollButtons();
	}
	
	
	/* 
		this function is triggered either by mouseclick or by pressing arrow-down
		while the action is maintained, keep scrolling
	*/
	this.scrollDown = function(triggeredBy)
	{
		this._scrollDown();
		if (triggeredBy == "mouse" && this.hotbutton == 10){ 
			setTimeout(this.name + "_cmb.scrollDown('mouse')", SCROLLDELAY);
		}
	}
	

	/* 
		this function is triggered either by mouseclick or by pressing arrow-down
		while the action is maintained, keep scrolling
	*/
	this.scrollUp = function(triggeredBy)
	{
		this._scrollUp();
		if (triggeredBy == "mouse" && this.hotbutton == 10){ 
			setTimeout(this.name + "_cmb.scrollUp('mouse')", SCROLLDELAY);
		}
	}


	this.createOption = function(opt)
	{
		var li = document.createElement('li');
		li.setAttribute('id', opt[0]);
		li.appendChild (document.createTextNode(opt[1]));
		
		var obj = this;
		if (!opt[2]) {
			li.onclick = function () {obj.selectOption(this);};
			li.onmouseover = function () {obj.highlightOption(this);};
			li.onmouseout = function () {obj.lowlightOption();};
		} else {
			li.style.color = "gray";
		}
		li.className = OPTION_CLASS;
		return li;
	}

	/*
		Render the options that we want to see right now
		Limit to MAX_SUGGESTIONS and place a scrollbar if necessary
	*/
	this.renderOptions = function()
	{
		list = document.createElement('ul');
		list.id = 'optionlist_'+this.name;

		var textboxwidth = this.textBox.currentStyle ? this.textBox.currentStyle.width : parseInt(document.defaultView.getComputedStyle(this.textBox, '').getPropertyValue("width"));
		if (list.style) list.style.width = textboxwidth + (!navigator.appName.match(/microsoft/i) ? "px" : "");
		
		var obj = this;
		list.onmouseout = function () {obj.onlist = false};
		list.onmouseover = function () {obj.onlist = true;};
		list.className = LIST_CLASS;
		list.style.zIndex = z < 1 ? 1 : z;

		for (i=0; i < this.relevantOptions.length; i++) {
			if (i == MAX_SUGGESTIONS) break; // don't let the list get too long
			var li = this.createOption(this.relevantOptions[i]);
			list.appendChild(li);
		}
		if (i > 0) this.container.appendChild(list);
		if (this.relevantOptions.length > MAX_SUGGESTIONS) this.makeScrollButtons(); // do we need a scrollbar?	
	}

	
	/*
		Select the option opt
		this function is triggered by after the user selects an option
	*/
	this.selectOption = function (opt)
	{
		if (!opt) opt = this.highlighted;
		if (!opt) return;
		
		if (this.removeOptions() && opt){
			this.passValue.value = opt.id;
			if (opt.childNodes.length) var txt = opt.childNodes.item(0).nodeValue; 
			this.relevantOptions = new Array(new Array(opt.id, txt));
			this.textBox.value = opt.id ? txt : "";

		}

		this.src.value = opt.id;
		this.blur(); // unset global variable active_combobox
		
		if (typeof this.src.onchange == 'function'){
			this.src.onchange();
		}
	}
	
	/*
		expects a select element as src
	*/
	this.setOptions = function ()
	{
		this.deleteOptions();
		
		for (i=0; i<this.src.options.length; i++) {
			if (this.src.options[i].text != "") { this.addOption(this.src.options[i].value, this.src.options[i].text, this.src.options[i].disabled); }
			if (this.src.options[i].getAttribute('selected') && this.src.options[i].value != "") this.setValue(this.src.options[i].value);// set the selected value
		} 
	}	

	this.createField(); // DO IT!!!
	
}; // class ComboBox





/*
	GENERAL FUNCTIONS NEEDED FOR THE COMBOBOX
*/
active_combobox = null;
just_blurred = false; // make sure no events are handled for comboboxes that have just lost focus

// global key handlers
function handleKeyDown(e)
{
	just_blurred = false;
	if (!e) e = window.event; // capture IE/Mozilla event
	e.srcElement ? theBox = e.srcElement : theBox = e.target;
	if (theBox.name && theBox.name.match(/_str$/, "i")){
		objName = theBox.name.replace(/_str$/, "_cmb");
	} else {
		return;
	}

	if (!just_blurred){
		if (eval("active_combobox = " + objName)){
			active_combobox.hotbutton = e.keyCode;				
		}
	}		

	if (active_combobox instanceof ComboBox) {
		if (e.keyCode == 13 || e.keyCode == 9) { // handle enter/tab key
			active_combobox.selectOption();
			//active_combobox.blur();
			just_blurred = true;
		}
	
		if (e.keyCode == 38 || e.keyCode == 40){ // scroll
			active_combobox.handleAction(e);
		}
	}
}

function handleKeyUp(e)
{
	if (!e) e = window.event; // capture IE/Mozilla event
	if (active_combobox instanceof ComboBox){
		active_combobox.hotbutton = 0;
		if (e.keyCode != 38 && e.keyCode != 40) active_combobox.handleAction(e);
	}
}


// global mousehandlers
function handleMouseDown(e)
{
	if (!e) e = window.event; // capture IE/Mozilla event
	e.srcElement ? src = e.srcElement : src = e.target;
	if (active_combobox instanceof ComboBox){
		active_combobox.hotbutton = 10;
		if (src && src.id.match(/_scrup$/)) active_combobox.scrollUp('mouse');
		if (src && src.id.match(/_scrdw$/)) active_combobox.scrollDown('mouse');
	}
}

function handleMouseUp(e)
{
	if (active_combobox instanceof ComboBox){
		active_combobox.hotbutton = 0;
	}
}

function prevEnter(e)
{
	if (!e) e = window.event;
	if (active_combobox || just_blurred) {
		navigator.appName.match(/microsoft/i) ? e.returnValue = false : e.preventDefault();	// prevent form submission
	}
}

// let key and mouse events be caught by their event handler
document.onkeydown = handleKeyDown;
document.onkeyup = handleKeyUp;
document.onmouseup = handleMouseUp;
document.onmousedown = handleMouseDown;



function ConvertAllToCombobox()
{
	// keep a reference to this node
	var nodes = document.getElementsByTagName("SELECT");
	var boxes = new Array();
	var cur = 0;
	
	while (nodes.length > cur) {
		if (nodes[cur].getAttribute('multiple')) {
			cur++;
			continue;
		}
		boxes[boxes.length] = ConvertToCombobox(nodes[cur]);
	}

	for (var i=0; i < boxes.length; i++){
		boxes[i].setOptions();
	}

	// to prevent form submission when the user presses enter in a ComboBox
/*	forms = document.getElementsByTagName("FORM");
	i = 0;
	while (i < forms.length) {
		addEvent(forms[i], "submit", prevEnter);	
		i++;
	}
*/
	return boxes;
}

/*
	convert an element to a combobox
*/
function ConvertToCombobox(element) {
	if (element.tagName != 'SELECT') return false;

	box = new ComboBox(element, z);
	box.placeCB();
	
	// make this ComboBox available in the global scope
	eval(element.name.replace(/\[|\]/g, '__') + "_cmb = box");
	
	z--; // decrement the z-index value so each ComboBox opens over the boxes below it
	return box;
}
