autocomplete.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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. };
  108. /**
  109. * Highlights the next suggestion.
  110. */
  111. Drupal.jsAC.prototype.selectDown = function () {
  112. if (this.selected && this.selected.nextSibling) {
  113. this.highlight(this.selected.nextSibling);
  114. }
  115. else if (this.popup) {
  116. var lis = $('li', this.popup);
  117. if (lis.length > 0) {
  118. this.highlight(lis.get(0));
  119. }
  120. }
  121. };
  122. /**
  123. * Highlights the previous suggestion.
  124. */
  125. Drupal.jsAC.prototype.selectUp = function () {
  126. if (this.selected && this.selected.previousSibling) {
  127. this.highlight(this.selected.previousSibling);
  128. }
  129. };
  130. /**
  131. * Highlights a suggestion.
  132. */
  133. Drupal.jsAC.prototype.highlight = function (node) {
  134. if (this.selected) {
  135. $(this.selected).removeClass('selected');
  136. }
  137. $(node).addClass('selected');
  138. this.selected = node;
  139. $(this.ariaLive).html($(this.selected).html());
  140. };
  141. /**
  142. * Unhighlights a suggestion.
  143. */
  144. Drupal.jsAC.prototype.unhighlight = function (node) {
  145. $(node).removeClass('selected');
  146. this.selected = false;
  147. $(this.ariaLive).empty();
  148. };
  149. /**
  150. * Hides the autocomplete suggestions.
  151. */
  152. Drupal.jsAC.prototype.hidePopup = function (keycode) {
  153. // Select item if the right key or mousebutton was pressed.
  154. if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
  155. this.input.value = $(this.selected).data('autocompleteValue');
  156. }
  157. // Hide popup.
  158. var popup = this.popup;
  159. if (popup) {
  160. this.popup = null;
  161. $(popup).fadeOut('fast', function () { $(popup).remove(); });
  162. }
  163. this.selected = false;
  164. $(this.ariaLive).empty();
  165. };
  166. /**
  167. * Positions the suggestions popup and starts a search.
  168. */
  169. Drupal.jsAC.prototype.populatePopup = function () {
  170. var $input = $(this.input);
  171. var position = $input.position();
  172. // Show popup.
  173. if (this.popup) {
  174. $(this.popup).remove();
  175. }
  176. this.selected = false;
  177. this.popup = $('<div id="autocomplete"></div>')[0];
  178. this.popup.owner = this;
  179. $(this.popup).css({
  180. top: parseInt(position.top + this.input.offsetHeight, 10) + 'px',
  181. left: parseInt(position.left, 10) + 'px',
  182. width: $input.innerWidth() + 'px',
  183. display: 'none'
  184. });
  185. $input.before(this.popup);
  186. // Do search.
  187. this.db.owner = this;
  188. this.db.search(this.input.value);
  189. };
  190. /**
  191. * Fills the suggestion popup with any matches received.
  192. */
  193. Drupal.jsAC.prototype.found = function (matches) {
  194. // If no value in the textfield, do not show the popup.
  195. if (!this.input.value.length) {
  196. return false;
  197. }
  198. // Prepare matches.
  199. var ul = $('<ul></ul>');
  200. var ac = this;
  201. for (key in matches) {
  202. $('<li></li>')
  203. .html($('<div></div>').html(matches[key]))
  204. .mousedown(function () { ac.select(this); })
  205. .mouseover(function () { ac.highlight(this); })
  206. .mouseout(function () { ac.unhighlight(this); })
  207. .data('autocompleteValue', key)
  208. .appendTo(ul);
  209. }
  210. // Show popup with matches, if any.
  211. if (this.popup) {
  212. if (ul.children().length) {
  213. $(this.popup).empty().append(ul).show();
  214. $(this.ariaLive).html(Drupal.t('Autocomplete popup'));
  215. }
  216. else {
  217. $(this.popup).css({ visibility: 'hidden' });
  218. this.hidePopup();
  219. }
  220. }
  221. };
  222. Drupal.jsAC.prototype.setStatus = function (status) {
  223. switch (status) {
  224. case 'begin':
  225. $(this.input).addClass('throbbing');
  226. $(this.ariaLive).html(Drupal.t('Searching for matches...'));
  227. break;
  228. case 'cancel':
  229. case 'error':
  230. case 'found':
  231. $(this.input).removeClass('throbbing');
  232. break;
  233. }
  234. };
  235. /**
  236. * An AutoComplete DataBase object.
  237. */
  238. Drupal.ACDB = function (uri) {
  239. this.uri = uri;
  240. this.delay = 300;
  241. this.cache = {};
  242. };
  243. /**
  244. * Performs a cached and delayed search.
  245. */
  246. Drupal.ACDB.prototype.search = function (searchString) {
  247. var db = this;
  248. this.searchString = searchString;
  249. // See if this string needs to be searched for anyway.
  250. searchString = searchString.replace(/^\s+|\s+$/, '');
  251. if (searchString.length <= 0 ||
  252. searchString.charAt(searchString.length - 1) == ',') {
  253. return;
  254. }
  255. // See if this key has been searched for before.
  256. if (this.cache[searchString]) {
  257. return this.owner.found(this.cache[searchString]);
  258. }
  259. // Initiate delayed search.
  260. if (this.timer) {
  261. clearTimeout(this.timer);
  262. }
  263. this.timer = setTimeout(function () {
  264. db.owner.setStatus('begin');
  265. // Ajax GET request for autocompletion. We use Drupal.encodePath instead of
  266. // encodeURIComponent to allow autocomplete search terms to contain slashes.
  267. $.ajax({
  268. type: 'GET',
  269. url: db.uri + '/' + Drupal.encodePath(searchString),
  270. dataType: 'json',
  271. success: function (matches) {
  272. if (typeof matches.status == 'undefined' || matches.status != 0) {
  273. db.cache[searchString] = matches;
  274. // Verify if these are still the matches the user wants to see.
  275. if (db.searchString == searchString) {
  276. db.owner.found(matches);
  277. }
  278. db.owner.setStatus('found');
  279. }
  280. },
  281. error: function (xmlhttp) {
  282. alert(Drupal.ajaxError(xmlhttp, db.uri));
  283. }
  284. });
  285. }, this.delay);
  286. };
  287. /**
  288. * Cancels the current autocomplete request.
  289. */
  290. Drupal.ACDB.prototype.cancel = function () {
  291. if (this.owner) this.owner.setStatus('cancel');
  292. if (this.timer) clearTimeout(this.timer);
  293. this.searchString = '';
  294. };
  295. })(jQuery);