PluginProbe
Gutenberg / 18.9.0
Gutenberg v18.9.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 12.6.0 All 403 releases
gutenberg / build / block-library / blocks / navigation.php

navigation.php in Gutenberg 18.9.0, at build/block-library/blocks/navigation.php

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