WordPress.org

Make WordPress Core

Changeset 41667


Ignore:
Timestamp:
10/02/2017 03:36:18 AM (23 months ago)
Author:
westonruter
Message:

Customize: Add infrastructure for trashing/reverting of unpublished changes; introduce full-screen OverlayNotification for trashing and theme install/preview.

  • Introduce a new wp.customize.previewer.trash() JS API to trash the current changeset, along with logic to WP_Customize_Manager to handle deleting changeset drafts.
  • Add trashing to wp.customize.state which is then used to update the UI.
  • UI for trashing is pending design feedback. One possibility is to add a new trash button to Publish Settings section that invokes wp.customize.previewer.trash().
  • Improve logic for managing the visibility and disabled states for publish buttons.
  • Prevent attempting requestChangesetUpdate while processing and bump processing while doing save.
  • Update changeset_date state only if sent in save response.
  • Merge ThemesSection#loadThemePreview() into ThemesPanel#loadThemePreview().
  • Remove unused autosaved state.
  • Start autosaving and prompting at beforeunload after a change first happens. This is key for theme previews since even if a user did not make any changes, there were still dirty settings which would get stored in an auto-draft unexpectedly.
  • Allow Notification to accept additional classes to be added to container.
  • Introduce OverlayNotification and use for theme installing, previewing, and trashing. Such overlay notifications take over the entire window.

Props westonruter, celloexpressions.
See #37661, #39896, #21666, #35210.

Location:
trunk
Files:
8 edited

Legend:

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

    r41649 r41667  
    3030}
    3131
     32body:not(.ready) #customize-save-button-wrapper .save {
     33    visibility: hidden;
     34}
    3235#customize-save-button-wrapper .save {
    3336    float: left;
    3437    border-radius: 3px;
    3538    box-shadow: none; /* @todo Adjust box shadow based on the disable states of paired button. */
    36     display: none; /* Shown when ready. */
    3739    margin-top: 0;
    3840}
     
    107109    width: 30px;
    108110    float: left;
    109     display: none; /* Shown when ready. */
    110111    -webkit-transform: none;
    111112    transform: none;
     
    113114}
    114115
     116body:not(.ready) #publish-settings,
     117body.trashing #customize-save-button-wrapper .save,
     118body.trashing #publish-settings {
     119    display: none;
     120}
     121
    115122#customize-header-actions .spinner {
    116123    margin-top: 13px;
     
    118125}
    119126
    120 .saving #customize-header-actions .spinner {
     127.saving #customize-header-actions .spinner,
     128.trashing #customize-header-actions .spinner {
    121129    visibility: visible;
    122130}
     
    146154    padding-left: 12px;
    147155    padding-right: 12px;
     156}
     157
     158#customize-control-trash_changeset {
     159    margin-top: 20px;
     160}
     161#customize-control-trash_changeset button {
     162    position: relative;
     163    padding-left: 24px;
     164    display: inline-block;
     165}
     166#customize-control-trash_changeset button:before {
     167    content: "\f182";
     168    font: normal 22px dashicons;
     169    text-decoration: none;
     170    position: absolute;
     171    left: 0;
     172    top: -2px;
    148173}
    149174
     
    11271152}
    11281153
     1154@-webkit-keyframes customize-fade-in {
     1155    0%   { opacity: 0; }
     1156    100% { opacity: 1; }
     1157}
     1158
     1159@keyframes customize-fade-in {
     1160    0%   { opacity: 0; }
     1161    100% { opacity: 1; }
     1162}
     1163
     1164#customize-controls .notice.notification-overlay,
     1165#customize-controls #customize-notifications-area .notice.notification-overlay {
     1166    margin: 0;
     1167    border-left: 0; /* @todo Appropriate styles could be added for notice-error, notice-warning, notice-success, etc */
     1168}
     1169
     1170#customize-controls .customize-control-notifications-container.has-overlay-notifications {
     1171    -webkit-animation: customize-fade-in 0.5s;
     1172    animation: customize-fade-in 0.5s;
     1173    z-index: 30;
     1174}
     1175
     1176/* Note: Styles for this are also defined in themes.css */
     1177#customize-controls #customize-notifications-area .notice.notification-overlay .notification-message {
     1178    clear: both;
     1179    color: #191e23;
     1180    font-size: 18px;
     1181    font-style: normal;
     1182    margin: 0;
     1183    padding: 2em 0;
     1184    text-align: center;
     1185    width: 100%;
     1186    display: block;
     1187    top: 50%;
     1188    position: relative;
     1189}
     1190
    11291191/* Style for custom settings */
    11301192
     
    15511613 */
    15521614
    1553 @-webkit-keyframes customize-reload {
    1554     0%   { opacity: 0; }
    1555     100% { opacity: 1; }
    1556 }
    1557 
    1558 @keyframes customize-reload {
    1559     0%   { opacity: 0; }
    1560     100% { opacity: 1; }
    1561 }
    1562 
    1563 .wp-customizer .customize-loading #customize-themes-loading-container {
    1564     display: block;
    1565     -webkit-animation: customize-reload .5s; /* Can't use `transition` because `display` changes here. */
    1566     animation: customize-reload .5s;
    1567 }
    1568 
    1569 .customize-loading #customize-themes-loading-container span {
    1570     clear: both;
    1571     color: #191e23;
    1572     font-size: 18px;
    1573     font-style: normal;
    1574     margin: 0;
    1575     padding: 2em 0;
    1576     text-align: center;
    1577     width: 100%;
    1578     display: block;
    1579     top: 50%;
    1580     position: relative;
    1581 }
    1582 
    1583 .customize-loading #customize-themes-loading-container .customize-loading-text {
    1584     display: none;
    1585 }
    1586 
    15871615#customize-theme-controls .control-panel-themes {
    15881616    border-bottom: none;
     
    16741702}
    16751703
    1676 .in-themes-panel:not(.animating) #customize-header-actions .save,
    1677 .in-themes-panel:not(.animating) #customize-header-actions #publish-settings,
    16781704.in-themes-panel:not(.animating) #customize-header-actions .spinner,
    16791705.in-themes-panel:not(.animating) #customize-header-actions .customize-controls-preview-toggle,
  • trunk/src/wp-admin/css/themes.css

    r41648 r41667  
    17081708
    17091709#customize-container,
    1710 #customize-themes-loading-container {
    1711     display: none;
     1710#customize-controls .notice.notification-overlay {
    17121711    background: #eee;
    17131712    z-index: 500000;
     
    17201719    height: 100%;
    17211720}
     1721#customize-container {
     1722    display: none;
     1723}
    17221724
    17231725/* Make the Customizer and Theme installer overlays the only available content. */
    17241726#customize-container,
    1725 #customize-themes-loading-container,
    17261727.theme-install-overlay {
    17271728    visibility: visible;
     
    18281829#customize-preview.wp-full-overlay-main:before,
    18291830.customize-loading #customize-container:before,
    1830 .customize-loading #customize-themes-loading-container:before,
     1831#customize-controls .notice.notification-overlay.notification-loading:before,
    18311832.theme-install-overlay .wp-full-overlay-main:before {
    18321833    content: "";
     
    18661867    #customize-preview.wp-full-overlay-main:before,
    18671868    .customize-loading #customize-container:before,
    1868     .customize-loading #customize-themes-loading-container:before,
     1869    #customize-controls .notice.notification-overlay.notification-loading:before,
    18691870    .theme-install-overlay .wp-full-overlay-main:before {
    18701871        background-image: url(../images/spinner-2x.gif);
  • trunk/src/wp-admin/js/customize-controls.js

    r41650 r41667  
    22(function( exports, $ ){
    33    var Container, focus, normalizedTransitionendEventName, api = wp.customize;
     4
     5    /**
     6     * A notification that is displayed in a full-screen overlay.
     7     *
     8     * @since 4.9.0
     9     * @class
     10     * @augments wp.customize.Notification
     11     */
     12    api.OverlayNotification = api.Notification.extend({
     13
     14        /**
     15         * Whether the notification should show a loading spinner.
     16         *
     17         * @since 4.9.0
     18         * @var {boolean}
     19         */
     20        loading: false,
     21
     22        /**
     23         * Initialize.
     24         *
     25         * @since 4.9.0
     26         *
     27         * @param {string} code - Code.
     28         * @param {object} params - Params.
     29         */
     30        initialize: function( code, params ) {
     31            var notification = this;
     32            api.Notification.prototype.initialize.call( notification, code, params );
     33            notification.classes += ' notification-overlay';
     34            if ( notification.loading ) {
     35                notification.classes += ' notification-loading';
     36            }
     37        }
     38    });
    439
    540    /**
     
    146181        render: function() {
    147182            var collection = this,
    148                 notifications,
     183                notifications, hadOverlayNotification = false, hasOverlayNotification,
    149184                previousNotificationsByCode = {},
    150185                listElement;
     
    179214            // Add all notifications in the sorted order.
    180215            _.each( notifications, function( notification ) {
     216                var notificationContainer;
    181217                if ( wp.a11y && ( ! previousNotificationsByCode[ notification.code ] || ! _.isEqual( notification.message, previousNotificationsByCode[ notification.code ].message ) ) ) {
    182218                    wp.a11y.speak( notification.message, 'assertive' );
    183219                }
    184                 listElement.append( $( notification.render() ) ); // @todo Consider slideDown() as enhancement.
    185             });
     220                notificationContainer = $( notification.render() );
     221                listElement.append( notificationContainer ); // @todo Consider slideDown() as enhancement.
     222
     223                // @todo Constraing focus in notificationContainer if notification.extended( api.OverlayNotification ).
     224            });
     225
     226            hasOverlayNotification = Boolean( _.find( notifications, function( notification ) {
     227                return notification.extended( api.OverlayNotification );
     228            } ) );
     229            if ( collection.previousNotifications ) {
     230                hadOverlayNotification = Boolean( _.find( collection.previousNotifications, function( notification ) {
     231                    return notification.extended( api.OverlayNotification );
     232                } ) );
     233            }
     234
     235            if ( hasOverlayNotification !== hadOverlayNotification ) {
     236                $( document.body ).toggleClass( 'customize-loading', hasOverlayNotification );
     237                collection.container.toggleClass( 'has-overlay-notifications', hasOverlayNotification );
     238            }
    186239
    187240            collection.previousNotifications = notifications;
     
    368421        var deferred, request, submittedChanges = {}, data, submittedArgs;
    369422        deferred = new $.Deferred();
     423
     424        // Prevent attempting changeset update while request is being made.
     425        if ( 0 !== api.state( 'processing' ).get() ) {
     426            deferred.reject( 'already_processing' );
     427            return deferred.promise();
     428        }
    370429
    371430        submittedArgs = _.extend( {
     
    15731632            // Preview installed themes.
    15741633            section.container.on( 'click', '.theme-actions .preview-theme', function() {
    1575                 var themeId = $( this ).data( 'slug' );
    1576 
    1577                 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
    1578                 api.panel( 'themes' ).loadThemePreview( themeId ).fail( function() {
    1579                     $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
    1580                 } );
     1634                api.panel( 'themes' ).loadThemePreview( $( this ).data( 'slug' ) );
    15811635            });
    15821636
     
    17641818            page = Math.ceil( section.loaded / 100 ) + 1;
    17651819            params = {
    1766                 'switch-themes-nonce': api.settings.nonce['switch-themes'],
     1820                'nonce': api.settings.nonce.switch_themes,
    17671821                'wp_customize': 'on',
    17681822                'theme_action': section.params.action,
     
    17811835            section.loading = true;
    17821836            section.container.find( '.no-themes' ).hide();
    1783             request = wp.ajax.post( 'customize-load-themes', params );
     1837            request = wp.ajax.post( 'customize_load_themes', params );
    17841838            request.done(function( data ) {
    17851839                var themes = data.themes, themeControl, newThemeControls;
     
    22142268         * @access public
    22152269         *
     2270         * @deprecated
    22162271         * @param {string} themeId Theme ID.
    22172272         * @returns {jQuery.promise} Promise.
    22182273         */
    22192274        loadThemePreview: function( themeId ) {
    2220             var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser;
    2221 
    2222             urlParser = document.createElement( 'a' );
    2223             urlParser.href = location.href;
    2224             urlParser.search = $.param( _.extend(
    2225                 api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
    2226                 {
    2227                     theme: themeId,
    2228                     changeset_uuid: api.settings.changeset.uuid
    2229                 }
    2230             ) );
    2231 
    2232             overlay = $( '.wp-full-overlay' );
    2233             overlay.addClass( 'customize-loading' );
    2234 
    2235             onceProcessingComplete = function() {
    2236                 var request;
    2237                 if ( api.state( 'processing' ).get() > 0 ) {
    2238                     return;
    2239                 }
    2240 
    2241                 api.state( 'processing' ).unbind( onceProcessingComplete );
    2242 
    2243                 request = api.requestChangesetUpdate( {}, { autosave: true } );
    2244                 request.done( function() {
    2245                     $( window ).off( 'beforeunload.customize-confirm' );
    2246 
    2247                     // Include autosaved param to load autosave revision without prompting user to restore it.
    2248                     if ( ! api.state( 'saved' ).get() ) {
    2249                         urlParser.search += '&customize_autosaved=on';
    2250                     }
    2251 
    2252                     top.location.href = urlParser.href;
    2253                     deferred.resolve();
    2254                 } );
    2255                 request.fail( function() {
    2256                     overlay.removeClass( 'customize-loading' );
    2257                     deferred.reject();
    2258                 } );
    2259             };
    2260 
    2261             if ( 0 === api.state( 'processing' ).get() ) {
    2262                 onceProcessingComplete();
    2263             } else {
    2264                 api.state( 'processing' ).bind( onceProcessingComplete );
    2265             }
    2266 
    2267             return deferred.promise();
     2275            return api.ThemesPanel.prototype.loadThemePreview.call( this, themeId );
    22682276        },
    22692277
     
    28452853                var theme = false, customizeId, themeControl;
    28462854                if ( preview ) {
    2847 
    2848                     panel.loadThemePreview( slug ).fail( function() {
    2849                         $( '.wp-full-overlay' ).removeClass( 'customize-loading' );
    2850                     } );
     2855                    api.notifications.remove( 'theme_installing' );
     2856
     2857                    panel.loadThemePreview( slug );
    28512858
    28522859                } else {
     
    29002907            if ( $( event.target ).hasClass( 'preview' ) ) {
    29012908                preview = true;
    2902                 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
    2903                 wp.a11y.speak( $( '#customize-themes-loading-container .customize-loading-text-installing-theme' ).text() );
     2909
     2910                api.notifications.add( 'theme_installing', new api.OverlayNotification( 'theme_installing', {
     2911                    message: api.l10n.themeDownloading,
     2912                    type: 'info',
     2913                    loading: true
     2914                } ) );
    29042915            }
    29052916        },
     
    29142925         */
    29152926        loadThemePreview: function( themeId ) {
    2916             var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser;
     2927            var deferred = $.Deferred(), onceProcessingComplete, urlParser, queryParams;
    29172928
    29182929            urlParser = document.createElement( 'a' );
    29192930            urlParser.href = location.href;
    2920             urlParser.search = $.param( _.extend(
     2931            queryParams = _.extend(
    29212932                api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
    29222933                {
     
    29242935                    changeset_uuid: api.settings.changeset.uuid
    29252936                }
    2926             ) );
     2937            );
     2938
     2939            // Include autosaved param to load autosave revision without prompting user to restore it.
     2940            if ( ! api.state( 'saved' ).get() ) {
     2941                queryParams.customize_autosaved = 'on';
     2942            }
     2943
     2944            urlParser.search = $.param( queryParams );
    29272945
    29282946            // Update loading message. Everything else is handled by reloading the page.
    2929             $( '#customize-themes-loading-container span' ).hide();
    2930             $( '#customize-themes-loading-container .customize-loading-text' ).css( 'display', 'block' );
    2931             wp.a11y.speak( $( '#customize-themes-loading-container .customize-loading-text' ).text() );
    2932             overlay = $( '.wp-full-overlay' );
    2933             overlay.addClass( 'customize-loading' );
     2947            api.notifications.add( 'theme_previewing', new api.OverlayNotification( 'theme_previewing', {
     2948                message: api.l10n.themePreviewWait,
     2949                type: 'info',
     2950                loading: true
     2951            } ) );
    29342952
    29352953            onceProcessingComplete = function() {
     
    29412959                api.state( 'processing' ).unbind( onceProcessingComplete );
    29422960
    2943                 request = api.requestChangesetUpdate();
     2961                request = api.requestChangesetUpdate( {}, { autosave: true } );
    29442962                request.done( function() {
    29452963                    deferred.resolve();
    29462964                    $( window ).off( 'beforeunload.customize-confirm' );
    2947                     window.location.href = urlParser.href;
     2965                    window.location.href = urlParser.href; // @todo Use location.replace()?
    29482966                } );
    29492967                request.fail( function() {
    2950                     overlay.removeClass( 'customize-loading' );
     2968
     2969                    // @todo Show notification regarding failure.
     2970                    api.notifications.remove( 'theme_previewing' );
     2971
    29512972                    deferred.reject();
    29522973                } );
     
    62316252    _.each( [
    62326253        'saved',
    6233         'autosaved',
    62346254        'saving',
     6255        'trashing',
    62356256        'activated',
    62366257        'processing',
     
    62806301            footerActions = $( '#customize-footer-actions' );
    62816302
    6282         saveBtn.show();
    6283 
    62846303        api.section( 'publish_settings', function( section ) {
    62856304            var updateButtonsState, previewLinkControl, previewLinkControlId = 'changeset_preview_link', updateSectionActive, isSectionActive;
     
    63026321             */
    63036322            isSectionActive = function() {
    6304                 if ( ! api.state( 'activated' ) ) {
     6323                if ( ! api.state( 'activated' ).get() ) {
     6324                    return false;
     6325                }
     6326                if ( api.state( 'trashing' ).get() || 'trash' === api.state( 'changesetStatus' ).get() ) {
    63056327                    return false;
    63066328                }
     
    63176339            };
    63186340            api.state( 'activated' ).bind( updateSectionActive );
     6341            api.state( 'trashing' ).bind( updateSectionActive );
    63196342            api.state( 'saved' ).bind( updateSectionActive );
    63206343            api.state( 'changesetStatus' ).bind( updateSectionActive );
     
    65526575                     */
    65536576                    request = wp.ajax.post( 'customize_save', query );
    6554 
    6555                     // Disable save button during the save request.
    6556                     saveBtn.prop( 'disabled', true );
     6577                    api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
    65576578
    65586579                    api.trigger( 'save', request );
    65596580
    65606581                    request.always( function () {
     6582                        api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
    65616583                        api.state( 'saving' ).set( false );
    6562                         saveBtn.prop( 'disabled', false );
    65636584                        api.unbind( 'change', captureSettingModifiedDuringSave );
    65646585                    } );
     
    66376658
    66386659                        api.state( 'changesetStatus' ).set( response.changeset_status );
    6639                         api.state( 'changesetDate' ).set( response.changeset_date );
     6660                        if ( response.changeset_date ) {
     6661                            api.state( 'changesetDate' ).set( response.changeset_date );
     6662                        }
    66406663
    66416664                        if ( 'publish' === response.changeset_status ) {
     
    66956718
    66966719            /**
     6720             * Trash the current changes.
     6721             *
     6722             * Revert the Customizer to it's previously-published state.
     6723             *
     6724             * @since 4.9.0
     6725             *
     6726             * @returns {jQuery.promise} Promise.
     6727             */
     6728            trash: function trash() {
     6729                var request, success, fail;
     6730
     6731                api.state( 'trashing' ).set( true );
     6732                api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
     6733
     6734                request = wp.ajax.post( 'customize_trash', {
     6735                    customize_changeset_uuid: api.settings.changeset.uuid,
     6736                    nonce: api.settings.nonce.trash
     6737                } );
     6738                api.notifications.add( 'changeset_trashing', new api.OverlayNotification( 'changeset_trashing', {
     6739                    type: 'info',
     6740                    message: api.l10n.revertingChanges,
     6741                    loading: true
     6742                } ) );
     6743
     6744                success = function() {
     6745                    var urlParser = document.createElement( 'a' ), queryParams;
     6746
     6747                    api.state( 'changesetStatus' ).set( 'trash' );
     6748                    api.each( function( setting ) {
     6749                        setting._dirty = false;
     6750                    } );
     6751                    api.state( 'saved' ).set( true );
     6752
     6753                    // Go back to Customizer without changeset.
     6754                    urlParser.href = location.href;
     6755                    queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
     6756                    delete queryParams.changeset_uuid;
     6757                    urlParser.search = $.param( queryParams );
     6758                    location.replace( urlParser.href );
     6759                };
     6760
     6761                fail = function( code, message ) {
     6762                    var notificationCode = code || 'unknown_error';
     6763                    api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
     6764                    api.state( 'trashing' ).set( false );
     6765                    api.notifications.remove( 'changeset_trashing' );
     6766                    api.notifications.add( notificationCode, new api.Notification( notificationCode, {
     6767                        message: message || api.l10n.unknownError,
     6768                        dismissible: true,
     6769                        type: 'error'
     6770                    } ) );
     6771                };
     6772
     6773                request.done( function( response ) {
     6774                    success( response.message );
     6775                } );
     6776
     6777                request.fail( function( response ) {
     6778                    var code = response.code || 'trashing_failed';
     6779                    if ( response.success || 'non_existent_changeset' === code || 'changeset_already_trashed' === code ) {
     6780                        success( response.message );
     6781                    } else {
     6782                        fail( code, response.message );
     6783                    }
     6784                } );
     6785            },
     6786
     6787            /**
    66976788             * Builds the front preview url with the current state of customizer.
    66986789             *
     
    68436934            var saved = state.instance( 'saved' ),
    68446935                saving = state.instance( 'saving' ),
     6936                trashing = state.instance( 'trashing' ),
    68456937                activated = state.instance( 'activated' ),
    68466938                processing = state.instance( 'processing' ),
     
    68646956                    saveBtn.val( api.l10n.activate );
    68656957                    closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
    6866                     publishSettingsBtn.prop( 'disabled', false );
    68676958
    68686959                } else if ( '' === changesetStatus.get() && saved() ) {
     
    68726963                        saveBtn.val( api.l10n.saved );
    68736964                    }
    6874                     publishSettingsBtn.prop( 'disabled', true );
    68756965                    closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
    68766966
     
    69006990                    }
    69016991                    closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
    6902                     publishSettingsBtn.prop( 'disabled', false );
    69036992                }
    69046993
     
    69076996                 * and if the theme is not active or the changeset exists but is not published.
    69086997                 */
    6909                 canSave = ! saving() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
     6998                canSave = ! saving() && ! trashing() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
    69106999
    69117000                saveBtn.prop( 'disabled', ! canSave );
     
    69587047            saving.bind( function( isSaving ) {
    69597048                body.toggleClass( 'saving', isSaving );
     7049            } );
     7050            trashing.bind( function( isTrashing ) {
     7051                body.toggleClass( 'trashing', isTrashing );
    69607052            } );
    69617053
     
    70297121            if ( api.settings.changeset.branching ) {
    70307122                changesetStatus.bind( function( newStatus ) {
    7031                     populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus );
     7123                    populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus && 'trash' !== newStatus );
    70327124                } );
    70337125            }
     
    71067198                        // Handle dismissal of notice.
    71077199                        li.find( '.notice-dismiss' ).on( 'click', function() {
    7108                             wp.ajax.post( 'dismiss_customize_changeset_autosave', {
     7200                            wp.ajax.post( 'customize_dismiss_autosave', {
    71097201                                wp_customize: 'on',
    71107202                                customize_theme: api.settings.theme.stylesheet,
     
    75337625            });
    75347626
    7535             // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
    7536             $( window ).on( 'beforeunload.customize-confirm', function() {
    7537                 if ( ! isCleanState() ) {
    7538                     setTimeout( function() {
    7539                         overlay.removeClass( 'customize-loading' );
    7540                     }, 1 );
    7541                     return api.l10n.saveAlert;
    7542                 }
    7543             });
     7627            function startPromptingBeforeUnload() {
     7628                api.unbind( 'change', startPromptingBeforeUnload );
     7629
     7630                // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
     7631                $( window ).on( 'beforeunload.customize-confirm', function() {
     7632                    if ( ! isCleanState() ) {
     7633                        setTimeout( function() {
     7634                            overlay.removeClass( 'customize-loading' );
     7635                        }, 1 );
     7636                        return api.l10n.saveAlert;
     7637                    }
     7638                });
     7639            }
     7640            api.bind( 'change', startPromptingBeforeUnload );
    75447641
    75457642            closeBtn.on( 'click.customize-controls-close', function( event ) {
     
    75677664                        clearedToClose.resolve();
    75687665                    } else {
    7569                         wp.ajax.send( 'dismiss_customize_changeset_autosave', {
     7666                        wp.ajax.send( 'customize_dismiss_autosave', {
    75707667                            timeout: 500, // Don't wait too long.
    75717668                            data: {
     
    79888085
    79898086        // Autosave changeset.
    7990         ( function() {
     8087        function startAutosaving() {
    79918088            var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
     8089
     8090            api.unbind( 'change', startAutosaving ); // Ensure startAutosaving only fires once.
    79928091
    79938092            api.state( 'saved' ).bind( function( isSaved ) {
     
    80418140                updateChangesetWithReschedule();
    80428141            } );
    8043         } ());
     8142        }
     8143        api.bind( 'change', startAutosaving );
    80448144
    80458145        // Make sure TinyMCE dialogs appear above Customizer UI.
     
    80508150        } );
    80518151
     8152        body.addClass( 'ready' );
    80528153        api.trigger( 'ready' );
    80538154    });
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r41648 r41667  
    375375        remove_action( 'admin_init', '_maybe_update_themes' );
    376376
    377         add_action( 'wp_ajax_customize_save',           array( $this, 'save' ) );
    378         add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) );
    379         add_action( 'wp_ajax_customize-load-themes',    array( $this, 'load_themes_ajax' ) );
    380         add_action( 'wp_ajax_dismiss_customize_changeset_autosave', array( $this, 'handle_dismiss_changeset_autosave_request' ) );
     377        add_action( 'wp_ajax_customize_save',             array( $this, 'save' ) );
     378        add_action( 'wp_ajax_customize_trash',            array( $this, 'handle_changeset_trash_request' ) );
     379        add_action( 'wp_ajax_customize_refresh_nonces',   array( $this, 'refresh_nonces' ) );
     380        add_action( 'wp_ajax_customize_load_themes',      array( $this, 'handle_load_themes_request' ) );
     381        add_action( 'wp_ajax_customize_dismiss_autosave', array( $this, 'handle_dismiss_autosave_request' ) );
    381382
    382383        add_action( 'customize_register',                 array( $this, 'register_controls' ) );
     
    28312832
    28322833    /**
     2834     * Handle request to trash a changeset.
     2835     *
     2836     * @since 4.9.0
     2837     */
     2838    public function handle_changeset_trash_request() {
     2839        if ( ! is_user_logged_in() ) {
     2840            wp_send_json_error( 'unauthenticated' );
     2841        }
     2842
     2843        if ( ! $this->is_preview() ) {
     2844            wp_send_json_error( 'not_preview' );
     2845        }
     2846
     2847        if ( ! check_ajax_referer( 'trash_customize_changeset', 'nonce', false ) ) {
     2848            wp_send_json_error( array(
     2849                'code' => 'invalid_nonce',
     2850                'message' => __( 'There was an authentication problem. Please reload and try again.' ),
     2851            ) );
     2852        }
     2853
     2854        $changeset_post_id = $this->changeset_post_id();
     2855
     2856        if ( ! $changeset_post_id ) {
     2857            wp_send_json_error( array(
     2858                'message' => __( 'No changes saved yet, so there is nothing to trash.' ),
     2859                'code' => 'non_existent_changeset',
     2860            ) );
     2861            return;
     2862        }
     2863
     2864        if ( $changeset_post_id && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) {
     2865            wp_send_json_error( array(
     2866                'code' => 'changeset_trash_unauthorized',
     2867                'message' => __( 'Unable to trash changes.' ),
     2868            ) );
     2869        }
     2870
     2871        if ( 'trash' === get_post_status( $changeset_post_id ) ) {
     2872            wp_send_json_error( array(
     2873                'message' => __( 'Changes have already been trashed.' ),
     2874                'code' => 'changeset_already_trashed',
     2875            ) );
     2876            return;
     2877        }
     2878
     2879        $r = wp_trash_post( $changeset_post_id );
     2880        if ( ! ( $r instanceof WP_Post ) ) {
     2881            wp_send_json_error( array(
     2882                'code' => 'changeset_trash_failure',
     2883                'message' => __( 'Unable to trash changes.' ),
     2884            ) );
     2885        }
     2886
     2887        wp_send_json_success( array(
     2888            'message' => __( 'Changes trashed successfully.' ),
     2889        ) );
     2890    }
     2891
     2892    /**
    28332893     * Re-map 'edit_post' meta cap for a customize_changeset post to be the same as 'customize' maps.
    28342894     *
     
    31033163     * @since 4.9.0
    31043164     */
    3105     public function handle_dismiss_changeset_autosave_request() {
     3165    public function handle_dismiss_autosave_request() {
    31063166        if ( ! $this->is_preview() ) {
    31073167            wp_send_json_error( 'not_preview', 400 );
    31083168        }
    31093169
    3110         if ( ! check_ajax_referer( 'dismiss_customize_changeset_autosave', 'nonce', false ) ) {
     3170        if ( ! check_ajax_referer( 'customize_dismiss_autosave', 'nonce', false ) ) {
    31113171            wp_send_json_error( 'invalid_nonce', 403 );
    31123172        }
     
    35443604        ?>
    35453605        <script type="text/html" id="tmpl-customize-notification">
    3546             <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
    3547                 {{{ data.message || data.code }}}
     3606            <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }} {{ data.classes || '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
     3607                <div class="notification-message">{{{ data.message || data.code }}}</div>
    35483608                <# if ( data.dismissible ) { #>
    35493609                    <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
     
    35773637            </div>
    35783638        </script>
     3639        <script type="text/html" id="tmpl-customize-trash-changeset-control">
     3640            <button type="button" class="button-link button-link-delete"><?php _e( 'Trash unpublished changes' ); ?></button>
     3641        </script>
    35793642        <?php
    35803643    }
     
    39023965            'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
    39033966            'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ),
    3904             'switch-themes' => wp_create_nonce( 'switch-themes' ),
    3905             'dismiss_autosave' => wp_create_nonce( 'dismiss_customize_changeset_autosave' ),
     3967            'switch_themes' => wp_create_nonce( 'switch_themes' ),
     3968            'dismiss_autosave' => wp_create_nonce( 'customize_dismiss_autosave' ),
     3969            'trash' => wp_create_nonce( 'trash_customize_changeset' ),
    39063970        );
    39073971
     
    41604224        $this->add_control( 'changeset_status', array(
    41614225            'section' => 'publish_settings',
     4226            'priority' => 10,
    41624227            'settings' => array(),
    41634228            'type' => 'radio',
     
    41744239        $this->add_control( new WP_Customize_Date_Time_Control( $this, 'changeset_scheduled_date', array(
    41754240            'section' => 'publish_settings',
     4241            'priority' => 20,
    41764242            'settings' => array(),
    41774243            'type' => 'date_time',
     
    47244790     * @since 4.9.0
    47254791     */
    4726     public function load_themes_ajax() {
    4727         check_ajax_referer( 'switch-themes', 'switch-themes-nonce' );
     4792    public function handle_load_themes_request() {
     4793        check_ajax_referer( 'switch_themes', 'nonce' );
    47284794
    47294795        if ( ! current_user_can( 'switch_themes' ) ) {
  • trunk/src/wp-includes/customize/class-wp-customize-themes-panel.php

    r41648 r41667  
    9090            <?php endif; ?>
    9191        </li>
    92         <li id="customize-themes-loading-container">
    93             <span class="customize-loading-text-installing-theme"><?php _e( 'Downloading your new theme&hellip;' ); ?></span>
    94             <span class="customize-loading-text"><?php _e( 'Setting up your live preview. This may take a bit.' ); ?></span>
    95         </li><?php // Used as a full-screen overlay transition after clicking to preview a theme. ?>
    9692        <li class="customize-themes-full-container-container">
    9793            <ul class="customize-themes-full-container">
  • trunk/src/wp-includes/js/customize-base.js

    r41387 r41667  
    811811
    812812        /**
     813         * Additional class names to add to the notification container.
     814         *
     815         * @since 4.9.0
     816         * @var {string}
     817         */
     818        classes: '',
     819
     820        /**
    813821         * Initialize notification.
    814822         *
     
    822830         * @param {Function} [params.template] - Function for rendering template. If not provided, this will come from templateId.
    823831         * @param {string}   [params.templateId] - ID for template to render the notification.
     832         * @param {string}   [params.classes] - Additional class names to add to the notification container.
    824833         * @param {boolean}  [params.dismissible] - Whether the notification can be dismissed.
    825834         */
     
    835844                    setting: null,
    836845                    template: null,
    837                     dismissible: false
     846                    dismissible: false,
     847                    classes: ''
    838848                },
    839849                params
  • trunk/src/wp-includes/script-loader.php

    r41640 r41667  
    572572        'untitledBlogName'   => __( '(Untitled)' ),
    573573        'serverSaveError'    => __( 'Failed connecting to the server. Please try saving again.' ),
     574        'themeDownloading'   => __( 'Downloading your new theme&hellip;' ),
     575        'themePreviewWait'   => __( 'Setting up your live preview. This may take a bit.' ),
     576        'revertingChanges'   => __( 'Reverting unpublished changes&hellip;' ),
     577        'trashConfirm'       => __( 'Are you sure you would like to discard your unpublished changes?' ),
    574578        /* translators: %s: URL to the Customizer to load the autosaved version */
    575579        'autosaveNotice'     => __( 'There is a more recent autosave of your changes than the one you are previewing. <a href="%s">Restore the autosave</a>' ),
  • trunk/tests/phpunit/tests/ajax/CustomizeManager.php

    r41626 r41667  
    444444
    445445    /**
    446      * Test request for dismissing autosave changesets.
     446     * Test request for trashing a changeset.
    447447     *
    448448     * @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() {
     449     * @covers WP_Customize_Manager::handle_changeset_trash_request()
     450     */
     451    public function test_handle_changeset_trash_request() {
    453452        $uuid = wp_generate_uuid4();
    454453        $wp_customize = $this->set_up_valid_state( $uuid );
    455454
    456         $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     455        $this->make_ajax_call( 'customize_trash' );
     456        $this->assertFalse( $this->_last_response_parsed['success'] );
     457        $this->assertEquals( 'invalid_nonce', $this->_last_response_parsed['data']['code'] );
     458
     459        $nonce = wp_create_nonce( 'trash_customize_changeset' );
     460        $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = $nonce;
     461        $this->make_ajax_call( 'customize_trash' );
     462        $this->assertFalse( $this->_last_response_parsed['success'] );
     463        $this->assertEquals( 'non_existent_changeset', $this->_last_response_parsed['data']['code'] );
     464
     465        $wp_customize->register_controls(); // And settings too.
     466        $wp_customize->set_post_value( 'blogname', 'HELLO' );
     467        $wp_customize->save_changeset_post( array(
     468            'status' => 'save',
     469        ) );
     470
     471        add_filter( 'map_meta_cap', array( $this, 'return_do_not_allow' ) );
     472        $this->make_ajax_call( 'customize_trash' );
     473        $this->assertFalse( $this->_last_response_parsed['success'] );
     474        $this->assertEquals( 'changeset_trash_unauthorized', $this->_last_response_parsed['data']['code'] );
     475        remove_filter( 'map_meta_cap', array( $this, 'return_do_not_allow' ) );
     476
     477        wp_update_post( array(
     478            'ID' => $wp_customize->changeset_post_id(),
     479            'post_status' => 'trash',
     480        ) );
     481        $this->make_ajax_call( 'customize_trash' );
     482        $this->assertFalse( $this->_last_response_parsed['success'] );
     483        $this->assertEquals( 'changeset_already_trashed', $this->_last_response_parsed['data']['code'] );
     484
     485        wp_update_post( array(
     486            'ID' => $wp_customize->changeset_post_id(),
     487            'post_status' => 'draft',
     488        ) );
     489
     490        $wp_trash_post_count = did_action( 'wp_trash_post' );
     491        add_filter( 'pre_trash_post', '__return_false' );
     492        $this->make_ajax_call( 'customize_trash' );
     493        $this->assertFalse( $this->_last_response_parsed['success'] );
     494        $this->assertEquals( 'changeset_trash_failure', $this->_last_response_parsed['data']['code'] );
     495        remove_filter( 'pre_trash_post', '__return_false' );
     496        $this->assertEquals( $wp_trash_post_count, did_action( 'wp_trash_post' ) );
     497
     498        $wp_trash_post_count = did_action( 'wp_trash_post' );
     499        $this->assertEquals( 'draft', get_post_status( $wp_customize->changeset_post_id() ) );
     500        $this->make_ajax_call( 'customize_trash' );
     501        $this->assertTrue( $this->_last_response_parsed['success'] );
     502        $this->assertEquals( 'trash', get_post_status( $wp_customize->changeset_post_id() ) );
     503        $this->assertEquals( $wp_trash_post_count + 1, did_action( 'wp_trash_post' ) );
     504    }
     505
     506    /**
     507     * Return caps array containing 'do_not_allow'.
     508     *
     509     * @return array Caps.
     510     */
     511    public function return_do_not_allow() {
     512        return array( 'do_not_allow' );
     513    }
     514
     515    /**
     516     * Test request for dismissing autosave changesets.
     517     *
     518     * @ticket 39896
     519     * @covers WP_Customize_Manager::handle_dismiss_autosave_request()
     520     * @covers WP_Customize_Manager::dismiss_user_auto_draft_changesets()
     521     */
     522    public function test_handle_dismiss_autosave_request() {
     523        $uuid = wp_generate_uuid4();
     524        $wp_customize = $this->set_up_valid_state( $uuid );
     525
     526        $this->make_ajax_call( 'customize_dismiss_autosave' );
    457527        $this->assertFalse( $this->_last_response_parsed['success'] );
    458528        $this->assertEquals( 'invalid_nonce', $this->_last_response_parsed['data'] );
    459529
    460         $nonce = wp_create_nonce( 'dismiss_customize_changeset_autosave' );
     530        $nonce = wp_create_nonce( 'customize_dismiss_autosave' );
    461531        $_POST['nonce'] = $_GET['nonce'] = $_REQUEST['nonce'] = $nonce;
    462         $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     532        $this->make_ajax_call( 'customize_dismiss_autosave' );
    463533        $this->assertFalse( $this->_last_response_parsed['success'] );
    464534        $this->assertEquals( 'no_auto_draft_to_delete', $this->_last_response_parsed['data'] );
     
    490560            $this->assertFalse( (bool) get_post_meta( $post_id, '_customize_restore_dismissed', true ) );
    491561        }
    492         $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     562        $this->make_ajax_call( 'customize_dismiss_autosave' );
    493563        $this->assertTrue( $this->_last_response_parsed['success'] );
    494564        $this->assertEquals( 'auto_draft_dismissed', $this->_last_response_parsed['data'] );
     
    503573
    504574        // Subsequent test results in none dismissed.
    505         $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     575        $this->make_ajax_call( 'customize_dismiss_autosave' );
    506576        $this->assertFalse( $this->_last_response_parsed['success'] );
    507577        $this->assertEquals( 'no_auto_draft_to_delete', $this->_last_response_parsed['data'] );
     
    521591
    522592        // Since no autosave yet, confirm no action.
    523         $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     593        $this->make_ajax_call( 'customize_dismiss_autosave' );
    524594        $this->assertFalse( $this->_last_response_parsed['success'] );
    525595        $this->assertEquals( 'no_autosave_revision_to_delete', $this->_last_response_parsed['data'] );
     
    541611
    542612        // Confirm autosave gets deleted.
    543         $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     613        $this->make_ajax_call( 'customize_dismiss_autosave' );
    544614        $this->assertTrue( $this->_last_response_parsed['success'] );
    545615        $this->assertEquals( 'autosave_revision_deleted', $this->_last_response_parsed['data'] );
     
    547617
    548618        // Since no autosave yet, confirm no action.
    549         $this->make_ajax_call( 'dismiss_customize_changeset_autosave' );
     619        $this->make_ajax_call( 'customize_dismiss_autosave' );
    550620        $this->assertFalse( $this->_last_response_parsed['success'] );
    551621        $this->assertEquals( 'no_autosave_revision_to_delete', $this->_last_response_parsed['data'] );
Note: See TracChangeset for help on using the changeset viewer.