Include autocomplete extension
[infodrom.org/service.infodrom.org] / src / jquery.autocomplete.js
1 /**
2 *  Ajax Autocomplete for jQuery, version 1.4.1
3 *  (c) 2017 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, single: true, this: true, multivar: 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         noop = $.noop;
55
56     function Autocomplete(el, options) {
57         var that = this;
58
59         // Shared variables:
60         that.element = el;
61         that.el = $(el);
62         that.suggestions = [];
63         that.badQueries = [];
64         that.selectedIndex = -1;
65         that.currentValue = that.element.value;
66         that.timeoutId = null;
67         that.cachedResponse = {};
68         that.onChangeTimeout = null;
69         that.onChange = null;
70         that.isLocal = false;
71         that.suggestionsContainer = null;
72         that.noSuggestionsContainer = null;
73         that.options = $.extend({}, Autocomplete.defaults, options);
74         that.classes = {
75             selected: 'autocomplete-selected',
76             suggestion: 'autocomplete-suggestion'
77         };
78         that.hint = null;
79         that.hintValue = '';
80         that.selection = null;
81
82         // Initialize and set options:
83         that.initialize();
84         that.setOptions(options);
85     }
86
87     Autocomplete.utils = utils;
88
89     $.Autocomplete = Autocomplete;
90
91     Autocomplete.defaults = {
92             ajaxSettings: {},
93             autoSelectFirst: false,
94             appendTo: 'body',
95             serviceUrl: null,
96             lookup: null,
97             onSelect: null,
98             width: 'auto',
99             minChars: 1,
100             maxHeight: 300,
101             deferRequestBy: 0,
102             params: {},
103             formatResult: _formatResult,
104             formatGroup: _formatGroup,
105             delimiter: null,
106             zIndex: 9999,
107             type: 'GET',
108             noCache: false,
109             onSearchStart: noop,
110             onSearchComplete: noop,
111             onSearchError: noop,
112             preserveInput: false,
113             containerClass: 'autocomplete-suggestions',
114             tabDisabled: false,
115             dataType: 'text',
116             currentRequest: null,
117             triggerSelectOnValidInput: true,
118             preventBadQueries: true,
119             lookupFilter: _lookupFilter,
120             paramName: 'query',
121             transformResult: _transformResult,
122             showNoSuggestionNotice: false,
123             noSuggestionNotice: 'No results',
124             orientation: 'bottom',
125             forceFixPosition: false
126     };
127
128     function _lookupFilter(suggestion, originalQuery, queryLowerCase) {
129         return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1;
130     };
131
132     function _transformResult(response) {
133         return typeof response === 'string' ? $.parseJSON(response) : response;
134     };
135
136     function _formatResult(suggestion, currentValue) {
137         // Do not replace anything if the current value is empty
138         if (!currentValue) {
139             return suggestion.value;
140         }
141         
142         var pattern = '(' + utils.escapeRegExChars(currentValue) + ')';
143
144         return suggestion.value
145             .replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>')
146             .replace(/&/g, '&amp;')
147             .replace(/</g, '&lt;')
148             .replace(/>/g, '&gt;')
149             .replace(/"/g, '&quot;')
150             .replace(/&lt;(\/?strong)&gt;/g, '<$1>');
151     };
152
153     function _formatGroup(suggestion, category) {
154         return '<div class="autocomplete-group">' + category + '</div>';
155     };
156
157     Autocomplete.prototype = {
158
159         initialize: function () {
160             var that = this,
161                 suggestionSelector = '.' + that.classes.suggestion,
162                 selected = that.classes.selected,
163                 options = that.options,
164                 container;
165
166             // Remove autocomplete attribute to prevent native suggestions:
167             that.element.setAttribute('autocomplete', 'off');
168
169             // html() deals with many types: htmlString or Element or Array or jQuery
170             that.noSuggestionsContainer = $('<div class="autocomplete-no-suggestion"></div>')
171                                           .html(this.options.noSuggestionNotice).get(0);
172
173             that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass);
174
175             container = $(that.suggestionsContainer);
176
177             container.appendTo(options.appendTo || 'body');
178
179             // Only set width if it was provided:
180             if (options.width !== 'auto') {
181                 container.css('width', options.width);
182             }
183
184             // Listen for mouse over event on suggestions list:
185             container.on('mouseover.autocomplete', suggestionSelector, function () {
186                 that.activate($(this).data('index'));
187             });
188
189             // Deselect active element when mouse leaves suggestions container:
190             container.on('mouseout.autocomplete', function () {
191                 that.selectedIndex = -1;
192                 container.children('.' + selected).removeClass(selected);
193             });
194
195
196             // Listen for click event on suggestions list:
197             container.on('click.autocomplete', suggestionSelector, function () {
198                 that.select($(this).data('index'));
199             });
200
201             container.on('click.autocomplete', function () {
202                 clearTimeout(that.blurTimeoutId);
203             })
204
205             that.fixPositionCapture = function () {
206                 if (that.visible) {
207                     that.fixPosition();
208                 }
209             };
210
211             $(window).on('resize.autocomplete', that.fixPositionCapture);
212
213             that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); });
214             that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); });
215             that.el.on('blur.autocomplete', function () { that.onBlur(); });
216             that.el.on('focus.autocomplete', function () { that.onFocus(); });
217             that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); });
218             that.el.on('input.autocomplete', function (e) { that.onKeyUp(e); });
219         },
220
221         onFocus: function () {
222             var that = this;
223
224             that.fixPosition();
225
226             if (that.el.val().length >= that.options.minChars) {
227                 that.onValueChange();
228             }
229         },
230
231         onBlur: function () {
232             var that = this;
233
234             // If user clicked on a suggestion, hide() will
235             // be canceled, otherwise close suggestions
236             that.blurTimeoutId = setTimeout(function () {
237                 that.hide();
238             }, 200);
239         },
240         
241         abortAjax: function () {
242             var that = this;
243             if (that.currentRequest) {
244                 that.currentRequest.abort();
245                 that.currentRequest = null;
246             }
247         },
248
249         setOptions: function (suppliedOptions) {
250             var that = this,
251                 options = that.options;
252
253             this.options = $.extend({}, options, suppliedOptions);
254
255             that.isLocal = $.isArray(options.lookup);
256
257             if (that.isLocal) {
258                 options.lookup = that.verifySuggestionsFormat(options.lookup);
259             }
260
261             options.orientation = that.validateOrientation(options.orientation, 'bottom');
262
263             // Adjust height, width and z-index:
264             $(that.suggestionsContainer).css({
265                 'max-height': options.maxHeight + 'px',
266                 'width': options.width + 'px',
267                 'z-index': options.zIndex
268             });
269         },
270
271
272         clearCache: function () {
273             this.cachedResponse = {};
274             this.badQueries = [];
275         },
276
277         clear: function () {
278             this.clearCache();
279             this.currentValue = '';
280             this.suggestions = [];
281         },
282
283         disable: function () {
284             var that = this;
285             that.disabled = true;
286             clearTimeout(that.onChangeTimeout);
287             that.abortAjax();
288         },
289
290         enable: function () {
291             this.disabled = false;
292         },
293
294         fixPosition: function () {
295             // Use only when container has already its content
296
297             var that = this,
298                 $container = $(that.suggestionsContainer),
299                 containerParent = $container.parent().get(0);
300             // Fix position automatically when appended to body.
301             // In other cases force parameter must be given.
302             if (containerParent !== document.body && !that.options.forceFixPosition) {
303                 return;
304             }
305
306             // Choose orientation
307             var orientation = that.options.orientation,
308                 containerHeight = $container.outerHeight(),
309                 height = that.el.outerHeight(),
310                 offset = that.el.offset(),
311                 styles = { 'top': offset.top, 'left': offset.left };
312
313             if (orientation === 'auto') {
314                 var viewPortHeight = $(window).height(),
315                     scrollTop = $(window).scrollTop(),
316                     topOverflow = -scrollTop + offset.top - containerHeight,
317                     bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight);
318
319                 orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) ? 'top' : 'bottom';
320             }
321
322             if (orientation === 'top') {
323                 styles.top += -containerHeight;
324             } else {
325                 styles.top += height;
326             }
327
328             // If container is not positioned to body,
329             // correct its position using offset parent offset
330             if(containerParent !== document.body) {
331                 var opacity = $container.css('opacity'),
332                     parentOffsetDiff;
333
334                     if (!that.visible){
335                         $container.css('opacity', 0).show();
336                     }
337
338                 parentOffsetDiff = $container.offsetParent().offset();
339                 styles.top -= parentOffsetDiff.top;
340                 styles.left -= parentOffsetDiff.left;
341
342                 if (!that.visible){
343                     $container.css('opacity', opacity).hide();
344                 }
345             }
346
347             if (that.options.width === 'auto') {
348                 styles.width = that.el.outerWidth() + 'px';
349             }
350
351             $container.css(styles);
352         },
353
354         isCursorAtEnd: function () {
355             var that = this,
356                 valLength = that.el.val().length,
357                 selectionStart = that.element.selectionStart,
358                 range;
359
360             if (typeof selectionStart === 'number') {
361                 return selectionStart === valLength;
362             }
363             if (document.selection) {
364                 range = document.selection.createRange();
365                 range.moveStart('character', -valLength);
366                 return valLength === range.text.length;
367             }
368             return true;
369         },
370
371         onKeyPress: function (e) {
372             var that = this;
373
374             // If suggestions are hidden and user presses arrow down, display suggestions:
375             if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) {
376                 that.suggest();
377                 return;
378             }
379
380             if (that.disabled || !that.visible) {
381                 return;
382             }
383
384             switch (e.which) {
385                 case keys.ESC:
386                     that.el.val(that.currentValue);
387                     that.hide();
388                     break;
389                 case keys.RIGHT:
390                     if (that.hint && that.options.onHint && that.isCursorAtEnd()) {
391                         that.selectHint();
392                         break;
393                     }
394                     return;
395                 case keys.TAB:
396                     if (that.hint && that.options.onHint) {
397                         that.selectHint();
398                         return;
399                     }
400                     if (that.selectedIndex === -1) {
401                         that.hide();
402                         return;
403                     }
404                     that.select(that.selectedIndex);
405                     if (that.options.tabDisabled === false) {
406                         return;
407                     }
408                     break;
409                 case keys.RETURN:
410                     if (that.selectedIndex === -1) {
411                         that.hide();
412                         return;
413                     }
414                     that.select(that.selectedIndex);
415                     break;
416                 case keys.UP:
417                     that.moveUp();
418                     break;
419                 case keys.DOWN:
420                     that.moveDown();
421                     break;
422                 default:
423                     return;
424             }
425
426             // Cancel event if function did not return:
427             e.stopImmediatePropagation();
428             e.preventDefault();
429         },
430
431         onKeyUp: function (e) {
432             var that = this;
433
434             if (that.disabled) {
435                 return;
436             }
437
438             switch (e.which) {
439                 case keys.UP:
440                 case keys.DOWN:
441                     return;
442             }
443
444             clearTimeout(that.onChangeTimeout);
445
446             if (that.currentValue !== that.el.val()) {
447                 that.findBestHint();
448                 if (that.options.deferRequestBy > 0) {
449                     // Defer lookup in case when value changes very quickly:
450                     that.onChangeTimeout = setTimeout(function () {
451                         that.onValueChange();
452                     }, that.options.deferRequestBy);
453                 } else {
454                     that.onValueChange();
455                 }
456             }
457         },
458
459         onValueChange: function () {
460             var that = this,
461                 options = that.options,
462                 value = that.el.val(),
463                 query = that.getQuery(value);
464
465             if (that.selection && that.currentValue !== query) {
466                 that.selection = null;
467                 (options.onInvalidateSelection || $.noop).call(that.element);
468             }
469
470             clearTimeout(that.onChangeTimeout);
471             that.currentValue = value;
472             that.selectedIndex = -1;
473
474             // Check existing suggestion for the match before proceeding:
475             if (options.triggerSelectOnValidInput && that.isExactMatch(query)) {
476                 that.select(0);
477                 return;
478             }
479
480             if (query.length < options.minChars) {
481                 that.hide();
482             } else {
483                 that.getSuggestions(query);
484             }
485         },
486
487         isExactMatch: function (query) {
488             var suggestions = this.suggestions;
489
490             return (suggestions.length === 1 && suggestions[0].value.toLowerCase() === query.toLowerCase());
491         },
492
493         getQuery: function (value) {
494             var delimiter = this.options.delimiter,
495                 parts;
496
497             if (!delimiter) {
498                 return value;
499             }
500             parts = value.split(delimiter);
501             return $.trim(parts[parts.length - 1]);
502         },
503
504         getSuggestionsLocal: function (query) {
505             var that = this,
506                 options = that.options,
507                 queryLowerCase = query.toLowerCase(),
508                 filter = options.lookupFilter,
509                 limit = parseInt(options.lookupLimit, 10),
510                 data;
511
512             data = {
513                 suggestions: $.grep(options.lookup, function (suggestion) {
514                     return filter(suggestion, query, queryLowerCase);
515                 })
516             };
517
518             if (limit && data.suggestions.length > limit) {
519                 data.suggestions = data.suggestions.slice(0, limit);
520             }
521
522             return data;
523         },
524
525         getSuggestions: function (q) {
526             var response,
527                 that = this,
528                 options = that.options,
529                 serviceUrl = options.serviceUrl,
530                 params,
531                 cacheKey,
532                 ajaxSettings;
533
534             options.params[options.paramName] = q;
535
536             if (options.onSearchStart.call(that.element, options.params) === false) {
537                 return;
538             }
539
540             params = options.ignoreParams ? null : options.params;
541
542             if ($.isFunction(options.lookup)){
543                 options.lookup(q, function (data) {
544                     that.suggestions = data.suggestions;
545                     that.suggest();
546                     options.onSearchComplete.call(that.element, q, data.suggestions);
547                 });
548                 return;
549             }
550
551             if (that.isLocal) {
552                 response = that.getSuggestionsLocal(q);
553             } else {
554                 if ($.isFunction(serviceUrl)) {
555                     serviceUrl = serviceUrl.call(that.element, q);
556                 }
557                 cacheKey = serviceUrl + '?' + $.param(params || {});
558                 response = that.cachedResponse[cacheKey];
559             }
560
561             if (response && $.isArray(response.suggestions)) {
562                 that.suggestions = response.suggestions;
563                 that.suggest();
564                 options.onSearchComplete.call(that.element, q, response.suggestions);
565             } else if (!that.isBadQuery(q)) {
566                 that.abortAjax();
567
568                 ajaxSettings = {
569                     url: serviceUrl,
570                     data: params,
571                     type: options.type,
572                     dataType: options.dataType
573                 };
574
575                 $.extend(ajaxSettings, options.ajaxSettings);
576
577                 that.currentRequest = $.ajax(ajaxSettings).done(function (data) {
578                     var result;
579                     that.currentRequest = null;
580                     result = options.transformResult(data, q);
581                     that.processResponse(result, q, cacheKey);
582                     options.onSearchComplete.call(that.element, q, result.suggestions);
583                 }).fail(function (jqXHR, textStatus, errorThrown) {
584                     options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown);
585                 });
586             } else {
587                 options.onSearchComplete.call(that.element, q, []);
588             }
589         },
590
591         isBadQuery: function (q) {
592             if (!this.options.preventBadQueries){
593                 return false;
594             }
595
596             var badQueries = this.badQueries,
597                 i = badQueries.length;
598
599             while (i--) {
600                 if (q.indexOf(badQueries[i]) === 0) {
601                     return true;
602                 }
603             }
604
605             return false;
606         },
607
608         hide: function () {
609             var that = this,
610                 container = $(that.suggestionsContainer);
611
612             if ($.isFunction(that.options.onHide) && that.visible) {
613                 that.options.onHide.call(that.element, container);
614             }
615
616             that.visible = false;
617             that.selectedIndex = -1;
618             clearTimeout(that.onChangeTimeout);
619             $(that.suggestionsContainer).hide();
620             that.signalHint(null);
621         },
622
623         suggest: function () {
624             if (!this.suggestions.length) {
625                 if (this.options.showNoSuggestionNotice) {
626                     this.noSuggestions();
627                 } else {
628                     this.hide();
629                 }
630                 return;
631             }
632
633             var that = this,
634                 options = that.options,
635                 groupBy = options.groupBy,
636                 formatResult = options.formatResult,
637                 value = that.getQuery(that.currentValue),
638                 className = that.classes.suggestion,
639                 classSelected = that.classes.selected,
640                 container = $(that.suggestionsContainer),
641                 noSuggestionsContainer = $(that.noSuggestionsContainer),
642                 beforeRender = options.beforeRender,
643                 html = '',
644                 category,
645                 formatGroup = function (suggestion, index) {
646                         var currentCategory = suggestion.data[groupBy];
647
648                         if (category === currentCategory){
649                             return '';
650                         }
651
652                         category = currentCategory;
653
654                         return options.formatGroup(suggestion, category);
655                     };
656
657             if (options.triggerSelectOnValidInput && that.isExactMatch(value)) {
658                 that.select(0);
659                 return;
660             }
661
662             // Build suggestions inner HTML:
663             $.each(that.suggestions, function (i, suggestion) {
664                 if (groupBy){
665                     html += formatGroup(suggestion, value, i);
666                 }
667
668                 html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value, i) + '</div>';
669             });
670
671             this.adjustContainerWidth();
672
673             noSuggestionsContainer.detach();
674             container.html(html);
675
676             if ($.isFunction(beforeRender)) {
677                 beforeRender.call(that.element, container, that.suggestions);
678             }
679
680             that.fixPosition();
681             container.show();
682
683             // Select first value by default:
684             if (options.autoSelectFirst) {
685                 that.selectedIndex = 0;
686                 container.scrollTop(0);
687                 container.children('.' + className).first().addClass(classSelected);
688             }
689
690             that.visible = true;
691             that.findBestHint();
692         },
693
694         noSuggestions: function() {
695              var that = this,
696                  beforeRender = that.options.beforeRender,
697                  container = $(that.suggestionsContainer),
698                  noSuggestionsContainer = $(that.noSuggestionsContainer);
699
700             this.adjustContainerWidth();
701
702             // Some explicit steps. Be careful here as it easy to get
703             // noSuggestionsContainer removed from DOM if not detached properly.
704             noSuggestionsContainer.detach();
705
706             // clean suggestions if any
707             container.empty(); 
708             container.append(noSuggestionsContainer);
709
710             if ($.isFunction(beforeRender)) {
711                 beforeRender.call(that.element, container, that.suggestions);
712             }
713
714             that.fixPosition();
715
716             container.show();
717             that.visible = true;
718         },
719
720         adjustContainerWidth: function() {
721             var that = this,
722                 options = that.options,
723                 width,
724                 container = $(that.suggestionsContainer);
725
726             // If width is auto, adjust width before displaying suggestions,
727             // because if instance was created before input had width, it will be zero.
728             // Also it adjusts if input width has changed.
729             if (options.width === 'auto') {
730                 width = that.el.outerWidth();
731                 container.css('width', width > 0 ? width : 300);
732             } else if(options.width === 'flex') {
733                 // Trust the source! Unset the width property so it will be the max length
734                 // the containing elements.
735                 container.css('width', '');
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) {
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             $(window).off('resize.autocomplete', that.fixPositionCapture);
950             $(that.suggestionsContainer).remove();
951         }
952     };
953
954     // Create chainable jQuery plugin:
955     $.fn.devbridgeAutocomplete = function (options, args) {
956         var dataKey = 'autocomplete';
957         // If function invoked without argument return
958         // instance of the first matched element:
959         if (!arguments.length) {
960             return this.first().data(dataKey);
961         }
962
963         return this.each(function () {
964             var inputElement = $(this),
965                 instance = inputElement.data(dataKey);
966
967             if (typeof options === 'string') {
968                 if (instance && typeof instance[options] === 'function') {
969                     instance[options](args);
970                 }
971             } else {
972                 // If instance already exists, destroy it:
973                 if (instance && instance.dispose) {
974                     instance.dispose();
975                 }
976                 instance = new Autocomplete(this, options);
977                 inputElement.data(dataKey, instance);
978             }
979         });
980     };
981
982     // Don't overwrite if it already exists
983     if (!$.fn.autocomplete) {
984         $.fn.autocomplete = $.fn.devbridgeAutocomplete;
985     }
986 }));