wp/wp-admin/js/nav-menu.js
changeset 22 8c2e4d02f4ef
parent 21 48c4eec2b7e6
--- 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 += '<option ' + $selected + ' value="0">' + wp.i18n._x( 'No Parent', 'menu item without a parent in navigation menu' ) + '</option>';
+
+							$.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 += '<option ' + $selected + ' value="' + menuID + '">' + menuTitle + '</option>';
+								}
+							});
+
+							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 += '<option ' + $selected + ' value="' + i + '">' + itemString + '</option>';
+								}
+
+							} 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 += '<option ' + $selected + ' value="' + i + '">' + submenuString + '</option>';
+								}
+
+							}
+
+							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();
 				});
 		},