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
This commit is contained in:
Weston Ruter 2016-10-28 02:56:16 +00:00
parent 76012a9a59
commit 630fd305fc
9 changed files with 429 additions and 5 deletions

View File

@ -516,6 +516,9 @@ function populate_options() {
// 4.4.0 // 4.4.0
'medium_large_size_w' => 768, 'medium_large_size_w' => 768,
'medium_large_size_h' => 0, 'medium_large_size_h' => 0,
// 4.7.0
'fresh_site' => 1,
); );
// 3.3 // 3.3

View File

@ -146,7 +146,7 @@
settingRevision = api._latestSettingRevisions[ setting.id ]; settingRevision = api._latestSettingRevisions[ setting.id ];
// Skip including settings that have already been included in the changeset, if only requesting unsaved. // 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; return;
} }
@ -4880,7 +4880,7 @@
api.bind( 'change', captureSettingModifiedDuringSave ); api.bind( 'change', captureSettingModifiedDuringSave );
submit = function () { submit = function () {
var request, query, settingInvalidities = {}; var request, query, settingInvalidities = {}, latestRevision = api._latestRevision;
/* /*
* Block saving if there are any settings that are marked as * Block saving if there are any settings that are marked as
@ -4984,6 +4984,20 @@
api.state( 'changesetStatus' ).set( response.changeset_status ); api.state( 'changesetStatus' ).set( response.changeset_status );
if ( 'publish' === 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.state( 'changesetStatus' ).set( '' );
api.settings.changeset.uuid = response.next_changeset_uuid; api.settings.changeset.uuid = response.next_changeset_uuid;
parent.send( 'changeset-uuid', api.settings.changeset.uuid ); parent.send( 'changeset-uuid', api.settings.changeset.uuid );
@ -5152,7 +5166,15 @@
}); });
// Set default states. // Set default states.
changesetStatus( api.settings.changeset.status );
saved( true ); saved( true );
if ( '' === changesetStatus() ) { // Handle case for loading starter content.
api.each( function( setting ) {
if ( setting._dirty ) {
saved( false );
}
} );
}
saving( false ); saving( false );
activated( api.settings.theme.active ); activated( api.settings.theme.active );
processing( 0 ); processing( 0 );
@ -5161,7 +5183,6 @@
expandedSection( false ); expandedSection( false );
previewerAlive( true ); previewerAlive( true );
editShortcutVisibility( 'initial' ); editShortcutVisibility( 'initial' );
changesetStatus( api.settings.changeset.status );
api.bind( 'change', function() { api.bind( 'change', function() {
state('saved').set( false ); state('saved').set( false );

View File

@ -102,6 +102,67 @@ function twentyseventeen_setup() {
* specifically font, colors, and column width. * specifically font, colors, and column width.
*/ */
add_editor_style( array( 'assets/css/editor-style.css', twentyseventeen_fonts_url() ) ); 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' ); add_action( 'after_setup_theme', 'twentyseventeen_setup' );

View File

@ -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(); $this->start_previewing_theme();
} }
@ -887,6 +892,135 @@ final class WP_Customize_Manager {
return $this->_changeset_data; 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<symbol>.+)}}$/', $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<symbol>.+)}}$/', $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<symbol>.+)}}$/', $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. * Get dirty pre-sanitized setting values in the current customized state.
* *

View File

@ -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 */ /** This filter is documented in wp-includes/nav-menu.php */
$post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title ); $post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title );

View File

@ -188,6 +188,13 @@ add_filter( 'the_guid', 'esc_url' );
// Email filters // Email filters
add_filter( 'wp_mail', 'wp_staticize_emoji_for_email' ); 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 // Misc filters
add_filter( 'option_ping_sites', 'privacy_ping_filter' ); 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 add_filter( 'option_blog_charset', '_wp_specialchars' ); // IMPORTANT: This must not be wp_specialchars() or esc_html() or it'll cause an infinite loop

View File

@ -3240,6 +3240,16 @@ function _config_wp_siteurl( $url = '' ) {
return $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. * Set the localized direction for MCE plugin.
* *

View File

@ -139,6 +139,12 @@ wp.customize.navMenusPreview = wp.customize.MenusCustomizerPreview = ( function(
_oldValue.url = urlParser.href; _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 ) ) { if ( _.isEqual( _oldValue, _newValue ) ) {
return false; return false;
} }

View File

@ -1755,6 +1755,175 @@ function get_editor_stylesheets() {
return apply_filters( 'editor_stylesheets', $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 (
'<p><strong>' . __( 'Address' ) . '</strong><br />',
__( '123 Main Street' ) . '<br />' . __( 'New York, NY 10001' ) . '</p>',
'<p><strong>' . __( 'Hours' ) . '</strong><br />',
__( 'Monday&mdash;Friday: 9:00AM&ndash;5:00PM' ) . '<br />' . __( 'Saturday &amp; Sunday: 11:00AM&ndash;3:00PM' ) . '</p>'
) ),
) ),
'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. * 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 3.9.0 The `html5` feature now also accepts 'gallery' and 'caption'
* @since 4.1.0 The `title-tag` feature was added * @since 4.1.0 The `title-tag` feature was added
* @since 4.5.0 The `customize-selective-refresh-widgets` 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 * @global array $_wp_theme_features
* *
* @param string $feature The feature being added. Likely core values include 'post-formats', * @param string $feature The feature being added. Likely core values include 'post-formats',
* 'post-thumbnails', 'html5', 'custom-logo', 'custom-header-uploads', * '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. * @param mixed $args,... Optional extra arguments to pass along with certain features.
* @return void|bool False on failure, void otherwise. * @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 * The dynamic portion of the hook name, `$feature`, refers to the specific theme
* feature. Possible values include 'post-formats', 'post-thumbnails', 'custom-background', * 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 * @since 3.4.0
* *