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

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

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