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

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