2 * jQuery UI Autocomplete 1.11.4
5 * Copyright 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
;