Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] Use template parts to preserve navigation between theme switches #36117

Closed
wants to merge 10 commits into from
13 changes: 13 additions & 0 deletions lib/full-site-editing/template-parts.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
}
Expand Down Expand Up @@ -189,6 +192,16 @@ function gutenberg_get_allowed_template_part_areas() {
'icon' => 'header',
'area_tag' => 'header',
),
array(
'area' => WP_TEMPLATE_PART_AREA_PRIMARY_MENU,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be an area per semantic menu type, or it could be more of a new, free field that can take any value..

'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' ),
Expand Down
242 changes: 242 additions & 0 deletions lib/navigation.php
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,245 @@ 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.

// Uncomment to enable.
// 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' );

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:
* <!-- wp:template-part {"slug":"header"} -->
* <!-- wp:template-part {"slug":"primary-menu","tagName":"primary-menu","className":"primary-menu","layout":{"inherit":true}} -->
* <!-- wp:navigation { "navigationMenuId": {primary-menu} } -->
* <!-- /wp:template-part -->
* <!-- /wp:template-part -->
*
* 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.
*/
Comment on lines +674 to +684
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an issue, not a @todo comment

4 changes: 4 additions & 0 deletions packages/block-library/src/navigation/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
],
"textdomain": "default",
"attributes": {
"initialNavigationMenuArea": {
"type": "string",
"default": ""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we don't need the default here?

},
"navigationMenuId": {
"type": "number"
},
Expand Down
10 changes: 10 additions & 0 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,16 @@ function render_block_core_navigation( $attributes, $content, $block ) {
$inner_blocks = new WP_Block_List( $parsed_blocks, $attributes );
}

if (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use an inline comment to explain what it does.

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'] );
Expand Down