/** * AutoComplete * @copyright (c) 2007, Beau D. Scott * @author Beau D. Scott * @version 1.2 * Oct 11, 2007 */ var AutoComplete = Class.create(); var Selector = Class.create(); var currentSelectedElement = null; Selector.prototype = { initialize: function(bindField, size) { this._element = document.createElement("DIV"); this._maxSize = 15; this._element.style.backgroundColor = "white"; this._element.style.border = "1px solid #000000"; //IE this._element.style.overflowX = "hidden"; this._size = 10; this._fixedHeight = false; this._element.style.overflow = "hidden"; this._element.autocomplete = 'off'; this._bindField = bindField; this._selectedIndex = 0; this._options = 0; this._currentSelected = null; }, getMaxSize : function() { return this._maxSize }, getCurrentSelected : function() { return this._currentSelected; }, getElement: function() { return this._element; }, getBindField: function() { return this._bindField; }, getSize: function() { return this._size; }, setSize: function(size) { this_size = size; }, getOptions: function() { return this._element.childNodes.length; }, hasOptions: function() { return this._element.childNodes != null; }, clearOptions : function() { this._element.style.height ="auto"; this._element.style.overflow = "visible"; while (this._element.firstChild) { this._element.removeChild(this._element.firstChild); }; this._element.style.display = 'none'; }, getSelectedIndex: function() { return this._selectedIndex; }, setSelectedIndex: function(no, down) { var element = this._element.childNodes[no]; if (element.scrollIntoView) { if (this._element.style.overflow == "auto" || this._element.style.overflow =="") { if (down) { if (no == 0) { element.scrollIntoView(true); } else { if ((no + 1) * (document.all ? 22 : 20) > parseInt(this._element.style.height)) { element.scrollIntoView(false); } } } else { if (no == this._element.childNodes.length -1) { element.scrollIntoView(false); } else { // if (no - 17 > 0 && this._element.scrolled) { element.scrollIntoView(true); // } } } } } this._selectedIndex = no; if (this._currentSelected) { this._currentSelected.style.backgroundColor = "white"; this._currentSelected.style.color = "#000000" } element.style.backgroundColor = "#D2212A"; element.style.color = "#ffffff"; this._currentSelected = element; } , getValue: function() { var value = this._currentSelected.getAttribute("xvalue") || this._currentSelected.innerText || this._currentSelected.textContent ||this._currentSelected.innerHTML; return stripCountryCode(value); } , setValue: function(value) { this._value = value; }, addOption: function(opt) { this._element.appendChild(opt); // Event.observe(opt, 'click', this.selectOption.bindAsEventListener(this)); }, selectOption: function(event) { } } AutoComplete.prototype = { /** * @param {Object} bindField ID of form element, or dom element, to suggest on * @param {String} URL of dictionary * @param {Object} options */ initialize: function(bindField, action, options) { this.options = Object.extend({ /** * @param {Number} Number of options to display before scrolling */ size: 10, /** * @param {String} CSS class name for autocomplete selector */ cssClass: null, /** * @param {Object} JavaScript callback function to execute upon selection */ onSelect: null, /** * @param {Number} minimum characters needed before an suggestion is executed */ threshold: 3, /** * @param {Number} Time delay between key stroke and execution */ delay: .2, fontSize : "11pt" }, options); this.action = action; this.bindField = bindField; this.selector = new Selector($(this.bindField, this.options.size)), this._elements = { input: $(this.bindField) }; if(!this._elements.input) alert('Failed to bind to form field[' + this.options.bindField + ']'); if(!this.action) alert('No action url specified'); this._timeout = null; this._visible = false; this.initialized = false; Event.observe(window, 'load', this.draw.bind(this)); Event.observe(window, 'resize', this._reposition.bind(this)); Event.observe(window, 'scroll', this._reposition.bind(this)); }, /** * Initializes the UI components of the object * @param {Object} e Event */ draw: function(e) { if(this.initialized) return; if(this.options.cssClass) this.selector.getElement().className = this.options.cssClass; Element.setStyle(this.selector.getElement(), { display: 'none', position: 'absolute', width: this._elements.input.offsetWidth -2 + 'px' }); this.selector.setSize(this.options.size); var selectorElement = this.selector.getElement(); selectorElement.style.zIndex= 200000; document.body.appendChild(selectorElement); Event.observe(this._elements.input, 'keyup', this.suggest.bindAsEventListener(this)); Event.observe(this._elements.input, 'keydown', this.suggest.bindAsEventListener(this)); // Event.observe(this._elements.input, 'blur', this.hide.bindAsEventListener(this)); var thisO = this; Event.observe(selectorElement, 'focus', this.show.bindAsEventListener(this)); Event.observe(document.body, 'click', function(e) { if (e) { element = Event.element(e); if (element.getAttribute("index") ==null) { thisO.hide(); } else { if (thisO ==currentSelectedElement && element.getAttribute("index") !=null) { if (thisO._currentSelected) { thisO._currentSelected.style.backgroundColor = "white"; thisO._currentSelected.style.color = "#000000" } element.style.backgroundColor = "D2212A"; element.style.color = "#ffffff"; var value = element.getAttribute("xvalue") || element.innerText || element.textContent || element.innerHTML; thisO._elements.input.value = stripCountryCode(value); thisO.hide(); } } } }); this.initialized = true; }, hide: function(e) { if(e) { trigger = Event.element(e); if (trigger.tagName == "DIV" && trigger.getAttribute("index") !=null) { if (this._currentSelected) { this._currentSelected.style.backgroundColor = "white"; this._currentSelected.style.color = "#000000" } trigger.style.backgroundColor = "D2212A"; trigger.style.color = "#ffffff"; this._elements.input.value = this.selector.getValue(); } } if(this._visible) { var trigger = null; if(e) { trigger = Event.element(e); } this._visible = false; Element.setStyle(this.selector.getElement(),{ display: 'none' }); // FF hack, wasn't selecting without this small delay for some reason setTimeout(this._restoreFocus.bind(this),50); } }, _restoreFocus: function() { this._elements.input.focus(); }, show: function(e) { var trigger = null; if(e) trigger = Event.element(e); if(this.selector.hasOptions()) { if(window.Scriptaculous && trigger != this._elements.selector) { new Effect.BlindDown(this._elements.selector,{ duration: this.options.delay, queue: 'end' }); } else { Element.setStyle(this.selector.getElement(),{ display: 'inline' }); } this._reposition(); this._visible = true; } }, /** * Removes the timeout function set by suggest */ _cancelTimeout: function() { if(!this._timeout) return; clearTimeout(this._timeout); this._timeout = null; }, /** * Triggers the suggest interaction * @param {Object} e Event */ selectItem : function(e) { if(e) { trigger = Event.element(e); currentSelectedElement = this; } }, suggest: function(e) { this._cancelTimeout(); var key = Event.keyPressed(e); var ignoreKeys = [ 20, // caps lock 16, // shift 17, // ctrl 91, // Windows key 121, // F1 - F12 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 45, // Insert 36, // Home 35, // End 33, // Page Up 34, // Page Down 144, // Num Lock 145, // Scroll Lock 44, // Print Screen 19, // Pause 93 // Mouse menu key ]; if(ignoreKeys.indexOf(key) > -1) return true; if(e.type == 'keydown') { if(Event.KEY_ESC == key) { this.cancel(); Event.stop(e); return false; } if(Event.KEY_TAB == key) { this.select(e); return true; } return true; } switch(key) { case Event.KEY_LEFT: case Event.KEY_RIGHT: return true; break; case Event.KEY_TAB: case 46: //Delete this.cancel(); return true; break; case Event.KEY_RETURN: this.select(e); Event.stop(e); return true; break; case Event.KEY_ESC: this.cancel(); return false; break; case Event.KEY_UP: case Event.KEY_DOWN: this.interact(e); Event.stop(e); return false; break; case Event.KEY_BACKSPACE: default: break; } if(this._elements.input.value.length < this.options.threshold) { this.selector.clearOptions(); return true; } else { this._timeout = setTimeout(this._sendRequest.bind(this), 1000 * this.options.delay); } }, _sendRequest: function() { this._request = new Ajax.Request(this.action + encodeURIComponent(this._elements.input.value), { onComplete: this._process.bind(this) }); }, /** * Adjusts the positioning of the suggestion box between displays and on screen resizing * @param {Object} e Event */ _reposition: function(e) { if(!this.initialized) return; var pos = Position.cumulativeOffset(this._elements.input); pos.push(pos[0] + this._elements.input.offsetWidth); pos.push(pos[1] + this._elements.input.offsetHeight); Element.setStyle(this.selector.getElement(),{ left: pos[0] + 'px', top: pos[3] + 'px' }); }, /** * Processes the resulting XML from a suggestion request, adds options to the suggestion box. * @param {Object} objXML XML */ _process: function(objXML) { this.selector.clearOptions(); var xml = objXML.responseXML; if(!xml) { return false; } //var suggestions = xml.getElementsByTagName('suggestion'); var suggestions = xml.getElementsByTagName('s'); for(i = 0; i < suggestions.length; i++) { var nodeText = suggestions.item(i).firstChild.nodeValue; // nodeText = nodeText.replace(/\(\*\)/, "(All airports)"); var xValue = nodeText.replace(/\s+\[[A-Z][A-Z]\]$/, ""); var countryCode = nodeText.split("[")[1].substring(0, 2); suggestion = stripCountryCode(nodeText); var opt = document.createElement("DIV"); opt.style.fontSize = this._elements.input.style.fontSize || "10pt"; Event.observe(opt, 'click', this.selectItem.bindAsEventListener(this)); //var type = suggestions.item(i).getAttribute("type"); opt.className = "autosuggestItem" ; opt.style.borderBottom = "1px solid #e1e1e1"; opt.innerHTML = "" + suggestion ; opt.setAttribute("index", i); opt.setAttribute("xvalue", xValue); this.selector.addOption(opt); } if(this.selector.getOptions() > (this.options.size)) this.selector.setSize(this.options.size); else this.selector.setSize(this.selector.getSize() > 1 ? this.selector.getOptions() : 2); if(this.selector.hasOptions()) { this.setHeight(); this.selector.setSelectedIndex(0); this.show(); } else { this.cancel(); } }, /** * Clears and hides the suggestion box. * @param {Object} e Event */ cancel: function(e) { this.hide(e); this.selector.clearOptions(); }, /** * Captures the currently selected suggestion option to the input field * @param {Object} e Event */ select: function(e) { if(this.selector.getOptions() >0) { this._elements.input.value = this.selector.getValue(); } this.cancel(); if(typeof this.options.onSelect == 'function') { this.options['onSelect'](this._elements.input); } }, setHeight: function(e) { if (this.selector.getOptions() >= this.selector.getMaxSize()) { this.selector.getElement().style.overflow = "auto"; this.selector.getElement().style.height = 1 + this.selector.getMaxSize() * (document.all ? 20 : 21) + "px"; } function getScrollHeight() { var height; if (document.compatMode && document.compatMode != "BackCompat") { theHeight = document.documentElement.clientHeight; } else { theHeight = document.body.clientHeight; } return theHeight; } }, /** * Processes key interactions with the input field, including navigating the selected option * with the up/down arrows, esc cancelling and selecting the option. * @param {Object} e */ interact: function(e) { if(!this._visible) return; var key = Event.keyPressed(e); if(key != Event.KEY_UP && key != Event.KEY_DOWN) return; var mx = this.selector.getSize(); if(key == Event.KEY_UP) { if(this.selector.getSelectedIndex() == 0) { this.selector.setSelectedIndex(this.selector.getOptions()-1, false); } else this.selector.setSelectedIndex(this.selector.getSelectedIndex()-1, false); } else { if(this.selector.getSelectedIndex() == this.selector.getOptions()-1) { this.selector.setSelectedIndex(0, true) } else { this.selector.setSelectedIndex(this.selector.getSelectedIndex()+1, true); } } } }; /** * Various Prototype Event extensions */ Object.extend(Event, { KEY_BACKSPACE: 8, KEY_TAB: 9, KEY_RETURN: 13, KEY_ESC: 27, KEY_LEFT: 37, KEY_UP: 38, KEY_RIGHT: 39, KEY_DOWN: 40, KEY_DELETE: 46, KEY_SHIFT: 16, KEY_CONTROL: 17, KEY_CAPSLOCK: 20, KEY_SPACE: 32, keyPressed: function(event) { return document.all ? window.event.keyCode : event.which; } }); //util function stripCountryCode(value) { var index = value.indexOf(" ["); if (index != -1) { return value.substring(0, index); } return value; }