event = event || window.event;
// Set focus to current theme.
themes.focusedTheme = this.$el;
// Construct a new Preview view.
themes.preview = preview = new themes.view.Preview({
// Render the view and append it.
this.setNavButtonsState();
// Hide previous/next navigation if there is only one theme.
if ( this.model.collection.length === 1 ) {
preview.$el.addClass( 'no-navigation' );
preview.$el.removeClass( 'no-navigation' );
$( 'div.wrap' ).append( preview.el );
// Listen to our preview object
// for `theme:next` and `theme:previous` events.
this.listenTo( preview, 'theme:next', function() {
// Keep local track of current theme model.
// If we have ventured away from current model update the current model position.
if ( ! _.isUndefined( self.current ) ) {
self.current = self.model.collection.at( self.model.collection.indexOf( current ) + 1 );
// If we have no more themes, bail.
if ( _.isUndefined( self.current ) ) {
self.options.parent.parent.trigger( 'theme:end' );
return self.current = current;
preview.model = self.current;
this.setNavButtonsState();
$( '.next-theme' ).trigger( 'focus' );
.listenTo( preview, 'theme:previous', function() {
// Keep track of current theme model.
// Bail early if we are at the beginning of the collection.
if ( self.model.collection.indexOf( self.current ) === 0 ) {
// If we have ventured away from current model update the current model position.
if ( ! _.isUndefined( self.current ) ) {
// Get previous theme model.
self.current = self.model.collection.at( self.model.collection.indexOf( current ) - 1 );
// If we have no more themes, bail.
if ( _.isUndefined( self.current ) ) {
preview.model = self.current;
this.setNavButtonsState();
$( '.previous-theme' ).trigger( 'focus' );
this.listenTo( preview, 'preview:close', function() {
self.current = self.model;
// Handles .disabled classes for previous/next buttons in theme installer preview.
setNavButtonsState: function() {
var $themeInstaller = $( '.theme-install-overlay' ),
current = _.isUndefined( this.current ) ? this.model : this.current,
previousThemeButton = $themeInstaller.find( '.previous-theme' ),
nextThemeButton = $themeInstaller.find( '.next-theme' );
// Disable previous at the zero position.
if ( 0 === this.model.collection.indexOf( current ) ) {
.prop( 'disabled', true );
nextThemeButton.trigger( 'focus' );
// Disable next if the next model is undefined.
if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
.prop( 'disabled', true );
previousThemeButton.trigger( 'focus' );
installTheme: function( event ) {
wp.updates.maybeRequestFilesystemCredentials( event );
$( document ).on( 'wp-theme-install-success', function( event, response ) {
if ( _this.model.get( 'id' ) === response.slug ) {
_this.model.set( { 'installed': true } );
wp.updates.installTheme( {
slug: $( event.target ).data( 'slug' )
updateTheme: function( event ) {
if ( ! this.model.get( 'hasPackage' ) ) {
wp.updates.maybeRequestFilesystemCredentials( event );
$( document ).on( 'wp-theme-update-success', function( event, response ) {
_this.model.off( 'change', _this.render, _this );
if ( _this.model.get( 'id' ) === response.slug ) {
version: response.newVersion
_this.model.on( 'change', _this.render, _this );
wp.updates.updateTheme( {
slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
// Sets up a modal overlay with the expanded theme data.
themes.view.Details = wp.Backbone.View.extend({
// Wrap theme data on a div.theme element.
className: 'theme-overlay',
'click .delete-theme': 'deleteTheme',
'click .left': 'previousTheme',
'click .right': 'nextTheme',
'click #update-theme': 'updateTheme',
'click .toggle-auto-update': 'autoupdateState'
// The HTML template for the theme overlay.
html: themes.template( 'theme-single' ),
var data = this.model.toJSON();
this.$el.html( this.html( data ) );
// Renders active theme styles.
// Set up navigation events.
// Checks screenshot size.
this.screenshotCheck( this.$el );
// Contain "tabbing" inside the overlay.
this.containFocus( this.$el );
// Adds a class to the currently active theme
// and to the overlay in detailed view mode.
activeTheme: function() {
// Check the model has the active property.
this.$el.toggleClass( 'active', this.model.get( 'active' ) );
// Set initial focus and constrain tabbing within the theme browser modal.
containFocus: function( $el ) {
// Set initial focus on the primary action control.
$( '.theme-overlay' ).trigger( 'focus' );
// Constrain tabbing within the modal.
$el.on( 'keydown.wp-themes', function( event ) {
var $firstFocusable = $el.find( '.theme-header button:not(.disabled)' ).first(),
$lastFocusable = $el.find( '.theme-actions a:visible' ).last();
// Check for the Tab key.
if ( 9 === event.which ) {
if ( $firstFocusable[0] === event.target && event.shiftKey ) {
$lastFocusable.trigger( 'focus' );
} else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) {
$firstFocusable.trigger( 'focus' );
// Single theme overlay screen.
// It's shown when clicking a theme.
collapse: function( event ) {
event = event || window.event;
// Prevent collapsing detailed view when there is only one theme available.
if ( themes.data.themes.length === 1 ) {
// Detect if the click is inside the overlay and don't close it
// unless the target was the div.back button.
if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
// Add a temporary closing class while overlay fades out.
$( 'body' ).addClass( 'closing-overlay' );
// With a quick fade out animation.
this.$el.fadeOut( 130, function() {
// Clicking outside the modal box closes the overlay.
$( 'body' ).removeClass( 'closing-overlay' );
// Get scroll position to avoid jumping to the top.
scroll = document.body.scrollTop;
// Clean the URL structure.
themes.router.navigate( themes.router.baseUrl( '' ) );
// Restore scroll position.
document.body.scrollTop = scroll;
// Return focus to the theme div.
if ( themes.focusedTheme ) {
themes.focusedTheme.trigger( 'focus' );
// Handles .disabled classes for next/previous buttons.
// Disable Left/Right when at the start or end of the collection.
if ( this.model.cid === this.model.collection.at(0).cid ) {
.prop( 'disabled', true );
if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
this.$el.find( '.right' )
.prop( 'disabled', true );
// Performs the actions to effectively close
// the theme details overlay.
closeOverlay: function() {
$( 'body' ).removeClass( 'modal-open' );
this.trigger( 'theme:collapse' );
// Set state of the auto-update settings link after it has been changed and saved.
autoupdateState: function() {
// Support concurrent clicks in different Theme Details overlays.
callback = function( event, data ) {
if ( _this.model.get( 'id' ) === data.asset ) {
autoupdate = _this.model.get( 'autoupdate' );
autoupdate.enabled = 'enable' === data.state;
_this.model.set( { autoupdate: autoupdate } );
$( document ).off( 'wp-auto-update-setting-changed', callback );
// Triggered in updates.js
$( document ).on( 'wp-auto-update-setting-changed', callback );
updateTheme: function( event ) {
wp.updates.maybeRequestFilesystemCredentials( event );
$( document ).on( 'wp-theme-update-success', function( event, response ) {
if ( _this.model.get( 'id' ) === response.slug ) {
version: response.newVersion
wp.updates.updateTheme( {
slug: $( event.target ).data( 'slug' )
deleteTheme: function( event ) {
_collection = _this.model.collection,
// Confirmation dialog for deleting a theme.
if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) {
wp.updates.maybeRequestFilesystemCredentials( event );
$( document ).one( 'wp-theme-delete-success', function( event, response ) {
_this.$el.find( '.close' ).trigger( 'click' );
$( '[data-slug="' + response.slug + '"]' ).css( { backgroundColor:'#faafaa' } ).fadeOut( 350, function() {
_themes.data.themes = _.without( _themes.data.themes, _.findWhere( _themes.data.themes, { id: response.slug } ) );
$( '.wp-filter-search' ).val( '' );
_collection.doSearch( '' );
_collection.remove( _this.model );
_collection.trigger( 'themes:update' );
wp.updates.deleteTheme( {
slug: this.model.get( 'id' )
self.trigger( 'theme:next', self.model.cid );
previousTheme: function() {
self.trigger( 'theme:previous', self.model.cid );
// Checks if the theme screenshot is the old 300px width version
// and adds a corresponding class if it's true.
screenshotCheck: function( el ) {
screenshot = el.find( '.screenshot img' );
image.src = screenshot.attr( 'src' );
if ( image.width && image.width <= 300 ) {
el.addClass( 'small-screenshot' );
// Sets up a modal overlay with the expanded theme data.
themes.view.Preview = themes.view.Details.extend({
className: 'wp-full-overlay expanded',
el: '.theme-install-overlay',
'click .close-full-overlay': 'close',
'click .collapse-sidebar': 'collapse',
'click .devices button': 'previewDevice',
'click .previous-theme': 'previousTheme',
'click .next-theme': 'nextTheme',
'click .theme-install': 'installTheme'
// The HTML template for the theme preview.
html: themes.template( 'theme-preview' ),
data = this.model.toJSON(),
$body = $( document.body );
$body.attr( 'aria-busy', 'true' );
this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );
currentPreviewDevice = this.$el.data( 'current-preview-device' );
if ( currentPreviewDevice ) {
self.tooglePreviewDeviceButtons( currentPreviewDevice );
themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: false } );
this.$el.fadeIn( 200, function() {
$body.addClass( 'theme-installer-active full-overlay-active' );
this.$el.find( 'iframe' ).one( 'load', function() {
iframeLoaded: function() {
this.$el.addClass( 'iframe-ready' );
$( document.body ).attr( 'aria-busy', 'false' );
this.$el.fadeOut( 200, function() {
$( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
// Return focus to the theme div.
if ( themes.focusedTheme ) {
themes.focusedTheme.trigger( 'focus' );
}).removeClass( 'iframe-ready' );
// Restore the previous browse tab if available.
if ( themes.router.selectedTab ) {
themes.router.navigate( themes.router.baseUrl( '?browse=' + themes.router.selectedTab ) );
themes.router.selectedTab = false;
themes.router.navigate( themes.router.baseUrl( '' ) );
this.trigger( 'preview:close' );
collapse: function( event ) {
var $button = $( event.currentTarget );
if ( 'true' === $button.attr( 'aria-expanded' ) ) {
$button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
$button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
previewDevice: function( event ) {
var device = $( event.currentTarget ).data( 'device' );
.removeClass( 'preview-desktop preview-tablet preview-mobile' )
.addClass( 'preview-' + device )
.data( 'current-preview-device', device );
this.tooglePreviewDeviceButtons( device );
tooglePreviewDeviceButtons: function( newDevice ) {
var $devices = $( '.wp-full-overlay-footer .devices' );
$devices.find( 'button' )
.attr( 'aria-pressed', false );