/* * * Ajax Autocomplete for Prototype, version 1.0.4 * (c) 2010 Tomas Kirda * * Ajax Autocomplete for Prototype is freely distributable under the terms of an MIT-style license. * For details, see the web site: http://www.devbridge.com/projects/autocomplete/ * * Adapted by Will 11/4/2011 to take a 'multiple' option and work with a comma-separated list of values. * Type-ahead would be nice too. */ var Autocomplete = function(el, options){ this.el = $(el); this.id = this.el.identify(); this.el.setAttribute('autocomplete','off'); this.suggestions = []; this.data = []; this.badQueries = []; this.selectedIndex = -1; this.currentValue = this.el.value; this.intervalId = 0; this.cachedResponse = []; this.instanceId = null; this.onChangeInterval = null; this.ignoreValueChange = false; this.serviceUrl = options.serviceUrl; this.options = { autoSubmit:false, minChars:1, maxHeight:300, deferRequestBy:0, width:0, container:null }; if(options){ Object.extend(this.options, options); } if(Autocomplete.isDomLoaded){ this.initialize(); }else{ Event.observe(document, 'dom:loaded', this.initialize.bind(this), false); } }; Autocomplete.instances = []; Autocomplete.isDomLoaded = false; Autocomplete.getInstance = function(id){ var instances = Autocomplete.instances; var i = instances.length; while(i--){ if(instances[i].id === id){ return instances[i]; }} }; Autocomplete.highlight = function(value, re){ return value.replace(re, function(match){ return '' + match + '<\/strong>'; }); }; Autocomplete.prototype = { killerFn: null, initialize: function() { var me = this; this.killerFn = function(e) { if (!$(Event.element(e)).up('.autocomplete')) { me.killSuggestions(); me.disableKillerFn(); } } .bindAsEventListener(this); if (!this.options.width) { this.options.width = this.el.getWidth(); } var div = new Element('div', { style: 'position:absolute;' }); div.update('
'); this.options.container = $(this.options.container); if (this.options.container) { this.options.container.appendChild(div); this.fixPosition = function() { }; } else { document.body.appendChild(div); } this.mainContainerId = div.identify(); this.container = $('Autocomplete_' + this.id); this.fixPosition(); Event.observe(this.el, window.opera ? 'keypress':'keydown', this.onKeyPress.bind(this)); Event.observe(this.el, 'keyup', this.onKeyUp.bind(this)); Event.observe(this.el, 'blur', this.enableKillerFn.bind(this)); Event.observe(this.el, 'focus', this.fixPosition.bind(this)); this.container.setStyle({ maxHeight: this.options.maxHeight + 'px' }); this.instanceId = Autocomplete.instances.push(this) - 1; }, fixPosition: function() { var offset = this.el.cumulativeOffset(); $(this.mainContainerId).setStyle({ top: (offset.top + this.el.getHeight()) + 'px', left: offset.left + 'px' }); }, enableKillerFn: function() { Event.observe(document.body, 'click', this.killerFn); }, disableKillerFn: function() { Event.stopObserving(document.body, 'click', this.killerFn); }, killSuggestions: function() { this.stopKillSuggestions(); this.intervalId = window.setInterval(function() { this.hide(); this.stopKillSuggestions(); } .bind(this), 300); }, stopKillSuggestions: function() { window.clearInterval(this.intervalId); }, onKeyPress: function(e) { if (!this.enabled) { return; } // return will exit the function // and event will not fire switch (e.keyCode) { case Event.KEY_ESC: this.el.value = this.currentValue; this.hide(); break; case Event.KEY_TAB: case Event.KEY_RETURN: if (this.selectedIndex === -1) { this.hide(); return; } this.select(this.selectedIndex); if (e.keyCode === Event.KEY_TAB) { return; } break; case Event.KEY_UP: this.moveUp(); break; case Event.KEY_DOWN: this.moveDown(); break; default: return; } Event.stop(e); }, onKeyUp: function(e) { switch (e.keyCode) { case Event.KEY_UP: case Event.KEY_DOWN: return; } clearInterval(this.onChangeInterval); if (this.currentValue !== this.el.value) { if (this.options.deferRequestBy > 0) { // Defer lookup in case when value changes very quickly: this.onChangeInterval = setInterval((function() { this.onValueChange(); }).bind(this), this.options.deferRequestBy); } else { this.onValueChange(); } } }, onValueChange: function() { clearInterval(this.onChangeInterval); var newValue = this.activeValue(); this.selectedIndex = -1; if (this.ignoreValueChange) { this.ignoreValueChange = false; return; } if (newValue === '' || newValue.length < this.options.minChars) { this.hide(); } else { this.getSuggestions(); } }, activeValue: function () { if (this.options.multiple) { return this.el.value.split(/,\s*/).last(); } else { return this.el.value; } }, getSuggestions: function() { var newValue = this.activeValue(); console.log('getting suggestions for ', newValue); var cr = this.cachedResponse[newValue]; if (cr && Object.isArray(cr.suggestions)) { this.suggestions = cr.suggestions; this.data = cr.data; this.suggest(); } else if (!this.isBadQuery(newValue)) { new Ajax.Request(this.serviceUrl, { parameters: { query: newValue }, onComplete: this.processResponse.bind(this), method: 'get' }); } }, isBadQuery: function(q) { var i = this.badQueries.length; while (i--) { if (q.indexOf(this.badQueries[i]) === 0) { return true; } } return false; }, hide: function() { this.enabled = false; this.selectedIndex = -1; this.container.hide(); }, suggest: function() { if (this.suggestions.length === 0) { this.hide(); return; } var content = []; var re = new RegExp('\\b' + this.activeValue().match(/\w+/g).join('|\\b'), 'gi'); this.suggestions.each(function(value, i) { content.push((this.selectedIndex === i ? '
', Autocomplete.highlight(value, re), '
'); } .bind(this)); this.enabled = true; this.container.update(content.join('')).show(); }, processResponse: function(xhr) { var response; try { response = xhr.responseText.evalJSON(); if (!Object.isArray(response.data)) { response.data = []; } } catch (err) { return; } this.cachedResponse[response.query] = response; if (response.suggestions.length === 0) { this.badQueries.push(response.query); } if (response.query === this.activeValue()) { this.suggestions = response.suggestions; this.data = response.data; this.suggest(); } }, activate: function(index) { var divs = this.container.childNodes; var activeItem; // Clear previous selection: if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) { divs[this.selectedIndex].className = ''; } this.selectedIndex = index; if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) { activeItem = divs[this.selectedIndex]; activeItem.className = 'selected'; } return activeItem; }, deactivate: function(div, index) { div.className = ''; if (this.selectedIndex === index) { this.selectedIndex = -1; } }, select: function(i) { var selectedValue = this.suggestions[i]; if (selectedValue) { this.updateValue(selectedValue); if (this.options.autoSubmit && this.el.form) { this.el.form.submit(); } this.ignoreValueChange = true; this.hide(); this.onSelect(i); } }, updateValue: function (selectedValue) { if (this.options.multiple) { var values = this.el.value.split(/,\s*/); values.pop(); values.push(selectedValue, ''); this.el.value = values.uniq().join(', '); } else { this.el.value = selectedValue; } this.currentValue = this.el.value; this.el.focus(); }, moveUp: function() { if (this.selectedIndex === -1) { return; } if (this.selectedIndex === 0) { this.container.childNodes[0].className = ''; this.selectedIndex = -1; this.updateValue(this.currentValue); return; } this.adjustScroll(this.selectedIndex - 1); }, moveDown: function() { if (this.selectedIndex === (this.suggestions.length - 1)) { return; } this.adjustScroll(this.selectedIndex + 1); }, adjustScroll: function(i) { var container = this.container; var activeItem = this.activate(i); var offsetTop = activeItem.offsetTop; var upperBound = container.scrollTop; var lowerBound = upperBound + this.options.maxHeight - 25; if (offsetTop < upperBound) { container.scrollTop = offsetTop; } else if (offsetTop > lowerBound) { container.scrollTop = offsetTop - this.options.maxHeight + 25; } this.updateValue(this.suggestions[i]); }, onSelect: function(i) { (this.options.onSelect || Prototype.emptyFunction)(this.suggestions[i], this.data[i]); } }; Event.observe(document, 'dom:loaded', function(){ Autocomplete.isDomLoaded = true; }, false);