if (typeof UTILITIES_VERSION == "undefined" || UTILITIES_VERSION < 0.1) {
	alert("A suitable version of the Utilities class is not available");
}

COMBOBOX_VERSION = 0.1;
var usernameDelim = "="; //delimiter used to seperate LDAP usernames from their full name in the drop down list. JS uses this to return only the LDAP username to the text field instead of the complete name as listed inthe drop down list.
var listWidth = "30em"; //set the width of the drop down list.

/*
Sample CSS for the combobox
	.comboBoxList {
		padding: 0px;
		border: 1px solid #999;
		background-color: #f7f7ff;
		overflow: visible;
	}
	.comboBoxItem {
		margin: 0px;
		padding: 2px 5px;
		background-color: inherit;
		cursor: default;
	}
	.comboBoxSelectedItem {
		background-color: #ddf;
	}
*/

/**
 * I create a new combobox, using the specified text input field and
 * population callback.  The item list is styled with three CSS
 * classes: comboBoxList, comboBoxItem, and comboBoxSelectedItem, which
 * are for the containing DIV, the individial item DIVs, and for the
 * currently selected item DIV.  Note that the selected item has both
 * the item and selectedItem classes applied.  Sample CSS is available
 * in a comment at the top of the implementation file.
 * 
 * The 'config' argument allows passing of additional parameters that
 * further govern the behaviour of the combo box.  Supported parameters
 * are listed here:
 * 	allowMultipleValues - whether the form field should allow multiple
 *		values to be provided.  Each individual value will get it's own
 *		separate dropdown with, so a field value such as "dog,ca" would
 *		operate as if the value were just "ca" (i.e. just "ca" would be
 *		passed to the callback, and a selection choice would only
 *		replace the "ca").  Defaults to false.
 *	valueDelimiter - if allowMultipleValues is set to true, this is the
 *		character used to delimit the values.  Defaults to a comma.
 *
 * @param id The ID of the text field the combobox is based around.
 * @param callback The function to call when the typed value changes.
 *		The function will be passed the current value of the field, and
 *		must return an array of values to display in the dropdown.
 * @param config additional config parameters, as explained above.
 * @param config An object containing configuration parameters for the
 *		instance.
 */
function ComboBox(id, callback, callbackUrl, config) {
	var self = this;
	// instance variables
	this.config = config || new Object();
	this.callback = callback;
	this.callbackUrl = callbackUrl;
	this.availableItems = new Array();
	this.selectedItemIndex = -1;
	this.id = id;
	this.field = document.getElementById(id);
	if (typeof this.field == "undefined")
		alert("You have specified an invalid id for the field you want to turn into a combo box");
	this.dropdown = document.createElement("div");
	this.isDropdownShowing = false;
	
	// configure the dropdown div
	this.dropdown.className = "comboBoxList";
	document.body.appendChild(this.dropdown);
	this.dropdown.style.position = 'absolute';
	var offsets = Utilities.getOffsets(this.field);
	this.dropdown.style.top = offsets.y + (this.field.offsetHeight ? this.field.offsetHeight : 22) + "px";
	this.dropdown.style.left = offsets.x + "px";
	this.dropdown.style.width = listWidth; //(this.field.offsetWidth ? this.field.offsetWidth : 100) + "px"
	this.hideDropdown();
	
	// initialize the field
	this.field.comboBox = this;
	this.field.oldValue = this.field.value;
	this.field.onkeyup = ComboBox.onKeyUp;
	this.field.moveCaretToEnd = function() {
		if (this.createTextRange) {
			var range = this.createTextRange();
			range.collapse(false);
			range.select();
		} else if (this.setSelectionRange) {
			this.focus();
			var length = this.value.length;
			this.setSelectionRange(length, length);
		}
	}
	this.field.form.oldonsubmit = this.field.form.onsubmit;
	this.field.onfocus = function() {
		this.form.onsubmit = function() {
			if (this.oldonsubmit) this.oldonsubmit();
			return ! self.isDropdownShowing;
		};
	}
	this.field.onblur = function() {
		var cb = this.comboBox;
		this.hideTimeout = setTimeout(function() { cb.hideDropdown(); }, 100);
		this.form.onsubmit = function() {
			if (this.oldonsubmit) this.oldonsubmit();
			return true;
		};
	}
	
	// privileged methods
	this.getConfigParam = function(name, defVal) {
		return self.config[name] || defVal;
	}
}



/**
 * I am the onKeyDown listener that gets installed on the input field
 * that is the core of the ComboBox.  I handle action operations.
 *
 * @param e The event object on Mozilla browsers, null on IE
 */
ComboBox.onKeyDown = function(e) {
	if (!e) e = window.event;
	var capture = function() {
		e.cancelBubble = true;
		if (e.stopPropagation) e.stopPropagation();
	}
	switch (e.keyCode) {
		case 13: // enter
		case 9: // tab
			this.comboBox.chooseSelection();
			capture();
			return false;
		case 27: // escape
			this.comboBox.hideDropdown();
			capture();
		case 38: // up arrow
			if (window.tsKeyPress) // Hack for Safari bug where the arrow keys fire twice
			{
				delta = e.timeStamp - window.tsKeyPress;
				if (delta < 15) break; // Safari's second trigger fires less than 15ms after the first
			}
			window.tsKeyPress = e.timeStamp;

			this.comboBox.selectPrevious();
			capture();
			break;
		case 40: // down arrow
			if (window.tsKeyPress) // Hack for Safari bug where the arrow keys fire twice
			{
				delta = e.timeStamp - window.tsKeyPress;
				if (delta < 15) break; // Safari's second trigger fires less than 15ms after the first
			}
			window.tsKeyPress = e.timeStamp;

			this.comboBox.selectNext();
			capture();
			break;
	}
}



/**
 * I am the onKeyUp listener that gets installed on the input field
 * that is the core of the ComboBox.  I handle value-change operations.
 *
 * @param e The event object on Mozilla browsers, null on IE
 */
ComboBox.onKeyUp = function(e) {
	if (!e) e = window.event;
	var capture = function() {
		e.cancelBubble = true;
		if (e.stopPropagation) e.stopPropagation();
	}
	switch (e.keyCode) {
		case 38: // up arrow
		case 40: // down arrow
			this.moveCaretToEnd();
			capture();
			break;
		default:
			if (this.value != this.oldValue) {
				this.comboBox.valueChanged();
				this.oldValue = this.value;
			}
			capture();
	}
}



/**
 * I am called by the onKeyUp listener when the entered value changes,
 * and am responsible for invoking the application callback function
 * and repopulating the dropdown, if appropriate.  
 */
ComboBox.prototype.valueChanged = function() {
	var value = this.field.value;
	if (this.getConfigParam("allowMultipleValues", false)) {
		value = value.split(this.getConfigParam("valueDelimiter", ","));
		value = value[value.length - 1].replace(/^ +/, "").replace(/ +$/, "");
	}
	var a = this.callback(value, this);
	if (typeof a == "undefined") // to catch null returns
		return;
	this.setItems(a);
}

/**
 * I am in charge of telling the ComboBox what options should be
 * displayed based on the current value of the field.  I will be called
 * as needed by the ComboBox internals, passed the current value of the
 * field, and can return an array containing the values to display in
 * the dropdown, in the order they should be displayed.
 *
 * In this particular case, I return an array of names that contain the
 * current value anywhere in them.  It would be trivial to make it only
 * match the value at the start of the name (just change the ">=" to
 * "==" on line 58).
 *
 * Note that the return value is optional.  It's merely a convenience
 * so that you don't have to worry about the 'comboBox' parameter for
 * simple usage (as this implementation doesn't).  You could replace
 * the last line of the function with "comboBox.setItems(items);" with
 * no change in behaviour.  This is useful for doing asynchronous JS
 * remoting; inside the callback initiate the request, and then when it
 * comes back, call setItems to reset the item list, without having to
 * lock up the user for the interim.
 *
 * @param value The current value of the form field.
 * @param comboBox A reference to the ComboBox object that called the
 *		function.  It can be used to call setItems at a later time.
 * @return An array of values to display in the combobox dropdown.
 * 
 * @author Matt Peterson
 */
function nameCallback(value, comboBox) {
	var req;
	var items = new Array();
	//alert("nameCallback. ("+value.length+")");
	if (value.length > 0) { // if no value, we'll not provide anything
		var url = eval(comboBox.callbackUrl);
		//alert(comboBox.callbackUrl);
		//alert(url);
	   // Internet Explorer
	   try { req = new ActiveXObject("Msxml2.XMLHTTP"); }
	   catch(e) {
		  try { req = new ActiveXObject("Microsoft.XMLHTTP"); }
		  catch(oc) { req = null; }
	   }
	
	   // Mozailla/Safari
	   if (!req && typeof XMLHttpRequest != "undefined") { req = new XMLHttpRequest(); }
	
	   // Call the processChange() function when the page has loaded
	   if (req != null) {
		  req.onreadystatechange = function () { if (req.readyState == 4 && req.status == 200) { items = req.responseText.split("\n"); comboBox.setItems(items); } }//processChange;
		  req.open("GET", url, true);
		  req.send(null);
		  
	   }
	}
	return items; //comboBox.setItems(items);
}


/**
 * I can be called at any time with a new set of items to display in
 * the dropdown.
 *
 * @param items The array of items that should be used for the dropdown
 *		values.
 */
ComboBox.prototype.setItems = function(items) {
	if (typeof items != "object") {
		alert("setItems wasn't passed a valid array: " + typeof a);
		return;
	}
	this.availableItems = items;
	this.populateDropdown();
}



/**
 * I am called to repopulate the dropdown.  There should never be a
 * need to invoke me externally.
 */
ComboBox.prototype.populateDropdown = function() {
	if (this.availableItems.length > 0) {
		Utilities.removeChildren(this.dropdown);
		for (var i = 0; i < this.availableItems.length-2; i++) {
			var item = document.createElement("div");
			item.className = "comboBoxItem";
			item.innerHTML = this.availableItems[i];
			item.id = "item_" + this.availableItems[i];
			item.comboBox = this;
			item.comboBoxIndex = i;
			item.onmouseover = function() {this.comboBox.select(this.comboBoxIndex);};
			item.onmousedown = function() {this.comboBox.choose(this.comboBoxIndex);};
			this.dropdown.appendChild(item);
		}
		this.selectedItemIndex = 0;
		this.updateSelection();
		this.showDropdown();
	} else {
		this.selectedItemIndex = -1;
		this.hideDropdown();
	}
}



/**
 * I am called by a mouse listener on the dropdown items to choose a
 * specific item straight away.
 *
 * @param index The index of the item to choose
 */
ComboBox.prototype.choose = function(index) {
	if (this.select(index))
		this.chooseSelection();
}



/**
 * I am called by the onKeyUp listener to indicate that the user wants
 * to use the current selection as the new value of the field.
 */
ComboBox.prototype.chooseSelection = function() {
	var i = this.selectedItemIndex;
	var a = this.availableItems;
	if (i >= 0 && i < a.length) {
		var valueToAdd1 = a[i].replace(/<[^>]+>/g, "");
		var valueToAdd = valueToAdd1.substr(0, valueToAdd1.indexOf(usernameDelim)); //added by mpeters5, 11-4-06. Returns LDAP username to text field, rather than name displayed in drop down list
		if (this.getConfigParam("allowMultipleValues", false)) {
			var currentValue = "";
			var delim = this.getConfigParam("valueDelimiter", ",");
			values = this.field.value.split(delim);
			for (var j = 0; j < values.length - 1; j++) {
				currentValue = Utilities.listAppend(currentValue, values[j], delim);
			}
			this.field.value = Utilities.listAppend(currentValue, valueToAdd, delim);
		} else {
			this.field.value = valueToAdd;
		}

		this.field.oldValue = this.field.value;
		this.field.focus();
		this.field.moveCaretToEnd();
		this.hideDropdown();
	}
}



/**
 * I am called by a mouse listener on the dropdown items to select a
 * specific item straight away.
 *
 * @param index The index of the item to select
 * @return whether the selection happened (the index was valid)
 */
ComboBox.prototype.select = function(index) {
	if (index < 0 || index >= this.availableItems.length)
		return false;
	this.selectedItemIndex = index;
	this.updateSelection();
	return true;
}



/**
 * I am called by the onKeyUp listener to indicate that the user wants
 * to select the next option in the dropdown.
 */
ComboBox.prototype.selectNext = function() {
	if (this.selectedItemIndex >= this.availableItems.length - 3)
		return false;
	this.selectedItemIndex++;
	this.updateSelection();
	return true;
}



/**
 * I am called by the onKeyUp listener to indicate that the user wants
 * to select the previous option in the dropdown.
 */
ComboBox.prototype.selectPrevious = function() {
	if (this.selectedItemIndex <= 0)
		return false;
	this.selectedItemIndex--;
	this.updateSelection();
}



/**
 * I show the dropdown DIV.
 */
ComboBox.prototype.showDropdown = function() {
	clearTimeout(this.field.hideTimeout);
	this.dropdown.style.display = 'block';
	this.field.onkeydown = ComboBox.onKeyDown;
	this.isDropdownShowing = true;
}



/**
 * I hide the dropdown DIV.
 */
ComboBox.prototype.hideDropdown = function() {
	var self = this;
	setTimeout(function() {self.isDropdownShowing = false;}, 100);
	this.field.onkeydown = null;
	this.dropdown.style.display = 'none';
}



/**
 * I update the dropdown so that the display reflects the internally
 * selected item,
 */
ComboBox.prototype.updateSelection = function() {
	for (var i = 0; i < this.dropdown.childNodes.length; i++) {
		if (i == this.selectedItemIndex) {
			this.dropdown.childNodes[i].className += " comboBoxSelectedItem";
		} else {
			this.dropdown.childNodes[i].className = this.dropdown.childNodes[i].className.replace(/ *comboBoxSelectedItem */g, "");
		}
	}
}
