From 3fbb8ed287219d25a930032d7231d138ec5d95c3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Oct 2017 03:36:18 +0000 Subject: [PATCH] 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. git-svn-id: https://develop.svn.wordpress.org/trunk@41667 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/customize-controls.css | 104 +++--- src/wp-admin/css/themes.css | 11 +- src/wp-admin/js/customize-controls.js | 303 ++++++++++++------ .../class-wp-customize-manager.php | 90 +++++- .../class-wp-customize-themes-panel.php | 4 - src/wp-includes/js/customize-base.js | 12 +- src/wp-includes/script-loader.php | 4 + tests/phpunit/tests/ajax/CustomizeManager.php | 94 +++++- 8 files changed, 448 insertions(+), 174 deletions(-) diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css index 09302a8a34..35095d160f 100644 --- a/src/wp-admin/css/customize-controls.css +++ b/src/wp-admin/css/customize-controls.css @@ -29,11 +29,13 @@ body { margin-top: 9px; } +body:not(.ready) #customize-save-button-wrapper .save { + visibility: hidden; +} #customize-save-button-wrapper .save { float: left; border-radius: 3px; box-shadow: none; /* @todo Adjust box shadow based on the disable states of paired button. */ - display: none; /* Shown when ready. */ margin-top: 0; } #customize-save-button-wrapper .save.has-next-sibling { @@ -106,18 +108,24 @@ body.outer-section-open .wp-full-overlay.expanded .wp-full-overlay-main { font-size: 14px; width: 30px; float: left; - display: none; /* Shown when ready. */ -webkit-transform: none; transform: none; margin-top: 0; } +body:not(.ready) #publish-settings, +body.trashing #customize-save-button-wrapper .save, +body.trashing #publish-settings { + display: none; +} + #customize-header-actions .spinner { margin-top: 13px; margin-right: 4px; } -.saving #customize-header-actions .spinner { +.saving #customize-header-actions .spinner, +.trashing #customize-header-actions .spinner { visibility: visible; } @@ -147,6 +155,23 @@ body.outer-section-open .wp-full-overlay.expanded .wp-full-overlay-main { padding-right: 12px; } +#customize-control-trash_changeset { + margin-top: 20px; +} +#customize-control-trash_changeset button { + position: relative; + padding-left: 24px; + display: inline-block; +} +#customize-control-trash_changeset button:before { + content: "\f182"; + font: normal 22px dashicons; + text-decoration: none; + position: absolute; + left: 0; + top: -2px; +} + #customize-controls .date-input:invalid { border-color: red; } @@ -1126,6 +1151,43 @@ p.customize-section-description { margin-top: 1px; } +@-webkit-keyframes customize-fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@keyframes customize-fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +#customize-controls .notice.notification-overlay, +#customize-controls #customize-notifications-area .notice.notification-overlay { + margin: 0; + border-left: 0; /* @todo Appropriate styles could be added for notice-error, notice-warning, notice-success, etc */ +} + +#customize-controls .customize-control-notifications-container.has-overlay-notifications { + -webkit-animation: customize-fade-in 0.5s; + animation: customize-fade-in 0.5s; + z-index: 30; +} + +/* Note: Styles for this are also defined in themes.css */ +#customize-controls #customize-notifications-area .notice.notification-overlay .notification-message { + clear: both; + color: #191e23; + font-size: 18px; + font-style: normal; + margin: 0; + padding: 2em 0; + text-align: center; + width: 100%; + display: block; + top: 50%; + position: relative; +} + /* Style for custom settings */ /** @@ -1550,40 +1612,6 @@ p.customize-section-description { * Themes */ -@-webkit-keyframes customize-reload { - 0% { opacity: 0; } - 100% { opacity: 1; } -} - -@keyframes customize-reload { - 0% { opacity: 0; } - 100% { opacity: 1; } -} - -.wp-customizer .customize-loading #customize-themes-loading-container { - display: block; - -webkit-animation: customize-reload .5s; /* Can't use `transition` because `display` changes here. */ - animation: customize-reload .5s; -} - -.customize-loading #customize-themes-loading-container span { - clear: both; - color: #191e23; - font-size: 18px; - font-style: normal; - margin: 0; - padding: 2em 0; - text-align: center; - width: 100%; - display: block; - top: 50%; - position: relative; -} - -.customize-loading #customize-themes-loading-container .customize-loading-text { - display: none; -} - #customize-theme-controls .control-panel-themes { border-bottom: none; } @@ -1673,8 +1701,6 @@ p.customize-section-description { transition: .18s bottom ease-in-out; } -.in-themes-panel:not(.animating) #customize-header-actions .save, -.in-themes-panel:not(.animating) #customize-header-actions #publish-settings, .in-themes-panel:not(.animating) #customize-header-actions .spinner, .in-themes-panel:not(.animating) #customize-header-actions .customize-controls-preview-toggle, .in-themes-panel:not(.animating) #customize-preview, diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css index b6522952e6..782d0cd066 100644 --- a/src/wp-admin/css/themes.css +++ b/src/wp-admin/css/themes.css @@ -1707,8 +1707,7 @@ body.full-overlay-active { } #customize-container, -#customize-themes-loading-container { - display: none; +#customize-controls .notice.notification-overlay { background: #eee; z-index: 500000; position: fixed; @@ -1719,10 +1718,12 @@ body.full-overlay-active { right: 0; height: 100%; } +#customize-container { + display: none; +} /* Make the Customizer and Theme installer overlays the only available content. */ #customize-container, -#customize-themes-loading-container, .theme-install-overlay { visibility: visible; } @@ -1827,7 +1828,7 @@ body.full-overlay-active { #customize-preview.wp-full-overlay-main:before, .customize-loading #customize-container:before, -.customize-loading #customize-themes-loading-container:before, +#customize-controls .notice.notification-overlay.notification-loading:before, .theme-install-overlay .wp-full-overlay-main:before { content: ""; display: block; @@ -1865,7 +1866,7 @@ body.full-overlay-active { #customize-preview.wp-full-overlay-main:before, .customize-loading #customize-container:before, - .customize-loading #customize-themes-loading-container:before, + #customize-controls .notice.notification-overlay.notification-loading:before, .theme-install-overlay .wp-full-overlay-main:before { background-image: url(../images/spinner-2x.gif); } diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js index 70dff7403d..7c1a040b75 100644 --- a/src/wp-admin/js/customize-controls.js +++ b/src/wp-admin/js/customize-controls.js @@ -2,6 +2,41 @@ (function( exports, $ ){ var Container, focus, normalizedTransitionendEventName, api = wp.customize; + /** + * A notification that is displayed in a full-screen overlay. + * + * @since 4.9.0 + * @class + * @augments wp.customize.Notification + */ + api.OverlayNotification = api.Notification.extend({ + + /** + * Whether the notification should show a loading spinner. + * + * @since 4.9.0 + * @var {boolean} + */ + loading: false, + + /** + * Initialize. + * + * @since 4.9.0 + * + * @param {string} code - Code. + * @param {object} params - Params. + */ + initialize: function( code, params ) { + var notification = this; + api.Notification.prototype.initialize.call( notification, code, params ); + notification.classes += ' notification-overlay'; + if ( notification.loading ) { + notification.classes += ' notification-loading'; + } + } + }); + /** * A collection of observable notifications. * @@ -145,7 +180,7 @@ */ render: function() { var collection = this, - notifications, + notifications, hadOverlayNotification = false, hasOverlayNotification, previousNotificationsByCode = {}, listElement; @@ -178,12 +213,30 @@ // Add all notifications in the sorted order. _.each( notifications, function( notification ) { + var notificationContainer; if ( wp.a11y && ( ! previousNotificationsByCode[ notification.code ] || ! _.isEqual( notification.message, previousNotificationsByCode[ notification.code ].message ) ) ) { wp.a11y.speak( notification.message, 'assertive' ); } - listElement.append( $( notification.render() ) ); // @todo Consider slideDown() as enhancement. + notificationContainer = $( notification.render() ); + listElement.append( notificationContainer ); // @todo Consider slideDown() as enhancement. + + // @todo Constraing focus in notificationContainer if notification.extended( api.OverlayNotification ). }); + hasOverlayNotification = Boolean( _.find( notifications, function( notification ) { + return notification.extended( api.OverlayNotification ); + } ) ); + if ( collection.previousNotifications ) { + hadOverlayNotification = Boolean( _.find( collection.previousNotifications, function( notification ) { + return notification.extended( api.OverlayNotification ); + } ) ); + } + + if ( hasOverlayNotification !== hadOverlayNotification ) { + $( document.body ).toggleClass( 'customize-loading', hasOverlayNotification ); + collection.container.toggleClass( 'has-overlay-notifications', hasOverlayNotification ); + } + collection.previousNotifications = notifications; collection.previousContainer = collection.container; collection.trigger( 'rendered' ); @@ -368,6 +421,12 @@ var deferred, request, submittedChanges = {}, data, submittedArgs; deferred = new $.Deferred(); + // Prevent attempting changeset update while request is being made. + if ( 0 !== api.state( 'processing' ).get() ) { + deferred.reject( 'already_processing' ); + return deferred.promise(); + } + submittedArgs = _.extend( { title: null, date: null, @@ -1572,12 +1631,7 @@ // Preview installed themes. section.container.on( 'click', '.theme-actions .preview-theme', function() { - var themeId = $( this ).data( 'slug' ); - - $( '.wp-full-overlay' ).addClass( 'customize-loading' ); - api.panel( 'themes' ).loadThemePreview( themeId ).fail( function() { - $( '.wp-full-overlay' ).removeClass( 'customize-loading' ); - } ); + api.panel( 'themes' ).loadThemePreview( $( this ).data( 'slug' ) ); }); // Theme navigation in details view. @@ -1763,7 +1817,7 @@ // Parameters for every API query. Additional params are set in PHP. page = Math.ceil( section.loaded / 100 ) + 1; params = { - 'switch-themes-nonce': api.settings.nonce['switch-themes'], + 'nonce': api.settings.nonce.switch_themes, 'wp_customize': 'on', 'theme_action': section.params.action, 'customized_theme': api.settings.theme.stylesheet, @@ -1780,7 +1834,7 @@ section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' ); section.loading = true; section.container.find( '.no-themes' ).hide(); - request = wp.ajax.post( 'customize-load-themes', params ); + request = wp.ajax.post( 'customize_load_themes', params ); request.done(function( data ) { var themes = data.themes, themeControl, newThemeControls; @@ -2213,58 +2267,12 @@ * @since 4.7.0 * @access public * + * @deprecated * @param {string} themeId Theme ID. * @returns {jQuery.promise} Promise. */ loadThemePreview: function( themeId ) { - var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser; - - urlParser = document.createElement( 'a' ); - urlParser.href = location.href; - urlParser.search = $.param( _.extend( - api.utils.parseQueryString( urlParser.search.substr( 1 ) ), - { - theme: themeId, - changeset_uuid: api.settings.changeset.uuid - } - ) ); - - overlay = $( '.wp-full-overlay' ); - overlay.addClass( 'customize-loading' ); - - onceProcessingComplete = function() { - var request; - if ( api.state( 'processing' ).get() > 0 ) { - return; - } - - api.state( 'processing' ).unbind( onceProcessingComplete ); - - request = api.requestChangesetUpdate( {}, { autosave: true } ); - request.done( function() { - $( window ).off( 'beforeunload.customize-confirm' ); - - // Include autosaved param to load autosave revision without prompting user to restore it. - if ( ! api.state( 'saved' ).get() ) { - urlParser.search += '&customize_autosaved=on'; - } - - top.location.href = urlParser.href; - deferred.resolve(); - } ); - request.fail( function() { - overlay.removeClass( 'customize-loading' ); - deferred.reject(); - } ); - }; - - if ( 0 === api.state( 'processing' ).get() ) { - onceProcessingComplete(); - } else { - api.state( 'processing' ).bind( onceProcessingComplete ); - } - - return deferred.promise(); + return api.ThemesPanel.prototype.loadThemePreview.call( this, themeId ); }, /** @@ -2844,10 +2852,9 @@ $( document ).one( 'wp-theme-install-success', function( event, response ) { var theme = false, customizeId, themeControl; if ( preview ) { + api.notifications.remove( 'theme_installing' ); - panel.loadThemePreview( slug ).fail( function() { - $( '.wp-full-overlay' ).removeClass( 'customize-loading' ); - } ); + panel.loadThemePreview( slug ); } else { api.control.each( function( control ) { @@ -2899,8 +2906,12 @@ // Also preview the theme as the event is triggered on Install & Preview. if ( $( event.target ).hasClass( 'preview' ) ) { preview = true; - $( '.wp-full-overlay' ).addClass( 'customize-loading' ); - wp.a11y.speak( $( '#customize-themes-loading-container .customize-loading-text-installing-theme' ).text() ); + + api.notifications.add( 'theme_installing', new api.OverlayNotification( 'theme_installing', { + message: api.l10n.themeDownloading, + type: 'info', + loading: true + } ) ); } }, @@ -2913,24 +2924,31 @@ * @returns {jQuery.promise} Promise. */ loadThemePreview: function( themeId ) { - var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser; + var deferred = $.Deferred(), onceProcessingComplete, urlParser, queryParams; urlParser = document.createElement( 'a' ); urlParser.href = location.href; - urlParser.search = $.param( _.extend( + queryParams = _.extend( api.utils.parseQueryString( urlParser.search.substr( 1 ) ), { theme: themeId, changeset_uuid: api.settings.changeset.uuid } - ) ); + ); + + // Include autosaved param to load autosave revision without prompting user to restore it. + if ( ! api.state( 'saved' ).get() ) { + queryParams.customize_autosaved = 'on'; + } + + urlParser.search = $.param( queryParams ); // Update loading message. Everything else is handled by reloading the page. - $( '#customize-themes-loading-container span' ).hide(); - $( '#customize-themes-loading-container .customize-loading-text' ).css( 'display', 'block' ); - wp.a11y.speak( $( '#customize-themes-loading-container .customize-loading-text' ).text() ); - overlay = $( '.wp-full-overlay' ); - overlay.addClass( 'customize-loading' ); + api.notifications.add( 'theme_previewing', new api.OverlayNotification( 'theme_previewing', { + message: api.l10n.themePreviewWait, + type: 'info', + loading: true + } ) ); onceProcessingComplete = function() { var request; @@ -2940,14 +2958,17 @@ api.state( 'processing' ).unbind( onceProcessingComplete ); - request = api.requestChangesetUpdate(); + request = api.requestChangesetUpdate( {}, { autosave: true } ); request.done( function() { deferred.resolve(); $( window ).off( 'beforeunload.customize-confirm' ); - window.location.href = urlParser.href; + window.location.href = urlParser.href; // @todo Use location.replace()? } ); request.fail( function() { - overlay.removeClass( 'customize-loading' ); + + // @todo Show notification regarding failure. + api.notifications.remove( 'theme_previewing' ); + deferred.reject(); } ); }; @@ -6230,8 +6251,8 @@ api.state = new api.Values(); _.each( [ 'saved', - 'autosaved', 'saving', + 'trashing', 'activated', 'processing', 'paneVisible', @@ -6279,8 +6300,6 @@ publishSettingsBtn = $( '#publish-settings' ), footerActions = $( '#customize-footer-actions' ); - saveBtn.show(); - api.section( 'publish_settings', function( section ) { var updateButtonsState, previewLinkControl, previewLinkControlId = 'changeset_preview_link', updateSectionActive, isSectionActive; @@ -6301,7 +6320,10 @@ * @return {boolean} Is section active. */ isSectionActive = function() { - if ( ! api.state( 'activated' ) ) { + if ( ! api.state( 'activated' ).get() ) { + return false; + } + if ( api.state( 'trashing' ).get() || 'trash' === api.state( 'changesetStatus' ).get() ) { return false; } if ( '' === api.state( 'changesetStatus' ).get() && api.state( 'saved' ).get() ) { @@ -6316,6 +6338,7 @@ section.active.set( isSectionActive() ); }; api.state( 'activated' ).bind( updateSectionActive ); + api.state( 'trashing' ).bind( updateSectionActive ); api.state( 'saved' ).bind( updateSectionActive ); api.state( 'changesetStatus' ).bind( updateSectionActive ); updateSectionActive(); @@ -6551,15 +6574,13 @@ * due to dependencies on other settings. */ request = wp.ajax.post( 'customize_save', query ); - - // Disable save button during the save request. - saveBtn.prop( 'disabled', true ); + api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 ); api.trigger( 'save', request ); request.always( function () { + api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 ); api.state( 'saving' ).set( false ); - saveBtn.prop( 'disabled', false ); api.unbind( 'change', captureSettingModifiedDuringSave ); } ); @@ -6636,7 +6657,9 @@ previewer.send( 'saved', response ); api.state( 'changesetStatus' ).set( response.changeset_status ); - api.state( 'changesetDate' ).set( response.changeset_date ); + if ( response.changeset_date ) { + api.state( 'changesetDate' ).set( response.changeset_date ); + } if ( 'publish' === response.changeset_status ) { @@ -6693,6 +6716,74 @@ return deferred.promise(); }, + /** + * Trash the current changes. + * + * Revert the Customizer to it's previously-published state. + * + * @since 4.9.0 + * + * @returns {jQuery.promise} Promise. + */ + trash: function trash() { + var request, success, fail; + + api.state( 'trashing' ).set( true ); + api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 ); + + request = wp.ajax.post( 'customize_trash', { + customize_changeset_uuid: api.settings.changeset.uuid, + nonce: api.settings.nonce.trash + } ); + api.notifications.add( 'changeset_trashing', new api.OverlayNotification( 'changeset_trashing', { + type: 'info', + message: api.l10n.revertingChanges, + loading: true + } ) ); + + success = function() { + var urlParser = document.createElement( 'a' ), queryParams; + + api.state( 'changesetStatus' ).set( 'trash' ); + api.each( function( setting ) { + setting._dirty = false; + } ); + api.state( 'saved' ).set( true ); + + // Go back to Customizer without changeset. + urlParser.href = location.href; + queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) ); + delete queryParams.changeset_uuid; + urlParser.search = $.param( queryParams ); + location.replace( urlParser.href ); + }; + + fail = function( code, message ) { + var notificationCode = code || 'unknown_error'; + api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 ); + api.state( 'trashing' ).set( false ); + api.notifications.remove( 'changeset_trashing' ); + api.notifications.add( notificationCode, new api.Notification( notificationCode, { + message: message || api.l10n.unknownError, + dismissible: true, + type: 'error' + } ) ); + }; + + request.done( function( response ) { + success( response.message ); + } ); + + request.fail( function( response ) { + var code = response.code || 'trashing_failed'; + if ( response.success || 'non_existent_changeset' === code || 'changeset_already_trashed' === code ) { + success( response.message ); + } else { + fail( code, response.message ); + } + } ); + }, + /** * Builds the front preview url with the current state of customizer. * @@ -6842,6 +6933,7 @@ (function( state ) { var saved = state.instance( 'saved' ), saving = state.instance( 'saving' ), + trashing = state.instance( 'trashing' ), activated = state.instance( 'activated' ), processing = state.instance( 'processing' ), paneVisible = state.instance( 'paneVisible' ), @@ -6863,7 +6955,6 @@ if ( ! activated() ) { saveBtn.val( api.l10n.activate ); closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); - publishSettingsBtn.prop( 'disabled', false ); } else if ( '' === changesetStatus.get() && saved() ) { if ( api.settings.changeset.currentUserCanPublish ) { @@ -6871,7 +6962,6 @@ } else { saveBtn.val( api.l10n.saved ); } - publishSettingsBtn.prop( 'disabled', true ); closeBtn.find( '.screen-reader-text' ).text( api.l10n.close ); } else { @@ -6899,14 +6989,13 @@ saveBtn.val( api.l10n.publish ); } closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel ); - publishSettingsBtn.prop( 'disabled', false ); } /* * Save (publish) button should be enabled if saving is not currently happening, * and if the theme is not active or the changeset exists but is not published. */ - canSave = ! saving() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) ); + canSave = ! saving() && ! trashing() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) ); saveBtn.prop( 'disabled', ! canSave ); }); @@ -6958,6 +7047,9 @@ saving.bind( function( isSaving ) { body.toggleClass( 'saving', isSaving ); } ); + trashing.bind( function( isTrashing ) { + body.toggleClass( 'trashing', isTrashing ); + } ); api.bind( 'saved', function( response ) { state('saved').set( true ); @@ -7028,7 +7120,7 @@ // Show changeset UUID in URL when in branching mode and there is a saved changeset. if ( api.settings.changeset.branching ) { changesetStatus.bind( function( newStatus ) { - populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus ); + populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus && 'trash' !== newStatus ); } ); } }( api.state ) ); @@ -7105,7 +7197,7 @@ // Handle dismissal of notice. li.find( '.notice-dismiss' ).on( 'click', function() { - wp.ajax.post( 'dismiss_customize_changeset_autosave', { + wp.ajax.post( 'customize_dismiss_autosave', { wp_customize: 'on', customize_theme: api.settings.theme.stylesheet, customize_changeset_uuid: api.settings.changeset.uuid, @@ -7532,15 +7624,20 @@ isInsideIframe = true; }); - // Prompt user with AYS dialog if leaving the Customizer with unsaved changes - $( window ).on( 'beforeunload.customize-confirm', function() { - if ( ! isCleanState() ) { - setTimeout( function() { - overlay.removeClass( 'customize-loading' ); - }, 1 ); - return api.l10n.saveAlert; - } - }); + function startPromptingBeforeUnload() { + api.unbind( 'change', startPromptingBeforeUnload ); + + // Prompt user with AYS dialog if leaving the Customizer with unsaved changes + $( window ).on( 'beforeunload.customize-confirm', function() { + if ( ! isCleanState() ) { + setTimeout( function() { + overlay.removeClass( 'customize-loading' ); + }, 1 ); + return api.l10n.saveAlert; + } + }); + } + api.bind( 'change', startPromptingBeforeUnload ); closeBtn.on( 'click.customize-controls-close', function( event ) { var clearedToClose = $.Deferred(); @@ -7566,7 +7663,7 @@ if ( '' === api.state( 'changesetStatus' ).get() ) { clearedToClose.resolve(); } else { - wp.ajax.send( 'dismiss_customize_changeset_autosave', { + wp.ajax.send( 'customize_dismiss_autosave', { timeout: 500, // Don't wait too long. data: { wp_customize: 'on', @@ -7987,9 +8084,11 @@ } ); // Autosave changeset. - ( function() { + function startAutosaving() { var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false; + api.unbind( 'change', startAutosaving ); // Ensure startAutosaving only fires once. + api.state( 'saved' ).bind( function( isSaved ) { if ( ! isSaved && ! api.settings.changeset.autosaved ) { api.settings.changeset.autosaved = true; // Once a change is made then autosaving kicks in. @@ -8040,7 +8139,8 @@ $( window ).on( 'beforeunload.wp-customize-changeset-update', function() { updateChangesetWithReschedule(); } ); - } ()); + } + api.bind( 'change', startAutosaving ); // Make sure TinyMCE dialogs appear above Customizer UI. $( document ).one( 'wp-before-tinymce-init', function() { @@ -8049,6 +8149,7 @@ } } ); + body.addClass( 'ready' ); api.trigger( 'ready' ); }); diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php index 95dfa794af..6611a06d3c 100644 --- a/src/wp-includes/class-wp-customize-manager.php +++ b/src/wp-includes/class-wp-customize-manager.php @@ -374,10 +374,11 @@ final class WP_Customize_Manager { remove_action( 'admin_init', '_maybe_update_plugins' ); remove_action( 'admin_init', '_maybe_update_themes' ); - add_action( 'wp_ajax_customize_save', array( $this, 'save' ) ); - add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) ); - add_action( 'wp_ajax_customize-load-themes', array( $this, 'load_themes_ajax' ) ); - add_action( 'wp_ajax_dismiss_customize_changeset_autosave', array( $this, 'handle_dismiss_changeset_autosave_request' ) ); + add_action( 'wp_ajax_customize_save', array( $this, 'save' ) ); + add_action( 'wp_ajax_customize_trash', array( $this, 'handle_changeset_trash_request' ) ); + add_action( 'wp_ajax_customize_refresh_nonces', array( $this, 'refresh_nonces' ) ); + add_action( 'wp_ajax_customize_load_themes', array( $this, 'handle_load_themes_request' ) ); + add_action( 'wp_ajax_customize_dismiss_autosave', array( $this, 'handle_dismiss_autosave_request' ) ); add_action( 'customize_register', array( $this, 'register_controls' ) ); add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first @@ -2829,6 +2830,65 @@ final class WP_Customize_Manager { return $response; } + /** + * Handle request to trash a changeset. + * + * @since 4.9.0 + */ + public function handle_changeset_trash_request() { + if ( ! is_user_logged_in() ) { + wp_send_json_error( 'unauthenticated' ); + } + + if ( ! $this->is_preview() ) { + wp_send_json_error( 'not_preview' ); + } + + if ( ! check_ajax_referer( 'trash_customize_changeset', 'nonce', false ) ) { + wp_send_json_error( array( + 'code' => 'invalid_nonce', + 'message' => __( 'There was an authentication problem. Please reload and try again.' ), + ) ); + } + + $changeset_post_id = $this->changeset_post_id(); + + if ( ! $changeset_post_id ) { + wp_send_json_error( array( + 'message' => __( 'No changes saved yet, so there is nothing to trash.' ), + 'code' => 'non_existent_changeset', + ) ); + return; + } + + if ( $changeset_post_id && ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->delete_post, $changeset_post_id ) ) { + wp_send_json_error( array( + 'code' => 'changeset_trash_unauthorized', + 'message' => __( 'Unable to trash changes.' ), + ) ); + } + + if ( 'trash' === get_post_status( $changeset_post_id ) ) { + wp_send_json_error( array( + 'message' => __( 'Changes have already been trashed.' ), + 'code' => 'changeset_already_trashed', + ) ); + return; + } + + $r = wp_trash_post( $changeset_post_id ); + if ( ! ( $r instanceof WP_Post ) ) { + wp_send_json_error( array( + 'code' => 'changeset_trash_failure', + 'message' => __( 'Unable to trash changes.' ), + ) ); + } + + wp_send_json_success( array( + 'message' => __( 'Changes trashed successfully.' ), + ) ); + } + /** * Re-map 'edit_post' meta cap for a customize_changeset post to be the same as 'customize' maps. * @@ -3102,12 +3162,12 @@ final class WP_Customize_Manager { * * @since 4.9.0 */ - public function handle_dismiss_changeset_autosave_request() { + public function handle_dismiss_autosave_request() { if ( ! $this->is_preview() ) { wp_send_json_error( 'not_preview', 400 ); } - if ( ! check_ajax_referer( 'dismiss_customize_changeset_autosave', 'nonce', false ) ) { + if ( ! check_ajax_referer( 'customize_dismiss_autosave', 'nonce', false ) ) { wp_send_json_error( 'invalid_nonce', 403 ); } @@ -3543,8 +3603,8 @@ final class WP_Customize_Manager { ?> + wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ), 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() ), - 'switch-themes' => wp_create_nonce( 'switch-themes' ), - 'dismiss_autosave' => wp_create_nonce( 'dismiss_customize_changeset_autosave' ), + 'switch_themes' => wp_create_nonce( 'switch_themes' ), + 'dismiss_autosave' => wp_create_nonce( 'customize_dismiss_autosave' ), + 'trash' => wp_create_nonce( 'trash_customize_changeset' ), ); /** @@ -4159,6 +4223,7 @@ final class WP_Customize_Manager { $this->add_control( 'changeset_status', array( 'section' => 'publish_settings', + 'priority' => 10, 'settings' => array(), 'type' => 'radio', 'label' => __( 'Action' ), @@ -4173,6 +4238,7 @@ final class WP_Customize_Manager { } $this->add_control( new WP_Customize_Date_Time_Control( $this, 'changeset_scheduled_date', array( 'section' => 'publish_settings', + 'priority' => 20, 'settings' => array(), 'type' => 'date_time', 'min_year' => date( 'Y' ), @@ -4723,8 +4789,8 @@ final class WP_Customize_Manager { * * @since 4.9.0 */ - public function load_themes_ajax() { - check_ajax_referer( 'switch-themes', 'switch-themes-nonce' ); + public function handle_load_themes_request() { + check_ajax_referer( 'switch_themes', 'nonce' ); if ( ! current_user_can( 'switch_themes' ) ) { wp_die( -1 ); diff --git a/src/wp-includes/customize/class-wp-customize-themes-panel.php b/src/wp-includes/customize/class-wp-customize-themes-panel.php index 8cd8e8dd88..bbb0d16daa 100644 --- a/src/wp-includes/customize/class-wp-customize-themes-panel.php +++ b/src/wp-includes/customize/class-wp-customize-themes-panel.php @@ -89,10 +89,6 @@ class WP_Customize_Themes_Panel extends WP_Customize_Panel { <# } #> -
  • - - -