: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else.
if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) {
// Abort if we're inside of a block editor instance.
if ( event.target.closest( '.block-editor-writing-flow' ) !== null ||
event.target.closest( '.block-editor-block-list__block-popover' ) !== null
// Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
api.control.each( function( control ) {
if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
expandedControls.push( control );
api.section.each( function( section ) {
if ( section.expanded() ) {
expandedSections.push( section );
api.panel.each( function( panel ) {
if ( panel.expanded() ) {
expandedPanels.push( panel );
// Skip collapsing expanded controls if there are no expanded sections.
if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
expandedControls.length = 0;
// Collapse the most granular expanded object.
collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
if ( 'themes' === collapsedObject.params.type ) {
// Themes panel or section.
if ( body.hasClass( 'modal-open' ) ) {
collapsedObject.closeDetails();
} else if ( api.panel.has( 'themes' ) ) {
// If we're collapsing a section, collapse the panel also.
api.panel( 'themes' ).collapse();
collapsedObject.collapse();
$( '.customize-controls-preview-toggle' ).on( 'click', function() {
api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
(function initStickyHeaders() {
var parentContainer = $( '.wp-full-overlay-sidebar-content' ),
changeContainer, updateHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader,
activeHeader, lastScrollTop;
* Determine which panel or section is currently expanded.
* @param {wp.customize.Panel|wp.customize.Section} container Construct.
changeContainer = function( container ) {
var newInstance = container,
expandedSection = api.state( 'expandedSection' ).get(),
expandedPanel = api.state( 'expandedPanel' ).get(),
if ( activeHeader && activeHeader.element ) {
// Release previously active header element.
releaseStickyHeader( activeHeader.element );
// Remove event listener in the previous panel or section.
activeHeader.element.find( '.description' ).off( 'toggled', updateHeaderHeight );
if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) {
newInstance = expandedPanel;
} else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) {
newInstance = expandedSection;
headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first();
if ( headerElement.length ) {
parent: headerElement.closest( '.customize-pane-child' ),
height: headerElement.outerHeight()
// Update header height whenever help text is expanded or collapsed.
activeHeader.element.find( '.description' ).on( 'toggled', updateHeaderHeight );
resetStickyHeader( activeHeader.element, activeHeader.parent );
api.state( 'expandedSection' ).bind( changeContainer );
api.state( 'expandedPanel' ).bind( changeContainer );
// Throttled scroll event handler.
parentContainer.on( 'scroll', _.throttle( function() {
var scrollTop = parentContainer.scrollTop(),
if ( scrollTop === lastScrollTop ) {
} else if ( scrollTop > lastScrollTop ) {
lastScrollTop = scrollTop;
if ( 0 !== scrollDirection ) {
positionStickyHeader( activeHeader, scrollTop, scrollDirection );
// Update header position on sidebar layout change.
api.notifications.bind( 'sidebarTopUpdated', function() {
if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) {
activeHeader.element.css( 'top', parentContainer.css( 'top' ) );
// Release header element if it is sticky.
releaseStickyHeader = function( headerElement ) {
if ( ! headerElement.hasClass( 'is-sticky' ) ) {
.removeClass( 'is-sticky' )
.addClass( 'maybe-sticky is-in-view' )
.css( 'top', parentContainer.scrollTop() + 'px' );
// Reset position of the sticky header.
resetStickyHeader = function( headerElement, headerParent ) {
if ( headerElement.hasClass( 'is-in-view' ) ) {
.removeClass( 'maybe-sticky is-in-view' )
headerParent.css( 'padding-top', '' );
* Update active header height.
updateHeaderHeight = function() {
activeHeader.height = activeHeader.element.outerHeight();
* Reposition header on throttled `scroll` event.
* @param {Object} header - Header.
* @param {number} scrollTop - Scroll top.
* @param {number} scrollDirection - Scroll direction, negative number being up and positive being down.
positionStickyHeader = function( header, scrollTop, scrollDirection ) {
var headerElement = header.element,
headerParent = header.parent,
headerHeight = header.height,
headerTop = parseInt( headerElement.css( 'top' ), 10 ),
maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
isSticky = headerElement.hasClass( 'is-sticky' ),
isInView = headerElement.hasClass( 'is-in-view' ),
isScrollingUp = ( -1 === scrollDirection );
// When scrolling down, gradually hide sticky header.
.removeClass( 'is-sticky' )
if ( isInView && scrollTop > headerTop + headerHeight ) {
headerElement.removeClass( 'is-in-view' );
headerParent.css( 'padding-top', '' );
if ( ! maybeSticky && scrollTop >= headerHeight ) {
headerElement.addClass( 'maybe-sticky' );
} else if ( 0 === scrollTop ) {
// Reset header in base position.
.removeClass( 'maybe-sticky is-in-view is-sticky' )
headerParent.css( 'padding-top', '' );
if ( isInView && ! isSticky ) {
// Header is in the view but is not yet sticky.
if ( headerTop >= scrollTop ) {
// Header is fully visible.
top: parentContainer.css( 'top' ),
width: headerParent.outerWidth() + 'px'
} else if ( maybeSticky && ! isInView ) {
// Header is out of the view.
.addClass( 'is-in-view' )
.css( 'top', ( scrollTop - headerHeight ) + 'px' );
headerParent.css( 'padding-top', headerHeight + 'px' );
// Previewed device bindings. (The api.previewedDevice property
// is how this Value was first introduced, but since it has moved to api.state.)
api.previewedDevice = api.state( 'previewedDevice' );
// Set the default device.
api.bind( 'ready', function() {
_.find( api.settings.previewableDevices, function( value, key ) {
if ( true === value['default'] ) {
api.previewedDevice.set( key );
// Set the toggled device.
footerActions.find( '.devices button' ).on( 'click', function( event ) {
api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
api.previewedDevice.bind( function( newDevice ) {
var overlay = $( '.wp-full-overlay' ),
footerActions.find( '.devices button' )
.attr( 'aria-pressed', false );
footerActions.find( '.devices .preview-' + newDevice )
.attr( 'aria-pressed', true );
$.each( api.settings.previewableDevices, function( device ) {
devices += ' preview-' + device;
.addClass( 'preview-' + newDevice );
// Bind site title display to the corresponding field.
api( 'blogname', function( setting ) {
var updateTitle = function() {
var blogTitle = setting() || '';
title.text( blogTitle.toString().trim() || api.l10n.untitledBlogName );
setting.bind( updateTitle );
* Create a postMessage connection with a parent frame,
* in case the Customizer frame was opened with the Customize loader.
* @see wp.customize.Loader
parent = new api.Messenger({
url: api.settings.url.parent,
// Handle exiting of Customizer.
var isInsideIframe = false;
function isCleanState() {
var defaultChangesetStatus;
* Handle special case of previewing theme switch since some settings (for nav menus and widgets)
* are pre-dirty and non-active themes can only ever be auto-drafts.
if ( ! api.state( 'activated' ).get() ) {
return 0 === api._latestRevision;
// Dirty if the changeset status has been changed but not saved yet.
defaultChangesetStatus = api.state( 'changesetStatus' ).get();
if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
defaultChangesetStatus = 'publish';
if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
// Dirty if scheduled but the changeset date hasn't been saved yet.
if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
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() {
function startPromptingBeforeUnload() {
api.unbind( 'change', startPromptingBeforeUnload );
api.state( 'selectedChangesetStatus' ).unbind( startPromptingBeforeUnload );
api.state( 'selectedChangesetDate' ).unbind( startPromptingBeforeUnload );
// Prompt user with AYS dialog if leaving the Customizer with unsaved changes.
$( window ).on( 'beforeunload.customize-confirm', function() {
if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) {
overlay.removeClass( 'customize-loading' );
return api.l10n.saveAlert;
api.bind( 'change', startPromptingBeforeUnload );
api.state( 'selectedChangesetStatus' ).bind( startPromptingBeforeUnload );
api.state( 'selectedChangesetDate' ).bind( startPromptingBeforeUnload );
function requestClose() {
var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false;
} else if ( confirm( api.l10n.saveAlert ) ) {
// Mark all settings as clean to prevent another call to requestChangesetUpdate.
api.each( function( setting ) {
$( document ).off( 'visibilitychange.wp-customize-changeset-update' );
$( window ).off( 'beforeunload.wp-customize-changeset-update' );
closeBtn.css( 'cursor', 'progress' );
if ( '' !== api.state( 'changesetStatus' ).get() ) {
if ( dismissLock || dismissAutoSave ) {
wp.ajax.send( 'customize_dismiss_autosave_or_lock', {
timeout: 500, // Don't wait too long.
customize_theme: api.settings.theme.stylesheet,
customize_changeset_uuid: api.settings.changeset.uuid,
nonce: api.settings.nonce.dismiss_autosave_or_lock,
dismiss_autosave: dismissAutoSave,
dismiss_lock: dismissLock
clearedToClose.resolve();
return clearedToClose.promise();
parent.bind( 'confirm-close', function() {
requestClose().done( function() {
parent.send( 'confirmed-close', true );
parent.send( 'confirmed-close', false );
closeBtn.on( 'click.customize-controls-close', function( event ) {
parent.send( 'close' ); // See confirm-close logic above.
requestClose().done( function() {
$( window ).off( 'beforeunload.customize-confirm' );
window.location.href = closeBtn.prop( 'href' );
// Pass events through to the parent.
$.each( [ 'saved', 'change' ], function ( i, event ) {
api.bind( event, function() {
// Pass titles to the parent.
api.bind( 'title', function( newTitle ) {
parent.send( 'title', newTitle );
if ( api.settings.changeset.branching ) {
parent.send( 'changeset-uuid', api.settings.changeset.uuid );
// Initialize the connection with the parent frame.
// Control visibility for default controls.
controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ],
callback: function( to ) { return !! to; }
controls: [ 'page_on_front', 'page_for_posts' ],
callback: function( to ) { return 'page' === to; }
controls: [ 'header_textcolor' ],
callback: function( to ) { return 'blank' !== to; }
}, function( settingId, o ) {
api( settingId, function( setting ) {
$.each( o.controls, function( i, controlId ) {
api.control( controlId, function( control ) {
var visibility = function( to ) {
control.container.toggle( o.callback( to ) );
visibility( setting.get() );
setting.bind( visibility );