autocomplete.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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. if (ui.item.value.search(',') > 0) {
  162. terms.push('"' + ui.item.value + '"');
  163. }
  164. else {
  165. terms.push(ui.item.value);
  166. }
  167. event.target.value = terms.join(', ');
  168. // Return false to tell jQuery UI that we've filled in the value already.
  169. return false;
  170. }
  171. /**
  172. * Override jQuery UI _renderItem function to output HTML by default.
  173. *
  174. * @param {jQuery} ul
  175. * jQuery collection of the ul element.
  176. * @param {object} item
  177. * The list item to append.
  178. *
  179. * @return {jQuery}
  180. * jQuery collection of the ul element.
  181. */
  182. function renderItem(ul, item) {
  183. return $('<li>')
  184. .append($('<a>').html(item.label))
  185. .appendTo(ul);
  186. }
  187. /**
  188. * Attaches the autocomplete behavior to all required fields.
  189. *
  190. * @type {Drupal~behavior}
  191. *
  192. * @prop {Drupal~behaviorAttach} attach
  193. * Attaches the autocomplete behaviors.
  194. * @prop {Drupal~behaviorDetach} detach
  195. * Detaches the autocomplete behaviors.
  196. */
  197. Drupal.behaviors.autocomplete = {
  198. attach: function (context) {
  199. // Act on textfields with the "form-autocomplete" class.
  200. var $autocomplete = $(context).find('input.form-autocomplete').once('autocomplete');
  201. if ($autocomplete.length) {
  202. // Allow options to be overriden per instance.
  203. var blacklist = $autocomplete.attr('data-autocomplete-first-character-blacklist');
  204. $.extend(autocomplete.options, {
  205. firstCharacterBlacklist: (blacklist) ? blacklist : ''
  206. });
  207. // Use jQuery UI Autocomplete on the textfield.
  208. $autocomplete.autocomplete(autocomplete.options)
  209. .each(function () {
  210. $(this).data('ui-autocomplete')._renderItem = autocomplete.options.renderItem;
  211. });
  212. // Use CompositionEvent to handle IME inputs. It requests remote server on "compositionend" event only.
  213. $autocomplete.on('compositionstart.autocomplete', function () {
  214. autocomplete.options.isComposing = true;
  215. });
  216. $autocomplete.on('compositionend.autocomplete', function () {
  217. autocomplete.options.isComposing = false;
  218. });
  219. }
  220. },
  221. detach: function (context, settings, trigger) {
  222. if (trigger === 'unload') {
  223. $(context).find('input.form-autocomplete')
  224. .removeOnce('autocomplete')
  225. .autocomplete('destroy');
  226. }
  227. }
  228. };
  229. /**
  230. * Autocomplete object implementation.
  231. *
  232. * @namespace Drupal.autocomplete
  233. */
  234. autocomplete = {
  235. cache: {},
  236. // Exposes options to allow overriding by contrib.
  237. splitValues: autocompleteSplitValues,
  238. extractLastTerm: extractLastTerm,
  239. // jQuery UI autocomplete options.
  240. /**
  241. * JQuery UI option object.
  242. *
  243. * @name Drupal.autocomplete.options
  244. */
  245. options: {
  246. source: sourceData,
  247. focus: focusHandler,
  248. search: searchHandler,
  249. select: selectHandler,
  250. renderItem: renderItem,
  251. minLength: 1,
  252. // Custom options, used by Drupal.autocomplete.
  253. firstCharacterBlacklist: '',
  254. // Custom options, indicate IME usage status.
  255. isComposing: false
  256. },
  257. ajax: {
  258. dataType: 'json'
  259. }
  260. };
  261. Drupal.autocomplete = autocomplete;
  262. })(jQuery, Drupal);