autocomplete.js 16 KB

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