| [ 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 // The default overlay displays every submenu it contains; a custom overlay 837 // opts out through the `disable-default-overlay` class on this container. 838 // Submenus inherit this flag to tell the two apart, derived from the same 839 // value as that class so the markup and the styles cannot disagree. 840 if ( $has_custom_overlay ) { 841 $responsive_container_directives .= ' ' . wp_interactivity_data_wp_context( array( 'hasCustomOverlay' => true ) ); 842 } 843 } 844 845 // Don't apply overlay inline styles if using a custom overlay template part. 846 // The custom overlay is responsible for its own styling. 847 $overlay_inline_styles = static::get_overlay_inline_styles( $has_custom_overlay, $colors ); 848 849 if ( $has_custom_overlay ) { 850 $custom_overlay_markup = sprintf( 851 '<div class="wp-block-navigation__overlay-container">%s</div>', 852 $overlay_blocks_html 853 ); 854 } 855 856 // Show default close button for all responsive navigation, 857 // unless custom overlay has its own close block. 858 if ( ! $has_custom_overlay_close_block ) { 859 $close_button_markup = sprintf( 860 '<button %1$s class="wp-block-navigation__responsive-container-close" %2$s>%3$s</button>', 861 $toggle_aria_label_close, 862 $close_button_directives, 863 $toggle_close_button_content 864 ); 865 } 866 867 return sprintf( 868 '<button aria-haspopup="dialog" %3$s class="%6$s" %10$s>%8$s</button> 869 <div class="%5$s" %7$s id="%1$s" %11$s> 870 <div class="wp-block-navigation__responsive-close" tabindex="-1"> 871 <div class="wp-block-navigation__responsive-dialog" %12$s> 872 %13$s 873 <div class="wp-block-navigation__responsive-container-content" %14$s id="%1$s-content"> 874 %2$s 875 %15$s 876 </div> 877 </div> 878 </div> 879 </div>', 880 esc_attr( $modal_unique_id ), 881 $inner_blocks_html, 882 $toggle_aria_label_open, 883 $toggle_aria_label_close, 884 esc_attr( trim( implode( ' ', $responsive_container_classes ) ) ), 885 esc_attr( trim( implode( ' ', $open_button_classes ) ) ), 886 $overlay_inline_styles, 887 $toggle_button_content, 888 $toggle_close_button_content, 889 $open_button_directives, 890 $responsive_container_directives, 891 $responsive_dialog_directives, 892 $close_button_markup, 893 $responsive_container_content_directives, 894 $has_custom_overlay ? $custom_overlay_markup : '' 895 ); 896 } 897 898 /** 899 * Get the wrapper attributes 900 * 901 * @since 6.5.0 902 * 903 * @param array $attributes The block attributes. 904 * @param WP_Block_List $inner_blocks A list of inner blocks. 905 * @return string Returns the navigation block markup. 906 */ 907 private static function get_nav_attributes( $attributes, $inner_blocks ) { 908 $is_interactive = static::is_interactive( $attributes, $inner_blocks ); 909 $is_responsive_menu = static::is_responsive( $attributes ); 910 $style = static::get_styles( $attributes ); 911 $class = static::get_classes( $attributes ); 912 $extra_attributes = array( 913 'class' => $class, 914 'style' => $style, 915 ); 916 // Only add aria-label for top-level navigation blocks. 917 // Skip navigation blocks marked as being within overlay template parts. 918 $is_within_overlay = $attributes['_isWithinOverlayTemplatePart'] ?? false; 919 if ( $is_within_overlay ) { 920 $nav_menu_name = static::get_navigation_name( $attributes ); 921 } else { 922 $nav_menu_name = static::get_unique_navigation_name( $attributes ); 923 } 924 925 if ( ! empty( $nav_menu_name ) ) { 926 $extra_attributes['aria-label'] = $nav_menu_name; 927 } 928 $wrapper_attributes = get_block_wrapper_attributes( $extra_attributes ); 929 930 if ( $is_responsive_menu ) { 931 $nav_element_directives = static::get_nav_element_directives( $is_interactive ); 932 $wrapper_attributes .= ' ' . $nav_element_directives; 933 } 934 935 return $wrapper_attributes; 936 } 937 938 /** 939 * Gets the nav element directives. 940 * 941 * @since 6.5.0 942 * 943 * @param bool $is_interactive Whether the block is interactive. 944 * @return string the directives for the navigation element. 945 */ 946 private static function get_nav_element_directives( $is_interactive ) { 947 if ( ! $is_interactive ) { 948 return ''; 949 } 950 // When adding to this array be mindful of security concerns. 951 $nav_element_context = wp_interactivity_data_wp_context( 952 array( 953 'overlayOpenedBy' => array( 954 'click' => false, 955 'hover' => false, 956 'focus' => false, 957 ), 958 'type' => 'overlay', 959 'roleAttribute' => '', 960 'ariaLabel' => __( 'Menu' ), 961 ) 962 ); 963 $nav_element_directives = ' 964 data-wp-interactive="core/navigation" ' 965 . $nav_element_context; 966 967 return $nav_element_directives; 968 } 969 970 /** 971 * Handle view script module loading. 972 * 973 * @since 6.5.0 974 * 975 * @param array $attributes The block attributes. 976 * @param WP_Block $block The parsed block. 977 * @param WP_Block_List $inner_blocks The list of inner blocks. 978 */ 979 private static function handle_view_script_module_loading( $attributes, $block, $inner_blocks ) { 980 if ( static::is_interactive( $attributes, $inner_blocks ) ) { 981 wp_enqueue_script_module( '@wordpress/block-library/navigation/view' ); 982 } 983 } 984 985 /** 986 * Returns the markup for the navigation block. 987 * 988 * @since 6.5.0 989 * 990 * @param array $attributes The block attributes. 991 * @param WP_Block_List $inner_blocks The list of inner blocks. 992 * @return string Returns the navigation wrapper markup. 993 */ 994 private static function get_inner_block_markup( $attributes, $inner_blocks ) { 995 $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); 996 if ( static::is_responsive( $attributes ) ) { 997 return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); 998 } 999 return $inner_blocks_html; 1000 } 1001 1002 /** 1003 * Returns a unique name for the navigation. 1004 * 1005 * @since 6.5.0 1006 * 1007 * @param array $attributes The block attributes. 1008 * @return string Returns a unique name for the navigation. 1009 */ 1010 private static function get_unique_navigation_name( $attributes ) { 1011 $nav_menu_name = static::get_navigation_name( $attributes ); 1012 1013 // This is used to count the number of times a navigation name has been seen, 1014 // so that we can ensure every navigation has a unique id. 1015 if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) ) { 1016 ++static::$seen_menu_names[ $nav_menu_name ]; 1017 } else { 1018 static::$seen_menu_names[ $nav_menu_name ] = 1; 1019 } 1020 1021 // If the menu name has been used previously then append an ID 1022 // to the name to ensure uniqueness across a given post. 1023 if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) && static::$seen_menu_names[ $nav_menu_name ] > 1 ) { 1024 $count = static::$seen_menu_names[ $nav_menu_name ]; 1025 $nav_menu_name = $nav_menu_name . ' ' . ( $count ); 1026 } 1027 1028 return $nav_menu_name; 1029 } 1030 1031 /** 1032 * Renders the navigation block. 1033 * 1034 * @since 6.5.0 1035 * 1036 * @param array $attributes The block attributes. 1037 * @param string $content The saved content. 1038 * @param WP_Block $block The parsed block. 1039 * @return string Returns the navigation block markup. 1040 */ 1041 public static function render( $attributes, $content, $block ) { 1042 /** 1043 * Deprecated: 1044 * The rgbTextColor and rgbBackgroundColor attributes 1045 * have been deprecated in favor of 1046 * customTextColor and customBackgroundColor ones. 1047 * Move the values from old attrs to the new ones. 1048 */ 1049 if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { 1050 $attributes['customTextColor'] = $attributes['rgbTextColor']; 1051 } 1052 1053 if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { 1054 $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; 1055 } 1056 1057 unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); 1058 1059 // Submenu detection is memoized while a navigation block renders. Reset it so 1060 // the result of a previously rendered navigation block is not reused for this one. 1061 static::$has_submenus = false; 1062 1063 $inner_blocks = static::get_inner_blocks( $attributes, $block ); 1064 // Prevent navigation blocks referencing themselves from rendering. 1065 if ( block_core_navigation_block_tree_has_block_type( 1066 $inner_blocks, 1067 'core/navigation' 1068 ) ) { 1069 return ''; 1070 } 1071 1072 static::handle_view_script_module_loading( $attributes, $block, $inner_blocks ); 1073 1074 // Use div wrapper if this navigation block is within an overlay template part. 1075 $is_within_overlay = $attributes['_isWithinOverlayTemplatePart'] ?? false; 1076 $tag_name = $is_within_overlay ? 'div' : 'nav'; 1077 1078 return sprintf( 1079 '<%1$s %2$s>%3$s</%1$s>', 1080 $tag_name, 1081 static::get_nav_attributes( $attributes, $inner_blocks ), 1082 static::get_inner_block_markup( $attributes, $inner_blocks ) 1083 ); 1084 } 1085 } 1086 1087 // These functions are used for the __unstableLocation feature and only active 1088 // when the gutenberg plugin is active. 1089 if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { 1090 /** 1091 * Returns the menu items for a WordPress menu location. 1092 * 1093 * @since 5.9.0 1094 * 1095 * @param string $location The menu location. 1096 * @return array Menu items for the location. 1097 */ 1098 function block_core_navigation_get_menu_items_at_location( $location ) { 1099 if ( empty( $location ) ) { 1100 return; 1101 } 1102 1103 // Build menu data. The following approximates the code in 1104 // `wp_nav_menu()` and `gutenberg_output_block_nav_menu`. 1105 1106 // Find the location in the list of locations, returning early if the 1107 // location can't be found. 1108 $locations = get_nav_menu_locations(); 1109 if ( ! isset( $locations[ $location ] ) ) { 1110 return; 1111 } 1112 1113 // Get the menu from the location, returning early if there is no 1114 // menu or there was an error. 1115 $menu = wp_get_nav_menu_object( $locations[ $location ] ); 1116 if ( ! $menu || is_wp_error( $menu ) ) { 1117 return; 1118 } 1119 1120 $menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) ); 1121 _wp_menu_item_classes_by_context( $menu_items ); 1122 1123 return $menu_items; 1124 } 1125 1126 1127 /** 1128 * Sorts a standard array of menu items into a nested structure keyed by the 1129 * id of the parent menu. 1130 * 1131 * @since 5.9.0 1132 * 1133 * @param array $menu_items Menu items to sort. 1134 * @return array An array keyed by the id of the parent menu where each element 1135 * is an array of menu items that belong to that parent. 1136 */ 1137 function block_core_navigation_sort_menu_items_by_parent_id( $menu_items ) { 1138 $sorted_menu_items = array(); 1139 foreach ( (array) $menu_items as $menu_item ) { 1140 $sorted_menu_items[ $menu_item->menu_order ] = $menu_item; 1141 } 1142 unset( $menu_items, $menu_item ); 1143 1144 $menu_items_by_parent_id = array(); 1145 foreach ( $sorted_menu_items as $menu_item ) { 1146 $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item; 1147 } 1148 1149 return $menu_items_by_parent_id; 1150 } 1151 1152 /** 1153 * Gets the inner blocks for the navigation block from the unstable location attribute. 1154 * 1155 * @since 6.5.0 1156 * 1157 * @param array $attributes The block attributes. 1158 * @return WP_Block_List Returns the inner blocks for the navigation block. 1159 */ 1160 function block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ) { 1161 $menu_items = block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ); 1162 if ( empty( $menu_items ) ) { 1163 return new WP_Block_List( array(), $attributes ); 1164 } 1165 1166 $menu_items_by_parent_id = block_core_navigation_sort_menu_items_by_parent_id( $menu_items ); 1167 $parsed_blocks = block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id ); 1168 return new WP_Block_List( $parsed_blocks, $attributes ); 1169 } 1170 } 1171 1172 /** 1173 * Checks if the overlay HTML contains a navigation-overlay-close block. 1174 * 1175 * Uses WP_HTML_Tag_Processor to detect the close button in rendered output, 1176 * so it works when the overlay uses patterns (pattern content is rendered at 1177 * output time, not in the block tree). 1178 * 1179 * @since 7.0.0 1180 * 1181 * @param string $html The rendered overlay HTML. 1182 * @return bool True if a close button element is found. 1183 */ 1184 function block_core_navigation_overlay_html_has_close_block( $html ) { 1185 $tags = new WP_HTML_Tag_Processor( $html ); 1186 return $tags->next_tag( 1187 array( 1188 'tag_name' => 'BUTTON', 1189 'class_name' => 'wp-block-navigation-overlay-close', 1190 ) 1191 ); 1192 } 1193 1194 /** 1195 * Add Interactivity API directives to the navigation-overlay-close block 1196 * markup using the Tag Processor. 1197 * 1198 * @since 6.5.0 1199 * 1200 * @param WP_HTML_Tag_Processor $tags Markup of the navigation block. 1201 * @return string Overlay close markup with the directives injected. 1202 */ 1203 function block_core_navigation_add_directives_to_overlay_close( $tags ) { 1204 // Find all navigation-overlay-close buttons. 1205 while ( $tags->next_tag( 1206 array( 1207 'tag_name' => 'BUTTON', 1208 'class_name' => 'wp-block-navigation-overlay-close', 1209 ) 1210 ) ) { 1211 // Add the same close directive as the default close button. 1212 $tags->set_attribute( 'data-wp-on--click', 'actions.closeMenuOnClick' ); 1213 } 1214 return $tags->get_updated_html(); 1215 } 1216 1217 /** 1218 * Sets fetchpriority="low" on all IMG tags within the navigation overlay. 1219 * 1220 * Images in the overlay are hidden until the menu is opened, so they should 1221 * not compete with any actual LCP element image on the page. 1222 * 1223 * @since 7.0.0 1224 * 1225 * @param string $overlay_blocks_html The rendered HTML of the overlay blocks. 1226 * @return string Modified HTML with fetchpriority="low" on all IMG tags. 1227 */ 1228 function block_core_navigation_set_overlay_image_fetch_priority( string $overlay_blocks_html ): string { 1229 $tags = new WP_HTML_Tag_Processor( $overlay_blocks_html ); 1230 while ( $tags->next_tag( 'IMG' ) ) { 1231 $tags->set_attribute( 'fetchpriority', 'low' ); 1232 } 1233 return $tags->get_updated_html(); 1234 } 1235 1236 /** 1237 * Add Interactivity API directives to the navigation-submenu and page-list 1238 * blocks markup using the Tag Processor. 1239 * 1240 * @since 6.3.0 1241 * 1242 * @param WP_HTML_Tag_Processor $tags Markup of the navigation block. 1243 * @param array $block_attributes Block attributes. 1244 * 1245 * @return string Submenu markup with the directives injected. 1246 */ 1247 function block_core_navigation_add_directives_to_submenu( $tags, $block_attributes ) { 1248 while ( $tags->next_tag( 1249 array( 1250 'tag_name' => 'LI', 1251 'class_name' => 'has-child', 1252 ) 1253 ) ) { 1254 // Add directives to the parent `<li>`. 1255 $tags->set_attribute( 'data-wp-interactive', 'core/navigation' ); 1256 $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "type": "submenu", "modal": null, "previousFocus": null }' ); 1257 $tags->set_attribute( 'data-wp-watch', 'callbacks.initMenu' ); 1258 $tags->set_attribute( 'data-wp-on--focusout', 'actions.handleMenuFocusout' ); 1259 $tags->set_attribute( 'data-wp-on--keydown', 'actions.handleMenuKeydown' ); 1260 1261 // This is a fix for Safari. Without it, Safari doesn't change the active 1262 // element when the user clicks on a button. It can be removed once we add 1263 // an overlay to capture the clicks, instead of relying on the focusout 1264 // event. 1265 $tags->set_attribute( 'tabindex', '-1' ); 1266 1267 $computed_visibility = block_core_navigation_get_submenu_visibility( $block_attributes ); 1268 $open_on_hover = 'hover' === $computed_visibility; 1269 1270 if ( $open_on_hover ) { 1271 $tags->set_attribute( 'data-wp-on--pointerenter', 'actions.openMenuOnHover' ); 1272 $tags->set_attribute( 'data-wp-on--pointerleave', 'actions.closeMenuOnHover' ); 1273 } 1274 1275 // Add directives to the toggle submenu button. 1276 if ( $tags->next_tag( 1277 array( 1278 'tag_name' => 'BUTTON', 1279 'class_name' => 'wp-block-navigation-submenu__toggle', 1280 ) 1281 ) ) { 1282 $tags->set_attribute( 'data-wp-on--click', 'actions.toggleMenuOnClick' ); 1283 $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isSubmenuOpen' ); 1284 // The `aria-expanded` attribute for SSR is already added in the submenu block. 1285 } 1286 // Add directives to the submenu. 1287 if ( $tags->next_tag( 1288 array( 1289 'tag_name' => 'UL', 1290 'class_name' => 'wp-block-navigation__submenu-container', 1291 ) 1292 ) ) { 1293 $tags->set_attribute( 'data-wp-on--focus', 'actions.openMenuOnFocus' ); 1294 } 1295 1296 // Iterate through subitems if exist. 1297 block_core_navigation_add_directives_to_submenu( $tags, $block_attributes ); 1298 } 1299 return $tags->get_updated_html(); 1300 } 1301 1302 /** 1303 * Build an array with CSS classes and inline styles defining the colors 1304 * which will be applied to the navigation markup in the front-end. 1305 * 1306 * @since 5.9.0 1307 * 1308 * @param array $attributes Navigation block attributes. 1309 * 1310 * @return array Colors CSS classes and inline styles. 1311 */ 1312 function block_core_navigation_build_css_colors( $attributes ) { 1313 $colors = array( 1314 'css_classes' => array(), 1315 'inline_styles' => '', 1316 'overlay_css_classes' => array(), 1317 'overlay_inline_styles' => '', 1318 ); 1319 1320 // Text color. 1321 $has_named_text_color = array_key_exists( 'textColor', $attributes ); 1322 $has_custom_text_color = array_key_exists( 'customTextColor', $attributes ); 1323 1324 // If has text color. 1325 if ( $has_custom_text_color || $has_named_text_color ) { 1326 // Add has-text-color class. 1327 $colors['css_classes'][] = 'has-text-color'; 1328 } 1329 1330 if ( $has_named_text_color ) { 1331 // Add the color class. 1332 $colors['css_classes'][] = sprintf( 'has-%s-color', $attributes['textColor'] ); 1333 } elseif ( $has_custom_text_color ) { 1334 // Add the custom color inline style. 1335 $colors['inline_styles'] .= sprintf( 'color: %s;', $attributes['customTextColor'] ); 1336 } 1337 1338 // Background color. 1339 $has_named_background_color = array_key_exists( 'backgroundColor', $attributes ); 1340 $has_custom_background_color = array_key_exists( 'customBackgroundColor', $attributes ); 1341 1342 // If has background color. 1343 if ( $has_custom_background_color || $has_named_background_color ) { 1344 // Add has-background class. 1345 $colors['css_classes'][] = 'has-background'; 1346 } 1347 1348 if ( $has_named_background_color ) { 1349 // Add the background-color class. 1350 $colors['css_classes'][] = sprintf( 'has-%s-background-color', $attributes['backgroundColor'] ); 1351 } elseif ( $has_custom_background_color ) { 1352 // Add the custom background-color inline style. 1353 $colors['inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customBackgroundColor'] ); 1354 } 1355 1356 // Overlay text color. 1357 $has_named_overlay_text_color = array_key_exists( 'overlayTextColor', $attributes ); 1358 $has_custom_overlay_text_color = array_key_exists( 'customOverlayTextColor', $attributes ); 1359 1360 // If has overlay text color. 1361 if ( $has_custom_overlay_text_color || $has_named_overlay_text_color ) { 1362 // Add has-text-color class. 1363 $colors['overlay_css_classes'][] = 'has-text-color'; 1364 } 1365 1366 if ( $has_named_overlay_text_color ) { 1367 // Add the overlay color class. 1368 $colors['overlay_css_classes'][] = sprintf( 'has-%s-color', $attributes['overlayTextColor'] ); 1369 } elseif ( $has_custom_overlay_text_color ) { 1370 // Add the custom overlay color inline style. 1371 $colors['overlay_inline_styles'] .= sprintf( 'color: %s;', $attributes['customOverlayTextColor'] ); 1372 } 1373 1374 // Overlay background color. 1375 $has_named_overlay_background_color = array_key_exists( 'overlayBackgroundColor', $attributes ); 1376 $has_custom_overlay_background_color = array_key_exists( 'customOverlayBackgroundColor', $attributes ); 1377 1378 // If has overlay background color. 1379 if ( $has_custom_overlay_background_color || $has_named_overlay_background_color ) { 1380 // Add has-background class. 1381 $colors['overlay_css_classes'][] = 'has-background'; 1382 } 1383 1384 if ( $has_named_overlay_background_color ) { 1385 // Add the overlay background-color class. 1386 $colors['overlay_css_classes'][] = sprintf( 'has-%s-background-color', $attributes['overlayBackgroundColor'] ); 1387 } elseif ( $has_custom_overlay_background_color ) { 1388 // Add the custom overlay background-color inline style. 1389 $colors['overlay_inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customOverlayBackgroundColor'] ); 1390 } 1391 1392 return $colors; 1393 } 1394 1395 /** 1396 * Build an array with CSS classes and inline styles defining the font sizes 1397 * which will be applied to the navigation markup in the front-end. 1398 * 1399 * @since 5.9.0 1400 * 1401 * @param array $attributes Navigation block attributes. 1402 * 1403 * @return array Font size CSS classes and inline styles. 1404 */ 1405 function block_core_navigation_build_css_font_sizes( $attributes ) { 1406 // CSS classes. 1407 $font_sizes = array( 1408 'css_classes' => array(), 1409 'inline_styles' => '', 1410 ); 1411 1412 $has_named_font_size = array_key_exists( 'fontSize', $attributes ); 1413 $has_custom_font_size = array_key_exists( 'customFontSize', $attributes ); 1414 1415 if ( $has_named_font_size ) { 1416 // Add the font size class. 1417 $font_sizes['css_classes'][] = sprintf( 'has-%s-font-size', $attributes['fontSize'] ); 1418 } elseif ( $has_custom_font_size ) { 1419 // Add the custom font size inline style. 1420 $font_sizes['inline_styles'] = sprintf( 'font-size: %spx;', $attributes['customFontSize'] ); 1421 } 1422 1423 return $font_sizes; 1424 } 1425 1426 /** 1427 * Filter out empty "null" blocks from the block list. 1428 * 'parse_blocks' includes a null block with '\n\n' as the content when 1429 * it encounters whitespace. This is not a bug but rather how the parser 1430 * is designed. 1431 * 1432 * @since 5.9.0 1433 * 1434 * @param array $parsed_blocks the parsed blocks to be normalized. 1435 * @return array the normalized parsed blocks. 1436 */ 1437 function block_core_navigation_filter_out_empty_blocks( $parsed_blocks ) { 1438 $filtered = array_filter( 1439 $parsed_blocks, 1440 static function ( $block ) { 1441 return isset( $block['blockName'] ); 1442 } 1443 ); 1444 1445 // Reset keys. 1446 return array_values( $filtered ); 1447 } 1448 1449 /** 1450 * Recursively checks if blocks contain a specific block type. 1451 * 1452 * @since 7.0.0 1453 * 1454 * @param WP_Block_List $blocks The list of blocks to check. 1455 * @param string $block_type The block type to search for (e.g., 'core/navigation'). 1456 * @param array $skip_block_types Optional. Block types to skip when recursing. Default empty array. 1457 * @return bool Returns true if the specified block type is found. 1458 */ 1459 function block_core_navigation_block_tree_has_block_type( $blocks, $block_type, $skip_block_types = array() ) { 1460 if ( empty( $blocks ) ) { 1461 return false; 1462 } 1463 1464 foreach ( $blocks as $block ) { 1465 if ( $block_type === $block->name ) { 1466 return true; 1467 } 1468 1469 // Recursively check inner blocks, skipping specified block types. 1470 if ( ! in_array( $block->name, $skip_block_types, true ) && ! empty( $block->inner_blocks ) ) { 1471 if ( block_core_navigation_block_tree_has_block_type( $block->inner_blocks, $block_type, $skip_block_types ) ) { 1472 return true; 1473 } 1474 } 1475 } 1476 1477 return false; 1478 } 1479 1480 /** 1481 * Returns true if the navigation block contains a nested navigation block. 1482 * 1483 * @since 6.2.0 1484 * @deprecated 7.0.0 Use block_core_navigation_block_tree_has_block_type() instead. 1485 * 1486 * @param WP_Block_List $inner_blocks Inner block instance to be normalized. 1487 * @return bool true if the navigation block contains a nested navigation block. 1488 */ 1489 function block_core_navigation_block_contains_core_navigation( $inner_blocks ) { 1490 _deprecated_function( __FUNCTION__, '7.0.0', 'block_core_navigation_block_tree_has_block_type()' ); 1491 1492 return block_core_navigation_block_tree_has_block_type( 1493 $inner_blocks, 1494 'core/navigation' 1495 ); 1496 } 1497 1498 /** 1499 * Retrieves the appropriate fallback to be used on the front of the 1500 * site when there is no menu assigned to the Nav block. 1501 * 1502 * This aims to mirror how the fallback mechanic for wp_nav_menu works. 1503 * See https://developer.wordpress.org/reference/functions/wp_nav_menu/#more-information. 1504 * 1505 * @since 5.9.0 1506 * 1507 * @return array the array of blocks to be used as a fallback. 1508 */ 1509 function block_core_navigation_get_fallback_blocks() { 1510 $page_list_fallback = array( 1511 array( 1512 'blockName' => 'core/page-list', 1513 'innerContent' => array(), 1514 'attrs' => array(), 1515 ), 1516 ); 1517 1518 $registry = WP_Block_Type_Registry::get_instance(); 1519 1520 // If `core/page-list` is not registered then return empty blocks. 1521 $fallback_blocks = $registry->is_registered( 'core/page-list' ) ? $page_list_fallback : array(); 1522 $navigation_post = WP_Navigation_Fallback::get_fallback(); 1523 1524 // Use the first non-empty Navigation as fallback if available. 1525 if ( $navigation_post ) { 1526 $parsed_blocks = parse_blocks( $navigation_post->post_content ); 1527 $maybe_fallback = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); 1528 1529 // Normalizing blocks may result in an empty array of blocks if they were all `null` blocks. 1530 // In this case default to the (Page List) fallback. 1531 $fallback_blocks = ! empty( $maybe_fallback ) ? $maybe_fallback : $fallback_blocks; 1532 1533 // Run Block Hooks algorithm to inject hooked blocks. 1534 // We have to run it here because we need the post ID of the Navigation block to track ignored hooked blocks. 1535 // TODO: See if we can move the apply_block_hooks_to_content_from_post_object() call 1536 // before the parse_blocks() call further above, to avoid the extra serialization/parsing. 1537 $markup = serialize_blocks( $fallback_blocks ); 1538 $markup = apply_block_hooks_to_content_from_post_object( $markup, $navigation_post ); 1539 $fallback_blocks = parse_blocks( $markup ); 1540 } 1541 1542 /** 1543 * Filters the fallback experience for the Navigation block. 1544 * 1545 * Returning a falsey value will opt out of the fallback and cause the block not to render. 1546 * To customise the blocks provided return an array of blocks - these should be valid 1547 * children of the `core/navigation` block. 1548 * 1549 * @since 5.9.0 1550 * 1551 * @param array[] $fallback_blocks default fallback blocks provided by the default block mechanic. 1552 */ 1553 return apply_filters( 'block_core_navigation_render_fallback', $fallback_blocks ); 1554 } 1555 1556 /** 1557 * Iterate through all inner blocks recursively and get navigation link block's post IDs. 1558 * 1559 * @since 6.0.0 1560 * 1561 * @param WP_Block_List $inner_blocks Block list class instance. 1562 * 1563 * @return array Array of post IDs. 1564 */ 1565 function block_core_navigation_get_post_ids( $inner_blocks ) { 1566 $post_ids = array_map( 'block_core_navigation_from_block_get_post_ids', iterator_to_array( $inner_blocks ) ); 1567 return array_unique( array_merge( ...$post_ids ) ); 1568 } 1569 1570 /** 1571 * Get post IDs from a navigation link block instance. 1572 * 1573 * @since 6.0.0 1574 * 1575 * @param WP_Block $block Instance of a block. 1576 * 1577 * @return array Array of post IDs. 1578 */ 1579 function block_core_navigation_from_block_get_post_ids( $block ) { 1580 $post_ids = array(); 1581 1582 if ( $block->inner_blocks ) { 1583 $post_ids = block_core_navigation_get_post_ids( $block->inner_blocks ); 1584 } 1585 1586 if ( 'core/navigation-link' === $block->name || 'core/navigation-submenu' === $block->name ) { 1587 if ( $block->attributes && isset( $block->attributes['kind'] ) && 'post-type' === $block->attributes['kind'] && isset( $block->attributes['id'] ) ) { 1588 $post_ids[] = $block->attributes['id']; 1589 } 1590 } 1591 1592 return $post_ids; 1593 } 1594 1595 /** 1596 * Renders the `core/navigation` block on server. 1597 * 1598 * @since 5.9.0 1599 * 1600 * @param array $attributes The block attributes. 1601 * @param string $content The saved content. 1602 * @param WP_Block $block The parsed block. 1603 * 1604 * @return string Returns the navigation block markup. 1605 */ 1606 function render_block_core_navigation( $attributes, $content, $block ) { 1607 return WP_Navigation_Block_Renderer::render( $attributes, $content, $block ); 1608 } 1609 1610 /** 1611 * Register the navigation block. 1612 * 1613 * @since 5.9.0 1614 * 1615 * @uses render_block_core_navigation() 1616 * @throws WP_Error An WP_Error exception parsing the block definition. 1617 */ 1618 function register_block_core_navigation() { 1619 register_block_type_from_metadata( 1620 __DIR__ . '/navigation', 1621 array( 1622 'render_callback' => 'render_block_core_navigation', 1623 ) 1624 ); 1625 } 1626 1627 add_action( 'init', 'register_block_core_navigation' ); 1628 1629 /** 1630 * Adds Navigation block support classes to inner list containers. 1631 * 1632 * State block support adds the generated `wp-states-*` class to the outer 1633 * block wrapper. The Navigation block renders its menu items inside an inner 1634 * `wp-block-navigation__container` list, so the same state class is also needed 1635 * there for state styles to apply directly to the menu list. 1636 * 1637 * Navigation also uses layout classes on its outer wrapper to define custom 1638 * properties consumed by its inner containers. Viewport layout styles cannot 1639 * change those classes, so equivalent custom properties and a scoping class 1640 * are generated for each configured viewport layout. 1641 * 1642 * Currently this is required as a workaround because of how difficult it is for nav 1643 * child blocks to inherit styles through the complex responsive nav block html. The 1644 * bug in https://github.com/WordPress/gutenberg/issues/62690 also prevents inheritance. 1645 * 1646 * @since 7.1.0 1647 * 1648 * @param string $block_content The block content. 1649 * @param array $block The full block, including name and attributes. 1650 * @return string The updated block content. 1651 */ 1652 function block_core_navigation_add_support_classes_to_container( $block_content, $block ) { 1653 if ( 'core/navigation' !== ( $block['blockName'] ?? null ) || empty( $block_content ) ) { 1654 return $block_content; 1655 } 1656 1657 $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); 1658 $style = is_array( $attributes['style'] ?? null ) ? $attributes['style'] : array(); 1659 if ( 1660 defined( 'IS_GUTENBERG_PLUGIN' ) && 1661 IS_GUTENBERG_PLUGIN && 1662 function_exists( 'gutenberg_resolve_style_state_aliases' ) 1663 ) { 1664 $style = gutenberg_resolve_style_state_aliases( $style, 'core/navigation' ); 1665 } 1666 1667 $global_settings = wp_get_global_settings(); 1668 $viewport_settings = $global_settings['viewport'] ?? null; 1669 $responsive_media_queries = array(); 1670 if ( method_exists( 'WP_Theme_JSON_Gutenberg', 'get_viewport_media_queries' ) ) { 1671 $responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings ); 1672 } elseif ( method_exists( 'WP_Theme_JSON', 'get_viewport_media_queries' ) ) { 1673 $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings ); 1674 } 1675 1676 $styles = array(); 1677 $base_layout = is_array( $attributes['layout'] ?? null ) ? $attributes['layout'] : array(); 1678 foreach ( $responsive_media_queries as $breakpoint => $media_query ) { 1679 $viewport_style = is_array( $style[ $breakpoint ] ?? null ) ? $style[ $breakpoint ] : array(); 1680 $viewport_layout = is_array( $viewport_style['layout'] ?? null ) ? $viewport_style['layout'] : array(); 1681 if ( empty( $viewport_layout ) ) { 1682 continue; 1683 } 1684 1685 $styles[] = array( 1686 'declarations' => block_core_navigation_get_layout_custom_property_declarations( 1687 array_replace( $base_layout, $viewport_layout ) 1688 ), 1689 'rules_group' => $media_query, 1690 ); 1691 } 1692 1693 $processor = new WP_HTML_Tag_Processor( $block_content ); 1694 if ( ! $processor->next_tag() ) { 1695 return $block_content; 1696 } 1697 1698 $class_attribute = $processor->get_attribute( 'class' ); 1699 $state_class = null; 1700 if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute, $matches ) ) { 1701 $state_class = $matches[0]; 1702 } 1703 1704 $layout_class = null; 1705 if ( ! empty( $styles ) ) { 1706 $layout_class = wp_unique_id( 'wp-block-navigation-' ); 1707 // The inner selector includes both Navigation classes so it overrides the 1708 // default layout custom properties set by `.wp-block-navigation.items-*`. 1709 $selector = ".wp-block-navigation.{$layout_class},.wp-block-navigation.wp-block-navigation__container.{$layout_class}"; 1710 foreach ( $styles as &$style_rule ) { 1711 $style_rule['selector'] = $selector; 1712 } 1713 unset( $style_rule ); 1714 1715 $processor->add_class( $layout_class ); 1716 wp_style_engine_get_stylesheet_from_css_rules( 1717 $styles, 1718 array( 'context' => 'block-supports' ) 1719 ); 1720 } 1721 1722 if ( null === $state_class && null === $layout_class ) { 1723 return $block_content; 1724 } 1725 1726 while ( $processor->next_tag() ) { 1727 // Custom overlay content can include nested Navigation blocks. 1728 // Avoid applying the outer Navigation classes to an inner nav block. 1729 if ( $processor->has_class( 'wp-block-navigation' ) && ! $processor->has_class( 'wp-block-navigation__container' ) ) { 1730 break; 1731 } 1732 1733 if ( ! $processor->has_class( 'wp-block-navigation__container' ) ) { 1734 continue; 1735 } 1736 1737 if ( null !== $layout_class ) { 1738 $processor->add_class( $layout_class ); 1739 } 1740 1741 if ( null === $state_class ) { 1742 continue; 1743 } 1744 1745 $class_attribute = $processor->get_attribute( 'class' ); 1746 if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute ) ) { 1747 continue; 1748 } 1749 1750 $processor->add_class( $state_class ); 1751 } 1752 1753 return $processor->get_updated_html(); 1754 } 1755 1756 add_filter( 'render_block', 'block_core_navigation_add_support_classes_to_container', 11, 2 ); 1757 1758 /** 1759 * Filter that changes the parsed attribute values of navigation blocks contain typographic presets to contain the values directly. 1760 * 1761 * @since 5.9.0 1762 * 1763 * @param array $parsed_block The block being rendered. 1764 * 1765 * @return array The block being rendered without typographic presets. 1766 */ 1767 function block_core_navigation_typographic_presets_backcompatibility( $parsed_block ) { 1768 if ( 'core/navigation' === $parsed_block['blockName'] ) { 1769 $attribute_to_prefix_map = array( 1770 'fontStyle' => 'var:preset|font-style|', 1771 'fontWeight' => 'var:preset|font-weight|', 1772 'textDecoration' => 'var:preset|text-decoration|', 1773 'textTransform' => 'var:preset|text-transform|', 1774 ); 1775 foreach ( $attribute_to_prefix_map as $style_attribute => $prefix ) { 1776 if ( ! empty( $parsed_block['attrs']['style']['typography'][ $style_attribute ] ) ) { 1777 $prefix_len = strlen( $prefix ); 1778 $attribute_value = &$parsed_block['attrs']['style']['typography'][ $style_attribute ]; 1779 if ( 0 === strncmp( $attribute_value, $prefix, $prefix_len ) ) { 1780 $attribute_value = substr( $attribute_value, $prefix_len ); 1781 } 1782 if ( 'textDecoration' === $style_attribute && 'strikethrough' === $attribute_value ) { 1783 $attribute_value = 'line-through'; 1784 } 1785 } 1786 } 1787 } 1788 1789 return $parsed_block; 1790 } 1791 1792 add_filter( 'render_block_data', 'block_core_navigation_typographic_presets_backcompatibility' ); 1793 1794 /** 1795 * Turns menu item data into a nested array of parsed blocks 1796 * 1797 * @since 5.9.0 1798 * 1799 * @deprecated 6.3.0 Use WP_Navigation_Fallback::parse_blocks_from_menu_items() instead. 1800 * 1801 * @param array $menu_items An array of menu items that represent 1802 * an individual level of a menu. 1803 * @param array $menu_items_by_parent_id An array keyed by the id of the 1804 * parent menu where each element is an 1805 * array of menu items that belong to 1806 * that parent. 1807 * @return array An array of parsed block data. 1808 */ 1809 function block_core_navigation_parse_blocks_from_menu_items( $menu_items, $menu_items_by_parent_id ) { 1810 1811 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::parse_blocks_from_menu_items' ); 1812 1813 if ( empty( $menu_items ) ) { 1814 return array(); 1815 } 1816 1817 $blocks = array(); 1818 1819 foreach ( $menu_items as $menu_item ) { 1820 $class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null; 1821 $id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null; 1822 $opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target; 1823 $rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null; 1824 $kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom'; 1825 1826 $block = array( 1827 'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link', 1828 'attrs' => array( 1829 'className' => $class_name, 1830 'description' => $menu_item->description, 1831 'id' => $id, 1832 'kind' => $kind, 1833 'label' => $menu_item->title, 1834 'opensInNewTab' => $opens_in_new_tab, 1835 'rel' => $rel, 1836 'title' => $menu_item->attr_title, 1837 'type' => $menu_item->object, 1838 'url' => $menu_item->url, 1839 ), 1840 ); 1841 1842 $block['innerBlocks'] = isset( $menu_items_by_parent_id[ $menu_item->ID ] ) 1843 ? block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id ) 1844 : array(); 1845 $block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] ); 1846 1847 $blocks[] = $block; 1848 } 1849 1850 return $blocks; 1851 } 1852 1853 /** 1854 * Get the classic navigation menu to use as a fallback. 1855 * 1856 * @since 6.2.0 1857 * 1858 * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback() instead. 1859 * 1860 * @return object WP_Term The classic navigation. 1861 */ 1862 function block_core_navigation_get_classic_menu_fallback() { 1863 1864 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback' ); 1865 1866 $classic_nav_menus = wp_get_nav_menus(); 1867 1868 // If menus exist. 1869 if ( $classic_nav_menus && ! is_wp_error( $classic_nav_menus ) ) { 1870 // Handles simple use case where user has a classic menu and switches to a block theme. 1871 1872 // Returns the menu assigned to location `primary`. 1873 $locations = get_nav_menu_locations(); 1874 if ( isset( $locations['primary'] ) ) { 1875 $primary_menu = wp_get_nav_menu_object( $locations['primary'] ); 1876 if ( $primary_menu ) { 1877 return $primary_menu; 1878 } 1879 } 1880 1881 // Returns a menu if `primary` is its slug. 1882 foreach ( $classic_nav_menus as $classic_nav_menu ) { 1883 if ( 'primary' === $classic_nav_menu->slug ) { 1884 return $classic_nav_menu; 1885 } 1886 } 1887 1888 // Otherwise return the most recently created classic menu. 1889 usort( 1890 $classic_nav_menus, 1891 static function ( $a, $b ) { 1892 return $b->term_id - $a->term_id; 1893 } 1894 ); 1895 return $classic_nav_menus[0]; 1896 } 1897 } 1898 1899 /** 1900 * Converts a classic navigation to blocks. 1901 * 1902 * @since 6.2.0 1903 * 1904 * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback_blocks() instead. 1905 * 1906 * @param object $classic_nav_menu WP_Term The classic navigation object to convert. 1907 * @return array the normalized parsed blocks. 1908 */ 1909 function block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ) { 1910 1911 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback_blocks' ); 1912 1913 // BEGIN: Code that already exists in wp_nav_menu(). 1914 $menu_items = wp_get_nav_menu_items( $classic_nav_menu->term_id, array( 'update_post_term_cache' => false ) ); 1915 1916 // Set up the $menu_item variables. 1917 _wp_menu_item_classes_by_context( $menu_items ); 1918 1919 $sorted_menu_items = array(); 1920 foreach ( (array) $menu_items as $menu_item ) { 1921 $sorted_menu_items[ $menu_item->menu_order ] = $menu_item; 1922 } 1923 1924 unset( $menu_items, $menu_item ); 1925 1926 // END: Code that already exists in wp_nav_menu(). 1927 1928 $menu_items_by_parent_id = array(); 1929 foreach ( $sorted_menu_items as $menu_item ) { 1930 $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item; 1931 } 1932 1933 $inner_blocks = block_core_navigation_parse_blocks_from_menu_items( 1934 $menu_items_by_parent_id[0] ?? array(), 1935 $menu_items_by_parent_id 1936 ); 1937 1938 return serialize_blocks( $inner_blocks ); 1939 } 1940 1941 /** 1942 * If there's a classic menu then use it as a fallback. 1943 * 1944 * @since 6.2.0 1945 * 1946 * @deprecated 6.3.0 Use WP_Navigation_Fallback::create_classic_menu_fallback() instead. 1947 * 1948 * @return array the normalized parsed blocks. 1949 */ 1950 function block_core_navigation_maybe_use_classic_menu_fallback() { 1951 1952 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::create_classic_menu_fallback' ); 1953 1954 // See if we have a classic menu. 1955 $classic_nav_menu = block_core_navigation_get_classic_menu_fallback(); 1956 1957 if ( ! $classic_nav_menu ) { 1958 return; 1959 } 1960 1961 // If we have a classic menu then convert it to blocks. 1962 $classic_nav_menu_blocks = block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ); 1963 1964 if ( empty( $classic_nav_menu_blocks ) ) { 1965 return; 1966 } 1967 1968 // Create a new navigation menu from the classic menu. 1969 $wp_insert_post_result = wp_insert_post( 1970 array( 1971 'post_content' => $classic_nav_menu_blocks, 1972 'post_title' => $classic_nav_menu->name, 1973 'post_name' => $classic_nav_menu->slug, 1974 'post_status' => 'publish', 1975 'post_type' => 'wp_navigation', 1976 ), 1977 true // So that we can check whether the result is an error. 1978 ); 1979 1980 if ( is_wp_error( $wp_insert_post_result ) ) { 1981 return; 1982 } 1983 1984 // Fetch the most recently published navigation which will be the classic one created above. 1985 return block_core_navigation_get_most_recently_published_navigation(); 1986 } 1987 1988 /** 1989 * Finds the most recently published `wp_navigation` Post. 1990 * 1991 * @since 6.1.0 1992 * 1993 * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_most_recently_published_navigation() instead. 1994 * 1995 * @return WP_Post|null the first non-empty Navigation or null. 1996 */ 1997 function block_core_navigation_get_most_recently_published_navigation() { 1998 1999 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_most_recently_published_navigation' ); 2000 2001 // Default to the most recently created menu. 2002 $parsed_args = array( 2003 'post_type' => 'wp_navigation', 2004 'no_found_rows' => true, 2005 'update_post_meta_cache' => false, 2006 'update_post_term_cache' => false, 2007 'order' => 'DESC', 2008 'orderby' => 'date', 2009 'post_status' => 'publish', 2010 'posts_per_page' => 1, // get only the most recent. 2011 ); 2012 2013 $navigation_post = new WP_Query( $parsed_args ); 2014 if ( count( $navigation_post->posts ) > 0 ) { 2015 return $navigation_post->posts[0]; 2016 } 2017 2018 return null; 2019 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Thu Oct 8 08:20:33 2026 | Cross-referenced by PHPXref |