PluginProbe
Gutenberg / 23.5.0
Gutenberg v23.5.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 23.5.0, at build/scripts/block-library/navigation-link.php

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