Make WordPress Core

Changeset 41626


Ignore:
Timestamp:
09/27/2017 10:24:37 PM (7 years ago)
Author:
westonruter
Message:

Customize: Introduce drafting and scheduling for Customizer changesets.

  • Incorporates code from the Customize Snapshots and Customize Posts feature plugins.
  • Adds a new Publish Settings section for managing the changeset status, scheduled date, and frontend preview link.
  • Updates Publish button to reflect the status selected in the Publish Settings (including Save Draft and Schedule).
  • Deactivates the Themes section when a non-publish status selected, and deactivates the Publish Settings section when previewing a theme switch.
  • Introduces an outer section type (wp.customize.OuterSection in JS) for the Publish Settings section to use and for available widgets and available nav menu panels to use in the future. These sections can be expanded while other sections are expanded.
  • Introduces WP_Customize_Date_Time_Control in PHP and wp.customize.DateTimeControl in JS for managing a date/time value.
  • Keeps track of scheduled time and proactively publish from the client when the time arrives, as opposed to waiting for WP Cron.
  • Auto-publishes a scheduled changeset when attempting to access one that missed its schedule.
  • Starts a new changeset if attempting to save a changeset that was previously publish.
  • Adds force arg to requestChangesetUpdate() to force an update request even when there are no pending changes.
  • Adds utils methods for getCurrentTimestamp and getRemainingTime.
  • Adds new state values for selectedChangesetStatus, changesetDate, selectedChangesetDate.
  • Fixes logic for when to short-circuit check to close Customizer when there are unsaved changes.
  • Adds getter methods for autosaved and branching parameters, with the latter applying the customize_changeset_branching filter.
  • Call to establish_loaded_changeset on the fly when changeset_uuid() is called if no changeset UUID was specififed.
  • De-duplicates logic for dismissing auto-draft changesets.
  • Includes unit tests.

Builds on [41597].
Props sayedwp, westonruter, melchoyce, JoshuaWold, folletto, stubgo, karmatosed, dlh, paaljoachim, afercia, johnregan3, utkarshpatel, valendesigns.
See #30937.
Fixes #39896, #28721, #39275.

Location:
trunk
Files:
1 added
12 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/css/customize-controls.css

    r41602 r41626  
    2525}
    2626
    27 #customize-header-actions .button-primary {
     27#customize-save-button-wrapper {
    2828    float: right;
    2929    margin-top: 9px;
     30}
     31
     32#customize-save-button-wrapper .save {
     33    float: left;
     34    border-radius: 3px;
     35    box-shadow: none; /* @todo Adjust box shadow based on the disable states of paired button. */
     36    display: none; /* Shown when ready. */
     37    margin-top: 0;
     38}
     39#customize-save-button-wrapper .save.has-next-sibling {
     40    border-radius: 3px 0 0 3px;
     41}
     42
     43#customize-outer-theme-controls-wrapper {
     44    position: absolute;
     45    top: 0;
     46    bottom: 0;
     47    left: -301px;
     48    visibility: hidden;
     49    overflow-x: hidden;
     50    overflow-y: auto;
     51    width: 300px;
     52    margin: 0;
     53    z-index: 4;
     54    background: #eee;
     55    transition: left .18s;
     56    border-right: 1px solid #ddd;
     57}
     58
     59.outer-section-open .wp-full-overlay.expanded {
     60    margin-left: 300px;
     61}
     62
     63#customize-theme-controls .control-section-outer {
     64    display: none !important;
     65}
     66
     67#customize-outer-theme-controls .accordion-section-content {
     68    padding: 12px;
     69}
     70
     71#customize-outer-theme-controls .accordion-section-content.open {
     72    display: block;
     73}
     74
     75.outer-section-open .wp-full-overlay.expanded #customize-outer-theme-controls-wrapper {
     76    visibility: visible;
     77    left: 0;
     78    transition: left .18s;
     79}
     80
     81.customize-outer-pane-parent {
     82    margin: 0;
     83}
     84
     85.outer-section-open .wp-full-overlay.expanded #customize-preview {
     86    opacity: 0.4;
     87}
     88
     89body.outer-section-open .wp-full-overlay.expanded .wp-full-overlay-main {
     90    left: 300px;
     91}
     92
     93#customize-outer-theme-controls li.notice {
     94    padding-top: 8px;
     95    padding-bottom: 8px;
     96    margin-left: 0;
     97    margin-bottom: 10px;
     98}
     99
     100#publish-settings {
     101    text-indent: 0;
     102    border-radius: 0 3px 3px 0;
     103    padding-left: 0;
     104    padding-right: 0;
     105    box-shadow: none; /* @todo Adjust box shadow based on the disable states of paired button. */
     106    font-size: 14px;
     107    width: 30px;
     108    float: left;
     109    display: none; /* Shown when ready. */
     110    -webkit-transform: none;
     111    transform: none;
     112    margin-top: 0;
    30113}
    31114
     
    54137}
    55138
     139#customize-control-changeset_status label,
     140#customize-control-changeset_preview_link input {
     141    background-color: #ffffff;
     142    border-bottom: 1px solid #ddd;
     143    box-sizing: content-box;
     144    width: 100%;
     145    margin-left: -12px;
     146    padding-left: 12px;
     147    padding-right: 12px;
     148}
     149
     150#customize-controls .date-input:invalid {
     151    border-color: red;
     152}
     153
     154.date-time-fields .month-field {
     155    width: 79px;
     156}
     157
     158.date-time-fields .day-field,
     159.date-time-fields .hour-field,
     160.date-time-fields .minute-field {
     161    width: 46px;
     162}
     163
     164.date-time-fields .year-field {
     165    width: 60px;
     166}
     167
     168.date-time-fields .am-pm-field {
     169    width: 53px;
     170}
     171
     172#customize-control-changeset_status label {
     173    padding-top: 10px;
     174    padding-bottom: 10px;
     175    font-weight: 500;
     176}
     177
     178#customize-control-changeset_status label:first-of-type {
     179    border-top: 1px solid #ddd;
     180}
     181
     182#customize-control-changeset_status .customize-control-title {
     183    margin-bottom: 6px;
     184}
     185
     186#customize-control-changeset_status input {
     187    margin-left: 0;
     188}
     189
     190#customize-control-changeset_preview_link {
     191    position: relative;
     192    display: block;
     193}
     194
     195.customize-copy-preview-link {
     196    position: absolute;
     197    bottom: 9px;
     198    right: 0;
     199}
     200
     201.customize-copy-preview-link:before,
     202.customize-copy-preview-link:after {
     203    content: '';
     204    height: 28px;
     205    position: absolute;
     206    background: #ffffff;
     207    top: -1px;
     208}
     209
     210.customize-copy-preview-link:before {
     211    left: -10px;
     212    width: 9px;
     213    opacity: 0.75;
     214}
     215
     216.customize-copy-preview-link:after {
     217    left: -5px;
     218    width: 4px;
     219    opacity: 0.8;
     220}
     221
     222#customize-control-changeset_preview_link input {
     223    line-height: 2.5;
     224    border-top: 1px solid #ddd;
     225    border-left: none;
     226    border-right: none;
     227    text-indent: -999px;
     228    color: white;
     229}
     230
     231#customize-control-changeset_preview_link label {
     232    position: relative;
     233    display: block;
     234}
     235
     236#customize-control-changeset_preview_link a.preview-control-element {
     237    display: inline-block;
     238    position: absolute;
     239    white-space: nowrap;
     240    overflow: hidden;
     241    width: 217px;
     242    bottom: 14px;
     243    font-size: 14px;
     244    text-decoration: none;
     245}
     246
     247#customize-control-changeset_preview_link a.preview-control-element.disabled,
     248#customize-control-changeset_preview_link a.preview-control-element.disabled:active,
     249#customize-control-changeset_preview_link a.preview-control-element.disabled:focus,
     250#customize-control-changeset_preview_link a.preview-control-element.disabled:visited {
     251    color: black;
     252    opacity: 0.4;
     253    cursor: default;
     254    outline: none;
     255    box-shadow: none;
     256}
     257
     258#sub-accordion-section-publish_settings .customize-section-description-container {
     259    display: none;
     260}
     261
    56262#customize-controls .customize-info.section-meta {
    57263    margin-bottom: 15px;
     264}
     265
     266.date-time-fields {
     267    padding-top: 10px;
     268    padding-bottom:10px;
     269}
     270
     271.date-time-fields label,
     272.date-time-fields .date-time-separator {
     273    float: left;
     274    margin-right:5px;
     275}
     276
     277.date-time-fields .date-time-separator {
     278    line-height: 2;
     279}
     280
     281.date-time-fields .time-row {
     282    padding-top: 12px;
     283}
     284
     285.date-time-fields .date-timezone {
     286    float: left;
     287    line-height: 2.2;
     288    text-decoration: none;
     289}
     290
     291#customize-control-changeset_preview_link {
     292    margin-top: 20px;
     293}
     294
     295#customize-control-changeset_status {
     296    margin-bottom: 0;
     297    padding-bottom: 0;
     298}
     299
     300#customize-control-changeset_scheduled_date {
     301    box-sizing: content-box;
     302    width: 100%;
     303    margin-left: -12px;
     304    padding: 12px 12px 18px;
     305    background: #ffffff;
     306    border-bottom: 1px solid #ddd;
     307    margin-bottom: 0;
     308}
     309
     310#customize-control-changeset_scheduled_date .customize-control-description {
     311    font-style: normal;
    58312}
    59313
     
    106360#customize-controls .customize-pane-child .customize-section-title h3,
    107361#customize-controls .customize-pane-child h3.customize-section-title,
     362#customize-outer-theme-controls .customize-pane-child .customize-section-title h3,
     363#customize-outer-theme-controls .customize-pane-child h3.customize-section-title,
    108364#customize-controls .customize-info .panel-title {
    109365    font-size: 20px;
     
    151407#customize-controls .customize-info .customize-panel-description,
    152408#customize-controls .customize-info .customize-section-description,
     409#customize-outer-theme-controls .customize-info .customize-section-description,
    153410#customize-controls .no-widget-areas-rendered-notice {
    154411    color: #555d66;
     
    172429}
    173430
    174 #customize-controls .customize-info .customize-section-description {
     431#customize-controls .customize-info .customize-section-description,
     432#customize-outer-theme-controls .customize-section-description {
    175433    margin-bottom: 15px;
    176434}
     
    190448}
    191449
    192 #customize-theme-controls .control-section {
     450#customize-theme-controls .control-section,
     451#customize-outer-theme-controls .control-section {
    193452    border: none;
    194453}
    195454
    196 #customize-theme-controls .accordion-section-title {
     455#customize-theme-controls .accordion-section-title,
     456#customize-outer-theme-controls .accordion-section-title {
    197457    color: #555d66;
    198458    background-color: #fff;
     
    210470}
    211471
    212 #customize-theme-controls .accordion-section-title:after {
     472#customize-theme-controls .accordion-section-title:after,
     473#customize-outer-theme-controls .accordion-section-title:after {
    213474    content: "\f345";
    214475    color: #a0a5aa;
    215476}
    216477
    217 #customize-theme-controls .accordion-section-content {
     478#customize-theme-controls .accordion-section-content,
     479#customize-outer-theme-controls .accordion-section-content {
    218480    color: #555d66;
    219481    background: transparent;
     
    223485#customize-controls .control-section .accordion-section-title:hover,
    224486#customize-controls .control-section.open .accordion-section-title,
     487#customize-outer-theme-controls .control-section .accordion-section-title:hover,
     488#customize-outer-theme-controls .control-section.open .accordion-section-title,
     489#customize-outer-theme-controls .control-section .accordion-section-title:focus,
    225490#customize-controls .control-section .accordion-section-title:focus {
    226491    color: #0073aa;
     
    243508#customize-theme-controls .control-section .accordion-section-title:hover:after,
    244509#customize-theme-controls .control-section.open .accordion-section-title:after,
    245 #customize-theme-controls .control-section .accordion-section-title:focus:after {
     510#customize-theme-controls .control-section .accordion-section-title:focus:after,
     511#customize-outer-theme-controls .control-section:hover > .accordion-section-title:after,
     512#customize-outer-theme-controls .control-section .accordion-section-title:hover:after,
     513#customize-outer-theme-controls .control-section.open .accordion-section-title:after,
     514#customize-outer-theme-controls .control-section .accordion-section-title:focus:after {
    246515    color: #0073aa;
    247516}
     
    251520}
    252521
    253 #customize-theme-controls .control-section.open .accordion-section-title {
     522#customize-theme-controls .control-section.open .accordion-section-title,
     523#customize-outer-theme-controls .control-section.open .accordion-section-title {
    254524    border-bottom-color: #eee !important;
    255525}
     
    8291099}
    8301100
     1101.wp-full-overlay.collapsed #customize-controls #customize-notifications-area {
     1102    display: none !important;
     1103}
     1104
    8311105#customize-controls #customize-notifications-area,
    8321106#customize-controls .customize-section-title > .customize-control-notifications-container,
     
    11201394}
    11211395
    1122 @-webkit-keyframes dice-color-change {
    1123     0% { color: #d4b146; }
    1124     50% { color: #ef54b0; }
    1125     75% { color: #7190d3; }
    1126     100% { color: #d4b146; }
    1127 }
    1128 
    1129 @keyframes dice-color-change {
    1130     0% { color: #d4b146; }
    1131     50% { color: #ef54b0; }
    1132     75% { color: #7190d3; }
    1133     100% { color: #d4b146; }
     1396.button-see-me {
     1397    -webkit-animation: bounce .7s 1;
     1398    animation: bounce .7s 1;
     1399    -webkit-transform-origin: center bottom;
     1400    transform-origin: center bottom;
     1401}
     1402
     1403@-webkit-keyframes bounce {
     1404    from, 20%, 53%, 80%, to {
     1405        -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
     1406        -webkit-transform: translate3d(0,0,0);
     1407    }
     1408
     1409    40%, 43% {
     1410        -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
     1411        -webkit-transform: translate3d(0, -12px, 0);
     1412    }
     1413
     1414    70% {
     1415        -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
     1416        -webkit-transform: translate3d(0, -6px, 0);
     1417    }
     1418
     1419    90% {
     1420        -webkit-transform: translate3d(0,-1px,0);
     1421    }
     1422}
     1423
     1424@keyframes bounce {
     1425    from, 20%, 53%, 80%, to {
     1426        -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
     1427        animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
     1428        -webkit-transform: translate3d(0,0,0);
     1429        transform: translate3d(0,0,0);
     1430    }
     1431
     1432    40%, 43% {
     1433        -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
     1434        animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
     1435        -webkit-transform: translate3d(0, -12px, 0);
     1436        transform: translate3d(0, -12px, 0);
     1437    }
     1438
     1439    70% {
     1440        -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
     1441        animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
     1442        -webkit-transform: translate3d(0, -6px, 0);
     1443        transform: translate3d(0, -6px, 0);
     1444    }
     1445
     1446    90% {
     1447        -webkit-transform: translate3d(0,-1px,0);
     1448        transform: translate3d(0,-1px,0);
     1449    }
    11341450}
    11351451
     
    13111627
    13121628#customize-controls .control-section-themes .accordion-section-title span.customize-action,
    1313 #customize-controls .customize-section-title span.customize-action {
     1629#customize-controls .customize-section-title span.customize-action,
     1630#customize-outer-theme-controls .customize-section-title span.customize-action {
    13141631    font-size: 13px;
    13151632    display: block;
     
    18442161    }
    18452162
     2163    .customize-control .date-time-fields select {
     2164        height: 39px;
     2165    }
     2166
     2167    .date-time-fields .month-field {
     2168        width: 79px;
     2169    }
     2170
     2171    .date-time-fields .day-field,
     2172    .date-time-fields .hour-field,
     2173    .date-time-fields .minute-field {
     2174        width: 55px;
     2175    }
     2176
     2177    .date-time-fields .year-field {
     2178        width: 80px;
     2179    }
     2180
     2181    .date-time-fields .date-timezone {
     2182        line-height: 3.2;
     2183    }
    18462184    .wp-core-ui.wp-customizer .button {
    18472185        margin-top: 12px;
     
    18542192    }
    18552193
    1856     .wp-full-overlay.expanded {
     2194    .wp-full-overlay.expanded,
     2195    .outer-section-open .wp-full-overlay.expanded {
    18572196        margin-left: 0;
    18582197    }
     
    19322271    }
    19332272
    1934     #customize-header-actions .button-primary {
    1935         margin-top: 6px;
     2273    #publish-settings {
     2274        height: 31px;
     2275    }
     2276
     2277    #customize-control-changeset_status label {
     2278        padding-top: 15px;
    19362279    }
    19372280
    19382281    body.adding-widget div#available-widgets,
    1939     body.adding-menu-items div#available-menu-items {
     2282    body.adding-menu-items div#available-menu-items,
     2283    body.outer-section-open div#customize-outer-theme-controls-wrapper {
    19402284        top: 46px;
    19412285        left: 0;
  • trunk/src/wp-admin/customize.php

    r41374 r41626  
    2828
    2929if ( $wp_customize->changeset_post_id() ) {
    30     if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() ) ) {
     30    $changeset_post = get_post( $wp_customize->changeset_post_id() );
     31
     32    if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $changeset_post->ID ) ) {
    3133        wp_die(
    3234            '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
     
    3537        );
    3638    }
    37     if ( in_array( get_post_status( $wp_customize->changeset_post_id() ), array( 'publish', 'trash' ), true ) ) {
     39
     40    $missed_schedule = (
     41        'future' === $changeset_post->post_status &&
     42        get_post_time( 'G', true, $changeset_post ) < time()
     43    );
     44    if ( $missed_schedule ) {
     45        wp_publish_post( $changeset_post->ID );
     46        wp_die(
     47            '<h1>' . __( 'Your scheduled changes just published' ) . '</h1>' .
     48            '<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>',
     49            200
     50        );
     51    }
     52
     53    if ( in_array( get_post_status( $changeset_post->ID ), array( 'publish', 'trash' ), true ) ) {
    3854        wp_die(
    3955            '<h1>' . __( 'Cheatin&#8217; uh?' ) . '</h1>' .
     
    133149    <form id="customize-controls" class="wrap wp-full-overlay-sidebar">
    134150        <div id="customize-header-actions" class="wp-full-overlay-header">
    135             <?php
    136             $save_text = $wp_customize->is_theme_active() ? __( 'Save &amp; Publish' ) : __( 'Save &amp; Activate' );
    137             $save_attrs = array();
    138             if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
    139                 $save_attrs['style'] = 'display: none';
    140             }
    141             submit_button( $save_text, 'primary save', 'save', false, $save_attrs );
    142             ?>
     151            <?php $save_text = $wp_customize->is_theme_active() ? __( 'Publish' ) : __( 'Activate &amp; Publish' ); ?>
     152            <div id="customize-save-button-wrapper" class="customize-save-button-wrapper" >
     153                <?php submit_button( $save_text, 'primary save', 'save', false ); ?>
     154                <button id="publish-settings" class="publish-settings button-primary button dashicons dashicons-admin-generic" aria-label="<?php esc_attr_e( 'Publish Settings' ); ?>" aria-expanded="false" disabled></button>
     155            </div>
    143156            <span class="spinner"></span>
    144157            <button type="button" class="customize-controls-preview-toggle">
     
    204217    </form>
    205218    <div id="customize-preview" class="wp-full-overlay-main"></div>
     219    <div id="customize-sidebar-outer-content">
     220        <div id="customize-outer-theme-controls-wrapper">
     221            <div id="customize-outer-theme-controls">
     222                <ul class="customize-outer-pane-parent"><?php // Outer panel and sections are not implemented, but its here as a placeholder to avoid any side-effect in api.Section. ?></ul>
     223            </div>
     224        </div>
     225    </div>
    206226    <?php
    207227
  • trunk/src/wp-admin/js/customize-controls.js

    r41603 r41626  
    360360     * @param {object}  [args] - Additional options for the save request.
    361361     * @param {boolean} [args.autosave=false] - Whether changes will be stored in autosave revision if the changeset has been promoted from an auto-draft.
     362     * @param {boolean} [args.force=false] - Send request to update even when there are no changes to submit. This can be used to request the latest status of the changeset on the server.
    362363     * @param {string}  [args.title] - Title to update in the changeset. Optional.
    363364     * @param {string}  [args.date] - Date to update in the changeset. Optional.
     
    371372            title: null,
    372373            date: null,
    373             autosave: false
     374            autosave: false,
     375            force: false
    374376        }, args );
    375377
     
    389391        } );
    390392
     393        // Allow plugins to attach additional params to the settings.
     394        api.trigger( 'changeset-save', submittedChanges, submittedArgs );
     395
    391396        // Short-circuit when there are no pending changes.
    392         if ( _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) {
     397        if ( ! submittedArgs.force && _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) {
    393398            deferred.resolve( {} );
    394399            return deferred.promise();
    395400        }
    396 
    397         // Allow plugins to attach additional params to the settings.
    398         api.trigger( 'changeset-save', submittedChanges, submittedArgs );
    399401
    400402        // A status would cause a revision to be made, and for this wp.customize.previewer.save() should be used. Status is also disallowed for revisions regardless.
     
    441443
    442444            api.state( 'changesetStatus' ).set( data.changeset_status );
     445
     446            if ( data.changeset_date ) {
     447                api.state( 'changesetDate' ).set( data.changeset_date );
     448            }
     449
    443450            deferred.resolve( data );
    444451            api.trigger( 'changeset-saved', data );
     
    583590        );
    584591        return equal;
     592    };
     593
     594    /**
     595     * Get current timestamp adjusted for server clock time.
     596     *
     597     * Same functionality as the `current_time( 'mysql', false )` function in PHP.
     598     *
     599     * @since 4.9.0
     600     *
     601     * @returns {int} Current timestamp.
     602     */
     603    api.utils.getCurrentTimestamp = function getCurrentTimestamp() {
     604        var currentDate, currentClientTimestamp, timestampDifferential;
     605        currentClientTimestamp = _.now();
     606        currentDate = new Date( api.settings.initialServerDate.replace( /-/g, '/' ) );
     607        timestampDifferential = currentClientTimestamp - api.settings.initialClientTimestamp;
     608        timestampDifferential += api.settings.initialClientTimestamp - api.settings.initialServerTimestamp;
     609        currentDate.setTime( currentDate.getTime() + timestampDifferential );
     610        return currentDate.getTime();
     611    };
     612
     613    /**
     614     * Get remaining time of when the date is set.
     615     *
     616     * @since 4.9.0
     617     *
     618     * @param {string|int|Date} datetime - Date time or timestamp of the future date.
     619     * @return {int} remainingTime - Remaining time in milliseconds.
     620     */
     621    api.utils.getRemainingTime = function getRemainingTime( datetime ) {
     622        var millisecondsDivider = 1000, remainingTime, timestamp;
     623        if ( datetime instanceof Date ) {
     624            timestamp = datetime.getTime();
     625        } else if ( 'string' === typeof datetime ) {
     626            timestamp = ( new Date( datetime.replace( /-/g, '/' ) ) ).getTime();
     627        } else {
     628            timestamp = datetime;
     629        }
     630
     631        remainingTime = timestamp - api.utils.getCurrentTimestamp();
     632        remainingTime = Math.ceil( remainingTime / millisecondsDivider );
     633        return remainingTime;
    585634    };
    586635
     
    10811130    api.Section = Container.extend({
    10821131        containerType: 'section',
     1132        containerParent: '#customize-theme-controls',
     1133        containerPaneParent: '.customize-pane-parent',
    10831134        defaults: {
    10841135            title: '',
     
    11331184        embed: function () {
    11341185            var inject,
    1135                 section = this,
    1136                 container = $( '#customize-theme-controls' );
     1186                section = this;
     1187
     1188            section.containerParent = api.ensure( section.containerParent );
    11371189
    11381190            // Watch for changes to the panel state
     
    11491201                            }
    11501202                            if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
    1151                                 container.append( section.contentContainer );
     1203                                section.containerParent.append( section.contentContainer );
    11521204                            }
    11531205                            section.deferred.embedded.resolve();
     
    11561208                } else {
    11571209                    // There is no panel, so embed the section in the root of the customizer
    1158                     parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
     1210                    parentContainer = api.ensure( section.containerPaneParent );
    11591211                    if ( ! section.headContainer.parent().is( parentContainer ) ) {
    11601212                        parentContainer.append( section.headContainer );
    11611213                    }
    11621214                    if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
    1163                         container.append( section.contentContainer );
     1215                        section.containerParent.append( section.contentContainer );
    11641216                    }
    11651217                    section.deferred.embedded.resolve();
     
    12981350                    });
    12991351                } else {
    1300                     api.panel.each( function( panel ) {
    1301                         panel.collapse();
    1302                     });
     1352                    if ( ! args.allowMultiple ) {
     1353                        api.panel.each( function( panel ) {
     1354                            panel.collapse();
     1355                        });
     1356                    }
    13031357                    expand();
    13041358                }
     
    18371891                }
    18381892            });
     1893        }
     1894    });
     1895
     1896    /**
     1897     * Class wp.customize.OuterSection.
     1898     *
     1899     * Creates section outside of the sidebar, there is no ui to trigger collapse/expand so
     1900     * it would require custom handling.
     1901     *
     1902     * @since 4.9
     1903     *
     1904     * @constructor
     1905     * @augments wp.customize.Section
     1906     * @augments wp.customize.Container
     1907     */
     1908    api.OuterSection = api.Section.extend({
     1909
     1910        /**
     1911         * Initialize.
     1912         *
     1913         * @since 4.9.0
     1914         *
     1915         * @returns {void}
     1916         */
     1917        initialize: function() {
     1918            var section = this;
     1919            section.containerParent = '#customize-outer-theme-controls';
     1920            section.containerPaneParent = '.customize-outer-pane-parent';
     1921            return api.Section.prototype.initialize.apply( section, arguments );
     1922        },
     1923
     1924        /**
     1925         * Overrides api.Section.prototype.onChangeExpanded to prevent collapse/expand effect
     1926         * on other sections and panels.
     1927         *
     1928         * @since 4.9.0
     1929         *
     1930         * @param {Boolean}  expanded - The expanded state to transition to.
     1931         * @param {Object}   [args] - Args.
     1932         * @param {boolean}  [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
     1933         * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
     1934         * @param {Object}   [args.duration] - The duration for the animation.
     1935         */
     1936        onChangeExpanded: function( expanded, args ) {
     1937            var section = this,
     1938                container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
     1939                content = section.contentContainer,
     1940                backBtn = content.find( '.customize-section-back' ),
     1941                sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
     1942                body = $( 'body' ),
     1943                expand, panel;
     1944
     1945            body.toggleClass( 'outer-section-open', expanded );
     1946            section.container.toggleClass( 'open', expanded );
     1947            section.container.removeClass( 'busy' );
     1948            api.section.each( function( _section ) {
     1949                if ( 'outer' === _section.params.type && _section.id !== section.id ) {
     1950                    _section.container.removeClass( 'open' );
     1951                }
     1952            } );
     1953
     1954            if ( expanded && ! content.hasClass( 'open' ) ) {
     1955
     1956                if ( args.unchanged ) {
     1957                    expand = args.completeCallback;
     1958                } else {
     1959                    expand = $.proxy( function() {
     1960                        section._animateChangeExpanded( function() {
     1961                            sectionTitle.attr( 'tabindex', '-1' );
     1962                            backBtn.attr( 'tabindex', '0' );
     1963
     1964                            backBtn.focus();
     1965                            content.css( 'top', '' );
     1966                            container.scrollTop( 0 );
     1967
     1968                            if ( args.completeCallback ) {
     1969                                args.completeCallback();
     1970                            }
     1971                        } );
     1972
     1973                        content.addClass( 'open' );
     1974                    }, this );
     1975                }
     1976
     1977                if ( section.panel() ) {
     1978                    api.panel( section.panel() ).expand({
     1979                        duration: args.duration,
     1980                        completeCallback: expand
     1981                    });
     1982                } else {
     1983                    expand();
     1984                }
     1985
     1986            } else if ( ! expanded && content.hasClass( 'open' ) ) {
     1987                if ( section.panel() ) {
     1988                    panel = api.panel( section.panel() );
     1989                    if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
     1990                        panel.collapse();
     1991                    }
     1992                }
     1993                section._animateChangeExpanded( function() {
     1994                    backBtn.attr( 'tabindex', '-1' );
     1995                    sectionTitle.attr( 'tabindex', '0' );
     1996
     1997                    sectionTitle.focus();
     1998                    content.css( 'top', '' );
     1999
     2000                    if ( args.completeCallback ) {
     2001                        args.completeCallback();
     2002                    }
     2003                } );
     2004
     2005                content.removeClass( 'open' );
     2006
     2007            } else {
     2008                if ( args.completeCallback ) {
     2009                    args.completeCallback();
     2010                }
     2011            }
    18392012        }
    18402013    });
     
    39614134    });
    39624135
     4136    /**
     4137     * Class wp.customize.DateTimeControl.
     4138     *
     4139     * @since 4.9.0
     4140     * @constructor
     4141     * @augments wp.customize.Control
     4142     * @augments wp.customize.Class
     4143     */
     4144    api.DateTimeControl = api.Control.extend({
     4145
     4146        dateInputs: {},
     4147        inputElements: {},
     4148        invalidDate: false,
     4149
     4150        /**
     4151         * Initialize behaviors.
     4152         *
     4153         * @since 4.9.0
     4154         * @returns {void}
     4155         */
     4156        ready: function ready() {
     4157            var control = this;
     4158
     4159            _.bindAll( control, 'populateSetting', 'updateDaysForMonth', 'updateMinutesForHour' );
     4160
     4161            control.dateInputs = control.container.find( '.date-input' );
     4162
     4163            // @todo This needs https://core.trac.wordpress.org/ticket/37964
     4164            if ( ! control.setting ) {
     4165                control.setting = new api.Value();
     4166            }
     4167
     4168            if ( ! control.setting.get() && control.params.defaultValue ) {
     4169                control.setting.set( control.params.defaultValue );
     4170            }
     4171
     4172            control.dateInputs.each( function() {
     4173                var input = $( this ), component, element;
     4174                component = input.data( 'component' );
     4175                element = new api.Element( input );
     4176                element.validate = function( value ) {
     4177                    return _.contains( [ 'am', 'pm' ], value ) ? value : parseInt( value, 10 );
     4178                };
     4179                control.inputElements[ component ] = element;
     4180                control.elements.push( element );
     4181            } );
     4182
     4183            control.dateInputs.on( 'input', control.populateSetting );
     4184            control.inputElements.month.bind( control.updateDaysForMonth );
     4185            control.inputElements.year.bind( control.updateDaysForMonth );
     4186            control.inputElements.hour.bind( control.updateMinutesForHour );
     4187            control.populateDateInputs();
     4188        },
     4189
     4190        /**
     4191         * Parse datetime string.
     4192         *
     4193         * @since 4.9.0
     4194         * @param {string} datetime Date/Time string. Accepts Y-m-d H:i:s format.
     4195         * @param {boolean} twelveHourFormat If twelve hour format array is required.
     4196         * @returns {object|null} Returns object containing date components or null if parse error.
     4197         */
     4198        parseDateTime: function parseDateTime( datetime, twelveHourFormat ) {
     4199            var matches, date, midDayHour = 12;
     4200
     4201            if ( datetime ) {
     4202                matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/ );
     4203            }
     4204
     4205            if ( ! matches ) {
     4206                return null;
     4207            }
     4208
     4209            matches.shift();
     4210
     4211            date = {
     4212                year: matches.shift(),
     4213                month: matches.shift(),
     4214                day: matches.shift(),
     4215                hour: matches.shift(),
     4216                minute: matches.shift(),
     4217                second: matches.shift()
     4218            };
     4219
     4220            if ( twelveHourFormat ) {
     4221                date.hour = parseInt( date.hour, 10 );
     4222                date.ampm = date.hour >= midDayHour ? 'pm' : 'am';
     4223                date.hour = date.hour % midDayHour ? String( date.hour % midDayHour ) : String( midDayHour );
     4224                delete date.second;
     4225            }
     4226
     4227            return date;
     4228        },
     4229
     4230        /**
     4231         * Validates if input components have valid date and time.
     4232         *
     4233         * @since 4.9.0
     4234         * @return {boolean} If date input fields has error.
     4235         */
     4236        validateInputs: function validateInputs() {
     4237            var control = this, errorMessage;
     4238
     4239            control.invalidDate = false;
     4240
     4241            _.each( [ 'day', 'hour', 'year', 'minute' ], function( component ) {
     4242                var element, el, max, min, maxLength, value;
     4243
     4244                if ( ! control.invalidDate ) {
     4245                    element = control.inputElements[ component ];
     4246                    el = element.element.get( 0 );
     4247                    max = parseInt( element.element.attr( 'max' ), 10 );
     4248                    min = parseInt( element.element.attr( 'min' ), 10 );
     4249                    maxLength = parseInt( element.element.attr( 'maxlength' ), 10 );
     4250                    value = element();
     4251                    control.invalidDate = value > max || value < min || String( value ).length > maxLength;
     4252                    errorMessage = control.invalidDate ? api.l10n.invalid + ' ' + component : '';
     4253
     4254                    el.setCustomValidity( errorMessage );
     4255                    _.result( el, 'reportValidity' );
     4256                }
     4257            } );
     4258
     4259            return control.invalidDate;
     4260        },
     4261
     4262        /**
     4263         * Updates number of days according to the month and year selected.
     4264         *
     4265         * @since 4.9.0
     4266         * @return {void}
     4267         */
     4268        updateDaysForMonth: function updateDaysForMonth() {
     4269            var control = this, daysInMonth, year, month, day;
     4270
     4271            month = control.inputElements.month();
     4272            year = control.inputElements.year();
     4273            day = control.inputElements.day();
     4274
     4275            if ( month && year ) {
     4276                daysInMonth = new Date( year, month, 0 ).getDate();
     4277                control.inputElements.day.element.attr( 'max', daysInMonth );
     4278
     4279                if ( day > daysInMonth ) {
     4280                    control.inputElements.day( daysInMonth );
     4281                }
     4282            }
     4283        },
     4284
     4285        /**
     4286         * Updates number of minutes according to the hour selected.
     4287         *
     4288         * @since 4.9.0
     4289         * @return {void}
     4290         */
     4291        updateMinutesForHour: function updateMinutesForHour() {
     4292            var control = this, maxHours = 24, minuteEl;
     4293
     4294            if ( control.inputElements.ampm ) {
     4295                return;
     4296            }
     4297
     4298            minuteEl = control.inputElements.minute.element;
     4299
     4300            if ( maxHours === control.inputElements.hour() ) {
     4301                control.inputElements.minute( 0 );
     4302                minuteEl.data( 'default-max', minuteEl.attr( 'max' ) );
     4303                minuteEl.data( 'default-maxlength', minuteEl.attr( 'maxlength' ) );
     4304                minuteEl.attr( 'max', '0' );
     4305            } else if ( minuteEl.data( 'default-max' ) ) {
     4306                minuteEl.attr( 'max', minuteEl.data( 'default-max' ) );
     4307                minuteEl.attr( 'maxlength', minuteEl.data( 'maxlength' ) );
     4308            }
     4309        },
     4310
     4311        /**
     4312         * Populate setting value from the inputs.
     4313         *
     4314         * @since 4.9.0
     4315         * @returns {boolean} If setting updated.
     4316         */
     4317        populateSetting: function populateSetting() {
     4318            var control = this, date;
     4319
     4320            if ( control.validateInputs() || ! control.params.allowPastDate && ! control.isFutureDate() ) {
     4321                return false;
     4322            }
     4323
     4324            date = control.convertInputDateToString();
     4325            control.setting.set( date );
     4326            return true;
     4327        },
     4328
     4329        /**
     4330         * Converts input values to string in Y-m-d H:i:s format.
     4331         *
     4332         * @since 4.9.0
     4333         * @return {string} Date string.
     4334         */
     4335        convertInputDateToString: function convertInputDateToString() {
     4336            var control = this, date = '', dateFormat, hourInTwentyFourHourFormat,
     4337                getElementValue, pad;
     4338
     4339            pad = function( number, padding ) {
     4340                var zeros;
     4341                if ( String( number ).length < padding ) {
     4342                    zeros = padding - String( number ).length;
     4343                    number = Math.pow( 10, zeros ).toString().substr( 1 ) + String( number );
     4344                }
     4345                return number;
     4346            };
     4347
     4348            getElementValue = function( component ) {
     4349                var value = control.inputElements[ component ].get();
     4350
     4351                if ( _.contains( [ 'month', 'day', 'hour', 'minute' ], component ) ) {
     4352                    value = pad( value, 2 );
     4353                } else if ( 'year' === component ) {
     4354                    value = pad( value, 4 );
     4355                }
     4356                return value;
     4357            };
     4358
     4359            hourInTwentyFourHourFormat = control.inputElements.ampm ? control.convertHourToTwentyFourHourFormat( control.inputElements.hour(), control.inputElements.ampm() ) : control.inputElements.hour();
     4360            dateFormat = [ 'year', '-', 'month', '-', 'day', ' ', pad( hourInTwentyFourHourFormat, 2 ), ':', 'minute', ':', '00' ];
     4361
     4362            _.each( dateFormat, function( component ) {
     4363                date += control.inputElements[ component ] ? getElementValue( component ) : component;
     4364            } );
     4365
     4366            return date;
     4367        },
     4368
     4369        /**
     4370         * Check if the date is in the future.
     4371         *
     4372         * @since 4.9.0
     4373         * @returns {boolean} True if future date.
     4374         */
     4375        isFutureDate: function isFutureDate() {
     4376            var control = this;
     4377            return 0 < api.utils.getRemainingTime( control.convertInputDateToString() );
     4378        },
     4379
     4380        /**
     4381         * Convert hour in twelve hour format to twenty four hour format.
     4382         *
     4383         * @since 4.9.0
     4384         * @param {string} hourInTwelveHourFormat Hour in twelve hour format.
     4385         * @param {string} ampm am/pm
     4386         * @return {string} Hour in twenty four hour format.
     4387         */
     4388        convertHourToTwentyFourHourFormat: function convertHour( hourInTwelveHourFormat, ampm ) {
     4389            var hourInTwentyFourHourFormat, hour, midDayHour = 12;
     4390
     4391            hour = parseInt( hourInTwelveHourFormat, 10 );
     4392
     4393            if ( 'pm' === ampm && hour < midDayHour ) {
     4394                hourInTwentyFourHourFormat = hour + midDayHour;
     4395            } else if ( 'am' === ampm && midDayHour === hour ) {
     4396                hourInTwentyFourHourFormat = hour - midDayHour;
     4397            } else {
     4398                hourInTwentyFourHourFormat = hour;
     4399            }
     4400
     4401            return String( hourInTwentyFourHourFormat );
     4402        },
     4403
     4404        /**
     4405         * Populates date inputs in date fields.
     4406         *
     4407         * @since 4.9.0
     4408         * @returns {boolean} Whether the inputs were populated.
     4409         */
     4410        populateDateInputs: function populateDateInputs() {
     4411            var control = this, parsed;
     4412
     4413            parsed = control.parseDateTime( control.setting.get(), control.params.twelveHourFormat );
     4414
     4415            if ( ! parsed ) {
     4416                return false;
     4417            }
     4418
     4419            _.each( control.inputElements, function( element, component ) {
     4420                element.set( parsed[ component ] );
     4421            } );
     4422
     4423            return true;
     4424        },
     4425
     4426        /**
     4427         * Toggle future date notification for date control.
     4428         *
     4429         * @since 4.9.0
     4430         * @param {boolean} notify Add or remove the notification.
     4431         * @return {wp.customize.DateTimeControl}
     4432         */
     4433        toggleFutureDateNotification: function toggleFutureDateNotification( notify ) {
     4434            var control = this, notificationCode, notification;
     4435
     4436            notificationCode = 'not_future_date';
     4437
     4438            if ( notify ) {
     4439                notification = new api.Notification( notificationCode, {
     4440                    type: 'error',
     4441                    message: api.l10n.futureDateError
     4442                } );
     4443                control.notifications.add( notificationCode, notification );
     4444            } else {
     4445                control.notifications.remove( notificationCode );
     4446            }
     4447
     4448            return control;
     4449        }
     4450    });
     4451
     4452    /**
     4453     * Class PreviewLinkControl.
     4454     *
     4455     * @since 4.9.0
     4456     * @constructor
     4457     * @augments wp.customize.Control
     4458     * @augments wp.customize.Class
     4459     */
     4460    api.PreviewLinkControl = api.Control.extend({
     4461
     4462        previewElements: {},
     4463
     4464        /**
     4465         * Override the templateSelector before embedding the control into the page.
     4466         *
     4467         * @since 4.9.0
     4468         * @return {void}
     4469         */
     4470        embed: function() {
     4471            var control = this;
     4472            control.templateSelector = 'customize-preview-link-control';
     4473            return api.Control.prototype.embed.apply( control, arguments );
     4474        },
     4475
     4476        /**
     4477         * Initialize behaviors.
     4478         *
     4479         * @since 4.9.0
     4480         * @returns {void}
     4481         */
     4482        ready: function ready() {
     4483            var control = this, element, component, node, link, input, button;
     4484
     4485            _.bindAll( control, 'updatePreviewLink' );
     4486
     4487            if ( ! control.setting ) {
     4488                control.setting = new api.Value();
     4489            }
     4490
     4491            control.container.find( '.preview-control-element' ).each( function() {
     4492                node = $( this );
     4493                component = node.data( 'component' );
     4494                element = new api.Element( node );
     4495                control.previewElements[ component ] = element;
     4496                control.elements.push( element );
     4497            } );
     4498
     4499            link = control.previewElements.link;
     4500            input = control.previewElements.input;
     4501            button = control.previewElements.button;
     4502
     4503            input.link( control.setting );
     4504            link.link( control.setting );
     4505
     4506            link.bind( function( value ) {
     4507                link.element.attr( 'href', value );
     4508                link.element.attr( 'target', api.settings.changeset.uuid );
     4509            } );
     4510
     4511            api.bind( 'ready', control.updatePreviewLink );
     4512            api.bind( 'change', control.updatePreviewLink );
     4513            api.state( 'saved' ).bind( control.updatePreviewLink );
     4514
     4515            button.element.on( 'click', function( event ) {
     4516                event.preventDefault();
     4517                if ( control.setting() ) {
     4518                    input.element.select();
     4519                    document.execCommand( 'copy' );
     4520                    button( button.element.data( 'copied-text' ) );
     4521                }
     4522            } );
     4523
     4524            link.element.on( 'click', function( event ) {
     4525                if ( link.element.hasClass( 'disabled' ) ) {
     4526                    event.preventDefault();
     4527                }
     4528            } );
     4529
     4530            button.element.on( 'mouseenter', function() {
     4531                if ( control.setting() ) {
     4532                    button( button.element.data( 'copy-text' ) );
     4533                }
     4534            } );
     4535        },
     4536
     4537        /**
     4538         * Updates Preview Link
     4539         *
     4540         * @since 4.9.0
     4541         * @return {void}
     4542         */
     4543        updatePreviewLink: function updatePreviewLink() {
     4544            var control = this, unsavedDirtyValues;
     4545
     4546            unsavedDirtyValues = ! _.isEmpty( api.dirtyValues( {
     4547                unsaved: true
     4548            } ) );
     4549
     4550            control.toggleSaveNotification( unsavedDirtyValues );
     4551            control.previewElements.link.element.toggleClass( 'disabled', unsavedDirtyValues );
     4552            control.previewElements.button.element.prop( 'disabled', unsavedDirtyValues );
     4553            control.setting.set( api.previewer.getFrontendPreviewUrl() );
     4554        },
     4555
     4556        /**
     4557         * Toggles save notification.
     4558         *
     4559         * @since 4.9.0
     4560         * @param {boolean} notify Add or remove notification.
     4561         * @return {void}
     4562         */
     4563        toggleSaveNotification: function toggleSaveNotification( notify ) {
     4564            var control = this, notificationCode, notification;
     4565
     4566            notificationCode = 'changes_not_saved';
     4567
     4568            if ( notify ) {
     4569                notification = new api.Notification( notificationCode, {
     4570                    type: 'info',
     4571                    message: api.l10n.saveBeforeShare
     4572                } );
     4573                control.notifications.add( notificationCode, notification );
     4574            } else {
     4575                control.notifications.remove( notificationCode );
     4576            }
     4577        }
     4578    });
     4579
    39634580    // Change objects contained within the main customize object to Settings.
    39644581    api.defaultConstructor = api.Setting;
     
    40604677                }
    40614678            );
    4062             if ( ! api.state( 'saved' ).get() ) {
     4679            if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
    40634680                params.customize_autosaved = 'on';
    40644681            }
     
    46615278        background_position: api.BackgroundPositionControl,
    46625279        theme:               api.ThemeControl,
     5280        date_time:           api.DateTimeControl,
    46635281        code_editor:         api.CodeEditorControl
    46645282    };
    46655283    api.panelConstructor = {};
    46665284    api.sectionConstructor = {
    4667         themes: api.ThemesSection
     5285        themes: api.ThemesSection,
     5286        outer: api.OuterSection
    46685287    };
    46695288
     
    48375456    }, api );
    48385457
     5458    // Define state values.
     5459    api.state = new api.Values();
     5460    _.each( [
     5461        'saved',
     5462        'autosaved',
     5463        'saving',
     5464        'activated',
     5465        'processing',
     5466        'paneVisible',
     5467        'expandedPanel',
     5468        'expandedSection',
     5469        'changesetDate',
     5470        'selectedChangesetDate',
     5471        'changesetStatus',
     5472        'selectedChangesetStatus',
     5473        'remainingTimeToPublish',
     5474        'previewerAlive',
     5475        'editShortcutVisibility'
     5476    ], function( name ) {
     5477        api.state.create( name );
     5478    });
     5479
    48395480    $( function() {
    48405481        api.settings = window._wpCustomizeSettings;
     
    48645505            closeBtn = $( '.customize-controls-close' ),
    48655506            saveBtn = $( '#save' ),
     5507            btnWrapper = $( '#customize-save-button-wrapper' ),
     5508            publishSettingsBtn = $( '#publish-settings' ),
    48665509            footerActions = $( '#customize-footer-actions' );
     5510
     5511        saveBtn.show();
     5512
     5513        api.section( 'publish_settings', function( section ) {
     5514            var updateButtonsState, previewLinkControl, previewLinkControlId = 'changeset_preview_link';
     5515
     5516            previewLinkControl = new api.PreviewLinkControl( previewLinkControlId, {
     5517                params: {
     5518                    section: section.id,
     5519                    active: true,
     5520                    priority: 100,
     5521                    content: '<li id="customize-control-' + previewLinkControlId + '" class="customize-control"></li>'
     5522                }
     5523            } );
     5524
     5525            api.control.add( previewLinkControlId, previewLinkControl );
     5526
     5527            // Make sure publish settings are not available until the theme has been activated.
     5528            if ( ! api.settings.theme.active ) {
     5529                section.active.set( false );
     5530                section.active.link( api.state( 'activated' ) );
     5531            }
     5532
     5533            // Bind visibility of the publish settings button to whether the section is active.
     5534            updateButtonsState = function() {
     5535                publishSettingsBtn.toggle( section.active.get() );
     5536                saveBtn.toggleClass( 'has-next-sibling', section.active.get() );
     5537            };
     5538            updateButtonsState();
     5539            section.active.bind( updateButtonsState );
     5540
     5541            section.contentContainer.find( '.customize-action' ).text( api.l10n.updating );
     5542            section.contentContainer.find( '.customize-section-back' ).removeAttr( 'tabindex' );
     5543            publishSettingsBtn.prop( 'disabled', false );
     5544
     5545            publishSettingsBtn.on( 'click', function( event ) {
     5546                event.preventDefault();
     5547                section.expanded.set( ! section.expanded.get() );
     5548            } );
     5549
     5550            section.expanded.bind( function( isExpanded ) {
     5551                publishSettingsBtn.attr( 'aria-expanded', String( isExpanded ) );
     5552                publishSettingsBtn.toggleClass( 'active', isExpanded );
     5553            } );
     5554
     5555            api.state( 'changesetStatus' ).bind( function( status ) {
     5556                if ( 'publish' === status ) {
     5557                    section.collapse();
     5558                }
     5559            } );
     5560        } );
    48675561
    48685562        // Prevent the form from saving when enter is pressed on an input or select element.
     
    49245618                    customize_changeset_uuid: api.settings.changeset.uuid
    49255619                };
    4926                 if ( ! api.state( 'saved' ).get() ) {
     5620                if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
    49275621                    queryVars.customize_autosaved = 'on';
    49285622                }
     
    49605654                var previewer = this,
    49615655                    deferred = $.Deferred(),
    4962                     changesetStatus = 'publish',
     5656                    changesetStatus = api.state( 'selectedChangesetStatus' ).get(),
     5657                    selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(),
    49635658                    processing = api.state( 'processing' ),
    49645659                    submitWhenDoneProcessing,
     
    49665661                    modifiedWhileSaving = {},
    49675662                    invalidSettings = [],
    4968                     invalidControls;
     5663                    invalidControls = [],
     5664                    invalidSettingLessControls = [];
    49695665
    49705666                if ( args && args.status ) {
     
    50055701                        } );
    50065702                    } );
    5007                     invalidControls = api.findControlsForSettings( invalidSettings );
     5703
     5704                    /**
     5705                     * Find all invalid setting less controls with notification type error.
     5706                     */
     5707                    api.control.each( function( control ) {
     5708                        if ( ! control.setting || ! control.setting.id && control.active.get() ) {
     5709                            control.notifications.each( function( notification ) {
     5710                                if ( 'error' === notification.type ) {
     5711                                    invalidSettingLessControls.push( [ control ] );
     5712                                }
     5713                            } );
     5714                        }
     5715                    } );
     5716
     5717                    invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) );
    50085718                    if ( ! _.isEmpty( invalidControls ) ) {
    5009                         _.values( invalidControls )[0][0].focus();
     5719
     5720                        invalidControls[0][0].focus();
    50105721                        api.unbind( 'change', captureSettingModifiedDuringSave );
    50115722
    5012                         api.notifications.add( errorCode, new api.Notification( errorCode, {
    5013                             message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ),
    5014                             type: 'error',
    5015                             dismissible: true,
    5016                             saveFailure: true
    5017                         } ) );
     5723                        if ( invalidSettings.length ) {
     5724                            api.notifications.add( errorCode, new api.Notification( errorCode, {
     5725                                message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ),
     5726                                type: 'error',
     5727                                dismissible: true,
     5728                                saveFailure: true
     5729                            } ) );
     5730                        }
    50185731
    50195732                        deferred.rejectWith( previewer, [
     
    50325745                        customize_changeset_status: changesetStatus
    50335746                    } );
     5747
    50345748                    if ( args && args.date ) {
    50355749                        query.customize_changeset_date = args.date;
     5750                    } else if ( 'future' === changesetStatus && selectedChangesetDate ) {
     5751                        query.customize_changeset_date = selectedChangesetDate;
    50365752                    }
     5753
    50375754                    if ( args && args.title ) {
    50385755                        query.customize_changeset_title = args.title;
     
    50715788
    50725789                    request.fail( function ( response ) {
     5790                        var notification, notificationArgs;
     5791                        notificationArgs = {
     5792                            type: 'error',
     5793                            dismissible: true,
     5794                            fromServer: true,
     5795                            saveFailure: true
     5796                        };
    50735797
    50745798                        if ( '0' === response ) {
     
    50885812                            } );
    50895813                        } else if ( response.code ) {
    5090                             api.notifications.add( response.code, new api.Notification( response.code, {
    5091                                 message: response.message,
    5092                                 type: 'error',
    5093                                 dismissible: true,
    5094                                 fromServer: true,
    5095                                 saveFailure: true
     5814                            if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) {
     5815                                api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus();
     5816                            } else {
     5817                                notification = new api.Notification( response.code, _.extend( notificationArgs, {
     5818                                    message: response.message
     5819                                } ) );
     5820                            }
     5821                        } else {
     5822                            notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
     5823                                message: api.l10n.serverSaveError
    50965824                            } ) );
    5097                         } else {
    5098                             api.notifications.add( 'unknown_error', new api.Notification( 'unknown_error', {
    5099                                 message: api.l10n.serverSaveError,
    5100                                 type: 'error',
    5101                                 dismissible: true,
    5102                                 fromServer: true,
    5103                                 saveFailure: true
    5104                             } ) );
     5825                        }
     5826
     5827                        if ( notification ) {
     5828                            api.notifications.add( notification.code, notification );
    51055829                        }
    51065830
     
    51145838                        deferred.rejectWith( previewer, [ response ] );
    51155839                        api.trigger( 'error', response );
     5840
     5841                        // Start a new changeset if the underlying changeset was published.
     5842                        if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) {
     5843                            api.settings.changeset.uuid = response.next_changeset_uuid;
     5844                            api.state( 'changesetStatus' ).set( '' );
     5845                            parent.send( 'changeset-uuid', api.settings.changeset.uuid );
     5846                            api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid );
     5847                        }
    51165848                    } );
    51175849
     
    51215853
    51225854                        api.state( 'changesetStatus' ).set( response.changeset_status );
     5855                        api.state( 'changesetDate' ).set( response.changeset_date );
     5856
    51235857                        if ( 'publish' === response.changeset_status ) {
    51245858
     
    51745908
    51755909                return deferred.promise();
     5910            },
     5911
     5912            /**
     5913             * Builds the front preview url with the current state of customizer.
     5914             *
     5915             * @since 4.9
     5916             *
     5917             * @return {string} Preview url.
     5918             */
     5919            getFrontendPreviewUrl: function() {
     5920                var previewer = this,
     5921                    a = document.createElement( 'a' ),
     5922                    params = {};
     5923
     5924                if ( api.state( 'changesetStatus' ).get() && 'publish' !== api.state( 'changesetStatus' ).get() ) {
     5925                    params.customize_changeset_uuid = api.settings.changeset.uuid;
     5926                }
     5927
     5928                a.href = previewer.previewUrl();
     5929                a.search = $.param( params );
     5930
     5931                return a.href;
    51765932            }
    51775933        });
     
    53006056
    53016057        // Save and activated states
    5302         (function() {
    5303             var state = new api.Values(),
    5304                 saved = state.create( 'saved' ),
    5305                 saving = state.create( 'saving' ),
    5306                 activated = state.create( 'activated' ),
    5307                 processing = state.create( 'processing' ),
    5308                 paneVisible = state.create( 'paneVisible' ),
    5309                 expandedPanel = state.create( 'expandedPanel' ),
    5310                 expandedSection = state.create( 'expandedSection' ),
    5311                 changesetStatus = state.create( 'changesetStatus' ),
    5312                 previewerAlive = state.create( 'previewerAlive' ),
    5313                 editShortcutVisibility  = state.create( 'editShortcutVisibility' ),
     6058        (function( state ) {
     6059            var saved = state.instance( 'saved' ),
     6060                saving = state.instance( 'saving' ),
     6061                activated = state.instance( 'activated' ),
     6062                processing = state.instance( 'processing' ),
     6063                paneVisible = state.instance( 'paneVisible' ),
     6064                expandedPanel = state.instance( 'expandedPanel' ),
     6065                expandedSection = state.instance( 'expandedSection' ),
     6066                changesetStatus = state.instance( 'changesetStatus' ),
     6067                selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ),
     6068                changesetDate = state.instance( 'changesetDate' ),
     6069                selectedChangesetDate = state.instance( 'selectedChangesetDate' ),
     6070                previewerAlive = state.instance( 'previewerAlive' ),
     6071                editShortcutVisibility  = state.instance( 'editShortcutVisibility' ),
    53146072                populateChangesetUuidParam;
    53156073
    53166074            state.bind( 'change', function() {
    53176075                var canSave;
     6076
     6077                btnWrapper.removeClass( 'button-see-me' );
    53186078
    53196079                if ( ! activated() ) {
    53206080                    saveBtn.val( api.l10n.activate );
    53216081                    closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
     6082                    publishSettingsBtn.prop( 'disabled', false );
    53226083
    53236084                } else if ( '' === changesetStatus.get() && saved() ) {
    5324                     saveBtn.val( api.l10n.saved );
     6085                    if ( api.settings.changeset.currentUserCanPublish ) {
     6086                        saveBtn.val( api.l10n.published );
     6087                    } else {
     6088                        saveBtn.val( api.l10n.saved );
     6089                    }
     6090                    publishSettingsBtn.prop( 'disabled', true );
    53256091                    closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
    53266092
    53276093                } else {
    5328                     saveBtn.val( api.l10n.save );
     6094                    if ( 'draft' === selectedChangesetStatus() ) {
     6095                        if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
     6096                            saveBtn.val( api.l10n.draftSaved );
     6097                        } else {
     6098                            saveBtn.val( api.l10n.saveDraft );
     6099                        }
     6100                    } else if ( 'future' === selectedChangesetStatus() ) {
     6101                        if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
     6102                            if ( changesetDate.get() !== selectedChangesetDate.get() ) {
     6103                                saveBtn.val( api.l10n.schedule );
     6104                                btnWrapper.addClass( 'button-see-me' );
     6105                            } else {
     6106                                saveBtn.val( api.l10n.scheduled );
     6107                            }
     6108                        } else {
     6109                            btnWrapper.addClass( 'button-see-me' );
     6110                            saveBtn.val( api.l10n.schedule );
     6111                        }
     6112                    } else if ( ! api.settings.changeset.currentUserCanPublish ) {
     6113                        selectedChangesetStatus( 'draft' );
     6114                    } else {
     6115                        saveBtn.val( api.l10n.publish );
     6116                    }
    53296117                    closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
     6118                    publishSettingsBtn.prop( 'disabled', false );
    53306119                }
    53316120
     
    53346123                 * and if the theme is not active or the changeset exists but is not published.
    53356124                 */
    5336                 canSave = ! saving() && ( ! activated() || ! saved() || ( '' !== changesetStatus() && 'publish' !== changesetStatus() ) );
     6125                canSave = ! saving() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
    53376126
    53386127                saveBtn.prop( 'disabled', ! canSave );
    53396128            });
     6129
     6130            selectedChangesetStatus.validate = function( status ) {
     6131                if ( '' === status || 'auto-draft' === status ) {
     6132                    return null;
     6133                }
     6134                return status;
     6135            };
    53406136
    53416137            // Set default states.
    53426138            changesetStatus( api.settings.changeset.status );
     6139            changesetDate( api.settings.changeset.publishDate );
     6140            selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? 'publish' : api.settings.changeset.status );
     6141            selectedChangesetStatus.link( changesetStatus ); // Ensure that direct updates to status on server via wp.customizer.previewer.save() will update selection.
    53436142            saved( true );
    53446143            if ( '' === changesetStatus() ) { // Handle case for loading starter content.
     
    54256224            };
    54266225
     6226            /**
     6227             * Deactivate themes section if changeset status is not auto-draft
     6228             */
     6229            api.section( 'themes', function( section ) {
     6230                var canActivate;
     6231
     6232                canActivate = function() {
     6233                    return ! changesetStatus() || 'auto-draft' === changesetStatus();
     6234                };
     6235
     6236                section.active.validate = canActivate;
     6237                section.active.set( canActivate() );
     6238                changesetStatus.bind( function() {
     6239                    section.active.set( canActivate() );
     6240                } );
     6241            } );
     6242
    54276243            // Show changeset UUID in URL when in branching mode and there is a saved changeset.
    54286244            if ( api.settings.changeset.branching ) {
     
    54316247                } );
    54326248            }
    5433 
    5434             // Expose states to the API.
    5435             api.state = state;
    5436         }());
    5437 
    5438         // Set up autosave prompt.
     6249        }( api.state ) );
     6250
     6251        // Set up initial notifications.
    54396252        (function() {
    54406253
     
    55236336                onStateChange = function() {
    55246337                    api.notifications.remove( code );
    5525                     api.state( 'saved' ).unbind( onStateChange );
    5526                     api.state( 'saving' ).unbind( onStateChange );
     6338                    api.unbind( 'change', onStateChange );
    55276339                    api.state( 'changesetStatus' ).unbind( onStateChange );
    55286340                };
    5529                 api.state( 'saved' ).bind( onStateChange );
    5530                 api.state( 'saving' ).bind( onStateChange );
     6341                api.bind( 'change', onStateChange );
    55316342                api.state( 'changesetStatus' ).bind( onStateChange );
    55326343            }
     
    55546365            event.preventDefault();
    55556366        }).keydown( function( event ) {
    5556             if ( 9 === event.which ) // tab
     6367            if ( 9 === event.which ) { // Tab.
    55576368                return;
    5558             if ( 13 === event.which ) // enter
     6369            }
     6370            if ( 13 === event.which ) { // Enter.
    55596371                api.previewer.save();
     6372            }
    55606373            event.preventDefault();
    55616374        });
    55626375
    55636376        closeBtn.keydown( function( event ) {
    5564             if ( 9 === event.which ) // tab
     6377            if ( 9 === event.which ) { // Tab.
    55656378                return;
    5566             if ( 13 === event.which ) // enter
     6379            }
     6380            if ( 13 === event.which ) { // Enter.
    55676381                this.click();
     6382            }
    55686383            event.preventDefault();
    55696384        });
     
    59406755                 * when customize-loader.js is used.
    59416756                 */
    5942                 if ( isInsideIframe && isCleanState() ) {
     6757                if ( isInsideIframe || isCleanState() ) {
    59436758                    clearedToClose.resolve();
    59446759                } else if ( confirm( api.l10n.saveAlert ) ) {
     
    62217036            });
    62227037        })();
     7038
     7039        /**
     7040         * Publish settings section and controls.
     7041         */
     7042        api.control( 'changeset_status', 'changeset_scheduled_date', function( statusControl, dateControl ) {
     7043            $.when( statusControl.deferred.embedded, dateControl.deferred.embedded ).done( function() {
     7044                var radioNodes, statusElement, toggleDateControl, publishWhenTime, pollInterval, updateTimeArrivedPoller, timeArrivedPollingInterval = 1000;
     7045
     7046                radioNodes = statusControl.container.find( 'input[type=radio][name]' );
     7047                statusElement = new api.Element( radioNodes );
     7048                statusControl.elements.push( statusElement );
     7049
     7050                statusElement.sync( api.state( 'selectedChangesetStatus' ) );
     7051                statusElement.set( api.state( 'selectedChangesetStatus' ).get() );
     7052
     7053                dateControl.notifications.alt = true;
     7054                dateControl.deferred.embedded.done( function() {
     7055                    api.state( 'selectedChangesetDate' ).sync( dateControl.setting );
     7056                    api.state( 'selectedChangesetDate' ).set( dateControl.setting() );
     7057                } );
     7058
     7059                publishWhenTime = function() {
     7060                    var publishSettingsSection;
     7061
     7062                    api.state( 'selectedChangesetStatus' ).set( 'publish' );
     7063                    publishSettingsSection = api.section( 'publish_settings' );
     7064                    if ( publishSettingsSection ) {
     7065                        publishSettingsSection.collapse();
     7066                    }
     7067                    api.previewer.save();
     7068                };
     7069
     7070                // Start countdown for when the dateTime arrives, or clear interval when it is .
     7071                updateTimeArrivedPoller = function() {
     7072                    var shouldPoll = (
     7073                        'future' === api.state( 'changesetStatus' ).get() &&
     7074                        'future' === api.state( 'selectedChangesetStatus' ).get() &&
     7075                        api.state( 'changesetDate' ).get() &&
     7076                        api.state( 'selectedChangesetDate' ).get() === api.state( 'changesetDate' ).get() &&
     7077                        api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ) >= 0
     7078                    );
     7079
     7080                    if ( shouldPoll && ! pollInterval ) {
     7081                        pollInterval = setInterval( function() {
     7082                            var remainingTime = api.utils.getRemainingTime( api.state( 'changesetDate' ).get() );
     7083                            api.state( 'remainingTimeToPublish' ).set( remainingTime );
     7084                            if ( remainingTime <= 0 ) {
     7085                                clearInterval( pollInterval );
     7086                                pollInterval = 0;
     7087                                publishWhenTime();
     7088                            }
     7089                        }, timeArrivedPollingInterval );
     7090                    } else if ( ! shouldPoll && pollInterval ) {
     7091                        clearInterval( pollInterval );
     7092                        pollInterval = 0;
     7093                    }
     7094                };
     7095
     7096                api.state( 'changesetDate' ).bind( updateTimeArrivedPoller );
     7097                api.state( 'selectedChangesetDate' ).bind( updateTimeArrivedPoller );
     7098                api.state( 'changesetStatus' ).bind( updateTimeArrivedPoller );
     7099                api.state( 'selectedChangesetStatus' ).bind( updateTimeArrivedPoller );
     7100                updateTimeArrivedPoller();
     7101
     7102                // Ensure dateControl only appears when selected status is future.
     7103                dateControl.active.validate = function() {
     7104                    return 'future' === statusElement.get();
     7105                };
     7106                toggleDateControl = function( value ) {
     7107                    dateControl.active.set( 'future' === value );
     7108                };
     7109                toggleDateControl( statusElement.get() );
     7110                statusElement.bind( toggleDateControl );
     7111
     7112                // Show notification on date control when status is future but it isn't a future date.
     7113                api.state( 'saving' ).bind( function( isSaving ) {
     7114                    if ( isSaving && 'future' === api.state( 'selectedChangesetStatus' ).get() ) {
     7115                        dateControl.toggleFutureDateNotification( ! dateControl.isFutureDate() );
     7116                    }
     7117                } );
     7118            } );
     7119        } );
    62237120
    62247121        // Toggle visibility of Header Video notice when active state change.
  • trunk/src/wp-includes/class-wp-customize-control.php

    r41162 r41626  
    748748 */
    749749require_once( ABSPATH . WPINC . '/customize/class-wp-customize-new-menu-control.php' );
     750
     751/**
     752 * WP_Customize_Date_Time_Control class.
     753 */
     754require_once( ABSPATH . WPINC . '/customize/class-wp-customize-date-time-control.php' );
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r41603 r41626  
    597597     */
    598598    public function establish_loaded_changeset() {
     599        if ( empty( $this->_changeset_uuid ) ) {
     600            $changeset_uuid = null;
     601
     602            if ( ! $this->branching() ) {
     603                $unpublished_changeset_posts = $this->get_changeset_posts( array(
     604                    'post_status' => array_diff( get_post_stati(), array( 'auto-draft', 'publish', 'trash', 'inherit', 'private' ) ),
     605                    'exclude_restore_dismissed' => false,
     606                    'posts_per_page' => 1,
     607                    'order' => 'DESC',
     608                    'orderby' => 'date',
     609                ) );
     610                $unpublished_changeset_post = array_shift( $unpublished_changeset_posts );
     611                if ( ! empty( $unpublished_changeset_post ) && wp_is_uuid( $unpublished_changeset_post->post_name ) ) {
     612                    $changeset_uuid = $unpublished_changeset_post->post_name;
     613                }
     614            }
     615
     616            // If no changeset UUID has been set yet, then generate a new one.
     617            if ( empty( $changeset_uuid ) ) {
     618                $changeset_uuid = wp_generate_uuid4();
     619            }
     620
     621            $this->_changeset_uuid = $changeset_uuid;
     622        }
     623    }
     624
     625    /**
     626     * Callback to validate a theme once it is loaded
     627     *
     628     * @since 3.4.0
     629     */
     630    public function after_setup_theme() {
     631        $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
     632        if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
     633            wp_redirect( 'themes.php?broken=true' );
     634            exit;
     635        }
     636    }
     637
     638    /**
     639     * If the theme to be previewed isn't the active theme, add filter callbacks
     640     * to swap it out at runtime.
     641     *
     642     * @since 3.4.0
     643     */
     644    public function start_previewing_theme() {
     645        // Bail if we're already previewing.
     646        if ( $this->is_preview() ) {
     647            return;
     648        }
     649
     650        $this->previewing = true;
     651
     652        if ( ! $this->is_theme_active() ) {
     653            add_filter( 'template', array( $this, 'get_template' ) );
     654            add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
     655            add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
     656
     657            // @link: https://core.trac.wordpress.org/ticket/20027
     658            add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
     659            add_filter( 'pre_option_template', array( $this, 'get_template' ) );
     660
     661            // Handle custom theme roots.
     662            add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
     663            add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
     664        }
     665
     666        /**
     667         * Fires once the Customizer theme preview has started.
     668         *
     669         * @since 3.4.0
     670         *
     671         * @param WP_Customize_Manager $this WP_Customize_Manager instance.
     672         */
     673        do_action( 'start_previewing_theme', $this );
     674    }
     675
     676    /**
     677     * Stop previewing the selected theme.
     678     *
     679     * Removes filters to change the current theme.
     680     *
     681     * @since 3.4.0
     682     */
     683    public function stop_previewing_theme() {
     684        if ( ! $this->is_preview() ) {
     685            return;
     686        }
     687
     688        $this->previewing = false;
     689
     690        if ( ! $this->is_theme_active() ) {
     691            remove_filter( 'template', array( $this, 'get_template' ) );
     692            remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
     693            remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
     694
     695            // @link: https://core.trac.wordpress.org/ticket/20027
     696            remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
     697            remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
     698
     699            // Handle custom theme roots.
     700            remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
     701            remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
     702        }
     703
     704        /**
     705         * Fires once the Customizer theme preview has stopped.
     706         *
     707         * @since 3.4.0
     708         *
     709         * @param WP_Customize_Manager $this WP_Customize_Manager instance.
     710         */
     711        do_action( 'stop_previewing_theme', $this );
     712    }
     713
     714    /**
     715     * Gets whether settings are or will be previewed.
     716     *
     717     * @since 4.9.0
     718     * @see WP_Customize_Setting::preview()
     719     *
     720     * @return bool
     721     */
     722    public function settings_previewed() {
     723        return $this->settings_previewed;
     724    }
     725
     726    /**
     727     * Gets whether data from a changeset's autosaved revision should be loaded if it exists.
     728     *
     729     * @since 4.9.0
     730     * @see WP_Customize_Manager::changeset_data()
     731     *
     732     * @return bool Is using autosaved changeset revision.
     733     */
     734    public function autosaved() {
     735        return $this->autosaved;
     736    }
     737
     738    /**
     739     * Whether the changeset branching is allowed.
     740     *
     741     * @since 4.9.0
     742     * @see WP_Customize_Manager::establish_loaded_changeset()
     743     *
     744     * @return bool Is changeset branching.
     745     */
     746    public function branching() {
    599747
    600748        /**
     
    625773        $this->branching = apply_filters( 'customize_changeset_branching', $this->branching, $this );
    626774
    627         if ( empty( $this->_changeset_uuid ) ) {
    628             $changeset_uuid = null;
    629 
    630             if ( ! $this->branching ) {
    631                 $unpublished_changeset_posts = $this->get_changeset_posts( array(
    632                     'post_status' => array_diff( get_post_stati(), array( 'auto-draft', 'publish', 'trash', 'inherit', 'private' ) ),
    633                     'exclude_restore_dismissed' => false,
    634                     'posts_per_page' => 1,
    635                     'order' => 'DESC',
    636                     'orderby' => 'date',
    637                 ) );
    638                 $unpublished_changeset_post = array_shift( $unpublished_changeset_posts );
    639                 if ( ! empty( $unpublished_changeset_post ) && wp_is_uuid( $unpublished_changeset_post->post_name ) ) {
    640                     $changeset_uuid = $unpublished_changeset_post->post_name;
    641                 }
    642             }
    643 
    644             // If no changeset UUID has been set yet, then generate a new one.
    645             if ( empty( $changeset_uuid ) ) {
    646                 $changeset_uuid = wp_generate_uuid4();
    647             }
    648 
    649             $this->_changeset_uuid = $changeset_uuid;
    650         }
    651     }
    652 
    653     /**
    654      * Callback to validate a theme once it is loaded
    655      *
    656      * @since 3.4.0
    657      */
    658     public function after_setup_theme() {
    659         $doing_ajax_or_is_customized = ( $this->doing_ajax() || isset( $_POST['customized'] ) );
    660         if ( ! $doing_ajax_or_is_customized && ! validate_current_theme() ) {
    661             wp_redirect( 'themes.php?broken=true' );
    662             exit;
    663         }
    664     }
    665 
    666     /**
    667      * If the theme to be previewed isn't the active theme, add filter callbacks
    668      * to swap it out at runtime.
    669      *
    670      * @since 3.4.0
    671      */
    672     public function start_previewing_theme() {
    673         // Bail if we're already previewing.
    674         if ( $this->is_preview() ) {
    675             return;
    676         }
    677 
    678         $this->previewing = true;
    679 
    680         if ( ! $this->is_theme_active() ) {
    681             add_filter( 'template', array( $this, 'get_template' ) );
    682             add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
    683             add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
    684 
    685             // @link: https://core.trac.wordpress.org/ticket/20027
    686             add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
    687             add_filter( 'pre_option_template', array( $this, 'get_template' ) );
    688 
    689             // Handle custom theme roots.
    690             add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
    691             add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
    692         }
    693 
    694         /**
    695          * Fires once the Customizer theme preview has started.
    696          *
    697          * @since 3.4.0
    698          *
    699          * @param WP_Customize_Manager $this WP_Customize_Manager instance.
    700          */
    701         do_action( 'start_previewing_theme', $this );
    702     }
    703 
    704     /**
    705      * Stop previewing the selected theme.
    706      *
    707      * Removes filters to change the current theme.
    708      *
    709      * @since 3.4.0
    710      */
    711     public function stop_previewing_theme() {
    712         if ( ! $this->is_preview() ) {
    713             return;
    714         }
    715 
    716         $this->previewing = false;
    717 
    718         if ( ! $this->is_theme_active() ) {
    719             remove_filter( 'template', array( $this, 'get_template' ) );
    720             remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
    721             remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
    722 
    723             // @link: https://core.trac.wordpress.org/ticket/20027
    724             remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
    725             remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
    726 
    727             // Handle custom theme roots.
    728             remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
    729             remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
    730         }
    731 
    732         /**
    733          * Fires once the Customizer theme preview has stopped.
    734          *
    735          * @since 3.4.0
    736          *
    737          * @param WP_Customize_Manager $this WP_Customize_Manager instance.
    738          */
    739         do_action( 'stop_previewing_theme', $this );
    740     }
    741 
    742     /**
    743      * Gets whether settings are or will be previewed.
    744      *
    745      * @since 4.9.0
    746      * @see WP_Customize_Setting::preview()
    747      *
    748      * @return bool
    749      */
    750     public function settings_previewed() {
    751         return $this->settings_previewed;
     775        return $this->branching;
    752776    }
    753777
     
    764788    public function changeset_uuid() {
    765789        if ( empty( $this->_changeset_uuid ) ) {
    766             throw new Exception( 'Changeset UUID has not been set.' ); // @todo Replace this with a call to `WP_Customize_Manager::establish_loaded_changeset()` during 4.9-beta2.
     790            $this->establish_loaded_changeset();
    767791        }
    768792        return $this->_changeset_uuid;
     
    9821006
    9831007    /**
     1008     * Dismiss all of the current user's auto-drafts (other than the present one).
     1009     *
     1010     * @since 4.9.0
     1011     * @return int The number of auto-drafts that were dismissed.
     1012     */
     1013    protected function dismiss_user_auto_draft_changesets() {
     1014        $changeset_autodraft_posts = $this->get_changeset_posts( array(
     1015            'post_status' => 'auto-draft',
     1016            'exclude_restore_dismissed' => true,
     1017            'posts_per_page' => -1,
     1018        ) );
     1019        $dismissed = 0;
     1020        foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) {
     1021            if ( $autosave_autodraft_post->ID === $this->changeset_post_id() ) {
     1022                continue;
     1023            }
     1024            if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) {
     1025                $dismissed++;
     1026            }
     1027        }
     1028        return $dismissed;
     1029    }
     1030
     1031    /**
    9841032     * Get the changeset post id for the loaded changeset.
    9851033     *
     
    10511099            $this->_changeset_data = array();
    10521100        } else {
    1053             if ( $this->autosaved ) {
     1101            if ( $this->autosaved() ) {
    10541102                $autosave_post = wp_get_post_autosave( $changeset_post_id );
    10551103                if ( $autosave_post ) {
     
    19732021            'changeset' => array(
    19742022                'uuid' => $this->changeset_uuid(),
    1975                 'autosaved' => $this->autosaved,
     2023                'autosaved' => $this->autosaved(),
    19762024            ),
    19772025            'timeouts' => array(
     
    23462394        } else {
    23472395            $response = $r;
     2396            $changeset_post = get_post( $this->changeset_post_id() );
    23482397
    23492398            // Dismiss all other auto-draft changeset posts for this user (they serve like autosave revisions), as there should only be one.
    23502399            if ( $is_new_changeset ) {
    2351                 $changeset_autodraft_posts = $this->get_changeset_posts( array(
    2352                     'post_status' => 'auto-draft',
    2353                     'exclude_restore_dismissed' => true,
    2354                     'posts_per_page' => -1,
    2355                 ) );
    2356                 foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) {
    2357                     if ( $autosave_autodraft_post->ID !== $this->changeset_post_id() ) {
    2358                         update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true );
    2359                     }
    2360                 }
     2400                $this->dismiss_user_auto_draft_changesets();
    23612401            }
    23622402
    23632403            // Note that if the changeset status was publish, then it will get set to trash if revisions are not supported.
    2364             $response['changeset_status'] = get_post_status( $this->changeset_post_id() );
     2404            $response['changeset_status'] = $changeset_post->post_status;
    23652405            if ( $is_publish && 'trash' === $response['changeset_status'] ) {
    23662406                $response['changeset_status'] = 'publish';
    23672407            }
    23682408
    2369             if ( 'publish' === $response['changeset_status'] ) {
     2409            if ( 'future' === $response['changeset_status'] ) {
     2410                $response['changeset_date'] = $changeset_post->post_date;
     2411            }
     2412
     2413            if ( 'publish' === $response['changeset_status'] || 'trash' === $response['changeset_status'] ) {
    23702414                $response['next_changeset_uuid'] = wp_generate_uuid4();
    23712415            }
     
    24352479            $existing_status = get_post_status( $changeset_post_id );
    24362480            if ( 'publish' === $existing_status || 'trash' === $existing_status ) {
    2437                 return new WP_Error( 'changeset_already_published' );
     2481                return new WP_Error(
     2482                    'changeset_already_published',
     2483                    __( 'The previous set of changes already been published. Please try saving your current set of changes again.' ),
     2484                    array(
     2485                        'next_changeset_uuid' => wp_generate_uuid4(),
     2486                    )
     2487                );
    24382488            }
    24392489
     
    24542504            $is_future_dated = ( mysql2date( 'U', $args['date_gmt'], false ) > mysql2date( 'U', $now, false ) );
    24552505            if ( ! $is_future_dated ) {
    2456                 return new WP_Error( 'not_future_date' ); // Only future dates are allowed.
     2506                return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) ); // Only future dates are allowed.
    24572507            }
    24582508
     
    24692519            $changeset_post = get_post( $changeset_post_id );
    24702520            if ( mysql2date( 'U', $changeset_post->post_date_gmt, false ) <= mysql2date( 'U', $now, false ) ) {
    2471                 return new WP_Error( 'not_future_date' );
     2521                return new WP_Error( 'not_future_date', __( 'You must supply a future date to schedule.' ) );
    24722522            }
    24732523        }
     
    30573107
    30583108        if ( empty( $changeset_post_id ) || 'auto-draft' === get_post_status( $changeset_post_id ) ) {
    3059             $changeset_autodraft_posts = $this->get_changeset_posts( array(
    3060                 'post_status' => 'auto-draft',
    3061                 'exclude_restore_dismissed' => true,
    3062                 'posts_per_page' => -1,
    3063             ) );
    3064             $dismissed = 0;
    3065             foreach ( $changeset_autodraft_posts as $autosave_autodraft_post ) {
    3066                 if ( $autosave_autodraft_post->ID === $changeset_post_id ) {
    3067                     continue;
    3068                 }
    3069                 if ( update_post_meta( $autosave_autodraft_post->ID, '_customize_restore_dismissed', true ) ) {
    3070                     $dismissed++;
    3071                 }
    3072             }
     3109            $dismissed = $this->dismiss_user_auto_draft_changesets();
    30733110            if ( $dismissed > 0 ) {
    30743111                wp_send_json_success( 'auto_draft_dismissed' );
    30753112            } else {
    3076                 wp_send_json_error( 'no_autosave_to_delete', 404 );
     3113                wp_send_json_error( 'no_auto_draft_to_delete', 404 );
    30773114            }
    30783115        } else {
     
    30903127                }
    30913128            } else {
    3092                 wp_send_json_error( 'no_autosave_to_delete', 404 );
     3129                wp_send_json_error( 'no_autosave_revision_to_delete', 404 );
    30933130            }
    30943131        }
     
    35173554            </ul>
    35183555        </script>
     3556        <script type="text/html" id="tmpl-customize-preview-link-control" >
     3557            <span class="customize-control-title">
     3558                <label><?php esc_html_e( 'Share Preview Link' ); ?></label>
     3559            </span>
     3560            <span class="description customize-control-description"><?php esc_html_e( 'See how changes would look live on your website, and share the preview with people who can\'t access the Customizer.' ); ?></span>
     3561            <div class="customize-control-notifications-container"></div>
     3562            <div class="preview-link-wrapper">
     3563                <label>
     3564                    <span class="screen-reader-text"><?php esc_html_e( 'Preview Link' ); ?></span>
     3565                    <a class="preview-control-element" data-component="link" href="" target=""></a>
     3566                    <input readonly class="preview-control-element" data-component="input" value="test" >
     3567                    <button class="customize-copy-preview-link preview-control-element button button-secondary" data-component="button" data-copy-text="<?php esc_attr_e( 'Copy' ); ?>" data-copied-text="<?php esc_attr_e( 'Copied' ); ?>" ><?php esc_html_e( 'Copy' ); ?></button>
     3568                </label>
     3569            </div>
     3570        </script>
    35193571        <?php
    35203572    }
     
    38783930        $autosave_autodraft_post = null;
    38793931        $changeset_post_id = $this->changeset_post_id();
    3880         if ( ! $this->saved_starter_content_changeset && ! $this->autosaved ) {
     3932        if ( ! $this->saved_starter_content_changeset && ! $this->autosaved() ) {
    38813933            if ( $changeset_post_id ) {
    38823934                $autosave_revision_post = wp_get_post_autosave( $changeset_post_id );
     
    38943946
    38953947        // Prepare Customizer settings to pass to JavaScript.
     3948        $changeset_post = null;
     3949        if ( $changeset_post_id ) {
     3950            $changeset_post = get_post( $changeset_post_id );
     3951        }
     3952
    38963953        $settings = array(
    38973954            'changeset' => array(
    38983955                'uuid' => $this->changeset_uuid(),
    3899                 'branching' => $this->branching,
    3900                 'autosaved' => $this->autosaved,
     3956                'branching' => $this->branching(),
     3957                'autosaved' => $this->autosaved(),
    39013958                'hasAutosaveRevision' => ! empty( $autosave_revision_post ),
    39023959                'latestAutoDraftUuid' => $autosave_autodraft_post ? $autosave_autodraft_post->post_name : null,
    3903                 'status' => $changeset_post_id ? get_post_status( $changeset_post_id ) : '',
     3960                'status' => $changeset_post ? $changeset_post->post_status : '',
     3961                'currentUserCanPublish' => current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ),
     3962                'publishDate' => $changeset_post ? $changeset_post->post_date : '', // @todo Only if future status? Rename to just date?
    39043963            ),
     3964            'initialServerDate' => current_time( 'mysql', false ),
     3965            'initialServerTimestamp' => floor( microtime( true ) * 1000 ),
     3966            'initialClientTimestamp' => -1, // To be set with JS below.
    39053967            'timeouts' => array(
    39063968                'windowRefresh' => 250,
     
    39584020        <script type="text/javascript">
    39594021            var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
     4022            _wpCustomizeSettings.initialClientTimestamp = _.now();
    39604023            _wpCustomizeSettings.controls = {};
    39614024            _wpCustomizeSettings.settings = {};
     
    40484111        $this->register_control_type( 'WP_Customize_Theme_Control' );
    40494112        $this->register_control_type( 'WP_Customize_Code_Editor_Control' );
     4113        $this->register_control_type( 'WP_Customize_Date_Time_Control' );
     4114
     4115        /* Publish Settings */
     4116
     4117        $this->add_section( 'publish_settings', array(
     4118            'title' => __( 'Publish Settings' ),
     4119            'priority' => 0,
     4120            'capability' => 'customize',
     4121            'type' => 'outer',
     4122            'active_callback' => array( $this, 'is_theme_active' ),
     4123        ) );
     4124
     4125        /* Publish Settings Controls */
     4126        $status_choices = array(
     4127            'publish' => __( 'Publish' ),
     4128            'draft' => __( 'Save Draft' ),
     4129            'future' => __( 'Schedule' ),
     4130        );
     4131
     4132        if ( ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->publish_posts ) ) {
     4133            unset( $status_choices['publish'] );
     4134        }
     4135
     4136        $this->add_control( 'changeset_status', array(
     4137            'section' => 'publish_settings',
     4138            'settings' => array(),
     4139            'type' => 'radio',
     4140            'label' => __( 'Action' ),
     4141            'choices' => $status_choices,
     4142            'capability' => 'customize',
     4143        ) );
     4144
     4145        if ( $this->changeset_post_id() && 'future' === get_post_status( $this->changeset_post_id() ) ) {
     4146            $initial_date = get_the_time( 'Y-m-d H:i:s', $this->changeset_post_id() );
     4147        } else {
     4148            $initial_date = current_time( 'mysql', false );
     4149        }
     4150        $this->add_control( new WP_Customize_Date_Time_Control( $this, 'changeset_scheduled_date', array(
     4151            'section' => 'publish_settings',
     4152            'settings' => array(),
     4153            'type' => 'date_time',
     4154            'min_year' => date( 'Y' ),
     4155            'allow_past_date' => false,
     4156            'twelve_hour_format' => false !== stripos( get_option( 'time_format' ), 'a' ),
     4157            'description' => __( 'Schedule your customization changes to publish ("go live") at a future date.' ),
     4158            'capability' => 'customize',
     4159            'default_value' => $initial_date,
     4160        ) ) );
    40504161
    40514162        /* Themes */
  • trunk/src/wp-includes/js/customize-preview.js

    r41597 r41626  
    673673
    674674    $( function() {
    675         var bg, setValue;
     675        var bg, setValue, handleUpdatedChangesetUuid;
    676676
    677677        api.settings = window._wpCustomizeSettings;
     
    766766        });
    767767
     768        /**
     769         * Handle update to changeset UUID.
     770         *
     771         * @param {string} uuid - UUID.
     772         * @returns {void}
     773         */
     774        handleUpdatedChangesetUuid = function( uuid ) {
     775            api.settings.changeset.uuid = uuid;
     776
     777            // Update UUIDs in links and forms.
     778            $( document.body ).find( 'a[href], area' ).each( function() {
     779                api.prepareLinkPreview( this );
     780            } );
     781            $( document.body ).find( 'form' ).each( function() {
     782                api.prepareFormPreview( this );
     783            } );
     784
     785            /*
     786             * Replace the UUID in the URL. Note that the wrapped history.replaceState()
     787             * will handle injecting the current api.settings.changeset.uuid into the URL,
     788             * so this is merely to trigger that logic.
     789             */
     790            if ( history.replaceState ) {
     791                history.replaceState( currentHistoryState, '', location.href );
     792            }
     793        };
     794
     795        api.preview.bind( 'changeset-uuid', handleUpdatedChangesetUuid );
     796
    768797        api.preview.bind( 'saved', function( response ) {
    769798            if ( response.next_changeset_uuid ) {
    770                 api.settings.changeset.uuid = response.next_changeset_uuid;
    771 
    772                 // Update UUIDs in links and forms.
    773                 $( document.body ).find( 'a[href], area' ).each( function() {
    774                     api.prepareLinkPreview( this );
    775                 } );
    776                 $( document.body ).find( 'form' ).each( function() {
    777                     api.prepareFormPreview( this );
    778                 } );
    779 
    780                 /*
    781                  * Replace the UUID in the URL. Note that the wrapped history.replaceState()
    782                  * will handle injecting the current api.settings.changeset.uuid into the URL,
    783                  * so this is merely to trigger that logic.
    784                  */
    785                 if ( history.replaceState ) {
    786                     history.replaceState( currentHistoryState, '', location.href );
    787                 }
    788             }
    789 
     799                handleUpdatedChangesetUuid( response.next_changeset_uuid );
     800            }
    790801            api.trigger( 'saved', response );
    791802        } );
  • trunk/src/wp-includes/script-loader.php

    r41608 r41626  
    548548    $scripts->add( 'customize-controls', "/wp-admin/js/customize-controls$suffix.js", array( 'customize-base', 'wp-a11y', 'wp-util' ), false, 1 );
    549549    did_action( 'init' ) && $scripts->localize( 'customize-controls', '_wpCustomizeControlsL10n', array(
    550         'activate'           => __( 'Save &amp; Activate' ),
    551         'save'               => __( 'Save &amp; Publish' ),
     550        'activate'           => __( 'Activate &amp; Publish' ),
     551        'save'               => __( 'Save &amp; Publish' ), // @todo Remove as not required.
     552        'publish'            => __( 'Publish' ),
     553        'published'          => __( 'Published' ),
     554        'saveDraft'          => __( 'Save Draft' ),
     555        'draftSaved'         => __( 'Draft Saved' ),
     556        'updating'           => __( 'Updating' ),
     557        'schedule'           => __( 'Schedule' ),
     558        'scheduled'          => __( 'Scheduled' ),
     559        'invalid'            => __( 'Invalid' ),
     560        'saveBeforeShare'    => __( 'Please save your changes in order to share the preview.' ),
     561        'futureDateError'    => __( 'You must supply a future date to schedule.' ),
    552562        'saveAlert'          => __( 'The changes you made will be lost if you navigate away from this page.' ),
    553563        'saved'              => __( 'Saved' ),
     
    564574        /* translators: placeholder is URL to the Customizer to load the autosaved version */
    565575        'autosaveNotice'     => __( 'There is a more recent autosave of your changes than the one you are previewing. <a href="%s">Restore the autosave</a>' ),
    566         'videoHeaderNotice'   => __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ),
     576        'videoHeaderNotice'  => __( 'This theme doesn\'t support video headers on this page. Navigate to the front page or another page that supports video headers.' ),
    567577        // Used for overriding the file types allowed in plupload.
    568578        'allowedFiles'       => __( 'Allowed Files' ),
  • trunk/tests/phpunit/tests/ajax/CustomizeManager.php

    r39409 r41626  
    166166        $this->assertFalse( $this->_last_response_parsed['success'] );
    167167        $this->assertEquals( 'changeset_already_published', $this->_last_response_parsed['data']['code'] );
    168         wp_update_post( array( 'ID' => $wp_customize->changeset_post_id(), 'post_status' => 'auto-draft' ) );
     168        wp_update_post( array(
     169            'ID' => $wp_customize->changeset_post_id(),
     170            'post_status' => 'auto-draft',
     171        ) );
    169172
    170173        // User cannot edit.
     
    223226        $this->assertTrue( $this->_last_response_parsed['success'] );
    224227        $this->assertEquals( 'future', get_post_status( $wp_customize->changeset_post_id() ) );
    225         wp_update_post( array( 'ID' => $wp_customize->changeset_post_id(), 'post_status' => 'auto-draft' ) );
    226 
     228        wp_update_post( array(
     229            'ID' => $wp_customize->changeset_post_id(),
     230            'post_status' => 'auto-draft',
     231        ) );
    227232    }
    228233
     
    255260        $wp_customize = $this->set_up_valid_state();
    256261
    257         // Successful future.
    258262        $_POST['customize_changeset_status'] = 'publish';
    259263        $_POST['customize_changeset_title'] = 'Success Changeset';
     
    269273        $this->assertEquals( 'publish', $this->_last_response_parsed['data']['changeset_status'] );
    270274        $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
     275        $this->assertTrue( wp_is_uuid( $this->_last_response_parsed['data']['next_changeset_uuid'], 4 ) );
    271276        $this->assertEquals( 'Success Changeset', get_post( $wp_customize->changeset_post_id() )->post_title );
    272277        $this->assertEquals( 'Successful Site Title', get_option( 'blogname' ) );
    273 
    274278    }
    275279
     
    296300        $wp_customize = $this->set_up_valid_state( $uuid );
    297301
    298         // Successful future.
    299302        $_POST['customize_changeset_status'] = 'publish';
    300303        $_POST['customize_changeset_title'] = 'Published';
     
    305308        $this->assertEquals( 'publish', $this->_last_response_parsed['data']['changeset_status'] );
    306309        $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
     310        $this->assertTrue( wp_is_uuid( $this->_last_response_parsed['data']['next_changeset_uuid'], 4 ) );
    307311        $this->assertEquals( 'New Site Title', get_option( 'blogname' ) );
    308312        $this->assertEquals( 'Published', get_post( $post_id )->post_title );
     
    337341        $this->make_ajax_call( 'customize_save' );
    338342        $this->assertTrue( $this->_last_response_parsed['success'] );
     343        $this->assertArrayHasKey( 'changeset_date', $this->_last_response_parsed['data'] );
    339344        $changeset_post_schedule = get_post( $post_id );
    340345        $this->assertEquals( $future_date, $changeset_post_schedule->post_date );
     
    345350        $this->make_ajax_call( 'customize_save' );
    346351        $this->assertTrue( $this->_last_response_parsed['success'] );
     352        $this->assertArrayNotHasKey( 'changeset_date', $this->_last_response_parsed['data'] );
    347353        $changeset_post_draft = get_post( $post_id );
    348354        $this->assertEquals( $future_date, $changeset_post_draft->post_date );
     
    352358        $this->make_ajax_call( 'customize_save' );
    353359        $this->assertTrue( $this->_last_response_parsed['success'] );
     360        $this->assertArrayHasKey( 'changeset_date', $this->_last_response_parsed['data'] );
    354361        $changeset_post_schedule = get_post( $post_id );
    355362        $this->assertEquals( $future_date, $changeset_post_schedule->post_date );
     
    381388        $this->make_ajax_call( 'customize_save' );
    382389        $this->assertTrue( $this->_last_response_parsed['success'] );
     390        $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
     391        $this->assertTrue( wp_is_uuid( $this->_last_response_parsed['data']['next_changeset_uuid'], 4 ) );
    383392        $changeset_post_publish = get_post( $post_id );
    384393        $this->assertNotEquals( $future_date, $changeset_post_publish->post_date );
     394
     395        // Check response when trying to update an already-published post.
     396        $this->assertEquals( 'trash', get_post_status( $post_id ) );
     397        $_POST['customize_changeset_status'] = 'publish';
     398        $this->make_ajax_call( 'customize_save' );
     399        $this->assertFalse( $this->_last_response_parsed['success'] );
     400        $this->assertEquals( 'changeset_already_published', $this->_last_response_parsed['data']['code'] );
     401        $this->assertArrayHasKey( 'next_changeset_uuid', $this->_last_response_parsed['data'] );
     402        $this->assertTrue( wp_is_uuid( $this->_last_response_parsed['data']['next_changeset_uuid'], 4 ) );
     403    }
     404
     405    /**
     406     * Test WP_Customize_Manager::save().
     407     *
     408     * @ticket 39896
     409     * @covers WP_Customize_Manager::save()
     410     */
     411    public function test_save_autosave() {
     412        $uuid = wp_generate_uuid4();
     413
     414        $post_id = $this->factory()->post->create( array(
     415            'post_name' => $uuid,
     416            'post_type' => 'customize_changeset',
     417            'post_status' => 'draft',
     418            'post_content' => wp_json_encode( array(
     419                'blogname' => array(
     420                    'value' => 'New Site Title',
     421                ),
     422            ) ),
     423        ) );
     424        $this->set_up_valid_state( $uuid );
     425
     426        $this->assertFalse( wp_get_post_autosave( $post_id ) );
     427
     428        $_POST['customize_changeset_data'] = wp_json_encode( array(
     429            'blogname' => array(
     430                'value' => 'Autosaved Site Title',
     431            ),
     432        ) );
     433
     434        $_POST['customize_changeset_autosave'] = 'on';
     435        $this->make_ajax_call( 'customize_save' );
     436        $this->assertTrue( $this->_last_response_parsed['success'] );
     437        $this->assertEquals( 'draft', $this->_last_response_parsed['data']['changeset_status'] );
     438        $autosave_revision = wp_get_post_autosave( $post_id );
     439        $this->assertInstanceOf( 'WP_Post', $autosave_revision );
     440
     441        $this->assertContains( 'New Site Title', get_post( $post_id )->post_content );
     442        $this->assertContains( 'Autosaved Site Title', $autosave_revision->post_content );
     443    }
     444
     445    /**
     446     * Test request for dismissing autosave changesets.
     447     *
     448     * @ticket 39896
     449     * @covers WP_Customize_Manager::handle_dismiss_changeset_autosave_request()
     450     * @covers WP_Customize_Manager::dismiss_user_auto_draft_changesets()
     451     */
     452    public function test_handle_dismiss_changeset_autosave_request() {
     453        $uuid = wp_generate_uuid4();
     454        $wp_customize = $this->set_up_valid_state( $uuid );
     455
     456        $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     457        $this->assertFalse( $this->_last_response_parsed['success'] );
     458        $this->assertEquals( 'invalid_nonce', $this->_last_response_parsed['data'] );
     459
     460        $nonce = wp_create_nonce( 'dismiss_customize_changeset_autosave' );
     461        $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = $nonce;
     462        $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     463        $this->assertFalse( $this->_last_response_parsed['success'] );
     464        $this->assertEquals( 'no_auto_draft_to_delete', $this->_last_response_parsed['data'] );
     465
     466        $other_user_id = $this->factory()->user->create();
     467
     468        // Create auto-drafts.
     469        $user_auto_draft_ids = array();
     470        for ( $i = 0; $i < 3; $i++ ) {
     471            $user_auto_draft_ids[] = $this->factory()->post->create( array(
     472                'post_name' => wp_generate_uuid4(),
     473                'post_type' => 'customize_changeset',
     474                'post_status' => 'auto-draft',
     475                'post_author' => self::$admin_user_id,
     476                'post_content' => wp_json_encode( array() ),
     477            ) );
     478        }
     479        $other_user_auto_draft_ids = array();
     480        for ( $i = 0; $i < 3; $i++ ) {
     481            $other_user_auto_draft_ids[] = $this->factory()->post->create( array(
     482                'post_name' => wp_generate_uuid4(),
     483                'post_type' => 'customize_changeset',
     484                'post_status' => 'auto-draft',
     485                'post_author' => $other_user_id,
     486                'post_content' => wp_json_encode( array() ),
     487            ) );
     488        }
     489        foreach ( array_merge( $user_auto_draft_ids, $other_user_auto_draft_ids ) as $post_id ) {
     490            $this->assertFalse( (bool) get_post_meta( $post_id, '_customize_restore_dismissed', true ) );
     491        }
     492        $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     493        $this->assertTrue( $this->_last_response_parsed['success'] );
     494        $this->assertEquals( 'auto_draft_dismissed', $this->_last_response_parsed['data'] );
     495        foreach ( $user_auto_draft_ids as $post_id ) {
     496            $this->assertEquals( 'auto-draft', get_post_status( $post_id ) );
     497            $this->assertTrue( (bool) get_post_meta( $post_id, '_customize_restore_dismissed', true ) );
     498        }
     499        foreach ( $other_user_auto_draft_ids as $post_id ) {
     500            $this->assertEquals( 'auto-draft', get_post_status( $post_id ) );
     501            $this->assertFalse( (bool) get_post_meta( $post_id, '_customize_restore_dismissed', true ) );
     502        }
     503
     504        // Subsequent test results in none dismissed.
     505        $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     506        $this->assertFalse( $this->_last_response_parsed['success'] );
     507        $this->assertEquals( 'no_auto_draft_to_delete', $this->_last_response_parsed['data'] );
     508
     509        // Save a changeset as a draft.
     510        $r = $wp_customize->save_changeset_post( array(
     511            'data' => array(
     512                'blogname' => array(
     513                    'value' => 'Foo',
     514                ),
     515            ),
     516            'status' => 'draft',
     517        ) );
     518        $this->assertNotInstanceOf( 'WP_Error', $r );
     519        $this->assertFalse( wp_get_post_autosave( $wp_customize->changeset_post_id() ) );
     520        $this->assertContains( 'Foo', get_post( $wp_customize->changeset_post_id() )->post_content );
     521
     522        // Since no autosave yet, confirm no action.
     523        $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     524        $this->assertFalse( $this->_last_response_parsed['success'] );
     525        $this->assertEquals( 'no_autosave_revision_to_delete', $this->_last_response_parsed['data'] );
     526
     527        // Add the autosave revision.
     528        $r = $wp_customize->save_changeset_post( array(
     529            'data' => array(
     530                'blogname' => array(
     531                    'value' => 'Bar',
     532                ),
     533            ),
     534            'autosave' => true,
     535        ) );
     536        $this->assertNotInstanceOf( 'WP_Error', $r );
     537        $autosave_revision = wp_get_post_autosave( $wp_customize->changeset_post_id() );
     538        $this->assertInstanceOf( 'WP_Post', $autosave_revision );
     539        $this->assertContains( 'Foo', get_post( $wp_customize->changeset_post_id() )->post_content );
     540        $this->assertContains( 'Bar', $autosave_revision->post_content );
     541
     542        // Confirm autosave gets deleted.
     543        $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     544        $this->assertTrue( $this->_last_response_parsed['success'] );
     545        $this->assertEquals( 'autosave_revision_deleted', $this->_last_response_parsed['data'] );
     546        $this->assertFalse( wp_get_post_autosave( $wp_customize->changeset_post_id() ) );
     547
     548        // Since no autosave yet, confirm no action.
     549        $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     550        $this->assertFalse( $this->_last_response_parsed['success'] );
     551        $this->assertEquals( 'no_autosave_revision_to_delete', $this->_last_response_parsed['data'] );
    385552    }
    386553}
  • trunk/tests/phpunit/tests/customize/manager.php

    r41558 r41626  
    121121        $this->assertEquals( $theme, $wp_customize->get_stylesheet() );
    122122        $this->assertEquals( $messenger_channel, $wp_customize->get_messenger_channel() );
     123        $this->assertFalse( $wp_customize->autosaved() );
     124        $this->assertTrue( $wp_customize->branching() );
     125
     126        $wp_customize = new WP_Customize_Manager( array(
     127            'changeset_uuid' => null,
     128        ) );
     129        $this->assertTrue( wp_is_uuid( $wp_customize->changeset_uuid(), 4 ) );
    123130
    124131        $theme = 'twentyfourteen';
     
    134141        $wp_customize = new WP_Customize_Manager();
    135142        $this->assertEquals( $theme, $wp_customize->get_stylesheet() );
    136         $this->assertNotEmpty( $wp_customize->changeset_uuid() );
     143        $this->assertTrue( wp_is_uuid( $wp_customize->changeset_uuid(), 4 ) );
     144    }
     145
     146    /**
     147     * Test constructor when deferring UUID.
     148     *
     149     * @ticket 39896
     150     * @covers WP_Customize_Manager::establish_loaded_changeset()
     151     * @covers WP_Customize_Manager::__construct()
     152     */
     153    public function test_constructor_deferred_changeset_uuid() {
     154        $data = array(
     155            'blogname' => array(
     156                'value' => 'Test',
     157            ),
     158        );
     159        $uuid = wp_generate_uuid4();
     160        $post_id = $this->factory()->post->create( array(
     161            'post_type' => 'customize_changeset',
     162            'post_name' => $uuid,
     163            'post_status' => 'draft',
     164            'post_content' => wp_json_encode( $data ),
     165        ) );
     166        $wp_customize = new WP_Customize_Manager( array(
     167            'changeset_uuid' => false, // Cause UUID to be deferred.
     168            'branching' => false, // To cause drafted changeset to be autoloaded.
     169        ) );
     170        $this->assertEquals( $uuid, $wp_customize->changeset_uuid() );
     171        $this->assertEquals( $post_id, $wp_customize->changeset_post_id() );
    137172    }
    138173
     
    255290
    256291    /**
     292     * Test WP_Customize_Manager::autosaved().
     293     *
     294     * @ticket 39896
     295     * @covers WP_Customize_Manager::autosaved()
     296     */
     297    public function test_autosaved() {
     298        $wp_customize = new WP_Customize_Manager();
     299        $this->assertFalse( $wp_customize->autosaved() );
     300
     301        $wp_customize = new WP_Customize_Manager( array( 'autosaved' => false ) );
     302        $this->assertFalse( $wp_customize->autosaved() );
     303
     304        $wp_customize = new WP_Customize_Manager( array( 'autosaved' => true ) );
     305        $this->assertTrue( $wp_customize->autosaved() );
     306    }
     307
     308    /**
     309     * Test WP_Customize_Manager::branching().
     310     *
     311     * @ticket 39896
     312     * @covers WP_Customize_Manager::branching()
     313     */
     314    public function test_branching() {
     315        $wp_customize = new WP_Customize_Manager();
     316        $this->assertTrue( $wp_customize->branching(), 'Branching should default to true since it is original behavior in 4.7.' );
     317
     318        $wp_customize = new WP_Customize_Manager( array( 'branching' => false ) );
     319        $this->assertFalse( $wp_customize->branching() );
     320        add_filter( 'customize_changeset_branching', '__return_true' );
     321        $this->assertTrue( $wp_customize->branching() );
     322        remove_filter( 'customize_changeset_branching', '__return_true' );
     323
     324        $wp_customize = new WP_Customize_Manager( array( 'branching' => true ) );
     325        $this->assertTrue( $wp_customize->branching() );
     326        add_filter( 'customize_changeset_branching', '__return_false' );
     327        $this->assertFalse( $wp_customize->branching() );
     328    }
     329
     330    /**
    257331     * Test WP_Customize_Manager::changeset_uuid().
    258332     *
     
    338412     */
    339413    function test_changeset_data() {
     414        wp_set_current_user( self::$admin_user_id );
    340415        $uuid = wp_generate_uuid4();
    341416        $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
     
    343418
    344419        $uuid = wp_generate_uuid4();
    345         $data = array( 'blogname' => array( 'value' => 'Hello World' ) );
     420        $data = array(
     421            'blogname' => array( 'value' => 'Hello World' ),
     422            'blogdescription' => array( 'value' => 'Greet the world' ),
     423        );
    346424        $this->factory()->post->create( array(
    347425            'post_name' => $uuid,
    348426            'post_type' => 'customize_changeset',
    349             'post_status' => 'auto-draft',
     427            'post_status' => 'draft',
    350428            'post_content' => wp_json_encode( $data ),
    351429        ) );
    352430        $wp_customize = new WP_Customize_Manager( array( 'changeset_uuid' => $uuid ) );
    353431        $this->assertEquals( $data, $wp_customize->changeset_data() );
     432
     433        // Autosave.
     434        $wp_customize->set_post_value( 'blogname', 'Hola Mundo' );
     435        $wp_customize->register_controls(); // That is, settings, so blogname setting is registered.
     436        $r = $wp_customize->save_changeset_post( array(
     437            'autosave' => true,
     438        ) );
     439        $this->assertNotInstanceOf( 'WP_Error', $r );
     440
     441        // No change to data if not requesting autosave.
     442        $wp_customize = new WP_Customize_Manager( array(
     443            'changeset_uuid' => $uuid,
     444            'autosaved' => false,
     445        ) );
     446        $wp_customize->register_controls(); // That is, settings.
     447        $this->assertFalse( $wp_customize->autosaved() );
     448        $this->assertEquals( $data, $wp_customize->changeset_data() );
     449
     450        // No change to data if not requesting autosave.
     451        $wp_customize = new WP_Customize_Manager( array(
     452            'changeset_uuid' => $uuid,
     453            'autosaved' => true,
     454        ) );
     455        $this->assertTrue( $wp_customize->autosaved() );
     456        $this->assertNotEquals( $data, $wp_customize->changeset_data() );
     457        $this->assertEquals(
     458            array_merge(
     459                wp_list_pluck( $data, 'value' ),
     460                array( 'blogname' => 'Hola Mundo' )
     461            ),
     462            wp_list_pluck( $wp_customize->changeset_data(), 'value' )
     463        );
    354464    }
    355465
     
    12741384
    12751385    /**
     1386     * Test writing changesets when user supplies unchanged values.
     1387     *
     1388     * @ticket 39896
     1389     * @covers WP_Customize_Manager::save_changeset_post()
     1390     * @covers WP_Customize_Manager::grant_edit_post_capability_for_changeset()
     1391     */
     1392    public function test_save_changeset_post_with_autosave() {
     1393        wp_set_current_user( self::$admin_user_id );
     1394        $uuid = wp_generate_uuid4();
     1395        $changeset_post_id = wp_insert_post( array(
     1396            'post_type' => 'customize_changeset',
     1397            'post_content' => wp_json_encode( array(
     1398                'blogname' => array(
     1399                    'value' => 'Auto-draft Title',
     1400                ),
     1401            ) ),
     1402            'post_author' => self::$admin_user_id,
     1403            'post_name' => $uuid,
     1404            'post_status' => 'auto-draft',
     1405        ) );
     1406
     1407        $wp_customize = new WP_Customize_Manager( array(
     1408            'changeset_uuid' => $uuid,
     1409        ) );
     1410        $wp_customize->register_controls(); // And settings too.
     1411
     1412        // Autosave of an auto-draft overwrites original.
     1413        $wp_customize->save_changeset_post( array(
     1414            'data' => array(
     1415                'blogname' => array(
     1416                    'value' => 'Autosaved Auto-draft Title',
     1417                ),
     1418            ),
     1419            'autosave' => true,
     1420        ) );
     1421        $this->assertFalse( wp_get_post_autosave( $changeset_post_id ) );
     1422        $this->assertContains( 'Autosaved Auto-draft Title', get_post( $changeset_post_id )->post_content );
     1423
     1424        // Update status to draft for subsequent tests.
     1425        $wp_customize->save_changeset_post( array(
     1426            'data' => array(
     1427                'blogname' => array(
     1428                    'value' => 'Draft Title',
     1429                ),
     1430            ),
     1431            'status' => 'draft',
     1432            'autosave' => false,
     1433        ) );
     1434        $this->assertContains( 'Draft Title', get_post( $changeset_post_id )->post_content );
     1435
     1436        // Fail: illegal_autosave_with_date_gmt.
     1437        $r = $wp_customize->save_changeset_post( array(
     1438            'autosave' => true,
     1439            'date_gmt' => ( gmdate( 'Y' ) + 1 ) . '-12-01 00:00:00',
     1440        ) );
     1441        $this->assertInstanceOf( 'WP_Error', $r );
     1442        $this->assertEquals( 'illegal_autosave_with_date_gmt', $r->get_error_code() );
     1443
     1444        // Fail: illegal_autosave_with_status.
     1445        $r = $wp_customize->save_changeset_post( array(
     1446            'autosave' => true,
     1447            'status' => 'pending',
     1448        ) );
     1449        $this->assertEquals( 'illegal_autosave_with_status', $r->get_error_code() );
     1450
     1451        // Fail: illegal_autosave_with_non_current_user.
     1452        $r = $wp_customize->save_changeset_post( array(
     1453            'autosave' => true,
     1454            'user_id' => $this->factory()->user->create( array( 'role' => 'administrator' ) ),
     1455        ) );
     1456        $this->assertEquals( 'illegal_autosave_with_non_current_user', $r->get_error_code() );
     1457
     1458        // Try autosave.
     1459        $this->assertFalse( wp_get_post_autosave( $changeset_post_id ) );
     1460        $r = $wp_customize->save_changeset_post( array(
     1461            'data' => array(
     1462                'blogname' => array(
     1463                    'value' => 'Autosave Title',
     1464                ),
     1465            ),
     1466            'autosave' => true,
     1467        ) );
     1468        $this->assertInternalType( 'array', $r );
     1469
     1470        // Verify that autosave happened.
     1471        $autosave_revision = wp_get_post_autosave( $changeset_post_id );
     1472        $this->assertInstanceOf( 'WP_Post', $autosave_revision );
     1473        $this->assertContains( 'Draft Title', get_post( $changeset_post_id )->post_content );
     1474        $this->assertContains( 'Autosave Title', $autosave_revision->post_content );
     1475    }
     1476
     1477    /**
    12761478     * Test passing `null` for a setting ID to remove it from the changeset.
    12771479     *
     
    23522554        $this->assertNotEmpty( $data );
    23532555
    2354         $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts' ), array_keys( $data ) );
     2556        $this->assertEqualSets( array( 'theme', 'url', 'browser', 'panels', 'sections', 'nonce', 'autofocus', 'documentTitleTmpl', 'previewableDevices', 'changeset', 'timeouts', 'initialClientTimestamp', 'initialServerDate', 'initialServerTimestamp' ), array_keys( $data ) );
    23552557        $this->assertEquals( $autofocus, $data['autofocus'] );
    23562558        $this->assertArrayHasKey( 'save', $data['nonce'] );
    23572559        $this->assertArrayHasKey( 'preview', $data['nonce'] );
     2560
     2561        $this->assertEqualSets(
     2562            array(
     2563                'branching',
     2564                'autosaved',
     2565                'hasAutosaveRevision',
     2566                'latestAutoDraftUuid',
     2567                'status',
     2568                'uuid',
     2569                'currentUserCanPublish',
     2570                'publishDate',
     2571            ),
     2572            array_keys( $data['changeset'] )
     2573        );
    23582574    }
    23592575
  • trunk/tests/qunit/fixtures/customize-settings.js

    r38810 r41626  
    107107            'type': 'reusing-default-template'
    108108        },
     109        'publish_settings': {
     110            'active': true,
     111            'description': '',
     112            'instanceNumber': 6,
     113            'priority': 20,
     114            'title': 'Fixture section of custom type re-using default template',
     115            'type': 'outer'
     116        },
    109117        'fixture-section-without-params': {}
    110118    },
     
    149157        }
    150158    },
     159    initialClientTimestamp: 1506510531595,
     160    initialServerDate: '2017-09-27 16:38:49',
     161    initialServerTimestamp: 1506510529913,
    151162    changeset: {
    152163        status: '',
    153         uuid: '0c674ff4-c159-4e7a-beb4-cb830ae73979'
     164        uuid: '0c674ff4-c159-4e7a-beb4-cb830ae73979',
     165        autosaved: false,
     166        branching: false,
     167        currentUserCanPublish: false,
     168        hasAutosaveRevision: false,
     169        latestAutoDraftUuid: '341b06f6-3c1f-454f-96df-3cf197f3e347',
     170        publishDate: ''
    154171    },
    155172    timeouts: {
  • trunk/tests/qunit/index.html

    r41590 r41626  
    873873            <# } #>
    874874        </script>
    875 
     875    <script type="text/html" id="tmpl-customize-control-date_time-content">
     876
     877        <# _.defaults( data, {"settings":[],"type":"date_time","priority":10,"active":true,"section":"","content":"<li id=\"customize-control-temp\" class=\"customize-control customize-control-date_time\">\n\t\t\t\t\t<\/li>","label":"","description":"","instanceNumber":69,"maxYear":9999,"minYear":1000,"allowPastDate":true,"twelveHourFormat":true,"defaultValue":null,"month_choices":{"1":{"text":"1-Jan","value":1},"2":{"text":"2-Feb","value":2},"3":{"text":"3-Mar","value":3},"4":{"text":"4-Apr","value":4},"5":{"text":"5-May","value":5},"6":{"text":"6-Jun","value":6},"7":{"text":"7-Jul","value":7},"8":{"text":"8-Aug","value":8},"9":{"text":"9-Sep","value":9},"10":{"text":"10-Oct","value":10},"11":{"text":"11-Nov","value":11},"12":{"text":"12-Dec","value":12}}} ); #>
     878
     879        <span class="customize-control-title">
     880            <label>{{ data.label }}</label>
     881        </span>
     882        <div class="customize-control-notifications-container"></div>
     883        <span class="description customize-control-description">{{ data.description }}</span>
     884        <div class="date-time-fields">
     885            <div class="day-row">
     886                <span class="title-day">Day</span>
     887                <div class="day-fields clear">
     888                    <label class="month-field">
     889                        <span class="screen-reader-text">Month</span>
     890                        <select class="date-input month" data-component="month">
     891                            <# _.each( data.month_choices, function( choice ) {
     892                                    if ( _.isObject( choice ) && ! _.isUndefined( choice.text ) && ! _.isUndefined( choice.value ) ) {
     893                                    text = choice.text;
     894                                    value = choice.value;
     895                                    }
     896                                    #>
     897                                <option value="{{ value }}" >
     898                                    {{ text }}
     899                                </option>
     900                                <# } ); #>
     901                        </select>
     902                    </label>
     903                    <label class="day-field">
     904                        <span class="screen-reader-text">Day</span>
     905                        <input type="number" size="2" maxlength="2" autocomplete="off" class="date-input day" data-component="day" min="1" max="31"" />
     906                    </label>
     907                    <span class="time-special-char date-time-separator">,</span>
     908                    <label class="year-field">
     909                        <span class="screen-reader-text">Year</span>
     910                        <# var maxYearLength = String( data.maxYear ).length; #>
     911                            <input type="number" size="4" maxlength="{{ maxYearLength }}" autocomplete="off" class="date-input year" data-component="year" min="{{ data.minYear }}" max="{{ data.maxYear }}" />
     912                    </label>
     913                </div>
     914            </div>
     915            <div class="time-row clear">
     916                <span class="title-time">Time</span>
     917                <div class="time-fields clear">
     918                    <label class="hour-field">
     919                        <span class="screen-reader-text">Hour</span>
     920                        <# var maxHour = data.twelveHourFormat ? 12 : 24; #>
     921                            <input type="number" size="2" maxlength="2" autocomplete="off" class="date-input hour" data-component="hour" min="1" max="{{ maxHour }}"" />
     922                    </label>
     923                    <span class="time-special-char date-time-separator">:</span>
     924                    <label class="minute-field">
     925                        <span class="screen-reader-text">Minute</span>
     926                        <input type="number" size="2" maxlength="2" autocomplete="off" class="date-input minute" data-component="minute" min="0" max="59" />
     927                    </label>
     928                    <# if ( data.twelveHourFormat ) { #>
     929                        <label class="am-pm-field">
     930                            <span class="screen-reader-text">AM / PM</span>
     931                            <select class="date-input" data-component="ampm">
     932                                <option value="am">AM</option>
     933                                <option value="pm">PM</option>
     934                            </select>
     935                        </label>
     936                        <# } #>
     937                    <abbr class="date-timezone" aria-label="Timezone" title="Timezone is Asia/Kolkata (IST), currently UTC+5:30.">IST</abbr>
     938                </div>
     939            </div>
     940        </div>
     941    </script>
     942    <script type="text/html" id="tmpl-customize-preview-link-control" >
     943        <span class="customize-control-title">
     944        <label>Share Preview Link</label>
     945    </span>
     946        <span class="description customize-control-description">See how changes would look live on your website, and share the preview with people who can&#039;t access the Customizer.</span>
     947        <div class="customize-control-notifications-container"></div>
     948        <div class="preview-link-wrapper">
     949            <label>
     950                <span class="screen-reader-text">Preview Link</span>
     951                <a class="preview-control-element" data-component="link" href="" target=""></a>
     952                <input readonly class="preview-control-element" data-component="input" value="test" >
     953                <button class="customize-copy-preview-link preview-control-element button button-secondary" data-component="button" data-copy-text="Copy" data-copied-text="Copied" >Copy</button>
     954            </label>
     955        </div>
     956    </script>
    876957    <script type="text/html" id="tmpl-media-modal">
    877958        <div class="media-modal wp-core-ui">
  • trunk/tests/qunit/wp-admin/js/customize-controls.js

    r41374 r41626  
    676676        } );
    677677    } );
     678
     679    module( 'Customize Utils: wp.customize.utils.getRemainingTime()' );
     680    test( 'utils.getRemainingTime calculates time correctly', function( assert ) {
     681        var datetime = '2599-08-06 12:12:13', timeRemaining, timeRemainingWithDateInstance, timeRemaingingWithTimestamp;
     682
     683        timeRemaining = wp.customize.utils.getRemainingTime( datetime );
     684        timeRemainingWithDateInstance = wp.customize.utils.getRemainingTime( new Date( datetime.replace( /-/g, '/' ) ) );
     685        timeRemaingingWithTimestamp = wp.customize.utils.getRemainingTime( ( new Date( datetime.replace( /-/g, '/' ) ) ).getTime() );
     686
     687        assert.equal( typeof timeRemaining, 'number', timeRemaining );
     688        assert.equal( typeof timeRemainingWithDateInstance, 'number', timeRemaining );
     689        assert.equal( typeof timeRemaingingWithTimestamp, 'number', timeRemaining );
     690        assert.deepEqual( timeRemaining, timeRemainingWithDateInstance );
     691        assert.deepEqual( timeRemaining, timeRemaingingWithTimestamp );
     692    });
     693
     694    module( 'Customize Utils: wp.customize.utils.getCurrentTimestamp()' );
     695    test( 'utils.getCurrentTimestamp returns timestamp', function( assert ) {
     696        var currentTimeStamp;
     697        currentTimeStamp = wp.customize.utils.getCurrentTimestamp();
     698        assert.equal( typeof currentTimeStamp, 'number' );
     699    });
     700
     701    module( 'Customize Controls: wp.customize.DateTimeControl' );
     702    test( 'Test DateTimeControl creation and its methods', function( assert ) {
     703        var control, controlId = 'date_time', section, sectionId = 'fixture-section',
     704            datetime = '2599-08-06 18:12:13', dateTimeArray, dateTimeArrayInampm, timeString,
     705            day, year, month, minute, ampm, hour;
     706
     707        section = wp.customize.section( sectionId );
     708
     709        control = new wp.customize.DateTimeControl( controlId, {
     710            params: {
     711                section: section.id,
     712                type: 'date_time',
     713                content: '<li id="customize-control-' + controlId + '" class="customize-control"></li>',
     714                defaultValue: datetime
     715            }
     716        } );
     717
     718        wp.customize.control.add( controlId, control );
     719
     720        // Test control creations.
     721        assert.ok( control.templateSelector, '#customize-control-date_time-content' );
     722        assert.ok( control.section(), sectionId );
     723        assert.equal( _.size( control.inputElements ), control.elements.length );
     724        assert.ok( control.setting(), datetime );
     725
     726        day = control.inputElements.day;
     727        month = control.inputElements.month;
     728        year = control.inputElements.year;
     729        minute = control.inputElements.minute;
     730        hour = control.inputElements.hour;
     731        ampm = control.inputElements.ampm;
     732
     733        year( '23' );
     734        assert.equal( typeof year(), 'number', 'Should always return integer' );
     735
     736        month( 'test' );
     737        assert.notOk( month(), 'Should not accept text' );
     738
     739        // Test control.parseDateTime();
     740        dateTimeArray = control.parseDateTime( datetime );
     741        assert.deepEqual( dateTimeArray, {
     742            year: '2599',
     743            month: '08',
     744            hour: '18',
     745            minute: '12',
     746            second: '13',
     747            day: '06'
     748        } );
     749
     750        dateTimeArrayInampm = control.parseDateTime( datetime, true );
     751        assert.deepEqual( dateTimeArrayInampm, {
     752            year: '2599',
     753            month: '08',
     754            hour: '6',
     755            minute: '12',
     756            ampm: 'pm',
     757            day: '06'
     758        } );
     759
     760        year( '2010' );
     761        month( '12' );
     762        day( '18' );
     763        hour( '3' );
     764        minute( '44' );
     765        ampm( 'am' );
     766
     767        // Test control.convertInputDateToString().
     768        timeString = control.convertInputDateToString();
     769        assert.equal( timeString, '2010-12-18 03:44:00' );
     770
     771        ampm( 'pm' );
     772        timeString = control.convertInputDateToString();
     773        assert.equal( timeString, '2010-12-18 15:44:00' );
     774
     775        // Test control.updateDaysForMonth();.
     776        year( 2017 );
     777        month( 2 );
     778        day( 31 );
     779        control.updateDaysForMonth();
     780        assert.deepEqual( day(), 28, 'Should update to the correct days' );
     781
     782        day( 20 );
     783        assert.deepEqual( day(), 20, 'Should not update if its less the correct number of days' );
     784
     785        // Test control.convertHourToTwentyFourHourFormat().
     786        assert.equal( control.convertHourToTwentyFourHourFormat( 11, 'pm' ), 23 );
     787        assert.equal( control.convertHourToTwentyFourHourFormat( 12, 'pm' ), 12 );
     788        assert.equal( control.convertHourToTwentyFourHourFormat( 12, 'am' ), 0 );
     789        assert.equal( control.convertHourToTwentyFourHourFormat( 11, 'am' ), 11 );
     790
     791        // Test control.toggleFutureDateNotification().
     792        assert.deepEqual( control.toggleFutureDateNotification(), control );
     793        control.toggleFutureDateNotification( true );
     794        assert.ok( control.notifications.has( 'not_future_date' ) );
     795        control.toggleFutureDateNotification( false );
     796        assert.notOk( control.notifications.has( 'not_future_date' ) );
     797
     798        // Test control.populateDateInputs();
     799        control.populateDateInputs();
     800        control.dateInputs.each( function() {
     801            var node = jQuery( this );
     802            assert.equal( node.val(), control.inputElements[ node.data( 'component' ) ].get() );
     803        } );
     804
     805        // Test control.validateInputs();
     806        hour( 33 );
     807        assert.ok( control.validateInputs() );
     808        hour( 10 );
     809        assert.notOk( control.validateInputs() );
     810        minute( 123 );
     811        assert.ok( control.validateInputs() );
     812        minute( 20 );
     813        assert.notOk( control.validateInputs() );
     814
     815        // Test control.populateSetting();
     816        day( 2 );
     817        month( 11 );
     818        year( 2018 );
     819        hour( 4 );
     820        minute( 20 );
     821        ampm( 'pm' );
     822        control.populateSetting();
     823        assert.equal( control.setting(), '2018-11-02 16:20:00' );
     824
     825        hour( 123 );
     826        control.populateSetting();
     827        assert.equal( control.setting(), '2018-11-02 16:20:00' ); // Should not update if invalid hour.
     828
     829        hour( 5 );
     830        control.populateSetting();
     831        assert.equal( control.setting(), '2018-11-02 17:20:00' );
     832
     833        // Test control.isFutureDate();
     834        day( 2 );
     835        month( 11 );
     836        year( 2318 );
     837        hour( 4 );
     838        minute( 20 );
     839        ampm( 'pm' );
     840        assert.ok( control.isFutureDate() );
     841
     842        year( 2016 );
     843        assert.notOk( control.isFutureDate() );
     844
     845        /**
     846         * Test control.updateMinutesForHour().
     847         * Run this at the end or else the above tests may fail.
     848         */
     849        hour( 24 );
     850        minute( 32 );
     851        control.inputElements.ampm = false; // Because it works only when the time is twenty four hour format.
     852        control.updateMinutesForHour();
     853        assert.deepEqual( minute(), 0 );
     854
     855        // Tear Down.
     856        wp.customize.control.remove( controlId );
     857    });
     858
     859    module( 'Customize Sections: wp.customize.OuterSection' );
     860    test( 'Test OuterSection', function( assert ) {
     861        var section, sectionId = 'test_outer_section', body = jQuery( 'body' ),
     862            defaultSection, defaultSectionId = 'fixture-section';
     863
     864        defaultSection = wp.customize.section( defaultSectionId );
     865
     866        section = new wp.customize.OuterSection( sectionId, {
     867            params: {
     868                content: defaultSection.params.content,
     869                type: 'outer'
     870            }
     871        } );
     872
     873        wp.customize.section.add( sectionId, section );
     874        wp.customize.section.add( defaultSectionId, section );
     875
     876        assert.equal( section.containerPaneParent, '.customize-outer-pane-parent' );
     877        assert.equal( section.containerParent.selector, '#customize-outer-theme-controls' );
     878
     879        defaultSection.expand();
     880        section.expand();
     881        assert.ok( body.hasClass( 'outer-section-open' ) );
     882        assert.ok( section.container.hasClass( 'open' ) );
     883        assert.ok( defaultSection.expanded() ); // Ensure it does not affect other sections state.
     884
     885        section.collapse();
     886        assert.notOk( body.hasClass( 'outer-section-open' ) );
     887        assert.notOk( section.container.hasClass( 'open' ) ); // Ensure it does not affect other sections state.
     888        assert.ok( defaultSection.expanded() );
     889
     890        // Tear down
     891        wp.customize.section.remove( sectionId );
     892    });
     893
     894    module( 'Customize Controls: PreviewLinkControl' );
     895    test( 'Test PreviewLinkControl creation and its methods', function( assert ) {
     896        var section, sectionId = 'publish_settings', newLink;
     897
     898        section = wp.customize.section( sectionId );
     899        section.deferred.embedded.resolve();
     900
     901        assert.expect( 9 );
     902        section.deferred.embedded.done( function() {
     903            _.each( section.controls(), function( control ) {
     904                if ( 'changeset_preview_link' === control.id ) {
     905                    assert.equal( control.templateSelector, 'customize-preview-link-control' );
     906                    assert.equal( _.size( control.previewElements ), control.elements.length );
     907
     908                    // Test control.ready().
     909                    newLink = 'http://example.org?' + wp.customize.settings.changeset.uuid;
     910                    control.setting.set( newLink );
     911
     912                    assert.equal( control.previewElements.input(), newLink );
     913                    assert.equal( control.previewElements.link(), newLink );
     914                    assert.equal( control.previewElements.link.element.attr( 'href' ), newLink );
     915                    assert.equal( control.previewElements.link.element.attr( 'target' ), wp.customize.settings.changeset.uuid );
     916
     917                    // Test control.toggleSaveNotification().
     918                    control.toggleSaveNotification( true );
     919                    assert.ok( control.notifications.has( 'changes_not_saved' ) );
     920                    control.toggleSaveNotification( false );
     921                    assert.notOk( control.notifications.has( 'changes_not_saved' ) );
     922
     923                    // Test control.updatePreviewLink().
     924                    control.updatePreviewLink();
     925                    assert.equal( control.setting.get(), wp.customize.previewer.getFrontendPreviewUrl() );
     926                }
     927            } );
     928        } );
     929    });
    678930});
Note: See TracChangeset for help on using the changeset viewer.