2 * jQuery UI Autocomplete 1.11.2
5 * Copyright 2014 jQuery Foundation and other contributors
6 * Released under the MIT license.
7 * http://jquery.org/license
9 * http://api.jqueryui.com/autocomplete/
11 (function( factory ) {
12 if ( typeof define === "function" && define.amd ) {
14 // AMD. Register as an anonymous module.
29 $.widget( "ui.autocomplete", {
31 defaultElement: "<input>",
58 // Some browsers only repeat keydown events, not keypress events,
59 // so we use the suppressKeyPress flag to determine if we've already
60 // handled the keydown event. #7269
61 // Unfortunately the code for & in keypress is the same as the up arrow,
62 // so we use the suppressKeyPressRepeat flag to avoid handling keypress
63 // events when we know the keydown event was used to modify the
65 var suppressKeyPress, suppressKeyPressRepeat, suppressInput,
66 nodeName = this.element[ 0 ].nodeName.toLowerCase(),
67 isTextarea = nodeName === "textarea",
68 isInput = nodeName === "input";
71 // Textareas are always multi-line
73 // Inputs are always single-line, even if inside a contentEditable element
74 // IE also treats inputs as contentEditable
76 // All other element types are determined by whether or not they're contentEditable
77 this.element.prop( "isContentEditable" );
79 this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ];
80 this.isNewMenu = true;
83 .addClass( "ui-autocomplete-input" )
84 .attr( "autocomplete", "off" );
86 this._on( this.element, {
87 keydown: function( event ) {
88 if ( this.element.prop( "readOnly" ) ) {
89 suppressKeyPress = true;
91 suppressKeyPressRepeat = true;
95 suppressKeyPress = false;
96 suppressInput = false;
97 suppressKeyPressRepeat = false;
98 var keyCode = $.ui.keyCode;
99 switch ( event.keyCode ) {
100 case keyCode.PAGE_UP:
101 suppressKeyPress = true;
102 this._move( "previousPage", event );
104 case keyCode.PAGE_DOWN:
105 suppressKeyPress = true;
106 this._move( "nextPage", event );
109 suppressKeyPress = true;
110 this._keyEvent( "previous", event );
113 suppressKeyPress = true;
114 this._keyEvent( "next", event );
117 // when menu is open and has focus
118 if ( this.menu.active ) {
119 // #6055 - Opera still allows the keypress to occur
120 // which causes forms to submit
121 suppressKeyPress = true;
122 event.preventDefault();
123 this.menu.select( event );
127 if ( this.menu.active ) {
128 this.menu.select( event );
132 if ( this.menu.element.is( ":visible" ) ) {
133 if ( !this.isMultiLine ) {
134 this._value( this.term );
137 // Different browsers have different default behavior for escape
138 // Single press can mean undo or clear
139 // Double press in IE means clear the whole form
140 event.preventDefault();
144 suppressKeyPressRepeat = true;
145 // search timeout should be triggered before the input value is changed
146 this._searchTimeout( event );
150 keypress: function( event ) {
151 if ( suppressKeyPress ) {
152 suppressKeyPress = false;
153 if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
154 event.preventDefault();
158 if ( suppressKeyPressRepeat ) {
162 // replicate some key handlers to allow them to repeat in Firefox and Opera
163 var keyCode = $.ui.keyCode;
164 switch ( event.keyCode ) {
165 case keyCode.PAGE_UP:
166 this._move( "previousPage", event );
168 case keyCode.PAGE_DOWN:
169 this._move( "nextPage", event );
172 this._keyEvent( "previous", event );
175 this._keyEvent( "next", event );
179 input: function( event ) {
180 if ( suppressInput ) {
181 suppressInput = false;
182 event.preventDefault();
185 this._searchTimeout( event );
188 this.selectedItem = null;
189 this.previous = this._value();
191 blur: function( event ) {
192 if ( this.cancelBlur ) {
193 delete this.cancelBlur;
197 clearTimeout( this.searching );
199 this._change( event );
204 this.menu = $( "<ul>" )
205 .addClass( "ui-autocomplete ui-front" )
206 .appendTo( this._appendTo() )
208 // disable ARIA support, the live region takes care of that
214 this._on( this.menu.element, {
215 mousedown: function( event ) {
216 // prevent moving focus out of the text field
217 event.preventDefault();
219 // IE doesn't prevent moving focus even with event.preventDefault()
220 // so we set a flag to know when we should ignore the blur event
221 this.cancelBlur = true;
222 this._delay(function() {
223 delete this.cancelBlur;
226 // clicking on the scrollbar causes focus to shift to the body
227 // but we can't detect a mouseup or a click immediately afterward
228 // so we have to track the next mousedown and close the menu if
229 // the user clicks somewhere outside of the autocomplete
230 var menuElement = this.menu.element[ 0 ];
231 if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
232 this._delay(function() {
234 this.document.one( "mousedown", function( event ) {
235 if ( event.target !== that.element[ 0 ] &&
236 event.target !== menuElement &&
237 !$.contains( menuElement, event.target ) ) {
244 menufocus: function( event, ui ) {
247 // Prevent accidental activation of menu items in Firefox (#7024 #9118)
248 if ( this.isNewMenu ) {
249 this.isNewMenu = false;
250 if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
253 this.document.one( "mousemove", function() {
254 $( event.target ).trigger( event.originalEvent );
261 item = ui.item.data( "ui-autocomplete-item" );
262 if ( false !== this._trigger( "focus", event, { item: item } ) ) {
263 // use value to match what will end up in the input, if it was a key event
264 if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
265 this._value( item.value );
269 // Announce the value in the liveRegion
270 label = ui.item.attr( "aria-label" ) || item.value;
271 if ( label && $.trim( label ).length ) {
272 this.liveRegion.children().hide();
273 $( "<div>" ).text( label ).appendTo( this.liveRegion );
276 menuselect: function( event, ui ) {
277 var item = ui.item.data( "ui-autocomplete-item" ),
278 previous = this.previous;
280 // only trigger when focus was lost (click on menu)
281 if ( this.element[ 0 ] !== this.document[ 0 ].activeElement ) {
282 this.element.focus();
283 this.previous = previous;
284 // #6109 - IE triggers two focus events and the second
285 // is asynchronous, so we need to reset the previous
286 // term synchronously and asynchronously :-(
287 this._delay(function() {
288 this.previous = previous;
289 this.selectedItem = item;
293 if ( false !== this._trigger( "select", event, { item: item } ) ) {
294 this._value( item.value );
296 // reset the term after the select event
297 // this allows custom select handling to work properly
298 this.term = this._value();
301 this.selectedItem = item;
305 this.liveRegion = $( "<span>", {
307 "aria-live": "assertive",
308 "aria-relevant": "additions"
310 .addClass( "ui-helper-hidden-accessible" )
311 .appendTo( this.document[ 0 ].body );
313 // turning off autocomplete prevents the browser from remembering the
314 // value when navigating through history, so we re-enable autocomplete
315 // if the page is unloaded before the widget is destroyed. #7790
316 this._on( this.window, {
317 beforeunload: function() {
318 this.element.removeAttr( "autocomplete" );
323 _destroy: function() {
324 clearTimeout( this.searching );
326 .removeClass( "ui-autocomplete-input" )
327 .removeAttr( "autocomplete" );
328 this.menu.element.remove();
329 this.liveRegion.remove();
332 _setOption: function( key, value ) {
333 this._super( key, value );
334 if ( key === "source" ) {
337 if ( key === "appendTo" ) {
338 this.menu.element.appendTo( this._appendTo() );
340 if ( key === "disabled" && value && this.xhr ) {
345 _appendTo: function() {
346 var element = this.options.appendTo;
349 element = element.jquery || element.nodeType ?
351 this.document.find( element ).eq( 0 );
354 if ( !element || !element[ 0 ] ) {
355 element = this.element.closest( ".ui-front" );
358 if ( !element.length ) {
359 element = this.document[ 0 ].body;
365 _initSource: function() {
368 if ( $.isArray( this.options.source ) ) {
369 array = this.options.source;
370 this.source = function( request, response ) {
371 response( $.ui.autocomplete.filter( array, request.term ) );
373 } else if ( typeof this.options.source === "string" ) {
374 url = this.options.source;
375 this.source = function( request, response ) {
383 success: function( data ) {
392 this.source = this.options.source;
396 _searchTimeout: function( event ) {
397 clearTimeout( this.searching );
398 this.searching = this._delay(function() {
400 // Search if the value has changed, or if the user retypes the same value (see #7434)
401 var equalValues = this.term === this._value(),
402 menuVisible = this.menu.element.is( ":visible" ),
403 modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
405 if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) {
406 this.selectedItem = null;
407 this.search( null, event );
409 }, this.options.delay );
412 search: function( value, event ) {
413 value = value != null ? value : this._value();
415 // always save the actual value, not the one passed as an argument
416 this.term = this._value();
418 if ( value.length < this.options.minLength ) {
419 return this.close( event );
422 if ( this._trigger( "search", event ) === false ) {
426 return this._search( value );
429 _search: function( value ) {
431 this.element.addClass( "ui-autocomplete-loading" );
432 this.cancelSearch = false;
434 this.source( { term: value }, this._response() );
437 _response: function() {
438 var index = ++this.requestIndex;
440 return $.proxy(function( content ) {
441 if ( index === this.requestIndex ) {
442 this.__response( content );
446 if ( !this.pending ) {
447 this.element.removeClass( "ui-autocomplete-loading" );
452 __response: function( content ) {
454 content = this._normalize( content );
456 this._trigger( "response", null, { content: content } );
457 if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
458 this._suggest( content );
459 this._trigger( "open" );
461 // use ._close() instead of .close() so we don't cancel future searches
466 close: function( event ) {
467 this.cancelSearch = true;
468 this._close( event );
471 _close: function( event ) {
472 if ( this.menu.element.is( ":visible" ) ) {
473 this.menu.element.hide();
475 this.isNewMenu = true;
476 this._trigger( "close", event );
480 _change: function( event ) {
481 if ( this.previous !== this._value() ) {
482 this._trigger( "change", event, { item: this.selectedItem } );
486 _normalize: function( items ) {
487 // assume all items have the right format when the first item is complete
488 if ( items.length && items[ 0 ].label && items[ 0 ].value ) {
491 return $.map( items, function( item ) {
492 if ( typeof item === "string" ) {
498 return $.extend( {}, item, {
499 label: item.label || item.value,
500 value: item.value || item.label
505 _suggest: function( items ) {
506 var ul = this.menu.element.empty();
507 this._renderMenu( ul, items );
508 this.isNewMenu = true;
511 // size and position menu
514 ul.position( $.extend({
516 }, this.options.position ) );
518 if ( this.options.autoFocus ) {
523 _resizeMenu: function() {
524 var ul = this.menu.element;
525 ul.outerWidth( Math.max(
526 // Firefox wraps long text (possibly a rounding bug)
527 // so we add 1px to avoid the wrapping (#7513)
528 ul.width( "" ).outerWidth() + 1,
529 this.element.outerWidth()
533 _renderMenu: function( ul, items ) {
535 $.each( items, function( index, item ) {
536 that._renderItemData( ul, item );
540 _renderItemData: function( ul, item ) {
541 return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
544 _renderItem: function( ul, item ) {
545 return $( "<li>" ).text( item.label ).appendTo( ul );
548 _move: function( direction, event ) {
549 if ( !this.menu.element.is( ":visible" ) ) {
550 this.search( null, event );
553 if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
554 this.menu.isLastItem() && /^next/.test( direction ) ) {
556 if ( !this.isMultiLine ) {
557 this._value( this.term );
563 this.menu[ direction ]( event );
567 return this.menu.element;
571 return this.valueMethod.apply( this.element, arguments );
574 _keyEvent: function( keyEvent, event ) {
575 if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
576 this._move( keyEvent, event );
578 // prevents moving cursor to beginning/end of the text field in some browsers
579 event.preventDefault();
584 $.extend( $.ui.autocomplete, {
585 escapeRegex: function( value ) {
586 return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
588 filter: function( array, term ) {
589 var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" );
590 return $.grep( array, function( value ) {
591 return matcher.test( value.label || value.value || value );
596 // live region extension, adding a `messages` option
597 // NOTE: This is an experimental API. We are still investigating
598 // a full solution for string manipulation and internationalization.
599 $.widget( "ui.autocomplete", $.ui.autocomplete, {
602 noResults: "No search results.",
603 results: function( amount ) {
604 return amount + ( amount > 1 ? " results are" : " result is" ) +
605 " available, use up and down arrow keys to navigate.";
610 __response: function( content ) {
612 this._superApply( arguments );
613 if ( this.options.disabled || this.cancelSearch ) {
616 if ( content && content.length ) {
617 message = this.options.messages.results( content.length );
619 message = this.options.messages.noResults;
621 this.liveRegion.children().hide();
622 $( "<div>" ).text( message ).appendTo( this.liveRegion );
626 return $.ui.autocomplete;