PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.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 All 404 releases
gutenberg / build / scripts / block-library / navigation.php

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

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