2 * Ajax Autocomplete for jQuery, version 1.1.3
\r
3 * (c) 2010 Tomas Kirda
\r
5 * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
\r
6 * For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/
\r
8 * Last Review: 04/19/2010
\r
11 /*jslint onevar: true, evil: true, nomen: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
\r
12 /*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */
\r
16 var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g');
\r
18 function fnFormatResult(value, data, currentValue) {
\r
19 var pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')';
\r
20 return value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
\r
23 function Autocomplete(el, options) {
\r
25 this.el.attr('autocomplete', 'off');
\r
26 this.suggestions = [];
\r
28 this.badQueries = [];
\r
29 this.selectedIndex = -1;
\r
30 this.currentValue = this.el.val();
\r
31 this.intervalId = 0;
\r
32 this.cachedResponse = [];
\r
33 this.onChangeInterval = null;
\r
34 this.ignoreValueChange = false;
\r
35 this.serviceUrl = options.serviceUrl;
\r
36 this.isLocal = false;
\r
45 fnFormatResult: fnFormatResult,
\r
50 this.setOptions(options);
\r
53 $.fn.autocomplete = function(options) {
\r
54 return new Autocomplete(this.get(0)||$('<input />'), options);
\r
58 Autocomplete.prototype = {
\r
62 initialize: function() {
\r
64 var me, uid, autocompleteElId;
\r
66 uid = Math.floor(Math.random()*0x100000).toString(16);
\r
67 autocompleteElId = 'Autocomplete_' + uid;
\r
69 this.killerFn = function(e) {
\r
70 if ($(e.target).parents('.autocomplete').size() === 0) {
\r
71 me.killSuggestions();
\r
72 me.disableKillerFn();
\r
76 if (!this.options.width) { this.options.width = this.el.width(); }
\r
77 this.mainContainerId = 'AutocompleteContainter_' + uid;
\r
79 $('<div id="' + this.mainContainerId + '" style="position:absolute;z-index:9999;"><div class="autocomplete-w1"><div class="autocomplete" id="' + autocompleteElId + '" style="display:none; width:300px;"></div></div></div>').appendTo('body');
\r
81 this.container = $('#' + autocompleteElId);
\r
84 this.el.keypress(function(e) { me.onKeyPress(e); });
\r
86 this.el.keydown(function(e) { me.onKeyPress(e); });
\r
88 this.el.keyup(function(e) { me.onKeyUp(e); });
\r
89 this.el.blur(function() { me.enableKillerFn(); });
\r
90 this.el.focus(function() { me.fixPosition(); });
\r
93 setOptions: function(options){
\r
94 var o = this.options;
\r
95 $.extend(o, options);
\r
97 this.isLocal = true;
\r
98 if($.isArray(o.lookup)){ o.lookup = { suggestions:o.lookup, data:[] }; }
\r
100 $('#'+this.mainContainerId).css({ zIndex:o.zIndex });
\r
101 this.container.css({ maxHeight: o.maxHeight + 'px', width:o.width });
\r
104 clearCache: function(){
\r
105 this.cachedResponse = [];
\r
106 this.badQueries = [];
\r
109 disable: function(){
\r
110 this.disabled = true;
\r
113 enable: function(){
\r
114 this.disabled = false;
\r
117 fixPosition: function() {
\r
118 var offset = this.el.offset();
\r
119 $('#' + this.mainContainerId).css({ top: (offset.top + this.el.innerHeight()) + 'px', left: offset.left + 'px' });
\r
122 enableKillerFn: function() {
\r
124 $(document).bind('click', me.killerFn);
\r
127 disableKillerFn: function() {
\r
129 $(document).unbind('click', me.killerFn);
\r
132 killSuggestions: function() {
\r
134 this.stopKillSuggestions();
\r
135 this.intervalId = window.setInterval(function() { me.hide(); me.stopKillSuggestions(); }, 300);
\r
138 stopKillSuggestions: function() {
\r
139 window.clearInterval(this.intervalId);
\r
142 onKeyPress: function(e) {
\r
143 if (this.disabled || !this.enabled) { return; }
\r
144 // return will exit the function
\r
145 // and event will not be prevented
\r
146 switch (e.keyCode) {
\r
147 case 27: //KEY_ESC:
\r
148 this.el.val(this.currentValue);
\r
152 case 13: //KEY_RETURN:
\r
153 if (this.selectedIndex === -1) {
\r
157 this.select(this.selectedIndex);
\r
158 if(e.keyCode === 9){ return; }
\r
163 case 40: //KEY_DOWN:
\r
169 e.stopImmediatePropagation();
\r
170 e.preventDefault();
\r
173 onKeyUp: function(e) {
\r
174 if(this.disabled){ return; }
\r
175 switch (e.keyCode) {
\r
177 case 40: //KEY_DOWN:
\r
180 clearInterval(this.onChangeInterval);
\r
181 if (this.currentValue !== this.el.val()) {
\r
182 if (this.options.deferRequestBy > 0) {
\r
183 // Defer lookup in case when value changes very quickly:
\r
185 this.onChangeInterval = setInterval(function() { me.onValueChange(); }, this.options.deferRequestBy);
\r
187 this.onValueChange();
\r
192 onValueChange: function() {
\r
193 clearInterval(this.onChangeInterval);
\r
194 this.currentValue = this.el.val();
\r
195 var q = this.getQuery(this.currentValue);
\r
196 this.selectedIndex = -1;
\r
197 if (this.ignoreValueChange) {
\r
198 this.ignoreValueChange = false;
\r
201 if (q === '' || q.length < this.options.minChars) {
\r
204 this.getSuggestions(q);
\r
208 getQuery: function(val) {
\r
210 d = this.options.delimiter;
\r
211 if (!d) { return $.trim(val); }
\r
212 arr = val.split(d);
\r
213 return $.trim(arr[arr.length - 1]);
\r
216 getSuggestionsLocal: function(q) {
\r
217 var ret, arr, len, val, i;
\r
218 arr = this.options.lookup;
\r
219 len = arr.suggestions.length;
\r
220 ret = { suggestions:[], data:[] };
\r
221 q = q.toLowerCase();
\r
222 for(i=0; i< len; i++){
\r
223 val = arr.suggestions[i];
\r
224 if(val.toLowerCase().indexOf(q) === 0){
\r
225 ret.suggestions.push(val);
\r
226 ret.data.push(arr.data[i]);
\r
232 getSuggestions: function(q) {
\r
234 cr = this.isLocal ? this.getSuggestionsLocal(q) : this.cachedResponse[q];
\r
235 if (cr && $.isArray(cr.suggestions)) {
\r
236 this.suggestions = cr.suggestions;
\r
237 this.data = cr.data;
\r
239 } else if (!this.isBadQuery(q)) {
\r
241 me.options.params.query = q;
\r
242 $.get(this.serviceUrl, me.options.params, function(txt) { me.processResponse(txt); }, 'text');
\r
246 isBadQuery: function(q) {
\r
247 var i = this.badQueries.length;
\r
249 if (q.indexOf(this.badQueries[i]) === 0) { return true; }
\r
255 this.enabled = false;
\r
256 this.selectedIndex = -1;
\r
257 this.container.hide();
\r
260 suggest: function() {
\r
261 if (this.suggestions.length === 0) {
\r
266 var me, len, div, f, v, i, s, mOver, mClick;
\r
268 len = this.suggestions.length;
\r
269 f = this.options.fnFormatResult;
\r
270 v = this.getQuery(this.currentValue);
\r
271 mOver = function(xi) { return function() { me.activate(xi); }; };
\r
272 mClick = function(xi) { return function() { me.select(xi); }; };
\r
273 this.container.hide().empty();
\r
274 for (i = 0; i < len; i++) {
\r
275 s = this.suggestions[i];
\r
276 div = $((me.selectedIndex === i ? '<div class="selected"' : '<div') + ' title="' + s + '">' + f(s, this.data[i], v) + '</div>');
\r
277 div.mouseover(mOver(i));
\r
278 div.click(mClick(i));
\r
279 this.container.append(div);
\r
281 this.enabled = true;
\r
282 this.container.show();
\r
285 processResponse: function(text) {
\r
288 response = eval('(' + text + ')');
\r
289 } catch (err) { return; }
\r
290 if (!$.isArray(response.data)) { response.data = []; }
\r
291 if(!this.options.noCache){
\r
292 this.cachedResponse[response.query] = response;
\r
293 if (response.suggestions.length === 0) { this.badQueries.push(response.query); }
\r
295 if (response.query === this.getQuery(this.currentValue)) {
\r
296 this.suggestions = response.suggestions;
\r
297 this.data = response.data;
\r
302 activate: function(index) {
\r
303 var divs, activeItem;
\r
304 divs = this.container.children();
\r
305 // Clear previous selection:
\r
306 if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
\r
307 $(divs.get(this.selectedIndex)).removeClass();
\r
309 this.selectedIndex = index;
\r
310 if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
\r
311 activeItem = divs.get(this.selectedIndex);
\r
312 $(activeItem).addClass('selected');
\r
317 deactivate: function(div, index) {
\r
318 div.className = '';
\r
319 if (this.selectedIndex === index) { this.selectedIndex = -1; }
\r
322 select: function(i) {
\r
323 var selectedValue, f;
\r
324 selectedValue = this.suggestions[i];
\r
325 if (selectedValue) {
\r
326 this.el.val(selectedValue);
\r
327 if (this.options.autoSubmit) {
\r
328 f = this.el.parents('form');
\r
329 if (f.length > 0) { f.get(0).submit(); }
\r
331 this.ignoreValueChange = true;
\r
337 moveUp: function() {
\r
338 if (this.selectedIndex === -1) { return; }
\r
339 if (this.selectedIndex === 0) {
\r
340 this.container.children().get(0).className = '';
\r
341 this.selectedIndex = -1;
\r
342 this.el.val(this.currentValue);
\r
345 this.adjustScroll(this.selectedIndex - 1);
\r
348 moveDown: function() {
\r
349 if (this.selectedIndex === (this.suggestions.length - 1)) { return; }
\r
350 this.adjustScroll(this.selectedIndex + 1);
\r
353 adjustScroll: function(i) {
\r
354 var activeItem, offsetTop, upperBound, lowerBound;
\r
355 activeItem = this.activate(i);
\r
356 offsetTop = activeItem.offsetTop;
\r
357 upperBound = this.container.scrollTop();
\r
358 lowerBound = upperBound + this.options.maxHeight - 25;
\r
359 if (offsetTop < upperBound) {
\r
360 this.container.scrollTop(offsetTop);
\r
361 } else if (offsetTop > lowerBound) {
\r
362 this.container.scrollTop(offsetTop - this.options.maxHeight + 25);
\r
364 this.el.val(this.getValue(this.suggestions[i]));
\r
367 onSelect: function(i) {
\r
370 fn = me.options.onSelect;
\r
371 s = me.suggestions[i];
\r
373 me.el.val(me.getValue(s));
\r
374 if ($.isFunction(fn)) { fn(s, d, me.el); }
\r
377 getValue: function(value){
\r
378 var del, currVal, arr, me;
\r
380 del = me.options.delimiter;
\r
381 if (!del) { return value; }
\r
382 currVal = me.currentValue;
\r
383 arr = currVal.split(del);
\r
384 if (arr.length === 1) { return value; }
\r
385 return currVal.substr(0, currVal.length - arr[arr.length - 1].length) + value;
\r