25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

679 lines
17 KiB

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