Skip to content
Draft
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
33 changes: 33 additions & 0 deletions src/js/_enqueues/lib/nav-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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/
Expand Down Expand Up @@ -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/
Expand Down
18 changes: 12 additions & 6 deletions src/wp-admin/includes/nav-menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,13 @@ function wp_nav_menu_item_link_meta_box() {

?>
<div class="customlinkdiv" id="customlinkdiv">
<input type="hidden" value="custom" name="menu-item[<?php echo $_nav_menu_placeholder; ?>][menu-item-type]" />
<input type="hidden" id="custom-menu-item-type" value="custom" name="menu-item[<?php echo $_nav_menu_placeholder; ?>][menu-item-type]" />
<p id="menu-item-placeholder-wrap" class="wp-clearfix">
<label>
<input type="checkbox" id="custom-menu-item-placeholder"<?php wp_nav_menu_disabled_check( $nav_menu_selected_id ); ?> />
<?php _e( 'No URL (use as a section label)' ); ?>
</label>
</p>
<p id="menu-item-url-wrap" class="wp-clearfix">
<label for="custom-menu-item-url"><?php _e( 'URL' ); ?></label>
<input id="custom-menu-item-url" name="menu-item[<?php echo $_nav_menu_placeholder; ?>][menu-item-url]"
Expand Down Expand Up @@ -1159,11 +1165,11 @@ function wp_save_nav_menu_items( $menu_id = 0, $menu_data = array() ) {
(
// And item type either isn't set.
! isset( $_item_object_data['menu-item-type'] ) ||
// Or URL is the default.
in_array( $_item_object_data['menu-item-url'], array( 'https://', 'http://', '' ), true ) ||
// Or it's not a custom menu item (but not the custom home page).
! ( 'custom' === $_item_object_data['menu-item-type'] && ! isset( $_item_object_data['menu-item-db-id'] ) ) ||
// Or it *is* a custom menu item that already exists.
// Or URL is the default (placeholders are exempt — they intentionally have no URL).
( in_array( $_item_object_data['menu-item-url'], array( 'https://', 'http://', '' ), true ) && 'placeholder' !== $_item_object_data['menu-item-type'] ) ||
// Or it's not a custom or placeholder menu item (but not the custom home page).
! ( in_array( $_item_object_data['menu-item-type'], array( 'custom', 'placeholder' ), true ) && ! isset( $_item_object_data['menu-item-db-id'] ) ) ||
// Or it *is* a custom/placeholder menu item that already exists.
! empty( $_item_object_data['menu-item-db-id'] )
)
) {
Expand Down
139 changes: 89 additions & 50 deletions src/wp-includes/class-walker-nav-menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,61 +248,100 @@ public function start_el( &$output, $data_object, $depth = 0, $args = null, $cur
*/
$title = apply_filters( 'nav_menu_item_title', $title, $menu_item, $args, $depth );

$atts = array();
$atts['target'] = ! empty( $menu_item->target ) ? $menu_item->target : '';
$atts['rel'] = ! empty( $menu_item->xfn ) ? $menu_item->xfn : '';
$menu_item_type = isset( $menu_item->type ) ? $menu_item->type : '';

if ( 'placeholder' === $menu_item_type ) {
/*
* Placeholder items render as a non-interactive `<span>` rather than an `<a>`.
* Because most themes style nav links via selectors like `> a`, the `<span>`
* will not automatically inherit those styles. Themes that wish to display
* placeholders consistently with their other nav items should target the
* `menu-item-type-placeholder` class that is added to the parent `<li>`:
*
* .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 `<span>` 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 .= '<span' . $attributes . '>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</span>';
$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 `<a>` 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 .= '<a' . $attributes . '>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$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 `<a>` 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 .= '<a' . $attributes . '>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;

/**
* Filters a menu item's starting output.
*
Expand Down
13 changes: 10 additions & 3 deletions src/wp-includes/nav-menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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',
);
Expand Down
Loading