]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - resources/jquery.ui/jquery.ui.autocomplete.js
MediaWiki 1.17.4
[autoinstalls/mediawiki.git] / resources / jquery.ui / jquery.ui.autocomplete.js
1 /*
2  * jQuery UI Autocomplete 1.8.2
3  *
4  * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
5  * Dual licensed under the MIT (MIT-LICENSE.txt)
6  * and GPL (GPL-LICENSE.txt) licenses.
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( $ ) {
16
17 $.widget( "ui.autocomplete", {
18         options: {
19                 minLength: 1,
20                 delay: 300
21         },
22         _create: function() {
23                 var self = this,
24                         doc = this.element[ 0 ].ownerDocument;
25                 this.element
26                         .addClass( "ui-autocomplete-input" )
27                         .attr( "autocomplete", "off" )
28                         // TODO verify these actually work as intended
29                         .attr({
30                                 role: "textbox",
31                                 "aria-autocomplete": "list",
32                                 "aria-haspopup": "true"
33                         })
34                         .bind( "keydown.autocomplete", function( event ) {
35                                 var keyCode = $.ui.keyCode;
36                                 switch( event.keyCode ) {
37                                 case keyCode.PAGE_UP:
38                                         self._move( "previousPage", event );
39                                         break;
40                                 case keyCode.PAGE_DOWN:
41                                         self._move( "nextPage", event );
42                                         break;
43                                 case keyCode.UP:
44                                         self._move( "previous", event );
45                                         // prevent moving cursor to beginning of text field in some browsers
46                                         event.preventDefault();
47                                         break;
48                                 case keyCode.DOWN:
49                                         self._move( "next", event );
50                                         // prevent moving cursor to end of text field in some browsers
51                                         event.preventDefault();
52                                         break;
53                                 case keyCode.ENTER:
54                                 case keyCode.NUMPAD_ENTER:
55                                         // when menu is open or has focus
56                                         if ( self.menu.active ) {
57                                                 event.preventDefault();
58                                         }
59                                         //passthrough - ENTER and TAB both select the current element
60                                 case keyCode.TAB:
61                                         if ( !self.menu.active ) {
62                                                 return;
63                                         }
64                                         self.menu.select( event );
65                                         break;
66                                 case keyCode.ESCAPE:
67                                         self.element.val( self.term );
68                                         self.close( event );
69                                         break;
70                                 case keyCode.LEFT:
71                                 case keyCode.RIGHT:
72                                 case keyCode.SHIFT:
73                                 case keyCode.CONTROL:
74                                 case keyCode.ALT:
75                                 case keyCode.COMMAND:
76                                 case keyCode.COMMAND_RIGHT:
77                                 case keyCode.INSERT:
78                                 case keyCode.CAPS_LOCK:
79                                 case keyCode.END:
80                                 case keyCode.HOME:
81                                         // ignore metakeys (shift, ctrl, alt)
82                                         break;
83                                 default:
84                                         // keypress is triggered before the input value is changed
85                                         clearTimeout( self.searching );
86                                         self.searching = setTimeout(function() {
87                                                 self.search( null, event );
88                                         }, self.options.delay );
89                                         break;
90                                 }
91                         })
92                         .bind( "focus.autocomplete", function() {
93                                 self.selectedItem = null;
94                                 self.previous = self.element.val();
95                         })
96                         .bind( "blur.autocomplete", function( event ) {
97                                 clearTimeout( self.searching );
98                                 // clicks on the menu (or a button to trigger a search) will cause a blur event
99                                 self.closing = setTimeout(function() {
100                                         self.close( event );
101                                         self._change( event );
102                                 }, 150 );
103                         });
104                 this._initSource();
105                 this.response = function() {
106                         return self._response.apply( self, arguments );
107                 };
108                 this.menu = $( "<ul></ul>" )
109                         .addClass( "ui-autocomplete" )
110                         .appendTo( "body", doc )
111                         // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
112                         .mousedown(function() {
113                                 // use another timeout to make sure the blur-event-handler on the input was already triggered
114                                 setTimeout(function() {
115                                         clearTimeout( self.closing );
116                                 }, 13);
117                         })
118                         .menu({
119                                 focus: function( event, ui ) {
120                                         var item = ui.item.data( "item.autocomplete" );
121                                         if ( false !== self._trigger( "focus", null, { item: item } ) ) {
122                                                 // use value to match what will end up in the input, if it was a key event
123                                                 if ( /^key/.test(event.originalEvent.type) ) {
124                                                         self.element.val( item.value );
125                                                 }
126                                         }
127                                 },
128                                 selected: function( event, ui ) {
129                                         var item = ui.item.data( "item.autocomplete" );
130                                         if ( false !== self._trigger( "select", event, { item: item } ) ) {
131                                                 self.element.val( item.value );
132                                         }
133                                         self.close( event );
134                                         // only trigger when focus was lost (click on menu)
135                                         var previous = self.previous;
136                                         if ( self.element[0] !== doc.activeElement ) {
137                                                 self.element.focus();
138                                                 self.previous = previous;
139                                         }
140                                         self.selectedItem = item;
141                                 },
142                                 blur: function( event, ui ) {
143                                         if ( self.menu.element.is(":visible") ) {
144                                                 self.element.val( self.term );
145                                         }
146                                 }
147                         })
148                         .zIndex( this.element.zIndex() + 1 )
149                         // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
150                         .css({ top: 0, left: 0 })
151                         .hide()
152                         .data( "menu" );
153                 if ( $.fn.bgiframe ) {
154                          this.menu.element.bgiframe();
155                 }
156         },
157
158         destroy: function() {
159                 this.element
160                         .removeClass( "ui-autocomplete-input" )
161                         .removeAttr( "autocomplete" )
162                         .removeAttr( "role" )
163                         .removeAttr( "aria-autocomplete" )
164                         .removeAttr( "aria-haspopup" );
165                 this.menu.element.remove();
166                 $.Widget.prototype.destroy.call( this );
167         },
168
169         _setOption: function( key ) {
170                 $.Widget.prototype._setOption.apply( this, arguments );
171                 if ( key === "source" ) {
172                         this._initSource();
173                 }
174         },
175
176         _initSource: function() {
177                 var array,
178                         url;
179                 if ( $.isArray(this.options.source) ) {
180                         array = this.options.source;
181                         this.source = function( request, response ) {
182                                 response( $.ui.autocomplete.filter(array, request.term) );
183                         };
184                 } else if ( typeof this.options.source === "string" ) {
185                         url = this.options.source;
186                         this.source = function( request, response ) {
187                                 $.getJSON( url, request, response );
188                         };
189                 } else {
190                         this.source = this.options.source;
191                 }
192         },
193
194         search: function( value, event ) {
195                 value = value != null ? value : this.element.val();
196                 if ( value.length < this.options.minLength ) {
197                         return this.close( event );
198                 }
199
200                 clearTimeout( this.closing );
201                 if ( this._trigger("search") === false ) {
202                         return;
203                 }
204
205                 return this._search( value );
206         },
207
208         _search: function( value ) {
209                 this.term = this.element
210                         .addClass( "ui-autocomplete-loading" )
211                         // always save the actual value, not the one passed as an argument
212                         .val();
213
214                 this.source( { term: value }, this.response );
215         },
216
217         _response: function( content ) {
218                 if ( content.length ) {
219                         content = this._normalize( content );
220                         this._suggest( content );
221                         this._trigger( "open" );
222                 } else {
223                         this.close();
224                 }
225                 this.element.removeClass( "ui-autocomplete-loading" );
226         },
227
228         close: function( event ) {
229                 clearTimeout( this.closing );
230                 if ( this.menu.element.is(":visible") ) {
231                         this._trigger( "close", event );
232                         this.menu.element.hide();
233                         this.menu.deactivate();
234                 }
235         },
236         
237         _change: function( event ) {
238                 if ( this.previous !== this.element.val() ) {
239                         this._trigger( "change", event, { item: this.selectedItem } );
240                 }
241         },
242
243         _normalize: function( items ) {
244                 // assume all items have the right format when the first item is complete
245                 if ( items.length && items[0].label && items[0].value ) {
246                         return items;
247                 }
248                 return $.map( items, function(item) {
249                         if ( typeof item === "string" ) {
250                                 return {
251                                         label: item,
252                                         value: item
253                                 };
254                         }
255                         return $.extend({
256                                 label: item.label || item.value,
257                                 value: item.value || item.label
258                         }, item );
259                 });
260         },
261
262         _suggest: function( items ) {
263                 var ul = this.menu.element
264                                 .empty()
265                                 .zIndex( this.element.zIndex() + 1 ),
266                         menuWidth,
267                         textWidth;
268                 this._renderMenu( ul, items );
269                 // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
270                 this.menu.deactivate();
271                 this.menu.refresh();
272                 this.menu.element.show().position({
273                         my: "left top",
274                         at: "left bottom",
275                         of: this.element,
276                         collision: "none"
277                 });
278
279                 menuWidth = ul.width( "" ).width();
280                 textWidth = this.element.width();
281                 ul.width( Math.max( menuWidth, textWidth ) );
282         },
283         
284         _renderMenu: function( ul, items ) {
285                 var self = this;
286                 $.each( items, function( index, item ) {
287                         self._renderItem( ul, item );
288                 });
289         },
290
291         _renderItem: function( ul, item) {
292                 return $( "<li></li>" )
293                         .data( "item.autocomplete", item )
294                         .append( "<a>" + item.label + "</a>" )
295                         .appendTo( ul );
296         },
297
298         _move: function( direction, event ) {
299                 if ( !this.menu.element.is(":visible") ) {
300                         this.search( null, event );
301                         return;
302                 }
303                 if ( this.menu.first() && /^previous/.test(direction) ||
304                                 this.menu.last() && /^next/.test(direction) ) {
305                         this.element.val( this.term );
306                         this.menu.deactivate();
307                         return;
308                 }
309                 this.menu[ direction ]( event );
310         },
311
312         widget: function() {
313                 return this.menu.element;
314         }
315 });
316
317 $.extend( $.ui.autocomplete, {
318         escapeRegex: function( value ) {
319                 return value.replace( /([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1" );
320         },
321         filter: function(array, term) {
322                 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
323                 return $.grep( array, function(value) {
324                         return matcher.test( value.label || value.value || value );
325                 });
326         }
327 });
328
329 }( jQuery ));
330
331 /*
332  * jQuery UI Menu (not officially released)
333  * 
334  * This widget isn't yet finished and the API is subject to change. We plan to finish
335  * it for the next release. You're welcome to give it a try anyway and give us feedback,
336  * as long as you're okay with migrating your code later on. We can help with that, too.
337  *
338  * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
339  * Dual licensed under the MIT (MIT-LICENSE.txt)
340  * and GPL (GPL-LICENSE.txt) licenses.
341  *
342  * http://docs.jquery.com/UI/Menu
343  *
344  * Depends:
345  *      jquery.ui.core.js
346  *  jquery.ui.widget.js
347  */
348 (function($) {
349
350 $.widget("ui.menu", {
351         _create: function() {
352                 var self = this;
353                 this.element
354                         .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
355                         .attr({
356                                 role: "listbox",
357                                 "aria-activedescendant": "ui-active-menuitem"
358                         })
359                         .click(function( event ) {
360                                 if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
361                                         return;
362                                 }
363                                 // temporary
364                                 event.preventDefault();
365                                 self.select( event );
366                         });
367                 this.refresh();
368         },
369         
370         refresh: function() {
371                 var self = this;
372
373                 // don't refresh list items that are already adapted
374                 var items = this.element.children("li:not(.ui-menu-item):has(a)")
375                         .addClass("ui-menu-item")
376                         .attr("role", "menuitem");
377                 
378                 items.children("a")
379                         .addClass("ui-corner-all")
380                         .attr("tabindex", -1)
381                         // mouseenter doesn't work with event delegation
382                         .mouseenter(function( event ) {
383                                 self.activate( event, $(this).parent() );
384                         })
385                         .mouseleave(function() {
386                                 self.deactivate();
387                         });
388         },
389
390         activate: function( event, item ) {
391                 this.deactivate();
392                 if (this.hasScroll()) {
393                         var offset = item.offset().top - this.element.offset().top,
394                                 scroll = this.element.attr("scrollTop"),
395                                 elementHeight = this.element.height();
396                         if (offset < 0) {
397                                 this.element.attr("scrollTop", scroll + offset);
398                         } else if (offset > elementHeight) {
399                                 this.element.attr("scrollTop", scroll + offset - elementHeight + item.height());
400                         }
401                 }
402                 this.active = item.eq(0)
403                         .children("a")
404                                 .addClass("ui-state-hover")
405                                 .attr("id", "ui-active-menuitem")
406                         .end();
407                 this._trigger("focus", event, { item: item });
408         },
409
410         deactivate: function() {
411                 if (!this.active) { return; }
412
413                 this.active.children("a")
414                         .removeClass("ui-state-hover")
415                         .removeAttr("id");
416                 this._trigger("blur");
417                 this.active = null;
418         },
419
420         next: function(event) {
421                 this.move("next", ".ui-menu-item:first", event);
422         },
423
424         previous: function(event) {
425                 this.move("prev", ".ui-menu-item:last", event);
426         },
427
428         first: function() {
429                 return this.active && !this.active.prev().length;
430         },
431
432         last: function() {
433                 return this.active && !this.active.next().length;
434         },
435
436         move: function(direction, edge, event) {
437                 if (!this.active) {
438                         this.activate(event, this.element.children(edge));
439                         return;
440                 }
441                 var next = this.active[direction + "All"](".ui-menu-item").eq(0);
442                 if (next.length) {
443                         this.activate(event, next);
444                 } else {
445                         this.activate(event, this.element.children(edge));
446                 }
447         },
448
449         // TODO merge with previousPage
450         nextPage: function(event) {
451                 if (this.hasScroll()) {
452                         // TODO merge with no-scroll-else
453                         if (!this.active || this.last()) {
454                                 this.activate(event, this.element.children(":first"));
455                                 return;
456                         }
457                         var base = this.active.offset().top,
458                                 height = this.element.height(),
459                                 result = this.element.children("li").filter(function() {
460                                         var close = $(this).offset().top - base - height + $(this).height();
461                                         // TODO improve approximation
462                                         return close < 10 && close > -10;
463                                 });
464
465                         // TODO try to catch this earlier when scrollTop indicates the last page anyway
466                         if (!result.length) {
467                                 result = this.element.children(":last");
468                         }
469                         this.activate(event, result);
470                 } else {
471                         this.activate(event, this.element.children(!this.active || this.last() ? ":first" : ":last"));
472                 }
473         },
474
475         // TODO merge with nextPage
476         previousPage: function(event) {
477                 if (this.hasScroll()) {
478                         // TODO merge with no-scroll-else
479                         if (!this.active || this.first()) {
480                                 this.activate(event, this.element.children(":last"));
481                                 return;
482                         }
483
484                         var base = this.active.offset().top,
485                                 height = this.element.height();
486                                 result = this.element.children("li").filter(function() {
487                                         var close = $(this).offset().top - base + height - $(this).height();
488                                         // TODO improve approximation
489                                         return close < 10 && close > -10;
490                                 });
491
492                         // TODO try to catch this earlier when scrollTop indicates the last page anyway
493                         if (!result.length) {
494                                 result = this.element.children(":first");
495                         }
496                         this.activate(event, result);
497                 } else {
498                         this.activate(event, this.element.children(!this.active || this.first() ? ":last" : ":first"));
499                 }
500         },
501
502         hasScroll: function() {
503                 return this.element.height() < this.element.attr("scrollHeight");
504         },
505
506         select: function( event ) {
507                 this._trigger("selected", event, { item: this.active });
508         }
509 });
510
511 }(jQuery));