2 * jQuery autocomplete plugin
3 * Version 2.0.0 (2008-03-22)
4 * @requires jQuery v1.1.1+
6 * Dual licensed under the MIT and GPL licenses:
7 * http://www.opensource.org/licenses/mit-license.php
8 * http://www.gnu.org/licenses/gpl.html
11 * http://www.dyve.net/jquery
16 * The autocompleter object
18 $.autocomplete
= function(input
, options
) {
20 // Create a link to self
22 // Create jQuery object for input element
23 var $input
= $(input
).attr("autocomplete", "off");
24 // Apply inputClass if necessary
25 if (options
.inputClass
) {
26 $input
.addClass(options
.inputClass
);
30 var results
= document
.createElement("div");
32 // Create jQuery object for results
33 // var $results = $(results);
34 var $results
= $(results
).hide().addClass(options
.resultsClass
).css("position", "absolute");
35 if( options
.width
> 0 ) {
36 $results
.css("width", options
.width
);
39 // Add to body element
40 $("body").append(results
);
42 input
.autocompleter
= me
;
50 var lastKeyPressCode
= null;
51 var mouseDownOnSelect
= false;
52 var hidingResults
= false;
55 function flushCache(){
64 // if there is a data array supplied
65 if( options
.data
!= null ){
66 var sFirstChar
= "", stMatchSets
= {}, row
= [];
68 // no url was specified, we need to adjust the cache length to make sure it fits the local data store
69 if (typeof options
.url
!= "string") {
70 options
.cacheLength
= 1;
73 // loop through the array and create a lookup structure
74 for( var i
=0; i
< options
.data
.length
; i
++ ){
75 // if row is a string, make an array otherwise just reference the array
76 row
= ((typeof options
.data
[i
] == "string") ? [options
.data
[i
]] : options
.data
[i
]);
78 // if the length is zero, don't add to list
79 if( row
[0].length
> 0 ){
80 // get the first character
81 sFirstChar
= row
[0].substring(0, 1).toLowerCase();
82 // if no lookup array for this character exists, look it up now
83 if( !stMatchSets
[sFirstChar
] ) stMatchSets
[sFirstChar
] = [];
84 // if the match is a string
85 stMatchSets
[sFirstChar
].push(row
);
89 // add the data items to the cache
90 for (var k
in stMatchSets
) {
91 // increase the cache size
92 options
.cacheLength
++;
94 addToCache(k
, stMatchSets
[k
]);
99 .keydown(function(e
) {
100 // track last key pressed
101 lastKeyPressCode
= e
.keyCode
;
113 if( selectCurrent() ){
114 // make sure to blur off the current field
117 // give focus with a small timeout (weird behaviour in FF)
118 setTimeout(function() { $input
.focus() }, 10);
123 if (timeout
) clearTimeout(timeout
);
124 timeout
= setTimeout(onChange
, options
.delay
);
129 // track whether the field has focus, we shouldn't process any results if the field no longer has focus
133 // track whether the field has focus
135 if (!mouseDownOnSelect
) {
142 function onChange() {
143 // ignore if the following keys are pressed: [del] [shift] [capslock]
144 if( lastKeyPressCode
== 46 || (lastKeyPressCode
> 8 && lastKeyPressCode
< 32) ) return $results
.hide();
145 var v
= $input
.val();
146 if (v
== prev
) return;
148 if (v
.length
>= options
.minChars
) {
149 $input
.addClass(options
.loadingClass
);
152 $input
.removeClass(options
.loadingClass
);
157 function moveSelect(step
) {
159 var lis
= $("li", results
);
166 } else if (active
>= lis
.size()) {
167 active
= lis
.size() - 1;
170 lis
.removeClass("ac_over");
172 $(lis
[active
]).addClass("ac_over");
174 // Weird behaviour in IE
175 // if (lis[active] && lis[active].scrollIntoView) {
176 // lis[active].scrollIntoView(false);
181 function selectCurrent() {
182 var li
= $("li.ac_over", results
)[0];
184 var $li
= $("li", results
);
185 if (options
.selectOnly
) {
186 if ($li
.length
== 1) li
= $li
[0];
187 } else if (options
.selectFirst
) {
199 function selectItem(li
) {
201 li
= document
.createElement("li");
205 var v
= $.trim(li
.selectValue
? li
.selectValue
: li
.innerHTML
);
206 input
.lastSelected
= v
;
211 if (options
.onItemSelect
) {
212 setTimeout(function() { options
.onItemSelect(li
) }, 1);
216 // selects a portion of the input string
217 function createSelection(start
, end
){
218 // get a reference to the input element
219 var field
= $input
.get(0);
220 if( field
.createTextRange
){
221 var selRange
= field
.createTextRange();
222 selRange
.collapse(true);
223 selRange
.moveStart("character", start
);
224 selRange
.moveEnd("character", end
);
226 } else if( field
.setSelectionRange
){
227 field
.setSelectionRange(start
, end
);
229 if( field
.selectionStart
){
230 field
.selectionStart
= start
;
231 field
.selectionEnd
= end
;
237 // fills in the input box w/the first
match (assumed to be the best match
)
238 function autoFill(sValue
){
239 // if the last user key pressed was backspace, don't autofill
240 if( lastKeyPressCode
!= 8 ){
241 // fill in the value (keep the case the user has typed)
242 $input
.val($input
.val() + sValue
.substring(prev
.length
));
243 // select the portion of the value not typed by the user (so the next character will erase)
244 createSelection(prev
.length
, sValue
.length
);
248 function showResults() {
249 // get the position of the input field right now (in case the DOM is shifted)
250 var pos
= findPos(input
);
251 // either use the specified width, or autocalculate based on form element
252 var iWidth
= (options
.width
> 0) ? options
.width
: $input
.width();
255 width
: parseInt(iWidth
) + "px",
256 top
: (pos
.y
+ input
.offsetHeight
) + "px",
261 function hideResults() {
262 if (timeout
) clearTimeout(timeout
);
263 timeout
= setTimeout(hideResultsNow
, 200);
266 function hideResultsNow() {
270 hidingResults
= true;
273 clearTimeout(timeout
);
276 var v
= $input
.removeClass(options
.loadingClass
).val();
278 if ($results
.is(":visible")) {
282 if (options
.mustMatch
) {
283 if (!input
.lastSelected
|| input
.lastSelected
!= v
) {
288 hidingResults
= false;
291 function receiveData(q
, data
) {
293 $input
.removeClass(options
.loadingClass
);
294 results
.innerHTML
= "";
296 // if the field no longer has focus or if there are no matches, do not display the drop down
297 if( !hasFocus
|| data
.length
== 0 ) return hideResultsNow();
299 if ($.browser
.msie
) {
300 // we put a styled iframe behind the calendar so HTML SELECT elements don't show through
301 $results
.append(document
.createElement('iframe'));
303 results
.appendChild(dataToDom(data
));
304 // autofill in the complete box w/the first match as
long as the user hasn
't entered in more data
305 if( options.autoFill && ($input.val().toLowerCase() == q.toLowerCase()) ) autoFill(data[0][0]);
312 function parseData(data) {
313 if (!data) return null;
315 var rows = data.split(options.lineSeparator);
316 for (var i=0; i < rows.length; i++) {
317 var row = $.trim(rows[i]);
319 parsed[parsed.length] = row.split(options.cellSeparator);
325 function dataToDom(data) {
326 var ul = document.createElement("ul");
327 var num = data.length;
329 // limited results to a max number
330 if( (options.maxItemsToShow > 0) && (options.maxItemsToShow < num) ) num = options.maxItemsToShow;
332 for (var i=0; i < num; i++) {
335 var li = document.createElement("li");
336 if (options.formatItem) {
337 li.innerHTML = options.formatItem(row, i, num);
338 li.selectValue = row[0];
340 li.innerHTML = row[0];
341 li.selectValue = row[0];
344 if (row.length > 1) {
346 for (var j=1; j < row.length; j++) {
347 extra[extra.length] = row[j];
356 active = $("li", ul).removeClass("ac_over").index($this[0]);
357 $this.addClass("ac_over");
360 $(this).removeClass("ac_over");
362 ).click(function(e) {
369 $(ul).mousedown(function() {
370 mouseDownOnSelect = true;
371 }).mouseup(function() {
372 mouseDownOnSelect = false;
377 function requestData(q) {
378 if (!options.matchCase) q = q.toLowerCase();
379 var data = options.cacheLength ? loadFromCache(q) : null;
380 // recieve the cached data
382 receiveData(q, data);
383 // if an AJAX url has been supplied, try loading the data now
384 } else if( (typeof options.url == "string") && (options.url.length > 0) ){
385 $.get(makeUrl(q), function(data) {
386 data = parseData(data);
388 receiveData(q, data);
390 // if there's been no data found
, remove the loading
class
392 $input
.removeClass(options
.loadingClass
);
396 function makeUrl(q
) {
397 var sep
= options
.url
.indexOf('?') == -1 ? '?' : '&';
398 var url
= options
.url
+ sep
+ "q=" + encodeURI(q
);
399 for (var i
in options
.extraParams
) {
400 url
+= "&" + i
+ "=" + encodeURI(options
.extraParams
[i
]);
405 function loadFromCache(q
) {
407 if (cache
.data
[q
]) return cache
.data
[q
];
408 if (options
.matchSubset
) {
409 for (var i
= q
.length
- 1; i
>= options
.minChars
; i
--) {
410 var qs
= q
.substr(0, i
);
411 var c
= cache
.data
[qs
];
414 for (var j
= 0; j
< c
.length
; j
++) {
417 if (matchSubset(x0
, q
)) {
418 csub
[csub
.length
] = x
;
428 function matchSubset(s
, sub
) {
429 if (!options
.matchCase
) s
= s
.toLowerCase();
430 var i
= s
.indexOf(sub
);
431 if (i
== -1) return false;
432 return i
== 0 || options
.matchContains
;
435 this.flushCache
= function() {
439 this.setExtraParams
= function(p
) {
440 options
.extraParams
= p
;
443 this.findValue
= function(){
444 var q
= $input
.val();
446 if (!options
.matchCase
) q
= q
.toLowerCase();
447 var data
= options
.cacheLength
? loadFromCache(q
) : null;
449 findValueCallback(q
, data
);
450 } else if( (typeof options
.url
== "string") && (options
.url
.length
> 0) ){
451 $.get(makeUrl(q
), function(data
) {
452 data
= parseData(data
)
454 findValueCallback(q
, data
);
458 findValueCallback(q
, null);
462 function findValueCallback(q
, data
){
463 if (data
) $input
.removeClass(options
.loadingClass
);
465 var num
= (data
) ? data
.length
: 0;
468 for (var i
=0; i
< num
; i
++) {
471 if( row
[0].toLowerCase() == q
.toLowerCase() ){
472 li
= document
.createElement("li");
473 if (options
.formatItem
) {
474 li
.innerHTML
= options
.formatItem(row
, i
, num
);
475 li
.selectValue
= row
[0];
477 li
.innerHTML
= row
[0];
478 li
.selectValue
= row
[0];
481 if( row
.length
> 1 ){
483 for (var j
=1; j
< row
.length
; j
++) {
484 extra
[extra
.length
] = row
[j
];
491 if( options
.onFindValue
) setTimeout(function() { options
.onFindValue(li
) }, 1);
494 function addToCache(q
, data
) {
495 if (!data
|| !q
|| !options
.cacheLength
) return;
496 if (!cache
.length
|| cache
.length
> options
.cacheLength
) {
499 } else if (!cache
[q
]) {
502 cache
.data
[q
] = data
;
505 function findPos(obj
) {
506 var curleft
= obj
.offsetLeft
|| 0;
507 var curtop
= obj
.offsetTop
|| 0;
508 while (obj
= obj
.offsetParent
) {
509 curleft
+= obj
.offsetLeft
510 curtop
+= obj
.offsetTop
512 return {x
:curleft
,y
:curtop
};
517 * The autocomplete plugin itself
519 $.fn
.autocomplete
= function(url
, options
, data
) {
521 // Make sure options exists
522 options
= options
|| {};
525 // set some bulk local data
526 options
.data
= ((typeof data
== "object") && (data
.constructor
== Array
)) ? data
: null;
528 // Set default values for required options (set global defaults in $.fn.autocomplete.defaults)
530 inputClass
: "ac_input",
531 resultsClass
: "ac_results",
542 loadingClass
: "ac_loading",
548 }, $.fn
.autocomplete
.defaults
, options
);
550 options
.width
= parseInt(options
.width
, 10);
552 return this.each(function() {
554 new $.autocomplete(input
, options
);