simple_autocomplete 0.3.0

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