PluginProbe
Gutenberg / 12.1.0
Gutenberg v12.1.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 / lib / navigation.php

navigation.php in Gutenberg 12.1.0, at lib/navigation.php

452 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Functions used in making nav menus interopable with block editors.
4 *
5 * @package gutenberg
6 */
7
8 /**
9 * Registers block editor 'wp_navigation' post type.
10 */
11 function gutenberg_register_navigation_post_type() {
12 $labels = array(
13 'name' => __( 'Navigation Menus', 'gutenberg' ),
14 'singular_name' => __( 'Navigation Menu', 'gutenberg' ),
15 'menu_name' => _x( 'Navigation Menus', 'Admin Menu text', 'gutenberg' ),
16 'add_new' => _x( 'Add New', 'Navigation Menu', 'gutenberg' ),
17 'add_new_item' => __( 'Add New Navigation Menu', 'gutenberg' ),
18 'new_item' => __( 'New Navigation Menu', 'gutenberg' ),
19 'edit_item' => __( 'Edit Navigation Menu', 'gutenberg' ),
20 'view_item' => __( 'View Navigation Menu', 'gutenberg' ),
21 'all_items' => __( 'All Navigation Menus', 'gutenberg' ),
22 'search_items' => __( 'Search Navigation Menus', 'gutenberg' ),
23 'parent_item_colon' => __( 'Parent Navigation Menu:', 'gutenberg' ),
24 'not_found' => __( 'No Navigation Menu found.', 'gutenberg' ),
25 'not_found_in_trash' => __( 'No Navigation Menu found in Trash.', 'gutenberg' ),
26 'archives' => __( 'Navigation Menu archives', 'gutenberg' ),
27 'insert_into_item' => __( 'Insert into Navigation Menu', 'gutenberg' ),
28 'uploaded_to_this_item' => __( 'Uploaded to this Navigation Menu', 'gutenberg' ),
29 // Some of these are a bit weird, what are they for?
30 'filter_items_list' => __( 'Filter Navigation Menu list', 'gutenberg' ),
31 'items_list_navigation' => __( 'Navigation Menus list navigation', 'gutenberg' ),
32 'items_list' => __( 'Navigation Menus list', 'gutenberg' ),
33 );
34
35 $args = array(
36 'labels' => $labels,
37 'description' => __( 'Navigation menus.', 'gutenberg' ),
38 'public' => false,
39 'has_archive' => false,
40 // We should disable UI for non-FSE themes.
41 'show_ui' => gutenberg_is_fse_theme(),
42 'show_in_menu' => 'themes.php',
43 'show_in_admin_bar' => false,
44 'show_in_rest' => true,
45 'map_meta_cap' => true,
46 'rest_base' => 'navigation',
47 'rest_controller_class' => WP_REST_Posts_Controller::class,
48 'supports' => array(
49 'title',
50 'editor',
51 'revisions',
52 ),
53 );
54
55 register_post_type( 'wp_navigation', $args );
56 }
57 add_action( 'init', 'gutenberg_register_navigation_post_type' );
58
59 /**
60 * Disable "Post Attributes" for wp_navigation post type.
61 *
62 * The attributes are also conditionally enabled when a site has custom templates.
63 * Block Theme templates can be available for every post type.
64 */
65 add_filter( 'theme_wp_navigation_templates', '__return_empty_array' );
66
67 /**
68 * Disable block editor for wp_navigation type posts so they can be managed via the UI.
69 *
70 * @param bool $value Whether the CPT supports block editor or not.
71 * @param string $post_type Post type.
72 *
73 * @return bool
74 */
75 function gutenberg_disable_block_editor_for_navigation_post_type( $value, $post_type ) {
76 if ( 'wp_navigation' === $post_type ) {
77 return false;
78 }
79
80 return $value;
81 }
82
83 add_filter( 'use_block_editor_for_post_type', 'gutenberg_disable_block_editor_for_navigation_post_type', 10, 2 );
84
85 /**
86 * This callback disables the content editor for wp_navigation type posts.
87 * Content editor cannot handle wp_navigation type posts correctly.
88 * We cannot disable the "editor" feature in the wp_navigation's CPT definition
89 * because it disables the ability to save navigation blocks via REST API.
90 *
91 * @param WP_Post $post An instance of WP_Post class.
92 */
93 function gutenberg_disable_content_editor_for_navigation_post_type( $post ) {
94 $post_type = get_post_type( $post );
95 if ( 'wp_navigation' !== $post_type ) {
96 return;
97 }
98
99 remove_post_type_support( $post_type, 'editor' );
100 }
101
102 add_action( 'edit_form_after_title', 'gutenberg_disable_content_editor_for_navigation_post_type', 10, 1 );
103
104 /**
105 * This callback enables content editor for wp_navigation type posts.
106 * We need to enable it back because we disable it to hide
107 * the content editor for wp_navigation type posts.
108 *
109 * @see gutenberg_disable_content_editor_for_navigation_post_type
110 *
111 * @param WP_Post $post An instance of WP_Post class.
112 */
113 function gutenberg_enable_content_editor_for_navigation_post_type( $post ) {
114 $post_type = get_post_type( $post );
115 if ( 'wp_navigation' !== $post_type ) {
116 return;
117 }
118
119 add_post_type_support( $post_type, 'editor' );
120 }
121
122 add_action( 'edit_form_after_editor', 'gutenberg_enable_content_editor_for_navigation_post_type', 10, 1 );
123
124 /**
125 * Rename the menu title from "All Navigation Menus" to "Navigation Menus".
126 */
127 function gutenberg_rename_navigation_post_type_admin_menu_entry() {
128 global $submenu;
129 if ( ! isset( $submenu['themes.php'] ) ) {
130 return;
131 }
132
133 $post_type = get_post_type_object( 'wp_navigation' );
134 if ( ! $post_type ) {
135 return;
136 }
137
138 $menu_title_index = 0;
139 foreach ( $submenu['themes.php'] as $key => $menu_item ) {
140 if ( $post_type->labels->all_items === $menu_item[ $menu_title_index ] ) {
141 $submenu['themes.php'][ $key ][ $menu_title_index ] = $post_type->labels->menu_name; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
142 return;
143 }
144 }
145 }
146
147 add_action( 'admin_menu', 'gutenberg_rename_navigation_post_type_admin_menu_entry' );
148
149 /**
150 * Registers the navigation areas supported by the current theme. The expected
151 * shape of the argument is:
152 * array(
153 * 'primary' => 'Primary',
154 * 'secondary' => 'Secondary',
155 * 'tertiary' => 'Tertiary',
156 * )
157 *
158 * @param array $new_areas Supported navigation areas.
159 */
160 function gutenberg_register_navigation_areas( $new_areas ) {
161 global $gutenberg_navigation_areas;
162 $gutenberg_navigation_areas = $new_areas;
163 }
164
165 // Register the default navigation areas.
166 gutenberg_register_navigation_areas(
167 array(
168 'primary' => 'Primary',
169 'secondary' => 'Secondary',
170 'tertiary' => 'Tertiary',
171 )
172 );
173
174 /**
175 * Returns the available navigation areas.
176 *
177 * @return array Registered navigation areas.
178 */
179 function gutenberg_get_navigation_areas() {
180 global $gutenberg_navigation_areas;
181 return $gutenberg_navigation_areas;
182 }
183
184 /**
185 * Returns the API paths to preload to make the navigation area block load fast.
186 *
187 * @return array A list of paths.
188 */
189 function gutenberg_get_navigation_areas_paths_to_preload() {
190 $areas = gutenberg_get_navigation_areas_menus();
191 $active_areas = array_intersect_key( $areas, gutenberg_get_navigation_areas() );
192 $paths = array(
193 '/wp/v2/block-navigation-areas?context=edit',
194 );
195 foreach ( $active_areas as $post_id ) {
196 if ( 0 !== $post_id ) {
197 $paths[] = "/wp/v2/navigation/$post_id?context=edit";
198 }
199 }
200 return $paths;
201 }
202
203 /**
204 * Migrates classic menus to a block-based navigation post on theme switch.
205 * Assigns the created navigation post to the corresponding navigation area.
206 *
207 * @param string $new_name Name of the new theme.
208 * @param WP_Theme $new_theme New theme.
209 * @param WP_Theme $old_theme Old theme.
210 * @see switch_theme WordPress action.
211 */
212 function gutenberg_migrate_menu_to_navigation_post( $new_name, $new_theme, $old_theme ) {
213 // Do nothing when switching to a theme that does not support site editor.
214 if ( ! gutenberg_experimental_is_site_editor_available() ) {
215 return;
216 }
217
218 // get_nav_menu_locations() calls get_theme_mod() which depends on the stylesheet option.
219 // At the same time, switch_theme runs only after the stylesheet option was updated to $new_theme.
220 // To retrieve theme mods of the old theme, the getter is hooked to get_option( 'stylesheet' ) so that we
221 // get the old theme, which causes the get_nav_menu_locations to get the locations of the old theme.
222 $get_old_theme_stylesheet = function() use ( $old_theme ) {
223 return $old_theme->get_stylesheet();
224 };
225 add_filter( 'option_stylesheet', $get_old_theme_stylesheet );
226
227 $locations = get_nav_menu_locations();
228 $area_mapping = gutenberg_get_navigation_areas_menus();
229
230 foreach ( $locations as $location_name => $menu_id ) {
231 // Get the menu from the location, skipping if there is no
232 // menu or there was an error.
233 $menu = wp_get_nav_menu_object( $menu_id );
234 if ( ! $menu || is_wp_error( $menu ) ) {
235 continue;
236 }
237
238 $menu_items = gutenberg_get_menu_items_at_location( $location_name );
239 if ( empty( $menu_items ) ) {
240 continue;
241 }
242
243 $post_name = 'classic_menu_' . $menu_id;
244 $post_status = 'publish';
245
246 // Get or create to avoid creating too many wp_navigation posts.
247 $query = new WP_Query;
248 $matching_posts = $query->query(
249 array(
250 'name' => $post_name,
251 'post_status' => $post_status,
252 'post_type' => 'wp_navigation',
253 'posts_per_page' => 1,
254 )
255 );
256
257 if ( count( $matching_posts ) ) {
258 $navigation_post_id = $matching_posts[0]->ID;
259 } else {
260 $menu_items_by_parent_id = gutenberg_sort_menu_items_by_parent_id( $menu_items );
261 $parsed_blocks = gutenberg_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id );
262 $post_data = array(
263 'post_type' => 'wp_navigation',
264 'post_title' => sprintf(
265 /* translators: %s: the name of the menu, e.g. "Main Menu". */
266 __( 'Classic menu: %s', 'gutenberg' ),
267 $menu->name
268 ),
269 'post_name' => $post_name,
270 'post_content' => serialize_blocks( $parsed_blocks ),
271 'post_status' => $post_status,
272 );
273 $navigation_post_id = wp_insert_post( $post_data, true );
274 // If wp_insert_post fails *at any time*, then bale out of the entire
275 // migration attempt returning the WP_Error object.
276 if ( is_wp_error( $navigation_post_id ) ) {
277 return $navigation_post_id;
278 }
279 }
280
281 $area_mapping[ $location_name ] = $navigation_post_id;
282 }
283 remove_filter( 'option_stylesheet', $get_old_theme_stylesheet );
284
285 update_option( 'wp_navigation_areas', $area_mapping );
286 }
287
288 add_action( 'switch_theme', 'gutenberg_migrate_menu_to_navigation_post', 99, 3 );
289
290 /**
291 * Retrieves navigation areas.
292 *
293 * @return array Navigation areas.
294 */
295 function gutenberg_get_navigation_areas_menus() {
296 $areas = get_option( 'wp_navigation_areas', array() );
297 if ( ! $areas ) {
298 // Original key used `fse` prefix but Core options should use `wp`.
299 // We fallback to the legacy option to catch sites with values in the
300 // original location.
301 $legacy_option_key = 'fse_navigation_areas';
302 $areas = get_option( $legacy_option_key, array() );
303 }
304 return $areas;
305 }
306
307 // The functions below are copied over from packages/block-library/src/navigation/index.php
308 // Let's figure out a better way of managing these global PHP dependencies.
309
310 /**
311 * Returns the menu items for a WordPress menu location.
312 *
313 * @param string $location The menu location.
314 * @return array Menu items for the location.
315 */
316 function gutenberg_get_menu_items_at_location( $location ) {
317 if ( empty( $location ) ) {
318 return;
319 }
320
321 // Build menu data. The following approximates the code in
322 // `wp_nav_menu()` and `gutenberg_output_block_nav_menu`.
323
324 // Find the location in the list of locations, returning early if the
325 // location can't be found.
326 $locations = get_nav_menu_locations();
327 if ( ! isset( $locations[ $location ] ) ) {
328 return;
329 }
330
331 // Get the menu from the location, returning early if there is no
332 // menu or there was an error.
333 $menu = wp_get_nav_menu_object( $locations[ $location ] );
334 if ( ! $menu || is_wp_error( $menu ) ) {
335 return;
336 }
337
338 $menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) );
339 _wp_menu_item_classes_by_context( $menu_items );
340
341 return $menu_items;
342 }
343
344
345 /**
346 * Sorts a standard array of menu items into a nested structure keyed by the
347 * id of the parent menu.
348 *
349 * @param array $menu_items Menu items to sort.
350 * @return array An array keyed by the id of the parent menu where each element
351 * is an array of menu items that belong to that parent.
352 */
353 function gutenberg_sort_menu_items_by_parent_id( $menu_items ) {
354 $sorted_menu_items = array();
355 foreach ( (array) $menu_items as $menu_item ) {
356 $sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
357 }
358 unset( $menu_items, $menu_item );
359
360 $menu_items_by_parent_id = array();
361 foreach ( $sorted_menu_items as $menu_item ) {
362 $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item;
363 }
364
365 return $menu_items_by_parent_id;
366 }
367
368 /**
369 * Turns menu item data into a nested array of parsed blocks
370 *
371 * @param array $menu_items An array of menu items that represent
372 * an individual level of a menu.
373 * @param array $menu_items_by_parent_id An array keyed by the id of the
374 * parent menu where each element is an
375 * array of menu items that belong to
376 * that parent.
377 * @return array An array of parsed block data.
378 */
379 function gutenberg_parse_blocks_from_menu_items( $menu_items, $menu_items_by_parent_id ) {
380 if ( empty( $menu_items ) ) {
381 return array();
382 }
383
384 $blocks = array();
385
386 foreach ( $menu_items as $menu_item ) {
387 $class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null;
388 $id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null;
389 $opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target;
390 $rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null;
391 $kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom';
392
393 $block = array(
394 'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link',
395 'attrs' => array(
396 'className' => $class_name,
397 'description' => $menu_item->description,
398 'id' => $id,
399 'kind' => $kind,
400 'label' => $menu_item->title,
401 'opensInNewTab' => $opens_in_new_tab,
402 'rel' => $rel,
403 'title' => $menu_item->attr_title,
404 'type' => $menu_item->object,
405 'url' => $menu_item->url,
406 ),
407 );
408
409 $block['innerBlocks'] = isset( $menu_items_by_parent_id[ $menu_item->ID ] )
410 ? gutenberg_parse_blocks_from_menu_items( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id )
411 : array();
412 $block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] );
413
414 $blocks[] = $block;
415 }
416
417 return $blocks;
418 }
419
420 /**
421 * Shim that hides ability to edit visibility and status for wp_navigation type posts.
422 * When merged to Core, the CSS below should be moved to wp-admin/css/edit.css.
423 *
424 * This shim can be removed when the Gutenberg plugin requires a WordPress
425 * version that has the ticket below.
426 *
427 * @see https://core.trac.wordpress.org/ticket/54407
428 *
429 * @param string $hook The current admin page.
430 */
431 function gutenberg_hide_visibility_and_status_for_navigation_posts( $hook ) {
432 $allowed_hooks = array( 'post.php', 'post-new.php' );
433 if ( ! in_array( $hook, $allowed_hooks, true ) ) {
434 return;
435 }
436
437 /**
438 * HACK: We're hiding the description field using CSS because this
439 * cannot be done using a filter or an action.
440 */
441
442 $css = <<<CSS
443 body.post-type-wp_navigation div#minor-publishing {
444 display: none;
445 }
446 CSS;
447
448 wp_add_inline_style( 'common', $css );
449 }
450
451 add_action( 'admin_enqueue_scripts', 'gutenberg_hide_visibility_and_status_for_navigation_posts' );
452