Merge "Special:Upload should not crash on failing previews"
[mediawiki.git] / resources / lib / jquery.ui / jquery.ui.autocomplete.js
blob3baed1daf084754485b45e27ed76b919197582a5
1 /*!
2  * jQuery UI Autocomplete 1.9.2
3  * http://jqueryui.com
4  *
5  * Copyright 2012 jQuery Foundation and other contributors
6  * Released under the MIT license.
7  * http://jquery.org/license
8  *
9  * http://api.jqueryui.com/autocomplete/
10  *
11  * Depends:
12  *      jquery.ui.core.js
13  *      jquery.ui.widget.js
14  *      jquery.ui.position.js
15  *      jquery.ui.menu.js
16  */
17 (function( $, undefined ) {
19 // used to prevent race conditions with remote data sources
20 var requestIndex = 0;
22 $.widget( "ui.autocomplete", {
23         version: "1.9.2",
24         defaultElement: "<input>",
25         options: {
26                 appendTo: "body",
27                 autoFocus: false,
28                 delay: 300,
29                 minLength: 1,
30                 position: {
31                         my: "left top",
32                         at: "left bottom",
33                         collision: "none"
34                 },
35                 source: null,
37                 // callbacks
38                 change: null,
39                 close: null,
40                 focus: null,
41                 open: null,
42                 response: null,
43                 search: null,
44                 select: null
45         },
47         pending: 0,
49         _create: function() {
50                 // Some browsers only repeat keydown events, not keypress events,
51                 // so we use the suppressKeyPress flag to determine if we've already
52                 // handled the keydown event. #7269
53                 // Unfortunately the code for & in keypress is the same as the up arrow,
54                 // so we use the suppressKeyPressRepeat flag to avoid handling keypress
55                 // events when we know the keydown event was used to modify the
56                 // search term. #7799
57                 var suppressKeyPress, suppressKeyPressRepeat, suppressInput;
59                 this.isMultiLine = this._isMultiLine();
60                 this.valueMethod = this.element[ this.element.is( "input,textarea" ) ? "val" : "text" ];
61                 this.isNewMenu = true;
63                 this.element
64                         .addClass( "ui-autocomplete-input" )
65                         .attr( "autocomplete", "off" );
67                 this._on( this.element, {
68                         keydown: function( event ) {
69                                 if ( this.element.prop( "readOnly" ) ) {
70                                         suppressKeyPress = true;
71                                         suppressInput = true;
72                                         suppressKeyPressRepeat = true;
73                                         return;
74                                 }
76                                 suppressKeyPress = false;
77                                 suppressInput = false;
78                                 suppressKeyPressRepeat = false;
79                                 var keyCode = $.ui.keyCode;
80                                 switch( event.keyCode ) {
81                                 case keyCode.PAGE_UP:
82                                         suppressKeyPress = true;
83                                         this._move( "previousPage", event );
84                                         break;
85                                 case keyCode.PAGE_DOWN:
86                                         suppressKeyPress = true;
87                                         this._move( "nextPage", event );
88                                         break;
89                                 case keyCode.UP:
90                                         suppressKeyPress = true;
91                                         this._keyEvent( "previous", event );
92                                         break;
93                                 case keyCode.DOWN:
94                                         suppressKeyPress = true;
95                                         this._keyEvent( "next", event );
96                                         break;
97                                 case keyCode.ENTER:
98                                 case keyCode.NUMPAD_ENTER:
99                                         // when menu is open and has focus
100                                         if ( this.menu.active ) {
101                                                 // #6055 - Opera still allows the keypress to occur
102                                                 // which causes forms to submit
103                                                 suppressKeyPress = true;
104                                                 event.preventDefault();
105                                                 this.menu.select( event );
106                                         }
107                                         break;
108                                 case keyCode.TAB:
109                                         if ( this.menu.active ) {
110                                                 this.menu.select( event );
111                                         }
112                                         break;
113                                 case keyCode.ESCAPE:
114                                         if ( this.menu.element.is( ":visible" ) ) {
115                                                 this._value( this.term );
116                                                 this.close( event );
117                                                 // Different browsers have different default behavior for escape
118                                                 // Single press can mean undo or clear
119                                                 // Double press in IE means clear the whole form
120                                                 event.preventDefault();
121                                         }
122                                         break;
123                                 default:
124                                         suppressKeyPressRepeat = true;
125                                         // search timeout should be triggered before the input value is changed
126                                         this._searchTimeout( event );
127                                         break;
128                                 }
129                         },
130                         keypress: function( event ) {
131                                 if ( suppressKeyPress ) {
132                                         suppressKeyPress = false;
133                                         event.preventDefault();
134                                         return;
135                                 }
136                                 if ( suppressKeyPressRepeat ) {
137                                         return;
138                                 }
140                                 // replicate some key handlers to allow them to repeat in Firefox and Opera
141                                 var keyCode = $.ui.keyCode;
142                                 switch( event.keyCode ) {
143                                 case keyCode.PAGE_UP:
144                                         this._move( "previousPage", event );
145                                         break;
146                                 case keyCode.PAGE_DOWN:
147                                         this._move( "nextPage", event );
148                                         break;
149                                 case keyCode.UP:
150                                         this._keyEvent( "previous", event );
151                                         break;
152                                 case keyCode.DOWN:
153                                         this._keyEvent( "next", event );
154                                         break;
155                                 }
156                         },
157                         input: function( event ) {
158                                 if ( suppressInput ) {
159                                         suppressInput = false;
160                                         event.preventDefault();
161                                         return;
162                                 }
163                                 this._searchTimeout( event );
164                         },
165                         focus: function() {
166                                 this.selectedItem = null;
167                                 this.previous = this._value();
168                         },
169                         blur: function( event ) {
170                                 if ( this.cancelBlur ) {
171                                         delete this.cancelBlur;
172                                         return;
173                                 }
175                                 clearTimeout( this.searching );
176                                 this.close( event );
177                                 this._change( event );
178                         }
179                 });
181                 this._initSource();
182                 this.menu = $( "<ul>" )
183                         .addClass( "ui-autocomplete" )
184                         .appendTo( this.document.find( this.options.appendTo || "body" )[ 0 ] )
185                         .menu({
186                                 // custom key handling for now
187                                 input: $(),
188                                 // disable ARIA support, the live region takes care of that
189                                 role: null
190                         })
191                         .zIndex( this.element.zIndex() + 1 )
192                         .hide()
193                         .data( "menu" );
195                 this._on( this.menu.element, {
196                         mousedown: function( event ) {
197                                 // prevent moving focus out of the text field
198                                 event.preventDefault();
200                                 // IE doesn't prevent moving focus even with event.preventDefault()
201                                 // so we set a flag to know when we should ignore the blur event
202                                 this.cancelBlur = true;
203                                 this._delay(function() {
204                                         delete this.cancelBlur;
205                                 });
207                                 // clicking on the scrollbar causes focus to shift to the body
208                                 // but we can't detect a mouseup or a click immediately afterward
209                                 // so we have to track the next mousedown and close the menu if
210                                 // the user clicks somewhere outside of the autocomplete
211                                 var menuElement = this.menu.element[ 0 ];
212                                 if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
213                                         this._delay(function() {
214                                                 var that = this;
215                                                 this.document.one( "mousedown", function( event ) {
216                                                         if ( event.target !== that.element[ 0 ] &&
217                                                                         event.target !== menuElement &&
218                                                                         !$.contains( menuElement, event.target ) ) {
219                                                                 that.close();
220                                                         }
221                                                 });
222                                         });
223                                 }
224                         },
225                         menufocus: function( event, ui ) {
226                                 // #7024 - Prevent accidental activation of menu items in Firefox
227                                 if ( this.isNewMenu ) {
228                                         this.isNewMenu = false;
229                                         if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
230                                                 this.menu.blur();
232                                                 this.document.one( "mousemove", function() {
233                                                         $( event.target ).trigger( event.originalEvent );
234                                                 });
236                                                 return;
237                                         }
238                                 }
240                                 // back compat for _renderItem using item.autocomplete, via #7810
241                                 // TODO remove the fallback, see #8156
242                                 var item = ui.item.data( "ui-autocomplete-item" ) || ui.item.data( "item.autocomplete" );
243                                 if ( false !== this._trigger( "focus", event, { item: item } ) ) {
244                                         // use value to match what will end up in the input, if it was a key event
245                                         if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
246                                                 this._value( item.value );
247                                         }
248                                 } else {
249                                         // Normally the input is populated with the item's value as the
250                                         // menu is navigated, causing screen readers to notice a change and
251                                         // announce the item. Since the focus event was canceled, this doesn't
252                                         // happen, so we update the live region so that screen readers can
253                                         // still notice the change and announce it.
254                                         this.liveRegion.text( item.value );
255                                 }
256                         },
257                         menuselect: function( event, ui ) {
258                                 // back compat for _renderItem using item.autocomplete, via #7810
259                                 // TODO remove the fallback, see #8156
260                                 var item = ui.item.data( "ui-autocomplete-item" ) || ui.item.data( "item.autocomplete" ),
261                                         previous = this.previous;
263                                 // only trigger when focus was lost (click on menu)
264                                 if ( this.element[0] !== this.document[0].activeElement ) {
265                                         this.element.focus();
266                                         this.previous = previous;
267                                         // #6109 - IE triggers two focus events and the second
268                                         // is asynchronous, so we need to reset the previous
269                                         // term synchronously and asynchronously :-(
270                                         this._delay(function() {
271                                                 this.previous = previous;
272                                                 this.selectedItem = item;
273                                         });
274                                 }
276                                 if ( false !== this._trigger( "select", event, { item: item } ) ) {
277                                         this._value( item.value );
278                                 }
279                                 // reset the term after the select event
280                                 // this allows custom select handling to work properly
281                                 this.term = this._value();
283                                 this.close( event );
284                                 this.selectedItem = item;
285                         }
286                 });
288                 this.liveRegion = $( "<span>", {
289                                 role: "status",
290                                 "aria-live": "polite"
291                         })
292                         .addClass( "ui-helper-hidden-accessible" )
293                         .insertAfter( this.element );
295                 if ( $.fn.bgiframe ) {
296                         this.menu.element.bgiframe();
297                 }
299                 // turning off autocomplete prevents the browser from remembering the
300                 // value when navigating through history, so we re-enable autocomplete
301                 // if the page is unloaded before the widget is destroyed. #7790
302                 this._on( this.window, {
303                         beforeunload: function() {
304                                 this.element.removeAttr( "autocomplete" );
305                         }
306                 });
307         },
309         _destroy: function() {
310                 clearTimeout( this.searching );
311                 this.element
312                         .removeClass( "ui-autocomplete-input" )
313                         .removeAttr( "autocomplete" );
314                 this.menu.element.remove();
315                 this.liveRegion.remove();
316         },
318         _setOption: function( key, value ) {
319                 this._super( key, value );
320                 if ( key === "source" ) {
321                         this._initSource();
322                 }
323                 if ( key === "appendTo" ) {
324                         this.menu.element.appendTo( this.document.find( value || "body" )[0] );
325                 }
326                 if ( key === "disabled" && value && this.xhr ) {
327                         this.xhr.abort();
328                 }
329         },
331         _isMultiLine: function() {
332                 // Textareas are always multi-line
333                 if ( this.element.is( "textarea" ) ) {
334                         return true;
335                 }
336                 // Inputs are always single-line, even if inside a contentEditable element
337                 // IE also treats inputs as contentEditable
338                 if ( this.element.is( "input" ) ) {
339                         return false;
340                 }
341                 // All other element types are determined by whether or not they're contentEditable
342                 return this.element.prop( "isContentEditable" );
343         },
345         _initSource: function() {
346                 var array, url,
347                         that = this;
348                 if ( $.isArray(this.options.source) ) {
349                         array = this.options.source;
350                         this.source = function( request, response ) {
351                                 response( $.ui.autocomplete.filter( array, request.term ) );
352                         };
353                 } else if ( typeof this.options.source === "string" ) {
354                         url = this.options.source;
355                         this.source = function( request, response ) {
356                                 if ( that.xhr ) {
357                                         that.xhr.abort();
358                                 }
359                                 that.xhr = $.ajax({
360                                         url: url,
361                                         data: request,
362                                         dataType: "json",
363                                         success: function( data ) {
364                                                 response( data );
365                                         },
366                                         error: function() {
367                                                 response( [] );
368                                         }
369                                 });
370                         };
371                 } else {
372                         this.source = this.options.source;
373                 }
374         },
376         _searchTimeout: function( event ) {
377                 clearTimeout( this.searching );
378                 this.searching = this._delay(function() {
379                         // only search if the value has changed
380                         if ( this.term !== this._value() ) {
381                                 this.selectedItem = null;
382                                 this.search( null, event );
383                         }
384                 }, this.options.delay );
385         },
387         search: function( value, event ) {
388                 value = value != null ? value : this._value();
390                 // always save the actual value, not the one passed as an argument
391                 this.term = this._value();
393                 if ( value.length < this.options.minLength ) {
394                         return this.close( event );
395                 }
397                 if ( this._trigger( "search", event ) === false ) {
398                         return;
399                 }
401                 return this._search( value );
402         },
404         _search: function( value ) {
405                 this.pending++;
406                 this.element.addClass( "ui-autocomplete-loading" );
407                 this.cancelSearch = false;
409                 this.source( { term: value }, this._response() );
410         },
412         _response: function() {
413                 var that = this,
414                         index = ++requestIndex;
416                 return function( content ) {
417                         if ( index === requestIndex ) {
418                                 that.__response( content );
419                         }
421                         that.pending--;
422                         if ( !that.pending ) {
423                                 that.element.removeClass( "ui-autocomplete-loading" );
424                         }
425                 };
426         },
428         __response: function( content ) {
429                 if ( content ) {
430                         content = this._normalize( content );
431                 }
432                 this._trigger( "response", null, { content: content } );
433                 if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
434                         this._suggest( content );
435                         this._trigger( "open" );
436                 } else {
437                         // use ._close() instead of .close() so we don't cancel future searches
438                         this._close();
439                 }
440         },
442         close: function( event ) {
443                 this.cancelSearch = true;
444                 this._close( event );
445         },
447         _close: function( event ) {
448                 if ( this.menu.element.is( ":visible" ) ) {
449                         this.menu.element.hide();
450                         this.menu.blur();
451                         this.isNewMenu = true;
452                         this._trigger( "close", event );
453                 }
454         },
456         _change: function( event ) {
457                 if ( this.previous !== this._value() ) {
458                         this._trigger( "change", event, { item: this.selectedItem } );
459                 }
460         },
462         _normalize: function( items ) {
463                 // assume all items have the right format when the first item is complete
464                 if ( items.length && items[0].label && items[0].value ) {
465                         return items;
466                 }
467                 return $.map( items, function( item ) {
468                         if ( typeof item === "string" ) {
469                                 return {
470                                         label: item,
471                                         value: item
472                                 };
473                         }
474                         return $.extend({
475                                 label: item.label || item.value,
476                                 value: item.value || item.label
477                         }, item );
478                 });
479         },
481         _suggest: function( items ) {
482                 var ul = this.menu.element
483                         .empty()
484                         .zIndex( this.element.zIndex() + 1 );
485                 this._renderMenu( ul, items );
486                 this.menu.refresh();
488                 // size and position menu
489                 ul.show();
490                 this._resizeMenu();
491                 ul.position( $.extend({
492                         of: this.element
493                 }, this.options.position ));
495                 if ( this.options.autoFocus ) {
496                         this.menu.next();
497                 }
498         },
500         _resizeMenu: function() {
501                 var ul = this.menu.element;
502                 ul.outerWidth( Math.max(
503                         // Firefox wraps long text (possibly a rounding bug)
504                         // so we add 1px to avoid the wrapping (#7513)
505                         ul.width( "" ).outerWidth() + 1,
506                         this.element.outerWidth()
507                 ) );
508         },
510         _renderMenu: function( ul, items ) {
511                 var that = this;
512                 $.each( items, function( index, item ) {
513                         that._renderItemData( ul, item );
514                 });
515         },
517         _renderItemData: function( ul, item ) {
518                 return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
519         },
521         _renderItem: function( ul, item ) {
522                 return $( "<li>" )
523                         .append( $( "<a>" ).text( item.label ) )
524                         .appendTo( ul );
525         },
527         _move: function( direction, event ) {
528                 if ( !this.menu.element.is( ":visible" ) ) {
529                         this.search( null, event );
530                         return;
531                 }
532                 if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
533                                 this.menu.isLastItem() && /^next/.test( direction ) ) {
534                         this._value( this.term );
535                         this.menu.blur();
536                         return;
537                 }
538                 this.menu[ direction ]( event );
539         },
541         widget: function() {
542                 return this.menu.element;
543         },
545         _value: function() {
546                 return this.valueMethod.apply( this.element, arguments );
547         },
549         _keyEvent: function( keyEvent, event ) {
550                 if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
551                         this._move( keyEvent, event );
553                         // prevents moving cursor to beginning/end of the text field in some browsers
554                         event.preventDefault();
555                 }
556         }
559 $.extend( $.ui.autocomplete, {
560         escapeRegex: function( value ) {
561                 return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
562         },
563         filter: function(array, term) {
564                 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
565                 return $.grep( array, function(value) {
566                         return matcher.test( value.label || value.value || value );
567                 });
568         }
572 // live region extension, adding a `messages` option
573 // NOTE: This is an experimental API. We are still investigating
574 // a full solution for string manipulation and internationalization.
575 $.widget( "ui.autocomplete", $.ui.autocomplete, {
576         options: {
577                 messages: {
578                         noResults: "No search results.",
579                         results: function( amount ) {
580                                 return amount + ( amount > 1 ? " results are" : " result is" ) +
581                                         " available, use up and down arrow keys to navigate.";
582                         }
583                 }
584         },
586         __response: function( content ) {
587                 var message;
588                 this._superApply( arguments );
589                 if ( this.options.disabled || this.cancelSearch ) {
590                         return;
591                 }
592                 if ( content && content.length ) {
593                         message = this.options.messages.results( content.length );
594                 } else {
595                         message = this.options.messages.noResults;
596                 }
597                 this.liveRegion.text( message );
598         }
602 }( jQuery ));