jquery.ui.autocomplete.js 16 KB

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