[ 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 * Helper functions used to render the navigation block. 10 */ 11 class WP_Navigation_Block_Renderer { 12 13 /** 14 * Used to determine whether or not a navigation has submenus. 15 */ 16 private static $has_submenus = false; 17 18 /** 19 * Used to determine which blocks need an <li> wrapper. 20 * 21 * @var array 22 */ 23 private static $needs_list_item_wrapper = array( 24 'core/site-title', 25 'core/site-logo', 26 ); 27 28 /** 29 * Keeps track of all the navigation names that have been seen. 30 * 31 * @var array 32 */ 33 private static $seen_menu_names = array(); 34 35 /** 36 * Returns whether or not this is responsive navigation. 37 * 38 * @param array $attributes The block attributes. 39 * @return bool Returns whether or not this is responsive navigation. 40 */ 41 private static function is_responsive( $attributes ) { 42 /** 43 * This is for backwards compatibility after the `isResponsive` attribute was been removed. 44 */ 45 46 $has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive']; 47 return isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute; 48 } 49 50 /** 51 * Returns whether or not a navigation has a submenu. 52 * 53 * @param WP_Block_List $inner_blocks The list of inner blocks. 54 * @return bool Returns whether or not a navigation has a submenu and also sets the member variable. 55 */ 56 private static function has_submenus( $inner_blocks ) { 57 if ( true === static::$has_submenus ) { 58 return static::$has_submenus; 59 } 60 61 foreach ( $inner_blocks as $inner_block ) { 62 // If this is a page list then work out if any of the pages have children. 63 if ( 'core/page-list' === $inner_block->name ) { 64 $all_pages = get_pages( 65 array( 66 'sort_column' => 'menu_order,post_title', 67 'order' => 'asc', 68 ) 69 ); 70 foreach ( (array) $all_pages as $page ) { 71 if ( $page->post_parent ) { 72 static::$has_submenus = true; 73 break; 74 } 75 } 76 } 77 // If this is a navigation submenu then we know we have submenus. 78 if ( 'core/navigation-submenu' === $inner_block->name ) { 79 static::$has_submenus = true; 80 break; 81 } 82 } 83 84 return static::$has_submenus; 85 } 86 87 /** 88 * Determine whether the navigation blocks is interactive. 89 * 90 * @param array $attributes The block attributes. 91 * @param WP_Block_List $inner_blocks The list of inner blocks. 92 * @return bool Returns whether or not to load the view script. 93 */ 94 private static function is_interactive( $attributes, $inner_blocks ) { 95 $has_submenus = static::has_submenus( $inner_blocks ); 96 $is_responsive_menu = static::is_responsive( $attributes ); 97 return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; 98 } 99 100 /** 101 * Returns whether or not a block needs a list item wrapper. 102 * 103 * @param WP_Block $block The block. 104 * @return bool Returns whether or not a block needs a list item wrapper. 105 */ 106 private static function does_block_need_a_list_item_wrapper( $block ) { 107 108 /** 109 * Filter the list of blocks that need a list item wrapper. 110 * 111 * Affords the ability to customize which blocks need a list item wrapper when rendered 112 * within a core/navigation block. 113 * This is useful for blocks that are not list items but should be wrapped in a list 114 * item when used as a child of a navigation block. 115 * 116 * @since 6.5.0 117 * 118 * @param array $needs_list_item_wrapper The list of blocks that need a list item wrapper. 119 * @return array The list of blocks that need a list item wrapper. 120 */ 121 $needs_list_item_wrapper = apply_filters( 'block_core_navigation_listable_blocks', static::$needs_list_item_wrapper ); 122 123 return in_array( $block->name, $needs_list_item_wrapper, true ); 124 } 125 126 /** 127 * Returns the markup for a single inner block. 128 * 129 * @param WP_Block $inner_block The inner block. 130 * @return string Returns the markup for a single inner block. 131 */ 132 private static function get_markup_for_inner_block( $inner_block ) { 133 $inner_block_content = $inner_block->render(); 134 if ( ! empty( $inner_block_content ) ) { 135 if ( static::does_block_need_a_list_item_wrapper( $inner_block ) ) { 136 return '<li class="wp-block-navigation-item">' . $inner_block_content . '</li>'; 137 } 138 } 139 140 return $inner_block_content; 141 } 142 143 /** 144 * Returns the html for the inner blocks of the navigation block. 145 * 146 * @param array $attributes The block attributes. 147 * @param WP_Block_List $inner_blocks The list of inner blocks. 148 * @return string Returns the html for the inner blocks of the navigation block. 149 */ 150 private static function get_inner_blocks_html( $attributes, $inner_blocks ) { 151 $has_submenus = static::has_submenus( $inner_blocks ); 152 $is_interactive = static::is_interactive( $attributes, $inner_blocks ); 153 154 $style = static::get_styles( $attributes ); 155 $class = static::get_classes( $attributes ); 156 $container_attributes = get_block_wrapper_attributes( 157 array( 158 'class' => 'wp-block-navigation__container ' . $class, 159 'style' => $style, 160 ) 161 ); 162 163 $inner_blocks_html = ''; 164 $is_list_open = false; 165 166 foreach ( $inner_blocks as $inner_block ) { 167 $inner_block_markup = static::get_markup_for_inner_block( $inner_block ); 168 $p = new WP_HTML_Tag_Processor( $inner_block_markup ); 169 $is_list_item = $p->next_tag( 'LI' ); 170 171 if ( $is_list_item && ! $is_list_open ) { 172 $is_list_open = true; 173 $inner_blocks_html .= sprintf( 174 '<ul %1$s>', 175 $container_attributes 176 ); 177 } 178 179 if ( ! $is_list_item && $is_list_open ) { 180 $is_list_open = false; 181 $inner_blocks_html .= '</ul>'; 182 } 183 184 $inner_blocks_html .= $inner_block_markup; 185 } 186 187 if ( $is_list_open ) { 188 $inner_blocks_html .= '</ul>'; 189 } 190 191 // Add directives to the submenu if needed. 192 if ( $has_submenus && $is_interactive ) { 193 $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); 194 $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); 195 } 196 197 return $inner_blocks_html; 198 } 199 200 /** 201 * Gets the inner blocks for the navigation block from the navigation post. 202 * 203 * @param array $attributes The block attributes. 204 * @return WP_Block_List Returns the inner blocks for the navigation block. 205 */ 206 private static function get_inner_blocks_from_navigation_post( $attributes ) { 207 $navigation_post = get_post( $attributes['ref'] ); 208 if ( ! isset( $navigation_post ) ) { 209 return new WP_Block_List( array(), $attributes ); 210 } 211 212 // Only published posts are valid. If this is changed then a corresponding change 213 // must also be implemented in `use-navigation-menu.js`. 214 if ( 'publish' === $navigation_post->post_status ) { 215 $parsed_blocks = parse_blocks( $navigation_post->post_content ); 216 217 // 'parse_blocks' includes a null block with '\n\n' as the content when 218 // it encounters whitespace. This code strips it. 219 $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); 220 221 if ( function_exists( 'set_ignored_hooked_blocks_metadata' ) ) { 222 // Run Block Hooks algorithm to inject hooked blocks. 223 $markup = block_core_navigation_insert_hooked_blocks( $blocks, $navigation_post ); 224 $root_nav_block = parse_blocks( $markup )[0]; 225 226 $blocks = isset( $root_nav_block['innerBlocks'] ) ? $root_nav_block['innerBlocks'] : $blocks; 227 } 228 229 // TODO - this uses the full navigation block attributes for the 230 // context which could be refined. 231 return new WP_Block_List( $blocks, $attributes ); 232 } 233 } 234 235 /** 236 * Gets the inner blocks for the navigation block from the fallback. 237 * 238 * @param array $attributes The block attributes. 239 * @return WP_Block_List Returns the inner blocks for the navigation block. 240 */ 241 private static function get_inner_blocks_from_fallback( $attributes ) { 242 $fallback_blocks = block_core_navigation_get_fallback_blocks(); 243 244 // Fallback my have been filtered so do basic test for validity. 245 if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { 246 return new WP_Block_List( array(), $attributes ); 247 } 248 249 return new WP_Block_List( $fallback_blocks, $attributes ); 250 } 251 252 /** 253 * Gets the inner blocks for the navigation block. 254 * 255 * @param array $attributes The block attributes. 256 * @param WP_Block $block The parsed block. 257 * @return WP_Block_List Returns the inner blocks for the navigation block. 258 */ 259 private static function get_inner_blocks( $attributes, $block ) { 260 $inner_blocks = $block->inner_blocks; 261 262 // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. 263 if ( array_key_exists( 'navigationMenuId', $attributes ) ) { 264 $attributes['ref'] = $attributes['navigationMenuId']; 265 } 266 267 // If: 268 // - the gutenberg plugin is active 269 // - `__unstableLocation` is defined 270 // - we have menu items at the defined location 271 // - we don't have a relationship to a `wp_navigation` Post (via `ref`). 272 // ...then create inner blocks from the classic menu assigned to that location. 273 if ( 274 defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && 275 array_key_exists( '__unstableLocation', $attributes ) && 276 ! array_key_exists( 'ref', $attributes ) && 277 ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) 278 ) { 279 $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); 280 } 281 282 // Load inner blocks from the navigation post. 283 if ( array_key_exists( 'ref', $attributes ) ) { 284 $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); 285 } 286 287 // If there are no inner blocks then fallback to rendering an appropriate fallback. 288 if ( empty( $inner_blocks ) ) { 289 $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); 290 } 291 292 /** 293 * Filter navigation block $inner_blocks. 294 * Allows modification of a navigation block menu items. 295 * 296 * @since 6.1.0 297 * 298 * @param \WP_Block_List $inner_blocks 299 */ 300 $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); 301 302 $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); 303 if ( $post_ids ) { 304 _prime_post_caches( $post_ids, false, false ); 305 } 306 307 return $inner_blocks; 308 } 309 310 /** 311 * Gets the name of the current navigation, if it has one. 312 * 313 * @param array $attributes The block attributes. 314 * @return string Returns the name of the navigation. 315 */ 316 private static function get_navigation_name( $attributes ) { 317 318 $navigation_name = $attributes['ariaLabel'] ?? ''; 319 320 // Load the navigation post. 321 if ( array_key_exists( 'ref', $attributes ) ) { 322 $navigation_post = get_post( $attributes['ref'] ); 323 if ( ! isset( $navigation_post ) ) { 324 return $navigation_name; 325 } 326 327 // Only published posts are valid. If this is changed then a corresponding change 328 // must also be implemented in `use-navigation-menu.js`. 329 if ( 'publish' === $navigation_post->post_status ) { 330 $navigation_name = $navigation_post->post_title; 331 332 // This is used to count the number of times a navigation name has been seen, 333 // so that we can ensure every navigation has a unique id. 334 if ( isset( static::$seen_menu_names[ $navigation_name ] ) ) { 335 ++static::$seen_menu_names[ $navigation_name ]; 336 } else { 337 static::$seen_menu_names[ $navigation_name ] = 1; 338 } 339 } 340 } 341 342 return $navigation_name; 343 } 344 345 /** 346 * Returns the layout class for the navigation block. 347 * 348 * @param array $attributes The block attributes. 349 * @return string Returns the layout class for the navigation block. 350 */ 351 private static function get_layout_class( $attributes ) { 352 $layout_justification = array( 353 'left' => 'items-justified-left', 354 'right' => 'items-justified-right', 355 'center' => 'items-justified-center', 356 'space-between' => 'items-justified-space-between', 357 ); 358 359 $layout_class = ''; 360 if ( 361 isset( $attributes['layout']['justifyContent'] ) && 362 isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) 363 ) { 364 $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; 365 } 366 if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { 367 $layout_class .= ' is-vertical'; 368 } 369 370 if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { 371 $layout_class .= ' no-wrap'; 372 } 373 return $layout_class; 374 } 375 376 /** 377 * Return classes for the navigation block. 378 * 379 * @param array $attributes The block attributes. 380 * @return string Returns the classes for the navigation block. 381 */ 382 private static function get_classes( $attributes ) { 383 // Restore legacy classnames for submenu positioning. 384 $layout_class = static::get_layout_class( $attributes ); 385 $colors = block_core_navigation_build_css_colors( $attributes ); 386 $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); 387 $is_responsive_menu = static::is_responsive( $attributes ); 388 389 // Manually add block support text decoration as CSS class. 390 $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; 391 $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); 392 393 $classes = array_merge( 394 $colors['css_classes'], 395 $font_sizes['css_classes'], 396 $is_responsive_menu ? array( 'is-responsive' ) : array(), 397 $layout_class ? array( $layout_class ) : array(), 398 $text_decoration ? array( $text_decoration_class ) : array() 399 ); 400 return implode( ' ', $classes ); 401 } 402 403 /** 404 * Get styles for the navigation block. 405 * 406 * @param array $attributes The block attributes. 407 * @return string Returns the styles for the navigation block. 408 */ 409 private static function get_styles( $attributes ) { 410 $colors = block_core_navigation_build_css_colors( $attributes ); 411 $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); 412 $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; 413 return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; 414 } 415 416 /** 417 * Get the responsive container markup 418 * 419 * @param array $attributes The block attributes. 420 * @param WP_Block_List $inner_blocks The list of inner blocks. 421 * @param string $inner_blocks_html The markup for the inner blocks. 422 * @return string Returns the container markup. 423 */ 424 private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { 425 $is_interactive = static::is_interactive( $attributes, $inner_blocks ); 426 $colors = block_core_navigation_build_css_colors( $attributes ); 427 $modal_unique_id = wp_unique_id( 'modal-' ); 428 429 $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; 430 431 $responsive_container_classes = array( 432 'wp-block-navigation__responsive-container', 433 $is_hidden_by_default ? 'hidden-by-default' : '', 434 implode( ' ', $colors['overlay_css_classes'] ), 435 ); 436 $open_button_classes = array( 437 'wp-block-navigation__responsive-container-open', 438 $is_hidden_by_default ? 'always-shown' : '', 439 ); 440 441 $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; 442 $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"><rect x="4" y="7.5" width="16" height="1.5" /><rect x="4" y="15" width="16" height="1.5" /></svg>'; 443 if ( isset( $attributes['icon'] ) ) { 444 if ( 'menu' === $attributes['icon'] ) { 445 $toggle_button_icon = '<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 5v1.5h14V5H5zm0 7.8h14v-1.5H5v1.5zM5 19h14v-1.5H5V19z" /></svg>'; 446 } 447 } 448 $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); 449 $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 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path></svg>'; 450 $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); 451 $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. 452 $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. 453 454 // Add Interactivity API directives to the markup if needed. 455 $open_button_directives = ''; 456 $responsive_container_directives = ''; 457 $responsive_dialog_directives = ''; 458 $close_button_directives = ''; 459 if ( $is_interactive ) { 460 $open_button_directives = ' 461 data-wp-on--click="actions.openMenuOnClick" 462 data-wp-on--keydown="actions.handleMenuKeydown" 463 '; 464 $responsive_container_directives = ' 465 data-wp-class--has-modal-open="state.isMenuOpen" 466 data-wp-class--is-menu-open="state.isMenuOpen" 467 data-wp-watch="callbacks.initMenu" 468 data-wp-on--keydown="actions.handleMenuKeydown" 469 data-wp-on--focusout="actions.handleMenuFocusout" 470 tabindex="-1" 471 '; 472 $responsive_dialog_directives = ' 473 data-wp-bind--aria-modal="state.ariaModal" 474 data-wp-bind--aria-label="state.ariaLabel" 475 data-wp-bind--role="state.roleAttribute" 476 '; 477 $close_button_directives = ' 478 data-wp-on--click="actions.closeMenuOnClick" 479 '; 480 $responsive_container_content_directives = ' 481 data-wp-watch="callbacks.focusFirstElement" 482 '; 483 } 484 485 return sprintf( 486 '<button aria-haspopup="dialog" %3$s class="%6$s" %10$s>%8$s</button> 487 <div class="%5$s" style="%7$s" id="%1$s" %11$s> 488 <div class="wp-block-navigation__responsive-close" tabindex="-1"> 489 <div class="wp-block-navigation__responsive-dialog" %12$s> 490 <button %4$s class="wp-block-navigation__responsive-container-close" %13$s>%9$s</button> 491 <div class="wp-block-navigation__responsive-container-content" %14$s id="%1$s-content"> 492 %2$s 493 </div> 494 </div> 495 </div> 496 </div>', 497 esc_attr( $modal_unique_id ), 498 $inner_blocks_html, 499 $toggle_aria_label_open, 500 $toggle_aria_label_close, 501 esc_attr( implode( ' ', $responsive_container_classes ) ), 502 esc_attr( implode( ' ', $open_button_classes ) ), 503 esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), 504 $toggle_button_content, 505 $toggle_close_button_content, 506 $open_button_directives, 507 $responsive_container_directives, 508 $responsive_dialog_directives, 509 $close_button_directives, 510 $responsive_container_content_directives 511 ); 512 } 513 514 /** 515 * Get the wrapper attributes 516 * 517 * @param array $attributes The block attributes. 518 * @param WP_Block_List $inner_blocks A list of inner blocks. 519 * @return string Returns the navigation block markup. 520 */ 521 private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) { 522 $nav_menu_name = static::get_unique_navigation_name( $attributes ); 523 $is_interactive = static::is_interactive( $attributes, $inner_blocks ); 524 $is_responsive_menu = static::is_responsive( $attributes ); 525 $style = static::get_styles( $attributes ); 526 $class = static::get_classes( $attributes ); 527 $wrapper_attributes = get_block_wrapper_attributes( 528 array( 529 'class' => $class, 530 'style' => $style, 531 'aria-label' => $nav_menu_name, 532 ) 533 ); 534 535 if ( $is_responsive_menu ) { 536 $nav_element_directives = static::get_nav_element_directives( $is_interactive ); 537 $wrapper_attributes .= ' ' . $nav_element_directives; 538 } 539 540 return $wrapper_attributes; 541 } 542 543 /** 544 * Gets the nav element directives. 545 * 546 * @param bool $is_interactive Whether the block is interactive. 547 * @return string the directives for the navigation element. 548 */ 549 private static function get_nav_element_directives( $is_interactive ) { 550 if ( ! $is_interactive ) { 551 return ''; 552 } 553 // When adding to this array be mindful of security concerns. 554 $nav_element_context = wp_interactivity_data_wp_context( 555 array( 556 'overlayOpenedBy' => array( 557 'click' => false, 558 'hover' => false, 559 'focus' => false, 560 ), 561 'type' => 'overlay', 562 'roleAttribute' => '', 563 'ariaLabel' => __( 'Menu' ), 564 ) 565 ); 566 $nav_element_directives = ' 567 data-wp-interactive="core/navigation" ' 568 . $nav_element_context; 569 570 return $nav_element_directives; 571 } 572 573 /** 574 * Handle view script module loading. 575 * 576 * @param array $attributes The block attributes. 577 * @param WP_Block $block The parsed block. 578 * @param WP_Block_List $inner_blocks The list of inner blocks. 579 */ 580 private static function handle_view_script_module_loading( $attributes, $block, $inner_blocks ) { 581 if ( static::is_interactive( $attributes, $inner_blocks ) ) { 582 $suffix = wp_scripts_get_suffix(); 583 if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { 584 $module_url = gutenberg_url( '/build/interactivity/navigation.min.js' ); 585 } 586 587 wp_register_script_module( 588 '@wordpress/block-library/navigation', 589 isset( $module_url ) ? $module_url : includes_url( "blocks/navigation/view{$suffix}.js" ), 590 array( '@wordpress/interactivity' ), 591 defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) 592 ); 593 wp_enqueue_script_module( '@wordpress/block-library/navigation' ); 594 } 595 } 596 597 /** 598 * Returns the markup for the navigation block. 599 * 600 * @param array $attributes The block attributes. 601 * @param WP_Block_List $inner_blocks The list of inner blocks. 602 * @return string Returns the navigation wrapper markup. 603 */ 604 private static function get_wrapper_markup( $attributes, $inner_blocks ) { 605 $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); 606 if ( static::is_responsive( $attributes ) ) { 607 return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); 608 } 609 return $inner_blocks_html; 610 } 611 612 /** 613 * Returns a unique name for the navigation. 614 * 615 * @param array $attributes The block attributes. 616 * @return string Returns a unique name for the navigation. 617 */ 618 private static function get_unique_navigation_name( $attributes ) { 619 $nav_menu_name = static::get_navigation_name( $attributes ); 620 621 // If the menu name has been used previously then append an ID 622 // to the name to ensure uniqueness across a given post. 623 if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) && static::$seen_menu_names[ $nav_menu_name ] > 1 ) { 624 $count = static::$seen_menu_names[ $nav_menu_name ]; 625 $nav_menu_name = $nav_menu_name . ' ' . ( $count ); 626 } 627 628 return $nav_menu_name; 629 } 630 631 /** 632 * Renders the navigation block. 633 * 634 * @param array $attributes The block attributes. 635 * @param string $content The saved content. 636 * @param WP_Block $block The parsed block. 637 * @return string Returns the navigation block markup. 638 */ 639 public static function render( $attributes, $content, $block ) { 640 /** 641 * Deprecated: 642 * The rgbTextColor and rgbBackgroundColor attributes 643 * have been deprecated in favor of 644 * customTextColor and customBackgroundColor ones. 645 * Move the values from old attrs to the new ones. 646 */ 647 if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { 648 $attributes['customTextColor'] = $attributes['rgbTextColor']; 649 } 650 651 if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { 652 $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; 653 } 654 655 unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); 656 657 $inner_blocks = static::get_inner_blocks( $attributes, $block ); 658 // Prevent navigation blocks referencing themselves from rendering. 659 if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { 660 return ''; 661 } 662 663 static::handle_view_script_module_loading( $attributes, $block, $inner_blocks ); 664 665 return sprintf( 666 '<nav %1$s>%2$s</nav>', 667 static::get_nav_wrapper_attributes( $attributes, $inner_blocks ), 668 static::get_wrapper_markup( $attributes, $inner_blocks ) 669 ); 670 } 671 } 672 673 // These functions are used for the __unstableLocation feature and only active 674 // when the gutenberg plugin is active. 675 if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { 676 /** 677 * Returns the menu items for a WordPress menu location. 678 * 679 * @param string $location The menu location. 680 * @return array Menu items for the location. 681 */ 682 function block_core_navigation_get_menu_items_at_location( $location ) { 683 if ( empty( $location ) ) { 684 return; 685 } 686 687 // Build menu data. The following approximates the code in 688 // `wp_nav_menu()` and `gutenberg_output_block_nav_menu`. 689 690 // Find the location in the list of locations, returning early if the 691 // location can't be found. 692 $locations = get_nav_menu_locations(); 693 if ( ! isset( $locations[ $location ] ) ) { 694 return; 695 } 696 697 // Get the menu from the location, returning early if there is no 698 // menu or there was an error. 699 $menu = wp_get_nav_menu_object( $locations[ $location ] ); 700 if ( ! $menu || is_wp_error( $menu ) ) { 701 return; 702 } 703 704 $menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) ); 705 _wp_menu_item_classes_by_context( $menu_items ); 706 707 return $menu_items; 708 } 709 710 711 /** 712 * Sorts a standard array of menu items into a nested structure keyed by the 713 * id of the parent menu. 714 * 715 * @param array $menu_items Menu items to sort. 716 * @return array An array keyed by the id of the parent menu where each element 717 * is an array of menu items that belong to that parent. 718 */ 719 function block_core_navigation_sort_menu_items_by_parent_id( $menu_items ) { 720 $sorted_menu_items = array(); 721 foreach ( (array) $menu_items as $menu_item ) { 722 $sorted_menu_items[ $menu_item->menu_order ] = $menu_item; 723 } 724 unset( $menu_items, $menu_item ); 725 726 $menu_items_by_parent_id = array(); 727 foreach ( $sorted_menu_items as $menu_item ) { 728 $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item; 729 } 730 731 return $menu_items_by_parent_id; 732 } 733 734 /** 735 * Gets the inner blocks for the navigation block from the unstable location attribute. 736 * 737 * @param array $attributes The block attributes. 738 * @return WP_Block_List Returns the inner blocks for the navigation block. 739 */ 740 function block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ) { 741 $menu_items = block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ); 742 if ( empty( $menu_items ) ) { 743 return new WP_Block_List( array(), $attributes ); 744 } 745 746 $menu_items_by_parent_id = block_core_navigation_sort_menu_items_by_parent_id( $menu_items ); 747 $parsed_blocks = block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id ); 748 return new WP_Block_List( $parsed_blocks, $attributes ); 749 } 750 } 751 752 /** 753 * Add Interactivity API directives to the navigation-submenu and page-list 754 * blocks markup using the Tag Processor. 755 * 756 * @param WP_HTML_Tag_Processor $tags Markup of the navigation block. 757 * @param array $block_attributes Block attributes. 758 * 759 * @return string Submenu markup with the directives injected. 760 */ 761 function block_core_navigation_add_directives_to_submenu( $tags, $block_attributes ) { 762 while ( $tags->next_tag( 763 array( 764 'tag_name' => 'LI', 765 'class_name' => 'has-child', 766 ) 767 ) ) { 768 // Add directives to the parent `<li>`. 769 $tags->set_attribute( 'data-wp-interactive', 'core/navigation' ); 770 $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "type": "submenu" }' ); 771 $tags->set_attribute( 'data-wp-watch', 'callbacks.initMenu' ); 772 $tags->set_attribute( 'data-wp-on--focusout', 'actions.handleMenuFocusout' ); 773 $tags->set_attribute( 'data-wp-on--keydown', 'actions.handleMenuKeydown' ); 774 775 // This is a fix for Safari. Without it, Safari doesn't change the active 776 // element when the user clicks on a button. It can be removed once we add 777 // an overlay to capture the clicks, instead of relying on the focusout 778 // event. 779 $tags->set_attribute( 'tabindex', '-1' ); 780 781 if ( ! isset( $block_attributes['openSubmenusOnClick'] ) || false === $block_attributes['openSubmenusOnClick'] ) { 782 $tags->set_attribute( 'data-wp-on--mouseenter', 'actions.openMenuOnHover' ); 783 $tags->set_attribute( 'data-wp-on--mouseleave', 'actions.closeMenuOnHover' ); 784 } 785 786 // Add directives to the toggle submenu button. 787 if ( $tags->next_tag( 788 array( 789 'tag_name' => 'BUTTON', 790 'class_name' => 'wp-block-navigation-submenu__toggle', 791 ) 792 ) ) { 793 $tags->set_attribute( 'data-wp-on--click', 'actions.toggleMenuOnClick' ); 794 $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isMenuOpen' ); 795 // The `aria-expanded` attribute for SSR is already added in the submenu block. 796 } 797 // Add directives to the submenu. 798 if ( $tags->next_tag( 799 array( 800 'tag_name' => 'UL', 801 'class_name' => 'wp-block-navigation__submenu-container', 802 ) 803 ) ) { 804 $tags->set_attribute( 'data-wp-on--focus', 'actions.openMenuOnFocus' ); 805 } 806 807 // Iterate through subitems if exist. 808 block_core_navigation_add_directives_to_submenu( $tags, $block_attributes ); 809 } 810 return $tags->get_updated_html(); 811 } 812 813 /** 814 * Build an array with CSS classes and inline styles defining the colors 815 * which will be applied to the navigation markup in the front-end. 816 * 817 * @param array $attributes Navigation block attributes. 818 * 819 * @return array Colors CSS classes and inline styles. 820 */ 821 function block_core_navigation_build_css_colors( $attributes ) { 822 $colors = array( 823 'css_classes' => array(), 824 'inline_styles' => '', 825 'overlay_css_classes' => array(), 826 'overlay_inline_styles' => '', 827 ); 828 829 // Text color. 830 $has_named_text_color = array_key_exists( 'textColor', $attributes ); 831 $has_custom_text_color = array_key_exists( 'customTextColor', $attributes ); 832 833 // If has text color. 834 if ( $has_custom_text_color || $has_named_text_color ) { 835 // Add has-text-color class. 836 $colors['css_classes'][] = 'has-text-color'; 837 } 838 839 if ( $has_named_text_color ) { 840 // Add the color class. 841 $colors['css_classes'][] = sprintf( 'has-%s-color', $attributes['textColor'] ); 842 } elseif ( $has_custom_text_color ) { 843 // Add the custom color inline style. 844 $colors['inline_styles'] .= sprintf( 'color: %s;', $attributes['customTextColor'] ); 845 } 846 847 // Background color. 848 $has_named_background_color = array_key_exists( 'backgroundColor', $attributes ); 849 $has_custom_background_color = array_key_exists( 'customBackgroundColor', $attributes ); 850 851 // If has background color. 852 if ( $has_custom_background_color || $has_named_background_color ) { 853 // Add has-background class. 854 $colors['css_classes'][] = 'has-background'; 855 } 856 857 if ( $has_named_background_color ) { 858 // Add the background-color class. 859 $colors['css_classes'][] = sprintf( 'has-%s-background-color', $attributes['backgroundColor'] ); 860 } elseif ( $has_custom_background_color ) { 861 // Add the custom background-color inline style. 862 $colors['inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customBackgroundColor'] ); 863 } 864 865 // Overlay text color. 866 $has_named_overlay_text_color = array_key_exists( 'overlayTextColor', $attributes ); 867 $has_custom_overlay_text_color = array_key_exists( 'customOverlayTextColor', $attributes ); 868 869 // If has overlay text color. 870 if ( $has_custom_overlay_text_color || $has_named_overlay_text_color ) { 871 // Add has-text-color class. 872 $colors['overlay_css_classes'][] = 'has-text-color'; 873 } 874 875 if ( $has_named_overlay_text_color ) { 876 // Add the overlay color class. 877 $colors['overlay_css_classes'][] = sprintf( 'has-%s-color', $attributes['overlayTextColor'] ); 878 } elseif ( $has_custom_overlay_text_color ) { 879 // Add the custom overlay color inline style. 880 $colors['overlay_inline_styles'] .= sprintf( 'color: %s;', $attributes['customOverlayTextColor'] ); 881 } 882 883 // Overlay background color. 884 $has_named_overlay_background_color = array_key_exists( 'overlayBackgroundColor', $attributes ); 885 $has_custom_overlay_background_color = array_key_exists( 'customOverlayBackgroundColor', $attributes ); 886 887 // If has overlay background color. 888 if ( $has_custom_overlay_background_color || $has_named_overlay_background_color ) { 889 // Add has-background class. 890 $colors['overlay_css_classes'][] = 'has-background'; 891 } 892 893 if ( $has_named_overlay_background_color ) { 894 // Add the overlay background-color class. 895 $colors['overlay_css_classes'][] = sprintf( 'has-%s-background-color', $attributes['overlayBackgroundColor'] ); 896 } elseif ( $has_custom_overlay_background_color ) { 897 // Add the custom overlay background-color inline style. 898 $colors['overlay_inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customOverlayBackgroundColor'] ); 899 } 900 901 return $colors; 902 } 903 904 /** 905 * Build an array with CSS classes and inline styles defining the font sizes 906 * which will be applied to the navigation markup in the front-end. 907 * 908 * @param array $attributes Navigation block attributes. 909 * 910 * @return array Font size CSS classes and inline styles. 911 */ 912 function block_core_navigation_build_css_font_sizes( $attributes ) { 913 // CSS classes. 914 $font_sizes = array( 915 'css_classes' => array(), 916 'inline_styles' => '', 917 ); 918 919 $has_named_font_size = array_key_exists( 'fontSize', $attributes ); 920 $has_custom_font_size = array_key_exists( 'customFontSize', $attributes ); 921 922 if ( $has_named_font_size ) { 923 // Add the font size class. 924 $font_sizes['css_classes'][] = sprintf( 'has-%s-font-size', $attributes['fontSize'] ); 925 } elseif ( $has_custom_font_size ) { 926 // Add the custom font size inline style. 927 $font_sizes['inline_styles'] = sprintf( 'font-size: %spx;', $attributes['customFontSize'] ); 928 } 929 930 return $font_sizes; 931 } 932 933 /** 934 * Returns the top-level submenu SVG chevron icon. 935 * 936 * @return string 937 */ 938 function block_core_navigation_render_submenu_icon() { 939 return '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true" focusable="false"><path d="M1.50002 4L6.00002 8L10.5 4" stroke-width="1.5"></path></svg>'; 940 } 941 942 /** 943 * Filter out empty "null" blocks from the block list. 944 * 'parse_blocks' includes a null block with '\n\n' as the content when 945 * it encounters whitespace. This is not a bug but rather how the parser 946 * is designed. 947 * 948 * @param array $parsed_blocks the parsed blocks to be normalized. 949 * @return array the normalized parsed blocks. 950 */ 951 function block_core_navigation_filter_out_empty_blocks( $parsed_blocks ) { 952 $filtered = array_filter( 953 $parsed_blocks, 954 static function ( $block ) { 955 return isset( $block['blockName'] ); 956 } 957 ); 958 959 // Reset keys. 960 return array_values( $filtered ); 961 } 962 963 /** 964 * Returns true if the navigation block contains a nested navigation block. 965 * 966 * @param WP_Block_List $inner_blocks Inner block instance to be normalized. 967 * @return bool true if the navigation block contains a nested navigation block. 968 */ 969 function block_core_navigation_block_contains_core_navigation( $inner_blocks ) { 970 foreach ( $inner_blocks as $block ) { 971 if ( 'core/navigation' === $block->name ) { 972 return true; 973 } 974 if ( $block->inner_blocks && block_core_navigation_block_contains_core_navigation( $block->inner_blocks ) ) { 975 return true; 976 } 977 } 978 979 return false; 980 } 981 982 /** 983 * Retrieves the appropriate fallback to be used on the front of the 984 * site when there is no menu assigned to the Nav block. 985 * 986 * This aims to mirror how the fallback mechanic for wp_nav_menu works. 987 * See https://developer.wordpress.org/reference/functions/wp_nav_menu/#more-information. 988 * 989 * @return array the array of blocks to be used as a fallback. 990 */ 991 function block_core_navigation_get_fallback_blocks() { 992 $page_list_fallback = array( 993 array( 994 'blockName' => 'core/page-list', 995 'innerContent' => array(), 996 'attrs' => array(), 997 ), 998 ); 999 1000 $registry = WP_Block_Type_Registry::get_instance(); 1001 1002 // If `core/page-list` is not registered then return empty blocks. 1003 $fallback_blocks = $registry->is_registered( 'core/page-list' ) ? $page_list_fallback : array(); 1004 $navigation_post = WP_Navigation_Fallback::get_fallback(); 1005 1006 // Use the first non-empty Navigation as fallback if available. 1007 if ( $navigation_post ) { 1008 $parsed_blocks = parse_blocks( $navigation_post->post_content ); 1009 $maybe_fallback = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); 1010 1011 // Normalizing blocks may result in an empty array of blocks if they were all `null` blocks. 1012 // In this case default to the (Page List) fallback. 1013 $fallback_blocks = ! empty( $maybe_fallback ) ? $maybe_fallback : $fallback_blocks; 1014 1015 if ( function_exists( 'set_ignored_hooked_blocks_metadata' ) ) { 1016 // Run Block Hooks algorithm to inject hooked blocks. 1017 // We have to run it here because we need the post ID of the Navigation block to track ignored hooked blocks. 1018 $markup = block_core_navigation_insert_hooked_blocks( $fallback_blocks, $navigation_post ); 1019 $blocks = parse_blocks( $markup ); 1020 1021 if ( isset( $blocks[0]['innerBlocks'] ) ) { 1022 $fallback_blocks = $blocks[0]['innerBlocks']; 1023 } 1024 } 1025 } 1026 1027 /** 1028 * Filters the fallback experience for the Navigation block. 1029 * 1030 * Returning a falsey value will opt out of the fallback and cause the block not to render. 1031 * To customise the blocks provided return an array of blocks - these should be valid 1032 * children of the `core/navigation` block. 1033 * 1034 * @since 5.9.0 1035 * 1036 * @param array[] $fallback_blocks default fallback blocks provided by the default block mechanic. 1037 */ 1038 return apply_filters( 'block_core_navigation_render_fallback', $fallback_blocks ); 1039 } 1040 1041 /** 1042 * Iterate through all inner blocks recursively and get navigation link block's post IDs. 1043 * 1044 * @param WP_Block_List $inner_blocks Block list class instance. 1045 * 1046 * @return array Array of post IDs. 1047 */ 1048 function block_core_navigation_get_post_ids( $inner_blocks ) { 1049 $post_ids = array_map( 'block_core_navigation_from_block_get_post_ids', iterator_to_array( $inner_blocks ) ); 1050 return array_unique( array_merge( ...$post_ids ) ); 1051 } 1052 1053 /** 1054 * Get post IDs from a navigation link block instance. 1055 * 1056 * @param WP_Block $block Instance of a block. 1057 * 1058 * @return array Array of post IDs. 1059 */ 1060 function block_core_navigation_from_block_get_post_ids( $block ) { 1061 $post_ids = array(); 1062 1063 if ( $block->inner_blocks ) { 1064 $post_ids = block_core_navigation_get_post_ids( $block->inner_blocks ); 1065 } 1066 1067 if ( 'core/navigation-link' === $block->name || 'core/navigation-submenu' === $block->name ) { 1068 if ( $block->attributes && isset( $block->attributes['kind'] ) && 'post-type' === $block->attributes['kind'] && isset( $block->attributes['id'] ) ) { 1069 $post_ids[] = $block->attributes['id']; 1070 } 1071 } 1072 1073 return $post_ids; 1074 } 1075 1076 /** 1077 * Renders the `core/navigation` block on server. 1078 * 1079 * @param array $attributes The block attributes. 1080 * @param string $content The saved content. 1081 * @param WP_Block $block The parsed block. 1082 * 1083 * @return string Returns the navigation block markup. 1084 */ 1085 function render_block_core_navigation( $attributes, $content, $block ) { 1086 return WP_Navigation_Block_Renderer::render( $attributes, $content, $block ); 1087 } 1088 1089 /** 1090 * Register the navigation block. 1091 * 1092 * @uses render_block_core_navigation() 1093 * @throws WP_Error An WP_Error exception parsing the block definition. 1094 */ 1095 function register_block_core_navigation() { 1096 register_block_type_from_metadata( 1097 __DIR__ . '/navigation', 1098 array( 1099 'render_callback' => 'render_block_core_navigation', 1100 ) 1101 ); 1102 } 1103 1104 add_action( 'init', 'register_block_core_navigation' ); 1105 1106 /** 1107 * Filter that changes the parsed attribute values of navigation blocks contain typographic presets to contain the values directly. 1108 * 1109 * @param array $parsed_block The block being rendered. 1110 * 1111 * @return array The block being rendered without typographic presets. 1112 */ 1113 function block_core_navigation_typographic_presets_backcompatibility( $parsed_block ) { 1114 if ( 'core/navigation' === $parsed_block['blockName'] ) { 1115 $attribute_to_prefix_map = array( 1116 'fontStyle' => 'var:preset|font-style|', 1117 'fontWeight' => 'var:preset|font-weight|', 1118 'textDecoration' => 'var:preset|text-decoration|', 1119 'textTransform' => 'var:preset|text-transform|', 1120 ); 1121 foreach ( $attribute_to_prefix_map as $style_attribute => $prefix ) { 1122 if ( ! empty( $parsed_block['attrs']['style']['typography'][ $style_attribute ] ) ) { 1123 $prefix_len = strlen( $prefix ); 1124 $attribute_value = &$parsed_block['attrs']['style']['typography'][ $style_attribute ]; 1125 if ( 0 === strncmp( $attribute_value, $prefix, $prefix_len ) ) { 1126 $attribute_value = substr( $attribute_value, $prefix_len ); 1127 } 1128 if ( 'textDecoration' === $style_attribute && 'strikethrough' === $attribute_value ) { 1129 $attribute_value = 'line-through'; 1130 } 1131 } 1132 } 1133 } 1134 1135 return $parsed_block; 1136 } 1137 1138 add_filter( 'render_block_data', 'block_core_navigation_typographic_presets_backcompatibility' ); 1139 1140 /** 1141 * Turns menu item data into a nested array of parsed blocks 1142 * 1143 * @deprecated 6.3.0 Use WP_Navigation_Fallback::parse_blocks_from_menu_items() instead. 1144 * 1145 * @param array $menu_items An array of menu items that represent 1146 * an individual level of a menu. 1147 * @param array $menu_items_by_parent_id An array keyed by the id of the 1148 * parent menu where each element is an 1149 * array of menu items that belong to 1150 * that parent. 1151 * @return array An array of parsed block data. 1152 */ 1153 function block_core_navigation_parse_blocks_from_menu_items( $menu_items, $menu_items_by_parent_id ) { 1154 1155 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::parse_blocks_from_menu_items' ); 1156 1157 if ( empty( $menu_items ) ) { 1158 return array(); 1159 } 1160 1161 $blocks = array(); 1162 1163 foreach ( $menu_items as $menu_item ) { 1164 $class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null; 1165 $id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null; 1166 $opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target; 1167 $rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null; 1168 $kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom'; 1169 1170 $block = array( 1171 'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link', 1172 'attrs' => array( 1173 'className' => $class_name, 1174 'description' => $menu_item->description, 1175 'id' => $id, 1176 'kind' => $kind, 1177 'label' => $menu_item->title, 1178 'opensInNewTab' => $opens_in_new_tab, 1179 'rel' => $rel, 1180 'title' => $menu_item->attr_title, 1181 'type' => $menu_item->object, 1182 'url' => $menu_item->url, 1183 ), 1184 ); 1185 1186 $block['innerBlocks'] = isset( $menu_items_by_parent_id[ $menu_item->ID ] ) 1187 ? block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id ) 1188 : array(); 1189 $block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] ); 1190 1191 $blocks[] = $block; 1192 } 1193 1194 return $blocks; 1195 } 1196 1197 /** 1198 * Get the classic navigation menu to use as a fallback. 1199 * 1200 * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback() instead. 1201 * 1202 * @return object WP_Term The classic navigation. 1203 */ 1204 function block_core_navigation_get_classic_menu_fallback() { 1205 1206 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback' ); 1207 1208 $classic_nav_menus = wp_get_nav_menus(); 1209 1210 // If menus exist. 1211 if ( $classic_nav_menus && ! is_wp_error( $classic_nav_menus ) ) { 1212 // Handles simple use case where user has a classic menu and switches to a block theme. 1213 1214 // Returns the menu assigned to location `primary`. 1215 $locations = get_nav_menu_locations(); 1216 if ( isset( $locations['primary'] ) ) { 1217 $primary_menu = wp_get_nav_menu_object( $locations['primary'] ); 1218 if ( $primary_menu ) { 1219 return $primary_menu; 1220 } 1221 } 1222 1223 // Returns a menu if `primary` is its slug. 1224 foreach ( $classic_nav_menus as $classic_nav_menu ) { 1225 if ( 'primary' === $classic_nav_menu->slug ) { 1226 return $classic_nav_menu; 1227 } 1228 } 1229 1230 // Otherwise return the most recently created classic menu. 1231 usort( 1232 $classic_nav_menus, 1233 static function ( $a, $b ) { 1234 return $b->term_id - $a->term_id; 1235 } 1236 ); 1237 return $classic_nav_menus[0]; 1238 } 1239 } 1240 1241 /** 1242 * Converts a classic navigation to blocks. 1243 * 1244 * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback_blocks() instead. 1245 * 1246 * @param object $classic_nav_menu WP_Term The classic navigation object to convert. 1247 * @return array the normalized parsed blocks. 1248 */ 1249 function block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ) { 1250 1251 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback_blocks' ); 1252 1253 // BEGIN: Code that already exists in wp_nav_menu(). 1254 $menu_items = wp_get_nav_menu_items( $classic_nav_menu->term_id, array( 'update_post_term_cache' => false ) ); 1255 1256 // Set up the $menu_item variables. 1257 _wp_menu_item_classes_by_context( $menu_items ); 1258 1259 $sorted_menu_items = array(); 1260 foreach ( (array) $menu_items as $menu_item ) { 1261 $sorted_menu_items[ $menu_item->menu_order ] = $menu_item; 1262 } 1263 1264 unset( $menu_items, $menu_item ); 1265 1266 // END: Code that already exists in wp_nav_menu(). 1267 1268 $menu_items_by_parent_id = array(); 1269 foreach ( $sorted_menu_items as $menu_item ) { 1270 $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item; 1271 } 1272 1273 $inner_blocks = block_core_navigation_parse_blocks_from_menu_items( 1274 isset( $menu_items_by_parent_id[0] ) 1275 ? $menu_items_by_parent_id[0] 1276 : array(), 1277 $menu_items_by_parent_id 1278 ); 1279 1280 return serialize_blocks( $inner_blocks ); 1281 } 1282 1283 /** 1284 * If there's a classic menu then use it as a fallback. 1285 * 1286 * @deprecated 6.3.0 Use WP_Navigation_Fallback::create_classic_menu_fallback() instead. 1287 * 1288 * @return array the normalized parsed blocks. 1289 */ 1290 function block_core_navigation_maybe_use_classic_menu_fallback() { 1291 1292 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::create_classic_menu_fallback' ); 1293 1294 // See if we have a classic menu. 1295 $classic_nav_menu = block_core_navigation_get_classic_menu_fallback(); 1296 1297 if ( ! $classic_nav_menu ) { 1298 return; 1299 } 1300 1301 // If we have a classic menu then convert it to blocks. 1302 $classic_nav_menu_blocks = block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ); 1303 1304 if ( empty( $classic_nav_menu_blocks ) ) { 1305 return; 1306 } 1307 1308 // Create a new navigation menu from the classic menu. 1309 $wp_insert_post_result = wp_insert_post( 1310 array( 1311 'post_content' => $classic_nav_menu_blocks, 1312 'post_title' => $classic_nav_menu->name, 1313 'post_name' => $classic_nav_menu->slug, 1314 'post_status' => 'publish', 1315 'post_type' => 'wp_navigation', 1316 ), 1317 true // So that we can check whether the result is an error. 1318 ); 1319 1320 if ( is_wp_error( $wp_insert_post_result ) ) { 1321 return; 1322 } 1323 1324 // Fetch the most recently published navigation which will be the classic one created above. 1325 return block_core_navigation_get_most_recently_published_navigation(); 1326 } 1327 1328 /** 1329 * Finds the most recently published `wp_navigation` Post. 1330 * 1331 * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_most_recently_published_navigation() instead. 1332 * 1333 * @return WP_Post|null the first non-empty Navigation or null. 1334 */ 1335 function block_core_navigation_get_most_recently_published_navigation() { 1336 1337 _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_most_recently_published_navigation' ); 1338 1339 // Default to the most recently created menu. 1340 $parsed_args = array( 1341 'post_type' => 'wp_navigation', 1342 'no_found_rows' => true, 1343 'update_post_meta_cache' => false, 1344 'update_post_term_cache' => false, 1345 'order' => 'DESC', 1346 'orderby' => 'date', 1347 'post_status' => 'publish', 1348 'posts_per_page' => 1, // get only the most recent. 1349 ); 1350 1351 $navigation_post = new WP_Query( $parsed_args ); 1352 if ( count( $navigation_post->posts ) > 0 ) { 1353 return $navigation_post->posts[0]; 1354 } 1355 1356 return null; 1357 } 1358 1359 /** 1360 * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks. 1361 * 1362 * @param string $serialized_block The serialized markup of a block and its inner blocks. 1363 * @return string 1364 */ 1365 function block_core_navigation_remove_serialized_parent_block( $serialized_block ) { 1366 $start = strpos( $serialized_block, '-->' ) + strlen( '-->' ); 1367 $end = strrpos( $serialized_block, '<!--' ); 1368 return substr( $serialized_block, $start, $end - $start ); 1369 } 1370 1371 /** 1372 * Mock a parsed block for the Navigation block given its inner blocks and the `wp_navigation` post object. 1373 * The `wp_navigation` post's `_wp_ignored_hooked_blocks` meta is queried to add the `metadata.ignoredHookedBlocks` attribute. 1374 * 1375 * @param array $inner_blocks Parsed inner blocks of a Navigation block. 1376 * @param WP_Post $post `wp_navigation` post object corresponding to the block. 1377 * 1378 * @return array the normalized parsed blocks. 1379 */ 1380 function block_core_navigation_mock_parsed_block( $inner_blocks, $post ) { 1381 $attributes = array(); 1382 1383 if ( isset( $post->ID ) ) { 1384 $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); 1385 if ( ! empty( $ignored_hooked_blocks ) ) { 1386 $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); 1387 $attributes['metadata'] = array( 1388 'ignoredHookedBlocks' => $ignored_hooked_blocks, 1389 ); 1390 } 1391 } 1392 1393 $mock_anchor_parent_block = array( 1394 'blockName' => 'core/navigation', 1395 'attrs' => $attributes, 1396 'innerBlocks' => $inner_blocks, 1397 'innerContent' => array_fill( 0, count( $inner_blocks ), null ), 1398 ); 1399 1400 return $mock_anchor_parent_block; 1401 } 1402 1403 /** 1404 * Insert hooked blocks into a Navigation block. 1405 * 1406 * Given a Navigation block's inner blocks and its corresponding `wp_navigation` post object, 1407 * this function inserts hooked blocks into it, and returns the serialized inner blocks in a 1408 * mock Navigation block wrapper. 1409 * 1410 * If there are any hooked blocks that need to be inserted as the Navigation block's first or last 1411 * children, the `wp_navigation` post's `_wp_ignored_hooked_blocks` meta is checked to see if any 1412 * of those hooked blocks should be exempted from insertion. 1413 * 1414 * @param array $inner_blocks Parsed inner blocks of a Navigation block. 1415 * @param WP_Post $post `wp_navigation` post object corresponding to the block. 1416 * @return string Serialized inner blocks in mock Navigation block wrapper, with hooked blocks inserted, if any. 1417 */ 1418 function block_core_navigation_insert_hooked_blocks( $inner_blocks, $post ) { 1419 $mock_navigation_block = block_core_navigation_mock_parsed_block( $inner_blocks, $post ); 1420 $hooked_blocks = get_hooked_blocks(); 1421 $before_block_visitor = null; 1422 $after_block_visitor = null; 1423 1424 if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) { 1425 $before_block_visitor = make_before_block_visitor( $hooked_blocks, $post, 'insert_hooked_blocks' ); 1426 $after_block_visitor = make_after_block_visitor( $hooked_blocks, $post, 'insert_hooked_blocks' ); 1427 } 1428 1429 return traverse_and_serialize_block( $mock_navigation_block, $before_block_visitor, $after_block_visitor ); 1430 } 1431 1432 /** 1433 * Insert ignoredHookedBlocks meta into the Navigation block and its inner blocks. 1434 * 1435 * Given a Navigation block's inner blocks and its corresponding `wp_navigation` post object, 1436 * this function inserts ignoredHookedBlocks meta into it, and returns the serialized inner blocks in a 1437 * mock Navigation block wrapper. 1438 * 1439 * @param array $inner_blocks Parsed inner blocks of a Navigation block. 1440 * @param WP_Post $post `wp_navigation` post object corresponding to the block. 1441 * @return string Serialized inner blocks in mock Navigation block wrapper, with hooked blocks inserted, if any. 1442 */ 1443 function block_core_navigation_set_ignored_hooked_blocks_metadata( $inner_blocks, $post ) { 1444 $mock_navigation_block = block_core_navigation_mock_parsed_block( $inner_blocks, $post ); 1445 $hooked_blocks = get_hooked_blocks(); 1446 $before_block_visitor = null; 1447 $after_block_visitor = null; 1448 1449 if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) { 1450 $before_block_visitor = make_before_block_visitor( $hooked_blocks, $post, 'set_ignored_hooked_blocks_metadata' ); 1451 $after_block_visitor = make_after_block_visitor( $hooked_blocks, $post, 'set_ignored_hooked_blocks_metadata' ); 1452 } 1453 1454 return traverse_and_serialize_block( $mock_navigation_block, $before_block_visitor, $after_block_visitor ); 1455 } 1456 1457 /** 1458 * Updates the post meta with the list of ignored hooked blocks when the navigation is created or updated via the REST API. 1459 * 1460 * @access private 1461 * @since 6.5.0 1462 * 1463 * @param stdClass $post Post object. 1464 * @return stdClass The updated post object. 1465 */ 1466 function block_core_navigation_update_ignore_hooked_blocks_meta( $post ) { 1467 /* 1468 * In this scenario the user has likely tried to create a navigation via the REST API. 1469 * In which case we won't have a post ID to work with and store meta against. 1470 */ 1471 if ( empty( $post->ID ) ) { 1472 return $post; 1473 } 1474 1475 /** 1476 * Skip meta generation when consumers intentionally update specific Navigation fields 1477 * and omit the content update. 1478 */ 1479 if ( ! isset( $post->post_content ) ) { 1480 return $post; 1481 } 1482 1483 /* 1484 * We run the Block Hooks mechanism to inject the `metadata.ignoredHookedBlocks` attribute into 1485 * all anchor blocks. For the root level, we create a mock Navigation and extract them from there. 1486 */ 1487 $blocks = parse_blocks( $post->post_content ); 1488 1489 /* 1490 * Block Hooks logic requires a `WP_Post` object (rather than the `stdClass` with the updates that 1491 * we're getting from the `rest_pre_insert_wp_navigation` filter) as its second argument (to be 1492 * used as context for hooked blocks insertion). 1493 * We thus have to look it up from the DB,based on `$post->ID`. 1494 */ 1495 $markup = block_core_navigation_set_ignored_hooked_blocks_metadata( $blocks, get_post( $post->ID ) ); 1496 1497 $root_nav_block = parse_blocks( $markup )[0]; 1498 $ignored_hooked_blocks = isset( $root_nav_block['attrs']['metadata']['ignoredHookedBlocks'] ) 1499 ? $root_nav_block['attrs']['metadata']['ignoredHookedBlocks'] 1500 : array(); 1501 1502 if ( ! empty( $ignored_hooked_blocks ) ) { 1503 $existing_ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); 1504 if ( ! empty( $existing_ignored_hooked_blocks ) ) { 1505 $existing_ignored_hooked_blocks = json_decode( $existing_ignored_hooked_blocks, true ); 1506 $ignored_hooked_blocks = array_unique( array_merge( $ignored_hooked_blocks, $existing_ignored_hooked_blocks ) ); 1507 } 1508 update_post_meta( $post->ID, '_wp_ignored_hooked_blocks', json_encode( $ignored_hooked_blocks ) ); 1509 } 1510 1511 $post->post_content = block_core_navigation_remove_serialized_parent_block( $markup ); 1512 return $post; 1513 } 1514 1515 /* 1516 * Before adding our filter, we verify if it's already added in Core. 1517 * However, during the build process, Gutenberg automatically prefixes our functions with "gutenberg_". 1518 * Therefore, we concatenate the Core's function name to circumvent this prefix for our check. 1519 */ 1520 $rest_insert_wp_navigation_core_callback = 'block_core_navigation_' . 'update_ignore_hooked_blocks_meta'; // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found 1521 1522 /* 1523 * Injection of hooked blocks into the Navigation block relies on some functions present in WP >= 6.5 1524 * that are not present in Gutenberg's WP 6.5 compatibility layer. 1525 */ 1526 if ( function_exists( 'set_ignored_hooked_blocks_metadata' ) && ! has_filter( 'rest_pre_insert_wp_navigation', $rest_insert_wp_navigation_core_callback ) ) { 1527 add_filter( 'rest_pre_insert_wp_navigation', 'block_core_navigation_update_ignore_hooked_blocks_meta' ); 1528 } 1529 1530 /* 1531 * Previous versions of Gutenberg were attaching the block_core_navigation_update_ignore_hooked_blocks_meta 1532 * function to the `rest_insert_wp_navigation` _action_ (rather than the `rest_pre_insert_wp_navigation` _filter_). 1533 * To avoid collisions, we need to remove the filter from that action if it's present. 1534 */ 1535 if ( has_filter( 'rest_insert_wp_navigation', $rest_insert_wp_navigation_core_callback ) ) { 1536 remove_filter( 'rest_insert_wp_navigation', $rest_insert_wp_navigation_core_callback ); 1537 } 1538 1539 /** 1540 * Hooks into the REST API response for the core/navigation block and adds the first and last inner blocks. 1541 * 1542 * @param WP_REST_Response $response The response object. 1543 * @param WP_Post $post Post object. 1544 * @return WP_REST_Response The response object. 1545 */ 1546 function block_core_navigation_insert_hooked_blocks_into_rest_response( $response, $post ) { 1547 if ( ! isset( $response->data['content']['raw'] ) || ! isset( $response->data['content']['rendered'] ) ) { 1548 return $response; 1549 } 1550 $parsed_blocks = parse_blocks( $response->data['content']['raw'] ); 1551 $content = block_core_navigation_insert_hooked_blocks( $parsed_blocks, $post ); 1552 1553 // Remove mock Navigation block wrapper. 1554 $content = block_core_navigation_remove_serialized_parent_block( $content ); 1555 1556 $response->data['content']['raw'] = $content; 1557 $response->data['content']['rendered'] = apply_filters( 'the_content', $content ); 1558 1559 return $response; 1560 } 1561 1562 /* 1563 * Before adding our filter, we verify if it's already added in Core. 1564 * However, during the build process, Gutenberg automatically prefixes our functions with "gutenberg_". 1565 * Therefore, we concatenate the Core's function name to circumvent this prefix for our check. 1566 */ 1567 $rest_prepare_wp_navigation_core_callback = 'block_core_navigation_' . 'insert_hooked_blocks_into_rest_response'; 1568 1569 /* 1570 * Injection of hooked blocks into the Navigation block relies on some functions present in WP >= 6.5 1571 * that are not present in Gutenberg's WP 6.5 compatibility layer. 1572 */ 1573 if ( function_exists( 'set_ignored_hooked_blocks_metadata' ) && ! has_filter( 'rest_prepare_wp_navigation', $rest_prepare_wp_navigation_core_callback ) ) { 1574 add_filter( 'rest_prepare_wp_navigation', 'block_core_navigation_insert_hooked_blocks_into_rest_response', 10, 3 ); 1575 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Tue Apr 23 08:20:01 2024 | Cross-referenced by PHPXref |