autocomplete.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. (function ($) {
  2. /**
  3. * Attaches the autocomplete behavior to all required fields.
  4. */
  5. Drupal.behaviors.autocomplete = {
  6. attach: function (context) {
  7. var $context = $(context);
  8. var acdb = [];
  9. $context.find('input.autocomplete').once('autocomplete', function () {
  10. var uri = this.value;
  11. if (!acdb[uri]) {
  12. acdb[uri] = new Drupal.ACDB(uri);
  13. }
  14. var $input = $context.find('#' + this.id.substr(0, this.id.length - 13))
  15. .attr('autocomplete', 'OFF')
  16. .attr('aria-autocomplete', 'list');
  17. $context.find($input[0].form).submit(Drupal.autocompleteSubmit);
  18. $input.parents('.form-item')
  19. .attr('role', 'application')
  20. .append($('<span class="element-invisible" aria-live="assertive"></span>')
  21. .attr('id', $input.attr('id') + '-autocomplete-aria-live')
  22. );
  23. new Drupal.jsAC($input, acdb[uri], $context);
  24. });
  25. }
  26. };
  27. /**
  28. * Prevents the form from submitting if the suggestions popup is open
  29. * and closes the suggestions popup when doing so.
  30. */
  31. Drupal.autocompleteSubmit = function () {
  32. // NOTE: Do not return true as this is non-standard. Keep it similar to
  33. // core. If another contrib project alters this functionality, then it is
  34. // the responsibility of a sub-theme to override this method and combine
  35. // this project with the other project.
  36. return $('.form-autocomplete > .dropdown').each(function () {
  37. this.owner.hidePopup();
  38. }).length == 0;
  39. };
  40. /**
  41. * Highlights a suggestion.
  42. */
  43. Drupal.jsAC.prototype.highlight = function (node) {
  44. if (this.selected) {
  45. $(this.selected).removeClass('active');
  46. }
  47. $(node).addClass('active');
  48. this.selected = node;
  49. $(this.ariaLive).html($(this.selected).html());
  50. };
  51. /**
  52. * Unhighlights a suggestion.
  53. */
  54. Drupal.jsAC.prototype.unhighlight = function (node) {
  55. $(node).removeClass('active');
  56. this.selected = false;
  57. $(this.ariaLive).empty();
  58. };
  59. /**
  60. * Positions the suggestions popup and starts a search.
  61. */
  62. Drupal.jsAC.prototype.populatePopup = function () {
  63. var $input = $(this.input);
  64. // Show popup.
  65. if (this.popup) {
  66. $(this.popup).remove();
  67. }
  68. this.selected = false;
  69. this.popup = $('<div class="dropdown"></div>')[0];
  70. this.popup.owner = this;
  71. $input.parent().after(this.popup);
  72. // Do search.
  73. this.db.owner = this;
  74. this.db.search(this.input.value);
  75. };
  76. /**
  77. * Fills the suggestion popup with any matches received.
  78. */
  79. Drupal.jsAC.prototype.found = function (matches) {
  80. // If no value in the textfield, do not show the popup.
  81. if (!this.input.value.length) {
  82. return false;
  83. }
  84. // Prepare matches.
  85. var ul = $('<ul class="dropdown-menu"></ul>');
  86. var ac = this;
  87. ul.css({
  88. display: 'block',
  89. right: 0
  90. });
  91. for (var key in matches) {
  92. $('<li></li>')
  93. .html($('<a href="#"></a>').html(matches[key]).on('click', function (e) {
  94. e.preventDefault();
  95. }))
  96. .on('mousedown', function () {
  97. ac.hidePopup(this);
  98. })
  99. .on('mouseover', function () {
  100. ac.highlight(this);
  101. })
  102. .on('mouseout', function () {
  103. ac.unhighlight(this);
  104. })
  105. .data('autocompleteValue', key)
  106. .appendTo(ul);
  107. }
  108. // Show popup with matches, if any.
  109. if (this.popup) {
  110. if (ul.children().length) {
  111. $(this.popup).empty().append(ul).show();
  112. $(this.ariaLive).html(Drupal.t('Autocomplete popup'));
  113. }
  114. else {
  115. $(this.popup).css({visibility: 'hidden'});
  116. this.hidePopup();
  117. }
  118. }
  119. };
  120. /**
  121. * Finds the next sibling item.
  122. */
  123. Drupal.jsAC.prototype.findNextSibling = function (element) {
  124. var sibling = element && element.nextSibling;
  125. if (sibling && !this.validItem(sibling)) {
  126. return this.findNextSibling(sibling.nextSibling);
  127. }
  128. return sibling;
  129. };
  130. /**
  131. * Finds the previous sibling item.
  132. */
  133. Drupal.jsAC.prototype.findPreviousSibling = function (element) {
  134. var sibling = element && element.previousSibling;
  135. if (sibling && !this.validItem(sibling)) {
  136. return this.findPreviousSibling(sibling.previousSibling);
  137. }
  138. return sibling;
  139. };
  140. /**
  141. * Highlights the next suggestion.
  142. */
  143. Drupal.jsAC.prototype.selectDown = function () {
  144. var sibling = this.findNextSibling(this.selected);
  145. if (sibling) {
  146. this.highlight(sibling);
  147. }
  148. else if (this.popup) {
  149. var lis = $('li', this.popup);
  150. if (lis.length > 0) {
  151. if (this.validItem(lis[0])) {
  152. this.highlight(lis[0]);
  153. }
  154. else {
  155. this.highlight(this.findNextSibling(lis[0]));
  156. }
  157. }
  158. }
  159. };
  160. /**
  161. * Highlights the previous suggestion.
  162. */
  163. Drupal.jsAC.prototype.selectUp = function () {
  164. var sibling = this.findPreviousSibling(this.selected);
  165. if (sibling) {
  166. this.highlight(sibling);
  167. }
  168. else if (this.popup) {
  169. var lis = $('li', this.popup);
  170. if (lis.length > 0) {
  171. if (this.validItem(lis[lis.length - 1])) {
  172. this.highlight(lis[lis.length - 1]);
  173. }
  174. else {
  175. this.highlight(this.findPreviousSibling(lis[lis.length - 1]));
  176. }
  177. }
  178. }
  179. };
  180. /**
  181. * Ensures the item is valid.
  182. */
  183. Drupal.jsAC.prototype.validItem = function (element) {
  184. return !$(element).is('.dropdown-header, .divider, .disabled');
  185. };
  186. Drupal.jsAC.prototype.setStatus = function (status) {
  187. var $throbber = $(this.input).parent().find('.glyphicon-refresh, .autocomplete-throbber').first();
  188. var throbbingClass = $throbber.is('.autocomplete-throbber') ? 'throbbing' : 'glyphicon-spin';
  189. switch (status) {
  190. case 'begin':
  191. $throbber.addClass(throbbingClass);
  192. $(this.ariaLive).html(Drupal.t('Searching for matches...'));
  193. break;
  194. case 'cancel':
  195. case 'error':
  196. case 'found':
  197. $throbber.removeClass(throbbingClass);
  198. break;
  199. }
  200. };
  201. // Save the previous autocomplete prototype.
  202. var oldPrototype = Drupal.jsAC.prototype;
  203. /**
  204. * Override the autocomplete constructor.
  205. */
  206. Drupal.jsAC = function ($input, db, context) {
  207. var ac = this;
  208. // Context is normally passed by Drupal.behaviors.autocomplete above. However,
  209. // if a module has manually invoked this method they will likely not know
  210. // about this feature and a global fallback context to document must be used.
  211. // @see https://www.drupal.org/node/2594243
  212. // @see https://www.drupal.org/node/2315295
  213. this.$context = context && $(context) || $(document);
  214. this.input = $input[0];
  215. this.ariaLive = this.$context.find('#' + this.input.id + '-autocomplete-aria-live');
  216. this.db = db;
  217. $input
  218. .keydown(function (event) {
  219. return ac.onkeydown(this, event);
  220. })
  221. .keyup(function (event) {
  222. ac.onkeyup(this, event);
  223. })
  224. .blur(function () {
  225. ac.hidePopup();
  226. ac.db.cancel();
  227. });
  228. };
  229. // Restore the previous prototype.
  230. Drupal.jsAC.prototype = oldPrototype;
  231. })(jQuery);