: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license.
* https://jquery.org/license
//>>description: Creates nestable menus.
//>>docs: https://api.jqueryui.com/menu/
//>>demos: https://jqueryui.com/menu/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/menu.css
//>>css.theme: ../../themes/base/theme.css
if ( typeof define === "function" && define.amd ) {
// AMD. Register as an anonymous module.
"../safe-active-element",
return $.widget( "ui.menu", {
submenu: "ui-icon-caret-1-e"
this.activeMenu = this.element;
// Flag used to prevent firing of the click handler
// as the event bubbles up through nested menus
this.mouseHandled = false;
this.lastMousePosition = { x: null, y: null };
this._addClass( "ui-menu", "ui-widget ui-widget-content" );
// Prevent focus from sticking to links inside menu after clicking
// them (focus should always stay on UL during navigation).
"mousedown .ui-menu-item": function( event ) {
this._activateItem( event );
"click .ui-menu-item": function( event ) {
var target = $( event.target );
var active = $( $.ui.safeActiveElement( this.document[ 0 ] ) );
if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) {
// Only set the mouseHandled flag if the event will bubble, see #9469.
if ( !event.isPropagationStopped() ) {
this.mouseHandled = true;
if ( target.has( ".ui-menu" ).length ) {
} else if ( !this.element.is( ":focus" ) &&
active.closest( ".ui-menu" ).length ) {
// Redirect focus to the menu
this.element.trigger( "focus", [ true ] );
// If the active item is on the top level, let it stay active.
// Otherwise, blur the active item since it is no longer visible.
if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) {
clearTimeout( this.timer );
"mouseenter .ui-menu-item": "_activateItem",
"mousemove .ui-menu-item": "_activateItem",
mouseleave: "collapseAll",
"mouseleave .ui-menu": "collapseAll",
focus: function( event, keepActiveItem ) {
// If there's already an active item, keep it active
// If not, activate the first item
var item = this.active || this._menuItems().first();
this.focus( event, item );
blur: function( event ) {
this._delay( function() {
var notContained = !$.contains(
$.ui.safeActiveElement( this.document[ 0 ] )
this.collapseAll( event );
// Clicks outside of a menu collapse any open menus
this._on( this.document, {
click: function( event ) {
if ( this._closeOnDocumentClick( event ) ) {
this.collapseAll( event, true );
// Reset the mouseHandled flag
this.mouseHandled = false;
_activateItem: function( event ) {
// Ignore mouse events while typeahead is active, see #10458.
// Prevents focusing the wrong item when typeahead causes a scroll while the mouse
// is over an item in the menu
if ( this.previousFilter ) {
// If the mouse didn't actually move, but the page was scrolled, ignore the event (#9356)
if ( event.clientX === this.lastMousePosition.x &&
event.clientY === this.lastMousePosition.y ) {
this.lastMousePosition = {
var actualTarget = $( event.target ).closest( ".ui-menu-item" ),
target = $( event.currentTarget );
// Ignore bubbled events on parent items, see #11641
if ( actualTarget[ 0 ] !== target[ 0 ] ) {
// If the item is already active, there's nothing to do
if ( target.is( ".ui-state-active" ) ) {
// Remove ui-state-active class from siblings of the newly focused menu item
// to avoid a jump caused by adjacent elements both having a class with a border
this._removeClass( target.siblings().children( ".ui-state-active" ),
null, "ui-state-active" );
this.focus( event, target );
var items = this.element.find( ".ui-menu-item" )
.removeAttr( "role aria-disabled" ),
submenus = items.children( ".ui-menu-item-wrapper" )
.removeAttr( "tabIndex role aria-haspopup" );
.removeAttr( "aria-activedescendant" )
.find( ".ui-menu" ).addBack()
.removeAttr( "role aria-labelledby aria-expanded aria-hidden aria-disabled " +
submenus.children().each( function() {
if ( elem.data( "ui-menu-submenu-caret" ) ) {
_keydown: function( event ) {
var match, prev, character, skip,
switch ( event.keyCode ) {
case $.ui.keyCode.PAGE_UP:
this.previousPage( event );
case $.ui.keyCode.PAGE_DOWN:
this._move( "first", "first", event );
this._move( "last", "last", event );
if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
case $.ui.keyCode.ESCAPE:
prev = this.previousFilter || "";
// Support number pad values
character = event.keyCode >= 96 && event.keyCode <= 105 ?
( event.keyCode - 96 ).toString() : String.fromCharCode( event.keyCode );
clearTimeout( this.filterTimer );
if ( character === prev ) {
character = prev + character;
match = this._filterMenuItems( character );
match = skip && match.index( this.active.next() ) !== -1 ?
this.active.nextAll( ".ui-menu-item" ) :
// If no matches on the current filter, reset to the last character pressed
// to move down the menu to the first item that starts with that character
character = String.fromCharCode( event.keyCode );
match = this._filterMenuItems( character );
this.focus( event, match );
this.previousFilter = character;
this.filterTimer = this._delay( function() {
delete this.previousFilter;
delete this.previousFilter;
_activate: function( event ) {
if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
if ( this.active.children( "[aria-haspopup='true']" ).length ) {
var menus, items, newSubmenus, newItems, newWrappers,
icon = this.options.icons.submenu,
submenus = this.element.find( this.options.menus );
this._toggleClass( "ui-menu-icons", null, !!this.element.find( ".ui-icon" ).length );
// Initialize nested menus
newSubmenus = submenus.filter( ":not(.ui-menu)" )
submenuCaret = $( "<span>" ).data( "ui-menu-submenu-caret", true );
that._addClass( submenuCaret, "ui-menu-icon", "ui-icon " + icon );
.attr( "aria-haspopup", "true" )
.prepend( submenuCaret );
menu.attr( "aria-labelledby", item.attr( "id" ) );
this._addClass( newSubmenus, "ui-menu", "ui-widget ui-widget-content ui-front" );
menus = submenus.add( this.element );
items = menus.find( this.options.items );
// Initialize menu-items containing spaces and/or dashes only as dividers
items.not( ".ui-menu-item" ).each( function() {
if ( that._isDivider( item ) ) {
that._addClass( item, "ui-menu-divider", "ui-widget-content" );
// Don't refresh list items that are already adapted
newItems = items.not( ".ui-menu-item, .ui-menu-divider" );
newWrappers = newItems.children()
this._addClass( newItems, "ui-menu-item" )
._addClass( newWrappers, "ui-menu-item-wrapper" );
// Add aria-disabled attribute to any disabled menu item
items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
// If the active item has been removed, blur the menu
if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
_setOption: function( key, value ) {
var icons = this.element.find( ".ui-menu-icon" );
this._removeClass( icons, null, this.options.icons.submenu )
._addClass( icons, null, value.submenu );
this._super( key, value );
_setOptionDisabled: function( value ) {
this.element.attr( "aria-disabled", String( value ) );
this._toggleClass( null, "ui-state-disabled", !!value );
focus: function( event, item ) {
var nested, focused, activeParent;
this.blur( event, event && event.type === "focus" );
this._scrollIntoView( item );
this.active = item.first();
focused = this.active.children( ".ui-menu-item-wrapper" );
this._addClass( focused, null, "ui-state-active" );
// Only update aria-activedescendant if there's a role
// otherwise we assume focus is managed elsewhere
if ( this.options.role ) {
this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
// Highlight active parent menu item, if any
activeParent = this.active
.closest( ".ui-menu-item" )
.children( ".ui-menu-item-wrapper" );
this._addClass( activeParent, null, "ui-state-active" );
if ( event && event.type === "keydown" ) {
this.timer = this._delay( function() {
nested = item.children( ".ui-menu" );
if ( nested.length && event && ( /^mouse/.test( event.type ) ) ) {
this._startOpening( nested );
this.activeMenu = item.parent();
this._trigger( "focus", event, { item: item } );
_scrollIntoView: function( item ) {
var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
if ( this._hasScroll() ) {
borderTop = parseFloat( $.css( this.activeMenu[ 0 ], "borderTopWidth" ) ) || 0;
paddingTop = parseFloat( $.css( this.activeMenu[ 0 ], "paddingTop" ) ) || 0;
offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
scroll = this.activeMenu.scrollTop();
elementHeight = this.activeMenu.height();
itemHeight = item.outerHeight();
this.activeMenu.scrollTop( scroll + offset );
} else if ( offset + itemHeight > elementHeight ) {
this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
blur: function( event, fromFocus ) {
clearTimeout( this.timer );
this._removeClass( this.active.children( ".ui-menu-item-wrapper" ),
null, "ui-state-active" );
this._trigger( "blur", event, { item: this.active } );
_startOpening: function( submenu ) {
clearTimeout( this.timer );
// Don't open if already open fixes a Firefox bug that caused a .5 pixel
// shift in the submenu position when mousing over the caret icon
if ( submenu.attr( "aria-hidden" ) !== "true" ) {
this.timer = this._delay( function() {
_open: function( submenu ) {
var position = $.extend( {
}, this.options.position );
clearTimeout( this.timer );
this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) )
.attr( "aria-hidden", "true" );