PluginProbe
Gutenberg / 23.7.2
Gutenberg v23.7.2
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / build / scripts / block-library / navigation-link.php

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

514 lines 16.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Server-side registering and rendering of the `core/navigation-link` block.
4 *
5 * @package WordPress
6 */
7
8 require_once __DIR__ . '/navigation-link/shared/item-should-render.php';
9 require_once __DIR__ . '/navigation-link/shared/render-submenu-icon.php';
10
11 /**
12 * Build an array with CSS classes and inline styles defining the colors
13 * which will be applied to the navigation markup in the front-end.
14 *
15 * @since 5.9.0
16 *
17 * @param array $context Navigation block context.
18 * @param array $attributes Block attributes.
19 * @param bool $is_sub_menu Whether the link is part of a sub-menu. Default false.
20 * @return array Colors CSS classes and inline styles.
21 */
22 function gutenberg_block_core_navigation_link_build_css_colors( $context, $attributes, $is_sub_menu = false ) {
23 $colors = array(
24 'css_classes' => array(),
25 'inline_styles' => '',
26 );
27
28 // Text color.
29 $named_text_color = null;
30 $custom_text_color = null;
31
32 if ( $is_sub_menu && array_key_exists( 'customOverlayTextColor', $context ) ) {
33 $custom_text_color = $context['customOverlayTextColor'];
34 } elseif ( $is_sub_menu && array_key_exists( 'overlayTextColor', $context ) ) {
35 $named_text_color = $context['overlayTextColor'];
36 } elseif ( array_key_exists( 'customTextColor', $context ) ) {
37 $custom_text_color = $context['customTextColor'];
38 } elseif ( array_key_exists( 'textColor', $context ) ) {
39 $named_text_color = $context['textColor'];
40 } elseif ( isset( $context['style']['color']['text'] ) ) {
41 $custom_text_color = $context['style']['color']['text'];
42 }
43
44 // If has text color.
45 if ( ! is_null( $named_text_color ) ) {
46 // Add the color class.
47 array_push( $colors['css_classes'], 'has-text-color', sprintf( 'has-%s-color', $named_text_color ) );
48 } elseif ( ! is_null( $custom_text_color ) ) {
49 // Add the custom color inline style.
50 $colors['css_classes'][] = 'has-text-color';
51 $colors['inline_styles'] .= sprintf( 'color: %s;', $custom_text_color );
52 }
53
54 // Background color.
55 $named_background_color = null;
56 $custom_background_color = null;
57
58 if ( $is_sub_menu && array_key_exists( 'customOverlayBackgroundColor', $context ) ) {
59 $custom_background_color = $context['customOverlayBackgroundColor'];
60 } elseif ( $is_sub_menu && array_key_exists( 'overlayBackgroundColor', $context ) ) {
61 $named_background_color = $context['overlayBackgroundColor'];
62 } elseif ( array_key_exists( 'customBackgroundColor', $context ) ) {
63 $custom_background_color = $context['customBackgroundColor'];
64 } elseif ( array_key_exists( 'backgroundColor', $context ) ) {
65 $named_background_color = $context['backgroundColor'];
66 } elseif ( isset( $context['style']['color']['background'] ) ) {
67 $custom_background_color = $context['style']['color']['background'];
68 }
69
70 // If has background color.
71 if ( ! is_null( $named_background_color ) ) {
72 // Add the background-color class.
73 array_push( $colors['css_classes'], 'has-background', sprintf( 'has-%s-background-color', $named_background_color ) );
74 } elseif ( ! is_null( $custom_background_color ) ) {
75 // Add the custom background-color inline style.
76 $colors['css_classes'][] = 'has-background';
77 $colors['inline_styles'] .= sprintf( 'background-color: %s;', $custom_background_color );
78 }
79
80 return $colors;
81 }
82
83 /**
84 * Build an array with CSS classes and inline styles defining the font sizes
85 * which will be applied to the navigation markup in the front-end.
86 *
87 * This function is no longer used internally and is kept only for backward
88 * compatibility with third-party code that may call it directly.
89 *
90 * @since 5.9.0
91 * @deprecated 7.0.0
92 *
93 * @param array $context Navigation block context.
94 * @return array Font size CSS classes and inline styles.
95 */
96 function gutenberg_block_core_navigation_link_build_css_font_sizes( $context ) {
97 _deprecated_function( __FUNCTION__, '7.0.0' );
98
99 // CSS classes.
100 $font_sizes = array(
101 'css_classes' => array(),
102 'inline_styles' => '',
103 );
104
105 $has_named_font_size = array_key_exists( 'fontSize', $context );
106 $has_custom_font_size = isset( $context['style']['typography']['fontSize'] );
107
108 if ( $has_named_font_size ) {
109 // Add the font size class.
110 $font_sizes['css_classes'][] = sprintf( 'has-%s-font-size', $context['fontSize'] );
111 } elseif ( $has_custom_font_size ) {
112 // Add the custom font size inline style.
113 $font_sizes['inline_styles'] = sprintf(
114 'font-size: %s;',
115 gutenberg_get_typography_font_size_value(
116 array(
117 'size' => $context['style']['typography']['fontSize'],
118 )
119 )
120 );
121 }
122
123 return $font_sizes;
124 }
125
126 /**
127 * Returns the top-level submenu SVG chevron icon.
128 *
129 * @since 5.9.0
130 * @deprecated 7.0.0 Use block_core_shared_navigation_render_submenu_icon() instead.
131 *
132 * @return string
133 */
134 function gutenberg_block_core_navigation_link_render_submenu_icon() {
135 _deprecated_function(
136 __FUNCTION__,
137 '7.0.0',
138 'block_core_shared_navigation_render_submenu_icon()'
139 );
140 return block_core_shared_navigation_render_submenu_icon();
141 }
142
143 /**
144 * Decodes a url if it's encoded, returning the same url if not.
145 *
146 * @since 6.2.0
147 *
148 * @param string $url The url to decode.
149 *
150 * @return string $url Returns the decoded url.
151 */
152 function gutenberg_block_core_navigation_link_maybe_urldecode( $url ) {
153 if ( ! is_string( $url ) ) {
154 return '';
155 }
156
157 $is_url_encoded = false;
158 $query = parse_url( $url, PHP_URL_QUERY );
159 $query_params = wp_parse_args( $query );
160
161 foreach ( $query_params as $query_param ) {
162 $can_query_param_be_encoded = is_string( $query_param ) && ! empty( $query_param );
163 if ( ! $can_query_param_be_encoded ) {
164 continue;
165 }
166 if ( rawurldecode( $query_param ) !== $query_param ) {
167 $is_url_encoded = true;
168 break;
169 }
170 }
171
172 if ( $is_url_encoded ) {
173 return rawurldecode( $url );
174 }
175
176 return $url;
177 }
178
179
180 /**
181 * Renders the `core/navigation-link` block.
182 *
183 * @since 5.9.0
184 *
185 * @param array $attributes The block attributes.
186 * @param string $content The saved content.
187 * @param WP_Block $block The parsed block.
188 *
189 * @return string Returns the post content with the legacy widget added.
190 */
191 function gutenberg_render_block_core_navigation_link( $attributes, $content, $block ) {
192 // Check if this navigation item should render based on post status.
193 if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
194 $should_render = gutenberg_block_core_shared_navigation_item_should_render( $attributes, $block );
195 } else {
196 $should_render = block_core_shared_navigation_item_should_render( $attributes, $block );
197 }
198 if ( ! $should_render ) {
199 return '';
200 }
201
202 // Don't render the block's subtree if it has no label.
203 if ( empty( $attributes['label'] ) ) {
204 return '';
205 }
206
207 $classes = array();
208
209 // Render inner blocks first to check if any menu items will actually display.
210 $inner_blocks_html = '';
211 foreach ( $block->inner_blocks as $inner_block ) {
212 $inner_blocks_html .= $inner_block->render();
213 }
214 $has_submenu = ! empty( trim( $inner_blocks_html ) );
215
216 $css_classes = trim( implode( ' ', $classes ) );
217 $kind = empty( $attributes['kind'] ) ? 'post_type' : str_replace( '-', '_', $attributes['kind'] );
218 $is_active = ! empty( $attributes['id'] ) && get_queried_object_id() === (int) $attributes['id'] && ! empty( get_queried_object()->$kind );
219
220 if ( is_post_type_archive() && ! empty( $attributes['url'] ) ) {
221 $queried_archive_link = get_post_type_archive_link( get_queried_object()->name );
222 if ( $attributes['url'] === $queried_archive_link ) {
223 $is_active = true;
224 }
225 }
226
227 $wrapper_attributes = get_block_wrapper_attributes(
228 array(
229 'class' => $css_classes . ' wp-block-navigation-item' . ( $has_submenu ? ' has-child' : '' ) .
230 ( $is_active ? ' current-menu-item' : '' ),
231 )
232 );
233 $html = '<li ' . $wrapper_attributes . '>' .
234 '<a class="wp-block-navigation-item__content" ';
235
236 // Start appending HTML attributes to anchor tag.
237 if ( isset( $attributes['url'] ) ) {
238 $html .= ' href="' . esc_url( gutenberg_block_core_navigation_link_maybe_urldecode( $attributes['url'] ) ) . '"';
239 }
240
241 if ( $is_active ) {
242 $html .= ' aria-current="page"';
243 }
244
245 if ( isset( $attributes['opensInNewTab'] ) && true === $attributes['opensInNewTab'] ) {
246 $html .= ' target="_blank" ';
247 }
248
249 if ( isset( $attributes['rel'] ) ) {
250 $html .= ' rel="' . esc_attr( $attributes['rel'] ) . '"';
251 } elseif ( isset( $attributes['nofollow'] ) && $attributes['nofollow'] ) {
252 $html .= ' rel="nofollow"';
253 }
254
255 if ( isset( $attributes['title'] ) ) {
256 $html .= ' title="' . esc_attr( $attributes['title'] ) . '"';
257 }
258
259 // End appending HTML attributes to anchor tag.
260
261 // Start anchor tag content.
262 $html .= '>' .
263 // Wrap title with span to isolate it from submenu icon.
264 '<span class="wp-block-navigation-item__label">';
265
266 if ( isset( $attributes['label'] ) ) {
267 $html .= wp_kses_post( $attributes['label'] );
268 }
269
270 $html .= '</span>';
271
272 // Add description if available.
273 if ( ! empty( $attributes['description'] ) ) {
274 $html .= '<span class="wp-block-navigation-item__description">';
275 $html .= wp_kses_post( $attributes['description'] );
276 $html .= '</span>';
277 }
278
279 $html .= '</a>';
280 // End anchor tag content.
281
282 if ( isset( $block->context['showSubmenuIcon'] ) && $block->context['showSubmenuIcon'] && $has_submenu ) {
283 // The submenu icon can be hidden by a CSS rule on the Navigation Block.
284 $html .= '<span class="wp-block-navigation__submenu-icon">';
285 if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
286 $html .= gutenberg_block_core_shared_navigation_render_submenu_icon();
287 } else {
288 $html .= block_core_shared_navigation_render_submenu_icon();
289 }
290 $html .= '</span>';
291 }
292
293 if ( $has_submenu ) {
294 $html .= sprintf(
295 '<ul class="wp-block-navigation__submenu-container">%s</ul>',
296 $inner_blocks_html
297 );
298 }
299
300 $html .= '</li>';
301
302 return $html;
303 }
304
305 /**
306 * Returns a navigation link variation
307 *
308 * @since 5.9.0
309 *
310 * @param WP_Taxonomy|WP_Post_Type $entity post type or taxonomy entity.
311 * @param string $kind string of value 'taxonomy' or 'post-type'.
312 *
313 * @return array
314 */
315 function gutenberg_build_variation_for_navigation_link( $entity, $kind ) {
316 $title = '';
317 $description = '';
318
319 // Get default labels based on entity type
320 $default_labels = null;
321 if ( $entity instanceof WP_Post_Type ) {
322 $default_labels = WP_Post_Type::get_default_labels();
323 } elseif ( $entity instanceof WP_Taxonomy ) {
324 $default_labels = WP_Taxonomy::get_default_labels();
325 }
326
327 // Get title and check if it's default
328 $is_default_title = false;
329 if ( property_exists( $entity->labels, 'item_link' ) ) {
330 $title = $entity->labels->item_link;
331 if ( isset( $default_labels['item_link'] ) ) {
332 $is_default_title = in_array( $title, $default_labels['item_link'], true );
333 }
334 }
335
336 // Get description and check if it's default
337 $is_default_description = false;
338 if ( property_exists( $entity->labels, 'item_link_description' ) ) {
339 $description = $entity->labels->item_link_description;
340 if ( isset( $default_labels['item_link_description'] ) ) {
341 $is_default_description = in_array( $description, $default_labels['item_link_description'], true );
342 }
343 }
344
345 // Calculate singular name once (used for both title and description)
346 $singular = $entity->labels->singular_name ?? ucfirst( $entity->name );
347
348 // Set default title if needed
349 if ( $is_default_title || '' === $title ) {
350 /* translators: %s: Singular label of the entity. */
351 $title = sprintf( __( '%s link' ), $singular );
352 }
353
354 // Default description if needed.
355 // Use a single space character instead of an empty string to prevent fallback to the
356 // block.json default description ("Add a page, link, or another item to your navigation.").
357 // An empty string would be treated as missing and trigger the fallback, while a single
358 // space appears blank in the UI but prevents the fallback behavior.
359 // We avoid generating descriptions like "A link to a %s" to prevent grammatical errors
360 // (e.g., "A link to a event" should be "A link to an event").
361 if ( $is_default_description || '' === $description ) {
362 $description = ' ';
363 }
364
365 $variation = array(
366 'name' => $entity->name,
367 'title' => $title,
368 'description' => $description,
369 'attributes' => array(
370 'type' => $entity->name,
371 'kind' => $kind,
372 ),
373 );
374
375 // Tweak some value for the variations.
376 $variation_overrides = array(
377 'post_tag' => array(
378 'name' => 'tag',
379 'attributes' => array(
380 'type' => 'tag',
381 'kind' => $kind,
382 ),
383 ),
384 'post_format' => array(
385 // The item_link and item_link_description for post formats is the
386 // same as for tags, so need to be overridden.
387 'title' => __( 'Post Format Link' ),
388 'description' => __( 'A link to a post format' ),
389 'attributes' => array(
390 'type' => 'post_format',
391 'kind' => $kind,
392 ),
393 ),
394 );
395
396 if ( array_key_exists( $entity->name, $variation_overrides ) ) {
397 $variation = array_merge(
398 $variation,
399 $variation_overrides[ $entity->name ]
400 );
401 }
402
403 return $variation;
404 }
405
406 /**
407 * Filters the registered variations for a block type.
408 * Returns the dynamically built variations for all post-types and taxonomies.
409 *
410 * @since 6.5.0
411 *
412 * @param array $variations Array of registered variations for a block type.
413 * @param WP_Block_Type $block_type The full block type object.
414 * @return array Numerically indexed array of block variations.
415 */
416 function gutenberg_block_core_navigation_link_filter_variations( $variations, $block_type ) {
417 if ( 'core/navigation-link' !== $block_type->name ) {
418 return $variations;
419 }
420
421 $generated_variations = gutenberg_block_core_navigation_link_build_variations();
422
423 /*
424 * IMPORTANT: Order matters for deduplication.
425 *
426 * The variations returned from this filter are bootstrapped to JavaScript and
427 * processed by the block variations reducer. The reducer uses `getUniqueItemsByName()`
428 * (packages/blocks/src/store/reducer.js:51-57) which keeps the FIRST variation with
429 * a given 'name' and discards later duplicates when processing the array in order.
430 *
431 * By placing generated variations first in `array_merge()`, the improved
432 * labels (e.g., "Product link" instead of generic "Post Link") are processed first
433 * and preserved. The generic incoming variations are then discarded as duplicates.
434 *
435 * Why `array_merge()` instead of manual deduplication?
436 * - Both arrays use numeric indices (0, 1, 2...), so `array_merge()` concatenates
437 * and re-indexes them sequentially, preserving order
438 * - The reducer handles deduplication, so it is not needed here
439 * - This keeps the PHP code simple and relies on the established JavaScript behavior
440 *
441 * See: https://github.com/WordPress/gutenberg/pull/72517
442 */
443 return array_merge( $generated_variations, $variations );
444 }
445
446 /**
447 * Returns an array of variations for the navigation link block.
448 *
449 * @since 6.5.0
450 *
451 * @return array
452 */
453 function gutenberg_block_core_navigation_link_build_variations() {
454 $post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
455 $taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' );
456
457 /*
458 * Use two separate arrays as a way to order the variations in the UI.
459 * Known variations (like Post Link and Page Link) are added to the
460 * `built_ins` array. Variations for custom post types and taxonomies are
461 * added to the `variations` array and will always appear after `built-ins.
462 */
463 $built_ins = array();
464 $variations = array();
465
466 if ( $post_types ) {
467 foreach ( $post_types as $post_type ) {
468 $variation = gutenberg_build_variation_for_navigation_link( $post_type, 'post-type' );
469 if ( $post_type->_builtin ) {
470 $built_ins[] = $variation;
471 } else {
472 $variations[] = $variation;
473 }
474 }
475 }
476 if ( $taxonomies ) {
477 foreach ( $taxonomies as $taxonomy ) {
478 $variation = gutenberg_build_variation_for_navigation_link( $taxonomy, 'taxonomy' );
479 if ( $taxonomy->_builtin ) {
480 $built_ins[] = $variation;
481 } else {
482 $variations[] = $variation;
483 }
484 }
485 }
486
487 $all_variations = array_merge( $built_ins, $variations );
488
489 return $all_variations;
490 }
491
492 /**
493 * Registers the navigation link block.
494 *
495 * @since 5.9.0
496 *
497 * @uses gutenberg_render_block_core_navigation_link()
498 * @throws WP_Error An WP_Error exception parsing the block definition.
499 */
500 function gutenberg_register_block_core_navigation_link() {
501 register_block_type_from_metadata(
502 __DIR__ . '/navigation-link',
503 array(
504 'render_callback' => 'gutenberg_render_block_core_navigation_link',
505 )
506 );
507 }
508 add_action( 'init', 'gutenberg_register_block_core_navigation_link', 20 );
509 /**
510 * Creates all variations for post types / taxonomies dynamically (= each time when variations are requested).
511 * Do not use variation_callback, to also account for unregistering post types/taxonomies later on.
512 */
513 add_filter( 'get_block_type_variations', 'gutenberg_block_core_navigation_link_filter_variations', 10, 2 );
514