Customize: Extend changesets to support autosave revisions with restoration notifications, and introduce a new default linear history mode for saved changesets (with a filter for opt-in to changeset branching).

* Autosaved changes made on top of `auto-draft` changesets get written on top of the `auto-draft` itself, similar to how autosaves for posts will overwrite post drafts.
* Autosaved changes made to saved changesets (e.g. `draft`, `future`) will be placed into an autosave revision for that changeset and that user.
* Opening the Customizer will now prompt the user to restore their most recent auto-draft changeset; if notification is dismissed or ignored then the auto-draft will be marked as dismissed and will not be prompted to user in a notification again.
* Customizer will no longer automatically supply the `changeset_uuid` param in the `customize.php` URL when branching changesets are not active.
* If user closes Customizer explicitly via clicking on X link, then autosave auto-draft/autosave will be dismissed so as to not be prompted again.
* If there is a changeset already saved as a `draft` or `future` (UI is forthcoming) then this changeset will now be autoloaded for the user to keep making additional changes. This is the linear model for changesets.
* To restore the previous behavior of the Customizer where each session started a new changeset, regardless of whether or not there was an existing changeset saved, there is now a `customize_changeset_branching` hook which can be filtered to return `true`.
* `wp.customize.requestChangesetUpdate()` now supports a second with options including `autosave`, `title`, and `date`.
* The window `blur` event for `customize.php` has been replaced with a `visibilitychange` event to reduce autosave requests when clicking into preview window.
* Adds `autosaved` and `branching` args to `WP_Customize_Manager`.
* The `changeset_uuid` param for `WP_Customize_Manager` is extended to recognize a `false` value which causes the Customizer to defer identifying the UUID until `after_setup_theme` in the new `WP_Customize_Manager::establish_loaded_changeset()` method.
* A new `customize_autosaved` query parameter can now be supplied which is passed into the `autosaved` arg in `WP_Customize_Manager`; this option is an opt-in to source data from the autosave revision, allowing a user to restore autosaved changes.

Props westonruter, dlh, sayedwp, JoshuaWold, melchoyce.
See #39896.

Built from https://develop.svn.wordpress.org/trunk@41597


git-svn-id: http://core.svn.wordpress.org/trunk@41430 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Weston Ruter
2017-09-26 07:38:45 +00:00
parent 85712eb281
commit 1a7616ad54
8 changed files with 657 additions and 69 deletions

View File

@@ -1,4 +1,4 @@
/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console */
/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console, confirm */
(function( exports, $ ){
var Container, focus, normalizedTransitionendEventName, api = wp.customize;
@@ -355,14 +355,24 @@
* @since 4.7.0
* @access public
*
* @param {object} [changes] Mapping of setting IDs to setting params each normally including a value property, or mapping to null.
* If not provided, then the changes will still be obtained from unsaved dirty settings.
* @param {object} [changes] - Mapping of setting IDs to setting params each normally including a value property, or mapping to null.
* If not provided, then the changes will still be obtained from unsaved dirty settings.
* @param {object} [args] - Additional options for the save request.
* @param {boolean} [args.autosave=false] - Whether changes will be stored in autosave revision if the changeset has been promoted from an auto-draft.
* @param {string} [args.title] - Title to update in the changeset. Optional.
* @param {string} [args.date] - Date to update in the changeset. Optional.
* @returns {jQuery.Promise} Promise resolving with the response data.
*/
api.requestChangesetUpdate = function requestChangesetUpdate( changes ) {
var deferred, request, submittedChanges = {}, data;
api.requestChangesetUpdate = function requestChangesetUpdate( changes, args ) {
var deferred, request, submittedChanges = {}, data, submittedArgs;
deferred = new $.Deferred();
submittedArgs = _.extend( {
title: null,
date: null,
autosave: false
}, args );
if ( changes ) {
_.extend( submittedChanges, changes );
}
@@ -379,20 +389,30 @@
} );
// Short-circuit when there are no pending changes.
if ( _.isEmpty( submittedChanges ) ) {
if ( _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) {
deferred.resolve( {} );
return deferred.promise();
}
// Allow plugins to attach additional params to the settings.
api.trigger( 'changeset-save', submittedChanges, submittedArgs );
// 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.
if ( submittedArgs.status ) {
return deferred.reject( { code: 'illegal_status_in_changeset_update' } ).promise();
}
// Dates not beung allowed for revisions are is a technical limitation of post revisions.
if ( submittedArgs.date && submittedArgs.autosave ) {
return deferred.reject( { code: 'illegal_autosave_with_date_gmt' } ).promise();
}
// Make sure that publishing a changeset waits for all changeset update requests to complete.
api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
deferred.always( function() {
api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
} );
// Allow plugins to attach additional params to the settings.
api.trigger( 'changeset-save', submittedChanges );
// Ensure that if any plugins add data to save requests by extending query() that they get included here.
data = api.previewer.query( { excludeCustomizedSaved: true } );
delete data.customized; // Being sent in customize_changeset_data instead.
@@ -401,6 +421,15 @@
customize_theme: api.settings.theme.stylesheet,
customize_changeset_data: JSON.stringify( submittedChanges )
} );
if ( null !== submittedArgs.title ) {
data.customize_changeset_title = submittedArgs.title;
}
if ( null !== submittedArgs.date ) {
data.customize_changeset_date = submittedArgs.date;
}
if ( false !== submittedArgs.autosave ) {
data.customize_changeset_autosave = 'true';
}
request = wp.ajax.post( 'customize_save', data );
@@ -1705,9 +1734,15 @@
api.state( 'processing' ).unbind( onceProcessingComplete );
request = api.requestChangesetUpdate();
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();
} );
@@ -4024,6 +4059,9 @@
customize_messenger_channel: previewFrame.query.customize_messenger_channel
}
);
if ( ! api.state( 'saved' ).get() ) {
params.customize_autosaved = 'on';
}
urlParser.search = $.param( params );
previewFrame.iframe = $( '<iframe />', {
@@ -4260,6 +4298,7 @@
delete queryParams.customize_changeset_uuid;
delete queryParams.customize_theme;
delete queryParams.customize_messenger_channel;
delete queryParams.customize_autosaved;
if ( _.isEmpty( queryParams ) ) {
urlParser.search = '';
} else {
@@ -4884,6 +4923,9 @@
nonce: this.nonce.preview,
customize_changeset_uuid: api.settings.changeset.uuid
};
if ( ! api.state( 'saved' ).get() ) {
queryVars.customize_autosaved = 'on';
}
/*
* Exclude customized data if requested especially for calls to requestChangesetUpdate.
@@ -5098,6 +5140,9 @@
parent.send( 'changeset-uuid', api.settings.changeset.uuid );
}
// Prevent subsequent requestChangesetUpdate() calls from including the settings that have been saved.
api._lastSavedRevision = Math.max( latestRevision, api._lastSavedRevision );
if ( response.setting_validities ) {
api._handleSettingValidities( {
settingValidities: response.setting_validities,
@@ -5315,10 +5360,18 @@
api.bind( 'change', function() {
if ( state( 'saved' ).get() ) {
state( 'saved' ).set( false );
populateChangesetUuidParam( true );
}
});
// Populate changeset UUID param when state becomes dirty.
if ( api.settings.changeset.branching ) {
saved.bind( function( isSaved ) {
if ( ! isSaved ) {
populateChangesetUuidParam( true );
}
});
}
saving.bind( function( isSaving ) {
body.toggleClass( 'saving', isSaving );
} );
@@ -5371,14 +5424,123 @@
history.replaceState( {}, document.title, urlParser.href );
};
changesetStatus.bind( function( newStatus ) {
populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus );
} );
// 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 );
} );
}
// Expose states to the API.
api.state = state;
}());
// Set up autosave prompt.
(function() {
/**
* Obtain the URL to restore the autosave.
*
* @returns {string} Customizer URL.
*/
function getAutosaveRestorationUrl() {
var urlParser, queryParams;
urlParser = document.createElement( 'a' );
urlParser.href = location.href;
queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
if ( api.settings.changeset.latestAutoDraftUuid ) {
queryParams.changeset_uuid = api.settings.changeset.latestAutoDraftUuid;
} else {
queryParams.customize_autosaved = 'on';
}
urlParser.search = $.param( queryParams );
return urlParser.href;
}
/**
* Remove parameter from the URL.
*
* @param {Array} params - Parameter names to remove.
* @returns {void}
*/
function stripParamsFromLocation( params ) {
var urlParser = document.createElement( 'a' ), queryParams, strippedParams = 0;
urlParser.href = location.href;
queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
_.each( params, function( param ) {
if ( 'undefined' !== typeof queryParams[ param ] ) {
strippedParams += 1;
delete queryParams[ param ];
}
} );
if ( 0 === strippedParams ) {
return;
}
urlParser.search = $.param( queryParams );
history.replaceState( {}, document.title, urlParser.href );
}
/**
* Add notification regarding the availability of an autosave to restore.
*
* @returns {void}
*/
function addAutosaveRestoreNotification() {
var code = 'autosave_available', onStateChange;
// Since there is an autosave revision and the user hasn't loaded with autosaved, add notification to prompt to load autosaved version.
api.notifications.add( code, new api.Notification( code, {
message: api.l10n.autosaveNotice,
type: 'warning',
dismissible: true,
render: function() {
var li = api.Notification.prototype.render.call( this ), link;
// Handle clicking on restoration link.
link = li.find( 'a' );
link.prop( 'href', getAutosaveRestorationUrl() );
link.on( 'click', function( event ) {
event.preventDefault();
location.replace( getAutosaveRestorationUrl() );
} );
// Handle dismissal of notice.
li.find( '.notice-dismiss' ).on( 'click', function() {
wp.ajax.post( 'dismiss_customize_changeset_autosave', {
wp_customize: 'on',
customize_theme: api.settings.theme.stylesheet,
customize_changeset_uuid: api.settings.changeset.latestAutoDraftUuid || api.settings.changeset.uuid,
nonce: api.settings.nonce.dismiss_autosave
} );
} );
return li;
}
} ) );
// Remove the notification once the user starts making changes.
onStateChange = function() {
api.notifications.remove( code );
api.state( 'saved' ).unbind( onStateChange );
api.state( 'saving' ).unbind( onStateChange );
api.state( 'changesetStatus' ).unbind( onStateChange );
};
api.state( 'saved' ).bind( onStateChange );
api.state( 'saving' ).bind( onStateChange );
api.state( 'changesetStatus' ).bind( onStateChange );
}
if ( api.settings.changeset.autosaved ) {
stripParamsFromLocation( [ 'customize_autosaved' ] ); // Remove param when restoring autosave revision.
} else if ( ! api.settings.changeset.branching && 'auto-draft' === api.settings.changeset.status ) {
stripParamsFromLocation( [ 'changeset_uuid' ] ); // Remove UUID when restoring autosave auto-draft.
}
if ( api.settings.changeset.latestAutoDraftUuid || api.settings.changeset.hasAutosaveRevision ) {
addAutosaveRestoreNotification();
}
})();
// Check if preview url is valid and load the preview frame.
if ( api.previewer.previewUrl() ) {
api.previewer.refresh();
@@ -5742,26 +5904,82 @@
channel: 'loader'
});
/*
* If we receive a 'back' event, we're inside an iframe.
* Send any clicks to the 'Return' link to the parent page.
*/
parent.bind( 'back', function() {
closeBtn.on( 'click.customize-controls-close', function( event ) {
event.preventDefault();
parent.send( 'close' );
});
});
// Handle exiting of Customizer.
(function() {
var isInsideIframe = false;
// Prompt user with AYS dialog if leaving the Customizer with unsaved changes
$( window ).on( 'beforeunload.customize-confirm', function () {
if ( ! api.state( 'saved' )() ) {
setTimeout( function() {
overlay.removeClass( 'customize-loading' );
}, 1 );
return api.l10n.saveAlert;
function isCleanState() {
return api.state( 'saved' ).get() && 'auto-draft' !== api.state( 'changesetStatus' ).get();
}
} );
/*
* If we receive a 'back' event, we're inside an iframe.
* Send any clicks to the 'Return' link to the parent page.
*/
parent.bind( 'back', function() {
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;
}
});
closeBtn.on( 'click.customize-controls-close', function( event ) {
var clearedToClose = $.Deferred();
event.preventDefault();
/*
* The isInsideIframe condition is because Customizer is not able to use a confirm()
* since customize-loader.js will also use one. So autosave restorations are disabled
* when customize-loader.js is used.
*/
if ( isInsideIframe && isCleanState() ) {
clearedToClose.resolve();
} else if ( confirm( api.l10n.saveAlert ) ) {
// Mark all settings as clean to prevent another call to requestChangesetUpdate.
api.each( function( setting ) {
setting._dirty = false;
});
$( document ).off( 'visibilitychange.wp-customize-changeset-update' );
$( window ).off( 'beforeunload.wp-customize-changeset-update' );
closeBtn.css( 'cursor', 'progress' );
if ( '' === api.state( 'changesetStatus' ).get() ) {
clearedToClose.resolve();
} else {
wp.ajax.send( 'dismiss_customize_changeset_autosave', {
timeout: 500, // Don't wait too long.
data: {
wp_customize: 'on',
customize_theme: api.settings.theme.stylesheet,
customize_changeset_uuid: api.settings.changeset.uuid,
nonce: api.settings.nonce.dismiss_autosave
}
} ).always( function() {
clearedToClose.resolve();
} );
}
} else {
clearedToClose.reject();
}
clearedToClose.done( function() {
$( window ).off( 'beforeunload.customize-confirm' );
if ( isInsideIframe ) {
parent.send( 'close' );
} else {
window.location.href = closeBtn.prop( 'href' );
}
} );
});
})();
// Pass events through to the parent.
$.each( [ 'saved', 'change' ], function ( i, event ) {
@@ -6084,6 +6302,13 @@
( function() {
var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
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.
api.previewer.send( 'autosaving' );
}
} );
/**
* Request changeset update and then re-schedule the next changeset update time.
*
@@ -6093,7 +6318,7 @@
updateChangesetWithReschedule = function() {
if ( ! updatePending ) {
updatePending = true;
api.requestChangesetUpdate().always( function() {
api.requestChangesetUpdate( {}, { autosave: true } ).always( function() {
updatePending = false;
} );
}
@@ -6117,8 +6342,10 @@
scheduleChangesetUpdate();
// Save changeset when focus removed from window.
$( window ).on( 'blur.wp-customize-changeset-update', function() {
updateChangesetWithReschedule();
$( document ).on( 'visibilitychange.wp-customize-changeset-update', function() {
if ( document.hidden ) {
updateChangesetWithReschedule();
}
} );
// Save changeset before unloading window.

File diff suppressed because one or more lines are too long