diff -r 48c4eec2b7e6 -r 8c2e4d02f4ef wp/wp-admin/js/nav-menu.js --- a/wp/wp-admin/js/nav-menu.js Fri Sep 05 18:40:08 2025 +0200 +++ b/wp/wp-admin/js/nav-menu.js Fri Sep 05 18:52:52 2025 +0200 @@ -216,6 +216,8 @@ checkboxes.prop( 'checked', false ); t.find( '.button-controls .select-all' ).prop( 'checked', false ); t.find( '.button-controls .spinner' ).removeClass( 'is-active' ); + t.updateParentDropdown(); + t.updateOrderDropdown(); }); }); }, @@ -288,6 +290,117 @@ }); }); return this; + }, + updateParentDropdown : function() { + return this.each(function(){ + var menuItems = $( '#menu-to-edit li' ), + parentDropdowns = $( '.edit-menu-item-parent' ); + + $.each( parentDropdowns, function() { + var parentDropdown = $( this ), + $html = '', + $selected = '', + currentItemID = parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-db-id' ).val(), + currentparentID = parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-parent-id' ).val(), + currentItem = parentDropdown.closest( 'li.menu-item' ), + currentMenuItemChild = currentItem.childMenuItems(), + excludeMenuItem = [ currentItemID ]; + + if ( currentMenuItemChild.length > 0 ) { + $.each( currentMenuItemChild, function(){ + var childItem = $(this), + childID = childItem.find( '.menu-item-data-db-id' ).val(); + + excludeMenuItem.push( childID ); + }); + } + + if ( currentparentID == 0 ) { + $selected = 'selected'; + } + + $html += ''; + + $.each( menuItems, function() { + var menuItem = $(this), + $selected = '', + menuID = menuItem.find( '.menu-item-data-db-id' ).val(), + menuTitle = menuItem.find( '.edit-menu-item-title' ).val(); + + if ( ! excludeMenuItem.includes( menuID ) ) { + if ( currentparentID == menuID ) { + $selected = 'selected'; + } + $html += ''; + } + }); + + parentDropdown.html( $html ); + }); + + }); + }, + updateOrderDropdown : function() { + return this.each( function() { + var itemPosition, + orderDropdowns = $( '.edit-menu-item-order' ); + + $.each( orderDropdowns, function() { + var orderDropdown = $( this ), + menuItem = orderDropdown.closest( 'li.menu-item' ).first(), + depth = menuItem.menuItemDepth(), + isPrimaryMenuItem = ( 0 === depth ), + $html = '', + $selected = ''; + + if ( isPrimaryMenuItem ) { + var primaryItems = $( '.menu-item-depth-0' ), + totalMenuItems = primaryItems.length; + + itemPosition = primaryItems.index( menuItem ) + 1; + + for ( let i = 1; i < totalMenuItems + 1; i++ ) { + $selected = ''; + if ( i == itemPosition ) { + $selected = 'selected'; + } + var itemString = wp.i18n.sprintf( + /* translators: 1: The current menu item number, 2: The total number of menu items. */ + wp.i18n._x( '%1$s of %2$s', 'part of a total number of menu items' ), + i, + totalMenuItems + ); + $html += ''; + } + + } else { + var parentItem = menuItem.prevAll( '.menu-item-depth-' + parseInt( depth - 1, 10 ) ).first(), + parentItemId = parentItem.find( '.menu-item-data-db-id' ).val(), + subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemId + '"]' ), + totalSubMenuItems = subItems.length; + + itemPosition = $( subItems.parents('.menu-item').get().reverse() ).index( menuItem ) + 1; + + for ( let i = 1; i < totalSubMenuItems + 1; i++ ) { + $selected = ''; + if ( i == itemPosition ) { + $selected = 'selected'; + } + var submenuString = wp.i18n.sprintf( + /* translators: 1: The current submenu item number, 2: The total number of submenu items. */ + wp.i18n._x( '%1$s of %2$s', 'part of a total number of menu items' ), + i, + totalSubMenuItems + ); + $html += ''; + } + + } + + orderDropdown.html( $html ); + }); + + }); } }); }, @@ -297,7 +410,6 @@ }, moveMenuItem : function( $this, dir ) { - var items, newItemPosition, newDepth, menuItems = $( '#menu-to-edit li' ), menuItemsCount = menuItems.length, @@ -400,6 +512,8 @@ api.registerChange(); api.refreshKeyboardAccessibility(); api.refreshAdvancedAccessibility(); + thisItem.updateParentDropdown(); + thisItem.updateOrderDropdown(); if ( a11ySpeech ) { wp.a11y.speak( a11ySpeech ); @@ -431,6 +545,123 @@ api.moveMenuItem( $( this ).parents( 'li.menu-item' ).find( 'a.item-edit' ), dir ); } }); + + // Set menu parents data for all menu items. + menu.updateParentDropdown(); + + // Set menu order data for all menu items. + menu.updateOrderDropdown(); + + // Update menu item parent when value is changed. + menu.on( 'change', '.edit-menu-item-parent', function() { + api.changeMenuParent( $( this ) ); + }); + + // Update menu item order when value is changed. + menu.on( 'change', '.edit-menu-item-order', function() { + api.changeMenuOrder( $( this ) ); + }); + }, + + /** + * changeMenuParent( [parentDropdown] ) + * + * @since 6.7.0 + * + * @param {object} parentDropdown select field + */ + changeMenuParent : function( parentDropdown ) { + var menuItemNewPosition, + menuItems = $( '#menu-to-edit li' ), + $this = $( parentDropdown ), + newParentID = $this.val(), + menuItem = $this.closest( 'li.menu-item' ).first(), + menuItemOldDepth = menuItem.menuItemDepth(), + menuItemChildren = menuItem.childMenuItems(), + menuItemNoChildren = parseInt( menuItem.childMenuItems().length, 10 ), + parentItem = $( '#menu-item-' + newParentID ), + parentItemDepth = parentItem.menuItemDepth(), + menuItemNewDepth = parseInt( parentItemDepth ) + 1; + + if ( newParentID == 0 ) { + menuItemNewDepth = 0; + } + + menuItem.find( '.menu-item-data-parent-id' ).val( newParentID ); + menuItem.moveHorizontally( menuItemNewDepth, menuItemOldDepth ); + + if ( menuItemNoChildren > 0 ) { + menuItem = menuItem.add( menuItemChildren ); + } + menuItem.detach(); + + menuItems = $( '#menu-to-edit li' ); + + var parentItemPosition = parseInt( parentItem.index(), 10 ), + parentItemNoChild = parseInt( parentItem.childMenuItems().length, 10 ); + + if ( parentItemNoChild > 0 ){ + menuItemNewPosition = parentItemPosition + parentItemNoChild; + } else { + menuItemNewPosition = parentItemPosition; + } + + if ( newParentID == 0 ) { + menuItemNewPosition = menuItems.length - 1; + } + + menuItem.insertAfter( menuItems.eq( menuItemNewPosition ) ).updateParentMenuItemDBId().updateParentDropdown().updateOrderDropdown(); + + api.registerChange(); + api.refreshKeyboardAccessibility(); + api.refreshAdvancedAccessibility(); + $this.trigger( 'focus' ); + wp.a11y.speak( menus.parentUpdated, 'polite' ); + }, + + /** + * changeMenuOrder( [OrderDropdown] ) + * + * @since 6.7.0 + * + * @param {object} orderDropdown select field + */ + changeMenuOrder : function( orderDropdown ) { + var menuItems = $( '#menu-to-edit li' ), + $this = $( orderDropdown ), + newOrderID = parseInt( $this.val(), 10), + menuItem = $this.closest( 'li.menu-item' ).first(), + menuItemChildren = menuItem.childMenuItems(), + menuItemNoChildren = menuItemChildren.length, + menuItemCurrentPosition = parseInt( menuItem.index(), 10 ), + parentItemID = menuItem.find( '.menu-item-data-parent-id' ).val(), + subItems = $( '.menu-item .menu-item-data-parent-id[value="' + parentItemID + '"]' ), + currentItemAtPosition = $(subItems[newOrderID - 1]).closest( 'li.menu-item' ); + + if ( menuItemNoChildren > 0 ) { + menuItem = menuItem.add( menuItemChildren ); + } + + var currentItemNoChildren = currentItemAtPosition.childMenuItems().length, + currentItemPosition = parseInt( currentItemAtPosition.index(), 10 ); + + menuItems = $( '#menu-to-edit li' ); + + var menuItemNewPosition = currentItemPosition; + + if(menuItemCurrentPosition > menuItemNewPosition){ + menuItemNewPosition = currentItemPosition; + menuItem.detach().insertBefore( menuItems.eq( menuItemNewPosition ) ).updateOrderDropdown(); + } else { + menuItemNewPosition = menuItemNewPosition + currentItemNoChildren; + menuItem.detach().insertAfter( menuItems.eq( menuItemNewPosition ) ).updateOrderDropdown(); + } + + api.registerChange(); + api.refreshKeyboardAccessibility(); + api.refreshAdvancedAccessibility(); + $this.trigger( 'focus' ); + wp.a11y.speak( menus.orderUpdated, 'polite' ); }, /** @@ -737,6 +968,8 @@ api.refreshKeyboardAccessibility(); api.refreshAdvancedAccessibility(); + ui.item.updateParentDropdown(); + ui.item.updateOrderDropdown(); api.refreshAdvancedAccessibilityOfItem( ui.item.find( 'a.item-edit' ) ); }, change: function(e, ui) { @@ -869,13 +1102,53 @@ }, 500 ) ); $('#add-custom-links input[type="text"]').on( 'keypress', function(e){ - $('#customlinkdiv').removeClass('form-invalid'); + $( '#customlinkdiv' ).removeClass( 'form-invalid' ); + $( '#custom-menu-item-url' ).removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' ); + $( '#custom-url-error' ).hide(); if ( e.keyCode === 13 ) { e.preventDefault(); $( '#submit-customlinkdiv' ).trigger( 'click' ); } }); + + $( '#submit-customlinkdiv' ).on( 'click', function (e) { + var urlInput = $( '#custom-menu-item-url' ), + url = urlInput.val().trim(), + errorMessage = $( '#custom-url-error' ), + urlWrap = $( '#menu-item-url-wrap' ), + urlRegex; + + // Hide the error message initially + errorMessage.hide(); + urlWrap.removeClass( 'has-error' ); + + /* + * Allow URLs including: + * - http://example.com/ + * - //example.com + * - /directory/ + * - ?query-param + * - #target + * - mailto:foo@example.com + * + * Any further validation will be handled on the server when the setting is attempted to be saved, + * so this pattern does not need to be complete. + */ + urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/; + if ( ! urlRegex.test( url ) ) { + e.preventDefault(); + urlInput.addClass( 'form-invalid' ) + .attr( 'aria-invalid', 'true' ) + .attr( 'aria-describedby', 'custom-url-error' ); + + errorMessage.show(); + var errorText = errorMessage.text(); + urlWrap.addClass( 'has-error' ); + // Announce error message via screen reader + wp.a11y.speak( errorText, 'assertive' ); + } + }); }, /** @@ -988,6 +1261,8 @@ deletionSpeech = menus.itemsDeleted.replace( '%s', itemsPendingDeletion ); wp.a11y.speak( deletionSpeech, 'polite' ); that.disableBulkSelection(); + $( '#menu-to-edit' ).updateParentDropdown(); + $( '#menu-to-edit' ).updateOrderDropdown(); } }); }, @@ -1154,7 +1429,8 @@ addCustomLink : function( processMethod ) { var url = $('#custom-menu-item-url').val().toString(), - label = $('#custom-menu-item-name').val(); + label = $('#custom-menu-item-name').val(), + urlRegex; if ( '' !== url ) { url = url.trim(); @@ -1162,7 +1438,20 @@ processMethod = processMethod || api.addMenuItemToBottom; - if ( '' === url || 'https://' == url || 'http://' == url ) { + /* + * Allow URLs including: + * - http://example.com/ + * - //example.com + * - /directory/ + * - ?query-param + * - #target + * - mailto:foo@example.com + * + * Any further validation will be handled on the server when the setting is attempted to be saved, + * so this pattern does not need to be complete. + */ + urlRegex = /^((\w+:)?\/\/\w.*|\w+:(?!\/\/$)|\/|\?|#)/; + if ( ! urlRegex.test( url ) ) { $('#customlinkdiv').addClass('form-invalid'); return false; } @@ -1527,6 +1816,8 @@ } api.refreshAdvancedAccessibility(); wp.a11y.speak( menus.itemRemoved ); + $( '#menu-to-edit' ).updateParentDropdown(); + $( '#menu-to-edit' ).updateOrderDropdown(); }); },