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