: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
control.container.find( '.edit-menu' ).on( 'click', function() {
var menuId = control.setting();
api.section( 'nav_menu[' + menuId + ']' ).focus();
control.setting.bind( 'change', function() {
var menuIsSelected = 0 !== control.setting();
control.container.find( '.create-menu' ).toggleClass( 'hidden', menuIsSelected );
control.container.find( '.edit-menu' ).toggleClass( 'hidden', ! menuIsSelected );
// Add/remove menus from the available options when they are added and removed.
api.bind( 'add', function( setting ) {
var option, menuId, matches = setting.id.match( navMenuIdRegex );
if ( ! matches || false === setting() ) {
option = new Option( displayNavMenuName( setting().name ), menuId );
control.container.find( 'select' ).append( option );
api.bind( 'remove', function( setting ) {
var menuId, matches = setting.id.match( navMenuIdRegex );
menuId = parseInt( matches[1], 10 );
if ( control.setting() === menuId ) {
control.setting.set( '' );
control.container.find( 'option[value=' + menuId + ']' ).remove();
api.bind( 'change', function( setting ) {
var menuId, matches = setting.id.match( navMenuIdRegex );
menuId = parseInt( matches[1], 10 );
if ( false === setting() ) {
if ( control.setting() === menuId ) {
control.setting.set( '' );
control.container.find( 'option[value=' + menuId + ']' ).remove();
control.container.find( 'option[value=' + menuId + ']' ).text( displayNavMenuName( setting().name ) );
api.Menus.MenuItemControl = api.Control.extend(/** @lends wp.customize.Menus.MenuItemControl.prototype */{
* wp.customize.Menus.MenuItemControl
* Customizer control for menu items.
* Note that 'menu_item' must match the WP_Customize_Menu_Item_Control::$type.
* @constructs wp.customize.Menus.MenuItemControl
* @augments wp.customize.Control
initialize: function( id, options ) {
control.expanded = new api.Value( false );
control.expandedArgumentsQueue = [];
control.expanded.bind( function( expanded ) {
var args = control.expandedArgumentsQueue.shift();
args = $.extend( {}, control.defaultExpandedArguments, args );
control.onChangeExpanded( expanded, args );
api.Control.prototype.initialize.call( control, id, options );
control.active.validate = function() {
var value, section = api.section( control.section() );
value = section.active();
* Set up the initial state of the screen reader accessibility information for menu items.
initAccessibility: function() {
menu = $( '#menu-to-edit' );
// Refresh the accessibility when the user comes close to the item in any way.
menu.on( 'mouseenter.refreshAccessibility focus.refreshAccessibility touchstart.refreshAccessibility', '.menu-item', function(){
control.refreshAdvancedAccessibilityOfItem( $( this ).find( 'button.item-edit' ) );
// We have to update on click as well because we might hover first, change the item, and then click.
menu.on( 'click', 'button.item-edit', function() {
control.refreshAdvancedAccessibilityOfItem( $( this ) );
* refreshAdvancedAccessibilityOfItem( [itemToRefresh] )
* Refreshes advanced accessibility buttons for one menu item.
* Shows or hides buttons based on the location of the menu item.
* @param {Object} itemToRefresh The menu item that might need its advanced accessibility buttons refreshed
refreshAdvancedAccessibilityOfItem: function( itemToRefresh ) {
// Only refresh accessibility when necessary.
if ( true !== $( itemToRefresh ).data( 'needs_accessibility_refresh' ) ) {
var primaryItems, itemPosition, title,
parentItem, parentItemId, parentItemName, subItems, totalSubItems,
$this = $( itemToRefresh ),
menuItem = $this.closest( 'li.menu-item' ).first(),
depth = menuItem.menuItemDepth(),
isPrimaryMenuItem = ( 0 === depth ),
itemName = $this.closest( '.menu-item-handle' ).find( '.menu-item-title' ).text(),
menuItemType = $this.closest( '.menu-item-handle' ).find( '.item-type' ).text(),
totalMenuItems = $( '#menu-to-edit li' ).length;
if ( isPrimaryMenuItem ) {
primaryItems = $( '.menu-item-depth-0' ),
itemPosition = primaryItems.index( menuItem ) + 1,
totalMenuItems = primaryItems.length,
// String together help text for primary menu items.
title = menus.menuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalMenuItems );
parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(),
parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(),
parentItemName = parentItem.find( '.menu-item-title' ).text(),
subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ),
totalSubItems = subItems.length,
itemPosition = $( subItems.parents( '.menu-item' ).get().reverse() ).index( menuItem ) + 1;
// String together help text for sub menu items.
title = menus.subMenuFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName );
title = menus.subMenuMoreDepthFocus.replace( '%1$s', itemName ).replace( '%2$s', menuItemType ).replace( '%3$d', itemPosition ).replace( '%4$d', totalSubItems ).replace( '%5$s', parentItemName ).replace( '%6$d', depth );
$this.find( '.screen-reader-text' ).text( title );
// Mark this item's accessibility as refreshed.
$this.data( 'needs_accessibility_refresh', false );
* Override the embed() method to do nothing,
* so that the control isn't embedded on load,
* unless the containing section is already expanded.
sectionId = control.section(),
section = api.section( sectionId );
if ( ( section && section.expanded() ) || api.settings.autofocus.control === control.id ) {
* This function is called in Section.onChangeExpanded() so the control
* will only get embedded when the Section is first expanded.
actuallyEmbed: function() {
if ( 'resolved' === control.deferred.embedded.state() ) {
control.deferred.embedded.resolve(); // This triggers control.ready().
// Mark all menu items as unprocessed.
$( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
if ( 'undefined' === typeof this.params.menu_item_id ) {
throw new Error( 'params.menu_item_id was not defined' );
this._setupControlToggle();
* Show/hide the settings when clicking on the menu item handle.
_setupControlToggle: function() {
this.container.find( '.menu-item-handle' ).on( 'click', function( e ) {
var menuControl = control.getMenuControl(),
isDeleteBtn = $( e.target ).is( '.item-delete, .item-delete *' ),
isAddNewBtn = $( e.target ).is( '.add-new-menu-item, .add-new-menu-item *' );
if ( $( 'body' ).hasClass( 'adding-menu-items' ) && ! isDeleteBtn && ! isAddNewBtn ) {
api.Menus.availableMenuItemsPanel.close();
if ( menuControl.isReordering || menuControl.isSorting ) {
* Set up the menu-item-reorder-nav
_setupReorderUI: function() {
var control = this, template, $reorderNav;
template = wp.template( 'menu-item-reorder-nav' );
// Add the menu item reordering elements to the menu item control.
control.container.find( '.item-controls' ).after( template );
// Handle clicks for up/down/left-right on the reorder nav.
$reorderNav = control.container.find( '.menu-item-reorder-nav' );
$reorderNav.find( '.menus-move-up, .menus-move-down, .menus-move-left, .menus-move-right' ).on( 'click', function() {
control.params.depth = control.getDepth();
var isMoveUp = moveBtn.is( '.menus-move-up' ),
isMoveDown = moveBtn.is( '.menus-move-down' ),
isMoveLeft = moveBtn.is( '.menus-move-left' ),
isMoveRight = moveBtn.is( '.menus-move-right' );
} else if ( isMoveDown ) {
} else if ( isMoveLeft ) {
} else if ( isMoveRight ) {
control.params.depth += 1;
moveBtn.focus(); // Re-focus after the container was moved.
// Mark all menu items as unprocessed.
$( 'button.item-edit' ).data( 'needs_accessibility_refresh', true );
* Set up event handlers for menu item updating.
_setupUpdateUI: function() {
settingValue = control.setting(),
control.elements.url = new api.Element( control.container.find( '.edit-menu-item-url' ) );
control.elements.title = new api.Element( control.container.find( '.edit-menu-item-title' ) );
control.elements.attr_title = new api.Element( control.container.find( '.edit-menu-item-attr-title' ) );
control.elements.target = new api.Element( control.container.find( '.edit-menu-item-target' ) );
control.elements.classes = new api.Element( control.container.find( '.edit-menu-item-classes' ) );
control.elements.xfn = new api.Element( control.container.find( '.edit-menu-item-xfn' ) );
control.elements.description = new api.Element( control.container.find( '.edit-menu-item-description' ) );
// @todo Allow other elements, added by plugins, to be automatically picked up here;
// allow additional values to be added to setting array.
_.each( control.elements, function( element, property ) {
element.bind(function( value ) {
if ( element.element.is( 'input[type=checkbox]' ) ) {
value = ( value ) ? element.element.val() : '';
var settingValue = control.setting();
if ( settingValue && settingValue[ property ] !== value ) {
settingValue = _.clone( settingValue );
settingValue[ property ] = value;
control.setting.set( settingValue );
if ( ( property === 'classes' || property === 'xfn' ) && _.isArray( settingValue[ property ] ) ) {
element.set( settingValue[ property ].join( ' ' ) );
element.set( settingValue[ property ] );
control.setting.bind(function( to, from ) {
var itemId = control.params.menu_item_id,
followingSiblingItemControls = [],
childrenItemControls = [],
menuControl = api.control( 'nav_menu[' + String( from.nav_menu_term_id ) + ']' );
control.container.remove();
_.each( menuControl.getMenuItemControls(), function( otherControl ) {
if ( from.menu_item_parent === otherControl.setting().menu_item_parent && otherControl.setting().position > from.position ) {
followingSiblingItemControls.push( otherControl );
} else if ( otherControl.setting().menu_item_parent === itemId ) {
childrenItemControls.push( otherControl );
// Shift all following siblings by the number of children this item has.
_.each( followingSiblingItemControls, function( followingSiblingItemControl ) {
var value = _.clone( followingSiblingItemControl.setting() );
value.position += childrenItemControls.length;
followingSiblingItemControl.setting.set( value );
// Now move the children up to be the new subsequent siblings.
_.each( childrenItemControls, function( childrenItemControl, i ) {
var value = _.clone( childrenItemControl.setting() );
value.position = from.position + i;
value.menu_item_parent = from.menu_item_parent;
childrenItemControl.setting.set( value );
menuControl.debouncedReflowMenuItems();
// Update the elements' values to match the new setting properties.
_.each( to, function( value, key ) {
if ( control.elements[ key] ) {
control.elements[ key ].set( to[ key ] );
control.container.find( '.menu-item-data-parent-id' ).val( to.menu_item_parent );
// Handle UI updates when the position or depth (parent) change.
if ( to.position !== from.position || to.menu_item_parent !== from.menu_item_parent ) {
control.getMenuControl().debouncedReflowMenuItems();
// Style the URL field as invalid when there is an invalid_url notification.
updateNotifications = function() {
control.elements.url.element.toggleClass( 'invalid', control.setting.notifications.has( 'invalid_url' ) );
control.setting.notifications.bind( 'add', updateNotifications );
control.setting.notifications.bind( 'removed', updateNotifications );
* Set up event handlers for menu item deletion.
_setupRemoveUI: function() {
var control = this, $removeBtn;
// Configure delete button.
$removeBtn = control.container.find( '.item-delete' );
$removeBtn.on( 'click', function() {
// Find an adjacent element to add focus to when this menu item goes away.
var addingItems = true, $adjacentFocusTarget, $next, $prev,
instanceCounter = 0, // Instance count of the menu item deleted.
deleteItemOriginalItemId = control.params.original_item_id,
addedItems = control.getMenuControl().$sectionContent.find( '.menu-item' ),
if ( ! $( 'body' ).hasClass( 'adding-menu-items' ) ) {
$next = control.container.nextAll( '.customize-control-nav_menu_item:visible' ).first();
$prev = control.container.prevAll( '.customize-control-nav_menu_item:visible' ).first();
$adjacentFocusTarget = $next.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
} else if ( $prev.length ) {
$adjacentFocusTarget = $prev.find( false === addingItems ? '.item-edit' : '.item-delete' ).first();
$adjacentFocusTarget = control.container.nextAll( '.customize-control-nav_menu' ).find( '.add-new-menu-item' ).first();
* If the menu item deleted is the only of its instance left,
* remove the check icon of this menu item in the right panel.
_.each( addedItems, function( addedItem ) {
var menuItemId, menuItemControl, matches;
// This is because menu item that's deleted is just hidden.
if ( ! $( addedItem ).is( ':visible' ) ) {
matches = addedItem.getAttribute( 'id' ).match( /^customize-control-nav_menu_item-(-?\d+)$/, '' );
menuItemId = parseInt( matches[1], 10 );
menuItemControl = api.control( 'nav_menu_item[' + String( menuItemId ) + ']' );
// Check for duplicate menu items.
if ( menuItemControl && deleteItemOriginalItemId == menuItemControl.params.original_item_id ) {
if ( instanceCounter <= 1 ) {
// Revert the check icon to add icon.
availableMenuItem = $( '#menu-item-tpl-' + control.params.original_item_id );
availableMenuItem.removeClass( 'selected' );
availableMenuItem.find( '.menu-item-handle' ).removeClass( 'item-added' );
control.container.slideUp( function() {
control.setting.set( false );
wp.a11y.speak( api.Menus.data.l10n.itemDeleted );
$adjacentFocusTarget.focus(); // Keyboard accessibility.
control.setting.set( false );
_setupLinksUI: function() {
// Configure original link.
$origBtn = this.container.find( 'a.original-link' );
$origBtn.on( 'click', function( e ) {
api.previewer.previewUrl( e.target.toString() );
* Update item handle title when changed.
_setupTitleUI: function() {
var control = this, titleEl;
// Ensure that whitespace is trimmed on blur so placeholder can be shown.
control.container.find( '.edit-menu-item-title' ).on( 'blur', function() {
$( this ).val( $( this ).val().trim() );
titleEl = control.container.find( '.menu-item-title' );
control.setting.bind( function( item ) {
var trimmedTitle, titleText;
item.title = item.title || '';
trimmedTitle = item.title.trim();
titleText = trimmedTitle || item.original_title || api.Menus.data.l10n.untitled;
titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText );
// Don't update to an empty title.
if ( trimmedTitle || item.original_title ) {
.removeClass( 'no-title' );