wp/wp-admin/js/theme.js
changeset 7 cf61fcea0001
parent 5 5e2f62d02dcd
child 9 177826044cd9
--- a/wp/wp-admin/js/theme.js	Tue Jun 09 11:14:17 2015 +0000
+++ b/wp/wp-admin/js/theme.js	Mon Oct 14 17:39:30 2019 +0200
@@ -77,14 +77,15 @@
 		// Render search form.
 		this.search();
 
+		this.$el.removeClass( 'search-loading' );
+
 		// Render and append
 		this.view.render();
 		this.$el.empty().append( this.view.el ).addClass( 'rendered' );
-		this.$el.append( '<br class="clear"/>' );
 	},
 
 	// Defines search element container
-	searchContainer: $( '#wpbody h2:first' ),
+	searchContainer: $( '.search-form' ),
 
 	// Search input and view
 	// for current theme collection
@@ -101,12 +102,16 @@
 			collection: self.collection,
 			parent: this
 		});
+		self.SearchView = view;
 
 		// Render and append after screen title
 		view.render();
 		this.searchContainer
 			.append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
-			.append( view.el );
+			.append( view.el )
+			.on( 'submit', function( event ) {
+				event.preventDefault();
+			});
 	},
 
 	// Checks when the user gets close to the bottom
@@ -159,8 +164,8 @@
 			$( 'body' ).removeClass( 'no-results' );
 		}
 
-		// Trigger an 'update' event
-		this.trigger( 'update' );
+		// Trigger a 'themes:update' event
+		this.trigger( 'themes:update' );
 	},
 
 	// Performs a search within the collection
@@ -186,7 +191,7 @@
 			description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
 			author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
 
-			haystack = _.union( name, data.get( 'id' ), description, author, data.get( 'tags' ) );
+			haystack = _.union( [ name, data.get( 'id' ), description, author, data.get( 'tags' ) ] );
 
 			if ( match.test( data.get( 'author' ) ) && term.length > 2 ) {
 				data.set( 'displayAuthor', true );
@@ -265,7 +270,7 @@
 
 				// Trigger a collection refresh event
 				// and a `query:success` event with a `count` argument.
-				self.trigger( 'update' );
+				self.trigger( 'themes:update' );
 				self.trigger( 'query:success', count );
 
 				if ( data.themes && data.themes.length === 0 ) {
@@ -309,7 +314,7 @@
 				this.count = this.length;
 			}
 
-			this.trigger( 'update' );
+			this.trigger( 'themes:update' );
 			this.trigger( 'query:success', this.count );
 		}
 	},
@@ -376,17 +381,25 @@
 		'keydown': themes.isInstall ? 'preview': 'expand',
 		'touchend': themes.isInstall ? 'preview': 'expand',
 		'keyup': 'addFocus',
-		'touchmove': 'preventExpand'
+		'touchmove': 'preventExpand',
+		'click .theme-install': 'installTheme',
+		'click .update-message': 'updateTheme'
 	},
 
 	touchDrag: false,
 
+	initialize: function() {
+		this.model.on( 'change', this.render, this );
+	},
+
 	render: function() {
 		var data = this.model.toJSON();
+
 		// Render themes using the html template
 		this.$el.html( this.html( data ) ).attr({
 			tabindex: 0,
-			'aria-describedby' : data.id + '-action ' + data.id + '-name'
+			'aria-describedby' : data.id + '-action ' + data.id + '-name',
+			'data-slug': data.id
 		});
 
 		// Renders active theme styles
@@ -395,10 +408,6 @@
 		if ( this.model.get( 'displayAuthor' ) ) {
 			this.$el.addClass( 'display-author' );
 		}
-
-		if ( this.model.get( 'installed' ) ) {
-			this.$el.addClass( 'is-installed' );
-		}
 	},
 
 	// Adds a class to the currently active theme
@@ -440,6 +449,11 @@
 			return;
 		}
 
+		// Prevent the modal from showing when the user clicks one of the direct action buttons.
+		if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
+			return;
+		}
+
 		// Set focused theme to current element
 		themes.focusedTheme = this.$el;
 
@@ -454,13 +468,15 @@
 		var self = this,
 			current, preview;
 
+		event = event || window.event;
+
 		// Bail if the user scrolled on a touch device
 		if ( this.touchDrag === true ) {
 			return this.touchDrag = false;
 		}
 
 		// Allow direct link path to installing a theme.
-		if ( $( event.target ).hasClass( 'button-primary' ) ) {
+		if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
 			return;
 		}
 
@@ -482,7 +498,7 @@
 		themes.focusedTheme = this.$el;
 
 		// Construct a new Preview view.
-		preview = new themes.view.Preview({
+		themes.preview = preview = new themes.view.Preview({
 			model: this.model
 		});
 
@@ -562,6 +578,7 @@
 		this.listenTo( preview, 'preview:close', function() {
 			self.current = self.model;
 		});
+
 	},
 
 	// Handles .disabled classes for previous/next buttons in theme installer preview
@@ -578,6 +595,51 @@
 		if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
 			$themeInstaller.find( '.next-theme' ).addClass( 'disabled' );
 		}
+	},
+
+	installTheme: function( event ) {
+		var _this = this;
+
+		event.preventDefault();
+
+		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 ) {
+		var _this = this;
+
+		if ( ! this.model.get( 'hasPackage' ) ) {
+			return;
+		}
+
+		event.preventDefault();
+
+		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 ) {
+				_this.model.set( {
+					hasUpdate: false,
+					version: response.newVersion
+				} );
+			}
+			_this.model.on( 'change', _this.render, _this );
+		} );
+
+		wp.updates.updateTheme( {
+			slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
+		} );
 	}
 });
 
@@ -592,7 +654,8 @@
 		'click': 'collapse',
 		'click .delete-theme': 'deleteTheme',
 		'click .left': 'previousTheme',
-		'click .right': 'nextTheme'
+		'click .right': 'nextTheme',
+		'click #update-theme': 'updateTheme'
 	},
 
 	// The HTML template for the theme overlay
@@ -618,28 +681,26 @@
 		this.$el.toggleClass( 'active', this.model.get( 'active' ) );
 	},
 
-	// Keeps :focus within the theme details elements
+	// Set initial focus and constrain tabbing within the theme browser modal.
 	containFocus: function( $el ) {
-		var $target;
 
-		// Move focus to the primary action
+		// Set initial focus on the primary action control.
 		_.delay( function() {
-			$( '.theme-wrap a.button-primary:visible' ).focus();
-		}, 500 );
-
-		$el.on( 'keydown.wp-themes', function( event ) {
+			$( '.theme-overlay' ).focus();
+		}, 100 );
 
-			// Tab key
-			if ( event.which === 9 ) {
-				$target = $( event.target );
+		// 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();
 
-				// Keep focus within the overlay by making the last link on theme actions
-				// switch focus to button.left on tabbing and vice versa
-				if ( $target.is( 'button.left' ) && event.shiftKey ) {
-					$el.find( '.theme-actions a:last-child' ).focus();
+			// Check for the Tab key.
+			if ( 9 === event.which ) {
+				if ( $firstFocusable[0] === event.target && event.shiftKey ) {
+					$lastFocusable.focus();
 					event.preventDefault();
-				} else if ( $target.is( '.theme-actions a:last-child' ) ) {
-					$el.find( 'button.left' ).focus();
+				} else if ( $lastFocusable[0] === event.target && ! event.shiftKey ) {
+					$firstFocusable.focus();
 					event.preventDefault();
 				}
 			}
@@ -696,10 +757,14 @@
 
 		// Disable Left/Right when at the start or end of the collection
 		if ( this.model.cid === this.model.collection.at(0).cid ) {
-			this.$el.find( '.left' ).addClass( 'disabled' );
+			this.$el.find( '.left' )
+				.addClass( 'disabled' )
+				.prop( 'disabled', true );
 		}
 		if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
-			this.$el.find( '.right' ).addClass( 'disabled' );
+			this.$el.find( '.right' )
+				.addClass( 'disabled' )
+				.prop( 'disabled', true );
 		}
 	},
 
@@ -712,9 +777,56 @@
 		this.trigger( 'theme:collapse' );
 	},
 
-	// Confirmation dialog for deleting a theme
-	deleteTheme: function() {
-		return confirm( themes.data.settings.confirmDelete );
+	updateTheme: function( event ) {
+		var _this = this;
+		event.preventDefault();
+
+		wp.updates.maybeRequestFilesystemCredentials( event );
+
+		$( document ).on( 'wp-theme-update-success', function( event, response ) {
+			if ( _this.model.get( 'id' ) === response.slug ) {
+				_this.model.set( {
+					hasUpdate: false,
+					version: response.newVersion
+				} );
+			}
+			_this.render();
+		} );
+
+		wp.updates.updateTheme( {
+			slug: $( event.target ).data( 'slug' )
+		} );
+	},
+
+	deleteTheme: function( event ) {
+		var _this = this,
+		    _collection = _this.model.collection,
+		    _themes = themes;
+		event.preventDefault();
+
+		// Confirmation dialog for deleting a theme.
+		if ( ! window.confirm( wp.themes.data.settings.confirmDelete ) ) {
+			return;
+		}
+
+		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() {
+				$( this ).remove();
+				_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' )
+		} );
 	},
 
 	nextTheme: function() {
@@ -755,27 +867,47 @@
 	events: {
 		'click .close-full-overlay': 'close',
 		'click .collapse-sidebar': 'collapse',
+		'click .devices button': 'previewDevice',
 		'click .previous-theme': 'previousTheme',
 		'click .next-theme': 'nextTheme',
-		'keyup': 'keyEvent'
+		'keyup': 'keyEvent',
+		'click .theme-install': 'installTheme'
 	},
 
 	// The HTML template for the theme preview
 	html: themes.template( 'theme-preview' ),
 
 	render: function() {
-		var data = this.model.toJSON();
+		var self = this,
+			currentPreviewDevice,
+			data = this.model.toJSON(),
+			$body = $( document.body );
+
+		$body.attr( 'aria-busy', 'true' );
 
-		this.$el.html( this.html( data ) );
+		this.$el.removeClass( 'iframe-ready' ).html( this.html( data ) );
 
-		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.get( 'id' ) ), { replace: true } );
+		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' );
-			$( '.close-full-overlay' ).focus();
+			$body.addClass( 'theme-installer-active full-overlay-active' );
+		});
+
+		this.$el.find( 'iframe' ).one( 'load', function() {
+			self.iframeLoaded();
 		});
 	},
 
+	iframeLoaded: function() {
+		this.$el.addClass( 'iframe-ready' );
+		$( document.body ).attr( 'aria-busy', 'false' );
+	},
+
 	close: function() {
 		this.$el.fadeOut( 200, function() {
 			$( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
@@ -784,21 +916,56 @@
 			if ( themes.focusedTheme ) {
 				themes.focusedTheme.focus();
 			}
-		});
+		}).removeClass( 'iframe-ready' );
 
-		themes.router.navigate( themes.router.baseUrl( '' ) );
+		// 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;
+		} else {
+			themes.router.navigate( themes.router.baseUrl( '' ) );
+		}
 		this.trigger( 'preview:close' );
 		this.undelegateEvents();
 		this.unbind();
 		return false;
 	},
 
-	collapse: function() {
+	collapse: function( event ) {
+		var $button = $( event.currentTarget );
+		if ( 'true' === $button.attr( 'aria-expanded' ) ) {
+			$button.attr({ 'aria-expanded': 'false', 'aria-label': l10n.expandSidebar });
+		} else {
+			$button.attr({ 'aria-expanded': 'true', 'aria-label': l10n.collapseSidebar });
+		}
 
 		this.$el.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
 		return false;
 	},
 
+	previewDevice: function( event ) {
+		var device = $( event.currentTarget ).data( 'device' );
+
+		this.$el
+			.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' )
+			.removeClass( 'active' )
+			.attr( 'aria-pressed', false );
+
+		$devices.find( 'button.preview-' + newDevice )
+			.addClass( 'active' )
+			.attr( 'aria-pressed', true );
+	},
+
 	keyEvent: function( event ) {
 		// The escape key closes the preview
 		if ( event.keyCode === 27 ) {
@@ -814,6 +981,26 @@
 		if ( event.keyCode === 37 ) {
 			this.previousTheme();
 		}
+	},
+
+	installTheme: function( event ) {
+		var _this   = this,
+		    $target = $( event.target );
+		event.preventDefault();
+
+		if ( $target.hasClass( 'disabled' ) ) {
+			return;
+		}
+
+		wp.updates.maybeRequestFilesystemCredentials( event );
+
+		$( document ).on( 'wp-theme-install-success', function() {
+			_this.model.set( { 'installed': true } );
+		} );
+
+		wp.updates.installTheme( {
+			slug: $target.data( 'slug' )
+		} );
 	}
 });
 
@@ -821,7 +1008,7 @@
 // a wrapper that will hold all the theme elements
 themes.view.Themes = wp.Backbone.View.extend({
 
-	className: 'themes',
+	className: 'themes wp-clearfix',
 	$overlay: $( 'div.theme-overlay' ),
 
 	// Number to keep track of scroll position
@@ -829,7 +1016,7 @@
 	index: 0,
 
 	// The theme count element
-	count: $( '.wp-core-ui .theme-count' ),
+	count: $( '.wrap .theme-count' ),
 
 	// The live themes count
 	liveThemeCount: 0,
@@ -847,11 +1034,11 @@
 		self.currentTheme();
 
 		// When the collection is updated by user input...
-		this.listenTo( self.collection, 'update', function() {
+		this.listenTo( self.collection, 'themes:update', function() {
 			self.parent.page = 0;
 			self.currentTheme();
 			self.render( this );
-		});
+		} );
 
 		// Update theme count to full result set when available.
 		this.listenTo( self.collection, 'query:success', function( count ) {
@@ -884,6 +1071,11 @@
 				return;
 			}
 
+			// Bail if the filesystem credentials dialog is shown.
+			if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
+				return;
+			}
+
 			// Pressing the right arrow key fires a theme:next event
 			if ( event.keyCode === 39 ) {
 				self.overlay.nextTheme();
@@ -934,7 +1126,13 @@
 		this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
 		this.count.text( this.liveThemeCount );
 
-		this.announceSearchResults( this.liveThemeCount );
+		/*
+		 * In the theme installer the themes count is already announced
+		 * because `announceSearchResults` is called on `query:success`.
+		 */
+		if ( ! themes.isInstall ) {
+			this.announceSearchResults( this.liveThemeCount );
+		}
 	},
 
 	// Iterates through each instance of the collection
@@ -952,7 +1150,7 @@
 		}
 
 		// Make sure the add-new stays at the end
-		if ( page >= 1 ) {
+		if ( ! themes.isInstall && page >= 1 ) {
 			$( '.add-new-theme' ).remove();
 		}
 
@@ -974,8 +1172,8 @@
 		});
 
 		// 'Add new theme' element shown at the end of the grid
-		if ( themes.data.settings.canInstall ) {
-			this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span></span></div><h3 class="theme-name">' + l10n.addNew + '</h3></a></div>' );
+		if ( ! themes.isInstall && themes.data.settings.canInstall ) {
+			this.$el.append( '<div class="theme add-new-theme"><a href="' + themes.data.settings.installURI + '"><div class="theme-screenshot"><span></span></div><h2 class="theme-name">' + l10n.addNew + '</h2></a></div>' );
 		}
 
 		this.parent.page++;
@@ -1003,7 +1201,7 @@
 	// Renders the overlay with the ThemeDetails view
 	// Uses the current model data
 	expand: function( id ) {
-		var self = this;
+		var self = this, $card, $modal;
 
 		// Set the current theme model
 		this.model = self.collection.get( id );
@@ -1021,6 +1219,22 @@
 		});
 
 		this.overlay.render();
+
+		if ( this.model.get( 'hasUpdate' ) ) {
+			$card  = $( '[data-slug="' + this.model.id + '"]' );
+			$modal = $( this.overlay.el );
+
+			if ( $card.find( '.updating-message' ).length ) {
+				$modal.find( '.notice-warning h3' ).remove();
+				$modal.find( '.notice-warning' )
+					.removeClass( 'notice-large' )
+					.addClass( 'updating-message' )
+					.find( 'p' ).text( wp.updates.l10n.updating );
+			} else if ( $card.find( '.notice-error' ).length ) {
+				$modal.find( '.notice-warning' ).remove();
+			}
+		}
+
 		this.$overlay.html( this.overlay.el );
 
 		// Bind to theme:next and theme:previous
@@ -1134,17 +1348,15 @@
 			event.target.value = '';
 		}
 
-		/**
-		 * Since doSearch is debounced, it will only run when user input comes to a rest
-		 */
+		// Since doSearch is debounced, it will only run when user input comes to a rest.
 		this.doSearch( event );
 	},
 
 	// Runs a search on the theme collection.
-	doSearch: _.debounce( function( event ) {
+	doSearch: function( event ) {
 		var options = {};
 
-		this.collection.doSearch( event.target.value );
+		this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );
 
 		// if search is initiated and key is not return
 		if ( this.searching && event.which !== 13 ) {
@@ -1159,13 +1371,13 @@
 		} else {
 			themes.router.navigate( themes.router.baseUrl( '' ) );
 		}
-	}, 500 ),
+	},
 
 	pushState: function( event ) {
 		var url = themes.router.baseUrl( '' );
 
 		if ( event.target.value ) {
-			url = themes.router.baseUrl( themes.router.searchPath + event.target.value );
+			url = themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( event.target.value ) );
 		}
 
 		this.searching = false;
@@ -1174,6 +1386,22 @@
 	}
 });
 
+/**
+ * Navigate router.
+ *
+ * @since 4.9.0
+ *
+ * @param {string} url - URL to navigate to.
+ * @param {object} state - State.
+ * @returns {void}
+ */
+function navigateRouter( url, state ) {
+	var router = this;
+	if ( Backbone.history._hasPushState ) {
+		Backbone.Router.prototype.navigate.call( router, url, state );
+	}
+}
+
 // Sets up the routes events for relevant url queries
 // Listens to [theme] and [search] params
 themes.Router = Backbone.Router.extend({
@@ -1194,18 +1422,14 @@
 	searchPath: '?search=',
 
 	search: function( query ) {
-		$( '.wp-filter-search' ).val( query );
+		$( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
 	},
 
 	themes: function() {
 		$( '.wp-filter-search' ).val( '' );
 	},
 
-	navigate: function() {
-		if ( Backbone.history._hasPushState ) {
-			Backbone.Router.prototype.navigate.apply( this, arguments );
-		}
-	}
+	navigate: navigateRouter
 
 });
 
@@ -1222,6 +1446,9 @@
 		});
 
 		this.render();
+
+		// Start debouncing user searches after Backbone.history.start().
+		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
 	},
 
 	render: function() {
@@ -1230,6 +1457,9 @@
 		this.view.render();
 		this.routes();
 
+		if ( Backbone.History.started ) {
+			Backbone.history.stop();
+		}
 		Backbone.history.start({
 			root: themes.data.settings.adminUrl,
 			pushState: true,
@@ -1274,6 +1504,8 @@
 		'keyup': 'search'
 	},
 
+	terms: '',
+
 	// Handles Ajax request for searching through themes in public repo
 	search: function( event ) {
 
@@ -1292,9 +1524,17 @@
 		this.doSearch( event.target.value );
 	},
 
-	doSearch: _.debounce( function( value ) {
+	doSearch: function( value ) {
 		var request = {};
 
+		// Don't do anything if the search terms haven't changed.
+		if ( this.terms === value ) {
+			return;
+		}
+
+		// Updates terms with the value passed.
+		this.terms = value;
+
 		request.search = value;
 
 		// Intercept an [author] search.
@@ -1315,16 +1555,20 @@
 			request.tag = [ value.slice( 4 ) ];
 		}
 
-		$( '.filter-links li > a.current' ).removeClass( 'current' );
-		$( 'body' ).removeClass( 'show-filters filters-applied' );
+		$( '.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 );
 
 		// Set route
-		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + value ), { replace: true } );
-	}, 500 )
+		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
+	}
 });
 
 themes.view.Installer = themes.view.Appearance.extend({
@@ -1339,7 +1583,9 @@
 		'click .filter-drawer .apply-filters': 'applyFilters',
 		'click .filter-group [type="checkbox"]': 'addFilter',
 		'click .filter-drawer .clear-filters': 'clearFilters',
-		'click .filtered-by': 'backToFilters'
+		'click .edit-filters': 'backToFilters',
+		'click .favorites-form-submit' : 'saveUsername',
+		'keyup #wporg-username-input': 'saveUsername'
 	},
 
 	// Initial render method
@@ -1376,7 +1622,11 @@
 		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></div>' );
+			$( '.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 ) {
+				e.preventDefault();
+				$( 'input.wp-filter-search' ).trigger( 'input' );
+			} );
 		});
 
 		if ( this.view ) {
@@ -1413,6 +1663,7 @@
 		event.preventDefault();
 
 		$( 'body' ).removeClass( 'filters-applied show-filters' );
+		$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
 
 		// Bail if this is already active
 		if ( $el.hasClass( this.activeClass ) ) {
@@ -1428,8 +1679,22 @@
 	sort: function( sort ) {
 		this.clearSearch();
 
-		$( '.filter-links li > a, .theme-filter' ).removeClass( this.activeClass );
-		$( '[data-sort="' + sort + '"]' ).addClass( this.activeClass );
+		// 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' );
+		} else {
+			$( 'body' ).removeClass( 'show-favorites-form' );
+		}
 
 		this.browse( sort );
 	},
@@ -1445,8 +1710,12 @@
 			return;
 		}
 
-		$( '.filter-links li > a, .theme-section' ).removeClass( this.activeClass );
-		$el.addClass( this.activeClass );
+		$( '.filter-links li > a, .theme-section' )
+			.removeClass( this.activeClass )
+			.removeAttr( 'aria-current' );
+		$el
+			.addClass( this.activeClass )
+			.attr( 'aria-current', 'page' );
 
 		if ( ! filter ) {
 			return;
@@ -1454,7 +1723,7 @@
 
 		// Construct the filter request
 		// using the default values
-		filter = _.union( filter, this.filtersChecked() );
+		filter = _.union( [ filter, this.filtersChecked() ] );
 		request = { tag: [ filter ] };
 
 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
@@ -1478,8 +1747,16 @@
 			event.preventDefault();
 		}
 
+		if ( ! tags ) {
+			wp.a11y.speak( l10n.selectFeatureFilter );
+			return;
+		}
+
 		$( 'body' ).addClass( 'filters-applied' );
-		$( '.filter-links li > a.current' ).removeClass( 'current' );
+		$( '.filter-links li > a.current' )
+			.removeClass( 'current' )
+			.removeAttr( 'aria-current' );
+
 		filteringBy.empty();
 
 		_.each( tags, function( tag ) {
@@ -1492,6 +1769,35 @@
 		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 },
+			that = this;
+
+		if ( event ) {
+			event.preventDefault();
+		}
+
+		// save username on enter
+		if ( event.type === 'keyup' && event.which !== 13 ) {
+			return;
+		}
+
+		return wp.ajax.send( 'save-wporg-username', {
+			data: {
+				_wpnonce: nonce,
+				username: username
+			},
+			success: function () {
+				// 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() {
@@ -1518,41 +1824,39 @@
 
 	activeClass: 'current',
 
-	// Overwrite search container class to append search
-	// in new location
-	searchContainer: $( '.wp-filter .search-form' ),
-
+	/*
+	 * When users press the "Upload Theme" button, show the upload form in place.
+	 */
 	uploader: function() {
-		$( 'a.upload' ).on( 'click', function( event ) {
-			event.preventDefault();
-			$( 'body' ).addClass( 'show-upload-theme' );
-			themes.router.navigate( themes.router.baseUrl( '?upload' ), { replace: true } );
-		});
-		$( 'a.browse-themes' ).on( 'click', function( event ) {
-			event.preventDefault();
-			$( 'body' ).removeClass( 'show-upload-theme' );
-			themes.router.navigate( themes.router.baseUrl( '' ), { replace: true } );
+		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 ) {
+		var $body = $( 'body' ),
+			$toggleButton = $( '.drawer-toggle' );
+
 		event.preventDefault();
 
-		if ( $( 'body' ).hasClass( 'filters-applied' ) ) {
+		if ( $body.hasClass( 'filters-applied' ) ) {
 			return this.backToFilters();
 		}
 
-		// If the filters section is opened and filters are checked
-		// run the relevant query collapsing to filtered-by state
-		if ( $( 'body' ).hasClass( 'show-filters' ) && this.filtersChecked() ) {
-			return this.addFilter();
-		}
-
 		this.clearSearch();
 
 		themes.router.navigate( themes.router.baseUrl( '' ) );
-		$( 'body' ).toggleClass( 'show-filters' );
+		// 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
@@ -1586,7 +1890,6 @@
 	routes: {
 		'theme-install.php?theme=:slug': 'preview',
 		'theme-install.php?browse=:sort': 'sort',
-		'theme-install.php?upload': 'upload',
 		'theme-install.php?search=:query': 'search',
 		'theme-install.php': 'sort'
 	},
@@ -1600,14 +1903,10 @@
 	searchPath: '?search=',
 
 	search: function( query ) {
-		$( '.wp-filter-search' ).val( query );
+		$( '.wp-filter-search' ).val( query.replace( /\+/g, ' ' ) );
 	},
 
-	navigate: function() {
-		if ( Backbone.history._hasPushState ) {
-			Backbone.Router.prototype.navigate.apply( this, arguments );
-		}
-	}
+	navigate: navigateRouter
 });
 
 
@@ -1624,6 +1923,8 @@
 		// Render results
 		this.render();
 
+		// Start debouncing user searches after Backbone.history.start().
+		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
 	},
 
 	render: function() {
@@ -1632,6 +1933,9 @@
 		this.view.render();
 		this.routes();
 
+		if ( Backbone.History.started ) {
+			Backbone.history.stop();
+		}
 		Backbone.history.start({
 			root: themes.data.settings.adminUrl,
 			pushState: true,
@@ -1650,8 +1954,30 @@
 		// Handles `theme` route event
 		// Queries the API for the passed theme slug
 		themes.router.on( 'route:preview', function( slug ) {
-			request.theme = slug;
-			self.view.collection.query( request );
+
+			// Remove existing handlers.
+			if ( themes.preview ) {
+				themes.preview.undelegateEvents();
+				themes.preview.unbind();
+			}
+
+			// If the theme preview is active, set the current theme.
+			if ( self.view.view.theme && self.view.view.theme.preview ) {
+				self.view.view.theme.model = self.view.collection.findWhere( { 'slug': slug } );
+				self.view.view.theme.preview();
+			} else {
+
+				// Select the theme by slug.
+				request.theme = slug;
+				self.view.collection.query( request );
+				self.view.collection.trigger( 'update' );
+
+				// Open the theme preview.
+				self.view.collection.once( 'query:success', function() {
+					$( 'div[data-slug="' + slug + '"]' ).trigger( 'click' );
+				});
+
+			}
 		});
 
 		// Handles sorting / browsing routes
@@ -1660,14 +1986,14 @@
 		themes.router.on( 'route:sort', function( sort ) {
 			if ( ! sort ) {
 				sort = 'featured';
+				themes.router.navigate( themes.router.baseUrl( '?browse=featured' ), { replace: true } );
 			}
 			self.view.sort( sort );
-			self.view.trigger( 'theme:close' );
-		});
 
-		// Support the `upload` route by going straight to upload section
-		themes.router.on( 'route:upload', function() {
-			$( 'a.upload' ).trigger( 'click' );
+			// Close the preview if open.
+			if ( themes.preview ) {
+				themes.preview.close();
+			}
 		});
 
 		// The `search` route event. The router populates the input field.
@@ -1691,6 +2017,19 @@
 		themes.Run.init();
 	}
 
+	// Update the return param just in time.
+	$( document.body ).on( 'click', '.load-customize', function() {
+		var link = $( this ), urlParser = document.createElement( 'a' );
+		urlParser.href = link.prop( 'href' );
+		urlParser.search = $.param( _.extend(
+			wp.customize.utils.parseQueryString( urlParser.search.substr( 1 ) ),
+			{
+				'return': window.location.href
+			}
+		) );
+		link.prop( 'href', urlParser.href );
+	});
+
 	$( '.broken-themes .delete-theme' ).on( 'click', function() {
 		return confirm( _wpThemeSettings.settings.confirmDelete );
 	});