autocomplete.es6.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /**
  2. * @file
  3. * Autocomplete based on jQuery UI.
  4. */
  5. (function($, Drupal) {
  6. let autocomplete;
  7. /**
  8. * Helper splitting terms from the autocomplete value.
  9. *
  10. * @function Drupal.autocomplete.splitValues
  11. *
  12. * @param {string} value
  13. * The value being entered by the user.
  14. *
  15. * @return {Array}
  16. * Array of values, split by comma.
  17. */
  18. function autocompleteSplitValues(value) {
  19. // We will match the value against comma-separated terms.
  20. const result = [];
  21. let quote = false;
  22. let current = '';
  23. const valueLength = value.length;
  24. let character;
  25. for (let i = 0; i < valueLength; i++) {
  26. character = value.charAt(i);
  27. if (character === '"') {
  28. current += character;
  29. quote = !quote;
  30. } else if (character === ',' && !quote) {
  31. result.push(current.trim());
  32. current = '';
  33. } else {
  34. current += character;
  35. }
  36. }
  37. if (value.length > 0) {
  38. result.push($.trim(current));
  39. }
  40. return result;
  41. }
  42. /**
  43. * Returns the last value of an multi-value textfield.
  44. *
  45. * @function Drupal.autocomplete.extractLastTerm
  46. *
  47. * @param {string} terms
  48. * The value of the field.
  49. *
  50. * @return {string}
  51. * The last value of the input field.
  52. */
  53. function extractLastTerm(terms) {
  54. return autocomplete.splitValues(terms).pop();
  55. }
  56. /**
  57. * The search handler is called before a search is performed.
  58. *
  59. * @function Drupal.autocomplete.options.search
  60. *
  61. * @param {object} event
  62. * The event triggered.
  63. *
  64. * @return {bool}
  65. * Whether to perform a search or not.
  66. */
  67. function searchHandler(event) {
  68. const options = autocomplete.options;
  69. if (options.isComposing) {
  70. return false;
  71. }
  72. const term = autocomplete.extractLastTerm(event.target.value);
  73. // Abort search if the first character is in firstCharacterBlacklist.
  74. if (
  75. term.length > 0 &&
  76. options.firstCharacterBlacklist.indexOf(term[0]) !== -1
  77. ) {
  78. return false;
  79. }
  80. // Only search when the term is at least the minimum length.
  81. return term.length >= options.minLength;
  82. }
  83. /**
  84. * JQuery UI autocomplete source callback.
  85. *
  86. * @param {object} request
  87. * The request object.
  88. * @param {function} response
  89. * The function to call with the response.
  90. */
  91. function sourceData(request, response) {
  92. const elementId = this.element.attr('id');
  93. if (!(elementId in autocomplete.cache)) {
  94. autocomplete.cache[elementId] = {};
  95. }
  96. /**
  97. * Filter through the suggestions removing all terms already tagged and
  98. * display the available terms to the user.
  99. *
  100. * @param {object} suggestions
  101. * Suggestions returned by the server.
  102. */
  103. function showSuggestions(suggestions) {
  104. const tagged = autocomplete.splitValues(request.term);
  105. const il = tagged.length;
  106. for (let i = 0; i < il; i++) {
  107. const index = suggestions.indexOf(tagged[i]);
  108. if (index >= 0) {
  109. suggestions.splice(index, 1);
  110. }
  111. }
  112. response(suggestions);
  113. }
  114. // Get the desired term and construct the autocomplete URL for it.
  115. const term = autocomplete.extractLastTerm(request.term);
  116. /**
  117. * Transforms the data object into an array and update autocomplete results.
  118. *
  119. * @param {object} data
  120. * The data sent back from the server.
  121. */
  122. function sourceCallbackHandler(data) {
  123. autocomplete.cache[elementId][term] = data;
  124. // Send the new string array of terms to the jQuery UI list.
  125. showSuggestions(data);
  126. }
  127. // Check if the term is already cached.
  128. if (autocomplete.cache[elementId].hasOwnProperty(term)) {
  129. showSuggestions(autocomplete.cache[elementId][term]);
  130. } else {
  131. const options = $.extend(
  132. { success: sourceCallbackHandler, data: { q: term } },
  133. autocomplete.ajax,
  134. );
  135. $.ajax(this.element.attr('data-autocomplete-path'), options);
  136. }
  137. }
  138. /**
  139. * Handles an autocompletefocus event.
  140. *
  141. * @return {bool}
  142. * Always returns false.
  143. */
  144. function focusHandler() {
  145. return false;
  146. }
  147. /**
  148. * Handles an autocompleteselect event.
  149. *
  150. * @param {jQuery.Event} event
  151. * The event triggered.
  152. * @param {object} ui
  153. * The jQuery UI settings object.
  154. *
  155. * @return {bool}
  156. * Returns false to indicate the event status.
  157. */
  158. function selectHandler(event, ui) {
  159. const terms = autocomplete.splitValues(event.target.value);
  160. // Remove the current input.
  161. terms.pop();
  162. // Add the selected item.
  163. terms.push(ui.item.value);
  164. event.target.value = terms.join(', ');
  165. // Return false to tell jQuery UI that we've filled in the value already.
  166. return false;
  167. }
  168. /**
  169. * Override jQuery UI _renderItem function to output HTML by default.
  170. *
  171. * @param {jQuery} ul
  172. * jQuery collection of the ul element.
  173. * @param {object} item
  174. * The list item to append.
  175. *
  176. * @return {jQuery}
  177. * jQuery collection of the ul element.
  178. */
  179. function renderItem(ul, item) {
  180. return $('<li>')
  181. .append($('<a>').html(item.label))
  182. .appendTo(ul);
  183. }
  184. /**
  185. * Attaches the autocomplete behavior to all required fields.
  186. *
  187. * @type {Drupal~behavior}
  188. *
  189. * @prop {Drupal~behaviorAttach} attach
  190. * Attaches the autocomplete behaviors.
  191. * @prop {Drupal~behaviorDetach} detach
  192. * Detaches the autocomplete behaviors.
  193. */
  194. Drupal.behaviors.autocomplete = {
  195. attach(context) {
  196. // Act on textfields with the "form-autocomplete" class.
  197. const $autocomplete = $(context)
  198. .find('input.form-autocomplete')
  199. .once('autocomplete');
  200. if ($autocomplete.length) {
  201. // Allow options to be overridden per instance.
  202. const blacklist = $autocomplete.attr(
  203. 'data-autocomplete-first-character-blacklist',
  204. );
  205. $.extend(autocomplete.options, {
  206. firstCharacterBlacklist: blacklist || '',
  207. });
  208. // Use jQuery UI Autocomplete on the textfield.
  209. $autocomplete.autocomplete(autocomplete.options).each(function() {
  210. $(this).data('ui-autocomplete')._renderItem =
  211. autocomplete.options.renderItem;
  212. });
  213. // Use CompositionEvent to handle IME inputs. It requests remote server on "compositionend" event only.
  214. $autocomplete.on('compositionstart.autocomplete', () => {
  215. autocomplete.options.isComposing = true;
  216. });
  217. $autocomplete.on('compositionend.autocomplete', () => {
  218. autocomplete.options.isComposing = false;
  219. });
  220. }
  221. },
  222. detach(context, settings, trigger) {
  223. if (trigger === 'unload') {
  224. $(context)
  225. .find('input.form-autocomplete')
  226. .removeOnce('autocomplete')
  227. .autocomplete('destroy');
  228. }
  229. },
  230. };
  231. /**
  232. * Autocomplete object implementation.
  233. *
  234. * @namespace Drupal.autocomplete
  235. */
  236. autocomplete = {
  237. cache: {},
  238. // Exposes options to allow overriding by contrib.
  239. splitValues: autocompleteSplitValues,
  240. extractLastTerm,
  241. // jQuery UI autocomplete options.
  242. /**
  243. * JQuery UI option object.
  244. *
  245. * @name Drupal.autocomplete.options
  246. */
  247. options: {
  248. source: sourceData,
  249. focus: focusHandler,
  250. search: searchHandler,
  251. select: selectHandler,
  252. renderItem,
  253. minLength: 1,
  254. // Custom options, used by Drupal.autocomplete.
  255. firstCharacterBlacklist: '',
  256. // Custom options, indicate IME usage status.
  257. isComposing: false,
  258. },
  259. ajax: {
  260. dataType: 'json',
  261. },
  262. };
  263. Drupal.autocomplete = autocomplete;
  264. })(jQuery, Drupal);