jquery.ui.autocomplete.js 16 KB

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