From 630fd305fc23aef03086e73ee3f88d990744c203 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 28 Oct 2016 02:56:16 +0000 Subject: [PATCH] Customize: Introduce starter content and site freshness state. A theme can opt-in for tailored starter content to apply to the customizer when previewing the theme on a fresh install, when `fresh_site` is at its initial `1` value. Starter content is staged in the customizer and does not go live unless the changes are published. Initial starter content is added to Twenty Seventeen. * The `fresh_site` flag is cleared when a published post or page is saved, when widgets are modified, or when the customizer state is saved. * Starter content is registered via `starter-content` theme support, where the argument is an array containing `widgets`, `posts`, `nav_menus`, `options`, and `theme_mods`. Posts/pages in starter content are created with the `auto-draft` status, re-using the page/post stubs feature added to nav menus and the static front page controls. * A `get_theme_starter_content` filter allows for plugins to extend a theme's starter content. * Starter content in themes can/should re-use existing starter content items in core by using named placeholders. * Import theme starter content into customized state when fresh site. * Prevent original_title differences from causing refreshes if title is present. * Ensure nav menu item url is set according to object when previewing. * Make sure initial saved state is false if there are dirty settings without an existing changeset. * Ensure dirty settings are cleaned upon changeset publishing. Props helen, westonruter, ocean90. Fixes #38114, #38533. git-svn-id: https://develop.svn.wordpress.org/trunk@38991 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/schema.php | 3 + src/wp-admin/js/customize-controls.js | 27 ++- .../themes/twentyseventeen/functions.php | 61 ++++++ .../class-wp-customize-manager.php | 134 ++++++++++++++ ...ass-wp-customize-nav-menu-item-setting.php | 11 ++ src/wp-includes/default-filters.php | 7 + src/wp-includes/functions.php | 10 + .../js/customize-preview-nav-menus.js | 6 + src/wp-includes/theme.php | 175 +++++++++++++++++- 9 files changed, 429 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/includes/schema.php b/src/wp-admin/includes/schema.php index cd66e1924b..59a5574d51 100644 --- a/src/wp-admin/includes/schema.php +++ b/src/wp-admin/includes/schema.php @@ -516,6 +516,9 @@ function populate_options() { // 4.4.0 'medium_large_size_w' => 768, 'medium_large_size_h' => 0, + + // 4.7.0 + 'fresh_site' => 1, ); // 3.3 diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js index d207e79883..fecdd1a908 100644 --- a/src/wp-admin/js/customize-controls.js +++ b/src/wp-admin/js/customize-controls.js @@ -146,7 +146,7 @@ settingRevision = api._latestSettingRevisions[ setting.id ]; // Skip including settings that have already been included in the changeset, if only requesting unsaved. - if ( ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) { + if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) { return; } @@ -4880,7 +4880,7 @@ api.bind( 'change', captureSettingModifiedDuringSave ); submit = function () { - var request, query, settingInvalidities = {}; + var request, query, settingInvalidities = {}, latestRevision = api._latestRevision; /* * Block saving if there are any settings that are marked as @@ -4984,6 +4984,20 @@ api.state( 'changesetStatus' ).set( response.changeset_status ); if ( 'publish' === response.changeset_status ) { + + // Mark all published as clean if they haven't been modified during the request. + api.each( function( setting ) { + /* + * Note that the setting revision will be undefined in the case of setting + * values that are marked as dirty when the customizer is loaded, such as + * when applying starter content. All other dirty settings will have an + * associated revision due to their modification triggering a change event. + */ + if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) { + setting._dirty = false; + } + } ); + api.state( 'changesetStatus' ).set( '' ); api.settings.changeset.uuid = response.next_changeset_uuid; parent.send( 'changeset-uuid', api.settings.changeset.uuid ); @@ -5152,7 +5166,15 @@ }); // Set default states. + changesetStatus( api.settings.changeset.status ); saved( true ); + if ( '' === changesetStatus() ) { // Handle case for loading starter content. + api.each( function( setting ) { + if ( setting._dirty ) { + saved( false ); + } + } ); + } saving( false ); activated( api.settings.theme.active ); processing( 0 ); @@ -5161,7 +5183,6 @@ expandedSection( false ); previewerAlive( true ); editShortcutVisibility( 'initial' ); - changesetStatus( api.settings.changeset.status ); api.bind( 'change', function() { state('saved').set( false ); diff --git a/src/wp-content/themes/twentyseventeen/functions.php b/src/wp-content/themes/twentyseventeen/functions.php index 6bf6e85be8..de8dd372ea 100644 --- a/src/wp-content/themes/twentyseventeen/functions.php +++ b/src/wp-content/themes/twentyseventeen/functions.php @@ -102,6 +102,67 @@ function twentyseventeen_setup() { * specifically font, colors, and column width. */ add_editor_style( array( 'assets/css/editor-style.css', twentyseventeen_fonts_url() ) ); + + add_theme_support( 'starter-content', array( + 'widgets' => array( + 'sidebar-1' => array( + 'text_business_info', + 'search', + 'text_credits', + ), + + 'sidebar-2' => array( + 'text_business_info', + ), + + 'sidebar-3' => array( + 'text_credits', + ), + ), + + 'posts' => array( + 'home', + 'about-us', + 'contact-us', + 'blog', + 'homepage-section', + ), + + 'options' => array( + 'show_on_front' => 'page', + 'page_on_front' => '{{home}}', + 'page_for_posts' => '{{blog}}', + ), + + 'theme_mods' => array( + 'panel_1' => '{{homepage-section}}', + 'panel_2' => '{{about-us}}', + 'panel_3' => '{{blog}}', + 'panel_4' => '{{contact-us}}', + ), + + 'nav_menus' => array( + 'top' => array( + 'name' => __( 'Top' ), + 'items' => array( + 'page_home', + 'page_about', + 'page_blog', + 'page_contact', + ), + ), + 'social' => array( + 'name' => __( 'Social' ), + 'items' => array( + 'link_yelp', + 'link_facebook', + 'link_twitter', + 'link_instagram', + 'link_email', + ), + ), + ), + ) ); } add_action( 'after_setup_theme', 'twentyseventeen_setup' ); diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php index 2c610fd941..ec0fa8de31 100644 --- a/src/wp-includes/class-wp-customize-manager.php +++ b/src/wp-includes/class-wp-customize-manager.php @@ -531,6 +531,11 @@ final class WP_Customize_Manager { } } + // Import theme starter content for fresh installs when landing in the customizer and no existing changeset loaded. + if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow && ! $this->changeset_post_id() ) { + add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 ); + } + $this->start_previewing_theme(); } @@ -887,6 +892,135 @@ final class WP_Customize_Manager { return $this->_changeset_data; } + /** + * Import theme starter content into post values. + * + * @since 4.7.0 + * @access public + * + * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`. + */ + function import_theme_starter_content( $starter_content = array() ) { + if ( empty( $starter_content ) ) { + $starter_content = get_theme_starter_content(); + } + + $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array(); + $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array(); + $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array(); + $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array(); + $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array(); + + // Widgets. + $max_widget_numbers = array(); + foreach ( $sidebars_widgets as $sidebar_id => $widgets ) { + $sidebar_widget_ids = array(); + foreach ( $widgets as $widget ) { + list( $id_base, $instance ) = $widget; + + if ( ! isset( $max_widget_numbers[ $id_base ] ) ) { + + // When $settings is an array-like object, get an intrinsic array for use with array_keys(). + $settings = get_option( "widget_{$id_base}", array() ); + if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) { + $settings = $settings->getArrayCopy(); + } + + // Find the max widget number for this type. + $widget_numbers = array_keys( $settings ); + $widget_numbers[] = 1; + $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers ); + } + $max_widget_numbers[ $id_base ] += 1; + + $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] ); + $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] ); + + $class = 'WP_Customize_Setting'; + + /** This filter is documented in wp-includes/class-wp-customize-manager.php */ + $args = apply_filters( 'customize_dynamic_setting_args', false, $setting_id ); + + if ( false !== $args ) { + + /** This filter is documented in wp-includes/class-wp-customize-manager.php */ + $class = apply_filters( 'customize_dynamic_setting_class', $class, $setting_id, $args ); + + $setting = new $class( $this, $setting_id, $args ); + $setting_value = call_user_func( $setting->sanitize_js_callback, $instance, $setting ); + $this->set_post_value( $setting_id, $setting_value ); + $sidebar_widget_ids[] = $widget_id; + } + } + + $this->set_post_value( sprintf( 'sidebars_widgets[%s]', $sidebar_id ), $sidebar_widget_ids ); + } + + // Posts & pages. + if ( ! empty( $posts ) ) { + foreach ( array_keys( $posts ) as $post_symbol ) { + $posts[ $post_symbol ]['ID'] = wp_insert_post( wp_slash( array_merge( + $posts[ $post_symbol ], + array( 'post_status' => 'auto-draft' ) + ) ) ); + } + $this->set_post_value( 'nav_menus_created_posts', wp_list_pluck( $posts, 'ID' ) ); // This is why nav_menus component is dependency for adding posts. + } + + // Nav menus. + $placeholder_id = -1; + foreach ( $nav_menus as $nav_menu_location => $nav_menu ) { + $nav_menu_term_id = $placeholder_id--; + $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $nav_menu_term_id ); + $this->set_post_value( $nav_menu_setting_id, array( + 'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location, + ) ); + + // @todo Add support for menu_item_parent. + $position = 0; + foreach ( $nav_menu['items'] as $nav_menu_item ) { + $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- ); + if ( ! isset( $nav_menu_item['position'] ) ) { + $nav_menu_item['position'] = $position++; + } + $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id; + + if ( isset( $nav_menu_item['object_id'] ) ) { + if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) { + $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID']; + if ( empty( $nav_menu_item['title'] ) ) { + $original_object = get_post( $nav_menu_item['object_id'] ); + $nav_menu_item['title'] = $original_object->post_title; + } + } else { + continue; + } + } else { + $nav_menu_item['object_id'] = 0; + } + $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item ); + } + + $this->set_post_value( sprintf( 'nav_menu_locations[%s]', $nav_menu_location ), $nav_menu_term_id ); + } + + // Options. + foreach ( $options as $name => $value ) { + if ( preg_match( '/^{{(?P.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) { + $value = $posts[ $matches['symbol'] ]['ID']; + } + $this->set_post_value( $name, $value ); + } + + // Theme mods. + foreach ( $theme_mods as $name => $value ) { + if ( preg_match( '/^{{(?P.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) { + $value = $posts[ $matches['symbol'] ]['ID']; + } + $this->set_post_value( $name, $value ); + } + } + /** * Get dirty pre-sanitized setting values in the current customized state. * diff --git a/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php b/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php index e1f8365100..00d69527b9 100644 --- a/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php +++ b/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php @@ -602,6 +602,17 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { } } + // Ensure nav menu item URL is set according to linked object. + if ( ! empty( $post->object_id ) ) { + if ( 'post_type' === $post->type ) { + $post->url = get_permalink( $post->object_id ); + } elseif ( 'post_type_archive' === $post->type && ! empty( $post->object ) ) { + $post->url = get_post_type_archive_link( $post->object ); + } elseif ( 'taxonomy' == $post->type && ! empty( $post->object ) ) { + $post->url = get_term_link( (int) $post->object, $post->object ); + } + } + /** This filter is documented in wp-includes/nav-menu.php */ $post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title ); diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 58aedf91f2..228235e362 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -188,6 +188,13 @@ add_filter( 'the_guid', 'esc_url' ); // Email filters add_filter( 'wp_mail', 'wp_staticize_emoji_for_email' ); +// Mark site as no longer fresh +if ( get_option( 'fresh_site' ) ) { + foreach ( array( 'publish_post', 'publish_page', 'wp_ajax_save-widget', 'wp_ajax_widgets-order', 'customize_save_after' ) as $action ) { + add_action( $action, '_delete_option_fresh_site' ); + } +} + // Misc filters add_filter( 'option_ping_sites', 'privacy_ping_filter' ); add_filter( 'option_blog_charset', '_wp_specialchars' ); // IMPORTANT: This must not be wp_specialchars() or esc_html() or it'll cause an infinite loop diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 112f0ffed0..ff489ae869 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -3240,6 +3240,16 @@ function _config_wp_siteurl( $url = '' ) { return $url; } +/** + * Delete the fresh site option. + * + * @since 4.7.0 + * @access private + */ +function _delete_option_fresh_site() { + update_option( 'fresh_site', 0 ); +} + /** * Set the localized direction for MCE plugin. * diff --git a/src/wp-includes/js/customize-preview-nav-menus.js b/src/wp-includes/js/customize-preview-nav-menus.js index 6b241cd72d..e7e4d14f5f 100644 --- a/src/wp-includes/js/customize-preview-nav-menus.js +++ b/src/wp-includes/js/customize-preview-nav-menus.js @@ -139,6 +139,12 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function( _oldValue.url = urlParser.href; } + // Prevent original_title differences from causing refreshes if title is present. + if ( newValue.title ) { + delete _oldValue.original_title; + delete _newValue.original_title; + } + if ( _.isEqual( _oldValue, _newValue ) ) { return false; } diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index c128f98938..957ee1b00d 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1755,6 +1755,175 @@ function get_editor_stylesheets() { return apply_filters( 'editor_stylesheets', $stylesheets ); } +/** + * Expand a theme's starter content configuration using core-provided data. + * + * @since 4.7.0 + * + * @return array Array of starter content. + */ +function get_theme_starter_content() { + $theme_support = get_theme_support( 'starter-content' ); + if ( ! empty( $theme_support ) ) { + $config = $theme_support[0]; + } else { + $config = array(); + } + + $core_content = array ( + 'widgets' => array( + 'text_business_info' => array ( 'text', array ( + 'title' => __( 'Find Us' ), + 'text' => join( '', array ( + '

' . __( 'Address' ) . '
', + __( '123 Main Street' ) . '
' . __( 'New York, NY 10001' ) . '

', + '

' . __( 'Hours' ) . '
', + __( 'Monday—Friday: 9:00AM–5:00PM' ) . '
' . __( 'Saturday & Sunday: 11:00AM–3:00PM' ) . '

' + ) ), + ) ), + 'search' => array ( 'search', array ( + 'title' => __( 'Site Search' ), + ) ), + 'text_credits' => array ( 'text', array ( + 'title' => __( 'Site Credits' ), + 'text' => sprintf( __( 'This site was created on %s' ), get_date_from_gmt( current_time( 'mysql', 1 ), 'c' ) ), + ) ), + ), + 'nav_menus' => array ( + 'page_home' => array( + 'type' => 'post_type', + 'object' => 'page', + 'object_id' => '{{home}}', + ), + 'page_about' => array( + 'type' => 'post_type', + 'object' => 'page', + 'object_id' => '{{about-us}}', + ), + 'page_blog' => array( + 'type' => 'post_type', + 'object' => 'page', + 'object_id' => '{{blog}}', + ), + 'page_contact' => array( + 'type' => 'post_type', + 'object' => 'page', + 'object_id' => '{{contact-us}}', + ), + + 'link_yelp' => array( + 'title' => __( 'Yelp' ), + 'url' => 'https://www.yelp.com', + ), + 'link_facebook' => array( + 'title' => __( 'Facebook' ), + 'url' => 'https://www.facebook.com/wordpress', + ), + 'link_twitter' => array( + 'title' => __( 'Twitter' ), + 'url' => 'https://twitter.com/wordpress', + ), + 'link_instagram' => array( + 'title' => __( 'Instagram' ), + 'url' => 'https://www.instagram.com/explore/tags/wordcamp/', + ), + 'link_email' => array( + 'title' => __( 'Email' ), + 'url' => 'mailto:wordpress@example.com', + ), + ), + 'posts' => array( + 'home' => array( + 'post_type' => 'page', + 'post_title' => __( 'Homepage' ), + 'post_content' => __( 'Welcome home.' ), + ), + 'about-us' => array( + 'post_type' => 'page', + 'post_title' => __( 'About Us' ), + 'post_content' => __( 'More than you ever wanted to know.' ), + ), + 'contact-us' => array( + 'post_type' => 'page', + 'post_title' => __( 'Contact Us' ), + 'post_content' => __( 'Call us at 999-999-9999.' ), + ), + 'blog' => array( + 'post_type' => 'page', + 'post_title' => __( 'Blog' ), + ), + + 'homepage-section' => array( + 'post_type' => 'page', + 'post_title' => __( 'A homepage section' ), + 'post_content' => __( 'This is an example of a homepage section, which are managed in theme options.' ), + ), + ), + ); + + $content = array(); + + foreach ( $config as $type => $args ) { + switch( $type ) { + // Use options and theme_mods as-is + case 'options' : + case 'theme_mods' : + $content[ $type ] = $config[ $type ]; + break; + + // Widgets are an extra level down due to groupings + case 'widgets' : + foreach ( $config[ $type ] as $group => $items ) { + foreach ( $items as $id ) { + if ( ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $id ] ) ) { + $content[ $type ][ $group ][ $id ] = $core_content[ $type ][ $id ]; + } + } + } + break; + + // And nav menus are yet another level down + case 'nav_menus' : + foreach ( $config[ $type ] as $group => $args2 ) { + // Menu groups need a name + if ( empty( $args['name'] ) ) { + $args2['name'] = $group; + } + + $content[ $type ][ $group ]['name'] = $args2['name']; + + // Do we need to check if this is empty? + foreach ( $args2['items'] as $id ) { + if ( ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $id ] ) ) { + $content[ $type ][ $group ]['items'][ $id ] = $core_content[ $type ][ $id ]; + } + } + } + break; + + + // Everything else should map at the next level + default : + foreach( $config[ $type ] as $id ) { + if ( ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $id ] ) ) { + $content[ $type ][ $id ] = $core_content[ $type ][ $id ]; + } + } + break; + } + } + + /** + * Filters the expanded array of starter content. + * + * @since 4.7.0 + * + * @param array $content Array of starter content. + * @param array $config Array of theme-specific starter content configuration. + */ + return apply_filters( 'get_theme_starter_content', $content, $config ); +} + /** * Registers theme support for a given feature. * @@ -1767,12 +1936,13 @@ function get_editor_stylesheets() { * @since 3.9.0 The `html5` feature now also accepts 'gallery' and 'caption' * @since 4.1.0 The `title-tag` feature was added * @since 4.5.0 The `customize-selective-refresh-widgets` feature was added + * @since 4.7.0 The `starter-content` feature was added * * @global array $_wp_theme_features * * @param string $feature The feature being added. Likely core values include 'post-formats', * 'post-thumbnails', 'html5', 'custom-logo', 'custom-header-uploads', - * 'custom-header', 'custom-background', 'title-tag', etc. + * 'custom-header', 'custom-background', 'title-tag', 'starter-content', etc. * @param mixed $args,... Optional extra arguments to pass along with certain features. * @return void|bool False on failure, void otherwise. */ @@ -2204,7 +2374,8 @@ function current_theme_supports( $feature ) { * * The dynamic portion of the hook name, `$feature`, refers to the specific theme * feature. Possible values include 'post-formats', 'post-thumbnails', 'custom-background', - * 'custom-header', 'menus', 'automatic-feed-links', 'html5', and `customize-selective-refresh-widgets`. + * 'custom-header', 'menus', 'automatic-feed-links', 'html5', + * 'starter-content', and 'customize-selective-refresh-widgets'. * * @since 3.4.0 *