| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Server-side rendering of the `core/navigation` block. 4 * 5 * @package WordPress 6 */ 7 8 /** 9 * Returns the submenu visibility value with backward compatibility 10 * for the deprecated openSubmenusOnClick attribute. 11 * 12 * This function centralizes the migration logic from the boolean 13 * openSubmenusOnClick to the new submenuVisibility enum. 14 * 15 * Backward compatibility: WordPress applies default attribute values, so submenuVisibility 16 * will always have a value even for legacy blocks. We check the legacy openSubmenusOnClick 17 * attribute first to preserve original behavior for blocks saved before the migration. 18 * 19 * @since 7.0.0 20 * 21 * @param array $attributes Block attributes containing submenuVisibility and/or openSubmenusOnClick. 22 * @return string The visibility mode: 'hover', 'click', or 'always'. 23 */ 24 function block_core_navigation_get_submenu_visibility( $attributes ) { 25 $deprecated_open_submenus_on_click = $attributes['openSubmenusOnClick'] ?? null; 26 27 // For backward compatibility, prioritize the legacy attribute if present. 28 // Legacy blocks have openSubmenusOnClick in the database. Since WordPress applies 29 // default values, submenuVisibility will also have a value, but we check the legacy 30 // attribute first to preserve the original behavior. If the block has been updated 31 // and saved in the editor, then the deprecated attribute will be replaced by submenuVisibility. 32 if ( null !== $deprecated_open_submenus_on_click ) { 33 // Convert boolean to string: true -> 'click', false -> 'hover'. 34 return ! empty( $deprecated_open_submenus_on_click ) ? 'click' : 'hover'; 35 } 36 37 $submenu_visibility = $attributes['submenuVisibility'] ?? null; 38 39 // Use submenuVisibility for migrated/new blocks (where openSubmenusOnClick is null). 40 return $submenu_visibility ?? 'hover'; 41 } 42 43 /** 44 * Returns the custom properties used by the Navigation block for a layout. 45 * 46 * @since 7.1.0 47 * 48 * @param array $layout Layout configuration. 49 * @return array Navigation layout custom property declarations. 50 */ 51 function block_core_navigation_get_layout_custom_property_declarations( $layout ) { 52 $justification_values = array( 53 'left' => 'flex-start', 54 'center' => 'center', 55 'right' => 'flex-end', 56 'space-between' => 'space-between', 57 ); 58 $justify_content = is_array( $layout ) ? ( $layout['justifyContent'] ?? 'left' ) : 'left'; 59 if ( ! is_string( $justify_content ) || ! isset( $justification_values[ $justify_content ] ) ) { 60 $justify_content = 'left'; 61 } 62 63 $justification = $justification_values[ $justify_content ]; 64 $is_vertical = is_array( $layout ) && 'vertical' === ( $layout['orientation'] ?? null ); 65 $align = 'center'; 66 $justify = $justification; 67 68 if ( $is_vertical ) { 69 $align = in_array( $justify_content, array( 'center', 'right' ), true ) ? $justification : 'flex-start'; 70 $justify = 'left' === $justify_content ? 'initial' : $justification; 71 } 72 73 return array( 74 '--navigation-layout-justification-setting' => $justification, 75 '--navigation-layout-direction' => $is_vertical ? 'column' : 'row', 76 '--navigation-layout-wrap' => is_array( $layout ) && 'nowrap' === ( $layout['flexWrap'] ?? null ) ? 'nowrap' : 'wrap', 77 '--navigation-layout-justify' => $justify, 78 '--navigation-layout-align' => $align, 79 ); 80 } 81 82 /** 83 * Helper functions used to render the navigation block. 84 * 85 * @since 6.5.0 86 */ 87 class WP_Navigation_Block_Renderer { 88 89 /** 90 * Used to determine whether or not a navigation has submenus. 91 * 92 * @since 6.5.0 93 */ 94 private static $has_submenus = false; 95 96 /** 97 * Used to determine which blocks need an <li> wrapper. 98 * 99 * @since 6.5.0 100 * 101 * @var array 102 */ 103 private static $needs_list_item_wrapper = array( 104 'core/site-title', 105 'core/site-logo', 106 'core/social-links', 107 ); 108 109 /** 110 * Keeps track of all the navigation names that have been seen. 111 * 112 * @since 6.5.0 113 * 114 * @var array 115 */ 116 private static $seen_menu_names = array(); 117 118 119 /** 120 * Returns whether or not this is responsive navigation. 121 * 122 * @since 6.5.0 123 * 124 * @param array $attributes The block attributes. 125 * @return bool Returns whether or not this is responsive navigation. 126 */ 127 private static function is_responsive( $attributes ) { 128 /** 129 * This is for backwards compatibility after the `isResponsive` attribute was been removed. 130 */ 131 132 $has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive']; 133 return isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute; 134 } 135 136 /** 137 * Returns whether or not a navigation has a submenu. 138 * 139 * @since 6.5.0 140 * 141 * @param WP_Block_List $inner_blocks The list of inner blocks. 142 * @return bool Returns whether or not a navigation has a submenu and also sets the member variable. 143 */ 144 private static function has_submenus( $inner_blocks ) { 145 if ( true === static::$has_submenus ) { 146 return static::$has_submenus; 147 } 148 149 foreach ( $inner_blocks as $inner_block ) { 150 // If this is a page list then work out if any of the pages have children. 151 if ( 'core/page-list' === $inner_block->name ) { 152 $all_pages = get_pages( 153 array( 154 'sort_column' => 'menu_order,post_title', 155 'order' => 'asc', 156 ) 157 ); 158 foreach ( (array) $all_pages as $page ) { 159 if ( $page->post_parent ) { 160 static::$has_submenus = true; 161 break; 162 } 163 } 164 } 165 // If this is a navigation submenu then we know we have submenus. 166 if ( 'core/navigation-submenu' === $inner_block->name ) { 167 static::$has_submenus = true; 168 break; 169 } 170 } 171 172 return static::$has_submenus; 173 } 174 175 /** 176 * Determine whether the navigation blocks is interactive. 177 * 178 * @since 6.5.0 179 * 180 * @param array $attributes The block attributes. 181 * @param WP_Block_List $inner_blocks The list of inner blocks. 182 * @return bool Returns whether or not to load the view script. 183 */ 184 private static function is_interactive( $attributes, $inner_blocks ) { 185 $has_submenus = static::has_submenus( $inner_blocks ); 186 $is_responsive_menu = static::is_responsive( $attributes ); 187 $computed_visibility = block_core_navigation_get_submenu_visibility( $attributes ); 188 $open_on_click = 'click' === $computed_visibility; 189 $show_submenu_icon = ! empty( $attributes['showSubmenuIcon'] ); 190 return ( $has_submenus && ( $open_on_click || $show_submenu_icon ) ) || $is_responsive_menu; 191 } 192 193 /** 194 * Returns whether or not a block needs a list item wrapper. 195 * 196 * @since 6.5.0 197 * 198 * @param WP_Block $block The block. 199 * @return bool Returns whether or not a block needs a list item wrapper. 200 */ 201 private static function does_block_need_a_list_item_wrapper( $block ) { 202 203 /** 204 * Filter the list of blocks that need a list item wrapper. 205 * 206 * Affords the ability to customize which blocks need a list item wrapper when rendered 207 * within a core/navigation block. 208 * This is useful for blocks that are not list items but should be wrapped in a list 209 * item when used as a child of a navigation block. 210 * 211 * @since 6.5.0 212 * 213 * @param array $needs_list_item_wrapper The list of blocks that need a list item wrapper. 214 */ 215 $needs_list_item_wrapper = apply_filters( 'block_core_navigation_listable_blocks', static::$needs_list_item_wrapper ); 216 217 return in_array( $block->name, $needs_list_item_wrapper, true ); 218 } 219 220 /** 221 * Returns the markup for a single inner block. 222 * 223 * @since 6.5.0 224 * 225 * @param WP_Block $inner_block The inner block. 226 * @return string Returns the markup for a single inner block. 227 */ 228 private static function get_markup_for_inner_block( $inner_block ) { 229 $inner_block_content = $inner_block->render(); 230 if ( ! empty( $inner_block_content ) ) { 231 if ( static::does_block_need_a_list_item_wrapper( $inner_block ) ) { 232 return '<li class="wp-block-navigation-item">' . $inner_block_content . '</li>'; 233 } 234 } 235 236 return $inner_block_content; 237 } 238 239 /** 240 * Returns the html for blocks from a template part (without navigation container wrapper). 241 * 242 * @since 6.5.0 243 * 244 * @param WP_Block_List $blocks The list of blocks to render. 245 * @return string Returns the html for the template part blocks. 246 */ 247 private static function get_template_part_blocks_html( $blocks ) { 248 $html = ''; 249 foreach ( $blocks as $block ) { 250 $html .= $block->render(); 251 } 252 return $html; 253 } 254 255 /** 256 * Returns the html for the inner blocks of the navigation block. 257 * 258 * @since 6.5.0 259 * 260 * @param array $attributes The block attributes. 261 * @param WP_Block_List $inner_blocks The list of inner blocks. 262 * @return string Returns the html for the inner blocks of the navigation block. 263 */ 264 private static function get_inner_blocks_html( $attributes, $inner_blocks ) { 265 $has_submenus = static::has_submenus( $inner_blocks ); 266 $is_interactive = static::is_interactive( $attributes, $inner_blocks ); 267 268 $style = static::get_styles( $attributes ); 269 $class = static::get_classes( $attributes ); 270 $container_attributes = get_block_wrapper_attributes( 271 array( 272 'class' => 'wp-block-navigation__container ' . $class, 273 'style' => $style, 274 ) 275 ); 276 277 $inner_blocks_html = ''; 278 $is_list_open = false; 279 280 foreach ( $inner_blocks as $inner_block ) { 281 $inner_block_markup = static::get_markup_for_inner_block( $inner_block ); 282 // Skip hidden blocks (e.g. hidden via block visibility) that render 283 // as an empty string. Without this check, empty markup is mistaken 284 // for a non-list-item and incorrectly closes the open <ul>. 285 if ( '' === $inner_block_markup ) { 286 continue; 287 } 288 $p = new WP_HTML_Tag_Processor( $inner_block_markup ); 289 $is_list_item = $p->next_tag( 'LI' ); 290 291 if ( $is_list_item && ! $is_list_open ) { 292 $is_list_open = true; 293 $inner_blocks_html .= sprintf( 294 '<ul %1$s>', 295 $container_attributes 296 ); 297 } 298 299 if ( ! $is_list_item && $is_list_open ) { 300 $is_list_open = false; 301 $inner_blocks_html .= '</ul>'; 302 } 303 304 $inner_blocks_html .= $inner_block_markup; 305 } 306 307 if ( $is_list_open ) { 308 $inner_blocks_html .= '</ul>'; 309 } 310 311 // Add directives to the submenu if needed. 312 if ( $has_submenus && $is_interactive ) { 313 $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); 314 $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); 315 } 316 317 return $inner_blocks_html; 318 } 319 320 /** 321 * Gets the inner blocks for the navigation block from the navigation post. 322 * 323 * @since 6.5.0 324 * 325 * @param array $attributes The block attributes. 326 * @return WP_Block_List Returns the inner blocks for the navigation block. 327 */ 328 private static function get_inner_blocks_from_navigation_post( $attributes ) { 329 $navigation_post = get_post( $attributes['ref'] ); 330 if ( ! isset( $navigation_post ) ) { 331 return new WP_Block_List( array(), $attributes ); 332 } 333 334 // Only published posts are valid. If this is changed then a corresponding change 335 // must also be implemented in `use-navigation-menu.js`. 336 if ( 'publish' === $navigation_post->post_status ) { 337 $parsed_blocks = parse_blocks( $navigation_post->post_content ); 338 339 // 'parse_blocks' includes a null block with '\n\n' as the content when 340 // it encounters whitespace. This code strips it. 341 $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); 342 343 // Re-serialize, and run Block Hooks algorithm to inject hooked blocks. 344 // TODO: See if we can move the apply_block_hooks_to_content_from_post_object() call 345 // before the parse_blocks() call further above, to avoid the extra serialization/parsing. 346 $markup = serialize_blocks( $blocks ); 347 $markup = apply_block_hooks_to_content_from_post_object( $markup, $navigation_post ); 348 $blocks = parse_blocks( $markup ); 349 350 // TODO - this uses the full navigation block attributes for the 351 // context which could be refined. 352 return new WP_Block_List( $blocks, $attributes ); 353 } 354 } 355 356 /** 357 * Gets the inner blocks for the navigation block from the fallback. 358 * 359 * @since 6.5.0 360 * 361 * @param array $attributes The block attributes. 362 * @return WP_Block_List Returns the inner blocks for the navigation block. 363 */ 364 private static function get_inner_blocks_from_fallback( $attributes ) { 365 $fallback_blocks = block_core_navigation_get_fallback_blocks(); 366 367 // Fallback my have been filtered so do basic test for validity. 368 if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { 369 return new WP_Block_List( array(), $attributes ); 370 } 371 372 return new WP_Block_List( $fallback_blocks, $attributes ); 373 } 374 375 /** 376 * Recursively disables overlay menu for navigation blocks within overlay blocks. 377 * Prevents nested overlays (inception). 378 * 379 * @since 6.5.0 380 * 381 * @param array $blocks Array of parsed block arrays. 382 * @return array Modified blocks with overlayMenu set to 'never' for navigation blocks. 383 */ 384 private static function disable_overlay_menu_for_nested_navigation_blocks( $blocks ) { 385 if ( empty( $blocks ) || ! is_array( $blocks ) ) { 386 return $blocks; 387 } 388 389 foreach ( $blocks as &$block ) { 390 if ( ! isset( $block['blockName'] ) ) { 391 continue; 392 } 393 394 // If this is a navigation block, disable its overlay menu. 395 if ( 'core/navigation' === $block['blockName'] ) { 396 if ( ! isset( $block['attrs'] ) ) { 397 $block['attrs'] = array(); 398 } 399 $block['attrs']['overlayMenu'] = 'never'; 400 // Mark this as a nested navigation within an overlay template part 401 // so we can handle its rendering differently. 402 $block['attrs']['_isWithinOverlayTemplatePart'] = true; 403 } 404 405 // Recursively process inner blocks. 406 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { 407 $block['innerBlocks'] = static::disable_overlay_menu_for_nested_navigation_blocks( $block['innerBlocks'] ); 408 } 409 } 410 411 return $blocks; 412 } 413 414 /** 415 * Gets the inner blocks for the navigation block from an overlay template part. 416 * 417 * @since 6.5.0 418 * 419 * @param string $overlay_template_part_id The overlay template part ID in format "theme//slug". 420 * @param array $attributes The block attributes. 421 * @return WP_Block_List Returns the inner blocks for the overlay template part. 422 */ 423 private static function get_overlay_blocks_from_template_part( $overlay_template_part_id, $attributes ) { 424 if ( empty( $overlay_template_part_id ) || ! is_string( $overlay_template_part_id ) ) { 425 return new WP_Block_List( array(), $attributes ); 426 } 427 428 // Parse the template part ID (format: "theme//slug"). 429 // If it's just a slug, construct the full ID using the current theme. 430 $parts = explode( '//', $overlay_template_part_id, 2 ); 431 if ( count( $parts ) === 2 ) { 432 // Already in "theme//slug" format (backward compatibility). 433 $theme = $parts[0]; 434 $slug = $parts[1]; 435 } else { 436 // Just a slug, use current theme. 437 $theme = get_stylesheet(); 438 $slug = $overlay_template_part_id; 439 } 440 441 // Only query for template parts from the active theme. 442 if ( get_stylesheet() !== $theme ) { 443 return new WP_Block_List( array(), $attributes ); 444 } 445 446 // Query for the template part post. 447 $template_part_query = new WP_Query( 448 array( 449 'post_type' => 'wp_template_part', 450 'post_status' => 'publish', 451 'post_name__in' => array( $slug ), 452 'tax_query' => array( 453 array( 454 'taxonomy' => 'wp_theme', 455 'field' => 'name', 456 'terms' => $theme, 457 ), 458 ), 459 'posts_per_page' => 1, 460 'no_found_rows' => true, 461 'lazy_load_term_meta' => false, // Do not lazy load term meta, as template parts only have one term. 462 ) 463 ); 464 465 $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null; 466 467 if ( ! $template_part_post ) { 468 // Try to get from theme file if not in database. 469 // Construct the full template part ID for get_block_file_template. 470 $full_template_part_id = $theme . '//' . $slug; 471 $block_template = get_block_file_template( $full_template_part_id, 'wp_template_part' ); 472 if ( isset( $block_template->content ) ) { 473 // Expand shortcodes before parsing blocks, matching the order in 474 // `render_block_core_template_part()`. 475 $content = shortcode_unautop( $block_template->content ); 476 $content = do_shortcode( $content ); 477 $parsed_blocks = parse_blocks( $content ); 478 $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); 479 // Disable overlay menu for any navigation blocks within the overlay to prevent nested overlays. 480 $blocks = static::disable_overlay_menu_for_nested_navigation_blocks( $blocks ); 481 return new WP_Block_List( $blocks, $attributes ); 482 } 483 return new WP_Block_List( array(), $attributes ); 484 } 485 486 // Get the template part content. 487 $block_template = _build_block_template_result_from_post( $template_part_post ); 488 if ( ! isset( $block_template->content ) ) { 489 return new WP_Block_List( array(), $attributes ); 490 } 491 492 $parsed_blocks = parse_blocks( $block_template->content ); 493 494 // 'parse_blocks' includes a null block with '\n\n' as the content when 495 // it encounters whitespace. This code strips it. 496 $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); 497 498 // Re-serialize, and run Block Hooks algorithm to inject hooked blocks. 499 $markup = serialize_blocks( $blocks ); 500 $markup = apply_block_hooks_to_content_from_post_object( $markup, $template_part_post ); 501 502 // Expand shortcodes before parsing blocks, matching the order in 503 // `render_block_core_template_part()`. 504 $markup = shortcode_unautop( $markup ); 505 $markup = do_shortcode( $markup ); 506 507 $blocks = parse_blocks( $markup ); 508 509 // Disable overlay menu for any navigation blocks within the overlay to prevent nested overlays. 510 $blocks = static::disable_overlay_menu_for_nested_navigation_blocks( $blocks ); 511 512 return new WP_Block_List( $blocks, $attributes ); 513 } 514 515 /** 516 * Gets the inner blocks for the navigation block. 517 * 518 * @since 6.5.0 519 * 520 * @param array $attributes The block attributes. 521 * @param WP_Block $block The parsed block. 522 * @return WP_Block_List Returns the inner blocks for the navigation block. 523 */ 524 private static function get_inner_blocks( $attributes, $block ) { 525 $inner_blocks = $block->inner_blocks; 526 527 // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. 528 if ( array_key_exists( 'navigationMenuId', $attributes ) ) { 529 $attributes['ref'] = $attributes['navigationMenuId']; 530 } 531 532 // If: 533 // - the gutenberg plugin is active 534 // - `__unstableLocation` is defined 535 // - we have menu items at the defined location 536 // - we don't have a relationship to a `wp_navigation` Post (via `ref`). 537 // ...then create inner blocks from the classic menu assigned to that location. 538 if ( 539 defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && 540 array_key_exists( '__unstableLocation', $attributes ) && 541 ! array_key_exists( 'ref', $attributes ) && 542 ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) 543 ) { 544 $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); 545 } 546 547 // Load inner blocks from the navigation post. 548 if ( array_key_exists( 'ref', $attributes ) ) { 549 $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); 550 } 551 552 // If there are no inner blocks then fallback to rendering an appropriate fallback. 553 if ( empty( $inner_blocks ) ) { 554 $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); 555 } 556 557 /** 558 * Filter navigation block $inner_blocks. 559 * Allows modification of a navigation block menu items. 560 * 561 * @since 6.1.0 562 * 563 * @param \WP_Block_List $inner_blocks 564 */ 565 $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); 566 567 $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); 568 if ( $post_ids ) { 569 _prime_post_caches( $post_ids, false, false ); 570 } 571 572 return $inner_blocks; 573 } 574 575 /** 576 * Gets the name of the current navigation, if it has one. 577 * 578 * @since 6.5.0 579 * 580 * @param array $attributes The block attributes. 581 * @return string Returns the name of the navigation. 582 */ 583 private static function get_navigation_name( $attributes ) { 584 585 $navigation_name = $attributes['ariaLabel'] ?? ''; 586 587 if ( ! empty( $navigation_name ) ) { 588 return $navigation_name; 589 } 590 591 // Load the navigation post. 592 if ( array_key_exists( 'ref', $attributes ) ) { 593 $navigation_post = get_post( $attributes['ref'] ); 594 if ( ! isset( $navigation_post ) ) { 595 return $navigation_name; 596 } 597 598 // Only published posts are valid. If this is changed then a corresponding change 599 // must also be implemented in `use-navigation-menu.js`. 600 if ( 'publish' === $navigation_post->post_status ) { 601 return $navigation_post->post_title; 602 } 603 } 604 605 return $navigation_name; 606 } 607 608 /** 609 * Returns the layout class for the navigation block. 610 * 611 * @since 6.5.0 612 * 613 * @param array $attributes The block attributes. 614 * @return string Returns the layout class for the navigation block. 615 */ 616 private static function get_layout_class( $attributes ) { 617 $layout_justification = array( 618 'left' => 'items-justified-left', 619 'right' => 'items-justified-right', 620 'center' => 'items-justified-center', 621 'space-between' => 'items-justified-space-between', 622 ); 623 624 $layout_class = ''; 625 $nav_justify_content = $attributes['layout']['justifyContent'] ?? null; 626 if ( 627 is_string( $nav_justify_content ) && 628 isset( $layout_justification[ $nav_justify_content ] ) 629 ) { 630 $layout_class .= $layout_justification[ $nav_justify_content ]; 631 } 632 if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { 633 $layout_class .= ' is-vertical'; 634 } 635 636 if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { 637 $layout_class .= ' no-wrap'; 638 } 639 return $layout_class; 640 } 641 642 /** 643 * Return classes for the navigation block. 644 * 645 * @since 6.5.0 646 * 647 * @param array $attributes The block attributes. 648 * @return string Returns the classes for the navigation block. 649 */ 650 private static function get_classes( $attributes ) { 651 // Restore legacy classnames for submenu positioning. 652 $layout_class = static::get_layout_class( $attributes ); 653 $colors = block_core_navigation_build_css_colors( $attributes ); 654 $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); 655 $is_responsive_menu = static::is_responsive( $attributes ); 656 657 // Manually add block support text decoration as CSS class. 658 $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; 659 $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); 660 661 $classes = array_merge( 662 $colors['css_classes'], 663 $font_sizes['css_classes'], 664 $is_responsive_menu ? array( 'is-responsive' ) : array(), 665 $layout_class ? array( $layout_class ) : array(), 666 $text_decoration ? array( $text_decoration_class ) : array() 667 ); 668 return implode( ' ', $classes ); 669 } 670 671 /** 672 * Get styles for the navigation block. 673 * 674 * @since 6.5.0 675 * 676 * @param array $attributes The block attributes. 677 * @return string Returns the styles for the navigation block. 678 */ 679 private static function get_styles( $attributes ) { 680 $colors = block_core_navigation_build_css_colors( $attributes ); 681 $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); 682 $block_styles = $attributes['styles'] ?? ''; 683 return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; 684 } 685 686 /** 687 * Get responsive container classes for the navigation block. 688 * 689 * @since 7.0.0 690 * 691 * @param bool $is_hidden_by_default Whether the responsive menu is hidden by default. 692 * @param bool $has_custom_overlay Whether a custom overlay is used. 693 * @param array $colors The colors array. 694 * @return array Returns the responsive container classes. 695 * 696 * @phpstan-param array{ 697 * overlay_css_classes: list<string>, 698 * ... 699 * } $colors 700 */ 701 private static function get_responsive_container_classes( $is_hidden_by_default, $has_custom_overlay, $colors ) { 702 $responsive_container_classes = array( 'wp-block-navigation__responsive-container' ); 703 704 if ( $is_hidden_by_default ) { 705 $responsive_container_classes[] = 'hidden-by-default'; 706 } 707 708 if ( $has_custom_overlay ) { 709 $responsive_container_classes[] = 'disable-default-overlay'; 710 } else { 711 // Don't apply overlay color classes if using a custom overlay template part. 712 // The custom overlay is responsible for its own styling. 713 $responsive_container_classes[] = implode( ' ', $colors['overlay_css_classes'] ); 714 } 715 716 return $responsive_container_classes; 717 } 718 719 /** 720 * Get overlay inline styles for the navigation block. 721 * 722 * @since 7.0.0 723 * 724 * @param bool $has_custom_overlay Whether a custom overlay is used. 725 * @param array $colors The colors array. 726 * @return string Returns the overlay inline styles. 727 * 728 * @phpstan-param array{ 729 * overlay_inline_styles: string, 730 * ... 731 * } $colors 732 */ 733 private static function get_overlay_inline_styles( $has_custom_overlay, $colors ) { 734 $overlay_inline_styles = $has_custom_overlay ? '' : esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ); 735 return ( ! empty( $overlay_inline_styles ) ) ? "style=\"$overlay_inline_styles\"" : ''; 736 } 737 738 /** 739 * Get the responsive container markup 740 * 741 * @since 6.5.0 742 * 743 * @param array $attributes The block attributes. 744 * @param WP_Block_List $inner_blocks The list of inner blocks. 745 * @param string $inner_blocks_html The markup for the inner blocks. 746 * @return string Returns the container markup. 747 */ 748 private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { 749 $is_interactive = static::is_interactive( $attributes, $inner_blocks ); 750 $colors = block_core_navigation_build_css_colors( $attributes ); 751 $modal_unique_id = wp_unique_id( 'modal-' ); 752 753 $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; 754 755 // Set-up variables for custom overlays. 756 $has_custom_overlay = false; 757 $close_button_markup = ''; 758 $has_custom_overlay_close_block = false; 759 $overlay_blocks_html = ''; 760 $custom_overlay_markup = ''; 761 762 // Check if an overlay template part is selected and render it. 763 // This needs to happen before building classes so we know if overlay blocks actually exist. 764 if ( ! empty( $attributes['overlay'] ) ) { 765 // Get blocks from the overlay template part. 766 $overlay_blocks = static::get_overlay_blocks_from_template_part( $attributes['overlay'], $attributes ); 767 // Render template part blocks directly without navigation container wrapper. 768 $overlay_blocks_html = static::get_template_part_blocks_html( $overlay_blocks ); 769 // Check if overlay contains a navigation-overlay-close block (detect in rendered HTML so it works with patterns). 770 $has_custom_overlay_close_block = block_core_navigation_overlay_html_has_close_block( $overlay_blocks_html ); 771 // Add Interactivity API directives to the overlay close block if present. 772 if ( $has_custom_overlay_close_block && $is_interactive ) { 773 $tags = new WP_HTML_Tag_Processor( $overlay_blocks_html ); 774 $overlay_blocks_html = block_core_navigation_add_directives_to_overlay_close( $tags ); 775 } 776 // Images in the overlay are hidden until the menu is opened. Pre-set 777 // fetchpriority="low" so that when wp_filter_content_tags() processes the 778 // parent template part, it sees the attribute already present and calls 779 // wp_get_loading_optimization_attributes() with fetchpriority="low", which both prevents 780 // fetchpriority="high" from being added and stops the LCP counter from being incremented. 781 $overlay_blocks_html = block_core_navigation_set_overlay_image_fetch_priority( $overlay_blocks_html ); 782 } 783 784 $has_custom_overlay = ! empty( $overlay_blocks_html ); 785 786 $responsive_container_classes = static::get_responsive_container_classes( $is_hidden_by_default, $has_custom_overlay, $colors ); 787 788 $open_button_classes = array( 789 'wp-block-navigation__responsive-container-open', 790 $is_hidden_by_default ? 'always-shown' : '', 791 ); 792 793 $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; 794 $toggle_button_icon = '<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7.5h16v1.5H4z"></path><path d="M4 15h16v1.5H4z"></path></svg>'; 795 if ( isset( $attributes['icon'] ) ) { 796 if ( 'menu' === $attributes['icon'] ) { 797 $toggle_button_icon = '<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 5v1.5h14V5H5z"></path><path d="M5 12.8h14v-1.5H5v1.5z"></path><path d="M5 19h14v-1.5H5V19z"></path></svg>'; 798 } 799 } 800 $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); 801 $toggle_close_button_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m13.06 12 6.47-6.47-1.06-1.06L12 10.94 5.53 4.47 4.47 5.53 10.94 12l-6.47 6.47 1.06 1.06L12 13.06l6.47 6.47 1.06-1.06L13.06 12Z"></path></svg>'; 802 $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); 803 $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. 804 $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. 805 806 // Add Interactivity API directives to the markup if needed. 807 $open_button_directives = ''; 808 $responsive_container_directives = ''; 809 $responsive_dialog_directives = ''; 810 $close_button_directives = ''; 811 if ( $is_interactive ) { 812 $open_button_directives = ' 813 data-wp-on--click="actions.openMenuOnClick" 814 data-wp-on--keydown="actions.handleMenuKeydown" 815 '; 816 $responsive_container_directives = ' 817 data-wp-class--has-modal-open="state.isMenuOpen" 818 data-wp-class--is-menu-open="state.isMenuOpen" 819 data-wp-watch="callbacks.initMenu" 820 data-wp-on--keydown="actions.handleMenuKeydown" 821 data-wp-on--focusout="actions.handleMenuFocusout" 822 tabindex="-1" 823 '; 824 $responsive_dialog_directives = ' 825 data-wp-bind--aria-modal="state.ariaModal" 826 data-wp-bind--aria-label="state.ariaLabel" 827 data-wp-bind--role="state.roleAttribute" 828 '; 829 $close_button_directives = ' 830 data-wp-on--click="actions.closeMenuOnClick" 831 '; 832 $responsive_container_content_directives = ' 833 data-wp-watch="callbacks.focusFirstElement" 834 '; 835 } 836 837 // Don't apply overlay inline styles if using a custom overlay template part. 838 // The custom overlay is responsible for its own styling. 839 $overlay_inline_styles = static::get_overlay_inline_styles( $has_custom_overlay, $colors ); 840 841 if ( $has_custom_overlay ) { 842 $custom_overlay_markup = sprintf( 843 '<div class="wp-block-navigation__overlay-container">%s</div>', 844 $overlay_blocks_html 845 ); 846 } 847 848 // Show default close button for all responsive navigation, 849 // unless custom overlay has its own close block. 850 if ( ! $has_custom_overlay_close_block ) { 851 $close_button_markup = sprintf( 852 '<button %1$s class="wp-block-navigation__responsive-container-close" %2$s>%3$s</button>', 853 $toggle_aria_label_close, 854 $close_button_directives, 855 $toggle_close_button_content 856 ); 857 } 858 859 return sprintf( 860 '<button aria-haspopup="dialog" %3$s class="%6$s" %10$s>%8$s</button> 861 <div class="%5$s" %7$s id="%1$s" %11$s> 862 <div class="wp-block-navigation__responsive-close" tabindex="-1"> 863 <div class="wp-block-navigation__responsive-dialog" %12$s> 864 %13$s 865 <div class="wp-block-navigation__responsive-container-content" %14$s id="%1$s-content"> 866 %2$s 867 %15$s 868 </div> 869 </div> 870 </div> 871 </div>', 872 esc_attr( $modal_unique_id ), 873 $inner_blocks_html, 874 $toggle_aria_label_open, 875 $toggle_aria_label_close, 876 esc_attr( trim( implode( ' ', $responsive_container_classes ) ) ), 877 esc_attr( trim( implode( ' ', $open_button_classes ) ) ), 878 $overlay_inline_styles, 879 $toggle_button_content, 880 $toggle_close_button_content, 881 $open_button_directives, 882 $responsive_container_directives, 883 $responsive_dialog_directives, 884 $close_button_markup, 885 $responsive_container_content_directives, 886 $has_custom_overlay ? $custom_overlay_markup : '' 887 ); 888 } 889 890 /** 891 * Get the wrapper attributes 892 * 893 * @since 6.5.0 894 * 895 * @param array $attributes The block attributes. 896 * @param WP_Block_List $inner_blocks A list of inner blocks. 897 * @return string Returns the navigation block markup. 898 */ 899 private static function get_nav_attributes( $attributes, $inner_blocks ) { 900 $is_interactive = static::is_interactive( $attributes, $inner_blocks ); 901 $is_responsive_menu = static::is_responsive( $attributes ); 902 $style = static::get_styles( $attributes ); 903 $class = static::get_classes( $attributes ); 904 $extra_attributes = array( 905 'class' => $class, 906 'style' => $style, 907 ); 908 // Only add aria-label for top-level navigation blocks. 909 // Skip navigation blocks marked as being within overlay template parts. 910 $is_within_overlay = $attributes['_isWithinOverlayTemplatePart'] ?? false; 911 if ( $is_within_overlay ) { 912 $nav_menu_name = static::get_navigation_name( $attributes ); 913 } else { 914 $nav_menu_name = static::get_unique_navigation_name( $attributes ); 915 } 916 917 if ( ! empty( $nav_menu_name ) ) { 918 $extra_attributes['aria-label'] = $nav_menu_name; 919 } 920 $wrapper_attributes = get_block_wrapper_attributes( $extra_attributes ); 921 922 if ( $is_responsive_menu ) { 923 $nav_element_directives = static::get_nav_element_directives( $is_interactive ); 924 $wrapper_attributes .= ' ' . $nav_element_directives; 925 } 926 927 return $wrapper_attributes; 928 } 929 930 /** 931 * Gets the nav element directives. 932 * 933 * @since 6.5.0 934 * 935 * @param bool $is_interactive Whether the block is interactive. 936 * @return string the directives for the navigation element. 937 */ 938 private static function get_nav_element_directives( $is_interactive ) { 939 if ( ! $is_interactive ) { 940 return ''; 941 } 942 // When adding to this array be mindful of security concerns. 943 $nav_element_context = wp_interactivity_data_wp_context( 944 array( 945 'overlayOpenedBy' => array( 946 'click' => false, 947 'hover' => false, 948 'focus' => false, 949 ), 950 'type' => 'overlay', 951 'roleAttribute' => '', 952 'ariaLabel' => __( 'Menu' ), 953 ) 954 ); 955 $nav_element_directives = ' 956 data-wp-interactive="core/navigation" ' 957 . $nav_element_context; 958 959 return $nav_element_directives; 960 } 961 962 /** 963 * Handle view script module loading. 964 * 965 * @since 6.5.0 966 * 967 * @param array $attributes The block attributes. 968 * @param WP_Block $block The parsed block. 969 * @param WP_Block_List $inner_blocks The list of inner blocks. 970 */ 971 private static function handle_view_script_module_loading( $attributes, $block, $inner_blocks ) { 972 if ( static::is_interactive( $attributes, $inner_blocks ) ) { 973 wp_enqueue_script_module( '@wordpress/block-library/navigation/view' ); 974 } 975 } 976 977 /** 978 * Returns the markup for the navigation block. 979 * 980 * @since 6.5.0 981 * 982 * @param array $attributes The block attributes. 983 * @param WP_Block_List $inner_blocks The list of inner blocks. 984 * @return string Returns the navigation wrapper markup. 985 */ 986 private static function get_inner_block_markup( $attributes, $inner_blocks ) { 987 $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); 988 if ( static::is_responsive( $attributes ) ) { 989 return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); 990 } 991 return $inner_blocks_html; 992 } 993 994 /** 995 * Returns a unique name for the navigation. 996 * 997 * @since 6.5.0 998 * 999 * @param array $attributes The block attributes. 1000 * @return string Returns a unique name for the navigation. 1001 */ 1002 private static function get_unique_navigation_name( $attributes ) { 1003 $nav_menu_name = static::get_navigation_name( $attributes ); 1004 1005 // This is used to count the number of times a navigation name has been seen, 1006 // so that we can ensure every navigation has a unique id. 1007 if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) ) { 1008 ++static::$seen_menu_names[ $nav_menu_name ]; 1009 } else { 1010 static::$seen_menu_names[ $nav_menu_name ] = 1; 1011 } 1012 1013 // If the menu name has been used previously then append an ID 1014 // to the name to ensure uniqueness across a given post. 1015 if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) && static::$seen_menu_names[ $nav_menu_name ] > 1 ) { 1016 $count = static::$seen_menu_names[ $nav_menu_name ]; 1017 $nav_menu_name = $nav_menu_name . ' ' . ( $count ); 1018 } 1019 1020 return $nav_menu_name; 1021 } 1022 1023 /** 1024 * Renders the navigation block. 1025 * 1026 * @since 6.5.0 1027 * 1028 * @param array $attributes The block attributes. 1029 * @param string $content The saved content. 1030 * @param WP_Block $block The parsed block. 1031 * @return string Returns the navigation block markup. 1032 */ 1033 public static function render( $attributes, $content, $block ) { 1034 /** 1035 * Deprecated: 1036 * The rgbTextColor and rgbBackgroundColor attributes 1037 * have been deprecated in favor of 1038 * customTextColor and customBackgroundColor ones. 1039 * Move the values from old attrs to the new ones. 1040 */ 1041 if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { 1042 $attributes['customTextColor'] = $attributes['rgbTextColor']; 1043 } 1044 1045 if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { 1046 $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; 1047 } 1048 1049 unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); 1050 1051 $inner_blocks = static::get_inner_blocks( $attributes, $block ); 1052 // Prevent navigation blocks referencing themselves from rendering. 1053 if ( block_core_navigation_block_tree_has_block_type( 1054 $inner_blocks, 1055 'core/navigation' 1056 ) ) { 1057 return ''; 1058 } 1059 1060 static::handle_view_script_module_loading( $attributes, $block, $inner_blocks ); 1061 1062 // Use div wrapper if this navigation block is within an overlay template part. 1063 $is_within_overlay = $attributes['_isWithinOverlayTemplatePart'] ?? false; 1064 $tag_name = $is_within_overlay ? 'div' : 'nav'; 1065 1066 return sprintf( 1067 '<%1$s %2$s>%3$s</%1$s>', 1068 $tag_name, 1069 static::get_nav_attributes( $attributes, $inner_blocks ), 1070 static::get_inner_block_markup( $attributes, $inner_blocks ) 1071 ); 1072 } 1073 } 1074 1075 // These functions are used for the __unstableLocation feature and only active 1076 // when the gutenberg plugin is active. 1077 if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { 1078 /** 1079 * Returns the menu items for a WordPress menu location. 1080 * 1081 * @since 5.9.0 1082 * 1083 * @param string $location The menu location. 1084 * @return array Menu items for the location. 1085 */ 1086 function block_core_navigation_get_menu_items_at_location( $location ) { 1087 if ( empty( $location ) ) { 1088 return; 1089 } 1090 1091 // Build menu data. The following approximates the code in 1092 // `wp_nav_menu()` and `gutenberg_output_block_nav_menu`. 1093 1094 // Find the location in the list of locations, returning early if the 1095 // location can't be found. 1096 $locations = get_nav_menu_locations(); 1097 if ( ! isset( $locations[ $location ] ) ) { 1098 return; 1099 } 1100 1101 // Get the menu from the location, returning early if there is no 1102 // menu or there was an error. 1103 $menu = wp_get_nav_menu_object( $locations[ $location ] ); 1104 if ( ! $menu || is_wp_error( $menu ) ) { 1105 return; 1106 } 1107 1108 $menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) ); 1109 _wp_menu_item_classes_by_context( $menu_items ); 1110 1111 return $menu_items; 1112 } 1113 1114 1115 /** 1116 * Sorts a standard array of menu items into a nested structure keyed by the 1117 * id of the parent menu. 1118 * 1119 * @since 5.9.0 1120 * 1121 * @param array $menu_items Menu items to sort. 1122 * @return array An array keyed by the id of the parent menu where each element 1123 * is an array of menu items that belong to that parent. 1124 */ 1125 function block_core_navigation_sort_menu_items_by_parent_id( $menu_items ) { 1126 $sorted_menu_items = array(); 1127 foreach ( (array) $menu_items as $menu_item ) { 1128 $sorted_menu_items[ $menu_item->menu_order ] = $menu_item; 1129 } 1130 unset( $menu_items, $menu_item ); 1131 1132 $menu_items_by_parent_id = array(); 1133 foreach ( $sorted_menu_items as $menu_item ) { 1134 $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item; 1135 } 1136 1137 return $menu_items_by_parent_id; 1138 } 1139 1140 /** 1141 * Gets the inner blocks for the navigation block from the unstable location attribute. 1142 * 1143 * @since 6.5.0 1144 * 1145 * @param array $attributes The block attributes. 1146 * @return WP_Block_List Returns the inner blocks for the navigation block. 1147 */ 1148 function block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ) { 1149 $menu_items = block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ); 1150 if ( empty( $menu_items ) ) { 1151 return new WP_Block_List( array(), $attributes ); 1152 } 1153 1154 $menu_items_by_parent_id = block_core_navigation_sort_menu_items_by_parent_id( $menu_items ); 1155 $parsed_blocks = block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id ); 1156 return new WP_Block_List( $parsed_blocks, $attributes ); 1157 } 1158 } 1159 1160 /** 1161 * Checks if the overlay HTML contains a navigation-overlay-close block. 1162 * 1163 * Uses WP_HTML_Tag_Processor to detect the close button in rendered output, 1164 * so it works when the overlay uses patterns (pattern content is rendered at 1165 * output time, not in the block tree). 1166 * 1167 * @since 7.0.0 1168 * 1169 * @param string $html The rendered overlay HTML. 1170 * @return bool True if a close button element is found. 1171 */ 1172 function block_core_navigation_overlay_html_has_close_block( $html ) { 1173 $tags = new WP_HTML_Tag_Processor( $html ); 1174 return $tags->next_tag( 1175 array( 1176 'tag_name' => 'BUTTON', 1177 'class_name' => 'wp-block-navigation-overlay-close', 1178 ) 1179 ); 1180 } 1181 1182 /** 1183 * Add Interactivity API directives to the navigation-overlay-close block 1184 * markup using the Tag Processor. 1185 * 1186 * @since 6.5.0 1187 * 1188 * @param WP_HTML_Tag_Processor $tags Markup of the navigation block. 1189 * @return string Overlay close markup with the directives injected. 1190 */ 1191 function block_core_navigation_add_directives_to_overlay_close( $tags ) { 1192 // Find all navigation-overlay-close buttons. 1193 while ( $tags->next_tag( 1194 array( 1195 'tag_name' => 'BUTTON', 1196 'class_name' => 'wp-block-navigation-overlay-close', 1197 ) 1198 ) ) { 1199 // Add the same close directive as the default close button. 1200 $tags->set_attribute( 'data-wp-on--click', 'actions.closeMenuOnClick' ); 1201 } 1202 return $tags->get_updated_html(); 1203 } 1204 1205 /** 1206 * Sets fetchpriority="low" on all IMG tags within the navigation overlay. 1207 * 1208 * Images in the overlay are hidden until the menu is opened, so they should 1209 * not compete with any actual LCP element image on the page. 1210 * 1211 * @since 7.0.0 1212 * 1213 * @param string $overlay_blocks_html The rendered HTML of the overlay blocks. 1214 * @return string Modified HTML with fetchpriority="low" on all IMG tags. 1215 */ 1216 function block_core_navigation_set_overlay_image_fetch_priority( string $overlay_blocks_html ): string { 1217 $tags = new WP_HTML_Tag_Processor( $overlay_blocks_html ); 1218 while ( $tags->next_tag( 'IMG' ) ) { 1219 $tags->set_attribute( 'fetchpriority', 'low' ); 1220 } 1221 return $tags->get_updated_html(); 1222 } 1223 1224 /** 1225 * Add Interactivity API directives to the navigation-submenu and page-list 1226 * blocks markup using the Tag Processor. 1227 * 1228 * @since 6.3.0 1229 * 1230 * @param WP_HTML_Tag_Processor $tags Markup of the navigation block. 1231 * @param array $block_attributes Block attributes. 1232 * 1233 * @return string Submenu markup with the directives injected. 1234 */ 1235 function block_core_navigation_add_directives_to_submenu( $tags, $block_attributes ) { 1236 while ( $tags->next_tag( 1237 array( 1238 'tag_name' => 'LI', 1239 'class_name' => 'has-child', 1240 ) 1241 ) ) { 1242 // Add directives to the parent `<li>`. 1243 $tags->set_attribute( 'data-wp-interactive', 'core/navigation' ); 1244 $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "type": "submenu", "modal": null, "previousFocus": null }' ); 1245 $tags->set_attribute( 'data-wp-watch', 'callbacks.initMenu' ); 1246 $tags->set_attribute( 'data-wp-on--focusout', 'actions.handleMenuFocusout' ); 1247 $tags->set_attribute( 'data-wp-on--keydown', 'actions.handleMenuKeydown' ); 1248 1249 // This is a fix for Safari. Without it, Safari doesn't change the active 1250 // element when the user clicks on a button. It can be removed once we add 1251 // an overlay to capture the clicks, instead of relying on the focusout 1252 // event. 1253 $tags->set_attribute( 'tabindex', '-1' ); 1254 1255 $computed_visibility = block_core_navigation_get_submenu_visibility( $block_attributes ); 1256 $open_on_hover = 'hover' === $computed_visibility; 1257 1258 if ( $open_on_hover ) { 1259 $tags->set_attribute( 'data-wp-on--pointerenter', 'actions.openMenuOnHover' ); 1260 $tags->set_attribute( 'data-wp-on--pointerleave', 'actions.closeMenuOnHover' ); 1261 } 1262 1263 // Add directives to the toggle submenu button. 1264 if ( $tags->next_tag( 1265 array( 1266 'tag_name' => 'BUTTON', 1267 'class_name' => 'wp-block-navigation-submenu__toggle', 1268 ) 1269 ) ) { 1270 $tags->set_attribute( 'data-wp-on--click', 'actions.toggleMenuOnClick' ); 1271 $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isSubmenuOpen' ); 1272 // The `aria-expanded` attribute for SSR is already added in the submenu block. 1273 } 1274 // Add directives to the submenu. 1275 if ( $tags->next_tag( 1276 array( 1277 'tag_name' => 'UL', 1278 'class_name' => 'wp-block-navigation__submenu-container', 1279 ) 1280 ) ) { 1281 $tags->set_attribute( 'data-wp-on--focus', 'actions.openMenuOnFocus' ); 1282 } 1283 1284 // Iterate through subitems if exist. 1285 block_core_navigation_add_directives_to_submenu( $tags, $block_attributes ); 1286 } 1287 return $tags->get_updated_html(); 1288 } 1289 1290 /** 1291 * Build an array with CSS classes and inline styles defining the colors 1292 * which will be applied to the navigation markup in the front-end. 1293 * 1294 * @since 5.9.0 1295 * 1296 * @param array $attributes Navigation block attributes. 1297 * 1298 * @return array Colors CSS classes and inline styles. 1299 */ 1300 function block_core_navigation_build_css_colors( $attributes ) { 1301 $colors = array( 1302 'css_classes' => array(), 1303 'inline_styles' => '', 1304 'overlay_css_classes' => array(), 1305 'overlay_inline_styles' => '', 1306 ); 1307 1308 // Text color. 1309 $has_named_text_color = array_key_exists( 'textColor', $attributes ); 1310 $has_custom_text_color = array_key_exists( 'customTextColor', $attributes ); 1311 1312 // If has text color. 1313 if ( $has_custom_text_color || $has_named_text_color ) { 1314 // Add has-text-color class. 1315 $colors['css_classes'][] = 'has-text-color'; 1316 } 1317 1318 if ( $has_named_text_color ) { 1319 // Add the color class. 1320 $colors['css_classes'][] = sprintf( 'has-%s-color', $attributes['textColor'] ); 1321 } elseif ( $has_custom_text_color ) { 1322 // Add the custom color inline style. 1323 $colors['inline_styles'] .= sprintf( 'color: %s;', $attributes['customTextColor'] ); 1324 } 1325 1326 // Background color. 1327 $has_named_background_color = array_key_exists( 'backgroundColor', $attributes ); 1328 $has_custom_background_color = array_key_exists( 'customBackgroundColor', $attributes ); 1329 1330 // If has background color. 1331 if ( $has_custom_background_color || $has_named_background_color ) { 1332 // Add has-background class. 1333 $colors['css_classes'][] = 'has-background'; 1334 } 1335 1336 if ( $has_named_background_color ) { 1337 // Add the background-color class. 1338 $colors['css_classes'][] = sprintf( 'has-%s-background-color', $attributes['backgroundColor'] ); 1339 } elseif ( $has_custom_background_color ) { 1340 // Add the custom background-color inline style. 1341 $colors['inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customBackgroundColor'] ); 1342 } 1343 1344 // Overlay text color. 1345 $has_named_overlay_text_color = array_key_exists( 'overlayTextColor', $attributes ); 1346 $has_custom_overlay_text_color = array_key_exists( 'customOverlayTextColor', $attributes ); 1347 1348 // If has overlay text color. 1349 if ( $has_custom_overlay_text_color || $has_named_overlay_text_color ) { 1350 // Add has-text-color class. 1351 $colors['overlay_css_classes'][] = 'has-text-color'; 1352 } 1353 1354 if ( $has_named_overlay_text_color ) { 1355 // Add the overlay color class. 1356 $colors['overlay_css_classes'][] = sprintf( 'has-%s-color', $attributes['overlayTextColor'] ); 1357 } elseif ( $has_custom_overlay_text_color ) { 1358 // Add the custom overlay color inline style. 1359 $colors['overlay_inline_styles'] .= sprintf( 'color: %s;', $attributes['customOverlayTextColor'] ); 1360 } 1361 1362 // Overlay background color. 1363 $has_named_overlay_background_color = array_key_exists( 'overlayBackgroundColor', $attributes ); 1364 $has_custom_overlay_background_color = array_key_exists( 'customOverlayBackgroundColor', $attributes ); 1365 1366 // If has overlay background color. 1367 if ( $has_custom_overlay_background_color || $has_named_overlay_background_color ) { 1368 // Add has-background class. 1369 $colors['overlay_css_classes'][] = 'has-background'; 1370 } 1371 1372 if ( $has_named_overlay_background_color ) { 1373 // Add the overlay background-color class. 1374 $colors['overlay_css_classes'][] = sprintf( 'has-%s-background-color', $attributes['overlayBackgroundColor'] ); 1375 } elseif ( $has_custom_overlay_background_color ) { 1376 // Add the custom overlay background-color inline style. 1377 $colors['overlay_inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customOverlayBackgroundColor'] ); 1378 } 1379 1380 return $colors; 1381 } 1382 1383 /** 1384 * Build an array with CSS classes and inline styles defining the font sizes 1385 * which will be applied to the navigation markup in the front-end. 1386 * 1387 * @since 5.9.0 1388 * 1389 * @param array $attributes Navigation block attributes. 1390 * 1391 * @return array Font size CSS classes and inline styles. 1392 */ 1393 function block_core_navigation_build_css_font_sizes( $attributes ) { 1394 // CSS classes. 1395 $font_sizes = array( 1396 'css_classes' => array(), 1397 'inline_styles' => '', 1398 ); 1399 1400 $has_named_font_size = array_key_exists( 'fontSize', $attributes ); 1401 $has_custom_font_size = array_key_exists( 'customFontSize', $attributes ); 1402 1403 if ( $has_named_font_size ) { 1404 // Add the font size class. 1405 $font_sizes['css_classes'][] = sprintf( 'has-%s-font-size', $attributes['fontSize'] ); 1406 } elseif ( $has_custom_font_size ) { 1407 // Add the custom font size inline style. 1408 $font_sizes['inline_styles'] = sprintf( 'font-size: %spx;', $attributes['customFontSize'] ); 1409 } 1410 1411 return $font_sizes; 1412 } 1413 1414 /** 1415 * Filter out empty "null" blocks from the block list. 1416 * 'parse_blocks' includes a null block with '\n\n' as the content when 1417 * it encounters whitespace. This is not a bug but rather how the parser 1418 * is designed. 1419 * 1420 * @since 5.9.0 1421 * 1422 * @param array $parsed_blocks the parsed blocks to be normalized. 1423 * @return array the normalized parsed blocks. 1424 */ 1425 function block_core_navigation_filter_out_empty_blocks( $parsed_blocks ) { 1426 $filtered = array_filter( 1427 $parsed_blocks, 1428 static function ( $block ) { 1429 return isset( $block['blockName'] ); 1430 } 1431 ); 1432 1433 // Reset keys. 1434 return array_values( $filtered ); 1435 } 1436 1437 /** 1438 * Recursively checks if blocks contain a specific block type. 1439 * 1440 * @since 7.0.0 1441 * 1442 * @param WP_Block_List $blocks The list of blocks to check. 1443 * @param string $block_type The block type to search for (e.g., 'core/navigation'). 1444 * @param array $skip_block_types Optional. Block types to skip when recursing. Default empty array. 1445 * @return bool Returns true if the specified block type is found. 1446 */ 1447 function block_core_navigation_block_tree_has_block_type( $blocks, $block_type, $skip_block_types = array() ) { 1448 if ( empty( $blocks ) ) { 1449 return false; 1450 } 1451 1452 foreach ( $blocks as $block ) { 1453 if ( $block_type === $block->name ) { 1454 return true; 1455 } 1456 1457 // Recursively check inner blocks, skipping specified block types. 1458 if ( ! in_array( $block->name, $skip_block_types, true ) && ! empty( $block->inner_blocks ) ) { 1459 if ( block_core_navigation_block_tree_has_block_type( $block->inner_blocks, $block_type, $skip_block_types ) ) { 1460 return true; 1461 } 1462 } 1463 } 1464 1465 return false; 1466 } 1467 1468 /** 1469 * Returns true if the navigation block contains a nested navigation block. 1470 * 1471 * @since 6.2.0 1472 * @deprecated 7.0.0 Use block_core_navigation_block_tree_has_block_type() instead. 1473 * 1474 * @param WP_Block_List $inner_blocks Inner block instance to be normalized. 1475 * @return bool true if the navigation block contains a nested navigation block. 1476 */ 1477 function block_core_navigation_block_contains_core_navigation( $inner_blocks ) { 1478 _deprecated_function( __FUNCTION__, '7.0.0', 'block_core_navigation_block_tree_has_block_type()' ); 1479 1480 return block_core_navigation_block_tree_has_block_type( 1481 $inner_blocks, 1482 'core/navigation' 1483 ); 1484 } 1485 1486 /** 1487 * Retrieves the appropriate fallback to be used on the front of the 1488 * site when there is no menu assigned to the Nav block. 1489 * 1490 * This aims to mirror how the fallback mechanic for wp_nav_menu works. 1491 * See https://developer.wordpress.org/reference/functions/wp_nav_menu/#more-information. 1492 * 1493 * @since 5.9.0 1494 * 1495 * @return array the array of blocks to be used as a fallback. 1496 */ 1497 function block_core_navigation_get_fallback_blocks() { 1498 $page_list_fallback = array( 1499 array( 1500 'blockName' => 'core/page-list', 1501 'innerContent' => array(), 1502 'attrs' => array(), 1503 ), 1504 ); 1505 1506 $registry = WP_Block_Type_Registry::get_instance(); 1507 1508 // If `core/page-list` is not registered then return empty blocks. 1509 $fallback_blocks = $registry->is_registered( 'core/page-list' ) ? $page_list_fallback : array(); 1510 $navigation_post = WP_Navigation_Fallback::get_fallback(); 1511 1512 // Use the first non-empty Navigation as fallback if available. 1513 if ( $navigation_post ) { 1514 $parsed_blocks = parse_blocks( $navigation_post->post_content ); 1515 $maybe_fallback = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); 1516 1517 // Normalizing blocks may result in an empty array of blocks if they were all `null` blocks. 1518 // In this case default to the (Page List) fallback. 1519 $fallback_blocks = ! empty( $maybe_fallback ) ? $maybe_fallback : $fallback_blocks; 1520 1521 // Run Block Hooks algorithm to inject hooked blocks. 1522 // We have to run it here because we need the post ID of the Navigation block to track ignored hooked blocks. 1523 // TODO: See if we can move the apply_block_hooks_to_content_from_post_object() call 1524 // before the parse_blocks() call further above, to avoid the extra serialization/parsing. 1525 $markup = serialize_blocks( $fallback_blocks ); 1526 $markup = apply_block_hooks_to_content_from_post_object( $markup, $navigation_post ); 1527 $fallback_blocks = parse_blocks( $markup ); 1528 } 1529 1530 /** 1531 * Filters the fallback experience for the Navigation block. 1532 * 1533 * Returning a falsey value will opt out of the fallback and cause the block not to render. 1534 * To customise the blocks provided return an array of blocks - these should be valid 1535 * children of the `core/navigation` block. 1536 * 1537 * @since 5.9.0 1538 * 1539 * @param array[] $fallback_blocks default fallback blocks provided by the default block mechanic. 1540 */ 1541 return apply_filters( 'block_core_navigation_render_fallback', $fallback_blocks ); 1542 } 1543 1544 /** 1545 * Iterate through all inner blocks recursively and get navigation link block's post IDs. 1546 * 1547 * @since 6.0.0 1548 * 1549 * @param WP_Block_List $inner_blocks Block list class instance. 1550 * 1551 * @return array Array of post IDs. 1552 */ 1553 function block_core_navigation_get_post_ids( $inner_blocks ) { 1554 $post_ids = array_map( 'block_core_navigation_from_block_get_post_ids', iterator_to_array( $inner_blocks ) ); 1555 return array_unique( array_merge( ...$post_ids ) ); 1556 } 1557 1558 /** 1559 * Get post IDs from a navigation link block instance. 1560 * 1561 * @since 6.0.0 1562 * 1563 * @param WP_Block $block Instance of a block. 1564 * 1565 * @return array Array of post IDs. 1566 */ 1567 function block_core_navigation_from_block_get_post_ids( $block ) { 1568 $post_ids = array(); 1569 1570 if ( $block->inner_blocks ) { 1571 $post_ids = block_core_navigation_get_post_ids( $block->inner_blocks ); 1572 } 1573 1574 if ( 'core/navigation-link' === $block->name || 'core/navigation-submenu' === $block->name ) { 1575 if ( $block->attributes && isset( $block->attributes['kind'] ) && 'post-type' === $block->attributes['kind'] && isset( $block->attributes['id'] ) ) { 1576 $post_ids[] = $block->attributes['id']; 1577 } 1578 } 1579 1580 return $post_ids; 1581 } 1582 1583 /** 1584 * Renders the `core/navigation` block on server. 1585 * 1586 * @since 5.9.0 1587 * 1588 * @param array $attributes The block attributes. 1589 * @param string $content The saved content. 1590 * @param WP_Block $block The parsed block. 1591 * 1592 * @return string Returns the navigation block markup. 1593 */ 1594 function render_block_core_navigation( $attributes, $content, $block ) { 1595 return WP_Navigation_Block_Renderer::render( $attributes, $content, $block ); 1596 } 1597 1598 /** 1599 * Register the navigation block. 1600 * 1601 * @since 5.9.0 1602 * 1603 * @uses render_block_core_navigation() 1604 * @throws WP_Error An WP_Error exception parsing the block definition. 1605 */ 1606 function register_block_core_navigation() { 1607 register_block_type_from_metadata( 1608 __DIR__ . '/navigation', 1609 array( 1610 'render_callback' => 'render_block_core_navigation', 1611 ) 1612 ); 1613 } 1614 1615 add_action( 'init', 'register_block_core_navigation' ); 1616 1617 /** 1618 * Adds Navigation block support classes to inner list containers. 1619 * 1620 * State block support adds the generated `wp-states-*` class to the outer 1621 * block wrapper. The Navigation block renders its menu items inside an inner 1622 * `wp-block-navigation__container` list, so the same state class is also needed 1623 * there for state styles to apply directly to the menu list. 1624 * 1625 * Navigation also uses layout classes on its outer wrapper to define custom 1626 * properties consumed by its inner containers. Viewport layout styles cannot 1627 * change those classes, so equivalent custom properties and a scoping class 1628 * are generated for each configured viewport layout. 1629 * 1630 * Currently this is required as a workaround because of how difficult it is for nav 1631 * child blocks to inherit styles through the complex responsive nav block html. The 1632 * bug in https://github.com/WordPress/gutenberg/issues/62690 also prevents inheritance. 1633 * 1634 * @since 7.1.0 1635 * 1636 * @param string $block_content The block content. 1637 * @param array $block The full block, including name and attributes. 1638 * @return string The updated block content. 1639 */ 1640 function block_core_navigation_add_support_classes_to_container( $block_content, $block ) { 1641 if ( 'core/navigation' !== ( $block['blockName'] ?? null ) || empty( $block_content ) ) { 1642 return $block_content; 1643 } 1644 1645 $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); 1646 $style = is_array( $attributes['style'] ?? null ) ? $attributes['style'] : array(); 1647 if ( 1648 defined( 'IS_GUTENBERG_PLUGIN' ) && 1649 IS_GUTENBERG_PLUGIN && 1650 function_exists( 'gutenberg_resolve_style_state_aliases' ) 1651 ) { 1652 $style = gutenberg_resolve_style_state_aliases( $style, 'core/navigation' ); 1653 } 1654 1655 $global_settings = wp_get_global_settings(); 1656 $viewport_settings = $global_settings['viewport'] ?? null; 1657 $responsive_media_queries = array(); 1658 if ( method_exists( 'WP_Theme_JSON_Gutenberg', 'get_viewport_media_queries' ) ) { 1659 $responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings ); 1660 } elseif ( method_exists( 'WP_Theme_JSON', 'get_viewport_media_queries' ) ) { 1661 $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings ); 1662 } 1663 1664 $styles = array(); 1665 $base_layout = is_array( $attributes['layout'] ?? null ) ? $attributes['layout'] : array(); 1666 foreach ( $responsive_media_queries as $breakpoint => $media_query ) { 1667 $viewport_style = is_array( $style[ $breakpoint ] ?? null ) ? $style[ $breakpoint ] : array(); 1668 $viewport_layout = is_array( $viewport_style['layout'] ?? null ) ? $viewport_style['layout'] : array(); 1669 if ( empty( $viewport_layout ) ) { 1670 continue; 1671 } 1672 1673 $styles[] = array( 1674 'declarations' => block_core_navigation_get_layout_custom_property_declarations( 1675 array_replace( $base_layout, $viewport_layout ) 1676 ), 1677 'rules_group' => $media_query, 1678 ); 1679 } 1680 1681 $processor = new WP_HTML_Tag_Processor( $block_content ); 1682 if ( ! $processor->next_tag() ) { 1683 return $block_content; 1684 } 1685 1686 $class_attribute = $processor->get_attribute( 'class' ); 1687 $state_class = null; 1688 if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute, $matches ) ) { 1689 $state_class = $matches[0]; 1690 } 1691 1692 $layout_class = null; 1693 if ( ! empty( $styles ) ) { 1694 $layout_class = wp_unique_id( 'wp-block-navigation-' ); 1695 // The inner selector includes both Navigation classes so it overrides the 1696 // default layout custom properties set by `.wp-block-navigation.items-*`. 1697 $selector = ".wp-block-navigation.{$layout_class},.wp-block-navigation.wp-block-navigation__container.{$layout_class}"; 1698 foreach ( $styles as &$style_rule ) { 1699 $style_rule['selector'] = $selector; 1700 } 1701 unset( $style_rule ); 1702 1703 $processor->add_class( $layout_class ); 1704 wp_style_engine_get_stylesheet_from_css_rules( 1705 $styles, 1706 array( 'context' => 'block-supports' ) 1707 ); 1708 } 1709 1710 if ( null === $state_class && null === $layout_class ) { 1711 return $block_content; 1712 } 1713 1714 while ( $processor->next_tag() ) { 1715 // Custom overlay content can include nested Navigation blocks. 1716 // Avoid applying the outer Navigation classes to an inner nav block. 1717 if ( $processor->has_class( 'wp-block-navigation' ) && ! $processor->has_class( 'wp-block-navigation__container' ) ) { 1718 break; 1719 } 1720 1721 if ( ! $processor->has_class( 'wp-block-navigation__container' ) ) { 1722 continue; 1723 } 1724 1725 if ( null !== $layout_class ) { 1726 $processor->add_class( $layout_class ); 1727 } 1728 1729 if ( null === $state_class ) { 1730 continue; 1731 } 1732 1733 $class_attribute = $processor->get_attribute( 'class' ); 1734 if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute ) ) { 1735 continue; 1736 } 1737 1738 $processor->add_class( $state_class ); 1739 } 1740 1741 return $processor->get_updated_html(); 1742 } 1743 1744 add_filter( 'render_block', 'block_core_navigation_add_support_classes_to_container', 11, 2 ); 1745 1746 /** 1747 * Filter that changes the parsed attribute values of navigation blocks contain typographic presets to contain the values directly. 1748 * 1749 * @since 5.9.0 1750 * 1751 * @param array $parsed_block The block being rendered. 1752 * 1753 * @return array The block being rendered without typographic presets. 1754 */ 1755 function block_core_navigation_typographic_presets_backcompatibility( $parsed_block ) { 1756 if ( 'core/navigation' === $parsed_block['blockName'] ) { 1757 $attribute_to_prefix_map = array( 1758 'fontStyle' => 'var:preset|font-style|', 1759 'fontWeight' => 'var:preset|font-weight|', 1760 'textDecoration' => 'var:preset|text-decoration|', 1761 'textTransform' => 'var:preset|text-transform|', 1762 ); 1763 foreach ( $attribute_to_prefix_map as $style_attribute => $prefix ) { 1764 if ( ! empty( $parsed_block['attrs']['style']['typography'][ $style_attribute ] ) ) { 1765 $prefix_len = strlen( $prefix ); 1766 $attribute_value = &$parsed_block['attrs']['style']['typography'][ $style_attribute ]; 1767 if ( 0 === strncmp( $attribute_value, $prefix, $prefix_len ) ) { 1768 $attribute_value = substr( $attribute_value, $prefix_len ); 1769 } 1770 if ( 'textDecoration' === $style_attribute && 'strikethrough' === $attribute_value ) { 1771 $attribute_value = 'line-through'; 1772 } 1773 } 1774 } 1775 } 1776 1777 return $parsed_block; 1778 } 1779 1780 add_filter( 'render_block_data', 'block_core_navigation_typographic_presets_backcompatibility' ); 1781 1782 /** 1783 * Turns menu item data into a nested array of parsed blocks 1784 * 1785 * @since 5.9.0 1786 * 1787 * @deprecated 6.3.0 Use WP_Navigation_Fallback::parse_blocks_from_menu_items() instead. 1788 * 1789 * @param array $menu_items An array of menu items that represent 1790 * an individual level of a menu. 1791 * @param array $menu_items_by_parent_id An array keyed by the id of the 1792 * parent menu where each element is an 1793 * array of menu items that belong to 1794 * that parent. 1795 * @return array An array of parsed block data. 1796 */ 1797 function block_core_navigation_parse_blocks_from_menu_items( $menu_items, $menu_items_by_parent_id ) { 1798 1799 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::parse_blocks_from_menu_items' ); 1800 1801 if ( empty( $menu_items ) ) { 1802 return array(); 1803 } 1804 1805 $blocks = array(); 1806 1807 foreach ( $menu_items as $menu_item ) { 1808 $class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null; 1809 $id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null; 1810 $opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target; 1811 $rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null; 1812 $kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom'; 1813 1814 $block = array( 1815 'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link', 1816 'attrs' => array( 1817 'className' => $class_name, 1818 'description' => $menu_item->description, 1819 'id' => $id, 1820 'kind' => $kind, 1821 'label' => $menu_item->title, 1822 'opensInNewTab' => $opens_in_new_tab, 1823 'rel' => $rel, 1824 'title' => $menu_item->attr_title, 1825 'type' => $menu_item->object, 1826 'url' => $menu_item->url, 1827 ), 1828 ); 1829 1830 $block['innerBlocks'] = isset( $menu_items_by_parent_id[ $menu_item->ID ] ) 1831 ? block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id ) 1832 : array(); 1833 $block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] ); 1834 1835 $blocks[] = $block; 1836 } 1837 1838 return $blocks; 1839 } 1840 1841 /** 1842 * Get the classic navigation menu to use as a fallback. 1843 * 1844 * @since 6.2.0 1845 * 1846 * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback() instead. 1847 * 1848 * @return object WP_Term The classic navigation. 1849 */ 1850 function block_core_navigation_get_classic_menu_fallback() { 1851 1852 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback' ); 1853 1854 $classic_nav_menus = wp_get_nav_menus(); 1855 1856 // If menus exist. 1857 if ( $classic_nav_menus && ! is_wp_error( $classic_nav_menus ) ) { 1858 // Handles simple use case where user has a classic menu and switches to a block theme. 1859 1860 // Returns the menu assigned to location `primary`. 1861 $locations = get_nav_menu_locations(); 1862 if ( isset( $locations['primary'] ) ) { 1863 $primary_menu = wp_get_nav_menu_object( $locations['primary'] ); 1864 if ( $primary_menu ) { 1865 return $primary_menu; 1866 } 1867 } 1868 1869 // Returns a menu if `primary` is its slug. 1870 foreach ( $classic_nav_menus as $classic_nav_menu ) { 1871 if ( 'primary' === $classic_nav_menu->slug ) { 1872 return $classic_nav_menu; 1873 } 1874 } 1875 1876 // Otherwise return the most recently created classic menu. 1877 usort( 1878 $classic_nav_menus, 1879 static function ( $a, $b ) { 1880 return $b->term_id - $a->term_id; 1881 } 1882 ); 1883 return $classic_nav_menus[0]; 1884 } 1885 } 1886 1887 /** 1888 * Converts a classic navigation to blocks. 1889 * 1890 * @since 6.2.0 1891 * 1892 * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback_blocks() instead. 1893 * 1894 * @param object $classic_nav_menu WP_Term The classic navigation object to convert. 1895 * @return array the normalized parsed blocks. 1896 */ 1897 function block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ) { 1898 1899 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback_blocks' ); 1900 1901 // BEGIN: Code that already exists in wp_nav_menu(). 1902 $menu_items = wp_get_nav_menu_items( $classic_nav_menu->term_id, array( 'update_post_term_cache' => false ) ); 1903 1904 // Set up the $menu_item variables. 1905 _wp_menu_item_classes_by_context( $menu_items ); 1906 1907 $sorted_menu_items = array(); 1908 foreach ( (array) $menu_items as $menu_item ) { 1909 $sorted_menu_items[ $menu_item->menu_order ] = $menu_item; 1910 } 1911 1912 unset( $menu_items, $menu_item ); 1913 1914 // END: Code that already exists in wp_nav_menu(). 1915 1916 $menu_items_by_parent_id = array(); 1917 foreach ( $sorted_menu_items as $menu_item ) { 1918 $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item; 1919 } 1920 1921 $inner_blocks = block_core_navigation_parse_blocks_from_menu_items( 1922 $menu_items_by_parent_id[0] ?? array(), 1923 $menu_items_by_parent_id 1924 ); 1925 1926 return serialize_blocks( $inner_blocks ); 1927 } 1928 1929 /** 1930 * If there's a classic menu then use it as a fallback. 1931 * 1932 * @since 6.2.0 1933 * 1934 * @deprecated 6.3.0 Use WP_Navigation_Fallback::create_classic_menu_fallback() instead. 1935 * 1936 * @return array the normalized parsed blocks. 1937 */ 1938 function block_core_navigation_maybe_use_classic_menu_fallback() { 1939 1940 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::create_classic_menu_fallback' ); 1941 1942 // See if we have a classic menu. 1943 $classic_nav_menu = block_core_navigation_get_classic_menu_fallback(); 1944 1945 if ( ! $classic_nav_menu ) { 1946 return; 1947 } 1948 1949 // If we have a classic menu then convert it to blocks. 1950 $classic_nav_menu_blocks = block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ); 1951 1952 if ( empty( $classic_nav_menu_blocks ) ) { 1953 return; 1954 } 1955 1956 // Create a new navigation menu from the classic menu. 1957 $wp_insert_post_result = wp_insert_post( 1958 array( 1959 'post_content' => $classic_nav_menu_blocks, 1960 'post_title' => $classic_nav_menu->name, 1961 'post_name' => $classic_nav_menu->slug, 1962 'post_status' => 'publish', 1963 'post_type' => 'wp_navigation', 1964 ), 1965 true // So that we can check whether the result is an error. 1966 ); 1967 1968 if ( is_wp_error( $wp_insert_post_result ) ) { 1969 return; 1970 } 1971 1972 // Fetch the most recently published navigation which will be the classic one created above. 1973 return block_core_navigation_get_most_recently_published_navigation(); 1974 } 1975 1976 /** 1977 * Finds the most recently published `wp_navigation` Post. 1978 * 1979 * @since 6.1.0 1980 * 1981 * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_most_recently_published_navigation() instead. 1982 * 1983 * @return WP_Post|null the first non-empty Navigation or null. 1984 */ 1985 function block_core_navigation_get_most_recently_published_navigation() { 1986 1987 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_most_recently_published_navigation' ); 1988 1989 // Default to the most recently created menu. 1990 $parsed_args = array( 1991 'post_type' => 'wp_navigation', 1992 'no_found_rows' => true, 1993 'update_post_meta_cache' => false, 1994 'update_post_term_cache' => false, 1995 'order' => 'DESC', 1996 'orderby' => 'date', 1997 'post_status' => 'publish', 1998 'posts_per_page' => 1, // get only the most recent. 1999 ); 2000 2001 $navigation_post = new WP_Query( $parsed_args ); 2002 if ( count( $navigation_post->posts ) > 0 ) { 2003 return $navigation_post->posts[0]; 2004 } 2005 2006 return null; 2007 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Thu Sep 17 08:20:31 2026 | Cross-referenced by PHPXref |