From d339af1bc1431162b215d4e47558b511a18edae3 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 9 Aug 2017 21:03:16 +0000 Subject: [PATCH] Map nav menu locations on theme switch This will send nav menu locations through three levels of mapping: 1. If both themes have only one location, that gets mapped. 2. If both themes have locations with the same slug, they get mapped. 3. Locations that (even partially) match slugs from a similar kind of menu location will get mapped. Menu locations are mapped for Live Previews in the Customizer and during theme switches. Props westonruter, obenland, welcher, melchoyce. Fixes #39692. git-svn-id: https://develop.svn.wordpress.org/trunk@41237 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-customize-nav-menus.php | 21 +- src/wp-includes/default-filters.php | 1 + src/wp-includes/nav-menu.php | 108 +++++++++++ src/wp-includes/theme.php | 8 +- tests/phpunit/tests/menu/nav-menu.php | 182 ++++++++++++++++++ 5 files changed, 308 insertions(+), 12 deletions(-) create mode 100644 tests/phpunit/tests/menu/nav-menu.php diff --git a/src/wp-includes/class-wp-customize-nav-menus.php b/src/wp-includes/class-wp-customize-nav-menus.php index 3d7b1f0deb..3b9ca487fd 100644 --- a/src/wp-includes/class-wp-customize-nav-menus.php +++ b/src/wp-includes/class-wp-customize-nav-menus.php @@ -27,12 +27,12 @@ final class WP_Customize_Nav_Menus { public $manager; /** - * Previewed Menus. + * Original nav menu locations before the theme was switched. * - * @since 4.3.0 + * @since 4.9.0 * @var array */ - public $previewed_menus; + protected $original_nav_menu_locations; /** * Constructor. @@ -42,8 +42,8 @@ final class WP_Customize_Nav_Menus { * @param object $manager An instance of the WP_Customize_Manager class. */ public function __construct( $manager ) { - $this->previewed_menus = array(); - $this->manager = $manager; + $this->manager = $manager; + $this->original_nav_menu_locations = get_nav_menu_locations(); // See https://github.com/xwp/wp-customize-snapshots/blob/962586659688a5b1fd9ae93618b7ce2d4e7a421c/php/class-customize-snapshot-manager.php#L469-L499 add_action( 'customize_register', array( $this, 'customize_register' ), 11 ); @@ -582,6 +582,12 @@ final class WP_Customize_Nav_Menus { $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '…' ); } + // Attempt to re-map the nav menu location assignments when previewing a theme switch. + $mapped_nav_menu_locations = array(); + if ( ! $this->manager->is_theme_active() ) { + $mapped_nav_menu_locations = wp_map_nav_menu_locations( get_nav_menu_locations(), $this->original_nav_menu_locations ); + } + foreach ( $locations as $location => $description ) { $setting_id = "nav_menu_locations[{$location}]"; @@ -600,6 +606,11 @@ final class WP_Customize_Nav_Menus { ) ); } + // Override the assigned nav menu location if mapped during previewed theme switch. + if ( isset( $mapped_nav_menu_locations[ $location ] ) ) { + $this->manager->set_post_value( $setting_id, $mapped_nav_menu_locations[ $location ] ); + } + $this->manager->add_control( new WP_Customize_Nav_Menu_Location_Control( $this->manager, $setting_id, array( 'label' => $description, 'location_id' => $location, diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index e689e1d606..39254ca7f9 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -262,6 +262,7 @@ add_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); add_action( 'template_redirect', 'wp_shortlink_header', 11, 0 ); add_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); add_action( 'init', 'check_theme_switched', 99 ); +add_action( 'after_switch_theme', '_wp_menus_changed' ); add_action( 'after_switch_theme', '_wp_sidebars_changed' ); add_action( 'wp_print_styles', 'print_emoji_styles' ); diff --git a/src/wp-includes/nav-menu.php b/src/wp-includes/nav-menu.php index 1585c62f3d..7adf8e1a4e 100644 --- a/src/wp-includes/nav-menu.php +++ b/src/wp-includes/nav-menu.php @@ -1026,3 +1026,111 @@ function _wp_delete_customize_changeset_dependent_auto_drafts( $post_id ) { } add_action( 'delete_post', '_wp_delete_customize_changeset_dependent_auto_drafts' ); } + +/** + * Handle menu config after theme change. + * + * @access private + * @since 4.9.0 + */ +function _wp_menus_changed() { + $old_nav_menu_locations = get_option( 'theme_switch_menu_locations', array() ); + $new_nav_menu_locations = get_nav_menu_locations(); + $mapped_nav_menu_locations = wp_map_nav_menu_locations( $new_nav_menu_locations, $old_nav_menu_locations ); + + set_theme_mod( 'nav_menu_locations', $mapped_nav_menu_locations ); + delete_option( 'theme_switch_menu_locations' ); +} + +/** + * Maps nav menu locations according to assignments in previously active theme. + * + * @since 4.9.0 + * + * @param array $new_nav_menu_locations New nav menu locations assignments. + * @param array $old_nav_menu_locations Old nav menu locations assignments. + * @return array Nav menus mapped to new nav menu locations. + */ +function wp_map_nav_menu_locations( $new_nav_menu_locations, $old_nav_menu_locations ) { + $registered_nav_menus = get_registered_nav_menus(); + + // Short-circuit if there are no old nav menu location assignments to map. + if ( empty( $old_nav_menu_locations ) ) { + return $new_nav_menu_locations; + } + + // If old and new theme have just one location, map it and we're done. + if ( 1 === count( $old_nav_menu_locations ) && 1 === count( $registered_nav_menus ) ) { + $new_nav_menu_locations[ key( $registered_nav_menus ) ] = array_pop( $old_nav_menu_locations ); + return $new_nav_menu_locations; + } + + $old_locations = array_keys( $old_nav_menu_locations ); + + // Map locations with the same slug. + foreach ( $registered_nav_menus as $location => $name ) { + if ( in_array( $location, $old_locations, true ) ) { + $new_nav_menu_locations[ $location ] = $old_nav_menu_locations[ $location ]; + unset( $old_nav_menu_locations[ $location ] ); + } + } + + // If there are no old nav menu locations left, then we're done. + if ( empty( $old_nav_menu_locations ) ) { + return $new_nav_menu_locations; + } + + /* + * If old and new theme both have locations that contain phrases + * from within the same group, make an educated guess and map it. + */ + $common_slug_groups = array( + array( 'header', 'main', 'navigation', 'primary', 'top' ), + array( 'bottom', 'footer', 'secondary', 'subsidiary' ), + ); + + // Go through each group... + foreach ( $common_slug_groups as $slug_group ) { + + // ...and see if any of these slugs... + foreach ( $slug_group as $slug ) { + + // ...and any of the new menu locations... + foreach ( $registered_nav_menus as $new_location => $name ) { + + // ...actually match! + if ( false === stripos( $new_location, $slug ) && false === stripos( $slug, $new_location ) ) { + continue; + } + + // Then see if any of the old locations... + foreach ( $old_nav_menu_locations as $location => $menu_id ) { + + // ...and any slug in the same group... + foreach ( $slug_group as $slug ) { + + // ... have a match as well. + if ( false === stripos( $location, $slug ) && false === stripos( $slug, $location ) ) { + continue; + } + + // Make sure this location wasn't mapped and removed previously. + if ( ! empty( $old_nav_menu_locations[ $location ] ) ) { + + // We have a match that can be mapped! + $new_nav_menu_locations[ $new_location ] = $old_nav_menu_locations[ $location ]; + + // Remove the mapped location so it can't be mapped again. + unset( $old_nav_menu_locations[ $location ] ); + + // Go back and check the next new menu location. + continue 3; + } + } // endforeach ( $slug_group as $slug ) + } // endforeach ( $old_nav_menu_locations as $location => $menu_id ) + } // endforeach foreach ( $registered_nav_menus as $new_location => $name ) + } // endforeach ( $slug_group as $slug ) + } // endforeach ( $common_slug_groups as $slug_group ) + + return $new_nav_menu_locations; +} diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 2b2fbf2403..1d55f4f27c 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -691,6 +691,7 @@ function switch_theme( $stylesheet ) { } $nav_menu_locations = get_theme_mod( 'nav_menu_locations' ); + add_option( 'theme_switch_menu_locations', $nav_menu_locations ); if ( func_num_args() > 1 ) { $stylesheet = func_get_arg( 1 ); @@ -731,13 +732,6 @@ function switch_theme( $stylesheet ) { if ( 'wp_ajax_customize_save' === current_action() ) { remove_theme_mod( 'sidebars_widgets' ); } - - if ( ! empty( $nav_menu_locations ) ) { - $nav_mods = get_theme_mod( 'nav_menu_locations' ); - if ( empty( $nav_mods ) ) { - set_theme_mod( 'nav_menu_locations', $nav_menu_locations ); - } - } } update_option( 'theme_switched', $old_theme->get_stylesheet() ); diff --git a/tests/phpunit/tests/menu/nav-menu.php b/tests/phpunit/tests/menu/nav-menu.php new file mode 100644 index 0000000000..ec346da53b --- /dev/null +++ b/tests/phpunit/tests/menu/nav-menu.php @@ -0,0 +1,182 @@ +register_nav_menu_locations( array( 'primary' ) ); + $prev_theme_nav_menu_locations = array( + 'unique-slug' => 1, + ); + $old_next_theme_nav_menu_locations = array(); // It was not active before. + $new_next_theme_nav_menu_locations = wp_map_nav_menu_locations( $old_next_theme_nav_menu_locations, $prev_theme_nav_menu_locations ); + + $expected_nav_menu_locations = array( + 'primary' => 1, + ); + $this->assertEquals( $expected_nav_menu_locations, $new_next_theme_nav_menu_locations ); + } + + /** + * Locations with the same name should map, switching to a theme not previously-active. + * + * @covers wp_map_nav_menu_locations() + */ + function test_locations_with_same_slug() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + $prev_theme_nav_menu_locations = array( + 'primary' => 1, + 'secondary' => 2, + ); + + $old_next_theme_nav_menu_locations = array(); // It was not active before. + $new_next_theme_nav_menu_locations = wp_map_nav_menu_locations( $old_next_theme_nav_menu_locations, $prev_theme_nav_menu_locations ); + + $expected_nav_menu_locations = $prev_theme_nav_menu_locations; + $this->assertEquals( $expected_nav_menu_locations, $new_next_theme_nav_menu_locations ); + } + + /** + * If the new theme was previously active, we should honor any changes to nav menu mapping done when the other theme was active. + * + * @covers wp_map_nav_menu_locations() + */ + function test_new_theme_previously_active() { + $this->register_nav_menu_locations( array( 'primary' ) ); + + $prev_theme_nav_menu_locations = array( + 'primary' => 1, + 'secondary' => 2, + ); + + // Nav menu location assignments that were set on the next theme when it was previously active. + $old_next_theme_nav_menu_locations = array( + 'primary' => 3, + ); + + $new_next_theme_nav_menu_locations = wp_map_nav_menu_locations( $old_next_theme_nav_menu_locations, $prev_theme_nav_menu_locations ); + + $expected_nav_menu_locations = wp_array_slice_assoc( $prev_theme_nav_menu_locations, array_keys( get_registered_nav_menus() ) ); + $this->assertEquals( $expected_nav_menu_locations, $new_next_theme_nav_menu_locations ); + } + + /** + * Make educated guesses on theme locations. + * + * @covers wp_map_nav_menu_locations() + */ + function test_location_guessing() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + + $prev_theme_nav_menu_locations = array( + 'header' => 1, + 'footer' => 2, + ); + + $old_next_theme_nav_menu_locations = array(); + $new_next_theme_nav_menu_locations = wp_map_nav_menu_locations( $old_next_theme_nav_menu_locations, $prev_theme_nav_menu_locations ); + + $expected_nav_menu_locations = array( + 'primary' => 1, + 'secondary' => 2, + ); + $this->assertEquals( $expected_nav_menu_locations, $new_next_theme_nav_menu_locations ); + } + + /** + * Make sure two locations that fall in the same group don't get the same menu assigned. + * + * @covers wp_map_nav_menu_locations() + */ + function test_location_guessing_one_menu_per_group() { + $this->register_nav_menu_locations( array( 'primary' ) ); + $prev_theme_nav_menu_locations = array( + 'top-menu' => 1, + 'secondary' => 2, + ); + + $old_next_theme_nav_menu_locations = array(); + $new_next_theme_nav_menu_locations = wp_map_nav_menu_locations( $old_next_theme_nav_menu_locations, $prev_theme_nav_menu_locations ); + + $expected_nav_menu_locations = array( + 'main' => 1, + ); + $this->assertEqualSets( $expected_nav_menu_locations, $new_next_theme_nav_menu_locations ); + } + + /** + * Make sure two locations that fall in the same group get menus assigned from the same group. + * + * @covers wp_map_nav_menu_locations() + */ + function test_location_guessing_one_menu_per_location() { + $this->register_nav_menu_locations( array( 'primary', 'main' ) ); + + $prev_theme_nav_menu_locations = array( + 'navigation-menu' => 1, + 'top-menu' => 2, + ); + + $old_next_theme_nav_menu_locations = array(); + $new_next_theme_nav_menu_locations = wp_map_nav_menu_locations( $old_next_theme_nav_menu_locations, $prev_theme_nav_menu_locations ); + + $expected_nav_menu_locations = array( + 'main' => 1, + 'primary' => 2, + ); + $this->assertEquals( $expected_nav_menu_locations, $new_next_theme_nav_menu_locations ); + } + + /** + * Technically possible to register menu locations numerically. + * + * @covers wp_map_nav_menu_locations() + */ + function test_numerical_locations() { + $this->register_nav_menu_locations( array( 'primary', 1 ) ); + + $prev_theme_nav_menu_locations = array( + 'main' => 1, + 'secondary' => 2, + 'tertiary' => 3, + ); + + $old_next_theme_nav_menu_locations = array(); + $new_next_theme_nav_menu_locations = wp_map_nav_menu_locations( $old_next_theme_nav_menu_locations, $prev_theme_nav_menu_locations ); + + $expected_nav_menu_locations = array( + 'primary' => 1, + ); + $this->assertEqualSets( $expected_nav_menu_locations, $new_next_theme_nav_menu_locations ); + } +}