Alright, IE7 is fixed by r89613. IE6 stays problematic, for some reason it contains...
[mediawiki.git] / resources / jquery.ui / jquery.ui.autocomplete.js
blob93424831d263af52e15e348253165d00624fbfe8
1 /*
2  * jQuery UI Autocomplete 1.8.11
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.attr( "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         },
220         destroy: function() {
221                 this.element
222                         .removeClass( "ui-autocomplete-input" )
223                         .removeAttr( "autocomplete" )
224                         .removeAttr( "role" )
225                         .removeAttr( "aria-autocomplete" )
226                         .removeAttr( "aria-haspopup" );
227                 this.menu.element.remove();
228                 $.Widget.prototype.destroy.call( this );
229         },
231         _setOption: function( key, value ) {
232                 $.Widget.prototype._setOption.apply( this, arguments );
233                 if ( key === "source" ) {
234                         this._initSource();
235                 }
236                 if ( key === "appendTo" ) {
237                         this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
238                 }
239                 if ( key === "disabled" && value && this.xhr ) {
240                         this.xhr.abort();
241                 }
242         },
244         _initSource: function() {
245                 var self = this,
246                         array,
247                         url;
248                 if ( $.isArray(this.options.source) ) {
249                         array = this.options.source;
250                         this.source = function( request, response ) {
251                                 response( $.ui.autocomplete.filter(array, request.term) );
252                         };
253                 } else if ( typeof this.options.source === "string" ) {
254                         url = this.options.source;
255                         this.source = function( request, response ) {
256                                 if ( self.xhr ) {
257                                         self.xhr.abort();
258                                 }
259                                 self.xhr = $.ajax({
260                                         url: url,
261                                         data: request,
262                                         dataType: "json",
263                                         autocompleteRequest: ++requestIndex,
264                                         success: function( data, status ) {
265                                                 if ( this.autocompleteRequest === requestIndex ) {
266                                                         response( data );
267                                                 }
268                                         },
269                                         error: function() {
270                                                 if ( this.autocompleteRequest === requestIndex ) {
271                                                         response( [] );
272                                                 }
273                                         }
274                                 });
275                         };
276                 } else {
277                         this.source = this.options.source;
278                 }
279         },
281         search: function( value, event ) {
282                 value = value != null ? value : this.element.val();
284                 // always save the actual value, not the one passed as an argument
285                 this.term = this.element.val();
287                 if ( value.length < this.options.minLength ) {
288                         return this.close( event );
289                 }
291                 clearTimeout( this.closing );
292                 if ( this._trigger( "search", event ) === false ) {
293                         return;
294                 }
296                 return this._search( value );
297         },
299         _search: function( value ) {
300                 this.pending++;
301                 this.element.addClass( "ui-autocomplete-loading" );
303                 this.source( { term: value }, this.response );
304         },
306         _response: function( content ) {
307                 if ( !this.options.disabled && content && content.length ) {
308                         content = this._normalize( content );
309                         this._suggest( content );
310                         this._trigger( "open" );
311                 } else {
312                         this.close();
313                 }
314                 this.pending--;
315                 if ( !this.pending ) {
316                         this.element.removeClass( "ui-autocomplete-loading" );
317                 }
318         },
320         close: function( event ) {
321                 clearTimeout( this.closing );
322                 if ( this.menu.element.is(":visible") ) {
323                         this.menu.element.hide();
324                         this.menu.deactivate();
325                         this._trigger( "close", event );
326                 }
327         },
328         
329         _change: function( event ) {
330                 if ( this.previous !== this.element.val() ) {
331                         this._trigger( "change", event, { item: this.selectedItem } );
332                 }
333         },
335         _normalize: function( items ) {
336                 // assume all items have the right format when the first item is complete
337                 if ( items.length && items[0].label && items[0].value ) {
338                         return items;
339                 }
340                 return $.map( items, function(item) {
341                         if ( typeof item === "string" ) {
342                                 return {
343                                         label: item,
344                                         value: item
345                                 };
346                         }
347                         return $.extend({
348                                 label: item.label || item.value,
349                                 value: item.value || item.label
350                         }, item );
351                 });
352         },
354         _suggest: function( items ) {
355                 var ul = this.menu.element
356                         .empty()
357                         .zIndex( this.element.zIndex() + 1 );
358                 this._renderMenu( ul, items );
359                 // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
360                 this.menu.deactivate();
361                 this.menu.refresh();
363                 // size and position menu
364                 ul.show();
365                 this._resizeMenu();
366                 ul.position( $.extend({
367                         of: this.element
368                 }, this.options.position ));
370                 if ( this.options.autoFocus ) {
371                         this.menu.next( new $.Event("mouseover") );
372                 }
373         },
375         _resizeMenu: function() {
376                 var ul = this.menu.element;
377                 ul.outerWidth( Math.max(
378                         ul.width( "" ).outerWidth(),
379                         this.element.outerWidth()
380                 ) );
381         },
383         _renderMenu: function( ul, items ) {
384                 var self = this;
385                 $.each( items, function( index, item ) {
386                         self._renderItem( ul, item );
387                 });
388         },
390         _renderItem: function( ul, item) {
391                 return $( "<li></li>" )
392                         .data( "item.autocomplete", item )
393                         .append( $( "<a></a>" ).text( item.label ) )
394                         .appendTo( ul );
395         },
397         _move: function( direction, event ) {
398                 if ( !this.menu.element.is(":visible") ) {
399                         this.search( null, event );
400                         return;
401                 }
402                 if ( this.menu.first() && /^previous/.test(direction) ||
403                                 this.menu.last() && /^next/.test(direction) ) {
404                         this.element.val( this.term );
405                         this.menu.deactivate();
406                         return;
407                 }
408                 this.menu[ direction ]( event );
409         },
411         widget: function() {
412                 return this.menu.element;
413         }
416 $.extend( $.ui.autocomplete, {
417         escapeRegex: function( value ) {
418                 return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
419         },
420         filter: function(array, term) {
421                 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
422                 return $.grep( array, function(value) {
423                         return matcher.test( value.label || value.value || value );
424                 });
425         }
428 }( jQuery ));
431  * jQuery UI Menu (not officially released)
432  * 
433  * This widget isn't yet finished and the API is subject to change. We plan to finish
434  * it for the next release. You're welcome to give it a try anyway and give us feedback,
435  * as long as you're okay with migrating your code later on. We can help with that, too.
437  * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
438  * Dual licensed under the MIT or GPL Version 2 licenses.
439  * http://jquery.org/license
441  * http://docs.jquery.com/UI/Menu
443  * Depends:
444  *      jquery.ui.core.js
445  *  jquery.ui.widget.js
446  */
447 (function($) {
449 $.widget("ui.menu", {
450         _create: function() {
451                 var self = this;
452                 this.element
453                         .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
454                         .attr({
455                                 role: "listbox",
456                                 "aria-activedescendant": "ui-active-menuitem"
457                         })
458                         .click(function( event ) {
459                                 if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
460                                         return;
461                                 }
462                                 // temporary
463                                 event.preventDefault();
464                                 self.select( event );
465                         });
466                 this.refresh();
467         },
468         
469         refresh: function() {
470                 var self = this;
472                 // don't refresh list items that are already adapted
473                 var items = this.element.children("li:not(.ui-menu-item):has(a)")
474                         .addClass("ui-menu-item")
475                         .attr("role", "menuitem");
476                 
477                 items.children("a")
478                         .addClass("ui-corner-all")
479                         .attr("tabindex", -1)
480                         // mouseenter doesn't work with event delegation
481                         .mouseenter(function( event ) {
482                                 self.activate( event, $(this).parent() );
483                         })
484                         .mouseleave(function() {
485                                 self.deactivate();
486                         });
487         },
489         activate: function( event, item ) {
490                 this.deactivate();
491                 if (this.hasScroll()) {
492                         var offset = item.offset().top - this.element.offset().top,
493                                 scroll = this.element.attr("scrollTop"),
494                                 elementHeight = this.element.height();
495                         if (offset < 0) {
496                                 this.element.attr("scrollTop", scroll + offset);
497                         } else if (offset >= elementHeight) {
498                                 this.element.attr("scrollTop", scroll + offset - elementHeight + item.height());
499                         }
500                 }
501                 this.active = item.eq(0)
502                         .children("a")
503                                 .addClass("ui-state-hover")
504                                 .attr("id", "ui-active-menuitem")
505                         .end();
506                 this._trigger("focus", event, { item: item });
507         },
509         deactivate: function() {
510                 if (!this.active) { return; }
512                 this.active.children("a")
513                         .removeClass("ui-state-hover")
514                         .removeAttr("id");
515                 this._trigger("blur");
516                 this.active = null;
517         },
519         next: function(event) {
520                 this.move("next", ".ui-menu-item:first", event);
521         },
523         previous: function(event) {
524                 this.move("prev", ".ui-menu-item:last", event);
525         },
527         first: function() {
528                 return this.active && !this.active.prevAll(".ui-menu-item").length;
529         },
531         last: function() {
532                 return this.active && !this.active.nextAll(".ui-menu-item").length;
533         },
535         move: function(direction, edge, event) {
536                 if (!this.active) {
537                         this.activate(event, this.element.children(edge));
538                         return;
539                 }
540                 var next = this.active[direction + "All"](".ui-menu-item").eq(0);
541                 if (next.length) {
542                         this.activate(event, next);
543                 } else {
544                         this.activate(event, this.element.children(edge));
545                 }
546         },
548         // TODO merge with previousPage
549         nextPage: function(event) {
550                 if (this.hasScroll()) {
551                         // TODO merge with no-scroll-else
552                         if (!this.active || this.last()) {
553                                 this.activate(event, this.element.children(".ui-menu-item:first"));
554                                 return;
555                         }
556                         var base = this.active.offset().top,
557                                 height = this.element.height(),
558                                 result = this.element.children(".ui-menu-item").filter(function() {
559                                         var close = $(this).offset().top - base - height + $(this).height();
560                                         // TODO improve approximation
561                                         return close < 10 && close > -10;
562                                 });
564                         // TODO try to catch this earlier when scrollTop indicates the last page anyway
565                         if (!result.length) {
566                                 result = this.element.children(".ui-menu-item:last");
567                         }
568                         this.activate(event, result);
569                 } else {
570                         this.activate(event, this.element.children(".ui-menu-item")
571                                 .filter(!this.active || this.last() ? ":first" : ":last"));
572                 }
573         },
575         // TODO merge with nextPage
576         previousPage: function(event) {
577                 if (this.hasScroll()) {
578                         // TODO merge with no-scroll-else
579                         if (!this.active || this.first()) {
580                                 this.activate(event, this.element.children(".ui-menu-item:last"));
581                                 return;
582                         }
584                         var base = this.active.offset().top,
585                                 height = this.element.height();
586                                 result = this.element.children(".ui-menu-item").filter(function() {
587                                         var close = $(this).offset().top - base + height - $(this).height();
588                                         // TODO improve approximation
589                                         return close < 10 && close > -10;
590                                 });
592                         // TODO try to catch this earlier when scrollTop indicates the last page anyway
593                         if (!result.length) {
594                                 result = this.element.children(".ui-menu-item:first");
595                         }
596                         this.activate(event, result);
597                 } else {
598                         this.activate(event, this.element.children(".ui-menu-item")
599                                 .filter(!this.active || this.first() ? ":last" : ":first"));
600                 }
601         },
603         hasScroll: function() {
604                 return this.element.height() < this.element.attr("scrollHeight");
605         },
607         select: function( event ) {
608                 this._trigger("selected", event, { item: this.active });
609         }
612 }(jQuery));