diff --git a/src/js/_enqueues/lib/nav-menu.js b/src/js/_enqueues/lib/nav-menu.js index 79917c8447f1a..127976de02dda 100644 --- a/src/js/_enqueues/lib/nav-menu.js +++ b/src/js/_enqueues/lib/nav-menu.js @@ -1116,6 +1116,18 @@ } }); + $( '#custom-menu-item-placeholder' ).on( 'change', function() { + if ( $( this ).is( ':checked' ) ) { + $( '#menu-item-url-wrap' ).hide().removeClass( 'has-error' ); + $( '#custom-menu-item-type' ).val( 'placeholder' ); + $( '#custom-menu-item-url' ).removeClass( 'form-invalid' ).removeAttr( 'aria-invalid' ).removeAttr( 'aria-describedby' ); + $( '#custom-url-error' ).hide(); + } else { + $( '#menu-item-url-wrap' ).show(); + $( '#custom-menu-item-type' ).val( 'custom' ); + } + }); + $( '#submit-customlinkdiv' ).on( 'click', function (e) { var urlInput = $( '#custom-menu-item-url' ), url = urlInput.val().trim(), @@ -1127,6 +1139,11 @@ errorMessage.hide(); urlWrap.removeClass( 'has-error' ); + // Placeholder items intentionally have no URL; skip validation. + if ( $( '#custom-menu-item-placeholder' ).is( ':checked' ) ) { + return; + } + /* * Allow URLs including: * - http://example.com/ @@ -1458,6 +1475,22 @@ processMethod = processMethod || api.addMenuItemToBottom; + // Placeholder items skip URL validation and use an empty URL. + if ( $( '#custom-menu-item-placeholder' ).is( ':checked' ) ) { + $( '.customlinkdiv .spinner' ).addClass( 'is-active' ); + api.addItemToMenu( + {'-1': {'menu-item-type': 'placeholder', 'menu-item-url': '', 'menu-item-title': label, 'menu-item-db-id': 0, 'menu-item-object': 'custom', 'menu-item-parent-id': 0}}, + processMethod, + function() { + $( '.customlinkdiv .spinner' ).removeClass( 'is-active' ); + $('#custom-menu-item-name').val('').trigger( 'blur' ); + $( '#custom-menu-item-url' ).val( '' ).attr( 'placeholder', 'https://' ); + $( '#custom-menu-item-placeholder' ).prop( 'checked', false ).trigger( 'change' ); + } + ); + return; + } + /* * Allow URLs including: * - http://example.com/ diff --git a/src/wp-admin/includes/nav-menu.php b/src/wp-admin/includes/nav-menu.php index 70263a2034807..b18740d8e5c85 100644 --- a/src/wp-admin/includes/nav-menu.php +++ b/src/wp-admin/includes/nav-menu.php @@ -361,7 +361,13 @@ function wp_nav_menu_item_link_meta_box() { ?>
- + +
  • `: + * + * .menu-item-type-placeholder > span { + * display: block; + * cursor: default; + * } + * + * Use the `nav_menu_placeholder_attributes` filter to add custom attributes + * or classes directly to the `` element. + */ + + /** + * Filters the HTML attributes applied to a placeholder menu item's span element. + * + * @since x.x.x + * + * @param array $atts The HTML attributes applied to the span element, empty strings are ignored. + * @param WP_Post $menu_item The current menu item object. + * @param stdClass $args An object of wp_nav_menu() arguments. + * @param int $depth Depth of menu item. Used for padding. + */ + $span_atts = apply_filters( 'nav_menu_placeholder_attributes', array(), $menu_item, $args, $depth ); + $attributes = $this->build_atts( $span_atts ); + + $item_output = $args->before; + $item_output .= ''; + $item_output .= $args->link_before . $title . $args->link_after; + $item_output .= ''; + $item_output .= $args->after; + } else { + $atts = array(); + $atts['target'] = ! empty( $menu_item->target ) ? $menu_item->target : ''; + $atts['rel'] = ! empty( $menu_item->xfn ) ? $menu_item->xfn : ''; + + if ( ! empty( $menu_item->url ) ) { + if ( $this->privacy_policy_url === $menu_item->url ) { + $atts['rel'] = empty( $atts['rel'] ) ? 'privacy-policy' : $atts['rel'] . ' privacy-policy'; + } + + $atts['href'] = $menu_item->url; + } else { + $atts['href'] = ''; + } - if ( ! empty( $menu_item->url ) ) { - if ( $this->privacy_policy_url === $menu_item->url ) { - $atts['rel'] = empty( $atts['rel'] ) ? 'privacy-policy' : $atts['rel'] . ' privacy-policy'; + $atts['aria-current'] = $menu_item->current ? 'page' : ''; + + // Add title attribute only if it does not match the link text (before or after filtering). + if ( ! empty( $menu_item->attr_title ) + && trim( strtolower( $menu_item->attr_title ) ) !== trim( strtolower( $menu_item->title ) ) + && trim( strtolower( $menu_item->attr_title ) ) !== trim( strtolower( $the_title_filtered ) ) + && trim( strtolower( $menu_item->attr_title ) ) !== trim( strtolower( $title ) ) + ) { + $atts['title'] = $menu_item->attr_title; + } else { + $atts['title'] = ''; } - $atts['href'] = $menu_item->url; - } else { - $atts['href'] = ''; + /** + * Filters the HTML attributes applied to a menu item's anchor element. + * + * @since 3.6.0 + * @since 4.1.0 The `$depth` parameter was added. + * + * @param array $atts { + * The HTML attributes applied to the menu item's `` element, empty strings are ignored. + * + * @type string $title Title attribute. + * @type string $target Target attribute. + * @type string $rel The rel attribute. + * @type string $href The href attribute. + * @type string $aria-current The aria-current attribute. + * } + * @param WP_Post $menu_item The current menu item object. + * @param stdClass $args An object of wp_nav_menu() arguments. + * @param int $depth Depth of menu item. Used for padding. + */ + $atts = apply_filters( 'nav_menu_link_attributes', $atts, $menu_item, $args, $depth ); + $attributes = $this->build_atts( $atts ); + + $item_output = $args->before; + $item_output .= ''; + $item_output .= $args->link_before . $title . $args->link_after; + $item_output .= ''; + $item_output .= $args->after; } - $atts['aria-current'] = $menu_item->current ? 'page' : ''; - - // Add title attribute only if it does not match the link text (before or after filtering). - if ( ! empty( $menu_item->attr_title ) - && trim( strtolower( $menu_item->attr_title ) ) !== trim( strtolower( $menu_item->title ) ) - && trim( strtolower( $menu_item->attr_title ) ) !== trim( strtolower( $the_title_filtered ) ) - && trim( strtolower( $menu_item->attr_title ) ) !== trim( strtolower( $title ) ) - ) { - $atts['title'] = $menu_item->attr_title; - } else { - $atts['title'] = ''; - } - - /** - * Filters the HTML attributes applied to a menu item's anchor element. - * - * @since 3.6.0 - * @since 4.1.0 The `$depth` parameter was added. - * - * @param array $atts { - * The HTML attributes applied to the menu item's `` element, empty strings are ignored. - * - * @type string $title Title attribute. - * @type string $target Target attribute. - * @type string $rel The rel attribute. - * @type string $href The href attribute. - * @type string $aria-current The aria-current attribute. - * } - * @param WP_Post $menu_item The current menu item object. - * @param stdClass $args An object of wp_nav_menu() arguments. - * @param int $depth Depth of menu item. Used for padding. - */ - $atts = apply_filters( 'nav_menu_link_attributes', $atts, $menu_item, $args, $depth ); - $attributes = $this->build_atts( $atts ); - - $item_output = $args->before; - $item_output .= ''; - $item_output .= $args->link_before . $title . $args->link_after; - $item_output .= ''; - $item_output .= $args->after; - /** * Filters a menu item's starting output. * diff --git a/src/wp-includes/nav-menu.php b/src/wp-includes/nav-menu.php index ed49892ac0eb6..b29dde395f7ea 100644 --- a/src/wp-includes/nav-menu.php +++ b/src/wp-includes/nav-menu.php @@ -475,7 +475,10 @@ function wp_update_nav_menu_item( $menu_id = 0, $menu_item_db_id = 0, $menu_item $original_parent = 0 < $menu_item_db_id ? get_post_field( 'post_parent', $menu_item_db_id ) : 0; - if ( 'custom' === $args['menu-item-type'] ) { + if ( 'placeholder' === $args['menu-item-type'] ) { + // Placeholder items intentionally have no URL. + $args['menu-item-url'] = ''; + } elseif ( 'custom' === $args['menu-item-type'] ) { // If custom menu item, trim the URL. $args['menu-item-url'] = trim( $args['menu-item-url'] ); } else { @@ -573,9 +576,9 @@ function wp_update_nav_menu_item( $menu_id = 0, $menu_item_db_id = 0, $menu_item } } - if ( 'custom' === $args['menu-item-type'] ) { + if ( 'custom' === $args['menu-item-type'] || 'placeholder' === $args['menu-item-type'] ) { $args['menu-item-object-id'] = $menu_item_db_id; - $args['menu-item-object'] = 'custom'; + $args['menu-item-object'] = $args['menu-item-type']; } $menu_item_db_id = (int) $menu_item_db_id; @@ -950,6 +953,10 @@ function wp_setup_nav_menu_item( $menu_item ) { $menu_item->title = ( '' === $menu_item->post_title ) ? $original_title : $menu_item->post_title; + } elseif ( 'placeholder' === $menu_item->type ) { + $menu_item->type_label = __( 'Placeholder' ); + $menu_item->title = $menu_item->post_title; + $menu_item->url = ''; } else { $menu_item->type_label = __( 'Custom Link' ); $menu_item->title = $menu_item->post_title; diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php index dd72bc1c15210..4b5d8178a8998 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php @@ -468,6 +468,16 @@ protected function prepare_item_for_database( $request ) { } } + // Placeholder items require a title and must not have a URL. + if ( 'placeholder' === $prepared_nav_item['menu-item-type'] ) { + if ( '' === $prepared_nav_item['menu-item-title'] ) { + $error->add( 'rest_title_required', __( 'The title is required when using a placeholder menu item type.' ), array( 'status' => 400 ) ); + } + if ( ! empty( $prepared_nav_item['menu-item-url'] ) ) { + $error->add( 'rest_placeholder_url_invalid', __( 'A placeholder menu item type must not have a URL.' ), array( 'status' => 400 ) ); + } + } + if ( $error->has_errors() ) { return $error; } @@ -775,7 +785,7 @@ public function get_item_schema() { $schema['properties']['type'] = array( 'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".' ), 'type' => 'string', - 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ), + 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom', 'placeholder' ), 'context' => array( 'view', 'edit', 'embed' ), 'default' => 'custom', );