You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

674 lines
17 KiB

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