jquery.autocomplete.js 33 KB

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