Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 253 additions & 1 deletion src/js/_enqueues/lib/nav-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

this.attachBulkSelectButtonListeners();
this.attachMenuCheckBoxListeners();
this.attachMenuItemMoveButton();
this.attachMenuItemDeleteButton();
this.attachPendingMenuItemsListForDeletion();

Expand Down Expand Up @@ -518,6 +519,7 @@
api.refreshAdvancedAccessibility();
thisItem.updateParentDropdown();
thisItem.updateOrderDropdown();
api.updateBulkMoveControls();

if ( a11ySpeech ) {
wp.a11y.speak( a11ySpeech );
Expand Down Expand Up @@ -616,6 +618,7 @@

menuItem.insertAfter( menuItems.eq( menuItemNewPosition ) ).updateParentMenuItemDBId().updateParentDropdown().updateOrderDropdown();

api.updateBulkMoveControls();
api.registerChange();
api.refreshKeyboardAccessibility();
api.refreshAdvancedAccessibility();
Expand Down Expand Up @@ -661,6 +664,7 @@
menuItem.detach().insertAfter( menuItems.eq( menuItemNewPosition ) ).updateOrderDropdown();
}

api.updateBulkMoveControls();
api.registerChange();
api.refreshKeyboardAccessibility();
api.refreshAdvancedAccessibility();
Expand Down Expand Up @@ -975,6 +979,7 @@
ui.item.updateParentDropdown();
ui.item.updateOrderDropdown();
api.refreshAdvancedAccessibilityOfItem( ui.item.find( 'a.item-edit' ) );
api.updateBulkMoveControls();
},
change: function(e, ui) {
// Make sure the placeholder is inside the menu.
Expand Down Expand Up @@ -1213,6 +1218,7 @@

$( '.menu-items-delete' ).addClass( 'disabled' );
$( '#pending-menu-items-to-delete ul' ).empty();
this.updateBulkMoveControls();
},

/**
Expand All @@ -1225,9 +1231,246 @@

$( '#menu-to-edit' ).on( 'change', '.menu-item-checkbox', function() {
that.setRemoveSelectedButtonStatus();
that.updateBulkMoveControls();
});
},

/**
* Get the topmost selected menu items.
*
* A selected child is omitted when its parent is also selected, because
* moving the parent already moves the entire subtree.
*
* @since 7.2.0
*
* @return {jQuery} Selected menu items without selected ancestors.
*/
getSelectedMenuItems : function() {
var selectedMenuItems = $(),
selectedDepth = -1;

api.menuList.children( '.menu-item' ).each( function() {
var menuItem = $( this ),
depth = menuItem.menuItemDepth();

if ( depth <= selectedDepth ) {
selectedDepth = -1;
}

if ( -1 === selectedDepth && menuItem.find( '.menu-item-checkbox' ).prop( 'checked' ) ) {
selectedMenuItems = selectedMenuItems.add( menuItem );
selectedDepth = depth;
}
});

return selectedMenuItems;
},

/**
* Get the greatest depth below any selected root.
*
* @since 7.2.0
*
* @param {jQuery} selectedMenuItems Selected root menu items.
* @return {number} Greatest relative child depth.
*/
getSelectedMenuItemsMaxChildDepth : function( selectedMenuItems ) {
var maxChildDepth = 0;

selectedMenuItems.each( function() {
var menuItem = $( this ),
depth = menuItem.menuItemDepth();

menuItem.childMenuItems().each( function() {
maxChildDepth = Math.max( maxChildDepth, $( this ).menuItemDepth() - depth );
});
});

return maxChildDepth;
},

/**
* Populate and set the state of the bulk move controls.
*
* @since 7.2.0
*/
updateBulkMoveControls : function() {
var menuItems = api.menuList.children( '.menu-item' ),
selectedMenuItems = api.getSelectedMenuItems(),
excludedMenuItems = selectedMenuItems.add( selectedMenuItems.childMenuItems() ),
maxChildDepth = api.getSelectedMenuItemsMaxChildDepth( selectedMenuItems ),
fieldset = $( '#bulk-menu-items-move' ),
parentDropdown = $( '#bulk-menu-item-parent' ),
positionDropdown = $( '#bulk-menu-item-position' ),
selectOption = parentDropdown.find( 'option' ).first().clone();

fieldset.prop( 'disabled', ! selectedMenuItems.length );
parentDropdown.empty().append( selectOption );
positionDropdown.val( '' ).prop( 'disabled', true );
$( '.menu-items-move' ).prop( 'disabled', true );

if ( ! selectedMenuItems.length ) {
return;
}

parentDropdown.append(
$( '<option>', {
value: '0',
text: wp.i18n._x( 'No Parent', 'menu item without a parent in navigation menu' ),
} )
);

menuItems.each( function() {
var menuItem = $( this );

if (
! excludedMenuItems.is( menuItem ) &&
menuItem.menuItemDepth() + maxChildDepth < api.options.globalMaxDepth
) {
parentDropdown.append(
$( '<option>', {
value: menuItem.find( '.menu-item-data-db-id' ).val(),
text: menuItem.find( '.edit-menu-item-title' ).val(),
} )
);
}
});
},

/**
* Populate the starting position dropdown for the selected parent.
*
* @since 7.2.0
*/
updateBulkMovePositionDropdown : function() {
var i,
parentID = $( '#bulk-menu-item-parent' ).val(),
selectedMenuItems = api.getSelectedMenuItems(),
positionDropdown = $( '#bulk-menu-item-position' ),
selectOption = positionDropdown.find( 'option' ).first().clone(),
siblings;

positionDropdown.empty().append( selectOption ).prop( 'disabled', true );
$( '.menu-items-move' ).prop( 'disabled', true );

if ( '' === parentID ) {
return;
}

siblings = api.menuList.children( '.menu-item' ).filter( function() {
return parentID === String( $( this ).find( '.menu-item-data-parent-id' ).val() );
}).not( selectedMenuItems );

for ( i = 1; i <= siblings.length + 1; i++ ) {
positionDropdown.append(
$( '<option>', {
value: i,
text: wp.i18n.sprintf(
/* translators: 1: The first selected menu item position, 2: The total number of menu items. */
wp.i18n._x( '%1$s of %2$s', 'part of a total number of menu items' ),
i,
siblings.length + selectedMenuItems.length
),
} )
);
}

positionDropdown.prop( 'disabled', false );
},

/**
* Set up the bulk move controls.
*
* @since 7.2.0
*/
attachMenuItemMoveButton : function() {
$( '#bulk-menu-item-parent' ).on( 'change', function() {
api.updateBulkMovePositionDropdown();
});

$( '#bulk-menu-item-position' ).on( 'change', function() {
$( '.menu-items-move' ).prop( 'disabled', ! this.value );
});

$( '.menu-items-move' ).on( 'click', function() {
if ( ! this.disabled ) {
api.moveSelectedMenuItems();
}
});
},

/**
* Move selected menu item subtrees as a contiguous group.
*
* @since 7.2.0
*/
moveSelectedMenuItems : function() {
var menuItem,
itemsToMove = $(),
selectedMenuItems = api.getSelectedMenuItems(),
selectedSubtrees = selectedMenuItems.add( selectedMenuItems.childMenuItems() ),
selectedItemsCount = api.menuList.find( '.menu-item-checkbox:checked' ).length,
maxChildDepth = api.getSelectedMenuItemsMaxChildDepth( selectedMenuItems ),
parentID = $( '#bulk-menu-item-parent' ).val(),
position = parseInt( $( '#bulk-menu-item-position' ).val(), 10 ),
parentItem = api.menuList.children( '#menu-item-' + parentID ),
newDepth = parentItem.length ? parseInt( parentItem.menuItemDepth(), 10 ) + 1 : 0,
siblings;

if ( ! selectedMenuItems.length || '' === parentID || isNaN( position ) ) {
return;
}

if (
( '0' !== parentID && ! parentItem.length ) ||
parentItem.length && (
selectedSubtrees.is( parentItem ) ||
newDepth + maxChildDepth > api.options.globalMaxDepth
)
) {
api.updateBulkMoveControls();
$( '#bulk-menu-item-parent' ).trigger( 'focus' );
wp.a11y.speak(
wp.i18n.__( 'The selected menu items could not be moved. Please choose a new parent and position.' ),
'assertive'
);
return;
}

selectedMenuItems.each( function() {
menuItem = $( this );
itemsToMove = itemsToMove.add( menuItem ).add( menuItem.childMenuItems() );
menuItem.moveHorizontally( newDepth, menuItem.menuItemDepth() );
});

itemsToMove.detach();
siblings = api.menuList.children( '.menu-item' ).filter( function() {
return parentID === String( $( this ).find( '.menu-item-data-parent-id' ).val() );
});

if ( position <= siblings.length ) {
itemsToMove.insertBefore( siblings.eq( position - 1 ) );
} else if ( parentItem.length ) {
menuItem = parentItem.childMenuItems().last();
itemsToMove.insertAfter( menuItem.length ? menuItem : parentItem );
} else {
itemsToMove.appendTo( api.menuList );
}

itemsToMove.updateParentMenuItemDBId();
api.menuList.updateParentDropdown().updateOrderDropdown();
api.registerChange();
api.refreshKeyboardAccessibility();
api.refreshAdvancedAccessibility();
wp.a11y.speak(
wp.i18n.sprintf(
wp.i18n._n( '%d menu item moved.', '%d menu items moved.', selectedItemsCount ),
selectedItemsCount
),
'polite'
);
},

/**
* Create delete button to remove menu items from collection.
*
Expand Down Expand Up @@ -1335,6 +1578,7 @@
});

that.setRemoveSelectedButtonStatus();
that.updateBulkMoveControls();
},

/**
Expand Down Expand Up @@ -1850,6 +2094,7 @@
wp.a11y.speak( menus.itemRemoved );
$( '#menu-to-edit' ).updateParentDropdown();
$( '#menu-to-edit' ).updateOrderDropdown();
api.updateBulkMoveControls();
});
},

Expand Down Expand Up @@ -1887,10 +2132,17 @@
});

// Show bulk action.
$( document ).on( 'menu-item-added', function() {
$( document ).on( 'menu-item-added', function( event, menuItem ) {
if ( ! $( '.bulk-actions' ).is( ':visible' ) ) {
$( '.bulk-actions' ).show();
}

if ( api.menuList ) {
if ( api.menuList.hasClass( 'bulk-selection' ) ) {
$( menuItem ).find( '.menu-item-checkbox' ).prop( 'disabled', false );
}
api.updateBulkMoveControls();
}
} );

// Hide bulk action.
Expand Down
19 changes: 19 additions & 0 deletions src/wp-admin/css/nav-menus.css
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,25 @@ input.bulk-select-switcher:focus + .bulk-select-button-label {
color: #0a4b78;
}

#bulk-menu-items-move {
display: none;
align-items: flex-end;
flex-wrap: wrap;
gap: 8px;
margin: 1em 0;
padding: 0;
border: 0;
}

.bulk-selection #bulk-menu-items-move {
display: flex;
}

#bulk-menu-items-move label,
#bulk-menu-items-move select {
display: block;
}

.bulk-actions input.menu-items-delete {
appearance: none;
font-size: inherit;
Expand Down
18 changes: 17 additions & 1 deletion src/wp-admin/nav-menus.php
Original file line number Diff line number Diff line change
Expand Up @@ -1163,9 +1163,25 @@ function wp_nav_menu_max_depth( $classes ) {
<input type="checkbox" id="bulk-select-switcher-bottom" name="bulk-select-switcher-top" class="bulk-select-switcher">
<span class="bulk-select-button-label"><?php _e( 'Bulk Select' ); ?></span>
</label>
<fieldset id="bulk-menu-items-move" disabled="disabled">
<legend class="screen-reader-text"><?php _e( 'Move selected menu items' ); ?></legend>
<label for="bulk-menu-item-parent">
<?php _e( 'Menu Parent' ); ?>
<select id="bulk-menu-item-parent">
<option value=""><?php _e( '&mdash; Select &mdash;' ); ?></option>
</select>
</label>
<label for="bulk-menu-item-position">
<?php _e( 'Starting Position' ); ?>
<select id="bulk-menu-item-position" disabled="disabled">
<option value=""><?php _e( '&mdash; Select &mdash;' ); ?></option>
</select>
</label>
<button type="button" class="button menu-items-move" disabled="disabled"><?php _e( 'Move Selected Items' ); ?></button>
</fieldset>
<input type="button" class="deletion menu-items-delete disabled" value="<?php esc_attr_e( 'Remove Selected Items' ); ?>">
<div id="pending-menu-items-to-delete">
<p><?php _e( 'List of menu items selected for deletion:' ); ?></p>
<p><?php _e( 'List of selected menu items:' ); ?></p>
<ul></ul>
</div>
</div>
Expand Down
Loading
Loading