// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) // // 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. Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { var children = $(element).childNodes; var text = ""; var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i"); for (var i = 0; i < children.length; i++) { if(children[i].nodeType==3) { text+=children[i].nodeValue; } else { if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); } } return text; } // Autocompleter.Base handles all the autocompletion functionality // that's independent of the data source for autocompletion. This // includes drawing the autocompletion menu, observing keyboard // and mouse events, and similar. // // Specific autocompleters need to provide, at the very least, // a getUpdatedChoices function that will be invoked every time // the text inside the monitored textbox changes. This method // should get the text for which to provide autocompletion by // invoking this.getEntry(), NOT by directly accessing // this.element.value. This is to allow incremental tokenized // autocompletion. Specific auto-completion logic (AJAX, etc) // belongs in getUpdatedChoices. // // Tokenized incremental autocompletion is enabled automatically // when an autocompleter is instantiated with the 'tokens' option // in the options parameter, e.g.: // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); // will incrementally autocomplete with a comma as the token. // Additionally, ',' in the above example can be replaced with // a token array, e.g. { tokens: new Array (',', '\n') } which // enables autocompletion on multiple tokens. This is most // useful when one of the tokens is \n (a newline), as it // allows smart autocompletion after linebreaks. var Autocompleter = {} Autocompleter.Base = function() {}; Autocompleter.Base.prototype = { base_initialize: function(element, update, options) { this.element = $(element); this.update = $(update); this.has_focus = false; this.changed = false; this.active = false; this.index = 0; this.entry_count = 0; if (this.setOptions) this.setOptions(options); else this.options = {} this.options.tokens = this.options.tokens || new Array(); this.options.frequency = this.options.frequency || 0.4; this.options.min_chars = this.options.min_chars || 1; this.options.onShow = this.options.onShow || function(element, update){ if(!update.style.position || update.style.position=='absolute') { update.style.position = 'absolute'; var offsets = Position.cumulativeOffset(element); update.style.left = offsets[0] + 'px'; update.style.top = (offsets[1] + element.offsetHeight) + 'px'; update.style.width = element.offsetWidth + 'px'; } new Effect.Appear(update,{duration:0.15}); }; this.options.onHide = this.options.onHide || function(element, update){ new Effect.Fade(update,{duration:0.15}) }; if(this.options.indicator) this.indicator = $(this.options.indicator); if (typeof(this.options.tokens) == 'string') this.options.tokens = new Array(this.options.tokens); this.observer = null; Element.hide(this.update); Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); }, show: function() { if(this.update.style.display=='none') this.options.onShow(this.element, this.update); if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') { new Insertion.After(this.update, ''); this.iefix = $(this.update.id+'_iefix'); } if(this.iefix) { Position.clone(this.update, this.iefix); this.iefix.style.zIndex = 1; this.update.style.zIndex = 2; Element.show(this.iefix); } }, hide: function() { if(this.update.style.display=='') this.options.onHide(this.element, this.update); if(this.iefix) Element.hide(this.iefix); }, startIndicator: function() { if(this.indicator) Element.show(this.indicator); }, stopIndicator: function() { if(this.indicator) Element.hide(this.indicator); }, onKeyPress: function(event) { if(this.active) switch(event.keyCode) { case Event.KEY_TAB: case Event.KEY_RETURN: this.select_entry(); Event.stop(event); case Event.KEY_ESC: this.hide(); this.active = false; return; case Event.KEY_LEFT: case Event.KEY_RIGHT: return; case Event.KEY_UP: this.mark_previous(); this.render(); if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); return; case Event.KEY_DOWN: this.mark_next(); this.render(); if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); return; } else if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) return; this.changed = true; this.has_focus = true; if(this.observer) clearTimeout(this.observer); this.observer = setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); }, onHover: function(event) { var element = Event.findElement(event, 'LI'); if(this.index != element.autocompleteIndex) { this.index = element.autocompleteIndex; this.render(); } Event.stop(event); }, onClick: function(event) { var element = Event.findElement(event, 'LI'); this.index = element.autocompleteIndex; this.select_entry(); Event.stop(event); }, onBlur: function(event) { // needed to make click events working setTimeout(this.hide.bind(this), 250); this.has_focus = false; this.active = false; }, render: function() { if(this.entry_count > 0) { for (var i = 0; i < this.entry_count; i++) this.index==i ? Element.addClassName(this.get_entry(i),"selected") : Element.removeClassName(this.get_entry(i),"selected"); if(this.has_focus) { if(this.get_current_entry().scrollIntoView) this.get_current_entry().scrollIntoView(false); this.show(); this.active = true; } } else this.hide(); }, mark_previous: function() { if(this.index > 0) this.index-- else this.index = this.entry_count-1; }, mark_next: function() { if(this.index < this.entry_count-1) this.index++ else this.index = 0; }, get_entry: function(index) { return this.update.firstChild.childNodes[index]; }, get_current_entry: function() { return this.get_entry(this.index); }, select_entry: function() { this.active = false; value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML(); this.updateElement(value); this.element.focus(); }, updateElement: function(value) { var last_token_pos = this.findLastToken(); if (last_token_pos != -1) { var new_value = this.element.value.substr(0, last_token_pos + 1); var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/); if (whitespace) new_value += whitespace[0]; this.element.value = new_value + value; } else { this.element.value = value; } }, updateChoices: function(choices) { if(!this.changed && this.has_focus) { this.update.innerHTML = choices; Element.cleanWhitespace(this.update); Element.cleanWhitespace(this.update.firstChild); if(this.update.firstChild && this.update.firstChild.childNodes) { this.entry_count = this.update.firstChild.childNodes.length; for (var i = 0; i < this.entry_count; i++) { entry = this.get_entry(i); entry.autocompleteIndex = i; this.addObservers(entry); } } else { this.entry_count = 0; } this.stopIndicator(); this.index = 0; this.render(); } }, addObservers: function(element) { Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); Event.observe(element, "click", this.onClick.bindAsEventListener(this)); }, onObserverEvent: function() { this.changed = false; if(this.getEntry().length>=this.options.min_chars) { this.startIndicator(); this.getUpdatedChoices(); } else { this.active = false; this.hide(); } }, getEntry: function() { var token_pos = this.findLastToken(); if (token_pos != -1) var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,''); else var ret = this.element.value; return /\n/.test(ret) ? '' : ret; }, findLastToken: function() { var last_token_pos = -1; for (var i=0; i last_token_pos) last_token_pos = this_token_pos; } return last_token_pos; } } Ajax.Autocompleter = Class.create(); Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(), Object.extend(new Ajax.Base(), { initialize: function(element, update, url, options) { this.base_initialize(element, update, options); this.options.asynchronous = true; this.options.onComplete = this.onComplete.bind(this) this.options.method = 'post'; this.options.defaultParams = this.options.parameters || null; this.url = url; }, getUpdatedChoices: function() { entry = encodeURIComponent(this.element.name) + '=' + encodeURIComponent(this.getEntry()); this.options.parameters = this.options.callback ? this.options.callback(this.element, entry) : entry; if(this.options.defaultParams) this.options.parameters += '&' + this.options.defaultParams; new Ajax.Request(this.url, this.options); }, onComplete: function(request) { this.updateChoices(request.responseText); } })); // The local array autocompleter. Used when you'd prefer to // inject an array of autocompletion options into the page, rather // than sending out Ajax queries, which can be quite slow sometimes. // // The constructor takes four parameters. The first two are, as usual, // the id of the monitored textbox, and id of the autocompletion menu. // The third is the array you want to autocomplete from, and the fourth // is the options block. // // Extra local autocompletion options: // - choices - How many autocompletion choices to offer // // - partial_search - If false, the autocompleter will match entered // text only at the beginning of strings in the // autocomplete array. Defaults to true, which will // match text at the beginning of any *word* in the // strings in the autocomplete array. If you want to // search anywhere in the string, additionally set // the option full_search to true (default: off). // // - full_search - Search anywhere in autocomplete array strings. // // - partial_chars - How many characters to enter before triggering // a partial match (unlike min_chars, which defines // how many characters are required to do any match // at all). Defaults to 2. // // - ignore_case - Whether to ignore case when autocompleting. // Defaults to true. // // It's possible to pass in a custom function as the 'selector' // option, if you prefer to write your own autocompletion logic. // In that case, the other options above will not apply unless // you support them. Autocompleter.Local = Class.create(); Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { initialize: function(element, update, array, options) { this.base_initialize(element, update, options); this.options.array = array; }, getUpdatedChoices: function() { this.updateChoices(this.options.selector(this)); }, setOptions: function(options) { this.options = Object.extend({ choices: 10, partial_search: true, partial_chars: 2, ignore_case: true, full_search: false, selector: function(instance) { var ret = new Array(); // Beginning matches var partial = new Array(); // Inside matches var entry = instance.getEntry(); var count = 0; for (var i = 0; i < instance.options.array.length && ret.length < instance.options.choices ; i++) { var elem = instance.options.array[i]; var found_pos = instance.options.ignore_case ? elem.toLowerCase().indexOf(entry.toLowerCase()) : elem.indexOf(entry); while (found_pos != -1) { if (found_pos == 0 && elem.length != entry.length) { ret.push("
  • " + elem.substr(0, entry.length) + "" + elem.substr(entry.length) + "
  • "); break; } else if (entry.length >= instance.options.partial_chars && instance.options.partial_search && found_pos != -1) { if (instance.options.full_search || /\s/.test(elem.substr(found_pos-1,1))) { partial.push("
  • " + elem.substr(0, found_pos) + "" + elem.substr(found_pos, entry.length) + "" + elem.substr( found_pos + entry.length) + "
  • "); break; } } found_pos = instance.options.ignore_case ? elem.toLowerCase().indexOf(entry.toLowerCase(), found_pos + 1) : elem.indexOf(entry, found_pos + 1); } } if (partial.length) ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) return ""; } }, options || {}); } });