/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ 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 #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, 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'; this.moveDropdown(); 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 this.comboBox.selectPrevious(); capture(); break; case 40: // down arrow 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 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; 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.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 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 - 1) 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() { this.moveDropdown(); 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'; } ComboBox.prototype.moveDropdown = function() { 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 : 100) + "px" } /** * 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, ""); } } }