diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index b2cff75e08c6c..df374c811c946 100644 --- a/src/wp-admin/includes/post.php +++ b/src/wp-admin/includes/post.php @@ -2447,3 +2447,63 @@ function the_block_editor_meta_box_post_form_hidden_fields( $post ) { */ do_action( 'block_editor_meta_box_hidden_fields', $post ); } + +/** + * Disable block editor for wp_navigation type posts so they can be managed via the UI. + * + * @since 5.9.0 + * @access private + * + * @param bool $value Whether the CPT supports block editor or not. + * @param string $post_type Post type. + * + * @return bool + */ +function _disable_block_editor_for_navigation_post_type( $value, $post_type ) { + if ( 'wp_navigation' === $post_type ) { + return false; + } + + return $value; +} + +/** + * This callback disables the content editor for wp_navigation type posts. + * Content editor cannot handle wp_navigation type posts correctly. + * We cannot disable the "editor" feature in the wp_navigation's CPT definition + * because it disables the ability to save navigation blocks via REST API. + * + * @since 5.9.0 + * @access private + * + * @param WP_Post $post An instance of WP_Post class. + */ +function _disable_content_editor_for_navigation_post_type( $post ) { + $post_type = get_post_type( $post ); + if ( 'wp_navigation' !== $post_type ) { + return; + } + + remove_post_type_support( $post_type, 'editor' ); +} + +/** + * This callback enables content editor for wp_navigation type posts. + * We need to enable it back because we disable it to hide + * the content editor for wp_navigation type posts. + * + * @since 5.9.0 + * @access private + * + * @see _disable_content_editor_for_navigation_post_type + * + * @param WP_Post $post An instance of WP_Post class. + */ +function _enable_content_editor_for_navigation_post_type( $post ) { + $post_type = get_post_type( $post ); + if ( 'wp_navigation' !== $post_type ) { + return; + } + + add_post_type_support( $post_type, 'editor' ); +} diff --git a/src/wp-admin/site-editor.php b/src/wp-admin/site-editor.php index 14c190be8c03c..2eaeb441c7574 100644 --- a/src/wp-admin/site-editor.php +++ b/src/wp-admin/site-editor.php @@ -59,7 +59,17 @@ static function( $classes ) { '/wp/v2/global-styles/' . $active_global_styles_id . '?context=edit', '/wp/v2/global-styles/' . $active_global_styles_id, '/wp/v2/themes/' . $active_theme . '/global-styles', + '/wp/v2/block-navigation-areas?context=edit', ); + +$areas = get_option( 'fse_navigation_areas', array() ); +$active_areas = array_intersect_key( $areas, get_navigation_areas() ); +foreach ( $active_areas as $post_id ) { + if ( $post_id ) { + $preload_paths[] = add_query_args( 'context', 'edit', rest_get_route_for_post( $post_id ) ); + } +} + block_editor_rest_api_preload( $preload_paths, $block_editor_context ); $editor_settings = get_block_editor_settings( diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 412251d2f292e..07ab2a5ed4943 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -583,6 +583,17 @@ add_action( 'admin_footer-post-new.php', 'wp_add_iframed_editor_assets_html' ); add_action( 'admin_footer-widgets.php', 'wp_add_iframed_editor_assets_html' ); +add_action( 'use_block_editor_for_post_type', '_disable_block_editor_for_navigation_post_type', 10, 2 ); +add_action( 'edit_form_after_title', '_disable_content_editor_for_navigation_post_type' ); +add_action( 'edit_form_after_editor', '_enable_content_editor_for_navigation_post_type' ); + +/* + * Disable "Post Attributes" for wp_navigation post type. The attributes are + * also conditionally enabled when a site has custom templates. Block Theme + * templates can be available for every post type. + */ +add_filter( 'theme_wp_navigation_templates', '__return_empty_array' ); + // Taxonomy. add_action( 'init', 'create_initial_taxonomies', 0 ); // Highest priority. add_action( 'change_locale', 'create_initial_taxonomies' ); @@ -670,6 +681,7 @@ add_action( 'setup_theme', 'wp_enable_block_templates' ); // Navigation areas. -add_action( 'setup_theme', '_register_default_navigation_areas' ); +add_action( 'setup_theme', '_wp_register_default_navigation_areas' ); +add_action( 'switch_theme', '_wp_migrate_menu_to_navigation_post', 99, 3 ); unset( $filter, $action ); diff --git a/src/wp-includes/navigation-areas.php b/src/wp-includes/navigation-areas.php index fd4536199b2a3..e2d7460c3815a 100644 --- a/src/wp-includes/navigation-areas.php +++ b/src/wp-includes/navigation-areas.php @@ -29,7 +29,7 @@ function register_navigation_areas( $new_areas ) { * @since 5.9.0 * @access private */ -function _register_default_navigation_areas() { +function _wp_register_default_navigation_areas() { register_navigation_areas( array( 'primary' => _x( 'Primary', 'navigation area' ), @@ -50,3 +50,208 @@ function get_navigation_areas() { global $navigation_areas; return $navigation_areas; } + +/** + * Migrates classic menus to a block-based navigation post on theme switch. + * Assigns the created navigation post to the corresponding navigation area. + * + * @since 5.9.0 + * @access private + * + * @param string $new_name Name of the new theme. + * @param WP_Theme $new_theme New theme. + * @param WP_Theme $old_theme Old theme. + */ +function _wp_migrate_menu_to_navigation_post( $new_name, WP_Theme $new_theme, WP_Theme $old_theme ) { + // Do nothing when switching to a theme that does not support site editor. + if ( ! wp_is_block_template_theme() ) { + return; + } + + // get_nav_menu_locations() calls get_theme_mod() which depends on the stylesheet option. + // At the same time, switch_theme runs only after the stylesheet option was updated to $new_theme. + // To retrieve theme mods of the old theme, the getter is hooked to get_option( 'stylesheet' ) so that we + // get the old theme, which causes the get_nav_menu_locations to get the locations of the old theme. + $get_old_theme_stylesheet = static function() use ( $old_theme ) { + return $old_theme->get_stylesheet(); + }; + add_filter( 'option_stylesheet', $get_old_theme_stylesheet ); + + $locations = get_nav_menu_locations(); + $area_mapping = get_option( 'fse_navigation_areas', array() ); + + foreach ( $locations as $location_name => $menu_id ) { + // Get the menu from the location, skipping if there is no + // menu or there was an error. + $menu = wp_get_nav_menu_object( $menu_id ); + if ( ! $menu || is_wp_error( $menu ) ) { + continue; + } + + $menu_items = _wp_get_menu_items_at_location( $location_name ); + if ( empty( $menu_items ) ) { + continue; + } + + $post_name = 'classic_menu_' . $menu_id; + + // Get or create to avoid creating too many wp_navigation posts. + $query = new WP_Query; + $matching_posts = $query->query( + array( + 'name' => $post_name, + 'post_status' => 'publish', + 'post_type' => 'wp_navigation', + 'posts_per_page' => 1, + 'fields' => 'ids', + ) + ); + + if ( ! empty( $matching_posts ) ) { + $navigation_post_id = $matching_posts[0]->ID; + } else { + $menu_items_by_parent_id = _wp_sort_menu_items_by_parent_id( $menu_items ); + $parsed_blocks = _wp_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id ); + $post_data = array( + 'post_type' => 'wp_navigation', + 'post_title' => sprintf( + /* translators: %s: the name of the menu, e.g. "Main Menu". */ + __( 'Classic menu: %s' ), + $menu->name + ), + 'post_name' => $post_name, + 'post_content' => serialize_blocks( $parsed_blocks ), + 'post_status' => 'publish', + ); + $navigation_post_id = wp_insert_post( $post_data ); + } + + $area_mapping[ $location_name ] = $navigation_post_id; + } + remove_filter( 'option_stylesheet', $get_old_theme_stylesheet ); + + update_option( 'fse_navigation_areas', $area_mapping ); +} + +/** + * Returns the menu items for a WordPress menu location. + * + * @since 5.9.0 + * @access private + * + * @param string $location The menu location. + * @return array Menu items for the location. + */ +function _wp_get_menu_items_at_location( $location ) { + if ( empty( $location ) ) { + return; + } + + // Build menu data. The following approximates the code in `wp_nav_menu()`. + + // Find the location in the list of locations, returning early if the + // location can't be found. + $locations = get_nav_menu_locations(); + if ( ! isset( $locations[ $location ] ) ) { + return; + } + + // Get the menu from the location, returning early if there is no + // menu or there was an error. + $menu = wp_get_nav_menu_object( $locations[ $location ] ); + if ( ! $menu || is_wp_error( $menu ) ) { + return; + } + + $menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) ); + _wp_menu_item_classes_by_context( $menu_items ); + + return $menu_items; +} + +/** + * Sorts a standard array of menu items into a nested structure keyed by the + * id of the parent menu. + * + * @since 5.9.0 + * @access private + * + * @param array $menu_items Menu items to sort. + * @return array An array keyed by the id of the parent menu where each element + * is an array of menu items that belong to that parent. + */ +function _wp_sort_menu_items_by_parent_id( $menu_items ) { + $sorted_menu_items = array(); + foreach ( $menu_items as $menu_item ) { + $sorted_menu_items[ $menu_item->menu_order ] = $menu_item; + } + unset( $menu_items, $menu_item ); + + $menu_items_by_parent_id = array(); + foreach ( $sorted_menu_items as $menu_item ) { + $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item; + } + + return $menu_items_by_parent_id; +} + +/** + * Turns menu item data into a nested array of parsed blocks + * + * @since 5.9.0 + * @access private + * + * @param array $menu_items An array of menu items that represent + * an individual level of a menu. + * @param array $menu_items_by_parent_id An array keyed by the id of the + * parent menu where each element is an + * array of menu items that belong to + * that parent. + * @return array An array of parsed block data. + */ +function _wp_parse_blocks_from_menu_items( $menu_items, $menu_items_by_parent_id ) { + if ( empty( $menu_items ) ) { + return array(); + } + + $blocks = array(); + + foreach ( $menu_items as $menu_item ) { + $class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null; + $id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null; + $opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target; + $rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null; + $kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom'; + + $block = array( + 'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link', + 'attrs' => array( + 'className' => $class_name, + 'description' => $menu_item->description, + 'id' => $id, + 'kind' => $kind, + 'label' => $menu_item->title, + 'opensInNewTab' => $opens_in_new_tab, + 'rel' => $rel, + 'title' => $menu_item->attr_title, + 'type' => $menu_item->object, + 'url' => $menu_item->url, + ), + ); + + if ( isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ) { + $block['innerBlocks'] = _wp_parse_blocks_from_menu_items( + $menu_items_by_parent_id[ $menu_item->ID ], + $menu_items_by_parent_id + ); + } else { + $block['innerBlocks'] = array(); + } + + $block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] ); + + $blocks[] = $block; + } + + return $blocks; +} diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 3e2cdbc415996..8f3e6f608262c 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -480,13 +480,12 @@ function create_initial_post_types() { 'labels' => array( 'name' => __( 'Navigation Menus' ), 'singular_name' => __( 'Navigation Menu' ), - 'menu_name' => _x( 'Navigation Menus', 'Admin Menu text' ), 'add_new' => _x( 'Add New', 'Navigation Menu' ), 'add_new_item' => __( 'Add New Navigation Menu' ), 'new_item' => __( 'New Navigation Menu' ), 'edit_item' => __( 'Edit Navigation Menu' ), 'view_item' => __( 'View Navigation Menu' ), - 'all_items' => __( 'All Navigation Menus' ), + 'all_items' => __( 'Navigation Menus' ), 'search_items' => __( 'Search Navigation Menus' ), 'parent_item_colon' => __( 'Parent Navigation Menu:' ), 'not_found' => __( 'No Navigation Menu found.' ), @@ -494,15 +493,15 @@ function create_initial_post_types() { 'archives' => __( 'Navigation Menu archives' ), 'insert_into_item' => __( 'Insert into Navigation Menu' ), 'uploaded_to_this_item' => __( 'Uploaded to this Navigation Menu' ), - // Some of these are a bit weird, what are they for? 'filter_items_list' => __( 'Filter Navigation Menu list' ), 'items_list_navigation' => __( 'Navigation Menus list navigation' ), 'items_list' => __( 'Navigation Menus list' ), ), + 'description' => __( 'Navigation menus that can be inserted into your site.' ), 'public' => false, '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ 'has_archive' => false, - 'show_ui' => false, + 'show_ui' => wp_is_block_template_theme(), 'show_in_menu' => 'themes.php', 'show_in_admin_bar' => false, 'show_in_rest' => true, diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 3e4b8f80adc3f..cd271ddec5d77 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -11445,7 +11445,7 @@ mockedApiResponse.TypesCollection = { } }, "wp_navigation": { - "description": "", + "description": "Navigation menus that can be inserted into your site.", "hierarchical": false, "name": "Navigation Menus", "slug": "wp_navigation",