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

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