Make WordPress Core

Changeset 38991


Ignore:
Timestamp:
10/28/2016 02:56:16 AM (8 years ago)
Author:
westonruter
Message:

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.

Location:
trunk/src
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/schema.php

    r38814 r38991  
    517517    'medium_large_size_w' => 768,
    518518    'medium_large_size_h' => 0,
     519
     520    // 4.7.0
     521    'fresh_site' => 1,
    519522    );
    520523
  • trunk/src/wp-admin/js/customize-controls.js

    r38977 r38991  
    147147
    148148            // Skip including settings that have already been included in the changeset, if only requesting unsaved.
    149             if ( ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) {
     149            if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) {
    150150                return;
    151151            }
     
    48814881
    48824882                submit = function () {
    4883                     var request, query, settingInvalidities = {};
     4883                    var request, query, settingInvalidities = {}, latestRevision = api._latestRevision;
    48844884
    48854885                    /*
     
    49854985                        api.state( 'changesetStatus' ).set( response.changeset_status );
    49864986                        if ( 'publish' === response.changeset_status ) {
     4987
     4988                            // Mark all published as clean if they haven't been modified during the request.
     4989                            api.each( function( setting ) {
     4990                                /*
     4991                                 * Note that the setting revision will be undefined in the case of setting
     4992                                 * values that are marked as dirty when the customizer is loaded, such as
     4993                                 * when applying starter content. All other dirty settings will have an
     4994                                 * associated revision due to their modification triggering a change event.
     4995                                 */
     4996                                if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) {
     4997                                    setting._dirty = false;
     4998                                }
     4999                            } );
     5000
    49875001                            api.state( 'changesetStatus' ).set( '' );
    49885002                            api.settings.changeset.uuid = response.next_changeset_uuid;
     
    51535167
    51545168            // Set default states.
     5169            changesetStatus( api.settings.changeset.status );
    51555170            saved( true );
     5171            if ( '' === changesetStatus() ) { // Handle case for loading starter content.
     5172                api.each( function( setting ) {
     5173                    if ( setting._dirty ) {
     5174                        saved( false );
     5175                    }
     5176                } );
     5177            }
    51565178            saving( false );
    51575179            activated( api.settings.theme.active );
     
    51625184            previewerAlive( true );
    51635185            editShortcutVisibility( 'initial' );
    5164             changesetStatus( api.settings.changeset.status );
    51655186
    51665187            api.bind( 'change', function() {
  • trunk/src/wp-content/themes/twentyseventeen/functions.php

    r38867 r38991  
    103103     */
    104104    add_editor_style( array( 'assets/css/editor-style.css', twentyseventeen_fonts_url() ) );
     105
     106    add_theme_support( 'starter-content', array(
     107        'widgets' => array(
     108            'sidebar-1' => array(
     109                'text_business_info',
     110                'search',
     111                'text_credits',
     112            ),
     113
     114            'sidebar-2' => array(
     115                'text_business_info',
     116            ),
     117
     118            'sidebar-3' => array(
     119                'text_credits',
     120            ),
     121        ),
     122
     123        'posts' => array(
     124            'home',
     125            'about-us',
     126            'contact-us',
     127            'blog',
     128            'homepage-section',
     129        ),
     130
     131        'options' => array(
     132            'show_on_front' => 'page',
     133            'page_on_front' => '{{home}}',
     134            'page_for_posts' => '{{blog}}',
     135        ),
     136
     137        'theme_mods' => array(
     138            'panel_1' => '{{homepage-section}}',
     139            'panel_2' => '{{about-us}}',
     140            'panel_3' => '{{blog}}',
     141            'panel_4' => '{{contact-us}}',
     142        ),
     143
     144        'nav_menus' => array(
     145            'top' => array(
     146                'name' => __( 'Top' ),
     147                'items' => array(
     148                    'page_home',
     149                    'page_about',
     150                    'page_blog',
     151                    'page_contact',
     152                ),
     153            ),
     154            'social' => array(
     155                'name' => __( 'Social' ),
     156                'items' => array(
     157                    'link_yelp',
     158                    'link_facebook',
     159                    'link_twitter',
     160                    'link_instagram',
     161                    'link_email',
     162                ),
     163            ),
     164        ),
     165    ) );
    105166}
    106167add_action( 'after_setup_theme', 'twentyseventeen_setup' );
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r38989 r38991  
    532532        }
    533533
     534        // Import theme starter content for fresh installs when landing in the customizer and no existing changeset loaded.
     535        if ( get_option( 'fresh_site' ) && 'customize.php' === $pagenow && ! $this->changeset_post_id() ) {
     536            add_action( 'after_setup_theme', array( $this, 'import_theme_starter_content' ), 100 );
     537        }
     538
    534539        $this->start_previewing_theme();
    535540    }
     
    886891        }
    887892        return $this->_changeset_data;
     893    }
     894
     895    /**
     896     * Import theme starter content into post values.
     897     *
     898     * @since 4.7.0
     899     * @access public
     900     *
     901     * @param array $starter_content Starter content. Defaults to `get_theme_starter_content()`.
     902     */
     903    function import_theme_starter_content( $starter_content = array() ) {
     904        if ( empty( $starter_content ) ) {
     905            $starter_content = get_theme_starter_content();
     906        }
     907
     908        $sidebars_widgets = isset( $starter_content['widgets'] ) && ! empty( $this->widgets ) ? $starter_content['widgets'] : array();
     909        $posts = isset( $starter_content['posts'] ) && ! empty( $this->nav_menus ) ? $starter_content['posts'] : array();
     910        $options = isset( $starter_content['options'] ) ? $starter_content['options'] : array();
     911        $nav_menus = isset( $starter_content['nav_menus'] ) && ! empty( $this->nav_menus ) ? $starter_content['nav_menus'] : array();
     912        $theme_mods = isset( $starter_content['theme_mods'] ) ? $starter_content['theme_mods'] : array();
     913
     914        // Widgets.
     915        $max_widget_numbers = array();
     916        foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
     917            $sidebar_widget_ids = array();
     918            foreach ( $widgets as $widget ) {
     919                list( $id_base, $instance ) = $widget;
     920
     921                if ( ! isset( $max_widget_numbers[ $id_base ] ) ) {
     922
     923                    // When $settings is an array-like object, get an intrinsic array for use with array_keys().
     924                    $settings = get_option( "widget_{$id_base}", array() );
     925                    if ( $settings instanceof ArrayObject || $settings instanceof ArrayIterator ) {
     926                        $settings = $settings->getArrayCopy();
     927                    }
     928
     929                    // Find the max widget number for this type.
     930                    $widget_numbers = array_keys( $settings );
     931                    $widget_numbers[] = 1;
     932                    $max_widget_numbers[ $id_base ] = call_user_func_array( 'max', $widget_numbers );
     933                }
     934                $max_widget_numbers[ $id_base ] += 1;
     935
     936                $widget_id = sprintf( '%s-%d', $id_base, $max_widget_numbers[ $id_base ] );
     937                $setting_id = sprintf( 'widget_%s[%d]', $id_base, $max_widget_numbers[ $id_base ] );
     938
     939                $class = 'WP_Customize_Setting';
     940
     941                /** This filter is documented in wp-includes/class-wp-customize-manager.php */
     942                $args = apply_filters( 'customize_dynamic_setting_args', false, $setting_id );
     943
     944                if ( false !== $args ) {
     945
     946                    /** This filter is documented in wp-includes/class-wp-customize-manager.php */
     947                    $class = apply_filters( 'customize_dynamic_setting_class', $class, $setting_id, $args );
     948
     949                    $setting = new $class( $this, $setting_id, $args );
     950                    $setting_value = call_user_func( $setting->sanitize_js_callback, $instance, $setting );
     951                    $this->set_post_value( $setting_id, $setting_value );
     952                    $sidebar_widget_ids[] = $widget_id;
     953                }
     954            }
     955
     956            $this->set_post_value( sprintf( 'sidebars_widgets[%s]', $sidebar_id ), $sidebar_widget_ids );
     957        }
     958
     959        // Posts & pages.
     960        if ( ! empty( $posts ) ) {
     961            foreach ( array_keys( $posts ) as $post_symbol ) {
     962                $posts[ $post_symbol ]['ID'] = wp_insert_post( wp_slash( array_merge(
     963                    $posts[ $post_symbol ],
     964                    array( 'post_status' => 'auto-draft' )
     965                ) ) );
     966            }
     967            $this->set_post_value( 'nav_menus_created_posts', wp_list_pluck( $posts, 'ID' ) ); // This is why nav_menus component is dependency for adding posts.
     968        }
     969
     970        // Nav menus.
     971        $placeholder_id = -1;
     972        foreach ( $nav_menus as $nav_menu_location => $nav_menu ) {
     973            $nav_menu_term_id = $placeholder_id--;
     974            $nav_menu_setting_id = sprintf( 'nav_menu[%d]', $nav_menu_term_id );
     975            $this->set_post_value( $nav_menu_setting_id, array(
     976                'name' => isset( $nav_menu['name'] ) ? $nav_menu['name'] : $nav_menu_location,
     977            ) );
     978
     979            // @todo Add support for menu_item_parent.
     980            $position = 0;
     981            foreach ( $nav_menu['items'] as $nav_menu_item ) {
     982                $nav_menu_item_setting_id = sprintf( 'nav_menu_item[%d]', $placeholder_id-- );
     983                if ( ! isset( $nav_menu_item['position'] ) ) {
     984                    $nav_menu_item['position'] = $position++;
     985                }
     986                $nav_menu_item['nav_menu_term_id'] = $nav_menu_term_id;
     987
     988                if ( isset( $nav_menu_item['object_id'] ) ) {
     989                    if ( 'post_type' === $nav_menu_item['type'] && preg_match( '/^{{(?P<symbol>.+)}}$/', $nav_menu_item['object_id'], $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
     990                        $nav_menu_item['object_id'] = $posts[ $matches['symbol'] ]['ID'];
     991                        if ( empty( $nav_menu_item['title'] ) ) {
     992                            $original_object = get_post( $nav_menu_item['object_id'] );
     993                            $nav_menu_item['title'] = $original_object->post_title;
     994                        }
     995                    } else {
     996                        continue;
     997                    }
     998                } else {
     999                    $nav_menu_item['object_id'] = 0;
     1000                }
     1001                $this->set_post_value( $nav_menu_item_setting_id, $nav_menu_item );
     1002            }
     1003
     1004            $this->set_post_value( sprintf( 'nav_menu_locations[%s]', $nav_menu_location ), $nav_menu_term_id );
     1005        }
     1006
     1007        // Options.
     1008        foreach ( $options as $name => $value ) {
     1009            if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
     1010                $value = $posts[ $matches['symbol'] ]['ID'];
     1011            }
     1012            $this->set_post_value( $name, $value );
     1013        }
     1014
     1015        // Theme mods.
     1016        foreach ( $theme_mods as $name => $value ) {
     1017            if ( preg_match( '/^{{(?P<symbol>.+)}}$/', $value, $matches ) && isset( $posts[ $matches['symbol'] ] ) ) {
     1018                $value = $posts[ $matches['symbol'] ]['ID'];
     1019            }
     1020            $this->set_post_value( $name, $value );
     1021        }
    8881022    }
    8891023
  • trunk/src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php

    r38928 r38991  
    603603        }
    604604
     605        // Ensure nav menu item URL is set according to linked object.
     606        if ( ! empty( $post->object_id ) ) {
     607            if ( 'post_type' === $post->type ) {
     608                $post->url = get_permalink( $post->object_id );
     609            } elseif ( 'post_type_archive' === $post->type && ! empty( $post->object ) ) {
     610                $post->url = get_post_type_archive_link( $post->object );
     611            } elseif ( 'taxonomy' == $post->type && ! empty( $post->object ) ) {
     612                $post->url = get_term_link( (int) $post->object, $post->object );
     613            }
     614        }
     615
    605616        /** This filter is documented in wp-includes/nav-menu.php */
    606617        $post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title );
  • trunk/src/wp-includes/default-filters.php

    r38961 r38991  
    188188// Email filters
    189189add_filter( 'wp_mail', 'wp_staticize_emoji_for_email' );
     190
     191// Mark site as no longer fresh
     192if ( get_option( 'fresh_site' ) ) {
     193    foreach ( array( 'publish_post', 'publish_page', 'wp_ajax_save-widget', 'wp_ajax_widgets-order', 'customize_save_after' ) as $action ) {
     194        add_action( $action, '_delete_option_fresh_site' );
     195    }
     196}
    190197
    191198// Misc filters
  • trunk/src/wp-includes/functions.php

    r38956 r38991  
    32423242
    32433243/**
     3244 * Delete the fresh site option.
     3245 *
     3246 * @since 4.7.0
     3247 * @access private
     3248 */
     3249function _delete_option_fresh_site() {
     3250    update_option( 'fresh_site', 0 );
     3251}
     3252
     3253/**
    32443254 * Set the localized direction for MCE plugin.
    32453255 *
  • trunk/src/wp-includes/js/customize-preview-nav-menus.js

    r38810 r38991  
    140140                    }
    141141
     142                    // Prevent original_title differences from causing refreshes if title is present.
     143                    if ( newValue.title ) {
     144                        delete _oldValue.original_title;
     145                        delete _newValue.original_title;
     146                    }
     147
    142148                    if ( _.isEqual( _oldValue, _newValue ) ) {
    143149                        return false;
  • trunk/src/wp-includes/theme.php

    r38985 r38991  
    17571757
    17581758/**
     1759 * Expand a theme's starter content configuration using core-provided data.
     1760 *
     1761 * @since 4.7.0
     1762 *
     1763 * @return array Array of starter content.
     1764 */
     1765function get_theme_starter_content() {
     1766    $theme_support = get_theme_support( 'starter-content' );
     1767    if ( ! empty( $theme_support ) ) {
     1768        $config = $theme_support[0];
     1769    } else {
     1770        $config = array();
     1771    }
     1772
     1773    $core_content = array (
     1774        'widgets' => array(
     1775            'text_business_info' => array ( 'text', array (
     1776                'title' => __( 'Find Us' ),
     1777                'text' => join( '', array (
     1778                    '<p><strong>' . __( 'Address' ) . '</strong><br />',
     1779                    __( '123 Main Street' ) . '<br />' . __( 'New York, NY 10001' ) . '</p>',
     1780                    '<p><strong>' . __( 'Hours' ) . '</strong><br />',
     1781                    __( 'Monday&mdash;Friday: 9:00AM&ndash;5:00PM' ) . '<br />' . __( 'Saturday &amp; Sunday: 11:00AM&ndash;3:00PM' ) . '</p>'
     1782                ) ),
     1783            ) ),
     1784            'search' => array ( 'search', array (
     1785                'title' => __( 'Site Search' ),
     1786            ) ),
     1787            'text_credits' => array ( 'text', array (
     1788                'title' => __( 'Site Credits' ),
     1789                'text' => sprintf( __( 'This site was created on %s' ), get_date_from_gmt( current_time( 'mysql', 1 ), 'c' ) ),
     1790            ) ),
     1791        ),
     1792        'nav_menus' => array (
     1793            'page_home' => array(
     1794                'type' => 'post_type',
     1795                'object' => 'page',
     1796                'object_id' => '{{home}}',
     1797            ),
     1798            'page_about' => array(
     1799                'type' => 'post_type',
     1800                'object' => 'page',
     1801                'object_id' => '{{about-us}}',
     1802            ),
     1803            'page_blog' => array(
     1804                'type' => 'post_type',
     1805                'object' => 'page',
     1806                'object_id' => '{{blog}}',
     1807            ),
     1808            'page_contact' => array(
     1809                'type' => 'post_type',
     1810                'object' => 'page',
     1811                'object_id' => '{{contact-us}}',
     1812            ),
     1813
     1814            'link_yelp' => array(
     1815                'title' => __( 'Yelp' ),
     1816                'url' => 'https://www.yelp.com',
     1817            ),
     1818            'link_facebook' => array(
     1819                'title' => __( 'Facebook' ),
     1820                'url' => 'https://www.facebook.com/wordpress',
     1821            ),
     1822            'link_twitter' => array(
     1823                'title' => __( 'Twitter' ),
     1824                'url' => 'https://twitter.com/wordpress',
     1825            ),
     1826            'link_instagram' => array(
     1827                'title' => __( 'Instagram' ),
     1828                'url' => 'https://www.instagram.com/explore/tags/wordcamp/',
     1829            ),
     1830            'link_email' => array(
     1831                'title' => __( 'Email' ),
     1832                'url' => 'mailto:wordpress@example.com',
     1833            ),
     1834        ),
     1835        'posts' => array(
     1836            'home' => array(
     1837                'post_type' => 'page',
     1838                'post_title' => __( 'Homepage' ),
     1839                'post_content' => __( 'Welcome home.' ),
     1840            ),
     1841            'about-us' => array(
     1842                'post_type' => 'page',
     1843                'post_title' => __( 'About Us' ),
     1844                'post_content' => __( 'More than you ever wanted to know.' ),
     1845            ),
     1846            'contact-us' => array(
     1847                'post_type' => 'page',
     1848                'post_title' => __( 'Contact Us' ),
     1849                'post_content' => __( 'Call us at 999-999-9999.' ),
     1850            ),
     1851            'blog' => array(
     1852                'post_type' => 'page',
     1853                'post_title' => __( 'Blog' ),
     1854            ),
     1855
     1856            'homepage-section' => array(
     1857                'post_type' => 'page',
     1858                'post_title' => __( 'A homepage section' ),
     1859                'post_content' => __( 'This is an example of a homepage section, which are managed in theme options.' ),
     1860            ),
     1861        ),
     1862    );
     1863
     1864    $content = array();
     1865
     1866    foreach ( $config as $type => $args ) {
     1867        switch( $type ) {
     1868            // Use options and theme_mods as-is
     1869            case 'options' :
     1870            case 'theme_mods' :
     1871                $content[ $type ] = $config[ $type ];
     1872                break;
     1873
     1874            // Widgets are an extra level down due to groupings
     1875            case 'widgets' :
     1876                foreach ( $config[ $type ] as $group => $items ) {
     1877                    foreach ( $items as $id ) {
     1878                        if ( ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $id ] ) ) {
     1879                            $content[ $type ][ $group ][ $id ] = $core_content[ $type ][ $id ];
     1880                        }
     1881                    }
     1882                }
     1883                break;
     1884
     1885            // And nav menus are yet another level down
     1886            case 'nav_menus' :
     1887                foreach ( $config[ $type ] as $group => $args2 ) {
     1888                    // Menu groups need a name
     1889                    if ( empty( $args['name'] ) ) {
     1890                        $args2['name'] = $group;
     1891                    }
     1892
     1893                    $content[ $type ][ $group ]['name'] = $args2['name'];
     1894
     1895                    // Do we need to check if this is empty?
     1896                    foreach ( $args2['items'] as $id ) {
     1897                        if ( ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $id ] ) ) {
     1898                            $content[ $type ][ $group ]['items'][ $id ] = $core_content[ $type ][ $id ];
     1899                        }
     1900                    }
     1901                }
     1902                break;
     1903
     1904
     1905            // Everything else should map at the next level
     1906            default :
     1907                foreach( $config[ $type ] as $id ) {
     1908                    if ( ! empty( $core_content[ $type ] ) && ! empty( $core_content[ $type ][ $id ] ) ) {
     1909                        $content[ $type ][ $id ] = $core_content[ $type ][ $id ];
     1910                    }
     1911                }
     1912                break;
     1913        }
     1914    }
     1915
     1916    /**
     1917     * Filters the expanded array of starter content.
     1918     *
     1919     * @since 4.7.0
     1920     *
     1921     * @param array $content Array of starter content.
     1922     * @param array $config  Array of theme-specific starter content configuration.
     1923     */
     1924    return apply_filters( 'get_theme_starter_content', $content, $config );
     1925}
     1926
     1927/**
    17591928 * Registers theme support for a given feature.
    17601929 *
     
    17681937 * @since 4.1.0 The `title-tag` feature was added
    17691938 * @since 4.5.0 The `customize-selective-refresh-widgets` feature was added
     1939 * @since 4.7.0 The `starter-content` feature was added
    17701940 *
    17711941 * @global array $_wp_theme_features
     
    17731943 * @param string $feature  The feature being added. Likely core values include 'post-formats',
    17741944 *                         'post-thumbnails', 'html5', 'custom-logo', 'custom-header-uploads',
    1775  *                         'custom-header', 'custom-background', 'title-tag', etc.
     1945 *                         'custom-header', 'custom-background', 'title-tag', 'starter-content', etc.
    17761946 * @param mixed  $args,... Optional extra arguments to pass along with certain features.
    17771947 * @return void|bool False on failure, void otherwise.
     
    22052375     * The dynamic portion of the hook name, `$feature`, refers to the specific theme
    22062376     * feature. Possible values include 'post-formats', 'post-thumbnails', 'custom-background',
    2207      * 'custom-header', 'menus', 'automatic-feed-links', 'html5', and `customize-selective-refresh-widgets`.
     2377     * 'custom-header', 'menus', 'automatic-feed-links', 'html5',
     2378     * 'starter-content', and 'customize-selective-refresh-widgets'.
    22082379     *
    22092380     * @since 3.4.0
Note: See TracChangeset for help on using the changeset viewer.