|
1 jQuery.autocomplete = function(input, options) { |
|
2 // Create a link to self |
|
3 var me = this; |
|
4 |
|
5 // Create jQuery object for input element |
|
6 var $input = $(input).attr("autocomplete", "off"); |
|
7 |
|
8 // Apply inputClass if necessary |
|
9 if (options.inputClass) $input.addClass(options.inputClass); |
|
10 |
|
11 // Create results |
|
12 var results = document.createElement("div"); |
|
13 // Create jQuery object for results |
|
14 var $results = $(results); |
|
15 $results.hide().addClass(options.resultsClass).css("position", "absolute"); |
|
16 if( options.width > 0 ) $results.css("width", options.width); |
|
17 |
|
18 // Add to body element |
|
19 $("body").append(results); |
|
20 |
|
21 input.autocompleter = me; |
|
22 |
|
23 var timeout = null; |
|
24 var prev = ""; |
|
25 var active = -1; |
|
26 var cache = {}; |
|
27 var keyb = false; |
|
28 var hasFocus = false; |
|
29 var lastKeyPressCode = null; |
|
30 |
|
31 // flush cache |
|
32 function flushCache(){ |
|
33 cache = {}; |
|
34 cache.data = {}; |
|
35 cache.length = 0; |
|
36 }; |
|
37 |
|
38 // flush cache |
|
39 flushCache(); |
|
40 |
|
41 // if there is a data array supplied |
|
42 if( options.data != null ){ |
|
43 var sFirstChar = "", stMatchSets = {}, row = []; |
|
44 |
|
45 // no url was specified, we need to adjust the cache length to make sure it fits the local data store |
|
46 if( typeof options.url != "string" ) options.cacheLength = 1; |
|
47 |
|
48 // loop through the array and create a lookup structure |
|
49 for( var i=0; i < options.data.length; i++ ){ |
|
50 // if row is a string, make an array otherwise just reference the array |
|
51 row = ((typeof options.data[i] == "string") ? [options.data[i]] : options.data[i]); |
|
52 |
|
53 // if the length is zero, don't add to list |
|
54 if( row[0].length > 0 ){ |
|
55 // get the first character |
|
56 sFirstChar = row[0].substring(0, 1).toLowerCase(); |
|
57 // if no lookup array for this character exists, look it up now |
|
58 if( !stMatchSets[sFirstChar] ) stMatchSets[sFirstChar] = []; |
|
59 // if the match is a string |
|
60 stMatchSets[sFirstChar].push(row); |
|
61 } |
|
62 } |
|
63 |
|
64 // add the data items to the cache |
|
65 for( var k in stMatchSets ){ |
|
66 // increase the cache size |
|
67 options.cacheLength++; |
|
68 // add to the cache |
|
69 addToCache(k, stMatchSets[k]); |
|
70 } |
|
71 } |
|
72 |
|
73 $input |
|
74 .keydown(function(e) { |
|
75 // track last key pressed |
|
76 lastKeyPressCode = e.keyCode; |
|
77 switch(e.keyCode) { |
|
78 case 38: // up |
|
79 e.preventDefault(); |
|
80 moveSelect(-1); |
|
81 break; |
|
82 case 40: // down |
|
83 e.preventDefault(); |
|
84 moveSelect(1); |
|
85 break; |
|
86 case 9: // tab |
|
87 case 13: // return |
|
88 if( selectCurrent() ){ |
|
89 // make sure to blur off the current field |
|
90 $input.get(0).blur(); |
|
91 e.preventDefault(); |
|
92 } |
|
93 break; |
|
94 default: |
|
95 active = -1; |
|
96 if (timeout) clearTimeout(timeout); |
|
97 timeout = setTimeout(function(){onChange();}, options.delay); |
|
98 break; |
|
99 } |
|
100 }) |
|
101 .focus(function(){ |
|
102 // track whether the field has focus, we shouldn't process any results if the field no longer has focus |
|
103 hasFocus = true; |
|
104 }) |
|
105 .blur(function() { |
|
106 // track whether the field has focus |
|
107 hasFocus = false; |
|
108 hideResults(); |
|
109 }); |
|
110 |
|
111 hideResultsNow(); |
|
112 |
|
113 function onChange() { |
|
114 // ignore if the following keys are pressed: [del] [shift] [capslock] |
|
115 if( lastKeyPressCode == 46 || (lastKeyPressCode > 8 && lastKeyPressCode < 32) ) return $results.hide(); |
|
116 var v = $input.val(); |
|
117 if (v == prev) return; |
|
118 prev = v; |
|
119 if (v.length >= options.minChars) { |
|
120 $input.addClass(options.loadingClass); |
|
121 requestData(v); |
|
122 } else { |
|
123 $input.removeClass(options.loadingClass); |
|
124 $results.hide(); |
|
125 } |
|
126 }; |
|
127 |
|
128 function moveSelect(step) { |
|
129 |
|
130 var lis = $("li", results); |
|
131 if (!lis) return; |
|
132 |
|
133 active += step; |
|
134 |
|
135 if (active < 0) { |
|
136 active = 0; |
|
137 } else if (active >= lis.size()) { |
|
138 active = lis.size() - 1; |
|
139 } |
|
140 |
|
141 lis.removeClass("ac_over"); |
|
142 |
|
143 $(lis[active]).addClass("ac_over"); |
|
144 |
|
145 // Weird behaviour in IE |
|
146 // if (lis[active] && lis[active].scrollIntoView) { |
|
147 // lis[active].scrollIntoView(false); |
|
148 // } |
|
149 |
|
150 }; |
|
151 |
|
152 function selectCurrent() { |
|
153 var li = $("li.ac_over", results)[0]; |
|
154 if (!li) { |
|
155 var $li = $("li", results); |
|
156 if (options.selectOnly) { |
|
157 if ($li.length == 1) li = $li[0]; |
|
158 } else if (options.selectFirst) { |
|
159 li = $li[0]; |
|
160 } |
|
161 } |
|
162 if (li) { |
|
163 selectItem(li); |
|
164 return true; |
|
165 } else { |
|
166 return false; |
|
167 } |
|
168 }; |
|
169 |
|
170 function selectItem(li) { |
|
171 if (!li) { |
|
172 li = document.createElement("li"); |
|
173 li.extra = []; |
|
174 li.selectValue = ""; |
|
175 } |
|
176 var v = $.trim(li.selectValue ? li.selectValue : li.innerHTML); |
|
177 input.lastSelected = v; |
|
178 prev = v; |
|
179 $results.html(""); |
|
180 $input.val(v); |
|
181 hideResultsNow(); |
|
182 if (options.onItemSelect) setTimeout(function() { options.onItemSelect(li) }, 1); |
|
183 }; |
|
184 |
|
185 // selects a portion of the input string |
|
186 function createSelection(start, end){ |
|
187 // get a reference to the input element |
|
188 var field = $input.get(0); |
|
189 if( field.createTextRange ){ |
|
190 var selRange = field.createTextRange(); |
|
191 selRange.collapse(true); |
|
192 selRange.moveStart("character", start); |
|
193 selRange.moveEnd("character", end); |
|
194 selRange.select(); |
|
195 } else if( field.setSelectionRange ){ |
|
196 field.setSelectionRange(start, end); |
|
197 } else { |
|
198 if( field.selectionStart ){ |
|
199 field.selectionStart = start; |
|
200 field.selectionEnd = end; |
|
201 } |
|
202 } |
|
203 field.focus(); |
|
204 }; |
|
205 |
|
206 // fills in the input box w/the first match (assumed to be the best match) |
|
207 function autoFill(sValue){ |
|
208 // if the last user key pressed was backspace, don't autofill |
|
209 if( lastKeyPressCode != 8 ){ |
|
210 // fill in the value (keep the case the user has typed) |
|
211 $input.val($input.val() + sValue.substring(prev.length)); |
|
212 // select the portion of the value not typed by the user (so the next character will erase) |
|
213 createSelection(prev.length, sValue.length); |
|
214 } |
|
215 }; |
|
216 |
|
217 function showResults() { |
|
218 // get the position of the input field right now (in case the DOM is shifted) |
|
219 var pos = findPos(input); |
|
220 // either use the specified width, or autocalculate based on form element |
|
221 var iWidth = (options.width > 0) ? options.width : $input.width(); |
|
222 // reposition |
|
223 $results.css({ |
|
224 width: parseInt(iWidth) + "px", |
|
225 top: (pos.y + input.offsetHeight) + "px", |
|
226 left: pos.x + "px" |
|
227 }).show(); |
|
228 }; |
|
229 |
|
230 function hideResults() { |
|
231 if (timeout) clearTimeout(timeout); |
|
232 timeout = setTimeout(hideResultsNow, 200); |
|
233 }; |
|
234 |
|
235 function hideResultsNow() { |
|
236 if (timeout) clearTimeout(timeout); |
|
237 $input.removeClass(options.loadingClass); |
|
238 if ($results.is(":visible")) { |
|
239 $results.hide(); |
|
240 } |
|
241 if (options.mustMatch) { |
|
242 var v = $input.val(); |
|
243 if (v != input.lastSelected) { |
|
244 selectItem(null); |
|
245 } |
|
246 } |
|
247 }; |
|
248 |
|
249 function receiveData(q, data) { |
|
250 if (data) { |
|
251 $input.removeClass(options.loadingClass); |
|
252 results.innerHTML = ""; |
|
253 |
|
254 // if the field no longer has focus or if there are no matches, do not display the drop down |
|
255 if( !hasFocus || data.length == 0 ) return hideResultsNow(); |
|
256 |
|
257 if ($.browser.msie) { |
|
258 // we put a styled iframe behind the calendar so HTML SELECT elements don't show through |
|
259 $results.append(document.createElement('iframe')); |
|
260 } |
|
261 results.appendChild(dataToDom(data)); |
|
262 // autofill in the complete box w/the first match as long as the user hasn't entered in more data |
|
263 if( options.autoFill && ($input.val().toLowerCase() == q.toLowerCase()) ) autoFill(data[0][0]); |
|
264 showResults(); |
|
265 } else { |
|
266 hideResultsNow(); |
|
267 } |
|
268 }; |
|
269 |
|
270 function parseData(data) { |
|
271 if (!data) return null; |
|
272 var parsed = []; |
|
273 var rows = data.split(options.lineSeparator); |
|
274 for (var i=0; i < rows.length; i++) { |
|
275 var row = $.trim(rows[i]); |
|
276 if (row) { |
|
277 parsed[parsed.length] = row.split(options.cellSeparator); |
|
278 } |
|
279 } |
|
280 return parsed; |
|
281 }; |
|
282 |
|
283 function dataToDom(data) { |
|
284 var ul = document.createElement("ul"); |
|
285 var num = data.length; |
|
286 |
|
287 // limited results to a max number |
|
288 if( (options.maxItemsToShow > 0) && (options.maxItemsToShow < num) ) num = options.maxItemsToShow; |
|
289 |
|
290 for (var i=0; i < num; i++) { |
|
291 var row = data[i]; |
|
292 if (!row) continue; |
|
293 var li = document.createElement("li"); |
|
294 if (options.formatItem) { |
|
295 li.innerHTML = options.formatItem(row, i, num); |
|
296 li.selectValue = row[0]; |
|
297 } else { |
|
298 li.innerHTML = row[0]; |
|
299 li.selectValue = row[0]; |
|
300 } |
|
301 var extra = null; |
|
302 if (row.length > 1) { |
|
303 extra = []; |
|
304 for (var j=1; j < row.length; j++) { |
|
305 extra[extra.length] = row[j]; |
|
306 } |
|
307 } |
|
308 li.extra = extra; |
|
309 ul.appendChild(li); |
|
310 $(li).hover( |
|
311 function() { $("li", ul).removeClass("ac_over"); $(this).addClass("ac_over"); active = $("li", ul).indexOf($(this).get(0)); }, |
|
312 function() { $(this).removeClass("ac_over"); } |
|
313 ).click(function(e) { e.preventDefault(); e.stopPropagation(); selectItem(this) }); |
|
314 } |
|
315 return ul; |
|
316 }; |
|
317 |
|
318 function requestData(q) { |
|
319 if (!options.matchCase) q = q.toLowerCase(); |
|
320 var data = options.cacheLength ? loadFromCache(q) : null; |
|
321 // recieve the cached data |
|
322 if (data) { |
|
323 receiveData(q, data); |
|
324 // if an AJAX url has been supplied, try loading the data now |
|
325 } else if( (typeof options.url == "string") && (options.url.length > 0) ){ |
|
326 $.get(makeUrl(q), function(data) { |
|
327 data = parseData(data); |
|
328 addToCache(q, data); |
|
329 receiveData(q, data); |
|
330 }); |
|
331 // if there's been no data found, remove the loading class |
|
332 } else { |
|
333 $input.removeClass(options.loadingClass); |
|
334 } |
|
335 }; |
|
336 |
|
337 function makeUrl(q) { |
|
338 var url = options.url + "?q=" + encodeURI(q); |
|
339 for (var i in options.extraParams) { |
|
340 url += "&" + i + "=" + encodeURI(options.extraParams[i]); |
|
341 } |
|
342 return url; |
|
343 }; |
|
344 |
|
345 function loadFromCache(q) { |
|
346 if (!q) return null; |
|
347 if (cache.data[q]) return cache.data[q]; |
|
348 if (options.matchSubset) { |
|
349 for (var i = q.length - 1; i >= options.minChars; i--) { |
|
350 var qs = q.substr(0, i); |
|
351 var c = cache.data[qs]; |
|
352 if (c) { |
|
353 var csub = []; |
|
354 for (var j = 0; j < c.length; j++) { |
|
355 var x = c[j]; |
|
356 var x0 = x[0]; |
|
357 if (matchSubset(x0, q)) { |
|
358 csub[csub.length] = x; |
|
359 } |
|
360 } |
|
361 return csub; |
|
362 } |
|
363 } |
|
364 } |
|
365 return null; |
|
366 }; |
|
367 |
|
368 function matchSubset(s, sub) { |
|
369 if (!options.matchCase) s = s.toLowerCase(); |
|
370 var i = s.indexOf(sub); |
|
371 if (i == -1) return false; |
|
372 return i == 0 || options.matchContains; |
|
373 }; |
|
374 |
|
375 this.flushCache = function() { |
|
376 flushCache(); |
|
377 }; |
|
378 |
|
379 this.setExtraParams = function(p) { |
|
380 options.extraParams = p; |
|
381 }; |
|
382 |
|
383 this.findValue = function(){ |
|
384 var q = $input.val(); |
|
385 |
|
386 if (!options.matchCase) q = q.toLowerCase(); |
|
387 var data = options.cacheLength ? loadFromCache(q) : null; |
|
388 if (data) { |
|
389 findValueCallback(q, data); |
|
390 } else if( (typeof options.url == "string") && (options.url.length > 0) ){ |
|
391 $.get(makeUrl(q), function(data) { |
|
392 data = parseData(data) |
|
393 addToCache(q, data); |
|
394 findValueCallback(q, data); |
|
395 }); |
|
396 } else { |
|
397 // no matches |
|
398 findValueCallback(q, null); |
|
399 } |
|
400 } |
|
401 |
|
402 function findValueCallback(q, data){ |
|
403 if (data) $input.removeClass(options.loadingClass); |
|
404 |
|
405 var num = (data) ? data.length : 0; |
|
406 var li = null; |
|
407 |
|
408 for (var i=0; i < num; i++) { |
|
409 var row = data[i]; |
|
410 |
|
411 if( row[0].toLowerCase() == q.toLowerCase() ){ |
|
412 li = document.createElement("li"); |
|
413 if (options.formatItem) { |
|
414 li.innerHTML = options.formatItem(row, i, num); |
|
415 li.selectValue = row[0]; |
|
416 } else { |
|
417 li.innerHTML = row[0]; |
|
418 li.selectValue = row[0]; |
|
419 } |
|
420 var extra = null; |
|
421 if( row.length > 1 ){ |
|
422 extra = []; |
|
423 for (var j=1; j < row.length; j++) { |
|
424 extra[extra.length] = row[j]; |
|
425 } |
|
426 } |
|
427 li.extra = extra; |
|
428 } |
|
429 } |
|
430 |
|
431 if( options.onFindValue ) setTimeout(function() { options.onFindValue(li) }, 1); |
|
432 } |
|
433 |
|
434 function addToCache(q, data) { |
|
435 if (!data || !q || !options.cacheLength) return; |
|
436 if (!cache.length || cache.length > options.cacheLength) { |
|
437 flushCache(); |
|
438 cache.length++; |
|
439 } else if (!cache[q]) { |
|
440 cache.length++; |
|
441 } |
|
442 cache.data[q] = data; |
|
443 }; |
|
444 |
|
445 function findPos(obj) { |
|
446 var curleft = obj.offsetLeft || 0; |
|
447 var curtop = obj.offsetTop || 0; |
|
448 while (obj = obj.offsetParent) { |
|
449 curleft += obj.offsetLeft |
|
450 curtop += obj.offsetTop |
|
451 } |
|
452 return {x:curleft,y:curtop}; |
|
453 } |
|
454 } |
|
455 |
|
456 jQuery.fn.autocomplete = function(url, options, data) { |
|
457 // Make sure options exists |
|
458 options = options || {}; |
|
459 // Set url as option |
|
460 options.url = url; |
|
461 // set some bulk local data |
|
462 options.data = ((typeof data == "object") && (data.constructor == Array)) ? data : null; |
|
463 |
|
464 // Set default values for required options |
|
465 options.inputClass = options.inputClass || "ac_input"; |
|
466 options.resultsClass = options.resultsClass || "ac_results"; |
|
467 options.lineSeparator = options.lineSeparator || "\n"; |
|
468 options.cellSeparator = options.cellSeparator || "|"; |
|
469 options.minChars = options.minChars || 1; |
|
470 options.delay = options.delay || 400; |
|
471 options.matchCase = options.matchCase || 0; |
|
472 options.matchSubset = options.matchSubset || 1; |
|
473 options.matchContains = options.matchContains || 0; |
|
474 options.cacheLength = options.cacheLength || 1; |
|
475 options.mustMatch = options.mustMatch || 0; |
|
476 options.extraParams = options.extraParams || {}; |
|
477 options.loadingClass = options.loadingClass || "ac_loading"; |
|
478 options.selectFirst = options.selectFirst || false; |
|
479 options.selectOnly = options.selectOnly || false; |
|
480 options.maxItemsToShow = options.maxItemsToShow || -1; |
|
481 options.autoFill = options.autoFill || false; |
|
482 options.width = parseInt(options.width, 10) || 0; |
|
483 |
|
484 this.each(function() { |
|
485 var input = this; |
|
486 new jQuery.autocomplete(input, options); |
|
487 }); |
|
488 |
|
489 // Don't break the chain |
|
490 return this; |
|
491 } |
|
492 |
|
493 jQuery.fn.autocompleteArray = function(data, options) { |
|
494 return this.autocomplete(null, options, data); |
|
495 } |
|
496 |
|
497 jQuery.fn.indexOf = function(e){ |
|
498 for( var i=0; i<this.length; i++ ){ |
|
499 if( this[i] == e ) return i; |
|
500 } |
|
501 return -1; |
|
502 }; |