Make WordPress Core

Changeset 41626


Ignore:
Timestamp:
09/27/2017 10:24:37 PM (9 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.