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:
@@ -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.
|
||||
|
||||
6
wp-admin/js/customize-controls.min.js
vendored
6
wp-admin/js/customize-controls.min.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user