Bez popisu

autocomplete.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. /*!
  2. * jQuery UI Autocomplete 1.12.1
  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. if ( typeof define === "function" && define.amd ) {
  19. // AMD. Register as an anonymous module.
  20. define( [
  21. "jquery",
  22. "./menu",
  23. "./core"
  24. ], factory );
  25. } else {
  26. // Browser globals
  27. factory( jQuery );
  28. }
  29. }( function( $ ) {
  30. $.widget( "ui.autocomplete", {
  31. version: "1.12.1",
  32. defaultElement: "<input>",
  33. options: {
  34. appendTo: null,
  35. autoFocus: false,
  36. delay: 300,
  37. minLength: 1,
  38. position: {
  39. my: "left top",
  40. at: "left bottom",
  41. collision: "none"
  42. },
  43. source: null,
  44. // Callbacks
  45. change: null,
  46. close: null,
  47. focus: null,
  48. open: null,
  49. response: null,
  50. search: null,
  51. select: null
  52. },
  53. requestIndex: 0,
  54. pending: 0,
  55. _create: function() {
  56. // Some browsers only repeat keydown events, not keypress events,
  57. // so we use the suppressKeyPress flag to determine if we've already
  58. // handled the keydown event. #7269
  59. // Unfortunately the code for & in keypress is the same as the up arrow,
  60. // so we use the suppressKeyPressRepeat flag to avoid handling keypress
  61. // events when we know the keydown event was used to modify the
  62. // search term. #7799
  63. var suppressKeyPress, suppressKeyPressRepeat, suppressInput,
  64. nodeName = this.element[ 0 ].nodeName.toLowerCase(),
  65. isTextarea = nodeName === "textarea",
  66. isInput = nodeName === "input";
  67. // Textareas are always multi-line
  68. // Inputs are always single-line, even if inside a contentEditable element
  69. // IE also treats inputs as contentEditable
  70. // All other element types are determined by whether or not they're contentEditable
  71. this.isMultiLine = isTextarea || !isInput && this._isContentEditable( this.element );
  72. this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ];
  73. this.isNewMenu = true;
  74. this._addClass( "ui-autocomplete-input" );
  75. this.element.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. .appendTo( this._appendTo() )
  192. .menu( {
  193. // disable ARIA support, the live region takes care of that
  194. role: null
  195. } )
  196. .hide()
  197. .menu( "instance" );
  198. this._addClass( this.menu.element, "ui-autocomplete", "ui-front" );
  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. // Support: IE 8 only
  209. // Right clicking a menu item or selecting text from the menu items will
  210. // result in focus moving out of the input. However, we've already received
  211. // and ignored the blur event because of the cancelBlur flag set above. So
  212. // we restore focus to ensure that the menu closes properly based on the user's
  213. // next actions.
  214. if ( this.element[ 0 ] !== $.ui.safeActiveElement( this.document[ 0 ] ) ) {
  215. this.element.trigger( "focus" );
  216. }
  217. } );
  218. },
  219. menufocus: function( event, ui ) {
  220. var label, item;
  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. 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. }
  240. // Announce the value in the liveRegion
  241. label = ui.item.attr( "aria-label" ) || item.value;
  242. if ( label && $.trim( label ).length ) {
  243. this.liveRegion.children().hide();
  244. $( "<div>" ).text( label ).appendTo( this.liveRegion );
  245. }
  246. },
  247. menuselect: function( event, ui ) {
  248. var item = ui.item.data( "ui-autocomplete-item" ),
  249. previous = this.previous;
  250. // Only trigger when focus was lost (click on menu)
  251. if ( this.element[ 0 ] !== $.ui.safeActiveElement( this.document[ 0 ] ) ) {
  252. this.element.trigger( "focus" );
  253. this.previous = previous;
  254. // #6109 - IE triggers two focus events and the second
  255. // is asynchronous, so we need to reset the previous
  256. // term synchronously and asynchronously :-(
  257. this._delay( function() {
  258. this.previous = previous;
  259. this.selectedItem = item;
  260. } );
  261. }
  262. if ( false !== this._trigger( "select", event, { item: item } ) ) {
  263. this._value( item.value );
  264. }
  265. // reset the term after the select event
  266. // this allows custom select handling to work properly
  267. this.term = this._value();
  268. this.close( event );
  269. this.selectedItem = item;
  270. }
  271. } );
  272. this.liveRegion = $( "<div>", {
  273. role: "status",
  274. "aria-live": "assertive",
  275. "aria-relevant": "additions"
  276. } )
  277. .appendTo( this.document[ 0 ].body );
  278. this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" );
  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.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. _isEventTargetInWidget: function( event ) {
  307. var menuElement = this.menu.element[ 0 ];
  308. return event.target === this.element[ 0 ] ||
  309. event.target === menuElement ||
  310. $.contains( menuElement, event.target );
  311. },
  312. _closeOnClickOutside: function( event ) {
  313. if ( !this._isEventTargetInWidget( event ) ) {
  314. this.close();
  315. }
  316. },
  317. _appendTo: function() {
  318. var element = this.options.appendTo;
  319. if ( element ) {
  320. element = element.jquery || element.nodeType ?
  321. $( element ) :
  322. this.document.find( element ).eq( 0 );
  323. }
  324. if ( !element || !element[ 0 ] ) {
  325. element = this.element.closest( ".ui-front, dialog" );
  326. }
  327. if ( !element.length ) {
  328. element = this.document[ 0 ].body;
  329. }
  330. return element;
  331. },
  332. _initSource: function() {
  333. var array, url,
  334. that = this;
  335. if ( $.isArray( this.options.source ) ) {
  336. array = this.options.source;
  337. this.source = function( request, response ) {
  338. response( $.ui.autocomplete.filter( array, request.term ) );
  339. };
  340. } else if ( typeof this.options.source === "string" ) {
  341. url = this.options.source;
  342. this.source = function( request, response ) {
  343. if ( that.xhr ) {
  344. that.xhr.abort();
  345. }
  346. that.xhr = $.ajax( {
  347. url: url,
  348. data: request,
  349. dataType: "json",
  350. success: function( data ) {
  351. response( data );
  352. },
  353. error: function() {
  354. response( [] );
  355. }
  356. } );
  357. };
  358. } else {
  359. this.source = this.options.source;
  360. }
  361. },
  362. _searchTimeout: function( event ) {
  363. clearTimeout( this.searching );
  364. this.searching = this._delay( function() {
  365. // Search if the value has changed, or if the user retypes the same value (see #7434)
  366. var equalValues = this.term === this._value(),
  367. menuVisible = this.menu.element.is( ":visible" ),
  368. modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
  369. if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) {
  370. this.selectedItem = null;
  371. this.search( null, event );
  372. }
  373. }, this.options.delay );
  374. },
  375. search: function( value, event ) {
  376. value = value != null ? value : this._value();
  377. // Always save the actual value, not the one passed as an argument
  378. this.term = this._value();
  379. if ( value.length < this.options.minLength ) {
  380. return this.close( event );
  381. }
  382. if ( this._trigger( "search", event ) === false ) {
  383. return;
  384. }
  385. return this._search( value );
  386. },
  387. _search: function( value ) {
  388. this.pending++;
  389. this._addClass( "ui-autocomplete-loading" );
  390. this.cancelSearch = false;
  391. this.source( { term: value }, this._response() );
  392. },
  393. _response: function() {
  394. var index = ++this.requestIndex;
  395. return $.proxy( function( content ) {
  396. if ( index === this.requestIndex ) {
  397. this.__response( content );
  398. }
  399. this.pending--;
  400. if ( !this.pending ) {
  401. this._removeClass( "ui-autocomplete-loading" );
  402. }
  403. }, this );
  404. },
  405. __response: function( content ) {
  406. if ( content ) {
  407. content = this._normalize( content );
  408. }
  409. this._trigger( "response", null, { content: content } );
  410. if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
  411. this._suggest( content );
  412. this._trigger( "open" );
  413. } else {
  414. // use ._close() instead of .close() so we don't cancel future searches
  415. this._close();
  416. }
  417. },
  418. close: function( event ) {
  419. this.cancelSearch = true;
  420. this._close( event );
  421. },
  422. _close: function( event ) {
  423. // Remove the handler that closes the menu on outside clicks
  424. this._off( this.document, "mousedown" );
  425. if ( this.menu.element.is( ":visible" ) ) {
  426. this.menu.element.hide();
  427. this.menu.blur();
  428. this.isNewMenu = true;
  429. this._trigger( "close", event );
  430. }
  431. },
  432. _change: function( event ) {
  433. if ( this.previous !== this._value() ) {
  434. this._trigger( "change", event, { item: this.selectedItem } );
  435. }
  436. },
  437. _normalize: function( items ) {
  438. // assume all items have the right format when the first item is complete
  439. if ( items.length && items[ 0 ].label && items[ 0 ].value ) {
  440. return items;
  441. }
  442. return $.map( items, function( item ) {
  443. if ( typeof item === "string" ) {
  444. return {
  445. label: item,
  446. value: item
  447. };
  448. }
  449. return $.extend( {}, item, {
  450. label: item.label || item.value,
  451. value: item.value || item.label
  452. } );
  453. } );
  454. },
  455. _suggest: function( items ) {
  456. var ul = this.menu.element.empty();
  457. this._renderMenu( ul, items );
  458. this.isNewMenu = true;
  459. this.menu.refresh();
  460. // Size and position menu
  461. ul.show();
  462. this._resizeMenu();
  463. ul.position( $.extend( {
  464. of: this.element
  465. }, this.options.position ) );
  466. if ( this.options.autoFocus ) {
  467. this.menu.next();
  468. }
  469. // Listen for interactions outside of the widget (#6642)
  470. this._on( this.document, {
  471. mousedown: "_closeOnClickOutside"
  472. } );
  473. },
  474. _resizeMenu: function() {
  475. var ul = this.menu.element;
  476. ul.outerWidth( Math.max(
  477. // Firefox wraps long text (possibly a rounding bug)
  478. // so we add 1px to avoid the wrapping (#7513)
  479. ul.width( "" ).outerWidth() + 1,
  480. this.element.outerWidth()
  481. ) );
  482. },
  483. _renderMenu: function( ul, items ) {
  484. var that = this;
  485. $.each( items, function( index, item ) {
  486. that._renderItemData( ul, item );
  487. } );
  488. },
  489. _renderItemData: function( ul, item ) {
  490. return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
  491. },
  492. _renderItem: function( ul, item ) {
  493. return $( "<li>" )
  494. .append( $( "<div>" ).text( item.label ) )
  495. .appendTo( ul );
  496. },
  497. _move: function( direction, event ) {
  498. if ( !this.menu.element.is( ":visible" ) ) {
  499. this.search( null, event );
  500. return;
  501. }
  502. if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
  503. this.menu.isLastItem() && /^next/.test( direction ) ) {
  504. if ( !this.isMultiLine ) {
  505. this._value( this.term );
  506. }
  507. this.menu.blur();
  508. return;
  509. }
  510. this.menu[ direction ]( event );
  511. },
  512. widget: function() {
  513. return this.menu.element;
  514. },
  515. _value: function() {
  516. return this.valueMethod.apply( this.element, arguments );
  517. },
  518. _keyEvent: function( keyEvent, event ) {
  519. if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
  520. this._move( keyEvent, event );
  521. // Prevents moving cursor to beginning/end of the text field in some browsers
  522. event.preventDefault();
  523. }
  524. },
  525. // Support: Chrome <=50
  526. // We should be able to just use this.element.prop( "isContentEditable" )
  527. // but hidden elements always report false in Chrome.
  528. // https://code.google.com/p/chromium/issues/detail?id=313082
  529. _isContentEditable: function( element ) {
  530. if ( !element.length ) {
  531. return false;
  532. }
  533. var editable = element.prop( "contentEditable" );
  534. if ( editable === "inherit" ) {
  535. return this._isContentEditable( element.parent() );
  536. }
  537. return editable === "true";
  538. }
  539. } );
  540. $.extend( $.ui.autocomplete, {
  541. escapeRegex: function( value ) {
  542. return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
  543. },
  544. filter: function( array, term ) {
  545. var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" );
  546. return $.grep( array, function( value ) {
  547. return matcher.test( value.label || value.value || value );
  548. } );
  549. }
  550. } );
  551. // Live region extension, adding a `messages` option
  552. // NOTE: This is an experimental API. We are still investigating
  553. // a full solution for string manipulation and internationalization.
  554. $.widget( "ui.autocomplete", $.ui.autocomplete, {
  555. options: {
  556. messages: {
  557. noResults: "No search results.",
  558. results: function( amount ) {
  559. return amount + ( amount > 1 ? " results are" : " result is" ) +
  560. " available, use up and down arrow keys to navigate.";
  561. }
  562. }
  563. },
  564. __response: function( content ) {
  565. var message;
  566. this._superApply( arguments );
  567. if ( this.options.disabled || this.cancelSearch ) {
  568. return;
  569. }
  570. if ( content && content.length ) {
  571. message = this.options.messages.results( content.length );
  572. } else {
  573. message = this.options.messages.noResults;
  574. }
  575. this.liveRegion.children().hide();
  576. $( "<div>" ).text( message ).appendTo( this.liveRegion );
  577. }
  578. } );
  579. return $.ui.autocomplete;
  580. } ) );