PluginProbe
Gutenberg / 23.7.2
Gutenberg v23.7.2
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.7.2, at build/scripts/block-library/navigation.php

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