autocomplete.js 7.5 KB

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