jquery-autocomplete-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jquery-autocomplete-rails.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2015 by Matt Cowley
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Chosen for rails asset pipeline
2
+
3
+ [JQueryAutocomplete](https://github.com/devbridge/jQuery-Autocomplete) is a jquery plugin for autocompletion.
4
+
5
+ The `jquery-autocomplete-rails` gem integrates the `Chosen` with the Rails asset pipeline.
6
+
7
+ ## Usage
8
+
9
+ ### Install jquery-autocomplete-rails gem
10
+
11
+
12
+ ```rb
13
+ gem 'jquery-autocomplete-rails'
14
+ ```
15
+
16
+ Then run `bundle install`
17
+
18
+ ### Include chosen javascript assets
19
+
20
+ Add to your `app/assets/javascripts/application.js`
21
+
22
+ ```coffee
23
+ //= require jquery.autocomplete
24
+ ```
25
+
26
+
27
+
28
+ The infrastructure for this gem is heavily borrowed from <https://github.com/tsechingho/chosen-rails>.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require File.expand_path('../lib/jquery-autocomplete-rails/source_file', __FILE__)
4
+
5
+ desc "Update with devgridge's jquery-autocomplete libary"
6
+ task 'update-jquery-autocomplete', 'repository_url', 'branch' do |task, args|
7
+ remote = args['repository_url'] || 'https://github.com/devbridge/jQuery-Autocomplete'
8
+ branch = args['branch'] || 'master'
9
+ files = SourceFile.new
10
+ files.fetch remote, branch
11
+ files.cleanup
12
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/jquery-autocomplete-rails/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ['Matt Cowley']
6
+ gem.email = ['madcowley@gmail.com']
7
+ gem.description = %q{juery-autocomplete is a javascript library for autocomplete. This gem integrates the library with the rails asset pipeline.}
8
+ gem.summary = %q{Integrate jquery-autocomplete javascript library with Rails asset pipeline}
9
+ gem.homepage = 'https://github.com/madcowley/jquery-autocomplete-rails'
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = 'jquery-autocomplete-rails'
15
+ gem.require_paths = ['lib']
16
+ gem.version = JqueryAutocomplete::Rails::VERSION
17
+
18
+ gem.add_dependency 'railties', '>= 3.0'
19
+ gem.add_dependency 'coffee-rails', '>= 3.2'
20
+ gem.add_dependency 'sass-rails', '>= 3.2'
21
+ gem.add_dependency 'compass-rails', '>= 1.1.2'
22
+
23
+ gem.add_development_dependency 'bundler', '>= 1.0'
24
+ gem.add_development_dependency 'rails', '>= 3.0'
25
+ gem.add_development_dependency 'thor', '>= 0.14'
26
+ end
@@ -0,0 +1,17 @@
1
+ require 'jquery-autocomplete-rails/version'
2
+
3
+ module Chosen
4
+ module Rails
5
+ end
6
+ end
7
+
8
+ case ::Rails.version.to_s
9
+ when /^4/
10
+ require 'jquery-autocomplete-rails/engine'
11
+ when /^3\.[12]/
12
+ require 'jquery-autocomplete-rails/engine3'
13
+ when /^3\.[0]/
14
+ require 'jquery-autocomplete-rails/railtie'
15
+ end
16
+
17
+ require 'compass-rails'
@@ -0,0 +1,9 @@
1
+ module JqueryAutocomplete
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ rake_tasks do
5
+ load 'jquery-autocomplete-rails/tasks.rake'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module JqueryAutocomplete
2
+ module Rails
3
+ class Engine3 < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module JqueryAutocomplete
2
+ module Rails
3
+ class Railtie < ::Rails::Railtie
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,34 @@
1
+ require 'thor'
2
+ require 'json'
3
+
4
+ class SourceFile < Thor
5
+ include Thor::Actions
6
+
7
+ desc 'fetch source files', 'fetch source files from GitHub'
8
+ def fetch remote, branch
9
+ self.destination_root = 'vendor/assets'
10
+ get "#{remote}/raw/#{branch}/dist/jquery.autocomplete.js", 'javascripts/jquery.autocomplete.js'
11
+ get "#{remote}/raw/#{branch}/dist/jquery.autocomplete.min.js", 'javascripts/jquery.autocomplete.min.js'
12
+ get "#{remote}/raw/#{branch}/package.json", 'package.json'
13
+ bump_version
14
+ end
15
+
16
+
17
+ desc 'clean up useless files', 'clean up useless files'
18
+ def cleanup
19
+ self.destination_root = 'vendor/assets'
20
+ remove_file 'package.json'
21
+ end
22
+
23
+ protected
24
+
25
+ def bump_version
26
+ inside destination_root do
27
+ package_json = JSON.load(File.open('package.json'))
28
+ version = package_json['version']
29
+ gsub_file '../../lib/jquery-autocomplete-rails/version.rb', /JQUERY_AUTOCOMPLETE_VERSION\s=\s'(\d|\.)+'$/ do |match|
30
+ %Q{JQUERY_AUTOCOMPLETE_VERSION = '#{version}'}
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ require 'fileutils'
2
+
3
+ desc 'Create nondigest versions of all digest assets'
4
+ task 'assets:precompile' do
5
+ fingerprint = /\-[0-9a-f]{32}\./
6
+ Dir['public/assets/jquery-autocomplete-*'].each do |file|
7
+ next unless file =~ fingerprint
8
+ nondigest = file.sub fingerprint, '.'
9
+ FileUtils.cp file, nondigest, verbose: true
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module JqueryAutocomplete
2
+ module Rails
3
+ VERSION = '0.1.0'
4
+ JQUERY_AUTOCOMPLETE_VERSION = '1.2.18'
5
+ end
6
+ end
@@ -0,0 +1,982 @@
1
+ /**
2
+ * Ajax Autocomplete for jQuery, version 1.2.18
3
+ * (c) 2015 Tomas Kirda
4
+ *
5
+ * Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
6
+ * For details, see the web site: https://github.com/devbridge/jQuery-Autocomplete
7
+ */
8
+
9
+ /*jslint browser: true, white: true, plusplus: true, vars: true */
10
+ /*global define, window, document, jQuery, exports, require */
11
+
12
+ // Expose plugin as an AMD module if AMD loader is present:
13
+ (function (factory) {
14
+ 'use strict';
15
+ if (typeof define === 'function' && define.amd) {
16
+ // AMD. Register as an anonymous module.
17
+ define(['jquery'], factory);
18
+ } else if (typeof exports === 'object' && typeof require === 'function') {
19
+ // Browserify
20
+ factory(require('jquery'));
21
+ } else {
22
+ // Browser globals
23
+ factory(jQuery);
24
+ }
25
+ }(function ($) {
26
+ 'use strict';
27
+
28
+ var
29
+ utils = (function () {
30
+ return {
31
+ escapeRegExChars: function (value) {
32
+ return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
33
+ },
34
+ createNode: function (containerClass) {
35
+ var div = document.createElement('div');
36
+ div.className = containerClass;
37
+ div.style.position = 'absolute';
38
+ div.style.display = 'none';
39
+ return div;
40
+ }
41
+ };
42
+ }()),
43
+
44
+ keys = {
45
+ ESC: 27,
46
+ TAB: 9,
47
+ RETURN: 13,
48
+ LEFT: 37,
49
+ UP: 38,
50
+ RIGHT: 39,
51
+ DOWN: 40
52
+ };
53
+
54
+ function Autocomplete(el, options) {
55
+ var noop = function () { },
56
+ that = this,
57
+ defaults = {
58
+ ajaxSettings: {},
59
+ autoSelectFirst: false,
60
+ appendTo: document.body,
61
+ serviceUrl: null,
62
+ lookup: null,
63
+ onSelect: null,
64
+ width: 'auto',
65
+ minChars: 1,
66
+ maxHeight: 300,
67
+ deferRequestBy: 0,
68
+ params: {},
69
+ formatResult: Autocomplete.formatResult,
70
+ delimiter: null,
71
+ zIndex: 9999,
72
+ type: 'GET',
73
+ noCache: false,
74
+ onSearchStart: noop,
75
+ onSearchComplete: noop,
76
+ onSearchError: noop,
77
+ preserveInput: false,
78
+ containerClass: 'autocomplete-suggestions',
79
+ tabDisabled: false,
80
+ dataType: 'text',
81
+ currentRequest: null,
82
+ triggerSelectOnValidInput: true,
83
+ preventBadQueries: true,
84
+ lookupFilter: function (suggestion, originalQuery, queryLowerCase) {
85
+ return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1;
86
+ },
87
+ paramName: 'query',
88
+ transformResult: function (response) {
89
+ return typeof response === 'string' ? $.parseJSON(response) : response;
90
+ },
91
+ showNoSuggestionNotice: false,
92
+ noSuggestionNotice: 'No results',
93
+ orientation: 'bottom',
94
+ forceFixPosition: false
95
+ };
96
+
97
+ // Shared variables:
98
+ that.element = el;
99
+ that.el = $(el);
100
+ that.suggestions = [];
101
+ that.badQueries = [];
102
+ that.selectedIndex = -1;
103
+ that.currentValue = that.element.value;
104
+ that.intervalId = 0;
105
+ that.cachedResponse = {};
106
+ that.onChangeInterval = null;
107
+ that.onChange = null;
108
+ that.isLocal = false;
109
+ that.suggestionsContainer = null;
110
+ that.noSuggestionsContainer = null;
111
+ that.options = $.extend({}, defaults, options);
112
+ that.classes = {
113
+ selected: 'autocomplete-selected',
114
+ suggestion: 'autocomplete-suggestion'
115
+ };
116
+ that.hint = null;
117
+ that.hintValue = '';
118
+ that.selection = null;
119
+
120
+ // Initialize and set options:
121
+ that.initialize();
122
+ that.setOptions(options);
123
+ }
124
+
125
+ Autocomplete.utils = utils;
126
+
127
+ $.Autocomplete = Autocomplete;
128
+
129
+ Autocomplete.formatResult = function (suggestion, currentValue) {
130
+ var pattern = '(' + utils.escapeRegExChars(currentValue) + ')';
131
+
132
+ return suggestion.value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
133
+ };
134
+
135
+ Autocomplete.prototype = {
136
+
137
+ killerFn: null,
138
+
139
+ initialize: function () {
140
+ var that = this,
141
+ suggestionSelector = '.' + that.classes.suggestion,
142
+ selected = that.classes.selected,
143
+ options = that.options,
144
+ container;
145
+
146
+ // Remove autocomplete attribute to prevent native suggestions:
147
+ that.element.setAttribute('autocomplete', 'off');
148
+
149
+ that.killerFn = function (e) {
150
+ if ($(e.target).closest('.' + that.options.containerClass).length === 0) {
151
+ that.killSuggestions();
152
+ that.disableKillerFn();
153
+ }
154
+ };
155
+
156
+ // html() deals with many types: htmlString or Element or Array or jQuery
157
+ that.noSuggestionsContainer = $('<div class="autocomplete-no-suggestion"></div>')
158
+ .html(this.options.noSuggestionNotice).get(0);
159
+
160
+ that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass);
161
+
162
+ container = $(that.suggestionsContainer);
163
+
164
+ container.appendTo(options.appendTo);
165
+
166
+ // Only set width if it was provided:
167
+ if (options.width !== 'auto') {
168
+ container.width(options.width);
169
+ }
170
+
171
+ // Listen for mouse over event on suggestions list:
172
+ container.on('mouseover.autocomplete', suggestionSelector, function () {
173
+ that.activate($(this).data('index'));
174
+ });
175
+
176
+ // Deselect active element when mouse leaves suggestions container:
177
+ container.on('mouseout.autocomplete', function () {
178
+ that.selectedIndex = -1;
179
+ container.children('.' + selected).removeClass(selected);
180
+ });
181
+
182
+ // Listen for click event on suggestions list:
183
+ container.on('click.autocomplete', suggestionSelector, function () {
184
+ that.select($(this).data('index'));
185
+ });
186
+
187
+ that.fixPositionCapture = function () {
188
+ if (that.visible) {
189
+ that.fixPosition();
190
+ }
191
+ };
192
+
193
+ $(window).on('resize.autocomplete', that.fixPositionCapture);
194
+
195
+ that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); });
196
+ that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); });
197
+ that.el.on('blur.autocomplete', function () { that.onBlur(); });
198
+ that.el.on('focus.autocomplete', function () { that.onFocus(); });
199
+ that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); });
200
+ that.el.on('input.autocomplete', function (e) { that.onKeyUp(e); });
201
+ },
202
+
203
+ onFocus: function () {
204
+ var that = this;
205
+ that.fixPosition();
206
+ if (that.options.minChars <= that.el.val().length) {
207
+ that.onValueChange();
208
+ }
209
+ },
210
+
211
+ onBlur: function () {
212
+ this.enableKillerFn();
213
+ },
214
+
215
+ setOptions: function (suppliedOptions) {
216
+ var that = this,
217
+ options = that.options;
218
+
219
+ $.extend(options, suppliedOptions);
220
+
221
+ that.isLocal = $.isArray(options.lookup);
222
+
223
+ if (that.isLocal) {
224
+ options.lookup = that.verifySuggestionsFormat(options.lookup);
225
+ }
226
+
227
+ options.orientation = that.validateOrientation(options.orientation, 'bottom');
228
+
229
+ // Adjust height, width and z-index:
230
+ $(that.suggestionsContainer).css({
231
+ 'max-height': options.maxHeight + 'px',
232
+ 'width': options.width + 'px',
233
+ 'z-index': options.zIndex
234
+ });
235
+ },
236
+
237
+
238
+ clearCache: function () {
239
+ this.cachedResponse = {};
240
+ this.badQueries = [];
241
+ },
242
+
243
+ clear: function () {
244
+ this.clearCache();
245
+ this.currentValue = '';
246
+ this.suggestions = [];
247
+ },
248
+
249
+ disable: function () {
250
+ var that = this;
251
+ that.disabled = true;
252
+ clearInterval(that.onChangeInterval);
253
+ if (that.currentRequest) {
254
+ that.currentRequest.abort();
255
+ }
256
+ },
257
+
258
+ enable: function () {
259
+ this.disabled = false;
260
+ },
261
+
262
+ fixPosition: function () {
263
+ // Use only when container has already its content
264
+
265
+ var that = this,
266
+ $container = $(that.suggestionsContainer),
267
+ containerParent = $container.parent().get(0);
268
+ // Fix position automatically when appended to body.
269
+ // In other cases force parameter must be given.
270
+ if (containerParent !== document.body && !that.options.forceFixPosition) {
271
+ return;
272
+ }
273
+
274
+ // Choose orientation
275
+ var orientation = that.options.orientation,
276
+ containerHeight = $container.outerHeight(),
277
+ height = that.el.outerHeight(),
278
+ offset = that.el.offset(),
279
+ styles = { 'top': offset.top, 'left': offset.left };
280
+
281
+ if (orientation === 'auto') {
282
+ var viewPortHeight = $(window).height(),
283
+ scrollTop = $(window).scrollTop(),
284
+ topOverflow = -scrollTop + offset.top - containerHeight,
285
+ bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight);
286
+
287
+ orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) ? 'top' : 'bottom';
288
+ }
289
+
290
+ if (orientation === 'top') {
291
+ styles.top += -containerHeight;
292
+ } else {
293
+ styles.top += height;
294
+ }
295
+
296
+ // If container is not positioned to body,
297
+ // correct its position using offset parent offset
298
+ if(containerParent !== document.body) {
299
+ var opacity = $container.css('opacity'),
300
+ parentOffsetDiff;
301
+
302
+ if (!that.visible){
303
+ $container.css('opacity', 0).show();
304
+ }
305
+
306
+ parentOffsetDiff = $container.offsetParent().offset();
307
+ styles.top -= parentOffsetDiff.top;
308
+ styles.left -= parentOffsetDiff.left;
309
+
310
+ if (!that.visible){
311
+ $container.css('opacity', opacity).hide();
312
+ }
313
+ }
314
+
315
+ // -2px to account for suggestions border.
316
+ if (that.options.width === 'auto') {
317
+ styles.width = (that.el.outerWidth() - 2) + 'px';
318
+ }
319
+
320
+ $container.css(styles);
321
+ },
322
+
323
+ enableKillerFn: function () {
324
+ var that = this;
325
+ $(document).on('click.autocomplete', that.killerFn);
326
+ },
327
+
328
+ disableKillerFn: function () {
329
+ var that = this;
330
+ $(document).off('click.autocomplete', that.killerFn);
331
+ },
332
+
333
+ killSuggestions: function () {
334
+ var that = this;
335
+ that.stopKillSuggestions();
336
+ that.intervalId = window.setInterval(function () {
337
+ that.hide();
338
+ that.stopKillSuggestions();
339
+ }, 50);
340
+ },
341
+
342
+ stopKillSuggestions: function () {
343
+ window.clearInterval(this.intervalId);
344
+ },
345
+
346
+ isCursorAtEnd: function () {
347
+ var that = this,
348
+ valLength = that.el.val().length,
349
+ selectionStart = that.element.selectionStart,
350
+ range;
351
+
352
+ if (typeof selectionStart === 'number') {
353
+ return selectionStart === valLength;
354
+ }
355
+ if (document.selection) {
356
+ range = document.selection.createRange();
357
+ range.moveStart('character', -valLength);
358
+ return valLength === range.text.length;
359
+ }
360
+ return true;
361
+ },
362
+
363
+ onKeyPress: function (e) {
364
+ var that = this;
365
+
366
+ // If suggestions are hidden and user presses arrow down, display suggestions:
367
+ if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) {
368
+ that.suggest();
369
+ return;
370
+ }
371
+
372
+ if (that.disabled || !that.visible) {
373
+ return;
374
+ }
375
+
376
+ switch (e.which) {
377
+ case keys.ESC:
378
+ that.el.val(that.currentValue);
379
+ that.hide();
380
+ break;
381
+ case keys.RIGHT:
382
+ if (that.hint && that.options.onHint && that.isCursorAtEnd()) {
383
+ that.selectHint();
384
+ break;
385
+ }
386
+ return;
387
+ case keys.TAB:
388
+ if (that.hint && that.options.onHint) {
389
+ that.selectHint();
390
+ return;
391
+ }
392
+ if (that.selectedIndex === -1) {
393
+ that.hide();
394
+ return;
395
+ }
396
+ that.select(that.selectedIndex);
397
+ if (that.options.tabDisabled === false) {
398
+ return;
399
+ }
400
+ break;
401
+ case keys.RETURN:
402
+ if (that.selectedIndex === -1) {
403
+ that.hide();
404
+ return;
405
+ }
406
+ that.select(that.selectedIndex);
407
+ break;
408
+ case keys.UP:
409
+ that.moveUp();
410
+ break;
411
+ case keys.DOWN:
412
+ that.moveDown();
413
+ break;
414
+ default:
415
+ return;
416
+ }
417
+
418
+ // Cancel event if function did not return:
419
+ e.stopImmediatePropagation();
420
+ e.preventDefault();
421
+ },
422
+
423
+ onKeyUp: function (e) {
424
+ var that = this;
425
+
426
+ if (that.disabled) {
427
+ return;
428
+ }
429
+
430
+ switch (e.which) {
431
+ case keys.UP:
432
+ case keys.DOWN:
433
+ return;
434
+ }
435
+
436
+ clearInterval(that.onChangeInterval);
437
+
438
+ if (that.currentValue !== that.el.val()) {
439
+ that.findBestHint();
440
+ if (that.options.deferRequestBy > 0) {
441
+ // Defer lookup in case when value changes very quickly:
442
+ that.onChangeInterval = setInterval(function () {
443
+ that.onValueChange();
444
+ }, that.options.deferRequestBy);
445
+ } else {
446
+ that.onValueChange();
447
+ }
448
+ }
449
+ },
450
+
451
+ onValueChange: function () {
452
+ var that = this,
453
+ options = that.options,
454
+ value = that.el.val(),
455
+ query = that.getQuery(value),
456
+ index;
457
+
458
+ if (that.selection && that.currentValue !== query) {
459
+ that.selection = null;
460
+ (options.onInvalidateSelection || $.noop).call(that.element);
461
+ }
462
+
463
+ clearInterval(that.onChangeInterval);
464
+ that.currentValue = value;
465
+ that.selectedIndex = -1;
466
+
467
+ // Check existing suggestion for the match before proceeding:
468
+ if (options.triggerSelectOnValidInput) {
469
+ index = that.findSuggestionIndex(query);
470
+ if (index !== -1) {
471
+ that.select(index);
472
+ return;
473
+ }
474
+ }
475
+
476
+ if (query.length < options.minChars) {
477
+ that.hide();
478
+ } else {
479
+ that.getSuggestions(query);
480
+ }
481
+ },
482
+
483
+ findSuggestionIndex: function (query) {
484
+ var that = this,
485
+ index = -1,
486
+ queryLowerCase = query.toLowerCase();
487
+
488
+ $.each(that.suggestions, function (i, suggestion) {
489
+ if (suggestion.value.toLowerCase() === queryLowerCase) {
490
+ index = i;
491
+ return false;
492
+ }
493
+ });
494
+
495
+ return index;
496
+ },
497
+
498
+ getQuery: function (value) {
499
+ var delimiter = this.options.delimiter,
500
+ parts;
501
+
502
+ if (!delimiter) {
503
+ return value;
504
+ }
505
+ parts = value.split(delimiter);
506
+ return $.trim(parts[parts.length - 1]);
507
+ },
508
+
509
+ getSuggestionsLocal: function (query) {
510
+ var that = this,
511
+ options = that.options,
512
+ queryLowerCase = query.toLowerCase(),
513
+ filter = options.lookupFilter,
514
+ limit = parseInt(options.lookupLimit, 10),
515
+ data;
516
+
517
+ data = {
518
+ suggestions: $.grep(options.lookup, function (suggestion) {
519
+ return filter(suggestion, query, queryLowerCase);
520
+ })
521
+ };
522
+
523
+ if (limit && data.suggestions.length > limit) {
524
+ data.suggestions = data.suggestions.slice(0, limit);
525
+ }
526
+
527
+ return data;
528
+ },
529
+
530
+ getSuggestions: function (q) {
531
+ var response,
532
+ that = this,
533
+ options = that.options,
534
+ serviceUrl = options.serviceUrl,
535
+ params,
536
+ cacheKey,
537
+ ajaxSettings;
538
+
539
+ options.params[options.paramName] = q;
540
+ params = options.ignoreParams ? null : options.params;
541
+
542
+ if (options.onSearchStart.call(that.element, options.params) === false) {
543
+ return;
544
+ }
545
+
546
+ if ($.isFunction(options.lookup)){
547
+ options.lookup(q, function (data) {
548
+ that.suggestions = data.suggestions;
549
+ that.suggest();
550
+ options.onSearchComplete.call(that.element, q, data.suggestions);
551
+ });
552
+ return;
553
+ }
554
+
555
+ if (that.isLocal) {
556
+ response = that.getSuggestionsLocal(q);
557
+ } else {
558
+ if ($.isFunction(serviceUrl)) {
559
+ serviceUrl = serviceUrl.call(that.element, q);
560
+ }
561
+ cacheKey = serviceUrl + '?' + $.param(params || {});
562
+ response = that.cachedResponse[cacheKey];
563
+ }
564
+
565
+ if (response && $.isArray(response.suggestions)) {
566
+ that.suggestions = response.suggestions;
567
+ that.suggest();
568
+ options.onSearchComplete.call(that.element, q, response.suggestions);
569
+ } else if (!that.isBadQuery(q)) {
570
+ if (that.currentRequest) {
571
+ that.currentRequest.abort();
572
+ }
573
+
574
+ ajaxSettings = {
575
+ url: serviceUrl,
576
+ data: params,
577
+ type: options.type,
578
+ dataType: options.dataType
579
+ };
580
+
581
+ $.extend(ajaxSettings, options.ajaxSettings);
582
+
583
+ that.currentRequest = $.ajax(ajaxSettings).done(function (data) {
584
+ var result;
585
+ that.currentRequest = null;
586
+ result = options.transformResult(data);
587
+ that.processResponse(result, q, cacheKey);
588
+ options.onSearchComplete.call(that.element, q, result.suggestions);
589
+ }).fail(function (jqXHR, textStatus, errorThrown) {
590
+ options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown);
591
+ });
592
+ } else {
593
+ options.onSearchComplete.call(that.element, q, []);
594
+ }
595
+ },
596
+
597
+ isBadQuery: function (q) {
598
+ if (!this.options.preventBadQueries){
599
+ return false;
600
+ }
601
+
602
+ var badQueries = this.badQueries,
603
+ i = badQueries.length;
604
+
605
+ while (i--) {
606
+ if (q.indexOf(badQueries[i]) === 0) {
607
+ return true;
608
+ }
609
+ }
610
+
611
+ return false;
612
+ },
613
+
614
+ hide: function () {
615
+ var that = this,
616
+ container = $(that.suggestionsContainer);
617
+
618
+ if ($.isFunction(that.options.onHide) && that.visible) {
619
+ that.options.onHide.call(that.element, container);
620
+ }
621
+
622
+ that.visible = false;
623
+ that.selectedIndex = -1;
624
+ clearInterval(that.onChangeInterval);
625
+ $(that.suggestionsContainer).hide();
626
+ that.signalHint(null);
627
+ },
628
+
629
+ suggest: function () {
630
+ if (this.suggestions.length === 0) {
631
+ if (this.options.showNoSuggestionNotice) {
632
+ this.noSuggestions();
633
+ } else {
634
+ this.hide();
635
+ }
636
+ return;
637
+ }
638
+
639
+ var that = this,
640
+ options = that.options,
641
+ groupBy = options.groupBy,
642
+ formatResult = options.formatResult,
643
+ value = that.getQuery(that.currentValue),
644
+ className = that.classes.suggestion,
645
+ classSelected = that.classes.selected,
646
+ container = $(that.suggestionsContainer),
647
+ noSuggestionsContainer = $(that.noSuggestionsContainer),
648
+ beforeRender = options.beforeRender,
649
+ html = '',
650
+ category,
651
+ formatGroup = function (suggestion, index) {
652
+ var currentCategory = suggestion.data[groupBy];
653
+
654
+ if (category === currentCategory){
655
+ return '';
656
+ }
657
+
658
+ category = currentCategory;
659
+
660
+ return '<div class="autocomplete-group"><strong>' + category + '</strong></div>';
661
+ },
662
+ index;
663
+
664
+ if (options.triggerSelectOnValidInput) {
665
+ index = that.findSuggestionIndex(value);
666
+ if (index !== -1) {
667
+ that.select(index);
668
+ return;
669
+ }
670
+ }
671
+
672
+ // Build suggestions inner HTML:
673
+ $.each(that.suggestions, function (i, suggestion) {
674
+ if (groupBy){
675
+ html += formatGroup(suggestion, value, i);
676
+ }
677
+
678
+ html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value) + '</div>';
679
+ });
680
+
681
+ this.adjustContainerWidth();
682
+
683
+ noSuggestionsContainer.detach();
684
+ container.html(html);
685
+
686
+ if ($.isFunction(beforeRender)) {
687
+ beforeRender.call(that.element, container);
688
+ }
689
+
690
+ that.fixPosition();
691
+ container.show();
692
+
693
+ // Select first value by default:
694
+ if (options.autoSelectFirst) {
695
+ that.selectedIndex = 0;
696
+ container.scrollTop(0);
697
+ container.children('.' + className).first().addClass(classSelected);
698
+ }
699
+
700
+ that.visible = true;
701
+ that.findBestHint();
702
+ },
703
+
704
+ noSuggestions: function() {
705
+ var that = this,
706
+ container = $(that.suggestionsContainer),
707
+ noSuggestionsContainer = $(that.noSuggestionsContainer);
708
+
709
+ this.adjustContainerWidth();
710
+
711
+ // Some explicit steps. Be careful here as it easy to get
712
+ // noSuggestionsContainer removed from DOM if not detached properly.
713
+ noSuggestionsContainer.detach();
714
+ container.empty(); // clean suggestions if any
715
+ container.append(noSuggestionsContainer);
716
+
717
+ that.fixPosition();
718
+
719
+ container.show();
720
+ that.visible = true;
721
+ },
722
+
723
+ adjustContainerWidth: function() {
724
+ var that = this,
725
+ options = that.options,
726
+ width,
727
+ container = $(that.suggestionsContainer);
728
+
729
+ // If width is auto, adjust width before displaying suggestions,
730
+ // because if instance was created before input had width, it will be zero.
731
+ // Also it adjusts if input width has changed.
732
+ // -2px to account for suggestions border.
733
+ if (options.width === 'auto') {
734
+ width = that.el.outerWidth() - 2;
735
+ container.width(width > 0 ? width : 300);
736
+ }
737
+ },
738
+
739
+ findBestHint: function () {
740
+ var that = this,
741
+ value = that.el.val().toLowerCase(),
742
+ bestMatch = null;
743
+
744
+ if (!value) {
745
+ return;
746
+ }
747
+
748
+ $.each(that.suggestions, function (i, suggestion) {
749
+ var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0;
750
+ if (foundMatch) {
751
+ bestMatch = suggestion;
752
+ }
753
+ return !foundMatch;
754
+ });
755
+
756
+ that.signalHint(bestMatch);
757
+ },
758
+
759
+ signalHint: function (suggestion) {
760
+ var hintValue = '',
761
+ that = this;
762
+ if (suggestion) {
763
+ hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length);
764
+ }
765
+ if (that.hintValue !== hintValue) {
766
+ that.hintValue = hintValue;
767
+ that.hint = suggestion;
768
+ (this.options.onHint || $.noop)(hintValue);
769
+ }
770
+ },
771
+
772
+ verifySuggestionsFormat: function (suggestions) {
773
+ // If suggestions is string array, convert them to supported format:
774
+ if (suggestions.length && typeof suggestions[0] === 'string') {
775
+ return $.map(suggestions, function (value) {
776
+ return { value: value, data: null };
777
+ });
778
+ }
779
+
780
+ return suggestions;
781
+ },
782
+
783
+ validateOrientation: function(orientation, fallback) {
784
+ orientation = $.trim(orientation || '').toLowerCase();
785
+
786
+ if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){
787
+ orientation = fallback;
788
+ }
789
+
790
+ return orientation;
791
+ },
792
+
793
+ processResponse: function (result, originalQuery, cacheKey) {
794
+ var that = this,
795
+ options = that.options;
796
+
797
+ result.suggestions = that.verifySuggestionsFormat(result.suggestions);
798
+
799
+ // Cache results if cache is not disabled:
800
+ if (!options.noCache) {
801
+ that.cachedResponse[cacheKey] = result;
802
+ if (options.preventBadQueries && result.suggestions.length === 0) {
803
+ that.badQueries.push(originalQuery);
804
+ }
805
+ }
806
+
807
+ // Return if originalQuery is not matching current query:
808
+ if (originalQuery !== that.getQuery(that.currentValue)) {
809
+ return;
810
+ }
811
+
812
+ that.suggestions = result.suggestions;
813
+ that.suggest();
814
+ },
815
+
816
+ activate: function (index) {
817
+ var that = this,
818
+ activeItem,
819
+ selected = that.classes.selected,
820
+ container = $(that.suggestionsContainer),
821
+ children = container.find('.' + that.classes.suggestion);
822
+
823
+ container.find('.' + selected).removeClass(selected);
824
+
825
+ that.selectedIndex = index;
826
+
827
+ if (that.selectedIndex !== -1 && children.length > that.selectedIndex) {
828
+ activeItem = children.get(that.selectedIndex);
829
+ $(activeItem).addClass(selected);
830
+ return activeItem;
831
+ }
832
+
833
+ return null;
834
+ },
835
+
836
+ selectHint: function () {
837
+ var that = this,
838
+ i = $.inArray(that.hint, that.suggestions);
839
+
840
+ that.select(i);
841
+ },
842
+
843
+ select: function (i) {
844
+ var that = this;
845
+ that.hide();
846
+ that.onSelect(i);
847
+ },
848
+
849
+ moveUp: function () {
850
+ var that = this;
851
+
852
+ if (that.selectedIndex === -1) {
853
+ return;
854
+ }
855
+
856
+ if (that.selectedIndex === 0) {
857
+ $(that.suggestionsContainer).children().first().removeClass(that.classes.selected);
858
+ that.selectedIndex = -1;
859
+ that.el.val(that.currentValue);
860
+ that.findBestHint();
861
+ return;
862
+ }
863
+
864
+ that.adjustScroll(that.selectedIndex - 1);
865
+ },
866
+
867
+ moveDown: function () {
868
+ var that = this;
869
+
870
+ if (that.selectedIndex === (that.suggestions.length - 1)) {
871
+ return;
872
+ }
873
+
874
+ that.adjustScroll(that.selectedIndex + 1);
875
+ },
876
+
877
+ adjustScroll: function (index) {
878
+ var that = this,
879
+ activeItem = that.activate(index);
880
+
881
+ if (!activeItem) {
882
+ return;
883
+ }
884
+
885
+ var offsetTop,
886
+ upperBound,
887
+ lowerBound,
888
+ heightDelta = $(activeItem).outerHeight();
889
+
890
+ offsetTop = activeItem.offsetTop;
891
+ upperBound = $(that.suggestionsContainer).scrollTop();
892
+ lowerBound = upperBound + that.options.maxHeight - heightDelta;
893
+
894
+ if (offsetTop < upperBound) {
895
+ $(that.suggestionsContainer).scrollTop(offsetTop);
896
+ } else if (offsetTop > lowerBound) {
897
+ $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta);
898
+ }
899
+
900
+ if (!that.options.preserveInput) {
901
+ that.el.val(that.getValue(that.suggestions[index].value));
902
+ }
903
+ that.signalHint(null);
904
+ },
905
+
906
+ onSelect: function (index) {
907
+ var that = this,
908
+ onSelectCallback = that.options.onSelect,
909
+ suggestion = that.suggestions[index];
910
+
911
+ that.currentValue = that.getValue(suggestion.value);
912
+
913
+ if (that.currentValue !== that.el.val() && !that.options.preserveInput) {
914
+ that.el.val(that.currentValue);
915
+ }
916
+
917
+ that.signalHint(null);
918
+ that.suggestions = [];
919
+ that.selection = suggestion;
920
+
921
+ if ($.isFunction(onSelectCallback)) {
922
+ onSelectCallback.call(that.element, suggestion);
923
+ }
924
+ },
925
+
926
+ getValue: function (value) {
927
+ var that = this,
928
+ delimiter = that.options.delimiter,
929
+ currentValue,
930
+ parts;
931
+
932
+ if (!delimiter) {
933
+ return value;
934
+ }
935
+
936
+ currentValue = that.currentValue;
937
+ parts = currentValue.split(delimiter);
938
+
939
+ if (parts.length === 1) {
940
+ return value;
941
+ }
942
+
943
+ return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value;
944
+ },
945
+
946
+ dispose: function () {
947
+ var that = this;
948
+ that.el.off('.autocomplete').removeData('autocomplete');
949
+ that.disableKillerFn();
950
+ $(window).off('resize.autocomplete', that.fixPositionCapture);
951
+ $(that.suggestionsContainer).remove();
952
+ }
953
+ };
954
+
955
+ // Create chainable jQuery plugin:
956
+ $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) {
957
+ var dataKey = 'autocomplete';
958
+ // If function invoked without argument return
959
+ // instance of the first matched element:
960
+ if (arguments.length === 0) {
961
+ return this.first().data(dataKey);
962
+ }
963
+
964
+ return this.each(function () {
965
+ var inputElement = $(this),
966
+ instance = inputElement.data(dataKey);
967
+
968
+ if (typeof options === 'string') {
969
+ if (instance && typeof instance[options] === 'function') {
970
+ instance[options](args);
971
+ }
972
+ } else {
973
+ // If instance already exists, destroy it:
974
+ if (instance && instance.dispose) {
975
+ instance.dispose();
976
+ }
977
+ instance = new Autocomplete(this, options);
978
+ inputElement.data(dataKey, instance);
979
+ }
980
+ });
981
+ };
982
+ }));