Merge "Bug 35681 - Upgrade jQuery UI to 1.8.18"
[mediawiki.git] / resources / jquery.ui / jquery.ui.autocomplete.js
blob8889ca968a11ae96e9c51d260550fb16d2b730ec
1 /*
2  * jQuery UI Autocomplete 1.8.18
3  *
4  * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
5  * Dual licensed under the MIT or GPL Version 2 licenses.
6  * http://jquery.org/license
7  *
8  * http://docs.jquery.com/UI/Autocomplete
9  *
10  * Depends:
11  *      jquery.ui.core.js
12  *      jquery.ui.widget.js
13  *      jquery.ui.position.js
14  */
15 (function( $, undefined ) {
17 // used to prevent race conditions with remote data sources
18 var requestIndex = 0;
20 $.widget( "ui.autocomplete", {
21         options: {
22                 appendTo: "body",
23                 autoFocus: false,
24                 delay: 300,
25                 minLength: 1,
26                 position: {
27                         my: "left top",
28                         at: "left bottom",
29                         collision: "none"
30                 },
31                 source: null
32         },
34         pending: 0,
36         _create: function() {
37                 var self = this,
38                         doc = this.element[ 0 ].ownerDocument,
39                         suppressKeyPress;
41                 this.element
42                         .addClass( "ui-autocomplete-input" )
43                         .attr( "autocomplete", "off" )
44                         // TODO verify these actually work as intended
45                         .attr({
46                                 role: "textbox",
47                                 "aria-autocomplete": "list",
48                                 "aria-haspopup": "true"
49                         })
50                         .bind( "keydown.autocomplete", function( event ) {
51                                 if ( self.options.disabled || self.element.propAttr( "readOnly" ) ) {
52                                         return;
53                                 }
55                                 suppressKeyPress = false;
56                                 var keyCode = $.ui.keyCode;
57                                 switch( event.keyCode ) {
58                                 case keyCode.PAGE_UP:
59                                         self._move( "previousPage", event );
60                                         break;
61                                 case keyCode.PAGE_DOWN:
62                                         self._move( "nextPage", event );
63                                         break;
64                                 case keyCode.UP:
65                                         self._move( "previous", event );
66                                         // prevent moving cursor to beginning of text field in some browsers
67                                         event.preventDefault();
68                                         break;
69                                 case keyCode.DOWN:
70                                         self._move( "next", event );
71                                         // prevent moving cursor to end of text field in some browsers
72                                         event.preventDefault();
73                                         break;
74                                 case keyCode.ENTER:
75                                 case keyCode.NUMPAD_ENTER:
76                                         // when menu is open and has focus
77                                         if ( self.menu.active ) {
78                                                 // #6055 - Opera still allows the keypress to occur
79                                                 // which causes forms to submit
80                                                 suppressKeyPress = true;
81                                                 event.preventDefault();
82                                         }
83                                         //passthrough - ENTER and TAB both select the current element
84                                 case keyCode.TAB:
85                                         if ( !self.menu.active ) {
86                                                 return;
87                                         }
88                                         self.menu.select( event );
89                                         break;
90                                 case keyCode.ESCAPE:
91                                         self.element.val( self.term );
92                                         self.close( event );
93                                         break;
94                                 default:
95                                         // keypress is triggered before the input value is changed
96                                         clearTimeout( self.searching );
97                                         self.searching = setTimeout(function() {
98                                                 // only search if the value has changed
99                                                 if ( self.term != self.element.val() ) {
100                                                         self.selectedItem = null;
101                                                         self.search( null, event );
102                                                 }
103                                         }, self.options.delay );
104                                         break;
105                                 }
106                         })
107                         .bind( "keypress.autocomplete", function( event ) {
108                                 if ( suppressKeyPress ) {
109                                         suppressKeyPress = false;
110                                         event.preventDefault();
111                                 }
112                         })
113                         .bind( "focus.autocomplete", function() {
114                                 if ( self.options.disabled ) {
115                                         return;
116                                 }
118                                 self.selectedItem = null;
119                                 self.previous = self.element.val();
120                         })
121                         .bind( "blur.autocomplete", function( event ) {
122                                 if ( self.options.disabled ) {
123                                         return;
124                                 }
126                                 clearTimeout( self.searching );
127                                 // clicks on the menu (or a button to trigger a search) will cause a blur event
128                                 self.closing = setTimeout(function() {
129                                         self.close( event );
130                                         self._change( event );
131                                 }, 150 );
132                         });
133                 this._initSource();
134                 this.response = function() {
135                         return self._response.apply( self, arguments );
136                 };
137                 this.menu = $( "<ul></ul>" )
138                         .addClass( "ui-autocomplete" )
139                         .appendTo( $( this.options.appendTo || "body", doc )[0] )
140                         // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
141                         .mousedown(function( event ) {
142                                 // clicking on the scrollbar causes focus to shift to the body
143                                 // but we can't detect a mouseup or a click immediately afterward
144                                 // so we have to track the next mousedown and close the menu if
145                                 // the user clicks somewhere outside of the autocomplete
146                                 var menuElement = self.menu.element[ 0 ];
147                                 if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
148                                         setTimeout(function() {
149                                                 $( document ).one( 'mousedown', function( event ) {
150                                                         if ( event.target !== self.element[ 0 ] &&
151                                                                 event.target !== menuElement &&
152                                                                 !$.ui.contains( menuElement, event.target ) ) {
153                                                                 self.close();
154                                                         }
155                                                 });
156                                         }, 1 );
157                                 }
159                                 // use another timeout to make sure the blur-event-handler on the input was already triggered
160                                 setTimeout(function() {
161                                         clearTimeout( self.closing );
162                                 }, 13);
163                         })
164                         .menu({
165                                 focus: function( event, ui ) {
166                                         var item = ui.item.data( "item.autocomplete" );
167                                         if ( false !== self._trigger( "focus", event, { item: item } ) ) {
168                                                 // use value to match what will end up in the input, if it was a key event
169                                                 if ( /^key/.test(event.originalEvent.type) ) {
170                                                         self.element.val( item.value );
171                                                 }
172                                         }
173                                 },
174                                 selected: function( event, ui ) {
175                                         var item = ui.item.data( "item.autocomplete" ),
176                                                 previous = self.previous;
178                                         // only trigger when focus was lost (click on menu)
179                                         if ( self.element[0] !== doc.activeElement ) {
180                                                 self.element.focus();
181                                                 self.previous = previous;
182                                                 // #6109 - IE triggers two focus events and the second
183                                                 // is asynchronous, so we need to reset the previous
184                                                 // term synchronously and asynchronously :-(
185                                                 setTimeout(function() {
186                                                         self.previous = previous;
187                                                         self.selectedItem = item;
188                                                 }, 1);
189                                         }
191                                         if ( false !== self._trigger( "select", event, { item: item } ) ) {
192                                                 self.element.val( item.value );
193                                         }
194                                         // reset the term after the select event
195                                         // this allows custom select handling to work properly
196                                         self.term = self.element.val();
198                                         self.close( event );
199                                         self.selectedItem = item;
200                                 },
201                                 blur: function( event, ui ) {
202                                         // don't set the value of the text field if it's already correct
203                                         // this prevents moving the cursor unnecessarily
204                                         if ( self.menu.element.is(":visible") &&
205                                                 ( self.element.val() !== self.term ) ) {
206                                                 self.element.val( self.term );
207                                         }
208                                 }
209                         })
210                         .zIndex( this.element.zIndex() + 1 )
211                         // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
212                         .css({ top: 0, left: 0 })
213                         .hide()
214                         .data( "menu" );
215                 if ( $.fn.bgiframe ) {
216                          this.menu.element.bgiframe();
217                 }
218                 // turning off autocomplete prevents the browser from remembering the
219                 // value when navigating through history, so we re-enable autocomplete
220                 // if the page is unloaded before the widget is destroyed. #7790
221                 self.beforeunloadHandler = function() {
222                         self.element.removeAttr( "autocomplete" );
223                 };
224                 $( window ).bind( "beforeunload", self.beforeunloadHandler );
225         },
227         destroy: function() {
228                 this.element
229                         .removeClass( "ui-autocomplete-input" )
230                         .removeAttr( "autocomplete" )
231                         .removeAttr( "role" )
232                         .removeAttr( "aria-autocomplete" )
233                         .removeAttr( "aria-haspopup" );
234                 this.menu.element.remove();
235                 $( window ).unbind( "beforeunload", this.beforeunloadHandler );
236                 $.Widget.prototype.destroy.call( this );
237         },
239         _setOption: function( key, value ) {
240                 $.Widget.prototype._setOption.apply( this, arguments );
241                 if ( key === "source" ) {
242                         this._initSource();
243                 }
244                 if ( key === "appendTo" ) {
245                         this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
246                 }
247                 if ( key === "disabled" && value && this.xhr ) {
248                         this.xhr.abort();
249                 }
250         },
252         _initSource: function() {
253                 var self = this,
254                         array,
255                         url;
256                 if ( $.isArray(this.options.source) ) {
257                         array = this.options.source;
258                         this.source = function( request, response ) {
259                                 response( $.ui.autocomplete.filter(array, request.term) );
260                         };
261                 } else if ( typeof this.options.source === "string" ) {
262                         url = this.options.source;
263                         this.source = function( request, response ) {
264                                 if ( self.xhr ) {
265                                         self.xhr.abort();
266                                 }
267                                 self.xhr = $.ajax({
268                                         url: url,
269                                         data: request,
270                                         dataType: "json",
271                                         context: {
272                                                 autocompleteRequest: ++requestIndex
273                                         },
274                                         success: function( data, status ) {
275                                                 if ( this.autocompleteRequest === requestIndex ) {
276                                                         response( data );
277                                                 }
278                                         },
279                                         error: function() {
280                                                 if ( this.autocompleteRequest === requestIndex ) {
281                                                         response( [] );
282                                                 }
283                                         }
284                                 });
285                         };
286                 } else {
287                         this.source = this.options.source;
288                 }
289         },
291         search: function( value, event ) {
292                 value = value != null ? value : this.element.val();
294                 // always save the actual value, not the one passed as an argument
295                 this.term = this.element.val();
297                 if ( value.length < this.options.minLength ) {
298                         return this.close( event );
299                 }
301                 clearTimeout( this.closing );
302                 if ( this._trigger( "search", event ) === false ) {
303                         return;
304                 }
306                 return this._search( value );
307         },
309         _search: function( value ) {
310                 this.pending++;
311                 this.element.addClass( "ui-autocomplete-loading" );
313                 this.source( { term: value }, this.response );
314         },
316         _response: function( content ) {
317                 if ( !this.options.disabled && content && content.length ) {
318                         content = this._normalize( content );
319                         this._suggest( content );
320                         this._trigger( "open" );
321                 } else {
322                         this.close();
323                 }
324                 this.pending--;
325                 if ( !this.pending ) {
326                         this.element.removeClass( "ui-autocomplete-loading" );
327                 }
328         },
330         close: function( event ) {
331                 clearTimeout( this.closing );
332                 if ( this.menu.element.is(":visible") ) {
333                         this.menu.element.hide();
334                         this.menu.deactivate();
335                         this._trigger( "close", event );
336                 }
337         },
338         
339         _change: function( event ) {
340                 if ( this.previous !== this.element.val() ) {
341                         this._trigger( "change", event, { item: this.selectedItem } );
342                 }
343         },
345         _normalize: function( items ) {
346                 // assume all items have the right format when the first item is complete
347                 if ( items.length && items[0].label && items[0].value ) {
348                         return items;
349                 }
350                 return $.map( items, function(item) {
351                         if ( typeof item === "string" ) {
352                                 return {
353                                         label: item,
354                                         value: item
355                                 };
356                         }
357                         return $.extend({
358                                 label: item.label || item.value,
359                                 value: item.value || item.label
360                         }, item );
361                 });
362         },
364         _suggest: function( items ) {
365                 var ul = this.menu.element
366                         .empty()
367                         .zIndex( this.element.zIndex() + 1 );
368                 this._renderMenu( ul, items );
369                 // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
370                 this.menu.deactivate();
371                 this.menu.refresh();
373                 // size and position menu
374                 ul.show();
375                 this._resizeMenu();
376                 ul.position( $.extend({
377                         of: this.element
378                 }, this.options.position ));
380                 if ( this.options.autoFocus ) {
381                         this.menu.next( new $.Event("mouseover") );
382                 }
383         },
385         _resizeMenu: function() {
386                 var ul = this.menu.element;
387                 ul.outerWidth( Math.max(
388                         // Firefox wraps long text (possibly a rounding bug)
389                         // so we add 1px to avoid the wrapping (#7513)
390                         ul.width( "" ).outerWidth() + 1,
391                         this.element.outerWidth()
392                 ) );
393         },
395         _renderMenu: function( ul, items ) {
396                 var self = this;
397                 $.each( items, function( index, item ) {
398                         self._renderItem( ul, item );
399                 });
400         },
402         _renderItem: function( ul, item) {
403                 return $( "<li></li>" )
404                         .data( "item.autocomplete", item )
405                         .append( $( "<a></a>" ).text( item.label ) )
406                         .appendTo( ul );
407         },
409         _move: function( direction, event ) {
410                 if ( !this.menu.element.is(":visible") ) {
411                         this.search( null, event );
412                         return;
413                 }
414                 if ( this.menu.first() && /^previous/.test(direction) ||
415                                 this.menu.last() && /^next/.test(direction) ) {
416                         this.element.val( this.term );
417                         this.menu.deactivate();
418                         return;
419                 }
420                 this.menu[ direction ]( event );
421         },
423         widget: function() {
424                 return this.menu.element;
425         }
428 $.extend( $.ui.autocomplete, {
429         escapeRegex: function( value ) {
430                 return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
431         },
432         filter: function(array, term) {
433                 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
434                 return $.grep( array, function(value) {
435                         return matcher.test( value.label || value.value || value );
436                 });
437         }
440 }( jQuery ));
443  * jQuery UI Menu (not officially released)
444  * 
445  * This widget isn't yet finished and the API is subject to change. We plan to finish
446  * it for the next release. You're welcome to give it a try anyway and give us feedback,
447  * as long as you're okay with migrating your code later on. We can help with that, too.
449  * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
450  * Dual licensed under the MIT or GPL Version 2 licenses.
451  * http://jquery.org/license
453  * http://docs.jquery.com/UI/Menu
455  * Depends:
456  *      jquery.ui.core.js
457  *  jquery.ui.widget.js
458  */
459 (function($) {
461 $.widget("ui.menu", {
462         _create: function() {
463                 var self = this;
464                 this.element
465                         .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
466                         .attr({
467                                 role: "listbox",
468                                 "aria-activedescendant": "ui-active-menuitem"
469                         })
470                         .click(function( event ) {
471                                 if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
472                                         return;
473                                 }
474                                 // temporary
475                                 event.preventDefault();
476                                 self.select( event );
477                         });
478                 this.refresh();
479         },
480         
481         refresh: function() {
482                 var self = this;
484                 // don't refresh list items that are already adapted
485                 var items = this.element.children("li:not(.ui-menu-item):has(a)")
486                         .addClass("ui-menu-item")
487                         .attr("role", "menuitem");
488                 
489                 items.children("a")
490                         .addClass("ui-corner-all")
491                         .attr("tabindex", -1)
492                         // mouseenter doesn't work with event delegation
493                         .mouseenter(function( event ) {
494                                 self.activate( event, $(this).parent() );
495                         })
496                         .mouseleave(function() {
497                                 self.deactivate();
498                         });
499         },
501         activate: function( event, item ) {
502                 this.deactivate();
503                 if (this.hasScroll()) {
504                         var offset = item.offset().top - this.element.offset().top,
505                                 scroll = this.element.scrollTop(),
506                                 elementHeight = this.element.height();
507                         if (offset < 0) {
508                                 this.element.scrollTop( scroll + offset);
509                         } else if (offset >= elementHeight) {
510                                 this.element.scrollTop( scroll + offset - elementHeight + item.height());
511                         }
512                 }
513                 this.active = item.eq(0)
514                         .children("a")
515                                 .addClass("ui-state-hover")
516                                 .attr("id", "ui-active-menuitem")
517                         .end();
518                 this._trigger("focus", event, { item: item });
519         },
521         deactivate: function() {
522                 if (!this.active) { return; }
524                 this.active.children("a")
525                         .removeClass("ui-state-hover")
526                         .removeAttr("id");
527                 this._trigger("blur");
528                 this.active = null;
529         },
531         next: function(event) {
532                 this.move("next", ".ui-menu-item:first", event);
533         },
535         previous: function(event) {
536                 this.move("prev", ".ui-menu-item:last", event);
537         },
539         first: function() {
540                 return this.active && !this.active.prevAll(".ui-menu-item").length;
541         },
543         last: function() {
544                 return this.active && !this.active.nextAll(".ui-menu-item").length;
545         },
547         move: function(direction, edge, event) {
548                 if (!this.active) {
549                         this.activate(event, this.element.children(edge));
550                         return;
551                 }
552                 var next = this.active[direction + "All"](".ui-menu-item").eq(0);
553                 if (next.length) {
554                         this.activate(event, next);
555                 } else {
556                         this.activate(event, this.element.children(edge));
557                 }
558         },
560         // TODO merge with previousPage
561         nextPage: function(event) {
562                 if (this.hasScroll()) {
563                         // TODO merge with no-scroll-else
564                         if (!this.active || this.last()) {
565                                 this.activate(event, this.element.children(".ui-menu-item:first"));
566                                 return;
567                         }
568                         var base = this.active.offset().top,
569                                 height = this.element.height(),
570                                 result = this.element.children(".ui-menu-item").filter(function() {
571                                         var close = $(this).offset().top - base - height + $(this).height();
572                                         // TODO improve approximation
573                                         return close < 10 && close > -10;
574                                 });
576                         // TODO try to catch this earlier when scrollTop indicates the last page anyway
577                         if (!result.length) {
578                                 result = this.element.children(".ui-menu-item:last");
579                         }
580                         this.activate(event, result);
581                 } else {
582                         this.activate(event, this.element.children(".ui-menu-item")
583                                 .filter(!this.active || this.last() ? ":first" : ":last"));
584                 }
585         },
587         // TODO merge with nextPage
588         previousPage: function(event) {
589                 if (this.hasScroll()) {
590                         // TODO merge with no-scroll-else
591                         if (!this.active || this.first()) {
592                                 this.activate(event, this.element.children(".ui-menu-item:last"));
593                                 return;
594                         }
596                         var base = this.active.offset().top,
597                                 height = this.element.height();
598                                 result = this.element.children(".ui-menu-item").filter(function() {
599                                         var close = $(this).offset().top - base + height - $(this).height();
600                                         // TODO improve approximation
601                                         return close < 10 && close > -10;
602                                 });
604                         // TODO try to catch this earlier when scrollTop indicates the last page anyway
605                         if (!result.length) {
606                                 result = this.element.children(".ui-menu-item:first");
607                         }
608                         this.activate(event, result);
609                 } else {
610                         this.activate(event, this.element.children(".ui-menu-item")
611                                 .filter(!this.active || this.first() ? ":last" : ":first"));
612                 }
613         },
615         hasScroll: function() {
616                 return this.element.height() < this.element[ $.fn.prop ? "prop" : "attr" ]("scrollHeight");
617         },
619         select: function( event ) {
620                 this._trigger("selected", event, { item: this.active });
621         }
624 }(jQuery));