UPDATE 4.4.0.0
[phpmyadmin.git] / js / jquery / src / jquery-ui / menu.js
blobcc4e3bdcbfa9f43109c79ec09522aa3444a31da0
1 /*!
2  * jQuery UI Menu 1.11.2
3  * http://jqueryui.com
4  *
5  * Copyright 2014 jQuery Foundation and other contributors
6  * Released under the MIT license.
7  * http://jquery.org/license
8  *
9  * http://api.jqueryui.com/menu/
10  */
11 (function( factory ) {
12         if ( typeof define === "function" && define.amd ) {
14                 // AMD. Register as an anonymous module.
15                 define([
16                         "jquery",
17                         "./core",
18                         "./widget",
19                         "./position"
20                 ], factory );
21         } else {
23                 // Browser globals
24                 factory( jQuery );
25         }
26 }(function( $ ) {
28 return $.widget( "ui.menu", {
29         version: "1.11.2",
30         defaultElement: "<ul>",
31         delay: 300,
32         options: {
33                 icons: {
34                         submenu: "ui-icon-carat-1-e"
35                 },
36                 items: "> *",
37                 menus: "ul",
38                 position: {
39                         my: "left-1 top",
40                         at: "right top"
41                 },
42                 role: "menu",
44                 // callbacks
45                 blur: null,
46                 focus: null,
47                 select: null
48         },
50         _create: function() {
51                 this.activeMenu = this.element;
53                 // Flag used to prevent firing of the click handler
54                 // as the event bubbles up through nested menus
55                 this.mouseHandled = false;
56                 this.element
57                         .uniqueId()
58                         .addClass( "ui-menu ui-widget ui-widget-content" )
59                         .toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length )
60                         .attr({
61                                 role: this.options.role,
62                                 tabIndex: 0
63                         });
65                 if ( this.options.disabled ) {
66                         this.element
67                                 .addClass( "ui-state-disabled" )
68                                 .attr( "aria-disabled", "true" );
69                 }
71                 this._on({
72                         // Prevent focus from sticking to links inside menu after clicking
73                         // them (focus should always stay on UL during navigation).
74                         "mousedown .ui-menu-item": function( event ) {
75                                 event.preventDefault();
76                         },
77                         "click .ui-menu-item": function( event ) {
78                                 var target = $( event.target );
79                                 if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) {
80                                         this.select( event );
82                                         // Only set the mouseHandled flag if the event will bubble, see #9469.
83                                         if ( !event.isPropagationStopped() ) {
84                                                 this.mouseHandled = true;
85                                         }
87                                         // Open submenu on click
88                                         if ( target.has( ".ui-menu" ).length ) {
89                                                 this.expand( event );
90                                         } else if ( !this.element.is( ":focus" ) && $( this.document[ 0 ].activeElement ).closest( ".ui-menu" ).length ) {
92                                                 // Redirect focus to the menu
93                                                 this.element.trigger( "focus", [ true ] );
95                                                 // If the active item is on the top level, let it stay active.
96                                                 // Otherwise, blur the active item since it is no longer visible.
97                                                 if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) {
98                                                         clearTimeout( this.timer );
99                                                 }
100                                         }
101                                 }
102                         },
103                         "mouseenter .ui-menu-item": function( event ) {
104                                 // Ignore mouse events while typeahead is active, see #10458.
105                                 // Prevents focusing the wrong item when typeahead causes a scroll while the mouse
106                                 // is over an item in the menu
107                                 if ( this.previousFilter ) {
108                                         return;
109                                 }
110                                 var target = $( event.currentTarget );
111                                 // Remove ui-state-active class from siblings of the newly focused menu item
112                                 // to avoid a jump caused by adjacent elements both having a class with a border
113                                 target.siblings( ".ui-state-active" ).removeClass( "ui-state-active" );
114                                 this.focus( event, target );
115                         },
116                         mouseleave: "collapseAll",
117                         "mouseleave .ui-menu": "collapseAll",
118                         focus: function( event, keepActiveItem ) {
119                                 // If there's already an active item, keep it active
120                                 // If not, activate the first item
121                                 var item = this.active || this.element.find( this.options.items ).eq( 0 );
123                                 if ( !keepActiveItem ) {
124                                         this.focus( event, item );
125                                 }
126                         },
127                         blur: function( event ) {
128                                 this._delay(function() {
129                                         if ( !$.contains( this.element[0], this.document[0].activeElement ) ) {
130                                                 this.collapseAll( event );
131                                         }
132                                 });
133                         },
134                         keydown: "_keydown"
135                 });
137                 this.refresh();
139                 // Clicks outside of a menu collapse any open menus
140                 this._on( this.document, {
141                         click: function( event ) {
142                                 if ( this._closeOnDocumentClick( event ) ) {
143                                         this.collapseAll( event );
144                                 }
146                                 // Reset the mouseHandled flag
147                                 this.mouseHandled = false;
148                         }
149                 });
150         },
152         _destroy: function() {
153                 // Destroy (sub)menus
154                 this.element
155                         .removeAttr( "aria-activedescendant" )
156                         .find( ".ui-menu" ).addBack()
157                                 .removeClass( "ui-menu ui-widget ui-widget-content ui-menu-icons ui-front" )
158                                 .removeAttr( "role" )
159                                 .removeAttr( "tabIndex" )
160                                 .removeAttr( "aria-labelledby" )
161                                 .removeAttr( "aria-expanded" )
162                                 .removeAttr( "aria-hidden" )
163                                 .removeAttr( "aria-disabled" )
164                                 .removeUniqueId()
165                                 .show();
167                 // Destroy menu items
168                 this.element.find( ".ui-menu-item" )
169                         .removeClass( "ui-menu-item" )
170                         .removeAttr( "role" )
171                         .removeAttr( "aria-disabled" )
172                         .removeUniqueId()
173                         .removeClass( "ui-state-hover" )
174                         .removeAttr( "tabIndex" )
175                         .removeAttr( "role" )
176                         .removeAttr( "aria-haspopup" )
177                         .children().each( function() {
178                                 var elem = $( this );
179                                 if ( elem.data( "ui-menu-submenu-carat" ) ) {
180                                         elem.remove();
181                                 }
182                         });
184                 // Destroy menu dividers
185                 this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" );
186         },
188         _keydown: function( event ) {
189                 var match, prev, character, skip,
190                         preventDefault = true;
192                 switch ( event.keyCode ) {
193                 case $.ui.keyCode.PAGE_UP:
194                         this.previousPage( event );
195                         break;
196                 case $.ui.keyCode.PAGE_DOWN:
197                         this.nextPage( event );
198                         break;
199                 case $.ui.keyCode.HOME:
200                         this._move( "first", "first", event );
201                         break;
202                 case $.ui.keyCode.END:
203                         this._move( "last", "last", event );
204                         break;
205                 case $.ui.keyCode.UP:
206                         this.previous( event );
207                         break;
208                 case $.ui.keyCode.DOWN:
209                         this.next( event );
210                         break;
211                 case $.ui.keyCode.LEFT:
212                         this.collapse( event );
213                         break;
214                 case $.ui.keyCode.RIGHT:
215                         if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
216                                 this.expand( event );
217                         }
218                         break;
219                 case $.ui.keyCode.ENTER:
220                 case $.ui.keyCode.SPACE:
221                         this._activate( event );
222                         break;
223                 case $.ui.keyCode.ESCAPE:
224                         this.collapse( event );
225                         break;
226                 default:
227                         preventDefault = false;
228                         prev = this.previousFilter || "";
229                         character = String.fromCharCode( event.keyCode );
230                         skip = false;
232                         clearTimeout( this.filterTimer );
234                         if ( character === prev ) {
235                                 skip = true;
236                         } else {
237                                 character = prev + character;
238                         }
240                         match = this._filterMenuItems( character );
241                         match = skip && match.index( this.active.next() ) !== -1 ?
242                                 this.active.nextAll( ".ui-menu-item" ) :
243                                 match;
245                         // If no matches on the current filter, reset to the last character pressed
246                         // to move down the menu to the first item that starts with that character
247                         if ( !match.length ) {
248                                 character = String.fromCharCode( event.keyCode );
249                                 match = this._filterMenuItems( character );
250                         }
252                         if ( match.length ) {
253                                 this.focus( event, match );
254                                 this.previousFilter = character;
255                                 this.filterTimer = this._delay(function() {
256                                         delete this.previousFilter;
257                                 }, 1000 );
258                         } else {
259                                 delete this.previousFilter;
260                         }
261                 }
263                 if ( preventDefault ) {
264                         event.preventDefault();
265                 }
266         },
268         _activate: function( event ) {
269                 if ( !this.active.is( ".ui-state-disabled" ) ) {
270                         if ( this.active.is( "[aria-haspopup='true']" ) ) {
271                                 this.expand( event );
272                         } else {
273                                 this.select( event );
274                         }
275                 }
276         },
278         refresh: function() {
279                 var menus, items,
280                         that = this,
281                         icon = this.options.icons.submenu,
282                         submenus = this.element.find( this.options.menus );
284                 this.element.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length );
286                 // Initialize nested menus
287                 submenus.filter( ":not(.ui-menu)" )
288                         .addClass( "ui-menu ui-widget ui-widget-content ui-front" )
289                         .hide()
290                         .attr({
291                                 role: this.options.role,
292                                 "aria-hidden": "true",
293                                 "aria-expanded": "false"
294                         })
295                         .each(function() {
296                                 var menu = $( this ),
297                                         item = menu.parent(),
298                                         submenuCarat = $( "<span>" )
299                                                 .addClass( "ui-menu-icon ui-icon " + icon )
300                                                 .data( "ui-menu-submenu-carat", true );
302                                 item
303                                         .attr( "aria-haspopup", "true" )
304                                         .prepend( submenuCarat );
305                                 menu.attr( "aria-labelledby", item.attr( "id" ) );
306                         });
308                 menus = submenus.add( this.element );
309                 items = menus.find( this.options.items );
311                 // Initialize menu-items containing spaces and/or dashes only as dividers
312                 items.not( ".ui-menu-item" ).each(function() {
313                         var item = $( this );
314                         if ( that._isDivider( item ) ) {
315                                 item.addClass( "ui-widget-content ui-menu-divider" );
316                         }
317                 });
319                 // Don't refresh list items that are already adapted
320                 items.not( ".ui-menu-item, .ui-menu-divider" )
321                         .addClass( "ui-menu-item" )
322                         .uniqueId()
323                         .attr({
324                                 tabIndex: -1,
325                                 role: this._itemRole()
326                         });
328                 // Add aria-disabled attribute to any disabled menu item
329                 items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
331                 // If the active item has been removed, blur the menu
332                 if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
333                         this.blur();
334                 }
335         },
337         _itemRole: function() {
338                 return {
339                         menu: "menuitem",
340                         listbox: "option"
341                 }[ this.options.role ];
342         },
344         _setOption: function( key, value ) {
345                 if ( key === "icons" ) {
346                         this.element.find( ".ui-menu-icon" )
347                                 .removeClass( this.options.icons.submenu )
348                                 .addClass( value.submenu );
349                 }
350                 if ( key === "disabled" ) {
351                         this.element
352                                 .toggleClass( "ui-state-disabled", !!value )
353                                 .attr( "aria-disabled", value );
354                 }
355                 this._super( key, value );
356         },
358         focus: function( event, item ) {
359                 var nested, focused;
360                 this.blur( event, event && event.type === "focus" );
362                 this._scrollIntoView( item );
364                 this.active = item.first();
365                 focused = this.active.addClass( "ui-state-focus" ).removeClass( "ui-state-active" );
366                 // Only update aria-activedescendant if there's a role
367                 // otherwise we assume focus is managed elsewhere
368                 if ( this.options.role ) {
369                         this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
370                 }
372                 // Highlight active parent menu item, if any
373                 this.active
374                         .parent()
375                         .closest( ".ui-menu-item" )
376                         .addClass( "ui-state-active" );
378                 if ( event && event.type === "keydown" ) {
379                         this._close();
380                 } else {
381                         this.timer = this._delay(function() {
382                                 this._close();
383                         }, this.delay );
384                 }
386                 nested = item.children( ".ui-menu" );
387                 if ( nested.length && event && ( /^mouse/.test( event.type ) ) ) {
388                         this._startOpening(nested);
389                 }
390                 this.activeMenu = item.parent();
392                 this._trigger( "focus", event, { item: item } );
393         },
395         _scrollIntoView: function( item ) {
396                 var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
397                 if ( this._hasScroll() ) {
398                         borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0;
399                         paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0;
400                         offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
401                         scroll = this.activeMenu.scrollTop();
402                         elementHeight = this.activeMenu.height();
403                         itemHeight = item.outerHeight();
405                         if ( offset < 0 ) {
406                                 this.activeMenu.scrollTop( scroll + offset );
407                         } else if ( offset + itemHeight > elementHeight ) {
408                                 this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
409                         }
410                 }
411         },
413         blur: function( event, fromFocus ) {
414                 if ( !fromFocus ) {
415                         clearTimeout( this.timer );
416                 }
418                 if ( !this.active ) {
419                         return;
420                 }
422                 this.active.removeClass( "ui-state-focus" );
423                 this.active = null;
425                 this._trigger( "blur", event, { item: this.active } );
426         },
428         _startOpening: function( submenu ) {
429                 clearTimeout( this.timer );
431                 // Don't open if already open fixes a Firefox bug that caused a .5 pixel
432                 // shift in the submenu position when mousing over the carat icon
433                 if ( submenu.attr( "aria-hidden" ) !== "true" ) {
434                         return;
435                 }
437                 this.timer = this._delay(function() {
438                         this._close();
439                         this._open( submenu );
440                 }, this.delay );
441         },
443         _open: function( submenu ) {
444                 var position = $.extend({
445                         of: this.active
446                 }, this.options.position );
448                 clearTimeout( this.timer );
449                 this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) )
450                         .hide()
451                         .attr( "aria-hidden", "true" );
453                 submenu
454                         .show()
455                         .removeAttr( "aria-hidden" )
456                         .attr( "aria-expanded", "true" )
457                         .position( position );
458         },
460         collapseAll: function( event, all ) {
461                 clearTimeout( this.timer );
462                 this.timer = this._delay(function() {
463                         // If we were passed an event, look for the submenu that contains the event
464                         var currentMenu = all ? this.element :
465                                 $( event && event.target ).closest( this.element.find( ".ui-menu" ) );
467                         // If we found no valid submenu ancestor, use the main menu to close all sub menus anyway
468                         if ( !currentMenu.length ) {
469                                 currentMenu = this.element;
470                         }
472                         this._close( currentMenu );
474                         this.blur( event );
475                         this.activeMenu = currentMenu;
476                 }, this.delay );
477         },
479         // With no arguments, closes the currently active menu - if nothing is active
480         // it closes all menus.  If passed an argument, it will search for menus BELOW
481         _close: function( startMenu ) {
482                 if ( !startMenu ) {
483                         startMenu = this.active ? this.active.parent() : this.element;
484                 }
486                 startMenu
487                         .find( ".ui-menu" )
488                                 .hide()
489                                 .attr( "aria-hidden", "true" )
490                                 .attr( "aria-expanded", "false" )
491                         .end()
492                         .find( ".ui-state-active" ).not( ".ui-state-focus" )
493                                 .removeClass( "ui-state-active" );
494         },
496         _closeOnDocumentClick: function( event ) {
497                 return !$( event.target ).closest( ".ui-menu" ).length;
498         },
500         _isDivider: function( item ) {
502                 // Match hyphen, em dash, en dash
503                 return !/[^\-\u2014\u2013\s]/.test( item.text() );
504         },
506         collapse: function( event ) {
507                 var newItem = this.active &&
508                         this.active.parent().closest( ".ui-menu-item", this.element );
509                 if ( newItem && newItem.length ) {
510                         this._close();
511                         this.focus( event, newItem );
512                 }
513         },
515         expand: function( event ) {
516                 var newItem = this.active &&
517                         this.active
518                                 .children( ".ui-menu " )
519                                 .find( this.options.items )
520                                 .first();
522                 if ( newItem && newItem.length ) {
523                         this._open( newItem.parent() );
525                         // Delay so Firefox will not hide activedescendant change in expanding submenu from AT
526                         this._delay(function() {
527                                 this.focus( event, newItem );
528                         });
529                 }
530         },
532         next: function( event ) {
533                 this._move( "next", "first", event );
534         },
536         previous: function( event ) {
537                 this._move( "prev", "last", event );
538         },
540         isFirstItem: function() {
541                 return this.active && !this.active.prevAll( ".ui-menu-item" ).length;
542         },
544         isLastItem: function() {
545                 return this.active && !this.active.nextAll( ".ui-menu-item" ).length;
546         },
548         _move: function( direction, filter, event ) {
549                 var next;
550                 if ( this.active ) {
551                         if ( direction === "first" || direction === "last" ) {
552                                 next = this.active
553                                         [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" )
554                                         .eq( -1 );
555                         } else {
556                                 next = this.active
557                                         [ direction + "All" ]( ".ui-menu-item" )
558                                         .eq( 0 );
559                         }
560                 }
561                 if ( !next || !next.length || !this.active ) {
562                         next = this.activeMenu.find( this.options.items )[ filter ]();
563                 }
565                 this.focus( event, next );
566         },
568         nextPage: function( event ) {
569                 var item, base, height;
571                 if ( !this.active ) {
572                         this.next( event );
573                         return;
574                 }
575                 if ( this.isLastItem() ) {
576                         return;
577                 }
578                 if ( this._hasScroll() ) {
579                         base = this.active.offset().top;
580                         height = this.element.height();
581                         this.active.nextAll( ".ui-menu-item" ).each(function() {
582                                 item = $( this );
583                                 return item.offset().top - base - height < 0;
584                         });
586                         this.focus( event, item );
587                 } else {
588                         this.focus( event, this.activeMenu.find( this.options.items )
589                                 [ !this.active ? "first" : "last" ]() );
590                 }
591         },
593         previousPage: function( event ) {
594                 var item, base, height;
595                 if ( !this.active ) {
596                         this.next( event );
597                         return;
598                 }
599                 if ( this.isFirstItem() ) {
600                         return;
601                 }
602                 if ( this._hasScroll() ) {
603                         base = this.active.offset().top;
604                         height = this.element.height();
605                         this.active.prevAll( ".ui-menu-item" ).each(function() {
606                                 item = $( this );
607                                 return item.offset().top - base + height > 0;
608                         });
610                         this.focus( event, item );
611                 } else {
612                         this.focus( event, this.activeMenu.find( this.options.items ).first() );
613                 }
614         },
616         _hasScroll: function() {
617                 return this.element.outerHeight() < this.element.prop( "scrollHeight" );
618         },
620         select: function( event ) {
621                 // TODO: It should never be possible to not have an active item at this
622                 // point, but the tests don't trigger mouseenter before click.
623                 this.active = this.active || $( event.target ).closest( ".ui-menu-item" );
624                 var ui = { item: this.active };
625                 if ( !this.active.has( ".ui-menu" ).length ) {
626                         this.collapseAll( event, true );
627                 }
628                 this._trigger( "select", event, ui );
629         },
631         _filterMenuItems: function(character) {
632                 var escapedCharacter = character.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ),
633                         regex = new RegExp( "^" + escapedCharacter, "i" );
635                 return this.activeMenu
636                         .find( this.options.items )
638                         // Only match on items, not dividers or other content (#10571)
639                         .filter( ".ui-menu-item" )
640                         .filter(function() {
641                                 return regex.test( $.trim( $( this ).text() ) );
642                         });
643         }
646 }));