bgneal@45
|
1 /*
|
bgneal@45
|
2 * Autocomplete - jQuery plugin 1.0.2
|
bgneal@45
|
3 *
|
bgneal@45
|
4 * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
|
bgneal@45
|
5 *
|
bgneal@45
|
6 * Dual licensed under the MIT and GPL licenses:
|
bgneal@45
|
7 * http://www.opensource.org/licenses/mit-license.php
|
bgneal@45
|
8 * http://www.gnu.org/licenses/gpl.html
|
bgneal@45
|
9 *
|
bgneal@45
|
10 * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
|
bgneal@45
|
11 *
|
bgneal@45
|
12 */
|
bgneal@45
|
13
|
bgneal@45
|
14 ;(function($) {
|
bgneal@45
|
15
|
bgneal@45
|
16 $.fn.extend({
|
bgneal@45
|
17 autocomplete: function(urlOrData, options) {
|
bgneal@45
|
18 var isUrl = typeof urlOrData == "string";
|
bgneal@45
|
19 options = $.extend({}, $.Autocompleter.defaults, {
|
bgneal@45
|
20 url: isUrl ? urlOrData : null,
|
bgneal@45
|
21 data: isUrl ? null : urlOrData,
|
bgneal@45
|
22 delay: isUrl ? $.Autocompleter.defaults.delay : 10,
|
bgneal@45
|
23 max: options && !options.scroll ? 10 : 150
|
bgneal@45
|
24 }, options);
|
bgneal@45
|
25
|
bgneal@45
|
26 // if highlight is set to false, replace it with a do-nothing function
|
bgneal@45
|
27 options.highlight = options.highlight || function(value) { return value; };
|
bgneal@45
|
28
|
bgneal@45
|
29 // if the formatMatch option is not specified, then use formatItem for backwards compatibility
|
bgneal@45
|
30 options.formatMatch = options.formatMatch || options.formatItem;
|
bgneal@45
|
31
|
bgneal@45
|
32 return this.each(function() {
|
bgneal@45
|
33 new $.Autocompleter(this, options);
|
bgneal@45
|
34 });
|
bgneal@45
|
35 },
|
bgneal@45
|
36 result: function(handler) {
|
bgneal@45
|
37 return this.bind("result", handler);
|
bgneal@45
|
38 },
|
bgneal@45
|
39 search: function(handler) {
|
bgneal@45
|
40 return this.trigger("search", [handler]);
|
bgneal@45
|
41 },
|
bgneal@45
|
42 flushCache: function() {
|
bgneal@45
|
43 return this.trigger("flushCache");
|
bgneal@45
|
44 },
|
bgneal@45
|
45 setOptions: function(options){
|
bgneal@45
|
46 return this.trigger("setOptions", [options]);
|
bgneal@45
|
47 },
|
bgneal@45
|
48 unautocomplete: function() {
|
bgneal@45
|
49 return this.trigger("unautocomplete");
|
bgneal@45
|
50 }
|
bgneal@45
|
51 });
|
bgneal@45
|
52
|
bgneal@45
|
53 $.Autocompleter = function(input, options) {
|
bgneal@45
|
54
|
bgneal@45
|
55 var KEY = {
|
bgneal@45
|
56 UP: 38,
|
bgneal@45
|
57 DOWN: 40,
|
bgneal@45
|
58 DEL: 46,
|
bgneal@45
|
59 TAB: 9,
|
bgneal@45
|
60 RETURN: 13,
|
bgneal@45
|
61 ESC: 27,
|
bgneal@45
|
62 COMMA: 188,
|
bgneal@45
|
63 PAGEUP: 33,
|
bgneal@45
|
64 PAGEDOWN: 34,
|
bgneal@45
|
65 BACKSPACE: 8
|
bgneal@45
|
66 };
|
bgneal@45
|
67
|
bgneal@45
|
68 // Create $ object for input element
|
bgneal@45
|
69 var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
|
bgneal@45
|
70
|
bgneal@45
|
71 var timeout;
|
bgneal@45
|
72 var previousValue = "";
|
bgneal@45
|
73 var cache = $.Autocompleter.Cache(options);
|
bgneal@45
|
74 var hasFocus = 0;
|
bgneal@45
|
75 var lastKeyPressCode;
|
bgneal@45
|
76 var config = {
|
bgneal@45
|
77 mouseDownOnSelect: false
|
bgneal@45
|
78 };
|
bgneal@45
|
79 var select = $.Autocompleter.Select(options, input, selectCurrent, config);
|
bgneal@45
|
80
|
bgneal@45
|
81 var blockSubmit;
|
bgneal@45
|
82
|
bgneal@45
|
83 // prevent form submit in opera when selecting with return key
|
bgneal@45
|
84 $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
|
bgneal@45
|
85 if (blockSubmit) {
|
bgneal@45
|
86 blockSubmit = false;
|
bgneal@45
|
87 return false;
|
bgneal@45
|
88 }
|
bgneal@45
|
89 });
|
bgneal@45
|
90
|
bgneal@45
|
91 // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
|
bgneal@45
|
92 $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
|
bgneal@45
|
93 // track last key pressed
|
bgneal@45
|
94 lastKeyPressCode = event.keyCode;
|
bgneal@45
|
95 switch(event.keyCode) {
|
bgneal@45
|
96
|
bgneal@45
|
97 case KEY.UP:
|
bgneal@45
|
98 event.preventDefault();
|
bgneal@45
|
99 if ( select.visible() ) {
|
bgneal@45
|
100 select.prev();
|
bgneal@45
|
101 } else {
|
bgneal@45
|
102 onChange(0, true);
|
bgneal@45
|
103 }
|
bgneal@45
|
104 break;
|
bgneal@45
|
105
|
bgneal@45
|
106 case KEY.DOWN:
|
bgneal@45
|
107 event.preventDefault();
|
bgneal@45
|
108 if ( select.visible() ) {
|
bgneal@45
|
109 select.next();
|
bgneal@45
|
110 } else {
|
bgneal@45
|
111 onChange(0, true);
|
bgneal@45
|
112 }
|
bgneal@45
|
113 break;
|
bgneal@45
|
114
|
bgneal@45
|
115 case KEY.PAGEUP:
|
bgneal@45
|
116 event.preventDefault();
|
bgneal@45
|
117 if ( select.visible() ) {
|
bgneal@45
|
118 select.pageUp();
|
bgneal@45
|
119 } else {
|
bgneal@45
|
120 onChange(0, true);
|
bgneal@45
|
121 }
|
bgneal@45
|
122 break;
|
bgneal@45
|
123
|
bgneal@45
|
124 case KEY.PAGEDOWN:
|
bgneal@45
|
125 event.preventDefault();
|
bgneal@45
|
126 if ( select.visible() ) {
|
bgneal@45
|
127 select.pageDown();
|
bgneal@45
|
128 } else {
|
bgneal@45
|
129 onChange(0, true);
|
bgneal@45
|
130 }
|
bgneal@45
|
131 break;
|
bgneal@45
|
132
|
bgneal@45
|
133 // matches also semicolon
|
bgneal@45
|
134 case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
|
bgneal@45
|
135 case KEY.TAB:
|
bgneal@45
|
136 case KEY.RETURN:
|
bgneal@45
|
137 if( selectCurrent() ) {
|
bgneal@45
|
138 // stop default to prevent a form submit, Opera needs special handling
|
bgneal@45
|
139 event.preventDefault();
|
bgneal@45
|
140 blockSubmit = true;
|
bgneal@45
|
141 return false;
|
bgneal@45
|
142 }
|
bgneal@45
|
143 break;
|
bgneal@45
|
144
|
bgneal@45
|
145 case KEY.ESC:
|
bgneal@45
|
146 select.hide();
|
bgneal@45
|
147 break;
|
bgneal@45
|
148
|
bgneal@45
|
149 default:
|
bgneal@45
|
150 clearTimeout(timeout);
|
bgneal@45
|
151 timeout = setTimeout(onChange, options.delay);
|
bgneal@45
|
152 break;
|
bgneal@45
|
153 }
|
bgneal@45
|
154 }).focus(function(){
|
bgneal@45
|
155 // track whether the field has focus, we shouldn't process any
|
bgneal@45
|
156 // results if the field no longer has focus
|
bgneal@45
|
157 hasFocus++;
|
bgneal@45
|
158 }).blur(function() {
|
bgneal@45
|
159 hasFocus = 0;
|
bgneal@45
|
160 if (!config.mouseDownOnSelect) {
|
bgneal@45
|
161 hideResults();
|
bgneal@45
|
162 }
|
bgneal@45
|
163 }).click(function() {
|
bgneal@45
|
164 // show select when clicking in a focused field
|
bgneal@45
|
165 if ( hasFocus++ > 1 && !select.visible() ) {
|
bgneal@45
|
166 onChange(0, true);
|
bgneal@45
|
167 }
|
bgneal@45
|
168 }).bind("search", function() {
|
bgneal@45
|
169 // TODO why not just specifying both arguments?
|
bgneal@45
|
170 var fn = (arguments.length > 1) ? arguments[1] : null;
|
bgneal@45
|
171 function findValueCallback(q, data) {
|
bgneal@45
|
172 var result;
|
bgneal@45
|
173 if( data && data.length ) {
|
bgneal@45
|
174 for (var i=0; i < data.length; i++) {
|
bgneal@45
|
175 if( data[i].result.toLowerCase() == q.toLowerCase() ) {
|
bgneal@45
|
176 result = data[i];
|
bgneal@45
|
177 break;
|
bgneal@45
|
178 }
|
bgneal@45
|
179 }
|
bgneal@45
|
180 }
|
bgneal@45
|
181 if( typeof fn == "function" ) fn(result);
|
bgneal@45
|
182 else $input.trigger("result", result && [result.data, result.value]);
|
bgneal@45
|
183 }
|
bgneal@45
|
184 $.each(trimWords($input.val()), function(i, value) {
|
bgneal@45
|
185 request(value, findValueCallback, findValueCallback);
|
bgneal@45
|
186 });
|
bgneal@45
|
187 }).bind("flushCache", function() {
|
bgneal@45
|
188 cache.flush();
|
bgneal@45
|
189 }).bind("setOptions", function() {
|
bgneal@45
|
190 $.extend(options, arguments[1]);
|
bgneal@45
|
191 // if we've updated the data, repopulate
|
bgneal@45
|
192 if ( "data" in arguments[1] )
|
bgneal@45
|
193 cache.populate();
|
bgneal@45
|
194 }).bind("unautocomplete", function() {
|
bgneal@45
|
195 select.unbind();
|
bgneal@45
|
196 $input.unbind();
|
bgneal@45
|
197 $(input.form).unbind(".autocomplete");
|
bgneal@45
|
198 });
|
bgneal@45
|
199
|
bgneal@45
|
200
|
bgneal@45
|
201 function selectCurrent() {
|
bgneal@45
|
202 var selected = select.selected();
|
bgneal@45
|
203 if( !selected )
|
bgneal@45
|
204 return false;
|
bgneal@45
|
205
|
bgneal@45
|
206 var v = selected.result;
|
bgneal@45
|
207 previousValue = v;
|
bgneal@45
|
208
|
bgneal@45
|
209 if ( options.multiple ) {
|
bgneal@45
|
210 var words = trimWords($input.val());
|
bgneal@45
|
211 if ( words.length > 1 ) {
|
bgneal@45
|
212 v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
|
bgneal@45
|
213 }
|
bgneal@45
|
214 v += options.multipleSeparator;
|
bgneal@45
|
215 }
|
bgneal@45
|
216
|
bgneal@45
|
217 $input.val(v);
|
bgneal@45
|
218 hideResultsNow();
|
bgneal@45
|
219 $input.trigger("result", [selected.data, selected.value]);
|
bgneal@45
|
220 return true;
|
bgneal@45
|
221 }
|
bgneal@45
|
222
|
bgneal@45
|
223 function onChange(crap, skipPrevCheck) {
|
bgneal@45
|
224 if( lastKeyPressCode == KEY.DEL ) {
|
bgneal@45
|
225 select.hide();
|
bgneal@45
|
226 return;
|
bgneal@45
|
227 }
|
bgneal@45
|
228
|
bgneal@45
|
229 var currentValue = $input.val();
|
bgneal@45
|
230
|
bgneal@45
|
231 if ( !skipPrevCheck && currentValue == previousValue )
|
bgneal@45
|
232 return;
|
bgneal@45
|
233
|
bgneal@45
|
234 previousValue = currentValue;
|
bgneal@45
|
235
|
bgneal@45
|
236 currentValue = lastWord(currentValue);
|
bgneal@45
|
237 if ( currentValue.length >= options.minChars) {
|
bgneal@45
|
238 $input.addClass(options.loadingClass);
|
bgneal@45
|
239 if (!options.matchCase)
|
bgneal@45
|
240 currentValue = currentValue.toLowerCase();
|
bgneal@45
|
241 request(currentValue, receiveData, hideResultsNow);
|
bgneal@45
|
242 } else {
|
bgneal@45
|
243 stopLoading();
|
bgneal@45
|
244 select.hide();
|
bgneal@45
|
245 }
|
bgneal@45
|
246 };
|
bgneal@45
|
247
|
bgneal@45
|
248 function trimWords(value) {
|
bgneal@45
|
249 if ( !value ) {
|
bgneal@45
|
250 return [""];
|
bgneal@45
|
251 }
|
bgneal@45
|
252 var words = value.split( options.multipleSeparator );
|
bgneal@45
|
253 var result = [];
|
bgneal@45
|
254 $.each(words, function(i, value) {
|
bgneal@45
|
255 if ( $.trim(value) )
|
bgneal@45
|
256 result[i] = $.trim(value);
|
bgneal@45
|
257 });
|
bgneal@45
|
258 return result;
|
bgneal@45
|
259 }
|
bgneal@45
|
260
|
bgneal@45
|
261 function lastWord(value) {
|
bgneal@45
|
262 if ( !options.multiple )
|
bgneal@45
|
263 return value;
|
bgneal@45
|
264 var words = trimWords(value);
|
bgneal@45
|
265 return words[words.length - 1];
|
bgneal@45
|
266 }
|
bgneal@45
|
267
|
bgneal@45
|
268 // fills in the input box w/the first match (assumed to be the best match)
|
bgneal@45
|
269 // q: the term entered
|
bgneal@45
|
270 // sValue: the first matching result
|
bgneal@45
|
271 function autoFill(q, sValue){
|
bgneal@45
|
272 // autofill in the complete box w/the first match as long as the user hasn't entered in more data
|
bgneal@45
|
273 // if the last user key pressed was backspace, don't autofill
|
bgneal@45
|
274 if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
|
bgneal@45
|
275 // fill in the value (keep the case the user has typed)
|
bgneal@45
|
276 $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
|
bgneal@45
|
277 // select the portion of the value not typed by the user (so the next character will erase)
|
bgneal@45
|
278 $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
|
bgneal@45
|
279 }
|
bgneal@45
|
280 };
|
bgneal@45
|
281
|
bgneal@45
|
282 function hideResults() {
|
bgneal@45
|
283 clearTimeout(timeout);
|
bgneal@45
|
284 timeout = setTimeout(hideResultsNow, 200);
|
bgneal@45
|
285 };
|
bgneal@45
|
286
|
bgneal@45
|
287 function hideResultsNow() {
|
bgneal@45
|
288 var wasVisible = select.visible();
|
bgneal@45
|
289 select.hide();
|
bgneal@45
|
290 clearTimeout(timeout);
|
bgneal@45
|
291 stopLoading();
|
bgneal@45
|
292 if (options.mustMatch) {
|
bgneal@45
|
293 // call search and run callback
|
bgneal@45
|
294 $input.search(
|
bgneal@45
|
295 function (result){
|
bgneal@45
|
296 // if no value found, clear the input box
|
bgneal@45
|
297 if( !result ) {
|
bgneal@45
|
298 if (options.multiple) {
|
bgneal@45
|
299 var words = trimWords($input.val()).slice(0, -1);
|
bgneal@45
|
300 $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
|
bgneal@45
|
301 }
|
bgneal@45
|
302 else
|
bgneal@45
|
303 $input.val( "" );
|
bgneal@45
|
304 }
|
bgneal@45
|
305 }
|
bgneal@45
|
306 );
|
bgneal@45
|
307 }
|
bgneal@45
|
308 if (wasVisible)
|
bgneal@45
|
309 // position cursor at end of input field
|
bgneal@45
|
310 $.Autocompleter.Selection(input, input.value.length, input.value.length);
|
bgneal@45
|
311 };
|
bgneal@45
|
312
|
bgneal@45
|
313 function receiveData(q, data) {
|
bgneal@45
|
314 if ( data && data.length && hasFocus ) {
|
bgneal@45
|
315 stopLoading();
|
bgneal@45
|
316 select.display(data, q);
|
bgneal@45
|
317 autoFill(q, data[0].value);
|
bgneal@45
|
318 select.show();
|
bgneal@45
|
319 } else {
|
bgneal@45
|
320 hideResultsNow();
|
bgneal@45
|
321 }
|
bgneal@45
|
322 };
|
bgneal@45
|
323
|
bgneal@45
|
324 function request(term, success, failure) {
|
bgneal@45
|
325 if (!options.matchCase)
|
bgneal@45
|
326 term = term.toLowerCase();
|
bgneal@45
|
327 var data = cache.load(term);
|
bgneal@45
|
328 // recieve the cached data
|
bgneal@45
|
329 if (data && data.length) {
|
bgneal@45
|
330 success(term, data);
|
bgneal@45
|
331 // if an AJAX url has been supplied, try loading the data now
|
bgneal@45
|
332 } else if( (typeof options.url == "string") && (options.url.length > 0) ){
|
bgneal@45
|
333
|
bgneal@45
|
334 var extraParams = {
|
bgneal@45
|
335 timestamp: +new Date()
|
bgneal@45
|
336 };
|
bgneal@45
|
337 $.each(options.extraParams, function(key, param) {
|
bgneal@45
|
338 extraParams[key] = typeof param == "function" ? param() : param;
|
bgneal@45
|
339 });
|
bgneal@45
|
340
|
bgneal@45
|
341 $.ajax({
|
bgneal@45
|
342 // try to leverage ajaxQueue plugin to abort previous requests
|
bgneal@45
|
343 mode: "abort",
|
bgneal@45
|
344 // limit abortion to this input
|
bgneal@45
|
345 port: "autocomplete" + input.name,
|
bgneal@45
|
346 dataType: options.dataType,
|
bgneal@45
|
347 url: options.url,
|
bgneal@45
|
348 data: $.extend({
|
bgneal@45
|
349 q: lastWord(term),
|
bgneal@45
|
350 limit: options.max
|
bgneal@45
|
351 }, extraParams),
|
bgneal@45
|
352 success: function(data) {
|
bgneal@45
|
353 var parsed = options.parse && options.parse(data) || parse(data);
|
bgneal@45
|
354 cache.add(term, parsed);
|
bgneal@45
|
355 success(term, parsed);
|
bgneal@45
|
356 }
|
bgneal@45
|
357 });
|
bgneal@45
|
358 } else {
|
bgneal@45
|
359 // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
|
bgneal@45
|
360 select.emptyList();
|
bgneal@45
|
361 failure(term);
|
bgneal@45
|
362 }
|
bgneal@45
|
363 };
|
bgneal@45
|
364
|
bgneal@45
|
365 function parse(data) {
|
bgneal@45
|
366 var parsed = [];
|
bgneal@45
|
367 var rows = data.split("\n");
|
bgneal@45
|
368 for (var i=0; i < rows.length; i++) {
|
bgneal@45
|
369 var row = $.trim(rows[i]);
|
bgneal@45
|
370 if (row) {
|
bgneal@45
|
371 row = row.split("|");
|
bgneal@45
|
372 parsed[parsed.length] = {
|
bgneal@45
|
373 data: row,
|
bgneal@45
|
374 value: row[0],
|
bgneal@45
|
375 result: options.formatResult && options.formatResult(row, row[0]) || row[0]
|
bgneal@45
|
376 };
|
bgneal@45
|
377 }
|
bgneal@45
|
378 }
|
bgneal@45
|
379 return parsed;
|
bgneal@45
|
380 };
|
bgneal@45
|
381
|
bgneal@45
|
382 function stopLoading() {
|
bgneal@45
|
383 $input.removeClass(options.loadingClass);
|
bgneal@45
|
384 };
|
bgneal@45
|
385
|
bgneal@45
|
386 };
|
bgneal@45
|
387
|
bgneal@45
|
388 $.Autocompleter.defaults = {
|
bgneal@45
|
389 inputClass: "ac_input",
|
bgneal@45
|
390 resultsClass: "ac_results",
|
bgneal@45
|
391 loadingClass: "ac_loading",
|
bgneal@45
|
392 minChars: 1,
|
bgneal@45
|
393 delay: 400,
|
bgneal@45
|
394 matchCase: false,
|
bgneal@45
|
395 matchSubset: true,
|
bgneal@45
|
396 matchContains: false,
|
bgneal@45
|
397 cacheLength: 10,
|
bgneal@45
|
398 max: 100,
|
bgneal@45
|
399 mustMatch: false,
|
bgneal@45
|
400 extraParams: {},
|
bgneal@45
|
401 selectFirst: true,
|
bgneal@45
|
402 formatItem: function(row) { return row[0]; },
|
bgneal@45
|
403 formatMatch: null,
|
bgneal@45
|
404 autoFill: false,
|
bgneal@45
|
405 width: 0,
|
bgneal@45
|
406 multiple: false,
|
bgneal@45
|
407 multipleSeparator: ", ",
|
bgneal@45
|
408 highlight: function(value, term) {
|
bgneal@45
|
409 return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
|
bgneal@45
|
410 },
|
bgneal@45
|
411 scroll: true,
|
bgneal@45
|
412 scrollHeight: 180
|
bgneal@45
|
413 };
|
bgneal@45
|
414
|
bgneal@45
|
415 $.Autocompleter.Cache = function(options) {
|
bgneal@45
|
416
|
bgneal@45
|
417 var data = {};
|
bgneal@45
|
418 var length = 0;
|
bgneal@45
|
419
|
bgneal@45
|
420 function matchSubset(s, sub) {
|
bgneal@45
|
421 if (!options.matchCase)
|
bgneal@45
|
422 s = s.toLowerCase();
|
bgneal@45
|
423 var i = s.indexOf(sub);
|
bgneal@45
|
424 if (i == -1) return false;
|
bgneal@45
|
425 return i == 0 || options.matchContains;
|
bgneal@45
|
426 };
|
bgneal@45
|
427
|
bgneal@45
|
428 function add(q, value) {
|
bgneal@45
|
429 if (length > options.cacheLength){
|
bgneal@45
|
430 flush();
|
bgneal@45
|
431 }
|
bgneal@45
|
432 if (!data[q]){
|
bgneal@45
|
433 length++;
|
bgneal@45
|
434 }
|
bgneal@45
|
435 data[q] = value;
|
bgneal@45
|
436 }
|
bgneal@45
|
437
|
bgneal@45
|
438 function populate(){
|
bgneal@45
|
439 if( !options.data ) return false;
|
bgneal@45
|
440 // track the matches
|
bgneal@45
|
441 var stMatchSets = {},
|
bgneal@45
|
442 nullData = 0;
|
bgneal@45
|
443
|
bgneal@45
|
444 // no url was specified, we need to adjust the cache length to make sure it fits the local data store
|
bgneal@45
|
445 if( !options.url ) options.cacheLength = 1;
|
bgneal@45
|
446
|
bgneal@45
|
447 // track all options for minChars = 0
|
bgneal@45
|
448 stMatchSets[""] = [];
|
bgneal@45
|
449
|
bgneal@45
|
450 // loop through the array and create a lookup structure
|
bgneal@45
|
451 for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
|
bgneal@45
|
452 var rawValue = options.data[i];
|
bgneal@45
|
453 // if rawValue is a string, make an array otherwise just reference the array
|
bgneal@45
|
454 rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
|
bgneal@45
|
455
|
bgneal@45
|
456 var value = options.formatMatch(rawValue, i+1, options.data.length);
|
bgneal@45
|
457 if ( value === false )
|
bgneal@45
|
458 continue;
|
bgneal@45
|
459
|
bgneal@45
|
460 var firstChar = value.charAt(0).toLowerCase();
|
bgneal@45
|
461 // if no lookup array for this character exists, look it up now
|
bgneal@45
|
462 if( !stMatchSets[firstChar] )
|
bgneal@45
|
463 stMatchSets[firstChar] = [];
|
bgneal@45
|
464
|
bgneal@45
|
465 // if the match is a string
|
bgneal@45
|
466 var row = {
|
bgneal@45
|
467 value: value,
|
bgneal@45
|
468 data: rawValue,
|
bgneal@45
|
469 result: options.formatResult && options.formatResult(rawValue) || value
|
bgneal@45
|
470 };
|
bgneal@45
|
471
|
bgneal@45
|
472 // push the current match into the set list
|
bgneal@45
|
473 stMatchSets[firstChar].push(row);
|
bgneal@45
|
474
|
bgneal@45
|
475 // keep track of minChars zero items
|
bgneal@45
|
476 if ( nullData++ < options.max ) {
|
bgneal@45
|
477 stMatchSets[""].push(row);
|
bgneal@45
|
478 }
|
bgneal@45
|
479 };
|
bgneal@45
|
480
|
bgneal@45
|
481 // add the data items to the cache
|
bgneal@45
|
482 $.each(stMatchSets, function(i, value) {
|
bgneal@45
|
483 // increase the cache size
|
bgneal@45
|
484 options.cacheLength++;
|
bgneal@45
|
485 // add to the cache
|
bgneal@45
|
486 add(i, value);
|
bgneal@45
|
487 });
|
bgneal@45
|
488 }
|
bgneal@45
|
489
|
bgneal@45
|
490 // populate any existing data
|
bgneal@45
|
491 setTimeout(populate, 25);
|
bgneal@45
|
492
|
bgneal@45
|
493 function flush(){
|
bgneal@45
|
494 data = {};
|
bgneal@45
|
495 length = 0;
|
bgneal@45
|
496 }
|
bgneal@45
|
497
|
bgneal@45
|
498 return {
|
bgneal@45
|
499 flush: flush,
|
bgneal@45
|
500 add: add,
|
bgneal@45
|
501 populate: populate,
|
bgneal@45
|
502 load: function(q) {
|
bgneal@45
|
503 if (!options.cacheLength || !length)
|
bgneal@45
|
504 return null;
|
bgneal@45
|
505 /*
|
bgneal@45
|
506 * if dealing w/local data and matchContains than we must make sure
|
bgneal@45
|
507 * to loop through all the data collections looking for matches
|
bgneal@45
|
508 */
|
bgneal@45
|
509 if( !options.url && options.matchContains ){
|
bgneal@45
|
510 // track all matches
|
bgneal@45
|
511 var csub = [];
|
bgneal@45
|
512 // loop through all the data grids for matches
|
bgneal@45
|
513 for( var k in data ){
|
bgneal@45
|
514 // don't search through the stMatchSets[""] (minChars: 0) cache
|
bgneal@45
|
515 // this prevents duplicates
|
bgneal@45
|
516 if( k.length > 0 ){
|
bgneal@45
|
517 var c = data[k];
|
bgneal@45
|
518 $.each(c, function(i, x) {
|
bgneal@45
|
519 // if we've got a match, add it to the array
|
bgneal@45
|
520 if (matchSubset(x.value, q)) {
|
bgneal@45
|
521 csub.push(x);
|
bgneal@45
|
522 }
|
bgneal@45
|
523 });
|
bgneal@45
|
524 }
|
bgneal@45
|
525 }
|
bgneal@45
|
526 return csub;
|
bgneal@45
|
527 } else
|
bgneal@45
|
528 // if the exact item exists, use it
|
bgneal@45
|
529 if (data[q]){
|
bgneal@45
|
530 return data[q];
|
bgneal@45
|
531 } else
|
bgneal@45
|
532 if (options.matchSubset) {
|
bgneal@45
|
533 for (var i = q.length - 1; i >= options.minChars; i--) {
|
bgneal@45
|
534 var c = data[q.substr(0, i)];
|
bgneal@45
|
535 if (c) {
|
bgneal@45
|
536 var csub = [];
|
bgneal@45
|
537 $.each(c, function(i, x) {
|
bgneal@45
|
538 if (matchSubset(x.value, q)) {
|
bgneal@45
|
539 csub[csub.length] = x;
|
bgneal@45
|
540 }
|
bgneal@45
|
541 });
|
bgneal@45
|
542 return csub;
|
bgneal@45
|
543 }
|
bgneal@45
|
544 }
|
bgneal@45
|
545 }
|
bgneal@45
|
546 return null;
|
bgneal@45
|
547 }
|
bgneal@45
|
548 };
|
bgneal@45
|
549 };
|
bgneal@45
|
550
|
bgneal@45
|
551 $.Autocompleter.Select = function (options, input, select, config) {
|
bgneal@45
|
552 var CLASSES = {
|
bgneal@45
|
553 ACTIVE: "ac_over"
|
bgneal@45
|
554 };
|
bgneal@45
|
555
|
bgneal@45
|
556 var listItems,
|
bgneal@45
|
557 active = -1,
|
bgneal@45
|
558 data,
|
bgneal@45
|
559 term = "",
|
bgneal@45
|
560 needsInit = true,
|
bgneal@45
|
561 element,
|
bgneal@45
|
562 list;
|
bgneal@45
|
563
|
bgneal@45
|
564 // Create results
|
bgneal@45
|
565 function init() {
|
bgneal@45
|
566 if (!needsInit)
|
bgneal@45
|
567 return;
|
bgneal@45
|
568 element = $("<div/>")
|
bgneal@45
|
569 .hide()
|
bgneal@45
|
570 .addClass(options.resultsClass)
|
bgneal@45
|
571 .css("position", "absolute")
|
bgneal@45
|
572 .appendTo(document.body);
|
bgneal@45
|
573
|
bgneal@45
|
574 list = $("<ul/>").appendTo(element).mouseover( function(event) {
|
bgneal@45
|
575 if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
|
bgneal@45
|
576 active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
|
bgneal@45
|
577 $(target(event)).addClass(CLASSES.ACTIVE);
|
bgneal@45
|
578 }
|
bgneal@45
|
579 }).click(function(event) {
|
bgneal@45
|
580 $(target(event)).addClass(CLASSES.ACTIVE);
|
bgneal@45
|
581 select();
|
bgneal@45
|
582 // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
|
bgneal@45
|
583 input.focus();
|
bgneal@45
|
584 return false;
|
bgneal@45
|
585 }).mousedown(function() {
|
bgneal@45
|
586 config.mouseDownOnSelect = true;
|
bgneal@45
|
587 }).mouseup(function() {
|
bgneal@45
|
588 config.mouseDownOnSelect = false;
|
bgneal@45
|
589 });
|
bgneal@45
|
590
|
bgneal@45
|
591 if( options.width > 0 )
|
bgneal@45
|
592 element.css("width", options.width);
|
bgneal@45
|
593
|
bgneal@45
|
594 needsInit = false;
|
bgneal@45
|
595 }
|
bgneal@45
|
596
|
bgneal@45
|
597 function target(event) {
|
bgneal@45
|
598 var element = event.target;
|
bgneal@45
|
599 while(element && element.tagName != "LI")
|
bgneal@45
|
600 element = element.parentNode;
|
bgneal@45
|
601 // more fun with IE, sometimes event.target is empty, just ignore it then
|
bgneal@45
|
602 if(!element)
|
bgneal@45
|
603 return [];
|
bgneal@45
|
604 return element;
|
bgneal@45
|
605 }
|
bgneal@45
|
606
|
bgneal@45
|
607 function moveSelect(step) {
|
bgneal@45
|
608 listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
|
bgneal@45
|
609 movePosition(step);
|
bgneal@45
|
610 var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
|
bgneal@45
|
611 if(options.scroll) {
|
bgneal@45
|
612 var offset = 0;
|
bgneal@45
|
613 listItems.slice(0, active).each(function() {
|
bgneal@45
|
614 offset += this.offsetHeight;
|
bgneal@45
|
615 });
|
bgneal@45
|
616 if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
|
bgneal@45
|
617 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
|
bgneal@45
|
618 } else if(offset < list.scrollTop()) {
|
bgneal@45
|
619 list.scrollTop(offset);
|
bgneal@45
|
620 }
|
bgneal@45
|
621 }
|
bgneal@45
|
622 };
|
bgneal@45
|
623
|
bgneal@45
|
624 function movePosition(step) {
|
bgneal@45
|
625 active += step;
|
bgneal@45
|
626 if (active < 0) {
|
bgneal@45
|
627 active = listItems.size() - 1;
|
bgneal@45
|
628 } else if (active >= listItems.size()) {
|
bgneal@45
|
629 active = 0;
|
bgneal@45
|
630 }
|
bgneal@45
|
631 }
|
bgneal@45
|
632
|
bgneal@45
|
633 function limitNumberOfItems(available) {
|
bgneal@45
|
634 return options.max && options.max < available
|
bgneal@45
|
635 ? options.max
|
bgneal@45
|
636 : available;
|
bgneal@45
|
637 }
|
bgneal@45
|
638
|
bgneal@45
|
639 function fillList() {
|
bgneal@45
|
640 list.empty();
|
bgneal@45
|
641 var max = limitNumberOfItems(data.length);
|
bgneal@45
|
642 for (var i=0; i < max; i++) {
|
bgneal@45
|
643 if (!data[i])
|
bgneal@45
|
644 continue;
|
bgneal@45
|
645 var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
|
bgneal@45
|
646 if ( formatted === false )
|
bgneal@45
|
647 continue;
|
bgneal@45
|
648 var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
|
bgneal@45
|
649 $.data(li, "ac_data", data[i]);
|
bgneal@45
|
650 }
|
bgneal@45
|
651 listItems = list.find("li");
|
bgneal@45
|
652 if ( options.selectFirst ) {
|
bgneal@45
|
653 listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
|
bgneal@45
|
654 active = 0;
|
bgneal@45
|
655 }
|
bgneal@45
|
656 // apply bgiframe if available
|
bgneal@45
|
657 if ( $.fn.bgiframe )
|
bgneal@45
|
658 list.bgiframe();
|
bgneal@45
|
659 }
|
bgneal@45
|
660
|
bgneal@45
|
661 return {
|
bgneal@45
|
662 display: function(d, q) {
|
bgneal@45
|
663 init();
|
bgneal@45
|
664 data = d;
|
bgneal@45
|
665 term = q;
|
bgneal@45
|
666 fillList();
|
bgneal@45
|
667 },
|
bgneal@45
|
668 next: function() {
|
bgneal@45
|
669 moveSelect(1);
|
bgneal@45
|
670 },
|
bgneal@45
|
671 prev: function() {
|
bgneal@45
|
672 moveSelect(-1);
|
bgneal@45
|
673 },
|
bgneal@45
|
674 pageUp: function() {
|
bgneal@45
|
675 if (active != 0 && active - 8 < 0) {
|
bgneal@45
|
676 moveSelect( -active );
|
bgneal@45
|
677 } else {
|
bgneal@45
|
678 moveSelect(-8);
|
bgneal@45
|
679 }
|
bgneal@45
|
680 },
|
bgneal@45
|
681 pageDown: function() {
|
bgneal@45
|
682 if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
|
bgneal@45
|
683 moveSelect( listItems.size() - 1 - active );
|
bgneal@45
|
684 } else {
|
bgneal@45
|
685 moveSelect(8);
|
bgneal@45
|
686 }
|
bgneal@45
|
687 },
|
bgneal@45
|
688 hide: function() {
|
bgneal@45
|
689 element && element.hide();
|
bgneal@45
|
690 listItems && listItems.removeClass(CLASSES.ACTIVE);
|
bgneal@45
|
691 active = -1;
|
bgneal@45
|
692 },
|
bgneal@45
|
693 visible : function() {
|
bgneal@45
|
694 return element && element.is(":visible");
|
bgneal@45
|
695 },
|
bgneal@45
|
696 current: function() {
|
bgneal@45
|
697 return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
|
bgneal@45
|
698 },
|
bgneal@45
|
699 show: function() {
|
bgneal@45
|
700 var offset = $(input).offset();
|
bgneal@45
|
701 element.css({
|
bgneal@45
|
702 width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
|
bgneal@45
|
703 top: offset.top + input.offsetHeight,
|
bgneal@45
|
704 left: offset.left
|
bgneal@45
|
705 }).show();
|
bgneal@45
|
706 if(options.scroll) {
|
bgneal@45
|
707 list.scrollTop(0);
|
bgneal@45
|
708 list.css({
|
bgneal@45
|
709 maxHeight: options.scrollHeight,
|
bgneal@45
|
710 overflow: 'auto'
|
bgneal@45
|
711 });
|
bgneal@45
|
712
|
bgneal@45
|
713 if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
|
bgneal@45
|
714 var listHeight = 0;
|
bgneal@45
|
715 listItems.each(function() {
|
bgneal@45
|
716 listHeight += this.offsetHeight;
|
bgneal@45
|
717 });
|
bgneal@45
|
718 var scrollbarsVisible = listHeight > options.scrollHeight;
|
bgneal@45
|
719 list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
|
bgneal@45
|
720 if (!scrollbarsVisible) {
|
bgneal@45
|
721 // IE doesn't recalculate width when scrollbar disappears
|
bgneal@45
|
722 listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
|
bgneal@45
|
723 }
|
bgneal@45
|
724 }
|
bgneal@45
|
725
|
bgneal@45
|
726 }
|
bgneal@45
|
727 },
|
bgneal@45
|
728 selected: function() {
|
bgneal@45
|
729 var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
|
bgneal@45
|
730 return selected && selected.length && $.data(selected[0], "ac_data");
|
bgneal@45
|
731 },
|
bgneal@45
|
732 emptyList: function (){
|
bgneal@45
|
733 list && list.empty();
|
bgneal@45
|
734 },
|
bgneal@45
|
735 unbind: function() {
|
bgneal@45
|
736 element && element.remove();
|
bgneal@45
|
737 }
|
bgneal@45
|
738 };
|
bgneal@45
|
739 };
|
bgneal@45
|
740
|
bgneal@45
|
741 $.Autocompleter.Selection = function(field, start, end) {
|
bgneal@45
|
742 if( field.createTextRange ){
|
bgneal@45
|
743 var selRange = field.createTextRange();
|
bgneal@45
|
744 selRange.collapse(true);
|
bgneal@45
|
745 selRange.moveStart("character", start);
|
bgneal@45
|
746 selRange.moveEnd("character", end);
|
bgneal@45
|
747 selRange.select();
|
bgneal@45
|
748 } else if( field.setSelectionRange ){
|
bgneal@45
|
749 field.setSelectionRange(start, end);
|
bgneal@45
|
750 } else {
|
bgneal@45
|
751 if( field.selectionStart ){
|
bgneal@45
|
752 field.selectionStart = start;
|
bgneal@45
|
753 field.selectionEnd = end;
|
bgneal@45
|
754 }
|
bgneal@45
|
755 }
|
bgneal@45
|
756 field.focus();
|
bgneal@45
|
757 };
|
bgneal@45
|
758
|
bgneal@45
|
759 })(jQuery); |