: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
// Collapse any sibling sections/panels.
api.section.each( function ( otherSection ) {
if ( otherSection !== section ) {
// Try to sync the current search term to the new section.
if ( 'themes' === otherSection.params.type ) {
searchTerm = otherSection.contentContainer.find( '.wp-filter-search' ).val();
section.contentContainer.find( '.wp-filter-search' ).val( searchTerm );
// Directly initialize an empty remote search to avoid a race condition.
if ( '' === searchTerm && '' !== section.term && 'local' !== section.params.filter_type ) {
section.initializeNewQuery( section.term, section.tags );
if ( 'remote' === section.params.filter_type ) {
section.checkTerm( section );
} else if ( 'local' === section.params.filter_type ) {
section.filterSearch( searchTerm );
otherSection.collapse( { duration: args.duration } );
section.contentContainer.addClass( 'current-section' );
container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
if ( args.completeCallback ) {
section.updateCount(); // Show this section's count.
if ( section.panel() && api.panel.has( section.panel() ) ) {
api.panel( section.panel() ).expand({
section.contentContainer.removeClass( 'current-section' );
// Always hide, even if they don't exist or are already hidden.
section.headerContainer.find( '.filter-details' ).slideUp( 180 );
container.off( 'scroll' );
if ( args.completeCallback ) {
* Return the section's content element without detaching from the parent.
return this.container.find( '.control-section-content' );
* Load theme data via Ajax and add themes to the section as controls.
var section = this, params, page, request;
return; // We're already loading a batch of themes.
// Parameters for every API query. Additional params are set in PHP.
page = Math.ceil( section.loaded / 100 ) + 1;
'nonce': api.settings.nonce.switch_themes,
'theme_action': section.params.action,
'customized_theme': api.settings.theme.stylesheet,
// Add fields for remote filtering.
if ( 'remote' === section.params.filter_type ) {
params.search = section.term;
params.tags = section.tags;
section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' );
section.container.find( '.no-themes' ).hide();
request = wp.ajax.post( 'customize_load_themes', params );
request.done(function( data ) {
var themes = data.themes;
// Stop and try again if the term changed while loading.
if ( '' !== section.nextTerm || '' !== section.nextTags ) {
if ( section.nextTerm ) {
section.term = section.nextTerm;
if ( section.nextTags ) {
section.tags = section.nextTags;
if ( 0 !== themes.length ) {
section.loadControls( themes, page );
// Pre-load the first 3 theme screenshots.
_.each( section.controls().slice( 0, 3 ), function( control ) {
var img, src = control.params.theme.screenshot[0];
if ( 'local' !== section.params.filter_type ) {
wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
_.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
if ( 'local' === section.params.filter_type || 100 > themes.length ) {
// If we have less than the requested 100 themes, it's the end of the list.
section.fullyLoaded = true;
if ( 0 === section.loaded ) {
section.container.find( '.no-themes' ).show();
wp.a11y.speak( section.container.find( '.no-themes' ).text() );
section.fullyLoaded = true;
if ( 'local' === section.params.filter_type ) {
section.updateCount(); // Count of visible theme controls.
section.updateCount( data.info.results ); // Total number of results including pages not yet loaded.
section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
// This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
request.fail(function( data ) {
if ( 'undefined' === typeof data ) {
section.container.find( '.unexpected-error' ).show();
wp.a11y.speak( section.container.find( '.unexpected-error' ).text() );
} else if ( 'undefined' !== typeof console && console.error ) {
// This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
* Loads controls into the section from data received from loadThemes().
* @param {Array} themes - Array of theme data to create controls with.
* @param {number} page - Page of results being loaded.
loadControls: function( themes, page ) {
var newThemeControls = [],
// Add controls for each theme.
_.each( themes, function( theme ) {
var themeControl = new api.controlConstructor.theme( section.params.action + '_theme_' + theme.id, {
section: section.params.id,
priority: section.loaded + 1
api.control.add( themeControl );
newThemeControls.push( themeControl );
section.loaded = section.loaded + 1;
Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
* Determines whether more themes should be loaded, and loads them.
var section = this, container, bottom, threshold;
if ( ! section.fullyLoaded && ! section.loading ) {
container = section.container.closest( '.customize-themes-full-container' );
bottom = container.scrollTop() + container.height();
// Use a fixed distance to the bottom of loaded results to avoid unnecessarily
// loading results sooner when using a percentage of scroll distance.
threshold = container.prop( 'scrollHeight' ) - 3000;
if ( bottom > threshold ) {
* Event handler for search input that filters visible controls.
* @param {string} term - The raw search input value.
filterSearch: function( term ) {
noFilter = ( api.section.has( 'wporg_themes' ) && 'remote' !== section.params.filter_type ) ? '.no-themes-local' : '.no-themes',
controls = section.controls(),
// Standardize search term format and split into an array of individual words.
terms = term.toLowerCase().trim().replace( /-/g, ' ' ).split( ' ' );
_.each( controls, function( control ) {
visible = control.filter( terms ); // Shows/hides and sorts control based on the applicability of the search term.
section.container.find( noFilter ).show();
wp.a11y.speak( section.container.find( noFilter ).text() );
section.container.find( noFilter ).hide();
section.renderScreenshots();
api.reflowPaneContents();
section.updateCountDebounced( count );
* Event handler for search input that determines if the terms have changed and loads new controls as needed.
* @param {wp.customize.ThemesSection} section - The current theme section, passed through the debouncer.
checkTerm: function( section ) {
if ( 'remote' === section.params.filter_type ) {
newTerm = section.contentContainer.find( '.wp-filter-search' ).val();
if ( section.term !== newTerm.trim() ) {
section.initializeNewQuery( newTerm, section.tags );
* Check for filters checked in the feature filter list and initialize a new query.
filtersChecked: function() {
items = section.container.find( '.filter-group' ).find( ':checkbox' ),
_.each( items.filter( ':checked' ), function( item ) {
tags.push( $( item ).prop( 'value' ) );
// When no filters are checked, restore initial state. Update filter count.
if ( 0 === tags.length ) {
section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).show();
section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).hide();
section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length );
section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).hide();
section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).show();
// Check whether tags have changed, and either load or queue them.
if ( ! _.isEqual( section.tags, tags ) ) {
if ( 'remote' === section.params.filter_type ) {
section.initializeNewQuery( section.term, tags );
} else if ( 'local' === section.params.filter_type ) {
section.filterSearch( tags.join( ' ' ) );
* Reset the current query and load new results.
* @param {string} newTerm - New term.
* @param {Array} newTags - New tags.
initializeNewQuery: function( newTerm, newTags ) {
// Clear the controls in the section.
_.each( section.controls(), function( control ) {
control.container.remove();
api.control.remove( control.id );
section.fullyLoaded = false;
section.screenshotQueue = null;
// Run a new query, with loadThemes handling paging, etc.
if ( ! section.loading ) {
section.nextTerm = newTerm; // This will reload from loadThemes() with the newest term once the current batch is loaded.
section.nextTags = newTags; // This will reload from loadThemes() with the newest tags once the current batch is loaded.
if ( ! section.expanded() ) {
section.expand(); // Expand the section if it isn't expanded.
* Render control's screenshot if the control comes into view.
renderScreenshots: function() {
// Fill queue initially, or check for more if empty.
if ( null === section.screenshotQueue || 0 === section.screenshotQueue.length ) {
// Add controls that haven't had their screenshots rendered.
section.screenshotQueue = _.filter( section.controls(), function( control ) {
return ! control.screenshotRendered;
// Are all screenshots rendered (for now)?
if ( ! section.screenshotQueue.length ) {
section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
var $imageWrapper = control.container.find( '.theme-screenshot' ),
$image = $imageWrapper.find( 'img' );
if ( $image.is( ':hidden' ) ) {
var wt = section.$window.scrollTop(),
wb = wt + section.$window.height(),
et = $image.offset().top,
ih = $imageWrapper.height(),
inView = eb >= wt - threshold && et <= wb + threshold;
control.container.trigger( 'render-screenshot' );
// If the image is in view return false so it's cleared from the queue.
* @return {number} Visible count.
getVisibleCount: function() {
return this.contentContainer.find( 'li.customize-control:visible' ).length;
* Update the number of themes in the section.
updateCount: function( count ) {
var section = this, countEl, displayed;
if ( ! count && 0 !== count ) {
count = section.getVisibleCount();
displayed = section.contentContainer.find( '.themes-displayed' );
countEl = section.contentContainer.find( '.theme-count' );
// Animate the count change for emphasis.
displayed.fadeOut( 180, function() {
wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) );
* Advance the modal to the next theme.
if ( section.getNextTheme() ) {
section.showDetails( section.getNextTheme(), function() {
section.overlay.find( '.right' ).focus();
* Get the next theme model.
* @return {wp.customize.ThemeControl|boolean} Next theme.
getNextTheme: function () {
var section = this, control, nextControl, sectionControls, i;
control = api.control( section.params.action + '_theme_' + section.currentTheme );
sectionControls = section.controls();
i = _.indexOf( sectionControls, control );