// Autocompleter, version 1.1.2 | http://digitarald.de/project/autocompleter/ | Author: Harald Kirschner - copyright Author, MIT-style license var Autocompleter = new Class({Implements: [Options, Events], options: {minLength: 1, markQuery: true, width: '200', maxChoices: 10, injectChoice: null, customChoices: null, emptyChoices: null, visibleChoices: true, className: 'contextxtra', zIndex: 42, delay: 300, observerOptions: {}, fxOptions: {}, autoSubmit: false, overflow: false, overflowMargin: 25, selectFirst: false, filter: null, filterCase: false, filterSubset: false, forceSelect: false, selectMode: true, choicesMatch: null, multiple: false, separator: ', ', separatorSplit: /\s*[,;]\s*/, autoTrim: false, allowDupes: false, cache: true, relative: false}, initialize: function(element, options) {this.element = $(element); this.setOptions(options); this.build(); this.observer = new Observer(this.element, this.prefetch.bind(this), $merge({'delay': this.options.delay}, this.options.observerOptions)); this.queryValue = null; if (this.options.filter) this.filter = this.options.filter.bind(this); var mode = this.options.selectMode; this.typeAhead = (mode == 'type-ahead'); this.selectMode = (mode === true) ? 'selection' : mode; this.cached = [];}, build: function() {if ($(this.options.customChoices)) {this.choices = this.options.customChoices;} else {this.choices = new Element('ul', {'id':'contexthits','class': this.options.className, 'styles': {'zIndex': this.options.zIndex}}).inject('contextdrop'); this.relative = false; if (this.options.relative) {this.choices.inject(this.element, 'after'); this.relative = this.element.getOffsetParent();} this.fix = new OverlayFix(this.choices);} if (!this.options.separator.test(this.options.separatorSplit)) {this.options.separatorSplit = this.options.separator;} this.fx = (!this.options.fxOptions) ? null : new Fx.Tween(this.choices, $merge({'property': 'opacity', 'link': 'cancel', 'duration': 200}, this.options.fxOptions)).addEvent('onStart', Chain.prototype.clearChain).set(0); this.element.setProperty('autocomplete', 'off') .addEvent('keydown', this.onCommand.bind(this)) .addEvent('click', this.onCommand.bind(this, [false])) .addEvent('focus', this.toggleFocus.create({bind: this, arguments: true, delay: 100})) .addEvent('blur', this.toggleFocus.create({bind: this, arguments: false, delay: 100}));}, destroy: function() {if (this.fix) this.fix.destroy(); this.choices = this.selected = this.choices.destroy();}, toggleFocus: function(state) {this.focussed = state; if (!state) this.hideChoices(true); this.fireEvent((state) ? 'onFocus' : 'onBlur', [this.element]);}, onCommand: function(e) {if (!e && this.focussed) return this.prefetch(); if (e && e.key && !e.shift) {switch (e.key) {case 'enter':if (this.element.value != this.opted) return true; if (this.selected && this.visible) {this.choiceSelect(this.selected); return !!(this.options.autoSubmit);} break; case 'up': case 'down':if (!this.prefetch() && this.queryValue !== null) {var up = (e.key == 'up'); this.choiceOver((this.selected || this.choices)[(this.selected) ? ((up) ? 'getPrevious' : 'getNext') : ((up) ? 'getLast' : 'getFirst')](this.options.choicesMatch), true);} return false; case 'esc': case 'tab':this.hideChoices(true); break; }} return true;}, setSelection: function(finish) {var input = this.selected.inputValue, value = input; var start = this.queryValue.length, end = input.length; if (input.substr(0, start).toLowerCase() != this.queryValue.toLowerCase()) start = 0; if (this.options.multiple) {var split = this.options.separatorSplit; value = this.element.value; start += this.queryIndex; end += this.queryIndex; var old = value.substr(this.queryIndex).split(split, 1)[0]; value = value.substr(0, this.queryIndex) + input + value.substr(this.queryIndex + old.length); if (finish) {var tokens = value.split(this.options.separatorSplit).filter(function(entry) {return this.test(entry);}, /[^\s,]+/); if (!this.options.allowDupes) tokens = [].combine(tokens); var sep = this.options.separator; value = tokens.join(sep) + sep; end = value.length; }} this.observer.setValue(value); this.opted = value; if (finish || this.selectMode == 'pick') start = end; this.element.selectRange(start, end); this.fireEvent('onSelection', [this.element, this.selected, value, input]);}, showChoices: function() {var match = this.options.choicesMatch, first = this.choices.getFirst(match); this.selected = this.selectedValue = null; if (this.fix) {var pos = this.element.getCoordinates(this.relative), width = this.options.width || 'auto'; this.choices.setStyles({'left': pos.left, 'top': pos.bottom, 'width': (width === true || width == 'inherit') ? pos.width : width});} if (!first) return; if (!this.visible) {this.visible = true; this.choices.setStyle('display', ''); if (this.fx) this.fx.start(1); this.fireEvent('onShow', [this.element, this.choices]);} if (this.options.selectFirst || this.typeAhead || first.inputValue == this.queryValue) this.choiceOver(first, this.typeAhead); var items = this.choices.getChildren(match), max = this.options.maxChoices; var styles = {'overflowY': 'hidden', 'height': ''}; this.overflown = false; if (items.length > max) {var item = items[max - 1]; styles.overflowY = 'scroll'; styles.height = item.getCoordinates(this.choices).bottom; this.overflown = true;}; this.choices.setStyles(styles); this.fix.show(); if (this.options.visibleChoices) {var scroll = document.getScroll(), size = document.getSize(), coords = this.choices.getCoordinates(); if (coords.right > scroll.x + size.x) scroll.x = coords.right - size.x; if (coords.bottom > scroll.y + size.y) scroll.y = coords.bottom - size.y; window.scrollTo(Math.min(scroll.x, coords.left), Math.min(scroll.y, coords.top));}}, hideChoices: function(clear) {if (clear) {var value = this.element.value; if (this.options.forceSelect) value = this.opted; if (this.options.autoTrim) {value = value.split(this.options.separatorSplit).filter($arguments(0)).join(this.options.separator);} this.observer.setValue(value);} if (!this.visible) return; this.visible = false; if (this.selected) this.selected.removeClass('contextselected'); this.observer.clear(); var hide = function(){this.choices.setStyle('display', 'none'); this.fix.hide();}.bind(this); if (this.fx) this.fx.start(0).chain(hide); else hide(); this.fireEvent('onHide', [this.element, this.choices]);}, prefetch: function() {var value = this.element.value, query = value; if (this.options.multiple) {var split = this.options.separatorSplit; var values = value.split(split); var index = this.element.getSelectedRange().start; var toIndex = value.substr(0, index).split(split); var last = toIndex.length - 1; index -= toIndex[last].length; query = values[last];} if (query.length < this.options.minlength) {this.hidechoices();} else {if (query === this.queryValue || (this.visible && query == this.selectedValue)) {if (this.visible) return false; this.showChoices();} else {this.queryValue = query; this.queryIndex = index; if (!this.fetchCached()) this.query();}} return true;}, fetchCached: function() {return false; if (!this.options.cache || !this.cached || !this.cached.length || this.cached.length >= this.options.maxChoices || this.queryValue) return false; this.update(this.filter(this.cached)); return true;}, update: function(tokens) {this.choices.empty(); this.cached = tokens; var type = tokens && $type(tokens); if (!type || (type == 'array' && !tokens.length) || (type == 'hash' && !tokens.getLength())) {(this.options.emptyChoices || this.hideChoices).call(this);} else {if (this.options.maxChoices < tokens.length && !this.options.overflow) tokens.length = this.options.maxChoices; tokens.each(this.options.injectChoice || function(token){var choice = new Element('li', {'html': ''+this.markQueryValue(token)+'','class':'contextlisting'}); choice.inputValue = token; this.addChoiceEvents(choice).inject(this.choices);}, this); this.showChoices();}}, choiceOver: function(choice, selection) {if (!choice || choice == this.selected) return; if (this.selected) this.selected.removeClass('contextselected'); this.selected = choice.addClass('contextselected'); this.fireEvent('onSelect', [this.element, this.selected, selection]); if (!this.selectMode) this.opted = this.element.value; if (!selection) return; this.selectedValue = this.selected.inputValue; if (this.overflown) {var coords = this.selected.getCoordinates(this.choices), margin = this.options.overflowMargin, top = this.choices.scrollTop, height = this.choices.offsetHeight, bottom = top + height; if (coords.top - margin < top && top) this.choices.scrollTop = Math.max(coords.top - margin, 0); else if (coords.bottom + margin > bottom) this.choices.scrollTop = Math.min(coords.bottom - height + margin, bottom);} if (this.selectMode) this.setSelection();}, choiceSelect: function(choice) {if (choice) this.choiceOver(choice); this.setSelection(true); this.queryValue = false; this.hideChoices();}, filter: function(tokens) {return (tokens || this.tokens).filter(function(token) {return this.test(token);}, new RegExp(((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp(), (this.options.filterCase) ? '' : 'i'));}, markQueryValue: function(str) {return (!this.options.markQuery || !this.queryValue) ? str : str.replace(new RegExp('(' + ((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp() + ')', (this.options.filterCase) ? '' : 'i'), '$1');}, addChoiceEvents: function(el) {return el.addEvents({'mouseover': this.choiceOver.bind(this, [el]), 'click': this.choiceSelect.bind(this, [el])});}}); var OverlayFix = new Class({initialize: function(el) {if (Browser.Engine.trident) {this.element = $(el); this.relative = this.element.getOffsetParent(); this.fix = new Element('iframe', {'frameborder': '0', 'scrolling': 'no', 'src': 'javascript:false;', 'styles': {'position': 'absolute', 'border': 'none', 'display': 'none', 'filter': 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)'}}).inject(this.element, 'after');}}, show: function() {if (this.fix) {var coords = this.element.getCoordinates(this.relative); delete coords.right; delete coords.bottom; this.fix.setStyles($extend(coords, {'display': '', 'zIndex': (this.element.getStyle('zIndex') || 1) - 1}));} return this;}, hide: function() {if (this.fix) this.fix.setStyle('display', 'none'); return this;}, destroy: function() {if (this.fix) this.fix = this.fix.destroy();}}); Element.implement({getSelectedRange: function() {if (!Browser.Engine.trident) return {start: this.selectionStart, end: this.selectionEnd}; var pos = {start: 0, end: 0}; var range = this.getDocument().selection.createRange(); if (!range || range.parentElement() != this) return pos; var dup = range.duplicate(); if (this.type == 'text') {pos.start = 0 - dup.moveStart('character', -100000); pos.end = pos.start + range.text.length;} else {var value = this.value; var offset = value.length - value.match(/[\n\r]*$/)[0].length; dup.moveToElementText(this); dup.setEndPoint('StartToEnd', range); pos.end = offset - dup.text.length; dup.setEndPoint('StartToStart', range); pos.start = offset - dup.text.length;} return pos;}, selectRange: function(start, end) {if (Browser.Engine.trident) {var diff = this.value.substr(start, end - start).replace(/\r/g, '').length; start = this.value.substr(0, start).replace(/\r/g, '').length; var range = this.createTextRange(); range.collapse(true); range.moveEnd('character', start + diff); range.moveStart('character', start); range.select();} else {this.focus(); this.setSelectionRange(start, end);} return this;}}); Autocompleter.Base = Autocompleter; Autocompleter.Local = new Class({Extends: Autocompleter, options: {minLength: 0, delay:150}, initialize: function(element, tokens, options) {this.parent(element, options); this.tokens = tokens;}, query: function() {this.update(this.filter());}}); Autocompleter.Local.URL = new Class({Extends : Autocompleter.Local, initialize : function(input,url,options) {options = options || {}; options.injectChoice = function(el) {var value = el[0];var id = el[1]; var choice = new Element('li',{'html' : ''+this.markQueryValue(value)+'','class':'contextlisting'}); choice.inputValue = value; choice.store('id',id);this.addChoiceEvents(choice).inject(this.choices);}.bind(this); options.onSelection = function(input,elm,value,bool) {var id = elm.retrieve('id');this.fireEvent('selectionID',[input,elm,value,id,bool]);}.bind(this); this.parent(input,url,options);}}); var Observer = new Class({ Implements: [Options, Events], options: {periodical: false, delay: 1000}, initialize: function(el, onFired, options){this.element = $(el) || $$(el); this.addEvent('onFired', onFired); this.setOptions(options); this.bound = this.changed.bind(this); this.resume();}, changed: function() {var value = this.element.get('value'); if ($equals(this.value, value)) return; this.clear(); this.value = value; this.timeout = this.onFired.delay(this.options.delay, this);}, setValue: function(value) {this.value = value; this.element.set('value', value); return this.clear();}, onFired: function() {this.fireEvent('onFired', [this.value, this.element]);}, clear: function() {$clear(this.timeout || null); return this;}, pause: function(){if (this.timer) $clear(this.timer); else this.element.removeEvent('keyup', this.bound); return this.clear();}, resume: function(){this.value = this.element.get('value'); if (this.options.periodical) this.timer = this.changed.periodical(this.options.periodical, this); else this.element.addEvent('keyup', this.bound); return this;} }); var $equals = function(obj1, obj2) {return (obj1 == obj2 || JSON.encode(obj1) == JSON.encode(obj2));};