autocomplete.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. (function ($) {
  2. /**
  3. * Attaches the autocomplete behavior to all required fields.
  4. */
  5. Drupal.behaviors.autocomplete = {
  6. attach: function (context, settings) {
  7. var acdb = [];
  8. $('input.autocomplete', context).once('autocomplete', function () {
  9. var uri = this.value;
  10. if (!acdb[uri]) {
  11. acdb[uri] = new Drupal.ACDB(uri);
  12. }
  13. var $input = $('#' + this.id.substr(0, this.id.length - 13))
  14. .attr('autocomplete', 'OFF')
  15. .attr('aria-autocomplete', 'list');
  16. $($input[0].form).submit(Drupal.autocompleteSubmit);
  17. $input.parent()
  18. .attr('role', 'application')
  19. .append($('<span class="element-invisible" aria-live="assertive"></span>')
  20. .attr('id', $input.attr('id') + '-autocomplete-aria-live')
  21. );
  22. new Drupal.jsAC($input, acdb[uri]);
  23. });
  24. }
  25. };
  26. /**
  27. * Prevents the form from submitting if the suggestions popup is open
  28. * and closes the suggestions popup when doing so.
  29. */
  30. Drupal.autocompleteSubmit = function () {
  31. return $('#autocomplete').each(function () {
  32. this.owner.hidePopup();
  33. }).length == 0;
  34. };
  35. /**
  36. * An AutoComplete object.
  37. */
  38. Drupal.jsAC = function ($input, db) {
  39. var ac = this;
  40. this.input = $input[0];
  41. this.ariaLive = $('#' + this.input.id + '-autocomplete-aria-live');
  42. this.db = db;
  43. $input
  44. .keydown(function (event) { return ac.onkeydown(this, event); })
  45. .keyup(function (event) { ac.onkeyup(this, event); })
  46. .blur(function () { ac.hidePopup(); ac.db.cancel(); });
  47. };
  48. /**
  49. * Handler for the "keydown" event.
  50. */
  51. Drupal.jsAC.prototype.onkeydown = function (input, e) {
  52. if (!e) {
  53. e = window.event;
  54. }
  55. switch (e.keyCode) {
  56. case 40: // down arrow.
  57. this.selectDown();
  58. return false;
  59. case 38: // up arrow.
  60. this.selectUp();
  61. return false;
  62. default: // All other keys.
  63. return true;
  64. }
  65. };
  66. /**
  67. * Handler for the "keyup" event.
  68. */
  69. Drupal.jsAC.prototype.onkeyup = function (input, e) {
  70. if (!e) {
  71. e = window.event;
  72. }
  73. switch (e.keyCode) {
  74. case 16: // Shift.
  75. case 17: // Ctrl.
  76. case 18: // Alt.
  77. case 20: // Caps lock.
  78. case 33: // Page up.
  79. case 34: // Page down.
  80. case 35: // End.
  81. case 36: // Home.
  82. case 37: // Left arrow.
  83. case 38: // Up arrow.
  84. case 39: // Right arrow.
  85. case 40: // Down arrow.
  86. return true;
  87. case 9: // Tab.
  88. case 13: // Enter.
  89. case 27: // Esc.
  90. this.hidePopup(e.keyCode);
  91. return true;
  92. default: // All other keys.
  93. if (input.value.length > 0 && !input.readOnly) {
  94. this.populatePopup();
  95. }
  96. else {
  97. this.hidePopup(e.keyCode);
  98. }
  99. return true;
  100. }
  101. };
  102. /**
  103. * Puts the currently highlighted suggestion into the autocomplete field.
  104. */
  105. Drupal.jsAC.prototype.select = function (node) {
  106. this.input.value = $(node).data('autocompleteValue');
  107. $(this.input).trigger('autocompleteSelect', [node]);
  108. };
  109. /**
  110. * Highlights the next suggestion.
  111. */
  112. Drupal.jsAC.prototype.selectDown = function () {
  113. if (this.selected && this.selected.nextSibling) {
  114. this.highlight(this.selected.nextSibling);
  115. }
  116. else if (this.popup) {
  117. var lis = $('li', this.popup);
  118. if (lis.length > 0) {
  119. this.highlight(lis.get(0));
  120. }
  121. }
  122. };
  123. /**
  124. * Highlights the previous suggestion.
  125. */
  126. Drupal.jsAC.prototype.selectUp = function () {
  127. if (this.selected && this.selected.previousSibling) {
  128. this.highlight(this.selected.previousSibling);
  129. }
  130. };
  131. /**
  132. * Highlights a suggestion.
  133. */
  134. Drupal.jsAC.prototype.highlight = function (node) {
  135. if (this.selected) {
  136. $(this.selected).removeClass('selected');
  137. }
  138. $(node).addClass('selected');
  139. this.selected = node;
  140. $(this.ariaLive).html($(this.selected).html());
  141. };
  142. /**
  143. * Unhighlights a suggestion.
  144. */
  145. Drupal.jsAC.prototype.unhighlight = function (node) {
  146. $(node).removeClass('selected');
  147. this.selected = false;
  148. $(this.ariaLive).empty();
  149. };
  150. /**
  151. * Hides the autocomplete suggestions.
  152. */
  153. Drupal.jsAC.prototype.hidePopup = function (keycode) {
  154. // Select item if the right key or mousebutton was pressed.
  155. if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
  156. this.select(this.selected);
  157. }
  158. // Hide popup.
  159. var popup = this.popup;
  160. if (popup) {
  161. this.popup = null;
  162. $(popup).fadeOut('fast', function () { $(popup).remove(); });
  163. }
  164. this.selected = false;
  165. $(this.ariaLive).empty();
  166. };
  167. /**
  168. * Positions the suggestions popup and starts a search.
  169. */
  170. Drupal.jsAC.prototype.populatePopup = function () {
  171. var $input = $(this.input);
  172. var position = $input.position();
  173. // Show popup.
  174. if (this.popup) {
  175. $(this.popup).remove();
  176. }
  177. this.selected = false;
  178. this.popup = $('<div id="autocomplete"></div>')[0];
  179. this.popup.owner = this;
  180. $(this.popup).css({
  181. top: parseInt(position.top + this.input.offsetHeight, 10) + 'px',
  182. left: parseInt(position.left, 10) + 'px',
  183. width: $input.innerWidth() + 'px',
  184. display: 'none'
  185. });
  186. $input.before(this.popup);
  187. // Do search.
  188. this.db.owner = this;
  189. this.db.search(this.input.value);
  190. };
  191. /**
  192. * Fills the suggestion popup with any matches received.
  193. */
  194. Drupal.jsAC.prototype.found = function (matches) {
  195. // If no value in the textfield, do not show the popup.
  196. if (!this.input.value.length) {
  197. return false;
  198. }
  199. // Prepare matches.
  200. var ul = $('<ul></ul>');
  201. var ac = this;
  202. for (key in matches) {
  203. $('<li></li>')
  204. .html($('<div></div>').html(matches[key]))
  205. .mousedown(function () { ac.hidePopup(this); })
  206. .mouseover(function () { ac.highlight(this); })
  207. .mouseout(function () { ac.unhighlight(this); })
  208. .data('autocompleteValue', key)
  209. .appendTo(ul);
  210. }
  211. // Show popup with matches, if any.
  212. if (this.popup) {
  213. if (ul.children().length) {
  214. $(this.popup).empty().append(ul).show();
  215. $(this.ariaLive).html(Drupal.t('Autocomplete popup'));
  216. }
  217. else {
  218. $(this.popup).css({ visibility: 'hidden' });
  219. this.hidePopup();
  220. }
  221. }
  222. };
  223. Drupal.jsAC.prototype.setStatus = function (status) {
  224. switch (status) {
  225. case 'begin':
  226. $(this.input).addClass('throbbing');
  227. $(this.ariaLive).html(Drupal.t('Searching for matches...'));
  228. break;
  229. case 'cancel':
  230. case 'error':
  231. case 'found':
  232. $(this.input).removeClass('throbbing');
  233. break;
  234. }
  235. };
  236. /**
  237. * An AutoComplete DataBase object.
  238. */
  239. Drupal.ACDB = function (uri) {
  240. this.uri = uri;
  241. this.delay = 300;
  242. this.cache = {};
  243. };
  244. /**
  245. * Performs a cached and delayed search.
  246. */
  247. Drupal.ACDB.prototype.search = function (searchString) {
  248. var db = this;
  249. this.searchString = searchString;
  250. // See if this string needs to be searched for anyway. The pattern ../ is
  251. // stripped since it may be misinterpreted by the browser.
  252. searchString = searchString.replace(/^\s+|\.{2,}\/|\s+$/g, '');
  253. // Skip empty search strings, or search strings ending with a comma, since
  254. // that is the separator between search terms.
  255. if (searchString.length <= 0 ||
  256. searchString.charAt(searchString.length - 1) == ',') {
  257. return;
  258. }
  259. // See if this key has been searched for before.
  260. if (this.cache[searchString]) {
  261. return this.owner.found(this.cache[searchString]);
  262. }
  263. // Initiate delayed search.
  264. if (this.timer) {
  265. clearTimeout(this.timer);
  266. }
  267. this.timer = setTimeout(function () {
  268. db.owner.setStatus('begin');
  269. // Ajax GET request for autocompletion. We use Drupal.encodePath instead of
  270. // encodeURIComponent to allow autocomplete search terms to contain slashes.
  271. $.ajax({
  272. type: 'GET',
  273. url: db.uri + '/' + Drupal.encodePath(searchString),
  274. dataType: 'json',
  275. success: function (matches) {
  276. if (typeof matches.status == 'undefined' || matches.status != 0) {
  277. db.cache[searchString] = matches;
  278. // Verify if these are still the matches the user wants to see.
  279. if (db.searchString == searchString) {
  280. db.owner.found(matches);
  281. }
  282. db.owner.setStatus('found');
  283. }
  284. },
  285. error: function (xmlhttp) {
  286. Drupal.displayAjaxError(Drupal.ajaxError(xmlhttp, db.uri));
  287. }
  288. });
  289. }, this.delay);
  290. };
  291. /**
  292. * Cancels the current autocomplete request.
  293. */
  294. Drupal.ACDB.prototype.cancel = function () {
  295. if (this.owner) this.owner.setStatus('cancel');
  296. if (this.timer) clearTimeout(this.timer);
  297. this.searchString = '';
  298. };
  299. })(jQuery);