angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

menu.js (17775B)


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