term_id, array( 'update_post_term_cache' => false ) );
_wp_menu_item_classes_by_context( $menu_items );
return $menu_items;
}
/**
* Sorts a standard array of menu items into a nested structure keyed by the
* id of the parent menu.
*
* @param array $menu_items Menu items to sort.
* @return array An array keyed by the id of the parent menu where each element
* is an array of menu items that belong to that parent.
*/
function block_core_navigation_sort_menu_items_by_parent_id( $menu_items ) {
$sorted_menu_items = array();
foreach ( (array) $menu_items as $menu_item ) {
$sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
}
unset( $menu_items, $menu_item );
$menu_items_by_parent_id = array();
foreach ( $sorted_menu_items as $menu_item ) {
$menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item;
}
return $menu_items_by_parent_id;
}
/**
* Add Interactivity API directives to the navigation-submenu and page-list blocks markup using the Tag Processor
* The final HTML of the navigation-submenu and the page-list blocks will look similar to this:
*
*
*
* Title
*
* SUBMENU ITEMS
*
*
*
* @param string $w Markup of the navigation block.
* @param array $block_attributes Block attributes.
*
* @return string Submenu markup with the directives injected.
*/
function gutenberg_block_core_navigation_add_directives_to_submenu( $w, $block_attributes ) {
while ( $w->next_tag(
array(
'tag_name' => 'LI',
'class_name' => 'has-child',
)
) ) {
// Add directives to the parent `
`.
$w->set_attribute( 'data-wp-interactive', true );
$w->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "isMenuOpen": { "click": false, "hover": false }, "overlay": false } } }' );
$w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initMenu' );
$w->set_attribute( 'data-wp-on--focusout', 'actions.core.navigation.handleMenuFocusout' );
$w->set_attribute( 'data-wp-on--keydown', 'actions.core.navigation.handleMenuKeydown' );
if ( ! isset( $block_attributes['openSubmenusOnClick'] ) || false === $block_attributes['openSubmenusOnClick'] ) {
$w->set_attribute( 'data-wp-on--mouseenter', 'actions.core.navigation.openMenuOnHover' );
$w->set_attribute( 'data-wp-on--mouseleave', 'actions.core.navigation.closeMenuOnHover' );
}
// Add directives to the toggle submenu button.
if ( $w->next_tag(
array(
'tag_name' => 'BUTTON',
'class_name' => 'wp-block-navigation-submenu__toggle',
)
) ) {
$w->set_attribute( 'data-wp-on--click', 'actions.core.navigation.toggleMenuOnClick' );
$w->set_attribute( 'data-wp-bind--aria-expanded', 'selectors.core.navigation.isMenuOpen' );
};
// Iterate through subitems if exist.
gutenberg_block_core_navigation_add_directives_to_submenu( $w, $block_attributes );
}
return $w->get_updated_html();
};
/**
* Replaces view script for the Navigation block with version using Interactivity API.
*
* @param array $metadata Block metadata as read in via block.json.
*
* @return array Filtered block type metadata.
*/
function gutenberg_block_core_navigation_update_interactive_view_script( $metadata ) {
if ( 'core/navigation' === $metadata['name'] ) {
$metadata['viewScript'] = array( 'file:./interactivity.min.js' );
}
return $metadata;
}
add_filter( 'block_type_metadata', 'gutenberg_block_core_navigation_update_interactive_view_script', 10, 1 );
}
/**
* Build an array with CSS classes and inline styles defining the colors
* which will be applied to the navigation markup in the front-end.
*
* @param array $attributes Navigation block attributes.
*
* @return array Colors CSS classes and inline styles.
*/
function block_core_navigation_build_css_colors( $attributes ) {
$colors = array(
'css_classes' => array(),
'inline_styles' => '',
'overlay_css_classes' => array(),
'overlay_inline_styles' => '',
);
// Text color.
$has_named_text_color = array_key_exists( 'textColor', $attributes );
$has_custom_text_color = array_key_exists( 'customTextColor', $attributes );
// If has text color.
if ( $has_custom_text_color || $has_named_text_color ) {
// Add has-text-color class.
$colors['css_classes'][] = 'has-text-color';
}
if ( $has_named_text_color ) {
// Add the color class.
$colors['css_classes'][] = sprintf( 'has-%s-color', $attributes['textColor'] );
} elseif ( $has_custom_text_color ) {
// Add the custom color inline style.
$colors['inline_styles'] .= sprintf( 'color: %s;', $attributes['customTextColor'] );
}
// Background color.
$has_named_background_color = array_key_exists( 'backgroundColor', $attributes );
$has_custom_background_color = array_key_exists( 'customBackgroundColor', $attributes );
// If has background color.
if ( $has_custom_background_color || $has_named_background_color ) {
// Add has-background class.
$colors['css_classes'][] = 'has-background';
}
if ( $has_named_background_color ) {
// Add the background-color class.
$colors['css_classes'][] = sprintf( 'has-%s-background-color', $attributes['backgroundColor'] );
} elseif ( $has_custom_background_color ) {
// Add the custom background-color inline style.
$colors['inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customBackgroundColor'] );
}
// Overlay text color.
$has_named_overlay_text_color = array_key_exists( 'overlayTextColor', $attributes );
$has_custom_overlay_text_color = array_key_exists( 'customOverlayTextColor', $attributes );
// If has overlay text color.
if ( $has_custom_overlay_text_color || $has_named_overlay_text_color ) {
// Add has-text-color class.
$colors['overlay_css_classes'][] = 'has-text-color';
}
if ( $has_named_overlay_text_color ) {
// Add the overlay color class.
$colors['overlay_css_classes'][] = sprintf( 'has-%s-color', $attributes['overlayTextColor'] );
} elseif ( $has_custom_overlay_text_color ) {
// Add the custom overlay color inline style.
$colors['overlay_inline_styles'] .= sprintf( 'color: %s;', $attributes['customOverlayTextColor'] );
}
// Overlay background color.
$has_named_overlay_background_color = array_key_exists( 'overlayBackgroundColor', $attributes );
$has_custom_overlay_background_color = array_key_exists( 'customOverlayBackgroundColor', $attributes );
// If has overlay background color.
if ( $has_custom_overlay_background_color || $has_named_overlay_background_color ) {
// Add has-background class.
$colors['overlay_css_classes'][] = 'has-background';
}
if ( $has_named_overlay_background_color ) {
// Add the overlay background-color class.
$colors['overlay_css_classes'][] = sprintf( 'has-%s-background-color', $attributes['overlayBackgroundColor'] );
} elseif ( $has_custom_overlay_background_color ) {
// Add the custom overlay background-color inline style.
$colors['overlay_inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customOverlayBackgroundColor'] );
}
return $colors;
}
/**
* Build an array with CSS classes and inline styles defining the font sizes
* which will be applied to the navigation markup in the front-end.
*
* @param array $attributes Navigation block attributes.
*
* @return array Font size CSS classes and inline styles.
*/
function block_core_navigation_build_css_font_sizes( $attributes ) {
// CSS classes.
$font_sizes = array(
'css_classes' => array(),
'inline_styles' => '',
);
$has_named_font_size = array_key_exists( 'fontSize', $attributes );
$has_custom_font_size = array_key_exists( 'customFontSize', $attributes );
if ( $has_named_font_size ) {
// Add the font size class.
$font_sizes['css_classes'][] = sprintf( 'has-%s-font-size', $attributes['fontSize'] );
} elseif ( $has_custom_font_size ) {
// Add the custom font size inline style.
$font_sizes['inline_styles'] = sprintf( 'font-size: %spx;', $attributes['customFontSize'] );
}
return $font_sizes;
}
/**
* Returns the top-level submenu SVG chevron icon.
*
* @return string
*/
function block_core_navigation_render_submenu_icon() {
return '';
}
/**
* Filter out empty "null" blocks from the block list.
* 'parse_blocks' includes a null block with '\n\n' as the content when
* it encounters whitespace. This is not a bug but rather how the parser
* is designed.
*
* @param array $parsed_blocks the parsed blocks to be normalized.
* @return array the normalized parsed blocks.
*/
function block_core_navigation_filter_out_empty_blocks( $parsed_blocks ) {
$filtered = array_filter(
$parsed_blocks,
static function( $block ) {
return isset( $block['blockName'] );
}
);
// Reset keys.
return array_values( $filtered );
}
/**
* Returns true if the navigation block contains a nested navigation block.
*
* @param WP_Block_List $inner_blocks Inner block instance to be normalized.
* @return bool true if the navigation block contains a nested navigation block.
*/
function block_core_navigation_block_contains_core_navigation( $inner_blocks ) {
foreach ( $inner_blocks as $block ) {
if ( 'core/navigation' === $block->name ) {
return true;
}
if ( $block->inner_blocks && block_core_navigation_block_contains_core_navigation( $block->inner_blocks ) ) {
return true;
}
}
return false;
}
/**
* Retrieves the appropriate fallback to be used on the front of the
* site when there is no menu assigned to the Nav block.
*
* This aims to mirror how the fallback mechanic for wp_nav_menu works.
* See https://developer.wordpress.org/reference/functions/wp_nav_menu/#more-information.
*
* @return array the array of blocks to be used as a fallback.
*/
function block_core_navigation_get_fallback_blocks() {
$page_list_fallback = array(
array(
'blockName' => 'core/page-list',
),
);
$registry = WP_Block_Type_Registry::get_instance();
// If `core/page-list` is not registered then return empty blocks.
$fallback_blocks = $registry->is_registered( 'core/page-list' ) ? $page_list_fallback : array();
$navigation_post = WP_Navigation_Fallback_Gutenberg::get_fallback();
// Use the first non-empty Navigation as fallback if available.
if ( $navigation_post ) {
$parsed_blocks = parse_blocks( $navigation_post->post_content );
$maybe_fallback = block_core_navigation_filter_out_empty_blocks( $parsed_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;
}
/**
* Filters the fallback experience for the Navigation block.
*
* Returning a falsey value will opt out of the fallback and cause the block not to render.
* To customise the blocks provided return an array of blocks - these should be valid
* children of the `core/navigation` block.
*
* @since 5.9.0
*
* @param array[] default fallback blocks provided by the default block mechanic.
*/
return apply_filters( 'block_core_navigation_render_fallback', $fallback_blocks );
}
/**
* Iterate through all inner blocks recursively and get navigation link block's post IDs.
*
* @param WP_Block_List $inner_blocks Block list class instance.
*
* @return array Array of post IDs.
*/
function block_core_navigation_get_post_ids( $inner_blocks ) {
$post_ids = array_map( 'block_core_navigation_from_block_get_post_ids', iterator_to_array( $inner_blocks ) );
return array_unique( array_merge( ...$post_ids ) );
}
/**
* Get post IDs from a navigation link block instance.
*
* @param WP_Block $block Instance of a block.
*
* @return array Array of post IDs.
*/
function block_core_navigation_from_block_get_post_ids( $block ) {
$post_ids = array();
if ( $block->inner_blocks ) {
$post_ids = block_core_navigation_get_post_ids( $block->inner_blocks );
}
if ( 'core/navigation-link' === $block->name || 'core/navigation-submenu' === $block->name ) {
if ( $block->attributes && isset( $block->attributes['kind'] ) && 'post-type' === $block->attributes['kind'] && isset( $block->attributes['id'] ) ) {
$post_ids[] = $block->attributes['id'];
}
}
return $post_ids;
}
/**
* Renders the `core/navigation` block on server.
*
* @param array $attributes The block attributes.
* @param string $content The saved content.
* @param WP_Block $block The parsed block.
*
* @return string Returns the post content with the legacy widget added.
*/
function render_block_core_navigation( $attributes, $content, $block ) {
static $seen_menu_names = array();
// Flag used to indicate whether the rendered output is considered to be
// a fallback (i.e. the block has no menu associated with it).
$is_fallback = false;
$nav_menu_name = '';
/**
* Deprecated:
* The rgbTextColor and rgbBackgroundColor attributes
* have been deprecated in favor of
* customTextColor and customBackgroundColor ones.
* Move the values from old attrs to the new ones.
*/
if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) {
$attributes['customTextColor'] = $attributes['rgbTextColor'];
}
if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) {
$attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor'];
}
unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] );
/**
* This is for backwards compatibility after `isResponsive` attribute has been removed.
*/
$has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive'];
$is_responsive_menu = isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute;
$inner_blocks = $block->inner_blocks;
// Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render.
if ( array_key_exists( 'navigationMenuId', $attributes ) ) {
$attributes['ref'] = $attributes['navigationMenuId'];
}
// If:
// - the gutenberg plugin is active
// - `__unstableLocation` is defined
// - we have menu items at the defined location
// - we don't have a relationship to a `wp_navigation` Post (via `ref`).
// ...then create inner blocks from the classic menu assigned to that location.
if (
defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN &&
array_key_exists( '__unstableLocation', $attributes ) &&
! array_key_exists( 'ref', $attributes ) &&
! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) )
) {
$menu_items = block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] );
if ( empty( $menu_items ) ) {
return '';
}
$menu_items_by_parent_id = block_core_navigation_sort_menu_items_by_parent_id( $menu_items );
$parsed_blocks = block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id );
$inner_blocks = new WP_Block_List( $parsed_blocks, $attributes );
}
// Load inner blocks from the navigation post.
if ( array_key_exists( 'ref', $attributes ) ) {
$navigation_post = get_post( $attributes['ref'] );
if ( ! isset( $navigation_post ) ) {
return '';
}
// Only published posts are valid. If this is changed then a corresponding change
// must also be implemented in `use-navigation-menu.js`.
if ( 'publish' === $navigation_post->post_status ) {
$nav_menu_name = $navigation_post->post_title;
if ( isset( $seen_menu_names[ $nav_menu_name ] ) ) {
++$seen_menu_names[ $nav_menu_name ];
} else {
$seen_menu_names[ $nav_menu_name ] = 1;
}
$parsed_blocks = parse_blocks( $navigation_post->post_content );
// '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 );
// TODO - this uses the full navigation block attributes for the
// context which could be refined.
$inner_blocks = new WP_Block_List( $compacted_blocks, $attributes );
}
}
// If there are no inner blocks then fallback to rendering an appropriate fallback.
if ( empty( $inner_blocks ) ) {
$is_fallback = true; // indicate we are rendering the fallback.
$fallback_blocks = block_core_navigation_get_fallback_blocks();
// Fallback my have been filtered so do basic test for validity.
if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) {
return '';
}
$inner_blocks = new WP_Block_List( $fallback_blocks, $attributes );
}
if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) {
return '';
}
/**
* Filter navigation block $inner_blocks.
* Allows modification of a navigation block menu items.
*
* @since 6.1.0
*
* @param \WP_Block_List $inner_blocks
*/
$inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks );
$layout_justification = array(
'left' => 'items-justified-left',
'right' => 'items-justified-right',
'center' => 'items-justified-center',
'space-between' => 'items-justified-space-between',
);
// Restore legacy classnames for submenu positioning.
$layout_class = '';
if (
isset( $attributes['layout']['justifyContent'] ) &&
isset( $layout_justification[ $attributes['layout']['justifyContent'] ] )
) {
$layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ];
}
if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) {
$layout_class .= ' is-vertical';
}
if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) {
$layout_class .= ' no-wrap';
}
// Manually add block support text decoration as CSS class.
$text_decoration = _wp_array_get( $attributes, array( 'style', 'typography', 'textDecoration' ), null );
$text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration );
$colors = block_core_navigation_build_css_colors( $attributes );
$font_sizes = block_core_navigation_build_css_font_sizes( $attributes );
$classes = array_merge(
$colors['css_classes'],
$font_sizes['css_classes'],
$is_responsive_menu ? array( 'is-responsive' ) : array(),
$layout_class ? array( $layout_class ) : array(),
$is_fallback ? array( 'is-fallback' ) : array(),
$text_decoration ? array( $text_decoration_class ) : array()
);
$post_ids = block_core_navigation_get_post_ids( $inner_blocks );
if ( $post_ids ) {
_prime_post_caches( $post_ids, false, false );
}
$list_item_nav_blocks = array(
'core/navigation-link',
'core/home-link',
'core/site-title',
'core/site-logo',
'core/navigation-submenu',
);
$needs_list_item_wrapper = array(
'core/site-title',
'core/site-logo',
);
$block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : '';
$style = $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles'];
$class = implode( ' ', $classes );
// If the menu name has been used previously then append an ID
// to the name to ensure uniqueness across a given post.
if ( isset( $seen_menu_names[ $nav_menu_name ] ) && $seen_menu_names[ $nav_menu_name ] > 1 ) {
$count = $seen_menu_names[ $nav_menu_name ];
$nav_menu_name = $nav_menu_name . ' ' . ( $count );
}
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $class,
'style' => $style,
'aria-label' => $nav_menu_name,
)
);
$container_attributes = get_block_wrapper_attributes(
array(
'class' => 'wp-block-navigation__container ' . $class,
'style' => $style,
)
);
$inner_blocks_html = '';
$is_list_open = false;
$has_submenus = false;
foreach ( $inner_blocks as $inner_block ) {
$is_list_item = in_array( $inner_block->name, $list_item_nav_blocks, true );
if ( $is_list_item && ! $is_list_open ) {
$is_list_open = true;
$inner_blocks_html .= sprintf(
'