: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
nextControl = sectionControls[ i + 1 ];
return nextControl.params.theme;
* Advance the modal to the previous theme.
previousTheme: function () {
if ( section.getPreviousTheme() ) {
section.showDetails( section.getPreviousTheme(), function() {
section.overlay.find( '.left' ).focus();
* Get the previous theme model.
* @return {wp.customize.ThemeControl|boolean} Previous theme.
getPreviousTheme: function () {
var section = this, control, nextControl, sectionControls, i;
control = api.control( section.params.action + '_theme_' + section.currentTheme );
sectionControls = section.controls();
i = _.indexOf( sectionControls, control );
nextControl = sectionControls[ i - 1 ];
return nextControl.params.theme;
* Disable buttons when we're viewing the first or last theme.
updateLimits: function () {
if ( ! this.getNextTheme() ) {
this.overlay.find( '.right' ).addClass( 'disabled' );
if ( ! this.getPreviousTheme() ) {
this.overlay.find( '.left' ).addClass( 'disabled' );
* @param {string} themeId Theme ID.
* @return {jQuery.promise} Promise.
loadThemePreview: function( themeId ) {
return api.ThemesPanel.prototype.loadThemePreview.call( this, themeId );
* Render & show the theme details for a given theme model.
* @param {Object} theme - Theme.
* @param {Function} [callback] - Callback once the details have been shown.
showDetails: function ( theme, callback ) {
var section = this, panel = api.panel( 'themes' );
section.currentTheme = theme.id;
section.overlay.html( section.template( theme ) )
function disableSwitchButtons() {
return ! panel.canSwitchTheme( theme.id );
// Temporary special function since supplying SFTP credentials does not work yet. See #42184.
function disableInstallButtons() {
return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
section.overlay.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
section.overlay.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
section.$body.addClass( 'modal-open' );
section.containFocus( section.overlay );
wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );
* Close the theme details modal.
closeDetails: function () {
section.$body.removeClass( 'modal-open' );
section.overlay.fadeOut( 'fast' );
api.control( section.params.action + '_theme_' + section.currentTheme ).container.find( '.theme' ).focus();
* Keep tab focus within the theme details modal.
* @param {jQuery} el - Element to contain focus.
containFocus: function( el ) {
el.on( 'keydown', function( event ) {
// Return if it's not the tab key
// When navigating with prev/next focus is already handled.
if ( 9 !== event.keyCode ) {
// Uses jQuery UI to get the tabbable elements.
tabbables = $( ':tabbable', el );
// Keep focus within the overlay.
if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
tabbables.first().focus();
} else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
tabbables.last().focus();
api.OuterSection = api.Section.extend(/** @lends wp.customize.OuterSection.prototype */{
* Class wp.customize.OuterSection.
* Creates section outside of the sidebar, there is no ui to trigger collapse/expand so
* it would require custom handling.
* @constructs wp.customize.OuterSection
* @augments wp.customize.Section
section.containerParent = '#customize-outer-theme-controls';
section.containerPaneParent = '.customize-outer-pane-parent';
api.Section.prototype.initialize.apply( section, arguments );
* Overrides api.Section.prototype.onChangeExpanded to prevent collapse/expand effect
* on other sections and panels.
* @param {boolean} expanded - The expanded state to transition to.
* @param {Object} [args] - Args.
* @param {boolean} [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
* @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
* @param {Object} [args.duration] - The duration for the animation.
onChangeExpanded: function( expanded, args ) {
container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
content = section.contentContainer,
backBtn = content.find( '.customize-section-back' ),
sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
body = $( document.body ),
body.toggleClass( 'outer-section-open', expanded );
section.container.toggleClass( 'open', expanded );
section.container.removeClass( 'busy' );
api.section.each( function( _section ) {
if ( 'outer' === _section.params.type && _section.id !== section.id ) {
_section.container.removeClass( 'open' );
if ( expanded && ! content.hasClass( 'open' ) ) {
expand = args.completeCallback;
section._animateChangeExpanded( function() {
sectionTitle.attr( 'tabindex', '-1' );
backBtn.attr( 'tabindex', '0' );
backBtn.trigger( 'focus' );
content.css( 'top', '' );
container.scrollTop( 0 );
if ( args.completeCallback ) {
content.addClass( 'open' );
api.panel( section.panel() ).expand({
} else if ( ! expanded && content.hasClass( 'open' ) ) {
panel = api.panel( section.panel() );
if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
section._animateChangeExpanded( function() {
backBtn.attr( 'tabindex', '-1' );
sectionTitle.attr( 'tabindex', '0' );
sectionTitle.trigger( 'focus' );
content.css( 'top', '' );
if ( args.completeCallback ) {
content.removeClass( 'open' );
if ( args.completeCallback ) {
api.Panel = Container.extend(/** @lends wp.customize.Panel.prototype */{
* @constructs wp.customize.Panel
* @augments wp.customize~Container
* @param {string} id - The ID for the panel.
* @param {Object} options - Object containing one property: params.
* @param {string} options.title - Title shown when panel is collapsed and expanded.
* @param {string} [options.description] - Description shown at the top of the panel.
* @param {number} [options.priority=100] - The sort priority for the panel.
* @param {string} [options.type=default] - The type of the panel. See wp.customize.panelConstructor.
* @param {string} [options.content] - The markup to be used for the panel container. If empty, a JS template is used.
* @param {boolean} [options.active=true] - Whether the panel is active or not.
* @param {Object} [options.params] - Deprecated wrapper for the above properties.
initialize: function ( id, options ) {
var panel = this, params;
params = options.params || options;
// Look up the type if one was not supplied.
_.find( api.panelConstructor, function( Constructor, type ) {
if ( Constructor === panel.constructor ) {
Container.prototype.initialize.call( panel, id, params );
panel.deferred.embedded.done( function () {
* Embed the container in the DOM when any parent panel is ready.
container = $( '#customize-theme-controls' ),
parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable.
if ( ! panel.headContainer.parent().is( parentContainer ) ) {
parentContainer.append( panel.headContainer );
if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
container.append( panel.contentContainer );
panel.deferred.embedded.resolve();
attachEvents: function () {
// Expand/Collapse accordion sections on click.
panel.headContainer.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
event.preventDefault(); // Keep this AFTER the key filter above.
if ( ! panel.expanded() ) {
panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
event.preventDefault(); // Keep this AFTER the key filter above.
if ( panel.expanded() ) {
meta = panel.container.find( '.panel-meta:first' );
meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
if ( meta.hasClass( 'cannot-expand' ) ) {
var content = meta.find( '.customize-panel-description:first' );
if ( meta.hasClass( 'open' ) ) {
meta.toggleClass( 'open' );
content.slideUp( panel.defaultExpandedArguments.duration, function() {
content.trigger( 'toggled' );
$( this ).attr( 'aria-expanded', false );
content.slideDown( panel.defaultExpandedArguments.duration, function() {
content.trigger( 'toggled' );
meta.toggleClass( 'open' );
$( this ).attr( 'aria-expanded', true );
* Get the sections that are associated with this panel, sorted by their priority Value.
return this._children( 'panel', 'section' );
* Return whether this panel has any active sections.
* @return {boolean} Whether contextually active.
isContextuallyActive: function () {
sections = panel.sections(),
_( sections ).each( function ( section ) {
if ( section.active() && section.isContextuallyActive() ) {
return ( activeCount !== 0 );
* Update UI to reflect expanded state.
* @param {boolean} expanded
* @param {boolean} args.unchanged
* @param {Function} args.completeCallback
onChangeExpanded: function ( expanded, args ) {
// Immediately call the complete callback if there were no changes.
if ( args.completeCallback ) {
// Note: there is a second argument 'args' passed.
accordionSection = panel.contentContainer,
overlay = accordionSection.closest( '.wp-full-overlay' ),
container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
topPanel = panel.headContainer.find( '.accordion-section-title' ),
backBtn = accordionSection.find( '.customize-panel-back' ),
childSections = panel.sections(),
if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) {
// Collapse any sibling sections/panels.
api.section.each( function ( section ) {
if ( panel.id !== section.panel() ) {
section.collapse( { duration: 0 } );
api.panel.each( function ( otherPanel ) {
if ( panel !== otherPanel ) {
otherPanel.collapse( { duration: 0 } );
if ( panel.params.autoExpandSoleSection && 1 === childSections.length && childSections[0].active.get() ) {
accordionSection.addClass( 'current-panel skip-transition' );
overlay.addClass( 'in-sub-panel' );
childSections[0].expand( {
completeCallback: args.completeCallback
panel._animateChangeExpanded( function() {
topPanel.attr( 'tabindex', '-1' );
backBtn.attr( 'tabindex', '0' );
backBtn.trigger( 'focus' );
accordionSection.css( 'top', '' );
container.scrollTop( 0 );
if ( args.completeCallback ) {
accordionSection.addClass( 'current-panel' );
overlay.addClass( 'in-sub-panel' );
api.state( 'expandedPanel' ).set( panel );
} else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) {
skipTransition = accordionSection.hasClass( 'skip-transition' );
if ( ! skipTransition ) {
panel._animateChangeExpanded( function() {
topPanel.attr( 'tabindex', '0' );
backBtn.attr( 'tabindex', '-1' );