OpenSencillo  2016.106
Long live the simplicity of PHP
 All Data Structures Namespaces Files Functions Pages
jquery.autocomplete.js
1 
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  // Do not replace anything if there current value is empty
131  if (!currentValue) {
132  return suggestion.value;
133  }
134 
135  var pattern = '(' + utils.escapeRegExChars(currentValue) + ')';
136 
137  return suggestion.value
138  .replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>')
139  .replace(/&/g, '&amp;')
140  .replace(/</g, '&lt;')
141  .replace(/>/g, '&gt;')
142  .replace(/"/g, '&quot;')
143  .replace(/&lt;(\/?strong)&gt;/g, '<$1>');
144  };
145 
146  Autocomplete.prototype = {
147 
148  killerFn: null,
149 
150  initialize: function () {
151  var that = this,
152  suggestionSelector = '.' + that.classes.suggestion,
153  selected = that.classes.selected,
154  options = that.options,
155  container;
156 
157  // Remove autocomplete attribute to prevent native suggestions:
158  that.element.setAttribute('autocomplete', 'off');
159 
160  that.killerFn = function (e) {
161  if ($(e.target).closest('.' + that.options.containerClass).length === 0) {
162  that.killSuggestions();
163  that.disableKillerFn();
164  }
165  };
166 
167  // html() deals with many types: htmlString or Element or Array or jQuery
168  that.noSuggestionsContainer = $('<div class="autocomplete-no-suggestion"></div>')
169  .html(this.options.noSuggestionNotice).get(0);
170 
171  that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass);
172 
173  container = $(that.suggestionsContainer);
174 
175  container.appendTo(options.appendTo);
176 
177  // Only set width if it was provided:
178  if (options.width !== 'auto') {
179  container.width(options.width);
180  }
181 
182  // Listen for mouse over event on suggestions list:
183  container.on('mouseover.autocomplete', suggestionSelector, function () {
184  that.activate($(this).data('index'));
185  });
186 
187  // Deselect active element when mouse leaves suggestions container:
188  container.on('mouseout.autocomplete', function () {
189  that.selectedIndex = -1;
190  container.children('.' + selected).removeClass(selected);
191  });
192 
193  // Listen for click event on suggestions list:
194  container.on('click.autocomplete', suggestionSelector, function () {
195  that.select($(this).data('index'));
196  });
197 
198  that.fixPositionCapture = function () {
199  if (that.visible) {
200  that.fixPosition();
201  }
202  };
203 
204  $(window).on('resize.autocomplete', that.fixPositionCapture);
205 
206  that.el.on('keydown.autocomplete', function (e) { that.onKeyPress(e); });
207  that.el.on('keyup.autocomplete', function (e) { that.onKeyUp(e); });
208  that.el.on('blur.autocomplete', function () { that.onBlur(); });
209  that.el.on('focus.autocomplete', function () { that.onFocus(); });
210  that.el.on('change.autocomplete', function (e) { that.onKeyUp(e); });
211  that.el.on('input.autocomplete', function (e) { that.onKeyUp(e); });
212  },
213 
214  onFocus: function () {
215  var that = this;
216 
217  that.fixPosition();
218 
219  if (that.el.val().length >= that.options.minChars) {
220  that.onValueChange();
221  }
222  },
223 
224  onBlur: function () {
225  this.enableKillerFn();
226  },
227 
228  abortAjax: function () {
229  var that = this;
230  if (that.currentRequest) {
231  that.currentRequest.abort();
232  that.currentRequest = null;
233  }
234  },
235 
236  setOptions: function (suppliedOptions) {
237  var that = this,
238  options = that.options;
239 
240  $.extend(options, suppliedOptions);
241 
242  that.isLocal = $.isArray(options.lookup);
243 
244  if (that.isLocal) {
245  options.lookup = that.verifySuggestionsFormat(options.lookup);
246  }
247 
248  options.orientation = that.validateOrientation(options.orientation, 'bottom');
249 
250  // Adjust height, width and z-index:
251  $(that.suggestionsContainer).css({
252  'max-height': options.maxHeight + 'px',
253  'width': options.width + 'px',
254  'z-index': options.zIndex
255  });
256  },
257 
258 
259  clearCache: function () {
260  this.cachedResponse = {};
261  this.badQueries = [];
262  },
263 
264  clear: function () {
265  this.clearCache();
266  this.currentValue = '';
267  this.suggestions = [];
268  },
269 
270  disable: function () {
271  var that = this;
272  that.disabled = true;
273  clearInterval(that.onChangeInterval);
274  that.abortAjax();
275  },
276 
277  enable: function () {
278  this.disabled = false;
279  },
280 
281  fixPosition: function () {
282  // Use only when container has already its content
283 
284  var that = this,
285  $container = $(that.suggestionsContainer),
286  containerParent = $container.parent().get(0);
287  // Fix position automatically when appended to body.
288  // In other cases force parameter must be given.
289  if (containerParent !== document.body && !that.options.forceFixPosition) {
290  return;
291  }
292 
293  // Choose orientation
294  var orientation = that.options.orientation,
295  containerHeight = $container.outerHeight(),
296  height = that.el.outerHeight(),
297  offset = that.el.offset(),
298  styles = { 'top': offset.top, 'left': offset.left };
299 
300  if (orientation === 'auto') {
301  var viewPortHeight = $(window).height(),
302  scrollTop = $(window).scrollTop(),
303  topOverflow = -scrollTop + offset.top - containerHeight,
304  bottomOverflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight);
305 
306  orientation = (Math.max(topOverflow, bottomOverflow) === topOverflow) ? 'top' : 'bottom';
307  }
308 
309  if (orientation === 'top') {
310  styles.top += -containerHeight;
311  } else {
312  styles.top += height;
313  }
314 
315  // If container is not positioned to body,
316  // correct its position using offset parent offset
317  if(containerParent !== document.body) {
318  var opacity = $container.css('opacity'),
319  parentOffsetDiff;
320 
321  if (!that.visible){
322  $container.css('opacity', 0).show();
323  }
324 
325  parentOffsetDiff = $container.offsetParent().offset();
326  styles.top -= parentOffsetDiff.top;
327  styles.left -= parentOffsetDiff.left;
328 
329  if (!that.visible){
330  $container.css('opacity', opacity).hide();
331  }
332  }
333 
334  // -2px to account for suggestions border.
335  if (that.options.width === 'auto') {
336  styles.width = (that.el.outerWidth() - 2) + 'px';
337  }
338 
339  $container.css(styles);
340  },
341 
342  enableKillerFn: function () {
343  var that = this;
344  $(document).on('click.autocomplete', that.killerFn);
345  },
346 
347  disableKillerFn: function () {
348  var that = this;
349  $(document).off('click.autocomplete', that.killerFn);
350  },
351 
352  killSuggestions: function () {
353  var that = this;
354  that.stopKillSuggestions();
355  that.intervalId = window.setInterval(function () {
356  if (that.visible) {
357  that.el.val(that.currentValue);
358  that.hide();
359  }
360 
361  that.stopKillSuggestions();
362  }, 50);
363  },
364 
365  stopKillSuggestions: function () {
366  window.clearInterval(this.intervalId);
367  },
368 
369  isCursorAtEnd: function () {
370  var that = this,
371  valLength = that.el.val().length,
372  selectionStart = that.element.selectionStart,
373  range;
374 
375  if (typeof selectionStart === 'number') {
376  return selectionStart === valLength;
377  }
378  if (document.selection) {
379  range = document.selection.createRange();
380  range.moveStart('character', -valLength);
381  return valLength === range.text.length;
382  }
383  return true;
384  },
385 
386  onKeyPress: function (e) {
387  var that = this;
388 
389  // If suggestions are hidden and user presses arrow down, display suggestions:
390  if (!that.disabled && !that.visible && e.which === keys.DOWN && that.currentValue) {
391  that.suggest();
392  return;
393  }
394 
395  if (that.disabled || !that.visible) {
396  return;
397  }
398 
399  switch (e.which) {
400  case keys.ESC:
401  that.el.val(that.currentValue);
402  that.hide();
403  break;
404  case keys.RIGHT:
405  if (that.hint && that.options.onHint && that.isCursorAtEnd()) {
406  that.selectHint();
407  break;
408  }
409  return;
410  case keys.TAB:
411  if (that.hint && that.options.onHint) {
412  that.selectHint();
413  return;
414  }
415  if (that.selectedIndex === -1) {
416  that.hide();
417  return;
418  }
419  that.select(that.selectedIndex);
420  if (that.options.tabDisabled === false) {
421  return;
422  }
423  break;
424  case keys.RETURN:
425  if (that.selectedIndex === -1) {
426  that.hide();
427  return;
428  }
429  that.select(that.selectedIndex);
430  break;
431  case keys.UP:
432  that.moveUp();
433  break;
434  case keys.DOWN:
435  that.moveDown();
436  break;
437  default:
438  return;
439  }
440 
441  // Cancel event if function did not return:
442  e.stopImmediatePropagation();
443  e.preventDefault();
444  },
445 
446  onKeyUp: function (e) {
447  var that = this;
448 
449  if (that.disabled) {
450  return;
451  }
452 
453  switch (e.which) {
454  case keys.UP:
455  case keys.DOWN:
456  return;
457  }
458 
459  clearInterval(that.onChangeInterval);
460 
461  if (that.currentValue !== that.el.val()) {
462  that.findBestHint();
463  if (that.options.deferRequestBy > 0) {
464  // Defer lookup in case when value changes very quickly:
465  that.onChangeInterval = setInterval(function () {
466  that.onValueChange();
467  }, that.options.deferRequestBy);
468  } else {
469  that.onValueChange();
470  }
471  }
472  },
473 
474  onValueChange: function () {
475  var that = this,
476  options = that.options,
477  value = that.el.val(),
478  query = that.getQuery(value);
479 
480  if (that.selection && that.currentValue !== query) {
481  that.selection = null;
482  (options.onInvalidateSelection || $.noop).call(that.element);
483  }
484 
485  clearInterval(that.onChangeInterval);
486  that.currentValue = value;
487  that.selectedIndex = -1;
488 
489  // Check existing suggestion for the match before proceeding:
490  if (options.triggerSelectOnValidInput && that.isExactMatch(query)) {
491  that.select(0);
492  return;
493  }
494 
495  if (query.length < options.minChars) {
496  that.hide();
497  } else {
498  that.getSuggestions(query);
499  }
500  },
501 
502  isExactMatch: function (query) {
503  var suggestions = this.suggestions;
504 
505  return (suggestions.length === 1 && suggestions[0].value.toLowerCase() === query.toLowerCase());
506  },
507 
508  getQuery: function (value) {
509  var delimiter = this.options.delimiter,
510  parts;
511 
512  if (!delimiter) {
513  return value;
514  }
515  parts = value.split(delimiter);
516  return $.trim(parts[parts.length - 1]);
517  },
518 
519  getSuggestionsLocal: function (query) {
520  var that = this,
521  options = that.options,
522  queryLowerCase = query.toLowerCase(),
523  filter = options.lookupFilter,
524  limit = parseInt(options.lookupLimit, 10),
525  data;
526 
527  data = {
528  suggestions: $.grep(options.lookup, function (suggestion) {
529  return filter(suggestion, query, queryLowerCase);
530  })
531  };
532 
533  if (limit && data.suggestions.length > limit) {
534  data.suggestions = data.suggestions.slice(0, limit);
535  }
536 
537  return data;
538  },
539 
540  getSuggestions: function (q) {
541  var response,
542  that = this,
543  options = that.options,
544  serviceUrl = options.serviceUrl,
545  params,
546  cacheKey,
547  ajaxSettings;
548 
549  options.params[options.paramName] = q;
550  params = options.ignoreParams ? null : options.params;
551 
552  if (options.onSearchStart.call(that.element, options.params) === false) {
553  return;
554  }
555 
556  if ($.isFunction(options.lookup)){
557  options.lookup(q, function (data) {
558  that.suggestions = data.suggestions;
559  that.suggest();
560  options.onSearchComplete.call(that.element, q, data.suggestions);
561  });
562  return;
563  }
564 
565  if (that.isLocal) {
566  response = that.getSuggestionsLocal(q);
567  } else {
568  if ($.isFunction(serviceUrl)) {
569  serviceUrl = serviceUrl.call(that.element, q);
570  }
571  cacheKey = serviceUrl + '?' + $.param(params || {});
572  response = that.cachedResponse[cacheKey];
573  }
574 
575  if (response && $.isArray(response.suggestions)) {
576  that.suggestions = response.suggestions;
577  that.suggest();
578  options.onSearchComplete.call(that.element, q, response.suggestions);
579  } else if (!that.isBadQuery(q)) {
580  that.abortAjax();
581 
582  ajaxSettings = {
583  url: serviceUrl,
584  data: params,
585  type: options.type,
586  dataType: options.dataType
587  };
588 
589  $.extend(ajaxSettings, options.ajaxSettings);
590 
591  that.currentRequest = $.ajax(ajaxSettings).done(function (data) {
592  var result;
593  that.currentRequest = null;
594  result = options.transformResult(data, q);
595  that.processResponse(result, q, cacheKey);
596  options.onSearchComplete.call(that.element, q, result.suggestions);
597  }).fail(function (jqXHR, textStatus, errorThrown) {
598  options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown);
599  });
600  } else {
601  options.onSearchComplete.call(that.element, q, []);
602  }
603  },
604 
605  isBadQuery: function (q) {
606  if (!this.options.preventBadQueries){
607  return false;
608  }
609 
610  var badQueries = this.badQueries,
611  i = badQueries.length;
612 
613  while (i--) {
614  if (q.indexOf(badQueries[i]) === 0) {
615  return true;
616  }
617  }
618 
619  return false;
620  },
621 
622  hide: function () {
623  var that = this,
624  container = $(that.suggestionsContainer);
625 
626  if ($.isFunction(that.options.onHide) && that.visible) {
627  that.options.onHide.call(that.element, container);
628  }
629 
630  that.visible = false;
631  that.selectedIndex = -1;
632  clearInterval(that.onChangeInterval);
633  $(that.suggestionsContainer).hide();
634  that.signalHint(null);
635  },
636 
637  suggest: function () {
638  if (this.suggestions.length === 0) {
639  if (this.options.showNoSuggestionNotice) {
640  this.noSuggestions();
641  } else {
642  this.hide();
643  }
644  return;
645  }
646 
647  var that = this,
648  options = that.options,
649  groupBy = options.groupBy,
650  formatResult = options.formatResult,
651  value = that.getQuery(that.currentValue),
652  className = that.classes.suggestion,
653  classSelected = that.classes.selected,
654  container = $(that.suggestionsContainer),
655  noSuggestionsContainer = $(that.noSuggestionsContainer),
656  beforeRender = options.beforeRender,
657  html = '',
658  category,
659  formatGroup = function (suggestion, index) {
660  var currentCategory = suggestion.data[groupBy];
661 
662  if (category === currentCategory){
663  return '';
664  }
665 
666  category = currentCategory;
667 
668  return '<div class="autocomplete-group"><strong>' + category + '</strong></div>';
669  };
670 
671  if (options.triggerSelectOnValidInput && that.isExactMatch(value)) {
672  that.select(0);
673  return;
674  }
675 
676  // Build suggestions inner HTML:
677  $.each(that.suggestions, function (i, suggestion) {
678  if (groupBy){
679  html += formatGroup(suggestion, value, i);
680  }
681 
682  html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value) + '</div>';
683  });
684 
685  this.adjustContainerWidth();
686 
687  noSuggestionsContainer.detach();
688  container.html(html);
689 
690  if ($.isFunction(beforeRender)) {
691  beforeRender.call(that.element, container);
692  }
693 
694  that.fixPosition();
695  container.show();
696 
697  // Select first value by default:
698  if (options.autoSelectFirst) {
699  that.selectedIndex = 0;
700  container.scrollTop(0);
701  container.children('.' + className).first().addClass(classSelected);
702  }
703 
704  that.visible = true;
705  that.findBestHint();
706  },
707 
708  noSuggestions: function() {
709  var that = this,
710  container = $(that.suggestionsContainer),
711  noSuggestionsContainer = $(that.noSuggestionsContainer);
712 
713  this.adjustContainerWidth();
714 
715  // Some explicit steps. Be careful here as it easy to get
716  // noSuggestionsContainer removed from DOM if not detached properly.
717  noSuggestionsContainer.detach();
718  container.empty(); // clean suggestions if any
719  container.append(noSuggestionsContainer);
720 
721  that.fixPosition();
722 
723  container.show();
724  that.visible = true;
725  },
726 
727  adjustContainerWidth: function() {
728  var that = this,
729  options = that.options,
730  width,
731  container = $(that.suggestionsContainer);
732 
733  // If width is auto, adjust width before displaying suggestions,
734  // because if instance was created before input had width, it will be zero.
735  // Also it adjusts if input width has changed.
736  // -2px to account for suggestions border.
737  if (options.width === 'auto') {
738  width = that.el.outerWidth() - 2;
739  container.width(width > 0 ? width : 300);
740  }
741  },
742 
743  findBestHint: function () {
744  var that = this,
745  value = that.el.val().toLowerCase(),
746  bestMatch = null;
747 
748  if (!value) {
749  return;
750  }
751 
752  $.each(that.suggestions, function (i, suggestion) {
753  var foundMatch = suggestion.value.toLowerCase().indexOf(value) === 0;
754  if (foundMatch) {
755  bestMatch = suggestion;
756  }
757  return !foundMatch;
758  });
759 
760  that.signalHint(bestMatch);
761  },
762 
763  signalHint: function (suggestion) {
764  var hintValue = '',
765  that = this;
766  if (suggestion) {
767  hintValue = that.currentValue + suggestion.value.substr(that.currentValue.length);
768  }
769  if (that.hintValue !== hintValue) {
770  that.hintValue = hintValue;
771  that.hint = suggestion;
772  (this.options.onHint || $.noop)(hintValue);
773  }
774  },
775 
776  verifySuggestionsFormat: function (suggestions) {
777  // If suggestions is string array, convert them to supported format:
778  if (suggestions.length && typeof suggestions[0] === 'string') {
779  return $.map(suggestions, function (value) {
780  return { value: value, data: null };
781  });
782  }
783 
784  return suggestions;
785  },
786 
787  validateOrientation: function(orientation, fallback) {
788  orientation = $.trim(orientation || '').toLowerCase();
789 
790  if($.inArray(orientation, ['auto', 'bottom', 'top']) === -1){
791  orientation = fallback;
792  }
793 
794  return orientation;
795  },
796 
797  processResponse: function (result, originalQuery, cacheKey) {
798  var that = this,
799  options = that.options;
800 
801  result.suggestions = that.verifySuggestionsFormat(result.suggestions);
802 
803  // Cache results if cache is not disabled:
804  if (!options.noCache) {
805  that.cachedResponse[cacheKey] = result;
806  if (options.preventBadQueries && result.suggestions.length === 0) {
807  that.badQueries.push(originalQuery);
808  }
809  }
810 
811  // Return if originalQuery is not matching current query:
812  if (originalQuery !== that.getQuery(that.currentValue)) {
813  return;
814  }
815 
816  that.suggestions = result.suggestions;
817  that.suggest();
818  },
819 
820  activate: function (index) {
821  var that = this,
822  activeItem,
823  selected = that.classes.selected,
824  container = $(that.suggestionsContainer),
825  children = container.find('.' + that.classes.suggestion);
826 
827  container.find('.' + selected).removeClass(selected);
828 
829  that.selectedIndex = index;
830 
831  if (that.selectedIndex !== -1 && children.length > that.selectedIndex) {
832  activeItem = children.get(that.selectedIndex);
833  $(activeItem).addClass(selected);
834  return activeItem;
835  }
836 
837  return null;
838  },
839 
840  selectHint: function () {
841  var that = this,
842  i = $.inArray(that.hint, that.suggestions);
843 
844  that.select(i);
845  },
846 
847  select: function (i) {
848  var that = this;
849  that.hide();
850  that.onSelect(i);
851  },
852 
853  moveUp: function () {
854  var that = this;
855 
856  if (that.selectedIndex === -1) {
857  return;
858  }
859 
860  if (that.selectedIndex === 0) {
861  $(that.suggestionsContainer).children().first().removeClass(that.classes.selected);
862  that.selectedIndex = -1;
863  that.el.val(that.currentValue);
864  that.findBestHint();
865  return;
866  }
867 
868  that.adjustScroll(that.selectedIndex - 1);
869  },
870 
871  moveDown: function () {
872  var that = this;
873 
874  if (that.selectedIndex === (that.suggestions.length - 1)) {
875  return;
876  }
877 
878  that.adjustScroll(that.selectedIndex + 1);
879  },
880 
881  adjustScroll: function (index) {
882  var that = this,
883  activeItem = that.activate(index);
884 
885  if (!activeItem) {
886  return;
887  }
888 
889  var offsetTop,
890  upperBound,
891  lowerBound,
892  heightDelta = $(activeItem).outerHeight();
893 
894  offsetTop = activeItem.offsetTop;
895  upperBound = $(that.suggestionsContainer).scrollTop();
896  lowerBound = upperBound + that.options.maxHeight - heightDelta;
897 
898  if (offsetTop < upperBound) {
899  $(that.suggestionsContainer).scrollTop(offsetTop);
900  } else if (offsetTop > lowerBound) {
901  $(that.suggestionsContainer).scrollTop(offsetTop - that.options.maxHeight + heightDelta);
902  }
903 
904  if (!that.options.preserveInput) {
905  that.el.val(that.getValue(that.suggestions[index].value));
906  }
907  that.signalHint(null);
908  },
909 
910  onSelect: function (index) {
911  var that = this,
912  onSelectCallback = that.options.onSelect,
913  suggestion = that.suggestions[index];
914 
915  that.currentValue = that.getValue(suggestion.value);
916 
917  if (that.currentValue !== that.el.val() && !that.options.preserveInput) {
918  that.el.val(that.currentValue);
919  }
920 
921  that.signalHint(null);
922  that.suggestions = [];
923  that.selection = suggestion;
924 
925  if ($.isFunction(onSelectCallback)) {
926  onSelectCallback.call(that.element, suggestion);
927  }
928  },
929 
930  getValue: function (value) {
931  var that = this,
932  delimiter = that.options.delimiter,
933  currentValue,
934  parts;
935 
936  if (!delimiter) {
937  return value;
938  }
939 
940  currentValue = that.currentValue;
941  parts = currentValue.split(delimiter);
942 
943  if (parts.length === 1) {
944  return value;
945  }
946 
947  return currentValue.substr(0, currentValue.length - parts[parts.length - 1].length) + value;
948  },
949 
950  dispose: function () {
951  var that = this;
952  that.el.off('.autocomplete').removeData('autocomplete');
953  that.disableKillerFn();
954  $(window).off('resize.autocomplete', that.fixPositionCapture);
955  $(that.suggestionsContainer).remove();
956  }
957  };
958 
959  // Create chainable jQuery plugin:
960  $.fn.autocomplete = $.fn.devbridgeAutocomplete = function (options, args) {
961  var dataKey = 'autocomplete';
962  // If function invoked without argument return
963  // instance of the first matched element:
964  if (arguments.length === 0) {
965  return this.first().data(dataKey);
966  }
967 
968  return this.each(function () {
969  var inputElement = $(this),
970  instance = inputElement.data(dataKey);
971 
972  if (typeof options === 'string') {
973  if (instance && typeof instance[options] === 'function') {
974  instance[options](args);
975  }
976  } else {
977  // If instance already exists, destroy it:
978  if (instance && instance.dispose) {
979  instance.dispose();
980  }
981  instance = new Autocomplete(this, options);
982  inputElement.data(dataKey, instance);
983  }
984  });
985  };
986 }));