jquery.ui.autocomplete.js 17 KB

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