jquery.ui.autocomplete.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. /*!
  2. * jQuery UI Autocomplete v1.9 stable
  3. * http://jqueryui.com
  4. *
  5. * Copyright 2012 jQuery Foundation and other contributors
  6. * Released under the MIT license.
  7. * http://jquery.org/license
  8. *
  9. * http://api.jqueryui.com/autocomplete/
  10. *
  11. * Depends:
  12. * jquery.ui.core.js
  13. * jquery.ui.widget.js
  14. * jquery.ui.position.js
  15. * jquery.ui.menu.js
  16. */
  17. (function( $, undefined ) {
  18. // used to prevent race conditions with remote data sources
  19. var requestIndex = 0;
  20. $.widget( "ui.autocomplete", {
  21. version: "@VERSION",
  22. defaultElement: "<input>",
  23. options: {
  24. appendTo: "body",
  25. autoFocus: false,
  26. delay: 300,
  27. minLength: 1,
  28. position: {
  29. my: "left top",
  30. at: "left bottom",
  31. collision: "none"
  32. },
  33. source: null,
  34. // callbacks
  35. change: null,
  36. close: null,
  37. focus: null,
  38. open: null,
  39. response: null,
  40. search: null,
  41. select: null
  42. },
  43. pending: 0,
  44. _create: function() {
  45. // Some browsers only repeat keydown events, not keypress events,
  46. // so we use the suppressKeyPress flag to determine if we've already
  47. // handled the keydown event. #7269
  48. // Unfortunately the code for & in keypress is the same as the up arrow,
  49. // so we use the suppressKeyPressRepeat flag to avoid handling keypress
  50. // events when we know the keydown event was used to modify the
  51. // search term. #7799
  52. var suppressKeyPress, suppressKeyPressRepeat, suppressInput;
  53. this.isMultiLine = this._isMultiLine();
  54. this.valueMethod = this.element[ this.element.is( "input,textarea" ) ? "val" : "text" ];
  55. this.isNewMenu = true;
  56. this.element
  57. .addClass( "ui-autocomplete-input" )
  58. .attr( "autocomplete", "off" );
  59. this._on( this.element, {
  60. keydown: function( event ) {
  61. if ( this.element.prop( "readOnly" ) ) {
  62. suppressKeyPress = true;
  63. suppressInput = true;
  64. suppressKeyPressRepeat = true;
  65. return;
  66. }
  67. suppressKeyPress = false;
  68. suppressInput = false;
  69. suppressKeyPressRepeat = false;
  70. var keyCode = $.ui.keyCode;
  71. switch( event.keyCode ) {
  72. case keyCode.PAGE_UP:
  73. suppressKeyPress = true;
  74. this._move( "previousPage", event );
  75. break;
  76. case keyCode.PAGE_DOWN:
  77. suppressKeyPress = true;
  78. this._move( "nextPage", event );
  79. break;
  80. case keyCode.UP:
  81. suppressKeyPress = true;
  82. this._keyEvent( "previous", event );
  83. break;
  84. case keyCode.DOWN:
  85. suppressKeyPress = true;
  86. this._keyEvent( "next", event );
  87. break;
  88. case keyCode.ENTER:
  89. case keyCode.NUMPAD_ENTER:
  90. // when menu is open and has focus
  91. if ( this.menu.active ) {
  92. // #6055 - Opera still allows the keypress to occur
  93. // which causes forms to submit
  94. suppressKeyPress = true;
  95. event.preventDefault();
  96. this.menu.select( event );
  97. }
  98. break;
  99. case keyCode.TAB:
  100. if ( this.menu.active ) {
  101. this.menu.select( event );
  102. }
  103. break;
  104. case keyCode.ESCAPE:
  105. if ( this.menu.element.is( ":visible" ) ) {
  106. this._value( this.term );
  107. this.close( event );
  108. // Different browsers have different default behavior for escape
  109. // Single press can mean undo or clear
  110. // Double press in IE means clear the whole form
  111. event.preventDefault();
  112. }
  113. break;
  114. default:
  115. suppressKeyPressRepeat = true;
  116. // search timeout should be triggered before the input value is changed
  117. this._searchTimeout( event );
  118. break;
  119. }
  120. },
  121. keypress: function( event ) {
  122. if ( suppressKeyPress ) {
  123. suppressKeyPress = false;
  124. event.preventDefault();
  125. return;
  126. }
  127. if ( suppressKeyPressRepeat ) {
  128. return;
  129. }
  130. // replicate some key handlers to allow them to repeat in Firefox and Opera
  131. var keyCode = $.ui.keyCode;
  132. switch( event.keyCode ) {
  133. case keyCode.PAGE_UP:
  134. this._move( "previousPage", event );
  135. break;
  136. case keyCode.PAGE_DOWN:
  137. this._move( "nextPage", event );
  138. break;
  139. case keyCode.UP:
  140. this._keyEvent( "previous", event );
  141. break;
  142. case keyCode.DOWN:
  143. this._keyEvent( "next", event );
  144. break;
  145. }
  146. },
  147. input: function( event ) {
  148. if ( suppressInput ) {
  149. suppressInput = false;
  150. event.preventDefault();
  151. return;
  152. }
  153. this._searchTimeout( event );
  154. },
  155. focus: function() {
  156. this.selectedItem = null;
  157. this.previous = this._value();
  158. },
  159. blur: function( event ) {
  160. if ( this.cancelBlur ) {
  161. delete this.cancelBlur;
  162. return;
  163. }
  164. clearTimeout( this.searching );
  165. this.close( event );
  166. this._change( event );
  167. }
  168. });
  169. this._initSource();
  170. this.menu = $( "<ul>" )
  171. .addClass( "ui-autocomplete" )
  172. .appendTo( this.document.find( this.options.appendTo || "body" )[ 0 ] )
  173. .menu({
  174. // custom key handling for now
  175. input: $(),
  176. // disable ARIA support, the live region takes care of that
  177. role: null
  178. })
  179. .zIndex( this.element.zIndex() + 1 )
  180. .hide()
  181. .data( "menu" );
  182. this._on( this.menu.element, {
  183. mousedown: function( event ) {
  184. // prevent moving focus out of the text field
  185. event.preventDefault();
  186. // IE doesn't prevent moving focus even with event.preventDefault()
  187. // so we set a flag to know when we should ignore the blur event
  188. this.cancelBlur = true;
  189. this._delay(function() {
  190. delete this.cancelBlur;
  191. });
  192. // clicking on the scrollbar causes focus to shift to the body
  193. // but we can't detect a mouseup or a click immediately afterward
  194. // so we have to track the next mousedown and close the menu if
  195. // the user clicks somewhere outside of the autocomplete
  196. var menuElement = this.menu.element[ 0 ];
  197. if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
  198. this._delay(function() {
  199. var that = this;
  200. this.document.one( "mousedown", function( event ) {
  201. if ( event.target !== that.element[ 0 ] &&
  202. event.target !== menuElement &&
  203. !$.contains( menuElement, event.target ) ) {
  204. that.close();
  205. }
  206. });
  207. });
  208. }
  209. },
  210. menufocus: function( event, ui ) {
  211. // #7024 - Prevent accidental activation of menu items in Firefox
  212. if ( this.isNewMenu ) {
  213. this.isNewMenu = false;
  214. if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
  215. this.menu.blur();
  216. this.document.one( "mousemove", function() {
  217. $( event.target ).trigger( event.originalEvent );
  218. });
  219. return;
  220. }
  221. }
  222. // back compat for _renderItem using item.autocomplete, via #7810
  223. // TODO remove the fallback, see #8156
  224. var item = ui.item.data( "ui-autocomplete-item" ) || ui.item.data( "item.autocomplete" );
  225. if ( false !== this._trigger( "focus", event, { item: item } ) ) {
  226. // use value to match what will end up in the input, if it was a key event
  227. if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
  228. this._value( item.value );
  229. }
  230. } else {
  231. // Normally the input is populated with the item's value as the
  232. // menu is navigated, causing screen readers to notice a change and
  233. // announce the item. Since the focus event was canceled, this doesn't
  234. // happen, so we update the live region so that screen readers can
  235. // still notice the change and announce it.
  236. this.liveRegion.text( item.value );
  237. }
  238. },
  239. menuselect: function( event, ui ) {
  240. // back compat for _renderItem using item.autocomplete, via #7810
  241. // TODO remove the fallback, see #8156
  242. var item = ui.item.data( "ui-autocomplete-item" ) || ui.item.data( "item.autocomplete" ),
  243. previous = this.previous;
  244. // only trigger when focus was lost (click on menu)
  245. if ( this.element[0] !== this.document[0].activeElement ) {
  246. this.element.focus();
  247. this.previous = previous;
  248. // #6109 - IE triggers two focus events and the second
  249. // is asynchronous, so we need to reset the previous
  250. // term synchronously and asynchronously :-(
  251. this._delay(function() {
  252. this.previous = previous;
  253. this.selectedItem = item;
  254. });
  255. }
  256. if ( false !== this._trigger( "select", event, { item: item } ) ) {
  257. this._value( item.value );
  258. }
  259. // reset the term after the select event
  260. // this allows custom select handling to work properly
  261. this.term = this._value();
  262. this.close( event );
  263. this.selectedItem = item;
  264. }
  265. });
  266. this.liveRegion = $( "<span>", {
  267. role: "status",
  268. "aria-live": "polite"
  269. })
  270. .addClass( "ui-helper-hidden-accessible" )
  271. .insertAfter( this.element );
  272. if ( $.fn.bgiframe ) {
  273. this.menu.element.bgiframe();
  274. }
  275. // turning off autocomplete prevents the browser from remembering the
  276. // value when navigating through history, so we re-enable autocomplete
  277. // if the page is unloaded before the widget is destroyed. #7790
  278. this._on( this.window, {
  279. beforeunload: function() {
  280. this.element.removeAttr( "autocomplete" );
  281. }
  282. });
  283. },
  284. _destroy: function() {
  285. clearTimeout( this.searching );
  286. this.element
  287. .removeClass( "ui-autocomplete-input" )
  288. .removeAttr( "autocomplete" );
  289. this.menu.element.remove();
  290. this.liveRegion.remove();
  291. },
  292. _setOption: function( key, value ) {
  293. this._super( key, value );
  294. if ( key === "source" ) {
  295. this._initSource();
  296. }
  297. if ( key === "appendTo" ) {
  298. this.menu.element.appendTo( this.document.find( value || "body" )[0] );
  299. }
  300. if ( key === "disabled" && value && this.xhr ) {
  301. this.xhr.abort();
  302. }
  303. },
  304. _isMultiLine: function() {
  305. // Textareas are always multi-line
  306. if ( this.element.is( "textarea" ) ) {
  307. return true;
  308. }
  309. // Inputs are always single-line, even if inside a contentEditable element
  310. // IE also treats inputs as contentEditable
  311. if ( this.element.is( "input" ) ) {
  312. return false;
  313. }
  314. // All other element types are determined by whether or not they're contentEditable
  315. return this.element.prop( "isContentEditable" );
  316. },
  317. _initSource: function() {
  318. var array, url,
  319. that = this;
  320. if ( $.isArray(this.options.source) ) {
  321. array = this.options.source;
  322. this.source = function( request, response ) {
  323. response( $.ui.autocomplete.filter( array, request.term ) );
  324. };
  325. } else if ( typeof this.options.source === "string" ) {
  326. url = this.options.source;
  327. this.source = function( request, response ) {
  328. if ( that.xhr ) {
  329. that.xhr.abort();
  330. }
  331. that.xhr = $.ajax({
  332. url: url,
  333. data: request,
  334. dataType: "json",
  335. success: function( data ) {
  336. response( data );
  337. },
  338. error: function() {
  339. response( [] );
  340. }
  341. });
  342. };
  343. } else {
  344. this.source = this.options.source;
  345. }
  346. },
  347. _searchTimeout: function( event ) {
  348. clearTimeout( this.searching );
  349. this.searching = this._delay(function() {
  350. // only search if the value has changed
  351. if ( this.term !== this._value() ) {
  352. this.selectedItem = null;
  353. this.search( null, event );
  354. }
  355. }, this.options.delay );
  356. },
  357. search: function( value, event ) {
  358. value = value != null ? value : this._value();
  359. // always save the actual value, not the one passed as an argument
  360. this.term = this._value();
  361. if ( value.length < this.options.minLength ) {
  362. return this.close( event );
  363. }
  364. if ( this._trigger( "search", event ) === false ) {
  365. return;
  366. }
  367. return this._search( value );
  368. },
  369. _search: function( value ) {
  370. this.pending++;
  371. this.element.addClass( "ui-autocomplete-loading" );
  372. this.cancelSearch = false;
  373. this.source( { term: value }, this._response() );
  374. },
  375. _response: function() {
  376. var that = this,
  377. index = ++requestIndex;
  378. return function( content ) {
  379. if ( index === requestIndex ) {
  380. that.__response( content );
  381. }
  382. that.pending--;
  383. if ( !that.pending ) {
  384. that.element.removeClass( "ui-autocomplete-loading" );
  385. }
  386. };
  387. },
  388. __response: function( content ) {
  389. if ( content ) {
  390. content = this._normalize( content );
  391. }
  392. this._trigger( "response", null, { content: content } );
  393. if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
  394. this._suggest( content );
  395. this._trigger( "open" );
  396. } else {
  397. // use ._close() instead of .close() so we don't cancel future searches
  398. this._close();
  399. }
  400. },
  401. close: function( event ) {
  402. this.cancelSearch = true;
  403. this._close( event );
  404. },
  405. _close: function( event ) {
  406. if ( this.menu.element.is( ":visible" ) ) {
  407. this.menu.element.hide();
  408. this.menu.blur();
  409. this.isNewMenu = true;
  410. this._trigger( "close", event );
  411. }
  412. },
  413. _change: function( event ) {
  414. if ( this.previous !== this._value() ) {
  415. this._trigger( "change", event, { item: this.selectedItem } );
  416. }
  417. },
  418. _normalize: function( items ) {
  419. // assume all items have the right format when the first item is complete
  420. if ( items.length && items[0].label && items[0].value ) {
  421. return items;
  422. }
  423. return $.map( items, function( item ) {
  424. if ( typeof item === "string" ) {
  425. return {
  426. label: item,
  427. value: item
  428. };
  429. }
  430. return $.extend({
  431. label: item.label || item.value,
  432. value: item.value || item.label
  433. }, item );
  434. });
  435. },
  436. _suggest: function( items ) {
  437. var ul = this.menu.element
  438. .empty()
  439. .zIndex( this.element.zIndex() + 1 );
  440. this._renderMenu( ul, items );
  441. this.menu.refresh();
  442. // size and position menu
  443. ul.show();
  444. this._resizeMenu();
  445. ul.position( $.extend({
  446. of: this.element
  447. }, this.options.position ));
  448. if ( this.options.autoFocus ) {
  449. this.menu.next();
  450. }
  451. },
  452. _resizeMenu: function() {
  453. var ul = this.menu.element;
  454. ul.outerWidth( Math.max(
  455. // Firefox wraps long text (possibly a rounding bug)
  456. // so we add 1px to avoid the wrapping (#7513)
  457. ul.width( "" ).outerWidth() + 1,
  458. this.element.outerWidth()
  459. ) );
  460. },
  461. _renderMenu: function( ul, items ) {
  462. var that = this;
  463. $.each( items, function( index, item ) {
  464. that._renderItemData( ul, item );
  465. });
  466. },
  467. _renderItemData: function( ul, item ) {
  468. return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
  469. },
  470. _renderItem: function( ul, item ) {
  471. return $( "<li>" )
  472. .append( $( "<a>" ).text( item.label ) )
  473. .appendTo( ul );
  474. },
  475. _move: function( direction, event ) {
  476. if ( !this.menu.element.is( ":visible" ) ) {
  477. this.search( null, event );
  478. return;
  479. }
  480. if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
  481. this.menu.isLastItem() && /^next/.test( direction ) ) {
  482. this._value( this.term );
  483. this.menu.blur();
  484. return;
  485. }
  486. this.menu[ direction ]( event );
  487. },
  488. widget: function() {
  489. return this.menu.element;
  490. },
  491. _value: function() {
  492. return this.valueMethod.apply( this.element, arguments );
  493. },
  494. _keyEvent: function( keyEvent, event ) {
  495. if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
  496. this._move( keyEvent, event );
  497. // prevents moving cursor to beginning/end of the text field in some browsers
  498. event.preventDefault();
  499. }
  500. }
  501. });
  502. $.extend( $.ui.autocomplete, {
  503. escapeRegex: function( value ) {
  504. return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
  505. },
  506. filter: function(array, term) {
  507. var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
  508. return $.grep( array, function(value) {
  509. return matcher.test( value.label || value.value || value );
  510. });
  511. }
  512. });
  513. // live region extension, adding a `messages` option
  514. // NOTE: This is an experimental API. We are still investigating
  515. // a full solution for string manipulation and internationalization.
  516. $.widget( "ui.autocomplete", $.ui.autocomplete, {
  517. options: {
  518. messages: {
  519. noResults: "No search results.",
  520. results: function( amount ) {
  521. return amount + ( amount > 1 ? " results are" : " result is" ) +
  522. " available, use up and down arrow keys to navigate.";
  523. }
  524. }
  525. },
  526. __response: function( content ) {
  527. var message;
  528. this._superApply( arguments );
  529. if ( this.options.disabled || this.cancelSearch ) {
  530. return;
  531. }
  532. if ( content && content.length ) {
  533. message = this.options.messages.results( content.length );
  534. } else {
  535. message = this.options.messages.noResults;
  536. }
  537. this.liveRegion.text( message );
  538. }
  539. });
  540. }( jQuery ));