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
9 * Heavily modified for contact completion in Friendica (add photos, hover tips. etc.) 11-May-2012 mike@macgirvin.com
\r
12 /*jslint onevar: true, evil: true, nomen: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
\r
13 /*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */
\r
17 var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g');
\r
19 function fnFormatResult(value, data, currentValue) {
\r
20 var pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')';
\r
21 return value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
\r
24 function Autocomplete(el, options) {
\r
26 this.el.attr('autocomplete', 'off');
\r
27 this.suggestions = [];
\r
29 this.badQueries = [];
\r
30 this.selectedIndex = -1;
\r
31 this.currentValue = this.el.val();
\r
32 this.intervalId = 0;
\r
33 this.cachedResponse = [];
\r
34 this.onChangeInterval = null;
\r
35 this.ignoreValueChange = false;
\r
36 this.serviceUrl = options.serviceUrl;
\r
37 this.isLocal = false;
\r
46 fnFormatResult: fnFormatResult,
\r
51 this.setOptions(options);
\r
54 $.fn.autocomplete = function(options) {
\r
55 return new Autocomplete(this.get(0)||$('<input />'), options);
\r
59 Autocomplete.prototype = {
\r
63 initialize: function() {
\r
65 var me, uid, autocompleteElId;
\r
67 uid = Math.floor(Math.random()*0x100000).toString(16);
\r
68 autocompleteElId = 'Autocomplete_' + uid;
\r
70 this.killerFn = function(e) {
\r
71 if ($(e.target).parents('.autocomplete').size() === 0) {
\r
72 me.killSuggestions();
\r
73 me.disableKillerFn();
\r
77 if (!this.options.width) { this.options.width = this.el.width(); }
\r
78 this.mainContainerId = 'AutocompleteContainter_' + uid;
\r
80 $('<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
82 this.container = $('#' + autocompleteElId);
\r
85 this.el.keypress(function(e) { me.onKeyPress(e); });
\r
87 this.el.keydown(function(e) { me.onKeyPress(e); });
\r
89 this.el.keyup(function(e) { me.onKeyUp(e); });
\r
90 this.el.blur(function() { me.enableKillerFn(); });
\r
91 this.el.focus(function() { me.fixPosition(); });
\r
94 setOptions: function(options){
\r
95 var o = this.options;
\r
96 $.extend(o, options);
\r
98 this.isLocal = true;
\r
99 if($.isArray(o.lookup)){ o.lookup = { suggestions:o.lookup, data:[] }; }
\r
101 $('#'+this.mainContainerId).css({ zIndex:o.zIndex });
\r
102 this.container.css({ maxHeight: o.maxHeight + 'px', width:o.width });
\r
105 clearCache: function(){
\r
106 this.cachedResponse = [];
\r
107 this.badQueries = [];
\r
110 disable: function(){
\r
111 this.disabled = true;
\r
114 enable: function(){
\r
115 this.disabled = false;
\r
118 fixPosition: function() {
\r
119 var offset = this.el.offset();
\r
120 $('#' + this.mainContainerId).css({ top: (offset.top + this.el.innerHeight()) + 'px', left: offset.left + 'px' });
\r
123 enableKillerFn: function() {
\r
125 $(document).bind('click', me.killerFn);
\r
128 disableKillerFn: function() {
\r
130 $(document).unbind('click', me.killerFn);
\r
133 killSuggestions: function() {
\r
135 this.stopKillSuggestions();
\r
136 this.intervalId = window.setInterval(function() { me.hide(); me.stopKillSuggestions(); }, 300);
\r
139 stopKillSuggestions: function() {
\r
140 window.clearInterval(this.intervalId);
\r
143 onKeyPress: function(e) {
\r
144 if (this.disabled || !this.enabled) { return; }
\r
145 // return will exit the function
\r
146 // and event will not be prevented
\r
147 switch (e.keyCode) {
\r
148 case 27: //KEY_ESC:
\r
149 this.el.val(this.currentValue);
\r
153 case 13: //KEY_RETURN:
\r
154 if (this.selectedIndex === -1) {
\r
158 this.select(this.selectedIndex);
\r
159 if(e.keyCode === 9){ return; }
\r
164 case 40: //KEY_DOWN:
\r
170 e.stopImmediatePropagation();
\r
171 e.preventDefault();
\r
174 onKeyUp: function(e) {
\r
175 if(this.disabled){ return; }
\r
176 switch (e.keyCode) {
\r
178 case 40: //KEY_DOWN:
\r
181 clearInterval(this.onChangeInterval);
\r
182 if (this.currentValue !== this.el.val()) {
\r
183 if (this.options.deferRequestBy > 0) {
\r
184 // Defer lookup in case when value changes very quickly:
\r
186 this.onChangeInterval = setInterval(function() { me.onValueChange(); }, this.options.deferRequestBy);
\r
188 this.onValueChange();
\r
193 onValueChange: function() {
\r
194 clearInterval(this.onChangeInterval);
\r
195 this.currentValue = this.el.val();
\r
196 var q = this.getQuery(this.currentValue);
\r
197 this.selectedIndex = -1;
\r
198 if (this.ignoreValueChange) {
\r
199 this.ignoreValueChange = false;
\r
202 if (q === '' || q.length < this.options.minChars) {
\r
205 this.getSuggestions(q);
\r
209 getQuery: function(val) {
\r
211 d = this.options.delimiter;
\r
212 if (!d) { return $.trim(val); }
\r
213 arr = val.split(d);
\r
214 return $.trim(arr[arr.length - 1]);
\r
217 getSuggestionsLocal: function(q) {
\r
218 var ret, arr, len, val, i;
\r
219 arr = this.options.lookup;
\r
220 len = arr.suggestions.length;
\r
221 ret = { suggestions:[], data:[] };
\r
222 q = q.toLowerCase();
\r
223 for(i=0; i< len; i++){
\r
224 val = arr.suggestions[i];
\r
225 if(val.toLowerCase().indexOf(q) === 0){
\r
226 ret.suggestions.push(val);
\r
227 ret.data.push(arr.data[i]);
\r
233 getSuggestions: function(q) {
\r
235 cr = this.isLocal ? this.getSuggestionsLocal(q) : this.cachedResponse[q];
\r
236 if (cr && $.isArray(cr.suggestions)) {
\r
237 this.suggestions = cr.suggestions;
\r
238 this.data = cr.data;
\r
240 } else if (!this.isBadQuery(q)) {
\r
242 me.options.params.query = q;
\r
243 $.get(this.serviceUrl, me.options.params, function(txt) { me.processResponse(txt); }, 'text');
\r
247 isBadQuery: function(q) {
\r
248 var i = this.badQueries.length;
\r
250 if (q.indexOf(this.badQueries[i]) === 0) { return true; }
\r
256 this.enabled = false;
\r
257 this.selectedIndex = -1;
\r
258 this.container.hide();
\r
261 suggest: function() {
\r
262 if (this.suggestions.length === 0) {
\r
267 var me, len, div, f, v, i, s, mOver, mClick, l, img;
\r
269 len = this.suggestions.length;
\r
270 f = this.options.fnFormatResult;
\r
271 v = this.getQuery(this.currentValue);
\r
272 mOver = function(xi) { return function() { me.activate(xi); }; };
\r
273 mClick = function(xi) { return function() { me.select(xi); }; };
\r
274 this.container.hide().empty();
\r
275 for (i = 0; i < len; i++) {
\r
276 s = this.suggestions[i];
\r
278 img = '<img height="24" width="24" src="' + this.photos[i] + '" alt="' + s + '" /> ';
\r
279 div = $((me.selectedIndex === i ? '<div class="selected"' : '<div') + ' title="' + l + '">' + img + f(s, this.data[i], v) + '</div>');
\r
280 div.mouseover(mOver(i));
\r
281 div.click(mClick(i));
\r
282 this.container.append(div);
\r
284 this.enabled = true;
\r
285 this.container.show();
\r
288 processResponse: function(text) {
\r
291 response = eval('(' + text + ')');
\r
292 } catch (err) { return; }
\r
293 if (!$.isArray(response.data)) { response.data = []; }
\r
294 if(!this.options.noCache){
\r
295 this.cachedResponse[response.query] = response;
\r
296 if (response.suggestions.length === 0) { this.badQueries.push(response.query); }
\r
298 if (response.query === this.getQuery(this.currentValue)) {
\r
299 this.photos = response.photos;
\r
300 this.links = response.links;
\r
301 this.suggestions = response.suggestions;
\r
302 this.data = response.data;
\r
307 activate: function(index) {
\r
308 var divs, activeItem;
\r
309 divs = this.container.children();
\r
310 // Clear previous selection:
\r
311 if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
\r
312 $(divs.get(this.selectedIndex)).removeClass();
\r
314 this.selectedIndex = index;
\r
315 if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
\r
316 activeItem = divs.get(this.selectedIndex);
\r
317 $(activeItem).addClass('selected');
\r
322 deactivate: function(div, index) {
\r
323 div.className = '';
\r
324 if (this.selectedIndex === index) { this.selectedIndex = -1; }
\r
327 select: function(i) {
\r
328 var selectedValue, f;
\r
329 selectedValue = this.suggestions[i];
\r
330 if (selectedValue) {
\r
331 this.el.val(selectedValue);
\r
332 if (this.options.autoSubmit) {
\r
333 f = this.el.parents('form');
\r
334 if (f.length > 0) { f.get(0).submit(); }
\r
336 this.ignoreValueChange = true;
\r
342 moveUp: function() {
\r
343 if (this.selectedIndex === -1) { return; }
\r
344 if (this.selectedIndex === 0) {
\r
345 this.container.children().get(0).className = '';
\r
346 this.selectedIndex = -1;
\r
347 this.el.val(this.currentValue);
\r
350 this.adjustScroll(this.selectedIndex - 1);
\r
353 moveDown: function() {
\r
354 if (this.selectedIndex === (this.suggestions.length - 1)) { return; }
\r
355 this.adjustScroll(this.selectedIndex + 1);
\r
358 adjustScroll: function(i) {
\r
359 var activeItem, offsetTop, upperBound, lowerBound;
\r
360 activeItem = this.activate(i);
\r
361 offsetTop = activeItem.offsetTop;
\r
362 upperBound = this.container.scrollTop();
\r
363 lowerBound = upperBound + this.options.maxHeight - 25;
\r
364 if (offsetTop < upperBound) {
\r
365 this.container.scrollTop(offsetTop);
\r
366 } else if (offsetTop > lowerBound) {
\r
367 this.container.scrollTop(offsetTop - this.options.maxHeight + 25);
\r
369 this.el.val(this.getValue(this.suggestions[i]));
\r
372 onSelect: function(i) {
\r
375 fn = me.options.onSelect;
\r
376 s = me.suggestions[i];
\r
378 me.el.val(me.getValue(s));
\r
379 if ($.isFunction(fn)) { fn(s, d, me.el); }
\r
382 getValue: function(value){
\r
383 var del, currVal, arr, me;
\r
385 del = me.options.delimiter;
\r
386 if (!del) { return value; }
\r
387 currVal = me.currentValue;
\r
388 arr = currVal.split(del);
\r
389 if (arr.length === 1) { return value; }
\r
390 return currVal.substr(0, currVal.length - arr[arr.length - 1].length) + value;
\r