PluginProbe
Gutenberg / 23.5.3
Gutenberg v23.5.3
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / build / scripts / block-library / navigation.php

navigation.php in Gutenberg 23.5.3, at build/scripts/block-library/navigation.php

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