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

COMBOBOX_VERSION = 0.1;

/*
Sample CSS for the combobox
.comboBoxList {
	padding: 0px;
	border: 1px solid #000;
	background-color: #ddeeff;
	overflow: auto;
	max-height: 300px;
	z-index: 100;
}
.comboBoxItem {
	margin: 1px;
	padding: 2px 5px;
	background-color: inherit;
	cursor: default;
	white-space: nowrap;
	color: #00f;
}
.comboBoxSelectedItem {
	background-color: #a9d1ff;
}
*/

/**
 * 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, config) {
	var self = this;
	// instance variables
	this.config = config || new Object();
	this.callback = callback;
	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 = this.field.offsetWidth ? this.field.offsetWidth + "px" : "auto"
	this.hideDropdown();
	
	// initialize the field
	this.field.comboBox = this;
	this.field.oldValue = this.field.value;
	this.field.setAttribute("autocomplete","off");
	this.field.form.setAttribute("autocomplete","off");
	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;
		e.returnValue = false;
		e.preventDefault && e.preventDefault();
		e.stopPropagation && e.stopPropagation();
		return false;
	}
	switch (e.keyCode) {
		case 13: // enter
		case 9: // tab
			this.comboBox.chooseSelection();
			return capture();
		case 27: // escape
			this.comboBox.hideDropdown();
			return capture();
		case 38: // up arrow
			this.comboBox.selectPrevious();
			return capture();
		case 40: // down arrow
			this.comboBox.selectNext();
			return capture();
	}
}



/**
 * 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 self = this;
	clearTimeout(this.changeTimeout);
	this.changeTimeout = setTimeout(function() {
		var a = self.callback(value, self);
		if (typeof a == "undefined") { // to catch null returns
			a = [];
		}
		self.setItems(a);
	}, 250);
}



/**
 * 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;
	}
	var values = this.field.value.split(this.getConfigParam("valueDelimiter", ","));
	for (var i = items.length - 1; i >= 0; i--) {
		for (var j = 0; j < values.length; j++) {
			if (items[i].replace(/ *<[^>]+> */g, "") == values[j]) {
				items.splice(i, 1);
				break;
			}
		}
	}
	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() {
//	this.dropdown.scrollTo(0, 0);
	Utilities.removeChildren(this.dropdown);
	if (this.availableItems.length > 0) {
		for (var i = 0; i < this.availableItems.length; 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.onclick = function() {this.comboBox.choose(this.comboBoxIndex);};
			this.dropdown.appendChild(item);
		}
		this.selectedItemIndex = 0;
		this.updateSelection();
		this.showDropdown();
	} else {
		this.selectedItemIndex = -1;
		this.updateSelection();
		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 valueToAdd = a[i].replace(/ *<[^>]+> */g, "");
		if (this.getConfigParam("allowMultipleValues", false)) {
			var delim = this.getConfigParam("valueDelimiter", ",");
			var values = this.field.value.split(delim);
			values.pop(); // kill the one we're completing
			values.push(valueToAdd);
			this.field.value = values.join(delim) + 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 - 1) {
		this.selectedItemIndex = 0;
	} else {
		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) {
		this.selectedItemIndex = this.availableItems.length - 1;
	} else {
		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;
	Utilities.hideCollidingElements(this.dropdown);
}



/**
 * 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';
	Utilities.showCollidingElements(this.dropdown);
}



/**
 * 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, "");
		}
	}
}