: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
root: themes.data.settings.adminUrl,
// Bind to our global thx object
// so that the object is available to sub-views.
themes.router = new themes.Router();
// Handles theme details route event.
themes.router.on( 'route:theme', function( slug ) {
self.view.view.expand( slug );
themes.router.on( 'route:themes', function() {
self.themes.doSearch( '' );
self.view.trigger( 'theme:close' );
// Handles search route event.
themes.router.on( 'route:search', function() {
$( '.wp-filter-search' ).trigger( 'keyup' );
extraRoutes: function() {
// Extend the main Search view.
themes.view.InstallerSearch = themes.view.Search.extend({
// Handles Ajax request for searching through themes in public repo.
search: function( event ) {
// Tabbing or reverse tabbing into the search input shouldn't trigger a search.
if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
this.collection = this.options.parent.view.collection;
if ( event.type === 'keyup' && event.which === 27 ) {
this.doSearch( event.target.value );
doSearch: function( value ) {
// Don't do anything if the search terms haven't changed.
if ( this.terms === value ) {
// Updates terms with the value passed.
* Intercept an [author] search.
* If input value starts with `author:` send a request
* for `author` instead of a regular `search`.
if ( value.substring( 0, 7 ) === 'author:' ) {
request.author = value.slice( 7 );
* Intercept a [tag] search.
* If input value starts with `tag:` send a request
* for `tag` instead of a regular `search`.
if ( value.substring( 0, 4 ) === 'tag:' ) {
request.tag = [ value.slice( 4 ) ];
$( '.filter-links li > a.current' )
.removeClass( 'current' )
.removeAttr( 'aria-current' );
$( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
// Get the themes by sending Ajax POST request to api.wordpress.org/themes
// or searching the local cache.
this.collection.query( request );
themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
themes.view.Installer = themes.view.Appearance.extend({
el: '#wpbody-content .wrap',
// Register events for sorting and filters in theme-navigation.
'click .filter-links li > a': 'onSort',
'click .theme-filter': 'onFilter',
'click .drawer-toggle': 'moreFilters',
'click .filter-drawer .apply-filters': 'applyFilters',
'click .filter-group [type="checkbox"]': 'addFilter',
'click .filter-drawer .clear-filters': 'clearFilters',
'click .edit-filters': 'backToFilters',
'click .favorites-form-submit' : 'saveUsername',
'keyup #wporg-username-input': 'saveUsername'
// Initial render method.
this.collection = new themes.Collection();
// Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
this.listenTo( this, 'theme:end', function() {
// Make sure we are not already loading.
if ( self.collection.loadingThemes ) {
// Set loadingThemes to true and bump page instance of currentQuery.
self.collection.loadingThemes = true;
self.collection.currentQuery.page++;
// Use currentQuery.page to build the themes request.
_.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
self.collection.query( self.collection.currentQuery.request );
this.listenTo( this.collection, 'query:success', function() {
$( 'body' ).removeClass( 'loading-content' );
$( '.theme-browser' ).find( 'div.error' ).remove();
this.listenTo( this.collection, 'query:fail', function() {
$( 'body' ).removeClass( 'loading-content' );
$( '.theme-browser' ).find( 'div.error' ).remove();
$( '.theme-browser' ).find( 'div.themes' ).before( '<div class="error"><p>' + l10n.error + '</p><p><button class="button try-again">' + l10n.tryAgain + '</button></p></div>' );
$( '.theme-browser .error .try-again' ).on( 'click', function( e ) {
$( 'input.wp-filter-search' ).trigger( 'input' );
// Sets up the view and passes the section argument.
this.view = new themes.view.Themes({
collection: this.collection,
// Reset pagination every time the install view handler is run.
this.$el.find( '.themes' ).remove();
this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
// Handles all the rendering of the public theme directory.
browse: function( section ) {
// Create a new collection with the proper theme data
if ( 'block-themes' === section ) {
// Get the themes by sending Ajax POST request to api.wordpress.org/themes
// or searching the local cache.
this.collection.query( { tag: 'full-site-editing' } );
this.collection.query( { browse: section } );
onSort: function( event ) {
var $el = $( event.target ),
sort = $el.data( 'sort' );
$( 'body' ).removeClass( 'filters-applied show-filters' );
$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
// Bail if this is already active.
if ( $el.hasClass( this.activeClass ) ) {
// Trigger a router.navigate update.
themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
// Track sorting so we can restore the correct tab when closing preview.
themes.router.selectedTab = sort;
$( '.filter-links li > a, .theme-filter' )
.removeClass( this.activeClass )
.removeAttr( 'aria-current' );
$( '[data-sort="' + sort + '"]' )
.addClass( this.activeClass )
.attr( 'aria-current', 'page' );
if ( 'favorites' === sort ) {
$( 'body' ).addClass( 'show-favorites-form' );
$( 'body' ).removeClass( 'show-favorites-form' );
onFilter: function( event ) {
filter = $el.data( 'filter' );
// Bail if this is already active.
if ( $el.hasClass( this.activeClass ) ) {
$( '.filter-links li > a, .theme-section' )
.removeClass( this.activeClass )
.removeAttr( 'aria-current' );
.addClass( this.activeClass )
.attr( 'aria-current', 'page' );
// Construct the filter request
// using the default values.
filter = _.union( [ filter, this.filtersChecked() ] );
request = { tag: [ filter ] };
// Get the themes by sending Ajax POST request to api.wordpress.org/themes
// or searching the local cache.
this.collection.query( request );
// Clicking on a checkbox to add another filter to the request.
// Applying filters triggers a tag request.
applyFilters: function( event ) {
tags = this.filtersChecked(),
filteringBy = $( '.filtered-by .tags' );
wp.a11y.speak( l10n.selectFeatureFilter );
$( 'body' ).addClass( 'filters-applied' );
$( '.filter-links li > a.current' )
.removeClass( 'current' )
.removeAttr( 'aria-current' );
_.each( tags, function( tag ) {
name = $( 'label[for="filter-id-' + tag + '"]' ).text();
filteringBy.append( '<span class="tag">' + name + '</span>' );
// Get the themes by sending Ajax POST request to api.wordpress.org/themes
// or searching the local cache.
this.collection.query( request );
// Save the user's WordPress.org username and get his favorite themes.
saveUsername: function ( event ) {
var username = $( '#wporg-username-input' ).val(),
nonce = $( '#wporg-username-nonce' ).val(),
request = { browse: 'favorites', user: username },
// Save username on enter.
if ( event.type === 'keyup' && event.which !== 13 ) {
return wp.ajax.send( 'save-wporg-username', {
// Get the themes by sending Ajax POST request to api.wordpress.org/themes
// or searching the local cache.
that.collection.query( request );
* Get the checked filters.
* @return {Array} of tags or false
filtersChecked: function() {
var items = $( '.filter-group' ).find( ':checkbox' ),
_.each( items.filter( ':checked' ), function( item ) {
tags.push( $( item ).prop( 'value' ) );
// When no filters are checked, restore initial state and return.
if ( tags.length === 0 ) {
$( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
$( '.filter-drawer .clear-filters' ).hide();
$( 'body' ).removeClass( 'filters-applied' );
$( '.filter-drawer .apply-filters' ).find( 'span' ).text( tags.length );
$( '.filter-drawer .clear-filters' ).css( 'display', 'inline-block' );
* When users press the "Upload Theme" button, show the upload form in place.
var uploadViewToggle = $( '.upload-view-toggle' ),
$body = $( document.body );
uploadViewToggle.on( 'click', function() {
// Toggle the upload view.
$body.toggleClass( 'show-upload-view' );
// Toggle the `aria-expanded` button attribute.
uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
// Toggle the full filters navigation.
moreFilters: function( event ) {
$toggleButton = $( '.drawer-toggle' );
if ( $body.hasClass( 'filters-applied' ) ) {
return this.backToFilters();
themes.router.navigate( themes.router.baseUrl( '' ) );
// Toggle the feature filters view.
$body.toggleClass( 'show-filters' );
// Toggle the `aria-expanded` button attribute.
$toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
* Clears all the checked filters.
clearFilters: function( event ) {
var items = $( '.filter-group' ).find( ':checkbox' ),
_.each( items.filter( ':checked' ), function( item ) {
$( item ).prop( 'checked', false );
return self.filtersChecked();
backToFilters: function( event ) {
$( 'body' ).removeClass( 'filters-applied' );
clearSearch: function() {
$( '#wp-filter-search-input').val( '' );
themes.InstallerRouter = Backbone.Router.extend({
'theme-install.php?theme=:slug': 'preview',
'theme-install.php?browse=:sort': 'sort',
'theme-install.php?search=:query': 'search',
'theme-install.php': 'sort'
baseUrl: function( url ) {
return 'theme-install.php' + url;
search: function( query ) {
$( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
// Passes the default 'section' as an option.
this.view = new themes.view.Installer({
SearchView: themes.view.InstallerSearch
// Start debouncing user searches after Backbone.history.start().
this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
if ( Backbone.History.started ) {
root: themes.data.settings.adminUrl,