jquery.ui.autocomplete.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. /*
  2. * jQuery UI Autocomplete 1.8.17
  3. *
  4. * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
  5. * Dual licensed under the MIT or GPL Version 2 licenses.
  6. * http://jquery.org/license
  7. *
  8. * http://docs.jquery.com/UI/Autocomplete
  9. *
  10. * Depends:
  11. * jquery.ui.core.js
  12. * jquery.ui.widget.js
  13. * jquery.ui.position.js
  14. */
  15. (function( $, undefined ) {
  16. // used to prevent race conditions with remote data sources
  17. var requestIndex = 0;
  18. $.widget( "ui.autocomplete", {
  19. options: {
  20. appendTo: "body",
  21. autoFocus: false,
  22. delay: 300,
  23. minLength: 1,
  24. position: {
  25. my: "left top",
  26. at: "left bottom",
  27. collision: "none"
  28. },
  29. source: null
  30. },
  31. pending: 0,
  32. _create: function() {
  33. var self = this,
  34. doc = this.element[ 0 ].ownerDocument,
  35. suppressKeyPress;
  36. this.element
  37. .addClass( "ui-autocomplete-input" )
  38. .attr( "autocomplete", "off" )
  39. // TODO verify these actually work as intended
  40. .attr({
  41. role: "textbox",
  42. "aria-autocomplete": "list",
  43. "aria-haspopup": "true"
  44. })
  45. .bind( "keydown.autocomplete", function( event ) {
  46. if ( self.options.disabled || self.element.propAttr( "readOnly" ) ) {
  47. return;
  48. }
  49. suppressKeyPress = false;
  50. var keyCode = $.ui.keyCode;
  51. switch( event.keyCode ) {
  52. case keyCode.PAGE_UP:
  53. self._move( "previousPage", event );
  54. break;
  55. case keyCode.PAGE_DOWN:
  56. self._move( "nextPage", event );
  57. break;
  58. case keyCode.UP:
  59. self._move( "previous", event );
  60. // prevent moving cursor to beginning of text field in some browsers
  61. event.preventDefault();
  62. break;
  63. case keyCode.DOWN:
  64. self._move( "next", event );
  65. // prevent moving cursor to end of text field in some browsers
  66. event.preventDefault();
  67. break;
  68. case keyCode.ENTER:
  69. case keyCode.NUMPAD_ENTER:
  70. // when menu is open and has focus
  71. if ( self.menu.active ) {
  72. // #6055 - Opera still allows the keypress to occur
  73. // which causes forms to submit
  74. suppressKeyPress = true;
  75. event.preventDefault();
  76. }
  77. //passthrough - ENTER and TAB both select the current element
  78. case keyCode.TAB:
  79. if ( !self.menu.active ) {
  80. return;
  81. }
  82. self.menu.select( event );
  83. break;
  84. case keyCode.ESCAPE:
  85. self.element.val( self.term );
  86. self.close( event );
  87. break;
  88. default:
  89. // keypress is triggered before the input value is changed
  90. clearTimeout( self.searching );
  91. self.searching = setTimeout(function() {
  92. // only search if the value has changed
  93. if ( self.term != self.element.val() ) {
  94. self.selectedItem = null;
  95. self.search( null, event );
  96. }
  97. }, self.options.delay );
  98. break;
  99. }
  100. })
  101. .bind( "keypress.autocomplete", function( event ) {
  102. if ( suppressKeyPress ) {
  103. suppressKeyPress = false;
  104. event.preventDefault();
  105. }
  106. })
  107. .bind( "focus.autocomplete", function() {
  108. if ( self.options.disabled ) {
  109. return;
  110. }
  111. self.selectedItem = null;
  112. self.previous = self.element.val();
  113. })
  114. .bind( "blur.autocomplete", function( event ) {
  115. if ( self.options.disabled ) {
  116. return;
  117. }
  118. clearTimeout( self.searching );
  119. // clicks on the menu (or a button to trigger a search) will cause a blur event
  120. self.closing = setTimeout(function() {
  121. self.close( event );
  122. self._change( event );
  123. }, 150 );
  124. });
  125. this._initSource();
  126. this.response = function() {
  127. return self._response.apply( self, arguments );
  128. };
  129. this.menu = $( "<ul></ul>" )
  130. .addClass( "ui-autocomplete" )
  131. .appendTo( $( this.options.appendTo || "body", doc )[0] )
  132. // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
  133. .mousedown(function( event ) {
  134. // clicking on the scrollbar causes focus to shift to the body
  135. // but we can't detect a mouseup or a click immediately afterward
  136. // so we have to track the next mousedown and close the menu if
  137. // the user clicks somewhere outside of the autocomplete
  138. var menuElement = self.menu.element[ 0 ];
  139. if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
  140. setTimeout(function() {
  141. $( document ).one( 'mousedown', function( event ) {
  142. if ( event.target !== self.element[ 0 ] &&
  143. event.target !== menuElement &&
  144. !$.ui.contains( menuElement, event.target ) ) {
  145. self.close();
  146. }
  147. });
  148. }, 1 );
  149. }
  150. // use another timeout to make sure the blur-event-handler on the input was already triggered
  151. setTimeout(function() {
  152. clearTimeout( self.closing );
  153. }, 13);
  154. })
  155. .menu({
  156. focus: function( event, ui ) {
  157. var item = ui.item.data( "item.autocomplete" );
  158. if ( false !== self._trigger( "focus", event, { item: item } ) ) {
  159. // use value to match what will end up in the input, if it was a key event
  160. if ( /^key/.test(event.originalEvent.type) ) {
  161. self.element.val( item.value );
  162. }
  163. }
  164. },
  165. selected: function( event, ui ) {
  166. var item = ui.item.data( "item.autocomplete" ),
  167. previous = self.previous;
  168. // only trigger when focus was lost (click on menu)
  169. if ( self.element[0] !== doc.activeElement ) {
  170. self.element.focus();
  171. self.previous = previous;
  172. // #6109 - IE triggers two focus events and the second
  173. // is asynchronous, so we need to reset the previous
  174. // term synchronously and asynchronously :-(
  175. setTimeout(function() {
  176. self.previous = previous;
  177. self.selectedItem = item;
  178. }, 1);
  179. }
  180. if ( false !== self._trigger( "select", event, { item: item } ) ) {
  181. self.element.val( item.value );
  182. }
  183. // reset the term after the select event
  184. // this allows custom select handling to work properly
  185. self.term = self.element.val();
  186. self.close( event );
  187. self.selectedItem = item;
  188. },
  189. blur: function( event, ui ) {
  190. // don't set the value of the text field if it's already correct
  191. // this prevents moving the cursor unnecessarily
  192. if ( self.menu.element.is(":visible") &&
  193. ( self.element.val() !== self.term ) ) {
  194. self.element.val( self.term );
  195. }
  196. }
  197. })
  198. .zIndex( this.element.zIndex() + 1 )
  199. // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
  200. .css({ top: 0, left: 0 })
  201. .hide()
  202. .data( "menu" );
  203. if ( $.fn.bgiframe ) {
  204. this.menu.element.bgiframe();
  205. }
  206. // turning off autocomplete prevents the browser from remembering the
  207. // value when navigating through history, so we re-enable autocomplete
  208. // if the page is unloaded before the widget is destroyed. #7790
  209. self.beforeunloadHandler = function() {
  210. self.element.removeAttr( "autocomplete" );
  211. };
  212. $( window ).bind( "beforeunload", self.beforeunloadHandler );
  213. },
  214. destroy: function() {
  215. this.element
  216. .removeClass( "ui-autocomplete-input" )
  217. .removeAttr( "autocomplete" )
  218. .removeAttr( "role" )
  219. .removeAttr( "aria-autocomplete" )
  220. .removeAttr( "aria-haspopup" );
  221. this.menu.element.remove();
  222. $( window ).unbind( "beforeunload", this.beforeunloadHandler );
  223. $.Widget.prototype.destroy.call( this );
  224. },
  225. _setOption: function( key, value ) {
  226. $.Widget.prototype._setOption.apply( this, arguments );
  227. if ( key === "source" ) {
  228. this._initSource();
  229. }
  230. if ( key === "appendTo" ) {
  231. this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
  232. }
  233. if ( key === "disabled" && value && this.xhr ) {
  234. this.xhr.abort();
  235. }
  236. },
  237. _initSource: function() {
  238. var self = this,
  239. array,
  240. url;
  241. if ( $.isArray(this.options.source) ) {
  242. array = this.options.source;
  243. this.source = function( request, response ) {
  244. response( $.ui.autocomplete.filter(array, request.term) );
  245. };
  246. } else if ( typeof this.options.source === "string" ) {
  247. url = this.options.source;
  248. this.source = function( request, response ) {
  249. if ( self.xhr ) {
  250. self.xhr.abort();
  251. }
  252. self.xhr = $.ajax({
  253. url: url,
  254. data: request,
  255. dataType: "json",
  256. autocompleteRequest: ++requestIndex,
  257. success: function( data, status ) {
  258. if ( this.autocompleteRequest === requestIndex ) {
  259. response( data );
  260. }
  261. },
  262. error: function() {
  263. if ( this.autocompleteRequest === requestIndex ) {
  264. response( [] );
  265. }
  266. }
  267. });
  268. };
  269. } else {
  270. this.source = this.options.source;
  271. }
  272. },
  273. search: function( value, event ) {
  274. value = value != null ? value : this.element.val();
  275. // always save the actual value, not the one passed as an argument
  276. this.term = this.element.val();
  277. if ( value.length < this.options.minLength ) {
  278. return this.close( event );
  279. }
  280. clearTimeout( this.closing );
  281. if ( this._trigger( "search", event ) === false ) {
  282. return;
  283. }
  284. return this._search( value );
  285. },
  286. _search: function( value ) {
  287. this.pending++;
  288. this.element.addClass( "ui-autocomplete-loading" );
  289. this.source( { term: value }, this.response );
  290. },
  291. _response: function( content ) {
  292. if ( !this.options.disabled && content && content.length ) {
  293. content = this._normalize( content );
  294. this._suggest( content );
  295. this._trigger( "open" );
  296. } else {
  297. this.close();
  298. }
  299. this.pending--;
  300. if ( !this.pending ) {
  301. this.element.removeClass( "ui-autocomplete-loading" );
  302. }
  303. },
  304. close: function( event ) {
  305. clearTimeout( this.closing );
  306. if ( this.menu.element.is(":visible") ) {
  307. this.menu.element.hide();
  308. this.menu.deactivate();
  309. this._trigger( "close", event );
  310. }
  311. },
  312. _change: function( event ) {
  313. if ( this.previous !== this.element.val() ) {
  314. this._trigger( "change", event, { item: this.selectedItem } );
  315. }
  316. },
  317. _normalize: function( items ) {
  318. // assume all items have the right format when the first item is complete
  319. if ( items.length && items[0].label && items[0].value ) {
  320. return items;
  321. }
  322. return $.map( items, function(item) {
  323. if ( typeof item === "string" ) {
  324. return {
  325. label: item,
  326. value: item
  327. };
  328. }
  329. return $.extend({
  330. label: item.label || item.value,
  331. value: item.value || item.label
  332. }, item );
  333. });
  334. },
  335. _suggest: function( items ) {
  336. var ul = this.menu.element
  337. .empty()
  338. .zIndex( this.element.zIndex() + 1 );
  339. this._renderMenu( ul, items );
  340. // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
  341. this.menu.deactivate();
  342. this.menu.refresh();
  343. // size and position menu
  344. ul.show();
  345. this._resizeMenu();
  346. ul.position( $.extend({
  347. of: this.element
  348. }, this.options.position ));
  349. if ( this.options.autoFocus ) {
  350. this.menu.next( new $.Event("mouseover") );
  351. }
  352. },
  353. _resizeMenu: function() {
  354. var ul = this.menu.element;
  355. ul.outerWidth( Math.max(
  356. // Firefox wraps long text (possibly a rounding bug)
  357. // so we add 1px to avoid the wrapping (#7513)
  358. ul.width( "" ).outerWidth() + 1,
  359. this.element.outerWidth()
  360. ) );
  361. },
  362. _renderMenu: function( ul, items ) {
  363. var self = this;
  364. $.each( items, function( index, item ) {
  365. self._renderItem( ul, item );
  366. });
  367. },
  368. _renderItem: function( ul, item) {
  369. return $( "<li></li>" )
  370. .data( "item.autocomplete", item )
  371. .append( $( "<a></a>" ).text( item.label ) )
  372. .appendTo( ul );
  373. },
  374. _move: function( direction, event ) {
  375. if ( !this.menu.element.is(":visible") ) {
  376. this.search( null, event );
  377. return;
  378. }
  379. if ( this.menu.first() && /^previous/.test(direction) ||
  380. this.menu.last() && /^next/.test(direction) ) {
  381. this.element.val( this.term );
  382. this.menu.deactivate();
  383. return;
  384. }
  385. this.menu[ direction ]( event );
  386. },
  387. widget: function() {
  388. return this.menu.element;
  389. }
  390. });
  391. $.extend( $.ui.autocomplete, {
  392. escapeRegex: function( value ) {
  393. return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
  394. },
  395. filter: function(array, term) {
  396. var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
  397. return $.grep( array, function(value) {
  398. return matcher.test( value.label || value.value || value );
  399. });
  400. }
  401. });
  402. }( jQuery ));
  403. /*
  404. * jQuery UI Menu (not officially released)
  405. *
  406. * This widget isn't yet finished and the API is subject to change. We plan to finish
  407. * it for the next release. You're welcome to give it a try anyway and give us feedback,
  408. * as long as you're okay with migrating your code later on. We can help with that, too.
  409. *
  410. * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
  411. * Dual licensed under the MIT or GPL Version 2 licenses.
  412. * http://jquery.org/license
  413. *
  414. * http://docs.jquery.com/UI/Menu
  415. *
  416. * Depends:
  417. * jquery.ui.core.js
  418. * jquery.ui.widget.js
  419. */
  420. (function($) {
  421. $.widget("ui.menu", {
  422. _create: function() {
  423. var self = this;
  424. this.element
  425. .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
  426. .attr({
  427. role: "listbox",
  428. "aria-activedescendant": "ui-active-menuitem"
  429. })
  430. .click(function( event ) {
  431. if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
  432. return;
  433. }
  434. // temporary
  435. event.preventDefault();
  436. self.select( event );
  437. });
  438. this.refresh();
  439. },
  440. refresh: function() {
  441. var self = this;
  442. // don't refresh list items that are already adapted
  443. var items = this.element.children("li:not(.ui-menu-item):has(a)")
  444. .addClass("ui-menu-item")
  445. .attr("role", "menuitem");
  446. items.children("a")
  447. .addClass("ui-corner-all")
  448. .attr("tabindex", -1)
  449. // mouseenter doesn't work with event delegation
  450. .mouseenter(function( event ) {
  451. self.activate( event, $(this).parent() );
  452. })
  453. .mouseleave(function() {
  454. self.deactivate();
  455. });
  456. },
  457. activate: function( event, item ) {
  458. this.deactivate();
  459. if (this.hasScroll()) {
  460. var offset = item.offset().top - this.element.offset().top,
  461. scroll = this.element.scrollTop(),
  462. elementHeight = this.element.height();
  463. if (offset < 0) {
  464. this.element.scrollTop( scroll + offset);
  465. } else if (offset >= elementHeight) {
  466. this.element.scrollTop( scroll + offset - elementHeight + item.height());
  467. }
  468. }
  469. this.active = item.eq(0)
  470. .children("a")
  471. .addClass("ui-state-hover")
  472. .attr("id", "ui-active-menuitem")
  473. .end();
  474. this._trigger("focus", event, { item: item });
  475. },
  476. deactivate: function() {
  477. if (!this.active) { return; }
  478. this.active.children("a")
  479. .removeClass("ui-state-hover")
  480. .removeAttr("id");
  481. this._trigger("blur");
  482. this.active = null;
  483. },
  484. next: function(event) {
  485. this.move("next", ".ui-menu-item:first", event);
  486. },
  487. previous: function(event) {
  488. this.move("prev", ".ui-menu-item:last", event);
  489. },
  490. first: function() {
  491. return this.active && !this.active.prevAll(".ui-menu-item").length;
  492. },
  493. last: function() {
  494. return this.active && !this.active.nextAll(".ui-menu-item").length;
  495. },
  496. move: function(direction, edge, event) {
  497. if (!this.active) {
  498. this.activate(event, this.element.children(edge));
  499. return;
  500. }
  501. var next = this.active[direction + "All"](".ui-menu-item").eq(0);
  502. if (next.length) {
  503. this.activate(event, next);
  504. } else {
  505. this.activate(event, this.element.children(edge));
  506. }
  507. },
  508. // TODO merge with previousPage
  509. nextPage: function(event) {
  510. if (this.hasScroll()) {
  511. // TODO merge with no-scroll-else
  512. if (!this.active || this.last()) {
  513. this.activate(event, this.element.children(".ui-menu-item:first"));
  514. return;
  515. }
  516. var base = this.active.offset().top,
  517. height = this.element.height(),
  518. result = this.element.children(".ui-menu-item").filter(function() {
  519. var close = $(this).offset().top - base - height + $(this).height();
  520. // TODO improve approximation
  521. return close < 10 && close > -10;
  522. });
  523. // TODO try to catch this earlier when scrollTop indicates the last page anyway
  524. if (!result.length) {
  525. result = this.element.children(".ui-menu-item:last");
  526. }
  527. this.activate(event, result);
  528. } else {
  529. this.activate(event, this.element.children(".ui-menu-item")
  530. .filter(!this.active || this.last() ? ":first" : ":last"));
  531. }
  532. },
  533. // TODO merge with nextPage
  534. previousPage: function(event) {
  535. if (this.hasScroll()) {
  536. // TODO merge with no-scroll-else
  537. if (!this.active || this.first()) {
  538. this.activate(event, this.element.children(".ui-menu-item:last"));
  539. return;
  540. }
  541. var base = this.active.offset().top,
  542. height = this.element.height();
  543. result = this.element.children(".ui-menu-item").filter(function() {
  544. var close = $(this).offset().top - base + height - $(this).height();
  545. // TODO improve approximation
  546. return close < 10 && close > -10;
  547. });
  548. // TODO try to catch this earlier when scrollTop indicates the last page anyway
  549. if (!result.length) {
  550. result = this.element.children(".ui-menu-item:first");
  551. }
  552. this.activate(event, result);
  553. } else {
  554. this.activate(event, this.element.children(".ui-menu-item")
  555. .filter(!this.active || this.first() ? ":last" : ":first"));
  556. }
  557. },
  558. hasScroll: function() {
  559. return this.element.height() < this.element[ $.fn.prop ? "prop" : "attr" ]("scrollHeight");
  560. },
  561. select: function( event ) {
  562. this._trigger("selected", event, { item: this.active });
  563. }
  564. });
  565. }(jQuery));