Skip to content

Commit

Permalink
Update Navigation block to render hooked inner blocks (#57754)
Browse files Browse the repository at this point in the history
  • Loading branch information
tjcafferkey committed Jan 24, 2024
1 parent 0c216f1 commit a11d04f
Showing 1 changed file with 139 additions and 3 deletions.
142 changes: 139 additions & 3 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,19 @@ private static function get_inner_blocks_from_navigation_post( $attributes ) {

// 'parse_blocks' includes a null block with '\n\n' as the content when
// it encounters whitespace. This code strips it.
$compacted_blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
$blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );

if ( function_exists( 'get_hooked_blocks' ) ) {
// Run Block Hooks algorithm to inject hooked blocks.
$markup = block_core_navigation_insert_hooked_blocks( $blocks, $navigation_post );
$root_nav_block = parse_blocks( $markup )[0];

$blocks = isset( $root_nav_block['innerBlocks'] ) ? $root_nav_block['innerBlocks'] : $blocks;
}

// TODO - this uses the full navigation block attributes for the
// context which could be refined.
return new WP_Block_List( $compacted_blocks, $attributes );
return new WP_Block_List( $blocks, $attributes );
}
}

Expand Down Expand Up @@ -983,6 +991,17 @@ function block_core_navigation_get_fallback_blocks() {
// Normalizing blocks may result in an empty array of blocks if they were all `null` blocks.
// In this case default to the (Page List) fallback.
$fallback_blocks = ! empty( $maybe_fallback ) ? $maybe_fallback : $fallback_blocks;

if ( function_exists( 'get_hooked_blocks' ) ) {
// Run Block Hooks algorithm to inject hooked blocks.
// We have to run it here because we need the post ID of the Navigation block to track ignored hooked blocks.
$markup = block_core_navigation_insert_hooked_blocks( $fallback_blocks, $navigation_post );
$blocks = parse_blocks( $markup );

if ( isset( $blocks[0]['innerBlocks'] ) ) {
$fallback_blocks = $blocks[0]['innerBlocks'];
}
}
}

/**
Expand All @@ -994,7 +1013,7 @@ function block_core_navigation_get_fallback_blocks() {
*
* @since 5.9.0
*
* @param array[] default fallback blocks provided by the default block mechanic.
* @param array[] $fallback_blocks default fallback blocks provided by the default block mechanic.
*/
return apply_filters( 'block_core_navigation_render_fallback', $fallback_blocks );
}
Expand Down Expand Up @@ -1323,3 +1342,120 @@ function block_core_navigation_get_most_recently_published_navigation() {

return null;
}

/**
* Insert hooked blocks into a Navigation block.
*
* Given a Navigation block's inner blocks and its corresponding `wp_navigation` post object,
* this function inserts hooked blocks into it, and returns the serialized inner blocks in a
* mock Navigation block wrapper.
*
* If there are any hooked blocks that need to be inserted as the Navigation block's first or last
* children, the `wp_navigation` post's `_wp_ignored_hooked_blocks` meta is checked to see if any
* of those hooked blocks should be exempted from insertion.
*
* @param array $inner_blocks Parsed inner blocks of a Navigation block.
* @param WP_Post $post `wp_navigation` post object corresponding to the block.
* @return string Serialized inner blocks in mock Navigation block wrapper, with hooked blocks inserted, if any.
*/
function block_core_navigation_insert_hooked_blocks( $inner_blocks, $post = null ) {
$before_block_visitor = null;
$after_block_visitor = null;
$hooked_blocks = get_hooked_blocks();
$attributes = array();

if ( isset( $post->ID ) ) {
$ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
if ( ! empty( $ignored_hooked_blocks ) ) {
$ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
$attributes['metadata'] = array(
'ignoredHookedBlocks' => $ignored_hooked_blocks,
);
}
}

$mock_anchor_parent_block = array(
'blockName' => 'core/navigation',
'attrs' => $attributes,
'innerBlocks' => $inner_blocks,
'innerContent' => array_fill( 0, count( $inner_blocks ), null ),
);
$before_block_visitor = null;
$after_block_visitor = null;

if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) {
$before_block_visitor = make_before_block_visitor( $hooked_blocks, $post );
$after_block_visitor = make_after_block_visitor( $hooked_blocks, $post );
}

return traverse_and_serialize_block( $mock_anchor_parent_block, $before_block_visitor, $after_block_visitor );
}

/**
* Updates the post meta with the list of ignored hooked blocks when the navigation is created or updated via the REST API.
*
* @param WP_Post $post Post object.
*/
function block_core_navigation_update_ignore_hooked_blocks_meta( $post ) {
if ( ! isset( $post->ID ) ) {
return;
}

// We run the Block Hooks mechanism so it will return the list of ignored hooked blocks
// in the mock root Navigation block's metadata attribute.
// We ignore the rest of the returned `$markup`; `$post->post_content` already has the hooked
// blocks inserted, whereas `$markup` will have them inserted twice.
$blocks = parse_blocks( $post->post_content );
$markup = block_core_navigation_insert_hooked_blocks( $blocks, $post );
$root_nav_block = parse_blocks( $markup )[0];
$ignored_hooked_blocks = isset( $root_nav_block['attrs']['metadata']['ignoredHookedBlocks'] )
? $root_nav_block['attrs']['metadata']['ignoredHookedBlocks']
: array();

if ( ! empty( $ignored_hooked_blocks ) ) {
$existing_ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
if ( ! empty( $existing_ignored_hooked_blocks ) ) {
$existing_ignored_hooked_blocks = json_decode( $existing_ignored_hooked_blocks, true );
$ignored_hooked_blocks = array_unique( array_merge( $ignored_hooked_blocks, $existing_ignored_hooked_blocks ) );
}
update_post_meta( $post->ID, '_wp_ignored_hooked_blocks', json_encode( $ignored_hooked_blocks ) );
}
}

// Injection of hooked blocks into the Navigation block relies on some functions present in WP >= 6.4
// that are not present in Gutenberg's WP 6.4 compatibility layer.
if ( function_exists( 'get_hooked_blocks' ) ) {
add_action( 'rest_insert_wp_navigation', 'block_core_navigation_update_ignore_hooked_blocks_meta', 10, 3 );
}

/**
* Hooks into the REST API response for the core/navigation block and adds the first and last inner blocks.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response The response object.
*/
function block_core_navigation_insert_hooked_blocks_into_rest_response( $response, $post ) {
if ( ! isset( $response->data['content']['raw'] ) || ! isset( $response->data['content']['rendered'] ) ) {
return $response;
}
$parsed_blocks = parse_blocks( $response->data['content']['raw'] );
$content = block_core_navigation_insert_hooked_blocks( $parsed_blocks, $post );

// Remove mock Navigation block wrapper.
$start = strpos( $content, '-->' ) + strlen( '-->' );
$end = strrpos( $content, '<!--' );
$content = substr( $content, $start, $end - $start );

$response->data['content']['raw'] = $content;
$response->data['content']['rendered'] = apply_filters( 'the_content', $content );

return $response;
}

// Injection of hooked blocks into the Navigation block relies on some functions present in WP >= 6.4
// that are not present in Gutenberg's WP 6.4 compatibility layer.
if ( function_exists( 'get_hooked_blocks' ) ) {
add_filter( 'rest_prepare_wp_navigation', 'block_core_navigation_insert_hooked_blocks_into_rest_response', 10, 3 );
}

0 comments on commit a11d04f

Please sign in to comment.