Skip to content

Commit

Permalink
Navigation: Allow Search block to be added alongside links
Browse files Browse the repository at this point in the history
Adds support for nav menu items with `type` set to `'html'`. These are
nav menu items that can contain HTML or block markup which is rendered
in place of the link. This allows the Navigation screen to use `'html`'
menu items to support the Search block alongside links.
  • Loading branch information
noisysocks committed Jun 22, 2020
1 parent cbef072 commit 60bbbb8
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 18 deletions.
63 changes: 62 additions & 1 deletion lib/class-wp-rest-menu-items-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ protected function prepare_item_for_database( $request ) {
'menu-item-title' => $menu_item_obj->title,
'menu-item-url' => $menu_item_obj->url,
'menu-item-description' => $menu_item_obj->description,
'menu-item-content' => $menu_item_obj->menu_item_content,
'menu-item-attr-title' => $menu_item_obj->attr_title,
'menu-item-target' => $menu_item_obj->target,
// Stored in the database as a string.
Expand All @@ -337,6 +338,7 @@ protected function prepare_item_for_database( $request ) {
'menu-item-title' => '',
'menu-item-url' => '',
'menu-item-description' => '',
'menu-item-content' => '',
'menu-item-attr-title' => '',
'menu-item-target' => '',
'menu-item-classes' => '',
Expand Down Expand Up @@ -390,6 +392,15 @@ protected function prepare_item_for_database( $request ) {
}
}

// Nav menu content.
if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
if ( is_string( $request['content'] ) ) {
$prepared_nav_item['menu-item-content'] = $request['content'];
} elseif ( isset( $request['content']['raw'] ) ) {
$prepared_nav_item['menu-item-content'] = $request['content']['raw'];
}
}

// Check if object id exists before saving.
if ( ! $prepared_nav_item['menu-item-object'] ) {
// If taxonony, check if term exists.
Expand Down Expand Up @@ -429,6 +440,13 @@ protected function prepare_item_for_database( $request ) {
}
}

// If menu item is type html, then content is required.
if ( 'html' === $prepared_nav_item['menu-item-type'] ) {
if ( empty( $prepared_nav_item['menu-item-content'] ) ) {
return new WP_Error( 'rest_content_required', __( 'Content required if menu item of type html.', 'gutenberg' ), array( 'status' => 400 ) );
}
}

// If menu id is set, valid the value of menu item position and parent id.
if ( ! empty( $prepared_nav_item['menu-id'] ) ) {
// Check if nav menu is valid.
Expand Down Expand Up @@ -596,6 +614,20 @@ public function prepare_item_for_response( $post, $request ) {
$data['object_id'] = absint( $menu_item->object_id );
}

if ( rest_is_field_included( 'content', $fields ) ) {
$data['content'] = array();
}
if ( rest_is_field_included( 'content.raw', $fields ) ) {
$data['content']['raw'] = $menu_item->content;
}
if ( rest_is_field_included( 'content.rendered', $fields ) ) {
/** This filter is documented in wp-includes/post-template.php */
$data['content']['rendered'] = apply_filters( 'the_content', $menu_item->content );
}
if ( rest_is_field_included( 'content.block_version', $fields ) ) {
$data['content']['block_version'] = block_version( $menu_item->content );
}

if ( in_array( 'parent', $fields, true ) ) {
// Same as post_parent, expose as integer.
$data['parent'] = absint( $menu_item->menu_item_parent );
Expand Down Expand Up @@ -787,7 +819,7 @@ public function get_item_schema() {
$schema['properties']['type'] = array(
'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".', 'gutenberg' ),
'type' => 'string',
'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ),
'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom', 'html' ),
'context' => array( 'view', 'edit', 'embed' ),
'default' => 'custom',
);
Expand Down Expand Up @@ -861,6 +893,35 @@ public function get_item_schema() {
'default' => 0,
);

$schema['properties']['content'] = array(
'description' => __( 'HTML content to display for this menu item. May contain blocks.', 'gutenberg' ),
'context' => array( 'view', 'edit', 'embed' ),
'type' => 'object',
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'HTML content, as it exists in the database.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML content, transformed for display.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'block_version' => array(
'description' => __( 'Version of the block format used in the HTML content.', 'gutenberg' ),
'type' => 'integer',
'context' => array( 'edit' ),
'readonly' => true,
),
),
);

$schema['properties']['target'] = array(
'description' => __( 'The target attribute of the link element for this menu item.', 'gutenberg' ),
'type' => 'string',
Expand Down
120 changes: 115 additions & 5 deletions lib/compat.php
Original file line number Diff line number Diff line change
Expand Up @@ -415,18 +415,20 @@ function gutenberg_render_block_with_assigned_block_context( $pre_render, $parse
$parsed_block = apply_filters( 'render_block_data', $parsed_block, $source_block );

$context = array(
'postId' => $post->ID,
'query' => array( 'categoryIds' => array() ),
);

if ( isset( $post ) ) {
$context['postId'] = $post->ID;

/*
* The `postType` context is largely unnecessary server-side, since the
* ID is usually sufficient on its own. That being said, since a block's
* manifest is expected to be shared between the server and the client,
* it should be included to consistently fulfill the expectation.
*/
'postType' => $post->post_type,

'query' => array( 'categoryIds' => array() ),
);
$context['postType'] = $post->post_type;
}

if ( isset( $wp_query->tax_query->queried_terms['category'] ) ) {
foreach ( $wp_query->tax_query->queried_terms['category']['terms'] as $category_slug_or_id ) {
Expand Down Expand Up @@ -461,3 +463,111 @@ function gutenberg_render_block_with_assigned_block_context( $pre_render, $parse
* @see WP_Block::render
*/
remove_action( 'enqueue_block_assets', 'wp_enqueue_registered_block_scripts_and_styles' );

/**
* Shim that hooks into `wp_update_nav_menu_item` and makes it so that nav menu
* items support a 'content' field. This field contains HTML and is used by nav
* menu items with `type` set to `'html'`.
*
* Specifically, this shim makes it so that:
*
* 1) The `wp_update_nav_menu_item()` function supports setting
* `'menu-item-content'` on a menu item. When merged to Core, this functionality
* should exist in `wp_update_nav_menu_item()`.
*
* 2) The `customize_save` ajax action supports setting `'content'` on a nav
* menu item. When merged to Core, this functionality should exist in
* `WP_Customize_Manager::save()`.
*
* This shim can be removed when the Gutenberg plugin requires a WordPress
* version that has the ticket below.
*
* @see <TICKET>
*
* @param int $menu_id ID of the updated menu.
* @param int $menu_item_db_id ID of the new menu item.
* @param array $args An array of arguments used to update/add the menu item.
*/
function gutenberg_update_nav_menu_item_content( $menu_id, $menu_item_db_id, $args ) {
global $wp_customize;

// Support setting content in customize_save admin-ajax.php requests by
// grabbing the unsanitized $_POST values.
if ( isset( $wp_customize ) ) {
$values = $wp_customize->unsanitized_post_values();
if ( isset( $values[ "nav_menu_item[$menu_item_db_id]" ]['content'] ) ) {
if ( is_string( $values[ "nav_menu_item[$menu_item_db_id]" ]['content'] ) ) {
$args['menu-item-content'] = $values[ "nav_menu_item[$menu_item_db_id]" ]['content'];
} elseif ( isset( $values[ "nav_menu_item[$menu_item_db_id]" ]['content']['raw'] ) ) {
$args['menu-item-content'] = $values[ "nav_menu_item[$menu_item_db_id]" ]['content']['raw'];
}
}
}

$defaults = array(
'menu-item-content' => '',
);

$args = wp_parse_args( $args, $defaults );

update_post_meta( $menu_item_db_id, '_menu_item_content', $args['menu-item-content'] );
}
add_action( 'wp_update_nav_menu_item', 'gutenberg_update_nav_menu_item_content', 10, 3 );

/**
* Shim that hooks into `wp_setup_nav_menu_items` and makes it so that nav menu
* items have a 'content' field. This field contains HTML and is used by nav
* menu items with `type` set to `'html'`.
*
* Specifically, this shim makes it so that the `wp_setup_nav_menu_item()`
* function sets `content` on the returned menu item. When merged to Core, this
* functionality should exist in `wp_setup_nav_menu_item()`.
*
* This shim can be removed when the Gutenberg plugin requires a WordPress
* version that has the ticket below.
*
* @see <TICKET>
*
* @param object $menu_item The menu item object.
*/
function gutenberg_setup_html_nav_menu_item( $menu_item ) {
if ( 'html' === $menu_item->type ) {
$menu_item->type_label = __( 'HTML', 'gutenberg' );
$menu_item->content = ! isset( $menu_item->content ) ? get_post_meta( $menu_item->db_id, '_menu_item_content', true ) : $menu_item->content;
}

return $menu_item;
}
add_filter( 'wp_setup_nav_menu_item', 'gutenberg_setup_html_nav_menu_item' );

/**
* Shim that hooks into `walker_nav_menu_start_el` and makes it so that the
* default walker which renders a menu will correctly render the HTML associated
* with any navigation menu item that has `type` set to `'html`'.
*
* Specifically, this shim makes it so that `Walker_Nav_Menu::start_el()`
* renders the `content` of a nav menu item when its `type` is `'html'`. When
* merged to Core, this functionality should exist in
* `Walker_Nav_Menu::start_el()`.
*
* This shim can be removed when the Gutenberg plugin requires a WordPress
* version that has the ticket below.
*
* @see <TICKET>
*
* @param string $item_output The menu item's starting HTML output.
* @param WP_Post $item Menu item data object.
* @param int $depth Depth of menu item. Used for padding.
* @param stdClass $args An object of wp_nav_menu() arguments.
*/
function gutenberg_output_html_nav_menu_item( $item_output, $item, $depth, $args ) {
if ( 'html' === $item->type ) {
$item_output = $args->before;
/** This filter is documented in wp-includes/post-template.php */
$item_output .= apply_filters( 'the_content', $item->content );
$item_output .= $args->after;
}

return $item_output;
}
add_filter( 'walker_nav_menu_start_el', 'gutenberg_output_html_nav_menu_item', 10, 4 );
5 changes: 4 additions & 1 deletion packages/block-library/src/navigation/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,10 @@ function Navigation( {
>
<InnerBlocks
ref={ ref }
allowedBlocks={ [ 'core/navigation-link' ] }
allowedBlocks={ [
'core/navigation-link',
'core/search',
] }
renderAppender={
( isImmediateParentOfSelectedBlock &&
! selectedBlockHasDescendants ) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { serialize } from '@wordpress/blocks';

/**
* Internal dependencies
Expand Down Expand Up @@ -139,11 +140,26 @@ function computeCustomizedAttribute( blocks, menuId, menuItemsRef ) {

function linkBlockToRequestItem( block, parentId, position ) {
const menuItem = omit( getMenuItemForBlock( block ), 'menus', 'meta' );

let attributes;

if ( block.name === 'core/navigation-link' ) {
attributes = {
type: 'custom',
title: block.attributes?.label,
url: block.attributes.url,
};
} else {
attributes = {
type: 'html',
content: serialize( block ),
};
}

return {
...menuItem,
...attributes,
position,
title: block.attributes?.label,
url: block.attributes.url,
original_title: '',
classes: ( menuItem.classes || [] ).join( ' ' ),
xfn: ( menuItem.xfn || [] ).join( ' ' ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { keyBy, groupBy, sortBy } from 'lodash';
/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks';
import { createBlock, parse } from '@wordpress/blocks';
import { useState, useRef, useEffect } from '@wordpress/element';

/**
Expand Down Expand Up @@ -88,19 +88,39 @@ function menuItemToLinkBlock(
innerBlocks = [],
existingBlock = null
) {
const attributes = {
label: menuItem.title.rendered,
url: menuItem.url,
};
let linkBlock;

if ( menuItem.type === 'html' ) {
const [ parsedBlock ] = parse( menuItem.content.raw ); // TODO: Handle multiple blocks?

if ( parsedBlock ) {
linkBlock = parsedBlock;
} else {
linkBlock = createBlock( 'core/freeform', {
originalContent: menuItem.content.raw,
} );
}
} else {
linkBlock = createBlock(
'core/navigation-link',
{
label: menuItem.title.rendered,
url: menuItem.url,
},
innerBlocks
);
}

if ( existingBlock ) {
return {
...existingBlock,
attributes,
innerBlocks,
name: linkBlock.name,
attributes: linkBlock.attributes,
innerBlocks: linkBlock.innerBlocks,
};
}
return createBlock( 'core/navigation-link', attributes, innerBlocks );

return linkBlock;
}

const mapBlocksByMenuId = ( blocks, menuItemsByClientId ) => {
Expand Down
Loading

0 comments on commit 60bbbb8

Please sign in to comment.