PluginProbe
Gutenberg / 22.9.0
Gutenberg v22.9.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 22.9.0, at build/scripts/block-library/navigation.php

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