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