jquery.autocomplete.js 33 KB

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