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