autocomplete.es6.js 7.4 KB

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