autocomplete.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. /*!
  2. * jQuery UI Autocomplete 1.14.1
  3. * https://jqueryui.com
  4. *
  5. * Copyright OpenJS Foundation and other contributors
  6. * Released under the MIT license.
  7. * https://jquery.org/license
  8. */
  9. //>>label: Autocomplete
  10. //>>group: Widgets
  11. //>>description: Lists suggested words as the user is typing.
  12. //>>docs: https://api.jqueryui.com/autocomplete/
  13. //>>demos: https://jqueryui.com/autocomplete/
  14. //>>css.structure: ../../themes/base/core.css
  15. //>>css.structure: ../../themes/base/autocomplete.css
  16. //>>css.theme: ../../themes/base/theme.css
  17. ( function( factory ) {
  18. "use strict";
  19. if ( typeof define === "function" && define.amd ) {
  20. // AMD. Register as an anonymous module.
  21. define( [
  22. "jquery",
  23. "./menu",
  24. "../keycode",
  25. "../position",
  26. "../version",
  27. "../widget"
  28. ], factory );
  29. } else {
  30. // Browser globals
  31. factory( jQuery );
  32. }
  33. } )( function( $ ) {
  34. "use strict";
  35. $.widget( "ui.autocomplete", {
  36. version: "1.14.1",
  37. defaultElement: "<input>",
  38. options: {
  39. appendTo: null,
  40. autoFocus: false,
  41. delay: 300,
  42. minLength: 1,
  43. position: {
  44. my: "left top",
  45. at: "left bottom",
  46. collision: "none"
  47. },
  48. source: null,
  49. // Callbacks
  50. change: null,
  51. close: null,
  52. focus: null,
  53. open: null,
  54. response: null,
  55. search: null,
  56. select: null
  57. },
  58. requestIndex: 0,
  59. pending: 0,
  60. liveRegionTimer: null,
  61. _create: function() {
  62. // Some browsers only repeat keydown events, not keypress events,
  63. // so we use the suppressKeyPress flag to determine if we've already
  64. // handled the keydown event. #7269
  65. // Unfortunately the code for & in keypress is the same as the up arrow,
  66. // so we use the suppressKeyPressRepeat flag to avoid handling keypress
  67. // events when we know the keydown event was used to modify the
  68. // search term. #7799
  69. var suppressKeyPress, suppressKeyPressRepeat, suppressInput,
  70. nodeName = this.element[ 0 ].nodeName.toLowerCase(),
  71. isTextarea = nodeName === "textarea",
  72. isInput = nodeName === "input";
  73. // Textareas are always multi-line
  74. // Inputs are always single-line, even if inside a contentEditable element
  75. // All other element types are determined by whether they're contentEditable
  76. this.isMultiLine = isTextarea ||
  77. !isInput && this.element.prop( "contentEditable" ) === "true";
  78. this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ];
  79. this.isNewMenu = true;
  80. this._addClass( "ui-autocomplete-input" );
  81. this.element.attr( "autocomplete", "off" );
  82. this._on( this.element, {
  83. keydown: function( event ) {
  84. if ( this.element.prop( "readOnly" ) ) {
  85. suppressKeyPress = true;
  86. suppressInput = true;
  87. suppressKeyPressRepeat = true;
  88. return;
  89. }
  90. suppressKeyPress = false;
  91. suppressInput = false;
  92. suppressKeyPressRepeat = false;
  93. var keyCode = $.ui.keyCode;
  94. switch ( event.keyCode ) {
  95. case keyCode.PAGE_UP:
  96. suppressKeyPress = true;
  97. this._move( "previousPage", event );
  98. break;
  99. case keyCode.PAGE_DOWN:
  100. suppressKeyPress = true;
  101. this._move( "nextPage", event );
  102. break;
  103. case keyCode.UP:
  104. suppressKeyPress = true;
  105. this._keyEvent( "previous", event );
  106. break;
  107. case keyCode.DOWN:
  108. suppressKeyPress = true;
  109. this._keyEvent( "next", event );
  110. break;
  111. case keyCode.ENTER:
  112. // when menu is open and has focus
  113. if ( this.menu.active ) {
  114. // #6055 - Opera still allows the keypress to occur
  115. // which causes forms to submit
  116. suppressKeyPress = true;
  117. event.preventDefault();
  118. this.menu.select( event );
  119. }
  120. break;
  121. case keyCode.TAB:
  122. if ( this.menu.active ) {
  123. this.menu.select( event );
  124. }
  125. break;
  126. case keyCode.ESCAPE:
  127. if ( this.menu.element.is( ":visible" ) ) {
  128. if ( !this.isMultiLine ) {
  129. this._value( this.term );
  130. }
  131. this.close( event );
  132. // Different browsers have different default behavior for escape
  133. // Single press can mean undo or clear
  134. event.preventDefault();
  135. }
  136. break;
  137. default:
  138. suppressKeyPressRepeat = true;
  139. // search timeout should be triggered before the input value is changed
  140. this._searchTimeout( event );
  141. break;
  142. }
  143. },
  144. keypress: function( event ) {
  145. if ( suppressKeyPress ) {
  146. suppressKeyPress = false;
  147. if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
  148. event.preventDefault();
  149. }
  150. return;
  151. }
  152. if ( suppressKeyPressRepeat ) {
  153. return;
  154. }
  155. // Replicate some key handlers to allow them to repeat in Firefox and Opera
  156. var keyCode = $.ui.keyCode;
  157. switch ( event.keyCode ) {
  158. case keyCode.PAGE_UP:
  159. this._move( "previousPage", event );
  160. break;
  161. case keyCode.PAGE_DOWN:
  162. this._move( "nextPage", event );
  163. break;
  164. case keyCode.UP:
  165. this._keyEvent( "previous", event );
  166. break;
  167. case keyCode.DOWN:
  168. this._keyEvent( "next", event );
  169. break;
  170. }
  171. },
  172. input: function( event ) {
  173. if ( suppressInput ) {
  174. suppressInput = false;
  175. event.preventDefault();
  176. return;
  177. }
  178. this._searchTimeout( event );
  179. },
  180. focus: function() {
  181. this.selectedItem = null;
  182. this.previous = this._value();
  183. },
  184. blur: function( event ) {
  185. clearTimeout( this.searching );
  186. this.close( event );
  187. this._change( event );
  188. }
  189. } );
  190. this._initSource();
  191. this.menu = $( "<ul>" )
  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._addClass( this.menu.element, "ui-autocomplete", "ui-front" );
  200. this._on( this.menu.element, {
  201. mousedown: function( event ) {
  202. // Prevent moving focus out of the text field
  203. event.preventDefault();
  204. },
  205. menufocus: function( event, ui ) {
  206. var label, item;
  207. // Support: Firefox
  208. // Prevent accidental activation of menu items in Firefox (#7024 #9118)
  209. if ( this.isNewMenu ) {
  210. this.isNewMenu = false;
  211. if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
  212. this.menu.blur();
  213. this.document.one( "mousemove", function() {
  214. $( event.target ).trigger( event.originalEvent );
  215. } );
  216. return;
  217. }
  218. }
  219. item = ui.item.data( "ui-autocomplete-item" );
  220. if ( false !== this._trigger( "focus", event, { item: item } ) ) {
  221. // use value to match what will end up in the input, if it was a key event
  222. if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
  223. this._value( item.value );
  224. }
  225. }
  226. // Announce the value in the liveRegion
  227. label = ui.item.attr( "aria-label" ) || item.value;
  228. if ( label && String.prototype.trim.call( label ).length ) {
  229. clearTimeout( this.liveRegionTimer );
  230. this.liveRegionTimer = this._delay( function() {
  231. this.liveRegion.html( $( "<div>" ).text( label ) );
  232. }, 100 );
  233. }
  234. },
  235. menuselect: function( event, ui ) {
  236. var item = ui.item.data( "ui-autocomplete-item" ),
  237. previous = this.previous;
  238. // Only trigger when focus was lost (click on menu)
  239. if ( this.element[ 0 ] !== this.document[ 0 ].activeElement ) {
  240. this.element.trigger( "focus" );
  241. this.previous = previous;
  242. }
  243. if ( false !== this._trigger( "select", event, { item: item } ) ) {
  244. this._value( item.value );
  245. }
  246. // reset the term after the select event
  247. // this allows custom select handling to work properly
  248. this.term = this._value();
  249. this.close( event );
  250. this.selectedItem = item;
  251. }
  252. } );
  253. this.liveRegion = $( "<div>", {
  254. role: "status",
  255. "aria-live": "assertive",
  256. "aria-relevant": "additions"
  257. } )
  258. .appendTo( this.document[ 0 ].body );
  259. this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" );
  260. // Turning off autocomplete prevents the browser from remembering the
  261. // value when navigating through history, so we re-enable autocomplete
  262. // if the page is unloaded before the widget is destroyed. #7790
  263. this._on( this.window, {
  264. beforeunload: function() {
  265. this.element.removeAttr( "autocomplete" );
  266. }
  267. } );
  268. },
  269. _destroy: function() {
  270. clearTimeout( this.searching );
  271. this.element.removeAttr( "autocomplete" );
  272. this.menu.element.remove();
  273. this.liveRegion.remove();
  274. },
  275. _setOption: function( key, value ) {
  276. this._super( key, value );
  277. if ( key === "source" ) {
  278. this._initSource();
  279. }
  280. if ( key === "appendTo" ) {
  281. this.menu.element.appendTo( this._appendTo() );
  282. }
  283. if ( key === "disabled" && value && this.xhr ) {
  284. this.xhr.abort();
  285. }
  286. },
  287. _isEventTargetInWidget: function( event ) {
  288. var menuElement = this.menu.element[ 0 ];
  289. return event.target === this.element[ 0 ] ||
  290. event.target === menuElement ||
  291. $.contains( menuElement, event.target );
  292. },
  293. _closeOnClickOutside: function( event ) {
  294. if ( !this._isEventTargetInWidget( event ) ) {
  295. this.close();
  296. }
  297. },
  298. _appendTo: function() {
  299. var element = this.options.appendTo;
  300. if ( element ) {
  301. element = element.jquery || element.nodeType ?
  302. $( element ) :
  303. this.document.find( element ).eq( 0 );
  304. }
  305. if ( !element || !element[ 0 ] ) {
  306. element = this.element.closest( ".ui-front, dialog" );
  307. }
  308. if ( !element.length ) {
  309. element = this.document[ 0 ].body;
  310. }
  311. return element;
  312. },
  313. _initSource: function() {
  314. var array, url,
  315. that = this;
  316. if ( Array.isArray( this.options.source ) ) {
  317. array = this.options.source;
  318. this.source = function( request, response ) {
  319. response( $.ui.autocomplete.filter( array, request.term ) );
  320. };
  321. } else if ( typeof this.options.source === "string" ) {
  322. url = this.options.source;
  323. this.source = function( request, response ) {
  324. if ( that.xhr ) {
  325. that.xhr.abort();
  326. }
  327. that.xhr = $.ajax( {
  328. url: url,
  329. data: request,
  330. dataType: "json",
  331. success: function( data ) {
  332. response( data );
  333. },
  334. error: function() {
  335. response( [] );
  336. }
  337. } );
  338. };
  339. } else {
  340. this.source = this.options.source;
  341. }
  342. },
  343. _searchTimeout: function( event ) {
  344. clearTimeout( this.searching );
  345. this.searching = this._delay( function() {
  346. // Search if the value has changed, or if the user retypes the same value (see #7434)
  347. var equalValues = this.term === this._value(),
  348. menuVisible = this.menu.element.is( ":visible" ),
  349. modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
  350. if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) {
  351. this.selectedItem = null;
  352. this.search( null, event );
  353. }
  354. }, this.options.delay );
  355. },
  356. search: function( value, event ) {
  357. value = value != null ? value : this._value();
  358. // Always save the actual value, not the one passed as an argument
  359. this.term = this._value();
  360. if ( value.length < this.options.minLength ) {
  361. return this.close( event );
  362. }
  363. if ( this._trigger( "search", event ) === false ) {
  364. return;
  365. }
  366. return this._search( value );
  367. },
  368. _search: function( value ) {
  369. this.pending++;
  370. this._addClass( "ui-autocomplete-loading" );
  371. this.cancelSearch = false;
  372. this.source( { term: value }, this._response() );
  373. },
  374. _response: function() {
  375. var index = ++this.requestIndex;
  376. return function( content ) {
  377. if ( index === this.requestIndex ) {
  378. this.__response( content );
  379. }
  380. this.pending--;
  381. if ( !this.pending ) {
  382. this._removeClass( "ui-autocomplete-loading" );
  383. }
  384. }.bind( this );
  385. },
  386. __response: function( content ) {
  387. if ( content ) {
  388. content = this._normalize( content );
  389. }
  390. this._trigger( "response", null, { content: content } );
  391. if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
  392. this._suggest( content );
  393. this._trigger( "open" );
  394. } else {
  395. // use ._close() instead of .close() so we don't cancel future searches
  396. this._close();
  397. }
  398. },
  399. close: function( event ) {
  400. this.cancelSearch = true;
  401. this._close( event );
  402. },
  403. _close: function( event ) {
  404. // Remove the handler that closes the menu on outside clicks
  405. this._off( this.document, "mousedown" );
  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( {}, item, {
  431. label: item.label || item.value,
  432. value: item.value || item.label
  433. } );
  434. } );
  435. },
  436. _suggest: function( items ) {
  437. var ul = this.menu.element.empty();
  438. this._renderMenu( ul, items );
  439. this.isNewMenu = true;
  440. this.menu.refresh();
  441. // Size and position menu
  442. ul.show();
  443. this._resizeMenu();
  444. ul.position( $.extend( {
  445. of: this.element
  446. }, this.options.position ) );
  447. if ( this.options.autoFocus ) {
  448. this.menu.next();
  449. }
  450. // Listen for interactions outside of the widget (#6642)
  451. this._on( this.document, {
  452. mousedown: "_closeOnClickOutside"
  453. } );
  454. },
  455. _resizeMenu: function() {
  456. var ul = this.menu.element;
  457. ul.outerWidth( Math.max(
  458. // Firefox wraps long text (possibly a rounding bug)
  459. // so we add 1px to avoid the wrapping (#7513)
  460. ul.width( "" ).outerWidth() + 1,
  461. this.element.outerWidth()
  462. ) );
  463. },
  464. _renderMenu: function( ul, items ) {
  465. var that = this;
  466. $.each( items, function( index, item ) {
  467. that._renderItemData( ul, item );
  468. } );
  469. },
  470. _renderItemData: function( ul, item ) {
  471. return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
  472. },
  473. _renderItem: function( ul, item ) {
  474. return $( "<li>" )
  475. .append( $( "<div>" ).text( item.label ) )
  476. .appendTo( ul );
  477. },
  478. _move: function( direction, event ) {
  479. if ( !this.menu.element.is( ":visible" ) ) {
  480. this.search( null, event );
  481. return;
  482. }
  483. if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
  484. this.menu.isLastItem() && /^next/.test( direction ) ) {
  485. if ( !this.isMultiLine ) {
  486. this._value( this.term );
  487. }
  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. clearTimeout( this.liveRegionTimer );
  543. this.liveRegionTimer = this._delay( function() {
  544. this.liveRegion.html( $( "<div>" ).text( message ) );
  545. }, 100 );
  546. }
  547. } );
  548. return $.ui.autocomplete;
  549. } );