diff --git a/lib/full-site-editing/template-parts.php b/lib/full-site-editing/template-parts.php index 2693821d03ace9..1356dedf689559 100644 --- a/lib/full-site-editing/template-parts.php +++ b/lib/full-site-editing/template-parts.php @@ -93,6 +93,9 @@ function gutenberg_register_wp_template_part_area_taxonomy() { if ( ! defined( 'WP_TEMPLATE_PART_AREA_HEADER' ) ) { define( 'WP_TEMPLATE_PART_AREA_HEADER', 'header' ); } +if ( ! defined( 'WP_TEMPLATE_PART_AREA_PRIMARY_MENU' ) ) { + define( 'WP_TEMPLATE_PART_AREA_PRIMARY_MENU', 'primary-menu' ); +} if ( ! defined( 'WP_TEMPLATE_PART_AREA_FOOTER' ) ) { define( 'WP_TEMPLATE_PART_AREA_FOOTER', 'footer' ); } @@ -189,6 +192,16 @@ function gutenberg_get_allowed_template_part_areas() { 'icon' => 'header', 'area_tag' => 'header', ), + array( + 'area' => WP_TEMPLATE_PART_AREA_PRIMARY_MENU, + 'label' => __( 'Primary menu', 'gutenberg' ), + 'description' => __( + 'The primary menu template defines a page area that typically contains a main navigation.', + 'gutenberg' + ), + 'icon' => 'primary-menu', + 'area_tag' => 'div', + ), array( 'area' => WP_TEMPLATE_PART_AREA_FOOTER, 'label' => __( 'Footer', 'gutenberg' ), diff --git a/lib/navigation.php b/lib/navigation.php index 452ee06a41b8f0..faa8f6a799fcc4 100644 --- a/lib/navigation.php +++ b/lib/navigation.php @@ -435,3 +435,250 @@ function gutenberg_register_navigation_post_type() { register_post_type( 'wp_navigation', $args ); } add_action( 'init', 'gutenberg_register_navigation_post_type' ); + +/** + * ========================== + * ========= IDEA 1 ========= + * ========================== + * + * On theme switch, take navigationMenuId from theme A template parts, and apply it to theme B template parts. + * Upsides: + * * New theme always gets the "primary", "secondary" etc. menus in its corresponding slots. + * * "Menu areas" are just template areas. + * * All the UI is in place thanks to the isolated template parts editor. + * + * Downsides: + * * Needs to look for the first available navigation block, it could be nested, missing etc. This should be easy to address. + * * Placing an intermediate template part between "Primary menu" and the navigation block would derail this function. + * I think the only way of addressing it is not accepting intermediate template parts, perhaps via a new flavor of the template part block. + * * New template parts are stored in the database on theme switch. + * * It adds a new concept of "re-writing content". + * + * Questions: + * * Template parts are the source of truth, but they could be deleted. I'm not sure if that's a problem. + * * There are now two blocks instead of one. It uncovers we are really managing two entities, but + * it also creates a more complex interaction. I like it, though. Do you? + */ + +/** + * Copies the navigationMenuId attribute from navigation template parts in the old theme, to + * corresponding ones in the new theme. + * + * @param string $new_name Name of the new theme. + * @param WP_Theme $new_theme New theme. + * @param WP_Theme $old_theme Old theme. + * @see switch_theme WordPress action. + */ +function gutenberg_migrate_nav_on_theme_switch( $new_name, $new_theme, $old_theme ) { + $old_theme_name = $old_theme->get_stylesheet(); + $settings_old_theme = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data(); + $old_nav_parts = get_navigation_template_part_names( $settings_old_theme->get_template_parts() ); + + WP_Theme_JSON_Resolver_Gutenberg::clean_cached_data(); + $new_theme_name = $new_theme->get_stylesheet(); + $settings_new_theme = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data(); + $new_nav_parts = get_navigation_template_part_names( $settings_new_theme->get_template_parts() ); + + $common_parts = array_intersect( $old_nav_parts, $new_nav_parts ); + foreach ( $common_parts as $common_part ) { + // Get a navigation template part from the old theme. + $id = $old_theme_name . '//' . $common_part; + $old_template_part = gutenberg_get_block_template( $id, 'wp_template_part' ); + if ( ! $old_template_part || empty( $old_template_part->content ) || empty( $old_template_part->wp_id ) ) { + continue; + } + + // Extract the navigationMenuId from the old template part. + $old_blocks = parse_blocks( $old_template_part->content ); + if ( + ! $old_blocks || + 'core/navigation' !== $old_blocks[0]['blockName'] || + empty( $old_blocks[0]['attrs']['navigationMenuId'] ) + ) { + continue; + } + $old_nav_menu_id = $old_blocks[0]['attrs']['navigationMenuId']; + + // Get a navigation template part from the new theme. + $new_id = $new_theme_name . '//' . $common_part; + $new_template_part = gutenberg_get_block_template( $new_id, 'wp_template_part' ); + + // Set the old post_name to something else because there is a hook in place that prevents + // the new template part from getting the same slug as the old template part. + // @TODO: Remove this once the post_name is retired as a slug container. + if ( $old_template_part->wp_id ) { + $old_post = get_post( $old_template_part->wp_id ); + $old_post->post_name = 'temp'; + wp_update_post( $old_post ); + } + + // Create a navigation template part for the new theme if one doesn't already exist. + if ( ! $new_template_part || empty( $new_template_part->content ) || empty( $new_template_part->wp_id ) ) { + $template_file = _gutenberg_get_template_file( 'wp_template_part', $common_part ); + $block_template = _gutenberg_build_template_result_from_file( $template_file, 'wp_template_part' ); + $template_part_args = array( + 'post_type' => $block_template->type, + 'post_name' => $common_part, + 'post_title' => $common_part, + 'post_content' => $block_template->content, + 'post_status' => 'publish', + 'tax_input' => array( + 'wp_theme' => array( + $new_theme_name, + ), + 'wp_template_part_area' => array( + $block_template->area, + ), + ), + ); + $template_part_post_id = wp_insert_post( $template_part_args ); + wp_set_post_terms( $template_part_post_id, $block_template->area, 'wp_template_part_area' ); + wp_set_post_terms( $template_part_post_id, $new_theme_name, 'wp_theme' ); + $new_template_part = gutenberg_get_block_template( $new_id, 'wp_template_part' ); + } + + // Apply the previous navigation post ID to the navigation block in the new theme. + $new_blocks = parse_blocks( $new_template_part->content ); + if ( + $new_blocks && + ! empty( $new_blocks[0]['blockName'] ) && + is_array( $new_blocks[0]['attrs'] ) && + 'core/navigation' === $new_blocks[0]['blockName'] + ) { + $new_blocks[0]['attrs']['navigationMenuId'] = $old_nav_menu_id; + $new_post = get_post( $new_template_part->wp_id ); + $new_post->post_name = $common_part; + $new_post->post_content = serialize_blocks( $new_blocks ); + wp_update_post( $new_post ); + } + + // Restore the old post_name to the old template part. + // @TODO: Remove this once the post_name is retired as a slug container. + if ( $old_template_part->wp_id ) { + $old_post = get_post( $old_template_part->wp_id ); + $old_post->post_name = $common_part; + wp_update_post( $old_post ); + } + } +} + +/** + * Returns a list of template parts representing labeled navigation areas such as primary, secondary, etc. + * + * @param array[] $template_parts Template parts from theme.json. + * @return array List of template parts na + */ +function get_navigation_template_part_names( $template_parts ) { + $menu_parts = array(); + foreach ( $template_parts as $key => $part ) { + if ( WP_TEMPLATE_PART_AREA_PRIMARY_MENU === $part['area'] ) { + $menu_parts[] = $key; + } + } + return $menu_parts; +} + +// Set a priority such that WP_Theme_JSON_Resolver_Gutenberg still has contains the cached data. +// This will clean the cache which may be unexpected, so it would be better to introduce a `before_theme_switch` action. + +// Enable re-writing like: add_action( 'switch_theme', 'gutenberg_migrate_nav_on_theme_switch', -200, 3 );. + +// IDEA 1 above is self contained and ends here. + + +/** + * ========================== + * ========= IDEA 2 ========= + * ========================== + * + * Parse navigation-related template parts on save, and store an association like {"primary-menu": 794} somewhere. + * When a new navigation-related template part is rendered for the first time, provide {"primary-menu": 794} to use + * as the initial value of the `navigationMenuId` attribute. + * + * Upsides: + * * Menus are matched between themes via a keyword. + * * "Menu areas" are just template areas. + * * All the UI is in place thanks to the isolated template parts editor. + * + * Downsides: + * * It still wouldn't work if there's a nested template part between the "primary-menu" and the navigation block. + * * We don't use "initial" attributes in other blocks, so it's a precedent. + */ + + +/** + * When the navigation-related template part post is saved, extract the navigationMenuId it contains, + * and store it in the database for later. + * + * @param int $post_id Post ID. + */ +function store_navigation_associations( $post_id ) { + $template_part_post = get_post( $post_id ); + + // Only proceed if the template part represents a navigation. + $area_terms = get_the_terms( $template_part_post, 'wp_template_part_area' ); + if ( is_wp_error( $area_terms ) || false === $area_terms ) { + return; + } + + $area = $area_terms[0]->name; + if ( ! in_array( $area, gutenberg_get_navigation_template_part_areas(), true ) ) { + return; + } + + // Only proceed if the template part is related to the current theme. + $theme_terms = get_the_terms( $post_id, 'wp_theme' ); + if ( is_wp_error( $theme_terms ) || false === $theme_terms ) { + return; + } + $theme = $theme_terms[0]->name; + if ( get_stylesheet() !== $theme ) { + return; + } + + // Get the first navigation menu ID from the post. + // @TODO: traverse the entire tree, not just the first block. + $blocks = parse_blocks( $template_part_post->post_content ); + if ( + ! $blocks || + 'core/navigation' !== $blocks[0]['blockName'] || + empty( $blocks[0]['attrs']['navigationMenuId'] ) + ) { + return; + } + $navigation_post_id = $blocks[0]['attrs']['navigationMenuId']; + + // Update the area -> navigation ID map. + // Site options are a quick&dirty choice for now. We could use taxonomies, theme mods, or anything else here. + $updated_associations = array_merge( + get_option( 'navigation_associations', array() ), + array( $area => $navigation_post_id ) + ); + update_option( 'navigation_associations', $updated_associations ); +} +add_action( 'save_post_wp_template_part', 'store_navigation_associations' ); + +/** + * Returns a list of template areas that are meant to hold navigation. + * + * @todo move this where WP_TEMPLATE_PART_AREA_PRIMARY_MENU is declared. + * @return string A list of area idetifiers. + */ +function gutenberg_get_navigation_template_part_areas() { + return array( + WP_TEMPLATE_PART_AREA_PRIMARY_MENU, + ); +} + + +/** + * TODO: Make changing nested entities affect parent entities in Gutenberg + * example: + * + * + * + * + * + * + * If I set navigationMenuId to something else, it should save the primary-menu template part. Currently it doesn't and it seems like a bug. + */ diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index 26212ce6313d49..511c58a89e2b0f 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -11,6 +11,10 @@ ], "textdomain": "default", "attributes": { + "initialNavigationMenuArea": { + "type": "string", + "default": "" + }, "navigationMenuId": { "type": "number" }, diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index b94970598a4e11..4991273af78daf 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -242,6 +242,16 @@ function render_block_core_navigation( $attributes, $content, $block ) { $inner_blocks = new WP_Block_List( $parsed_blocks, $attributes ); } + if ( + array_key_exists( 'initialNavigationMenuArea', $attributes ) && + empty( $attributes['navigationMenuId'] ) + ) { + $associations = get_option( 'navigation_associations', array() ); + if ( ! empty( $associations[ $attributes['initialNavigationMenuArea'] ] ) ) { + $attributes['navigationMenuId'] = $associations[ $attributes['initialNavigationMenuArea'] ]; + } + } + // Load inner blocks from the navigation post. if ( array_key_exists( 'navigationMenuId', $attributes ) ) { $navigation_post = get_post( $attributes['navigationMenuId'] );