: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
api.notifications.remove( errorCode );
* Block saving if there are any settings that are marked as
* invalid from the client (not from the server). Focus on
api.each( function( setting ) {
setting.notifications.each( function( notification ) {
if ( 'error' === notification.type && ! notification.fromServer ) {
invalidSettings.push( setting.id );
if ( ! settingInvalidities[ setting.id ] ) {
settingInvalidities[ setting.id ] = {};
settingInvalidities[ setting.id ][ notification.code ] = notification;
// Find all invalid setting less controls with notification type error.
api.control.each( function( control ) {
if ( ! control.setting || ! control.setting.id && control.active.get() ) {
control.notifications.each( function( notification ) {
if ( 'error' === notification.type ) {
invalidSettingLessControls.push( [ control ] );
invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) );
if ( ! _.isEmpty( invalidControls ) ) {
invalidControls[0][0].focus();
api.unbind( 'change', captureSettingModifiedDuringSave );
if ( invalidSettings.length ) {
api.notifications.add( new api.Notification( errorCode, {
message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ),
deferred.rejectWith( previewer, [
{ setting_invalidities: settingInvalidities }
api.state( 'saving' ).set( false );
return deferred.promise();
* Note that excludeCustomizedSaved is intentionally false so that the entire
* set of customized data will be included if bypassed changeset update.
query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), {
nonce: previewer.nonce.save,
customize_changeset_status: changesetStatus
if ( args && args.date ) {
query.customize_changeset_date = args.date;
} else if ( 'future' === changesetStatus && selectedChangesetDate ) {
query.customize_changeset_date = selectedChangesetDate;
if ( args && args.title ) {
query.customize_changeset_title = args.title;
// Allow plugins to modify the params included with the save request.
api.trigger( 'save-request-params', query );
* Note that the dirty customized values will have already been set in the
* changeset and so technically query.customized could be deleted. However,
* it is remaining here to make sure that any settings that got updated
* quietly which may have not triggered an update request will also get
* included in the values that get saved to the changeset. This will ensure
* that values that get injected via the saved event will be included in
* the changeset. This also ensures that setting values that were invalid
* will get re-validated, perhaps in the case of settings that are invalid
* due to dependencies on other settings.
request = wp.ajax.post( 'customize_save', query );
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 );
api.unbind( 'change', captureSettingModifiedDuringSave );
// Remove notifications that were added due to save failures.
api.notifications.each( function( notification ) {
if ( notification.saveFailure ) {
api.notifications.remove( notification.code );
request.fail( function ( response ) {
var notification, notificationArgs;
if ( '0' === response ) {
response = 'not_logged_in';
} else if ( '-1' === response ) {
// Back-compat in case any other check_ajax_referer() call is dying.
response = 'invalid_nonce';
if ( 'invalid_nonce' === response ) {
} else if ( 'not_logged_in' === response ) {
previewer.preview.iframe.hide();
previewer.login().done( function() {
previewer.preview.iframe.show();
} else if ( response.code ) {
if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) {
api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus();
} else if ( 'changeset_locked' !== response.code ) {
notification = new api.Notification( response.code, _.extend( notificationArgs, {
message: response.message
notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
message: api.l10n.unknownRequestFail
api.notifications.add( notification );
if ( response.setting_validities ) {
api._handleSettingValidities( {
settingValidities: response.setting_validities,
focusInvalidControl: true
deferred.rejectWith( previewer, [ response ] );
api.trigger( 'error', response );
// Start a new changeset if the underlying changeset was published.
if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) {
api.settings.changeset.uuid = response.next_changeset_uuid;
api.state( 'changesetStatus' ).set( '' );
if ( api.settings.changeset.branching ) {
parent.send( 'changeset-uuid', api.settings.changeset.uuid );
api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid );
request.done( function( response ) {
previewer.send( 'saved', response );
api.state( 'changesetStatus' ).set( response.changeset_status );
if ( response.changeset_date ) {
api.state( 'changesetDate' ).set( response.changeset_date );
if ( 'publish' === response.changeset_status ) {
// Mark all published as clean if they haven't been modified during the request.
api.each( function( setting ) {
* Note that the setting revision will be undefined in the case of setting
* values that are marked as dirty when the customizer is loaded, such as
* when applying starter content. All other dirty settings will have an
* associated revision due to their modification triggering a change event.
if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) {
api.state( 'changesetStatus' ).set( '' );
api.settings.changeset.uuid = response.next_changeset_uuid;
if ( api.settings.changeset.branching ) {
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,
focusInvalidControl: true
deferred.resolveWith( previewer, [ response ] );
api.trigger( 'saved', response );
// Restore the global dirty state if any settings were modified during save.
if ( ! _.isEmpty( modifiedWhileSaving ) ) {
api.state( 'saved' ).set( false );
if ( 0 === processing() ) {
submitWhenDoneProcessing = function () {
if ( 0 === processing() ) {
api.state.unbind( 'change', submitWhenDoneProcessing );
api.state.bind( 'change', submitWhenDoneProcessing );
return deferred.promise();
* Trash the current changes.
* Revert the Customizer to its previously-published state.
* @return {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( new api.OverlayNotification( 'changeset_trashing', {
message: api.l10n.revertingChanges,
var urlParser = document.createElement( 'a' ), queryParams;
api.state( 'changesetStatus' ).set( 'trash' );
api.each( function( setting ) {
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;
queryParams['return'] = api.settings.url['return'];
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( new api.Notification( notificationCode, {
message: message || api.l10n.unknownError,
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 );
fail( code, response.message );
* Builds the front preview URL with the current state of customizer.
* @return {string} Preview URL.
getFrontendPreviewUrl: function() {
var previewer = this, params, urlParser;
urlParser = document.createElement( 'a' );
urlParser.href = previewer.previewUrl.get();
params = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
if ( api.state( 'changesetStatus' ).get() && 'publish' !== api.state( 'changesetStatus' ).get() ) {
params.customize_changeset_uuid = api.settings.changeset.uuid;
if ( ! api.state( 'activated' ).get() ) {
params.customize_theme = api.settings.theme.stylesheet;
urlParser.search = $.param( params );
// Ensure preview nonce is included with every customized request, to allow post data to be read.
$.ajaxPrefilter( function injectPreviewNonce( options ) {
if ( ! /wp_customize=on/.test( options.data ) ) {
options.data += '&' + $.param({
customize_preview_nonce: api.settings.nonce.preview
// Refresh the nonces if the preview sends updated nonces over.
api.previewer.bind( 'nonce', function( nonce ) {
$.extend( this.nonce, nonce );
// Refresh the nonces if login sends updated nonces over.
api.bind( 'nonce-refresh', function( nonce ) {
$.extend( api.settings.nonce, nonce );
$.extend( api.previewer.nonce, nonce );
api.previewer.send( 'nonce-refresh', nonce );
$.each( api.settings.settings, function( id, data ) {
var Constructor = api.settingConstructor[ data.type ] || api.Setting;
api.add( new Constructor( id, data.value, {
transport: data.transport,
previewer: api.previewer,
$.each( api.settings.panels, function ( id, data ) {
var Constructor = api.panelConstructor[ data.type ] || api.Panel, options;
// Inclusion of params alias is for back-compat for custom panels that expect to augment this property.
options = _.extend( { params: data }, data );
api.panel.add( new Constructor( id, options ) );
$.each( api.settings.sections, function ( id, data ) {
var Constructor = api.sectionConstructor[ data.type ] || api.Section, options;
// Inclusion of params alias is for back-compat for custom sections that expect to augment this property.
options = _.extend( { params: data }, data );
api.section.add( new Constructor( id, options ) );
$.each( api.settings.controls, function( id, data ) {
var Constructor = api.controlConstructor[ data.type ] || api.Control, options;
// Inclusion of params alias is for back-compat for custom controls that expect to augment this property.
options = _.extend( { params: data }, data );
api.control.add( new Constructor( id, options ) );
// Focus the autofocused element.
_.each( [ 'panel', 'section', 'control' ], function( type ) {
var id = api.settings.autofocus[ type ];
* 1. The panel, section, or control exists (especially for dynamically-created ones).
* 2. The instance is embedded in the document (and so is focusable).
* 3. The preview has finished loading so that the active states have been set.
api[ type ]( id, function( instance ) {
instance.deferred.embedded.done( function() {
api.previewer.deferred.active.done( function() {
api.bind( 'ready', api.reflowPaneContents );
$( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents );
values.bind( 'add', debouncedReflowPaneContents );
values.bind( 'change', debouncedReflowPaneContents );
values.bind( 'remove', debouncedReflowPaneContents );
// Set up global notifications area.
api.bind( 'ready', function setUpGlobalNotificationsArea() {
var sidebar, containerHeight, containerInitialTop;
api.notifications.container = $( '#customize-notifications-area' );
api.notifications.bind( 'change', _.debounce( function() {
api.notifications.render();
sidebar = $( '.wp-full-overlay-sidebar-content' );
api.notifications.bind( 'rendered', function updateSidebarTop() {
sidebar.css( 'top', '' );
if ( 0 !== api.notifications.count() ) {
containerHeight = api.notifications.container.outerHeight() + 1;
containerInitialTop = parseInt( sidebar.css( 'top' ), 10 );
sidebar.css( 'top', containerInitialTop + containerHeight + 'px' );
api.notifications.trigger( 'sidebarTopUpdated' );
api.notifications.render();
// Save and activated states.
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' ),
expandedPanel = state.instance( 'expandedPanel' ),
expandedSection = state.instance( 'expandedSection' ),
changesetStatus = state.instance( 'changesetStatus' ),
selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ),
changesetDate = state.instance( 'changesetDate' ),
selectedChangesetDate = state.instance( 'selectedChangesetDate' ),
previewerAlive = state.instance( 'previewerAlive' ),
editShortcutVisibility = state.instance( 'editShortcutVisibility' ),
changesetLocked = state.instance( 'changesetLocked' ),
populateChangesetUuidParam, defaultSelectedChangesetStatus;
state.bind( 'change', function() {
saveBtn.val( api.l10n.activate );
closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
} else if ( '' === changesetStatus.get() && saved() ) {
if ( api.settings.changeset.currentUserCanPublish ) {
saveBtn.val( api.l10n.published );
saveBtn.val( api.l10n.saved );
closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
if ( 'draft' === selectedChangesetStatus() ) {
if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
saveBtn.val( api.l10n.draftSaved );
saveBtn.val( api.l10n.saveDraft );
} else if ( 'future' === selectedChangesetStatus() ) {
if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
if ( changesetDate.get() !== selectedChangesetDate.get() ) {
saveBtn.val( api.l10n.schedule );
saveBtn.val( api.l10n.scheduled );
saveBtn.val( api.l10n.schedule );
} else if ( api.settings.changeset.currentUserCanPublish ) {
saveBtn.val( api.l10n.publish );
closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
* 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() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
saveBtn.prop( 'disabled', ! canSave );