wp/wp-admin/js/theme.js
changeset 16 a86126ab1dd4
parent 9 177826044cd9
child 18 be944660c56a
equal deleted inserted replaced
15:3d4e9c994f10 16:a86126ab1dd4
     9 
     9 
    10 // Set up our namespace...
    10 // Set up our namespace...
    11 var themes, l10n;
    11 var themes, l10n;
    12 themes = wp.themes = wp.themes || {};
    12 themes = wp.themes = wp.themes || {};
    13 
    13 
    14 // Store the theme data and settings for organized and quick access
    14 // Store the theme data and settings for organized and quick access.
    15 // themes.data.settings, themes.data.themes, themes.data.l10n
    15 // themes.data.settings, themes.data.themes, themes.data.l10n.
    16 themes.data = _wpThemeSettings;
    16 themes.data = _wpThemeSettings;
    17 l10n = themes.data.l10n;
    17 l10n = themes.data.l10n;
    18 
    18 
    19 // Shortcut for isInstall check
    19 // Shortcut for isInstall check.
    20 themes.isInstall = !! themes.data.settings.isInstall;
    20 themes.isInstall = !! themes.data.settings.isInstall;
    21 
    21 
    22 // Setup app structure
    22 // Setup app structure.
    23 _.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
    23 _.extend( themes, { model: {}, view: {}, routes: {}, router: {}, template: wp.template });
    24 
    24 
    25 themes.Model = Backbone.Model.extend({
    25 themes.Model = Backbone.Model.extend({
    26 	// Adds attributes to the default data coming through the .org themes api
    26 	// Adds attributes to the default data coming through the .org themes api.
    27 	// Map `id` to `slug` for shared code
    27 	// Map `id` to `slug` for shared code.
    28 	initialize: function() {
    28 	initialize: function() {
    29 		var description;
    29 		var description;
    30 
    30 
    31 		// If theme is already installed, set an attribute.
    31 		if ( this.get( 'slug' ) ) {
    32 		if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) {
    32 			// If the theme is already installed, set an attribute.
    33 			this.set({ installed: true });
    33 			if ( _.indexOf( themes.data.installedThemes, this.get( 'slug' ) ) !== -1 ) {
    34 		}
    34 				this.set({ installed: true });
    35 
    35 			}
    36 		// Set the attributes
    36 
       
    37 			// If the theme is active, set an attribute.
       
    38 			if ( themes.data.activeTheme === this.get( 'slug' ) ) {
       
    39 				this.set({ active: true });
       
    40 			}
       
    41 		}
       
    42 
       
    43 		// Set the attributes.
    37 		this.set({
    44 		this.set({
    38 			// slug is for installation, id is for existing.
    45 			// `slug` is for installation, `id` is for existing.
    39 			id: this.get( 'slug' ) || this.get( 'id' )
    46 			id: this.get( 'slug' ) || this.get( 'id' )
    40 		});
    47 		});
    41 
    48 
    42 		// Map `section.description` to `description`
    49 		// Map `section.description` to `description`
    43 		// as the API sometimes returns it differently
    50 		// as the API sometimes returns it differently.
    44 		if ( this.has( 'sections' ) ) {
    51 		if ( this.has( 'sections' ) ) {
    45 			description = this.get( 'sections' ).description;
    52 			description = this.get( 'sections' ).description;
    46 			this.set({ description: description });
    53 			this.set({ description: description });
    47 		}
    54 		}
    48 	}
    55 	}
    49 });
    56 });
    50 
    57 
    51 // Main view controller for themes.php
    58 // Main view controller for themes.php.
    52 // Unifies and renders all available views
    59 // Unifies and renders all available views.
    53 themes.view.Appearance = wp.Backbone.View.extend({
    60 themes.view.Appearance = wp.Backbone.View.extend({
    54 
    61 
    55 	el: '#wpbody-content .wrap .theme-browser',
    62 	el: '#wpbody-content .wrap .theme-browser',
    56 
    63 
    57 	window: $( window ),
    64 	window: $( window ),
    58 	// Pagination instance
    65 	// Pagination instance.
    59 	page: 0,
    66 	page: 0,
    60 
    67 
    61 	// Sets up a throttler for binding to 'scroll'
    68 	// Sets up a throttler for binding to 'scroll'.
    62 	initialize: function( options ) {
    69 	initialize: function( options ) {
    63 		// Scroller checks how far the scroll position is
    70 		// Scroller checks how far the scroll position is.
    64 		_.bindAll( this, 'scroller' );
    71 		_.bindAll( this, 'scroller' );
    65 
    72 
    66 		this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
    73 		this.SearchView = options.SearchView ? options.SearchView : themes.view.Search;
    67 		// Bind to the scroll event and throttle
    74 		// Bind to the scroll event and throttle
    68 		// the results from this.scroller
    75 		// the results from this.scroller.
    69 		this.window.bind( 'scroll', _.throttle( this.scroller, 300 ) );
    76 		this.window.bind( 'scroll', _.throttle( this.scroller, 300 ) );
    70 	},
    77 	},
    71 
    78 
    72 	// Main render control
    79 	// Main render control.
    73 	render: function() {
    80 	render: function() {
    74 		// Setup the main theme view
    81 		// Setup the main theme view
    75 		// with the current theme collection
    82 		// with the current theme collection.
    76 		this.view = new themes.view.Themes({
    83 		this.view = new themes.view.Themes({
    77 			collection: this.collection,
    84 			collection: this.collection,
    78 			parent: this
    85 			parent: this
    79 		});
    86 		});
    80 
    87 
    81 		// Render search form.
    88 		// Render search form.
    82 		this.search();
    89 		this.search();
    83 
    90 
    84 		this.$el.removeClass( 'search-loading' );
    91 		this.$el.removeClass( 'search-loading' );
    85 
    92 
    86 		// Render and append
    93 		// Render and append.
    87 		this.view.render();
    94 		this.view.render();
    88 		this.$el.empty().append( this.view.el ).addClass( 'rendered' );
    95 		this.$el.empty().append( this.view.el ).addClass( 'rendered' );
    89 	},
    96 	},
    90 
    97 
    91 	// Defines search element container
    98 	// Defines search element container.
    92 	searchContainer: $( '.search-form' ),
    99 	searchContainer: $( '.search-form' ),
    93 
   100 
    94 	// Search input and view
   101 	// Search input and view
    95 	// for current theme collection
   102 	// for current theme collection.
    96 	search: function() {
   103 	search: function() {
    97 		var view,
   104 		var view,
    98 			self = this;
   105 			self = this;
    99 
   106 
   100 		// Don't render the search if there is only one theme
   107 		// Don't render the search if there is only one theme.
   101 		if ( themes.data.themes.length === 1 ) {
   108 		if ( themes.data.themes.length === 1 ) {
   102 			return;
   109 			return;
   103 		}
   110 		}
   104 
   111 
   105 		view = new this.SearchView({
   112 		view = new this.SearchView({
   106 			collection: self.collection,
   113 			collection: self.collection,
   107 			parent: this
   114 			parent: this
   108 		});
   115 		});
   109 		self.SearchView = view;
   116 		self.SearchView = view;
   110 
   117 
   111 		// Render and append after screen title
   118 		// Render and append after screen title.
   112 		view.render();
   119 		view.render();
   113 		this.searchContainer
   120 		this.searchContainer
   114 			.append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
   121 			.append( $.parseHTML( '<label class="screen-reader-text" for="wp-filter-search-input">' + l10n.search + '</label>' ) )
   115 			.append( view.el )
   122 			.append( view.el )
   116 			.on( 'submit', function( event ) {
   123 			.on( 'submit', function( event ) {
   117 				event.preventDefault();
   124 				event.preventDefault();
   118 			});
   125 			});
   119 	},
   126 	},
   120 
   127 
   121 	// Checks when the user gets close to the bottom
   128 	// Checks when the user gets close to the bottom
   122 	// of the mage and triggers a theme:scroll event
   129 	// of the mage and triggers a theme:scroll event.
   123 	scroller: function() {
   130 	scroller: function() {
   124 		var self = this,
   131 		var self = this,
   125 			bottom, threshold;
   132 			bottom, threshold;
   126 
   133 
   127 		bottom = this.window.scrollTop() + self.window.height();
   134 		bottom = this.window.scrollTop() + self.window.height();
   132 			this.trigger( 'theme:scroll' );
   139 			this.trigger( 'theme:scroll' );
   133 		}
   140 		}
   134 	}
   141 	}
   135 });
   142 });
   136 
   143 
   137 // Set up the Collection for our theme data
   144 // Set up the Collection for our theme data.
   138 // @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
   145 // @has 'id' 'name' 'screenshot' 'author' 'authorURI' 'version' 'active' ...
   139 themes.Collection = Backbone.Collection.extend({
   146 themes.Collection = Backbone.Collection.extend({
   140 
   147 
   141 	model: themes.Model,
   148 	model: themes.Model,
   142 
   149 
   143 	// Search terms
   150 	// Search terms.
   144 	terms: '',
   151 	terms: '',
   145 
   152 
   146 	// Controls searching on the current theme collection
   153 	// Controls searching on the current theme collection
   147 	// and triggers an update event
   154 	// and triggers an update event.
   148 	doSearch: function( value ) {
   155 	doSearch: function( value ) {
   149 
   156 
   150 		// Don't do anything if we've already done this search
   157 		// Don't do anything if we've already done this search.
   151 		// Useful because the Search handler fires multiple times per keystroke
   158 		// Useful because the Search handler fires multiple times per keystroke.
   152 		if ( this.terms === value ) {
   159 		if ( this.terms === value ) {
   153 			return;
   160 			return;
   154 		}
   161 		}
   155 
   162 
   156 		// Updates terms with the value passed
   163 		// Updates terms with the value passed.
   157 		this.terms = value;
   164 		this.terms = value;
   158 
   165 
   159 		// If we have terms, run a search...
   166 		// If we have terms, run a search...
   160 		if ( this.terms.length > 0 ) {
   167 		if ( this.terms.length > 0 ) {
   161 			this.search( this.terms );
   168 			this.search( this.terms );
   162 		}
   169 		}
   163 
   170 
   164 		// If search is blank, show all themes
   171 		// If search is blank, show all themes.
   165 		// Useful for resetting the views when you clean the input
   172 		// Useful for resetting the views when you clean the input.
   166 		if ( this.terms === '' ) {
   173 		if ( this.terms === '' ) {
   167 			this.reset( themes.data.themes );
   174 			this.reset( themes.data.themes );
   168 			$( 'body' ).removeClass( 'no-results' );
   175 			$( 'body' ).removeClass( 'no-results' );
   169 		}
   176 		}
   170 
   177 
   171 		// Trigger a 'themes:update' event
   178 		// Trigger a 'themes:update' event.
   172 		this.trigger( 'themes:update' );
   179 		this.trigger( 'themes:update' );
   173 	},
   180 	},
   174 
   181 
   175 	// Performs a search within the collection
   182 	/**
   176 	// @uses RegExp
   183 	 * Performs a search within the collection.
       
   184 	 *
       
   185 	 * @uses RegExp
       
   186 	 */
   177 	search: function( term ) {
   187 	search: function( term ) {
   178 		var match, results, haystack, name, description, author;
   188 		var match, results, haystack, name, description, author;
   179 
   189 
   180 		// Start with a full collection
   190 		// Start with a full collection.
   181 		this.reset( themes.data.themes, { silent: true } );
   191 		this.reset( themes.data.themes, { silent: true } );
   182 
   192 
   183 		// Escape the term string for RegExp meta characters
   193 		// Trim the term.
       
   194 		term = term.trim();
       
   195 
       
   196 		// Escape the term string for RegExp meta characters.
   184 		term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
   197 		term = term.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
   185 
   198 
   186 		// Consider spaces as word delimiters and match the whole string
   199 		// Consider spaces as word delimiters and match the whole string
   187 		// so matching terms can be combined
   200 		// so matching terms can be combined.
   188 		term = term.replace( / /g, ')(?=.*' );
   201 		term = term.replace( / /g, ')(?=.*' );
   189 		match = new RegExp( '^(?=.*' + term + ').+', 'i' );
   202 		match = new RegExp( '^(?=.*' + term + ').+', 'i' );
   190 
   203 
   191 		// Find results
   204 		// Find results.
   192 		// _.filter and .test
   205 		// _.filter() and .test().
   193 		results = this.filter( function( data ) {
   206 		results = this.filter( function( data ) {
   194 			name        = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
   207 			name        = data.get( 'name' ).replace( /(<([^>]+)>)/ig, '' );
   195 			description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
   208 			description = data.get( 'description' ).replace( /(<([^>]+)>)/ig, '' );
   196 			author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
   209 			author      = data.get( 'author' ).replace( /(<([^>]+)>)/ig, '' );
   197 
   210 
   212 
   225 
   213 		this.reset( results );
   226 		this.reset( results );
   214 	},
   227 	},
   215 
   228 
   216 	// Paginates the collection with a helper method
   229 	// Paginates the collection with a helper method
   217 	// that slices the collection
   230 	// that slices the collection.
   218 	paginate: function( instance ) {
   231 	paginate: function( instance ) {
   219 		var collection = this;
   232 		var collection = this;
   220 		instance = instance || 0;
   233 		instance = instance || 0;
   221 
   234 
   222 		// Themes per instance are set at 20
   235 		// Themes per instance are set at 20.
   223 		collection = _( collection.rest( 20 * instance ) );
   236 		collection = _( collection.rest( 20 * instance ) );
   224 		collection = _( collection.first( 20 ) );
   237 		collection = _( collection.first( 20 ) );
   225 
   238 
   226 		return collection;
   239 		return collection;
   227 	},
   240 	},
   228 
   241 
   229 	count: false,
   242 	count: false,
   230 
   243 
   231 	// Handles requests for more themes
   244 	/*
   232 	// and caches results
   245 	 * Handles requests for more themes and caches results.
   233 	//
   246 	 *
   234 	// When we are missing a cache object we fire an apiCall()
   247 	 *
   235 	// which triggers events of `query:success` or `query:fail`
   248 	 * When we are missing a cache object we fire an apiCall()
       
   249 	 * which triggers events of `query:success` or `query:fail`.
       
   250 	 */
   236 	query: function( request ) {
   251 	query: function( request ) {
   237 		/**
   252 		/**
   238 		 * @static
   253 		 * @static
   239 		 * @type Array
   254 		 * @type Array
   240 		 */
   255 		 */
   241 		var queries = this.queries,
   256 		var queries = this.queries,
   242 			self = this,
   257 			self = this,
   243 			query, isPaginated, count;
   258 			query, isPaginated, count;
   244 
   259 
   245 		// Store current query request args
   260 		// Store current query request args
   246 		// for later use with the event `theme:end`
   261 		// for later use with the event `theme:end`.
   247 		this.currentQuery.request = request;
   262 		this.currentQuery.request = request;
   248 
   263 
   249 		// Search the query cache for matches.
   264 		// Search the query cache for matches.
   250 		query = _.find( queries, function( query ) {
   265 		query = _.find( queries, function( query ) {
   251 			return _.isEqual( query.request, request );
   266 			return _.isEqual( query.request, request );
   253 
   268 
   254 		// If the request matches the stored currentQuery.request
   269 		// If the request matches the stored currentQuery.request
   255 		// it means we have a paginated request.
   270 		// it means we have a paginated request.
   256 		isPaginated = _.has( request, 'page' );
   271 		isPaginated = _.has( request, 'page' );
   257 
   272 
   258 		// Reset the internal api page counter for non paginated queries.
   273 		// Reset the internal api page counter for non-paginated queries.
   259 		if ( ! isPaginated ) {
   274 		if ( ! isPaginated ) {
   260 			this.currentQuery.page = 1;
   275 			this.currentQuery.page = 1;
   261 		}
   276 		}
   262 
   277 
   263 		// Otherwise, send a new API call and add it to the cache.
   278 		// Otherwise, send a new API call and add it to the cache.
   266 
   281 
   267 				// Update the collection with the queried data.
   282 				// Update the collection with the queried data.
   268 				if ( data.themes ) {
   283 				if ( data.themes ) {
   269 					self.reset( data.themes );
   284 					self.reset( data.themes );
   270 					count = data.info.results;
   285 					count = data.info.results;
   271 					// Store the results and the query request
   286 					// Store the results and the query request.
   272 					queries.push( { themes: data.themes, request: request, total: count } );
   287 					queries.push( { themes: data.themes, request: request, total: count } );
   273 				}
   288 				}
   274 
   289 
   275 				// Trigger a collection refresh event
   290 				// Trigger a collection refresh event
   276 				// and a `query:success` event with a `count` argument.
   291 				// and a `query:success` event with a `count` argument.
   286 			});
   301 			});
   287 		} else {
   302 		} else {
   288 			// If it's a paginated request we need to fetch more themes...
   303 			// If it's a paginated request we need to fetch more themes...
   289 			if ( isPaginated ) {
   304 			if ( isPaginated ) {
   290 				return this.apiCall( request, isPaginated ).done( function( data ) {
   305 				return this.apiCall( request, isPaginated ).done( function( data ) {
   291 					// Add the new themes to the current collection
   306 					// Add the new themes to the current collection.
   292 					// @todo update counter
   307 					// @todo Update counter.
   293 					self.add( data.themes );
   308 					self.add( data.themes );
   294 					self.trigger( 'query:success' );
   309 					self.trigger( 'query:success' );
   295 
   310 
   296 					// We are done loading themes for now.
   311 					// We are done loading themes for now.
   297 					self.loadingThemes = false;
   312 					self.loadingThemes = false;
   306 			} else {
   321 			} else {
   307 				$( 'body' ).removeClass( 'no-results' );
   322 				$( 'body' ).removeClass( 'no-results' );
   308 			}
   323 			}
   309 
   324 
   310 			// Only trigger an update event since we already have the themes
   325 			// Only trigger an update event since we already have the themes
   311 			// on our cached object
   326 			// on our cached object.
   312 			if ( _.isNumber( query.total ) ) {
   327 			if ( _.isNumber( query.total ) ) {
   313 				this.count = query.total;
   328 				this.count = query.total;
   314 			}
   329 			}
   315 
   330 
   316 			this.reset( query.themes );
   331 			this.reset( query.themes );
   321 			this.trigger( 'themes:update' );
   336 			this.trigger( 'themes:update' );
   322 			this.trigger( 'query:success', this.count );
   337 			this.trigger( 'query:success', this.count );
   323 		}
   338 		}
   324 	},
   339 	},
   325 
   340 
   326 	// Local cache array for API queries
   341 	// Local cache array for API queries.
   327 	queries: [],
   342 	queries: [],
   328 
   343 
   329 	// Keep track of current query so we can handle pagination
   344 	// Keep track of current query so we can handle pagination.
   330 	currentQuery: {
   345 	currentQuery: {
   331 		page: 1,
   346 		page: 1,
   332 		request: {}
   347 		request: {}
   333 	},
   348 	},
   334 
   349 
   335 	// Send request to api.wordpress.org/themes
   350 	// Send request to api.wordpress.org/themes.
   336 	apiCall: function( request, paginated ) {
   351 	apiCall: function( request, paginated ) {
   337 		return wp.ajax.send( 'query-themes', {
   352 		return wp.ajax.send( 'query-themes', {
   338 			data: {
   353 			data: {
   339 			// Request data
   354 				// Request data.
   340 				request: _.extend({
   355 				request: _.extend({
   341 					per_page: 100
   356 					per_page: 100
   342 				}, request)
   357 				}, request)
   343 			},
   358 			},
   344 
   359 
   345 			beforeSend: function() {
   360 			beforeSend: function() {
   346 				if ( ! paginated ) {
   361 				if ( ! paginated ) {
   347 					// Spin it
   362 					// Spin it.
   348 					$( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
   363 					$( 'body' ).addClass( 'loading-content' ).removeClass( 'no-results' );
   349 				}
   364 				}
   350 			}
   365 			}
   351 		});
   366 		});
   352 	},
   367 	},
   354 	// Static status controller for when we are loading themes.
   369 	// Static status controller for when we are loading themes.
   355 	loadingThemes: false
   370 	loadingThemes: false
   356 });
   371 });
   357 
   372 
   358 // This is the view that controls each theme item
   373 // This is the view that controls each theme item
   359 // that will be displayed on the screen
   374 // that will be displayed on the screen.
   360 themes.view.Theme = wp.Backbone.View.extend({
   375 themes.view.Theme = wp.Backbone.View.extend({
   361 
   376 
   362 	// Wrap theme data on a div.theme element
   377 	// Wrap theme data on a div.theme element.
   363 	className: 'theme',
   378 	className: 'theme',
   364 
   379 
   365 	// Reflects which theme view we have
   380 	// Reflects which theme view we have.
   366 	// 'grid' (default) or 'detail'
   381 	// 'grid' (default) or 'detail'.
   367 	state: 'grid',
   382 	state: 'grid',
   368 
   383 
   369 	// The HTML template for each element to be rendered
   384 	// The HTML template for each element to be rendered.
   370 	html: themes.template( 'theme' ),
   385 	html: themes.template( 'theme' ),
   371 
   386 
   372 	events: {
   387 	events: {
   373 		'click': themes.isInstall ? 'preview': 'expand',
   388 		'click': themes.isInstall ? 'preview': 'expand',
   374 		'keydown': themes.isInstall ? 'preview': 'expand',
   389 		'keydown': themes.isInstall ? 'preview': 'expand',
   386 	},
   401 	},
   387 
   402 
   388 	render: function() {
   403 	render: function() {
   389 		var data = this.model.toJSON();
   404 		var data = this.model.toJSON();
   390 
   405 
   391 		// Render themes using the html template
   406 		// Render themes using the html template.
   392 		this.$el.html( this.html( data ) ).attr({
   407 		this.$el.html( this.html( data ) ).attr({
   393 			tabindex: 0,
   408 			tabindex: 0,
   394 			'aria-describedby' : data.id + '-action ' + data.id + '-name',
   409 			'aria-describedby' : data.id + '-action ' + data.id + '-name',
   395 			'data-slug': data.id
   410 			'data-slug': data.id
   396 		});
   411 		});
   397 
   412 
   398 		// Renders active theme styles
   413 		// Renders active theme styles.
   399 		this.activeTheme();
   414 		this.activeTheme();
   400 
   415 
   401 		if ( this.model.get( 'displayAuthor' ) ) {
   416 		if ( this.model.get( 'displayAuthor' ) ) {
   402 			this.$el.addClass( 'display-author' );
   417 			this.$el.addClass( 'display-author' );
   403 		}
   418 		}
   404 	},
   419 	},
   405 
   420 
   406 	// Adds a class to the currently active theme
   421 	// Adds a class to the currently active theme
   407 	// and to the overlay in detailed view mode
   422 	// and to the overlay in detailed view mode.
   408 	activeTheme: function() {
   423 	activeTheme: function() {
   409 		if ( this.model.get( 'active' ) ) {
   424 		if ( this.model.get( 'active' ) ) {
   410 			this.$el.addClass( 'active' );
   425 			this.$el.addClass( 'active' );
   411 		}
   426 		}
   412 	},
   427 	},
   417 
   432 
   418 		$('.theme.focus').removeClass('focus');
   433 		$('.theme.focus').removeClass('focus');
   419 		$themeToFocus.addClass('focus');
   434 		$themeToFocus.addClass('focus');
   420 	},
   435 	},
   421 
   436 
   422 	// Single theme overlay screen
   437 	// Single theme overlay screen.
   423 	// It's shown when clicking a theme
   438 	// It's shown when clicking a theme.
   424 	expand: function( event ) {
   439 	expand: function( event ) {
   425 		var self = this;
   440 		var self = this;
   426 
   441 
   427 		event = event || window.event;
   442 		event = event || window.event;
   428 
   443 
   429 		// 'enter' and 'space' keys expand the details view when a theme is :focused
   444 		// 'Enter' and 'Space' keys expand the details view when a theme is :focused.
   430 		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
   445 		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
   431 			return;
   446 			return;
   432 		}
   447 		}
   433 
   448 
   434 		// Bail if the user scrolled on a touch device
   449 		// Bail if the user scrolled on a touch device.
   435 		if ( this.touchDrag === true ) {
   450 		if ( this.touchDrag === true ) {
   436 			return this.touchDrag = false;
   451 			return this.touchDrag = false;
   437 		}
   452 		}
   438 
   453 
   439 		// Prevent the modal from showing when the user clicks
   454 		// Prevent the modal from showing when the user clicks
   440 		// one of the direct action buttons
   455 		// one of the direct action buttons.
   441 		if ( $( event.target ).is( '.theme-actions a' ) ) {
   456 		if ( $( event.target ).is( '.theme-actions a' ) ) {
   442 			return;
   457 			return;
   443 		}
   458 		}
   444 
   459 
   445 		// Prevent the modal from showing when the user clicks one of the direct action buttons.
   460 		// Prevent the modal from showing when the user clicks one of the direct action buttons.
   446 		if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
   461 		if ( $( event.target ).is( '.theme-actions a, .update-message, .button-link, .notice-dismiss' ) ) {
   447 			return;
   462 			return;
   448 		}
   463 		}
   449 
   464 
   450 		// Set focused theme to current element
   465 		// Set focused theme to current element.
   451 		themes.focusedTheme = this.$el;
   466 		themes.focusedTheme = this.$el;
   452 
   467 
   453 		this.trigger( 'theme:expand', self.model.cid );
   468 		this.trigger( 'theme:expand', self.model.cid );
   454 	},
   469 	},
   455 
   470 
   461 		var self = this,
   476 		var self = this,
   462 			current, preview;
   477 			current, preview;
   463 
   478 
   464 		event = event || window.event;
   479 		event = event || window.event;
   465 
   480 
   466 		// Bail if the user scrolled on a touch device
   481 		// Bail if the user scrolled on a touch device.
   467 		if ( this.touchDrag === true ) {
   482 		if ( this.touchDrag === true ) {
   468 			return this.touchDrag = false;
   483 			return this.touchDrag = false;
   469 		}
   484 		}
   470 
   485 
   471 		// Allow direct link path to installing a theme.
   486 		// Allow direct link path to installing a theme.
   472 		if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
   487 		if ( $( event.target ).not( '.install-theme-preview' ).parents( '.theme-actions' ).length ) {
   473 			return;
   488 			return;
   474 		}
   489 		}
   475 
   490 
   476 		// 'enter' and 'space' keys expand the details view when a theme is :focused
   491 		// 'Enter' and 'Space' keys expand the details view when a theme is :focused.
   477 		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
   492 		if ( event.type === 'keydown' && ( event.which !== 13 && event.which !== 32 ) ) {
   478 			return;
   493 			return;
   479 		}
   494 		}
   480 
   495 
   481 		// pressing enter while focused on the buttons shouldn't open the preview
   496 		// Pressing Enter while focused on the buttons shouldn't open the preview.
   482 		if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
   497 		if ( event.type === 'keydown' && event.which !== 13 && $( ':focus' ).hasClass( 'button' ) ) {
   483 			return;
   498 			return;
   484 		}
   499 		}
   485 
   500 
   486 		event.preventDefault();
   501 		event.preventDefault();
   497 
   512 
   498 		// Render the view and append it.
   513 		// Render the view and append it.
   499 		preview.render();
   514 		preview.render();
   500 		this.setNavButtonsState();
   515 		this.setNavButtonsState();
   501 
   516 
   502 		// Hide previous/next navigation if there is only one theme
   517 		// Hide previous/next navigation if there is only one theme.
   503 		if ( this.model.collection.length === 1 ) {
   518 		if ( this.model.collection.length === 1 ) {
   504 			preview.$el.addClass( 'no-navigation' );
   519 			preview.$el.addClass( 'no-navigation' );
   505 		} else {
   520 		} else {
   506 			preview.$el.removeClass( 'no-navigation' );
   521 			preview.$el.removeClass( 'no-navigation' );
   507 		}
   522 		}
   508 
   523 
   509 		// Append preview
   524 		// Append preview.
   510 		$( 'div.wrap' ).append( preview.el );
   525 		$( 'div.wrap' ).append( preview.el );
   511 
   526 
   512 		// Listen to our preview object
   527 		// Listen to our preview object
   513 		// for `theme:next` and `theme:previous` events.
   528 		// for `theme:next` and `theme:previous` events.
   514 		this.listenTo( preview, 'theme:next', function() {
   529 		this.listenTo( preview, 'theme:next', function() {
   540 		.listenTo( preview, 'theme:previous', function() {
   555 		.listenTo( preview, 'theme:previous', function() {
   541 
   556 
   542 			// Keep track of current theme model.
   557 			// Keep track of current theme model.
   543 			current = self.model;
   558 			current = self.model;
   544 
   559 
   545 			// Bail early if we are at the beginning of the collection
   560 			// Bail early if we are at the beginning of the collection.
   546 			if ( self.model.collection.indexOf( self.current ) === 0 ) {
   561 			if ( self.model.collection.indexOf( self.current ) === 0 ) {
   547 				return;
   562 				return;
   548 			}
   563 			}
   549 
   564 
   550 			// If we have ventured away from current model update the current model position.
   565 			// If we have ventured away from current model update the current model position.
   572 			self.current = self.model;
   587 			self.current = self.model;
   573 		});
   588 		});
   574 
   589 
   575 	},
   590 	},
   576 
   591 
   577 	// Handles .disabled classes for previous/next buttons in theme installer preview
   592 	// Handles .disabled classes for previous/next buttons in theme installer preview.
   578 	setNavButtonsState: function() {
   593 	setNavButtonsState: function() {
   579 		var $themeInstaller = $( '.theme-install-overlay' ),
   594 		var $themeInstaller = $( '.theme-install-overlay' ),
   580 			current = _.isUndefined( this.current ) ? this.model : this.current,
   595 			current = _.isUndefined( this.current ) ? this.model : this.current,
   581 			previousThemeButton = $themeInstaller.find( '.previous-theme' ),
   596 			previousThemeButton = $themeInstaller.find( '.previous-theme' ),
   582 			nextThemeButton = $themeInstaller.find( '.next-theme' );
   597 			nextThemeButton = $themeInstaller.find( '.next-theme' );
   583 
   598 
   584 		// Disable previous at the zero position
   599 		// Disable previous at the zero position.
   585 		if ( 0 === this.model.collection.indexOf( current ) ) {
   600 		if ( 0 === this.model.collection.indexOf( current ) ) {
   586 			previousThemeButton
   601 			previousThemeButton
   587 				.addClass( 'disabled' )
   602 				.addClass( 'disabled' )
   588 				.prop( 'disabled', true );
   603 				.prop( 'disabled', true );
   589 
   604 
   590 			nextThemeButton.focus();
   605 			nextThemeButton.focus();
   591 		}
   606 		}
   592 
   607 
   593 		// Disable next if the next model is undefined
   608 		// Disable next if the next model is undefined.
   594 		if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
   609 		if ( _.isUndefined( this.model.collection.at( this.model.collection.indexOf( current ) + 1 ) ) ) {
   595 			nextThemeButton
   610 			nextThemeButton
   596 				.addClass( 'disabled' )
   611 				.addClass( 'disabled' )
   597 				.prop( 'disabled', true );
   612 				.prop( 'disabled', true );
   598 
   613 
   644 			slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
   659 			slug: $( event.target ).parents( 'div.theme' ).first().data( 'slug' )
   645 		} );
   660 		} );
   646 	}
   661 	}
   647 });
   662 });
   648 
   663 
   649 // Theme Details view
   664 // Theme Details view.
   650 // Set ups a modal overlay with the expanded theme data
   665 // Sets up a modal overlay with the expanded theme data.
   651 themes.view.Details = wp.Backbone.View.extend({
   666 themes.view.Details = wp.Backbone.View.extend({
   652 
   667 
   653 	// Wrap theme data on a div.theme element
   668 	// Wrap theme data on a div.theme element.
   654 	className: 'theme-overlay',
   669 	className: 'theme-overlay',
   655 
   670 
   656 	events: {
   671 	events: {
   657 		'click': 'collapse',
   672 		'click': 'collapse',
   658 		'click .delete-theme': 'deleteTheme',
   673 		'click .delete-theme': 'deleteTheme',
   659 		'click .left': 'previousTheme',
   674 		'click .left': 'previousTheme',
   660 		'click .right': 'nextTheme',
   675 		'click .right': 'nextTheme',
   661 		'click #update-theme': 'updateTheme'
   676 		'click #update-theme': 'updateTheme',
   662 	},
   677 		'click .toggle-auto-update': 'autoupdateState'
   663 
   678 	},
   664 	// The HTML template for the theme overlay
   679 
       
   680 	// The HTML template for the theme overlay.
   665 	html: themes.template( 'theme-single' ),
   681 	html: themes.template( 'theme-single' ),
   666 
   682 
   667 	render: function() {
   683 	render: function() {
   668 		var data = this.model.toJSON();
   684 		var data = this.model.toJSON();
   669 		this.$el.html( this.html( data ) );
   685 		this.$el.html( this.html( data ) );
   670 		// Renders active theme styles
   686 		// Renders active theme styles.
   671 		this.activeTheme();
   687 		this.activeTheme();
   672 		// Set up navigation events
   688 		// Set up navigation events.
   673 		this.navigation();
   689 		this.navigation();
   674 		// Checks screenshot size
   690 		// Checks screenshot size.
   675 		this.screenshotCheck( this.$el );
   691 		this.screenshotCheck( this.$el );
   676 		// Contain "tabbing" inside the overlay
   692 		// Contain "tabbing" inside the overlay.
   677 		this.containFocus( this.$el );
   693 		this.containFocus( this.$el );
   678 	},
   694 	},
   679 
   695 
   680 	// Adds a class to the currently active theme
   696 	// Adds a class to the currently active theme
   681 	// and to the overlay in detailed view mode
   697 	// and to the overlay in detailed view mode.
   682 	activeTheme: function() {
   698 	activeTheme: function() {
   683 		// Check the model has the active property
   699 		// Check the model has the active property.
   684 		this.$el.toggleClass( 'active', this.model.get( 'active' ) );
   700 		this.$el.toggleClass( 'active', this.model.get( 'active' ) );
   685 	},
   701 	},
   686 
   702 
   687 	// Set initial focus and constrain tabbing within the theme browser modal.
   703 	// Set initial focus and constrain tabbing within the theme browser modal.
   688 	containFocus: function( $el ) {
   704 	containFocus: function( $el ) {
   708 				}
   724 				}
   709 			}
   725 			}
   710 		});
   726 		});
   711 	},
   727 	},
   712 
   728 
   713 	// Single theme overlay screen
   729 	// Single theme overlay screen.
   714 	// It's shown when clicking a theme
   730 	// It's shown when clicking a theme.
   715 	collapse: function( event ) {
   731 	collapse: function( event ) {
   716 		var self = this,
   732 		var self = this,
   717 			scroll;
   733 			scroll;
   718 
   734 
   719 		event = event || window.event;
   735 		event = event || window.event;
   720 
   736 
   721 		// Prevent collapsing detailed view when there is only one theme available
   737 		// Prevent collapsing detailed view when there is only one theme available.
   722 		if ( themes.data.themes.length === 1 ) {
   738 		if ( themes.data.themes.length === 1 ) {
   723 			return;
   739 			return;
   724 		}
   740 		}
   725 
   741 
   726 		// Detect if the click is inside the overlay
   742 		// Detect if the click is inside the overlay and don't close it
   727 		// and don't close it unless the target was
   743 		// unless the target was the div.back button.
   728 		// the div.back button
       
   729 		if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
   744 		if ( $( event.target ).is( '.theme-backdrop' ) || $( event.target ).is( '.close' ) || event.keyCode === 27 ) {
   730 
   745 
   731 			// Add a temporary closing class while overlay fades out
   746 			// Add a temporary closing class while overlay fades out.
   732 			$( 'body' ).addClass( 'closing-overlay' );
   747 			$( 'body' ).addClass( 'closing-overlay' );
   733 
   748 
   734 			// With a quick fade out animation
   749 			// With a quick fade out animation.
   735 			this.$el.fadeOut( 130, function() {
   750 			this.$el.fadeOut( 130, function() {
   736 				// Clicking outside the modal box closes the overlay
   751 				// Clicking outside the modal box closes the overlay.
   737 				$( 'body' ).removeClass( 'closing-overlay' );
   752 				$( 'body' ).removeClass( 'closing-overlay' );
   738 				// Handle event cleanup
   753 				// Handle event cleanup.
   739 				self.closeOverlay();
   754 				self.closeOverlay();
   740 
   755 
   741 				// Get scroll position to avoid jumping to the top
   756 				// Get scroll position to avoid jumping to the top.
   742 				scroll = document.body.scrollTop;
   757 				scroll = document.body.scrollTop;
   743 
   758 
   744 				// Clean the url structure
   759 				// Clean the URL structure.
   745 				themes.router.navigate( themes.router.baseUrl( '' ) );
   760 				themes.router.navigate( themes.router.baseUrl( '' ) );
   746 
   761 
   747 				// Restore scroll position
   762 				// Restore scroll position.
   748 				document.body.scrollTop = scroll;
   763 				document.body.scrollTop = scroll;
   749 
   764 
   750 				// Return focus to the theme div
   765 				// Return focus to the theme div.
   751 				if ( themes.focusedTheme ) {
   766 				if ( themes.focusedTheme ) {
   752 					themes.focusedTheme.focus();
   767 					themes.focusedTheme.focus();
   753 				}
   768 				}
   754 			});
   769 			});
   755 		}
   770 		}
   756 	},
   771 	},
   757 
   772 
   758 	// Handles .disabled classes for next/previous buttons
   773 	// Handles .disabled classes for next/previous buttons.
   759 	navigation: function() {
   774 	navigation: function() {
   760 
   775 
   761 		// Disable Left/Right when at the start or end of the collection
   776 		// Disable Left/Right when at the start or end of the collection.
   762 		if ( this.model.cid === this.model.collection.at(0).cid ) {
   777 		if ( this.model.cid === this.model.collection.at(0).cid ) {
   763 			this.$el.find( '.left' )
   778 			this.$el.find( '.left' )
   764 				.addClass( 'disabled' )
   779 				.addClass( 'disabled' )
   765 				.prop( 'disabled', true );
   780 				.prop( 'disabled', true );
   766 		}
   781 		}
   770 				.prop( 'disabled', true );
   785 				.prop( 'disabled', true );
   771 		}
   786 		}
   772 	},
   787 	},
   773 
   788 
   774 	// Performs the actions to effectively close
   789 	// Performs the actions to effectively close
   775 	// the theme details overlay
   790 	// the theme details overlay.
   776 	closeOverlay: function() {
   791 	closeOverlay: function() {
   777 		$( 'body' ).removeClass( 'modal-open' );
   792 		$( 'body' ).removeClass( 'modal-open' );
   778 		this.remove();
   793 		this.remove();
   779 		this.unbind();
   794 		this.unbind();
   780 		this.trigger( 'theme:collapse' );
   795 		this.trigger( 'theme:collapse' );
       
   796 	},
       
   797 
       
   798 	// Set state of the auto-update settings link after it has been changed and saved.
       
   799 	autoupdateState: function() {
       
   800 		var callback,
       
   801 			_this = this;
       
   802 
       
   803 		// Support concurrent clicks in different Theme Details overlays.
       
   804 		callback = function( event, data ) {
       
   805 			var autoupdate;
       
   806 			if ( _this.model.get( 'id' ) === data.asset ) {
       
   807 				autoupdate = _this.model.get( 'autoupdate' );
       
   808 				autoupdate.enabled = 'enable' === data.state;
       
   809 				_this.model.set( { autoupdate: autoupdate } );
       
   810 				$( document ).off( 'wp-auto-update-setting-changed', callback );
       
   811 			}
       
   812 		};
       
   813 
       
   814 		// Triggered in updates.js
       
   815 		$( document ).on( 'wp-auto-update-setting-changed', callback );
   781 	},
   816 	},
   782 
   817 
   783 	updateTheme: function( event ) {
   818 	updateTheme: function( event ) {
   784 		var _this = this;
   819 		var _this = this;
   785 		event.preventDefault();
   820 		event.preventDefault();
   843 		self.trigger( 'theme:previous', self.model.cid );
   878 		self.trigger( 'theme:previous', self.model.cid );
   844 		return false;
   879 		return false;
   845 	},
   880 	},
   846 
   881 
   847 	// Checks if the theme screenshot is the old 300px width version
   882 	// Checks if the theme screenshot is the old 300px width version
   848 	// and adds a corresponding class if it's true
   883 	// and adds a corresponding class if it's true.
   849 	screenshotCheck: function( el ) {
   884 	screenshotCheck: function( el ) {
   850 		var screenshot, image;
   885 		var screenshot, image;
   851 
   886 
   852 		screenshot = el.find( '.screenshot img' );
   887 		screenshot = el.find( '.screenshot img' );
   853 		image = new Image();
   888 		image = new Image();
   854 		image.src = screenshot.attr( 'src' );
   889 		image.src = screenshot.attr( 'src' );
   855 
   890 
   856 		// Width check
   891 		// Width check.
   857 		if ( image.width && image.width <= 300 ) {
   892 		if ( image.width && image.width <= 300 ) {
   858 			el.addClass( 'small-screenshot' );
   893 			el.addClass( 'small-screenshot' );
   859 		}
   894 		}
   860 	}
   895 	}
   861 });
   896 });
   862 
   897 
   863 // Theme Preview view
   898 // Theme Preview view.
   864 // Set ups a modal overlay with the expanded theme data
   899 // Sets up a modal overlay with the expanded theme data.
   865 themes.view.Preview = themes.view.Details.extend({
   900 themes.view.Preview = themes.view.Details.extend({
   866 
   901 
   867 	className: 'wp-full-overlay expanded',
   902 	className: 'wp-full-overlay expanded',
   868 	el: '.theme-install-overlay',
   903 	el: '.theme-install-overlay',
   869 
   904 
   875 		'click .next-theme': 'nextTheme',
   910 		'click .next-theme': 'nextTheme',
   876 		'keyup': 'keyEvent',
   911 		'keyup': 'keyEvent',
   877 		'click .theme-install': 'installTheme'
   912 		'click .theme-install': 'installTheme'
   878 	},
   913 	},
   879 
   914 
   880 	// The HTML template for the theme preview
   915 	// The HTML template for the theme preview.
   881 	html: themes.template( 'theme-preview' ),
   916 	html: themes.template( 'theme-preview' ),
   882 
   917 
   883 	render: function() {
   918 	render: function() {
   884 		var self = this,
   919 		var self = this,
   885 			currentPreviewDevice,
   920 			currentPreviewDevice,
   913 
   948 
   914 	close: function() {
   949 	close: function() {
   915 		this.$el.fadeOut( 200, function() {
   950 		this.$el.fadeOut( 200, function() {
   916 			$( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
   951 			$( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
   917 
   952 
   918 			// Return focus to the theme div
   953 			// Return focus to the theme div.
   919 			if ( themes.focusedTheme ) {
   954 			if ( themes.focusedTheme ) {
   920 				themes.focusedTheme.focus();
   955 				themes.focusedTheme.focus();
   921 			}
   956 			}
   922 		}).removeClass( 'iframe-ready' );
   957 		}).removeClass( 'iframe-ready' );
   923 
   958 
   968 			.addClass( 'active' )
  1003 			.addClass( 'active' )
   969 			.attr( 'aria-pressed', true );
  1004 			.attr( 'aria-pressed', true );
   970 	},
  1005 	},
   971 
  1006 
   972 	keyEvent: function( event ) {
  1007 	keyEvent: function( event ) {
   973 		// The escape key closes the preview
  1008 		// The escape key closes the preview.
   974 		if ( event.keyCode === 27 ) {
  1009 		if ( event.keyCode === 27 ) {
   975 			this.undelegateEvents();
  1010 			this.undelegateEvents();
   976 			this.close();
  1011 			this.close();
   977 		}
  1012 		}
   978 		// The right arrow key, next theme
  1013 		// The right arrow key, next theme.
   979 		if ( event.keyCode === 39 ) {
  1014 		if ( event.keyCode === 39 ) {
   980 			_.once( this.nextTheme() );
  1015 			_.once( this.nextTheme() );
   981 		}
  1016 		}
   982 
  1017 
   983 		// The left arrow key, previous theme
  1018 		// The left arrow key, previous theme.
   984 		if ( event.keyCode === 37 ) {
  1019 		if ( event.keyCode === 37 ) {
   985 			this.previousTheme();
  1020 			this.previousTheme();
   986 		}
  1021 		}
   987 	},
  1022 	},
   988 
  1023 
  1006 		} );
  1041 		} );
  1007 	}
  1042 	}
  1008 });
  1043 });
  1009 
  1044 
  1010 // Controls the rendering of div.themes,
  1045 // Controls the rendering of div.themes,
  1011 // a wrapper that will hold all the theme elements
  1046 // a wrapper that will hold all the theme elements.
  1012 themes.view.Themes = wp.Backbone.View.extend({
  1047 themes.view.Themes = wp.Backbone.View.extend({
  1013 
  1048 
  1014 	className: 'themes wp-clearfix',
  1049 	className: 'themes wp-clearfix',
  1015 	$overlay: $( 'div.theme-overlay' ),
  1050 	$overlay: $( 'div.theme-overlay' ),
  1016 
  1051 
  1017 	// Number to keep track of scroll position
  1052 	// Number to keep track of scroll position
  1018 	// while in theme-overlay mode
  1053 	// while in theme-overlay mode.
  1019 	index: 0,
  1054 	index: 0,
  1020 
  1055 
  1021 	// The theme count element
  1056 	// The theme count element.
  1022 	count: $( '.wrap .theme-count' ),
  1057 	count: $( '.wrap .theme-count' ),
  1023 
  1058 
  1024 	// The live themes count
  1059 	// The live themes count.
  1025 	liveThemeCount: 0,
  1060 	liveThemeCount: 0,
  1026 
  1061 
  1027 	initialize: function( options ) {
  1062 	initialize: function( options ) {
  1028 		var self = this;
  1063 		var self = this;
  1029 
  1064 
  1030 		// Set up parent
  1065 		// Set up parent.
  1031 		this.parent = options.parent;
  1066 		this.parent = options.parent;
  1032 
  1067 
  1033 		// Set current view to [grid]
  1068 		// Set current view to [grid].
  1034 		this.setView( 'grid' );
  1069 		this.setView( 'grid' );
  1035 
  1070 
  1036 		// Move the active theme to the beginning of the collection
  1071 		// Move the active theme to the beginning of the collection.
  1037 		self.currentTheme();
  1072 		self.currentTheme();
  1038 
  1073 
  1039 		// When the collection is updated by user input...
  1074 		// When the collection is updated by user input...
  1040 		this.listenTo( self.collection, 'themes:update', function() {
  1075 		this.listenTo( self.collection, 'themes:update', function() {
  1041 			self.parent.page = 0;
  1076 			self.parent.page = 0;
  1077 			// Bail if the filesystem credentials dialog is shown.
  1112 			// Bail if the filesystem credentials dialog is shown.
  1078 			if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
  1113 			if ( $( '#request-filesystem-credentials-dialog' ).is( ':visible' ) ) {
  1079 				return;
  1114 				return;
  1080 			}
  1115 			}
  1081 
  1116 
  1082 			// Pressing the right arrow key fires a theme:next event
  1117 			// Pressing the right arrow key fires a theme:next event.
  1083 			if ( event.keyCode === 39 ) {
  1118 			if ( event.keyCode === 39 ) {
  1084 				self.overlay.nextTheme();
  1119 				self.overlay.nextTheme();
  1085 			}
  1120 			}
  1086 
  1121 
  1087 			// Pressing the left arrow key fires a theme:previous event
  1122 			// Pressing the left arrow key fires a theme:previous event.
  1088 			if ( event.keyCode === 37 ) {
  1123 			if ( event.keyCode === 37 ) {
  1089 				self.overlay.previousTheme();
  1124 				self.overlay.previousTheme();
  1090 			}
  1125 			}
  1091 
  1126 
  1092 			// Pressing the escape key fires a theme:collapse event
  1127 			// Pressing the escape key fires a theme:collapse event.
  1093 			if ( event.keyCode === 27 ) {
  1128 			if ( event.keyCode === 27 ) {
  1094 				self.overlay.collapse( event );
  1129 				self.overlay.collapse( event );
  1095 			}
  1130 			}
  1096 		});
  1131 		});
  1097 	},
  1132 	},
  1098 
  1133 
  1099 	// Manages rendering of theme pages
  1134 	// Manages rendering of theme pages
  1100 	// and keeping theme count in sync
  1135 	// and keeping theme count in sync.
  1101 	render: function() {
  1136 	render: function() {
  1102 		// Clear the DOM, please
  1137 		// Clear the DOM, please.
  1103 		this.$el.empty();
  1138 		this.$el.empty();
  1104 
  1139 
  1105 		// If the user doesn't have switch capabilities
  1140 		// If the user doesn't have switch capabilities or there is only one theme
  1106 		// or there is only one theme in the collection
  1141 		// in the collection, render the detailed view of the active theme.
  1107 		// render the detailed view of the active theme
       
  1108 		if ( themes.data.themes.length === 1 ) {
  1142 		if ( themes.data.themes.length === 1 ) {
  1109 
  1143 
  1110 			// Constructs the view
  1144 			// Constructs the view.
  1111 			this.singleTheme = new themes.view.Details({
  1145 			this.singleTheme = new themes.view.Details({
  1112 				model: this.collection.models[0]
  1146 				model: this.collection.models[0]
  1113 			});
  1147 			});
  1114 
  1148 
  1115 			// Render and apply a 'single-theme' class to our container
  1149 			// Render and apply a 'single-theme' class to our container.
  1116 			this.singleTheme.render();
  1150 			this.singleTheme.render();
  1117 			this.$el.addClass( 'single-theme' );
  1151 			this.$el.addClass( 'single-theme' );
  1118 			this.$el.append( this.singleTheme.el );
  1152 			this.$el.append( this.singleTheme.el );
  1119 		}
  1153 		}
  1120 
  1154 
  1121 		// Generate the themes
  1155 		// Generate the themes using page instance
  1122 		// Using page instance
  1156 		// while checking the collection has items.
  1123 		// While checking the collection has items
       
  1124 		if ( this.options.collection.size() > 0 ) {
  1157 		if ( this.options.collection.size() > 0 ) {
  1125 			this.renderThemes( this.parent.page );
  1158 			this.renderThemes( this.parent.page );
  1126 		}
  1159 		}
  1127 
  1160 
  1128 		// Display a live theme count for the collection
  1161 		// Display a live theme count for the collection.
  1129 		this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
  1162 		this.liveThemeCount = this.collection.count ? this.collection.count : this.collection.length;
  1130 		this.count.text( this.liveThemeCount );
  1163 		this.count.text( this.liveThemeCount );
  1131 
  1164 
  1132 		/*
  1165 		/*
  1133 		 * In the theme installer the themes count is already announced
  1166 		 * In the theme installer the themes count is already announced
  1137 			this.announceSearchResults( this.liveThemeCount );
  1170 			this.announceSearchResults( this.liveThemeCount );
  1138 		}
  1171 		}
  1139 	},
  1172 	},
  1140 
  1173 
  1141 	// Iterates through each instance of the collection
  1174 	// Iterates through each instance of the collection
  1142 	// and renders each theme module
  1175 	// and renders each theme module.
  1143 	renderThemes: function( page ) {
  1176 	renderThemes: function( page ) {
  1144 		var self = this;
  1177 		var self = this;
  1145 
  1178 
  1146 		self.instance = self.collection.paginate( page );
  1179 		self.instance = self.collection.paginate( page );
  1147 
  1180 
  1148 		// If we have no more themes bail
  1181 		// If we have no more themes, bail.
  1149 		if ( self.instance.size() === 0 ) {
  1182 		if ( self.instance.size() === 0 ) {
  1150 			// Fire a no-more-themes event.
  1183 			// Fire a no-more-themes event.
  1151 			this.parent.trigger( 'theme:end' );
  1184 			this.parent.trigger( 'theme:end' );
  1152 			return;
  1185 			return;
  1153 		}
  1186 		}
  1154 
  1187 
  1155 		// Make sure the add-new stays at the end
  1188 		// Make sure the add-new stays at the end.
  1156 		if ( ! themes.isInstall && page >= 1 ) {
  1189 		if ( ! themes.isInstall && page >= 1 ) {
  1157 			$( '.add-new-theme' ).remove();
  1190 			$( '.add-new-theme' ).remove();
  1158 		}
  1191 		}
  1159 
  1192 
  1160 		// Loop through the themes and setup each theme view
  1193 		// Loop through the themes and setup each theme view.
  1161 		self.instance.each( function( theme ) {
  1194 		self.instance.each( function( theme ) {
  1162 			self.theme = new themes.view.Theme({
  1195 			self.theme = new themes.view.Theme({
  1163 				model: theme,
  1196 				model: theme,
  1164 				parent: self
  1197 				parent: self
  1165 			});
  1198 			});
  1166 
  1199 
  1167 			// Render the views...
  1200 			// Render the views...
  1168 			self.theme.render();
  1201 			self.theme.render();
  1169 			// and append them to div.themes
  1202 			// ...and append them to div.themes.
  1170 			self.$el.append( self.theme.el );
  1203 			self.$el.append( self.theme.el );
  1171 
  1204 
  1172 			// Binds to theme:expand to show the modal box
  1205 			// Binds to theme:expand to show the modal box
  1173 			// with the theme details
  1206 			// with the theme details.
  1174 			self.listenTo( self.theme, 'theme:expand', self.expand, self );
  1207 			self.listenTo( self.theme, 'theme:expand', self.expand, self );
  1175 		});
  1208 		});
  1176 
  1209 
  1177 		// 'Add new theme' element shown at the end of the grid
  1210 		// 'Add new theme' element shown at the end of the grid.
  1178 		if ( ! themes.isInstall && themes.data.settings.canInstall ) {
  1211 		if ( ! themes.isInstall && themes.data.settings.canInstall ) {
  1179 			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>' );
  1212 			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>' );
  1180 		}
  1213 		}
  1181 
  1214 
  1182 		this.parent.page++;
  1215 		this.parent.page++;
  1183 	},
  1216 	},
  1184 
  1217 
  1185 	// Grabs current theme and puts it at the beginning of the collection
  1218 	// Grabs current theme and puts it at the beginning of the collection.
  1186 	currentTheme: function() {
  1219 	currentTheme: function() {
  1187 		var self = this,
  1220 		var self = this,
  1188 			current;
  1221 			current;
  1189 
  1222 
  1190 		current = self.collection.findWhere({ active: true });
  1223 		current = self.collection.findWhere({ active: true });
  1191 
  1224 
  1192 		// Move the active theme to the beginning of the collection
  1225 		// Move the active theme to the beginning of the collection.
  1193 		if ( current ) {
  1226 		if ( current ) {
  1194 			self.collection.remove( current );
  1227 			self.collection.remove( current );
  1195 			self.collection.add( current, { at:0 } );
  1228 			self.collection.add( current, { at:0 } );
  1196 		}
  1229 		}
  1197 	},
  1230 	},
  1198 
  1231 
  1199 	// Sets current view
  1232 	// Sets current view.
  1200 	setView: function( view ) {
  1233 	setView: function( view ) {
  1201 		return view;
  1234 		return view;
  1202 	},
  1235 	},
  1203 
  1236 
  1204 	// Renders the overlay with the ThemeDetails view
  1237 	// Renders the overlay with the ThemeDetails view.
  1205 	// Uses the current model data
  1238 	// Uses the current model data.
  1206 	expand: function( id ) {
  1239 	expand: function( id ) {
  1207 		var self = this, $card, $modal;
  1240 		var self = this, $card, $modal;
  1208 
  1241 
  1209 		// Set the current theme model
  1242 		// Set the current theme model.
  1210 		this.model = self.collection.get( id );
  1243 		this.model = self.collection.get( id );
  1211 
  1244 
  1212 		// Trigger a route update for the current model
  1245 		// Trigger a route update for the current model.
  1213 		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
  1246 		themes.router.navigate( themes.router.baseUrl( themes.router.themePath + this.model.id ) );
  1214 
  1247 
  1215 		// Sets this.view to 'detail'
  1248 		// Sets this.view to 'detail'.
  1216 		this.setView( 'detail' );
  1249 		this.setView( 'detail' );
  1217 		$( 'body' ).addClass( 'modal-open' );
  1250 		$( 'body' ).addClass( 'modal-open' );
  1218 
  1251 
  1219 		// Set up the theme details view
  1252 		// Set up the theme details view.
  1220 		this.overlay = new themes.view.Details({
  1253 		this.overlay = new themes.view.Details({
  1221 			model: self.model
  1254 			model: self.model
  1222 		});
  1255 		});
  1223 
  1256 
  1224 		this.overlay.render();
  1257 		this.overlay.render();
  1238 			}
  1271 			}
  1239 		}
  1272 		}
  1240 
  1273 
  1241 		this.$overlay.html( this.overlay.el );
  1274 		this.$overlay.html( this.overlay.el );
  1242 
  1275 
  1243 		// Bind to theme:next and theme:previous
  1276 		// Bind to theme:next and theme:previous triggered by the arrow keys.
  1244 		// triggered by the arrow keys
  1277 		// Keep track of the current model so we can infer an index position.
  1245 		//
       
  1246 		// Keep track of the current model so we
       
  1247 		// can infer an index position
       
  1248 		this.listenTo( this.overlay, 'theme:next', function() {
  1278 		this.listenTo( this.overlay, 'theme:next', function() {
  1249 			// Renders the next theme on the overlay
  1279 			// Renders the next theme on the overlay.
  1250 			self.next( [ self.model.cid ] );
  1280 			self.next( [ self.model.cid ] );
  1251 
  1281 
  1252 		})
  1282 		})
  1253 		.listenTo( this.overlay, 'theme:previous', function() {
  1283 		.listenTo( this.overlay, 'theme:previous', function() {
  1254 			// Renders the previous theme on the overlay
  1284 			// Renders the previous theme on the overlay.
  1255 			self.previous( [ self.model.cid ] );
  1285 			self.previous( [ self.model.cid ] );
  1256 		});
  1286 		});
  1257 	},
  1287 	},
  1258 
  1288 
  1259 	// This method renders the next theme on the overlay modal
  1289 	/*
  1260 	// based on the current position in the collection
  1290 	 * This method renders the next theme on the overlay modal
  1261 	// @params [model cid]
  1291 	 * based on the current position in the collection.
       
  1292 	 *
       
  1293 	 * @params [model cid]
       
  1294 	 */
  1262 	next: function( args ) {
  1295 	next: function( args ) {
  1263 		var self = this,
  1296 		var self = this,
  1264 			model, nextModel;
  1297 			model, nextModel;
  1265 
  1298 
  1266 		// Get the current theme
  1299 		// Get the current theme.
  1267 		model = self.collection.get( args[0] );
  1300 		model = self.collection.get( args[0] );
  1268 		// Find the next model within the collection
  1301 		// Find the next model within the collection.
  1269 		nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
  1302 		nextModel = self.collection.at( self.collection.indexOf( model ) + 1 );
  1270 
  1303 
  1271 		// Sanity check which also serves as a boundary test
  1304 		// Sanity check which also serves as a boundary test.
  1272 		if ( nextModel !== undefined ) {
  1305 		if ( nextModel !== undefined ) {
  1273 
  1306 
  1274 			// We have a new theme...
  1307 			// We have a new theme...
  1275 			// Close the overlay
  1308 			// Close the overlay.
  1276 			this.overlay.closeOverlay();
  1309 			this.overlay.closeOverlay();
  1277 
  1310 
  1278 			// Trigger a route update for the current model
  1311 			// Trigger a route update for the current model.
  1279 			self.theme.trigger( 'theme:expand', nextModel.cid );
  1312 			self.theme.trigger( 'theme:expand', nextModel.cid );
  1280 
  1313 
  1281 		}
  1314 		}
  1282 	},
  1315 	},
  1283 
  1316 
  1284 	// This method renders the previous theme on the overlay modal
  1317 	/*
  1285 	// based on the current position in the collection
  1318 	 * This method renders the previous theme on the overlay modal
  1286 	// @params [model cid]
  1319 	 * based on the current position in the collection.
       
  1320 	 *
       
  1321 	 * @params [model cid]
       
  1322 	 */
  1287 	previous: function( args ) {
  1323 	previous: function( args ) {
  1288 		var self = this,
  1324 		var self = this,
  1289 			model, previousModel;
  1325 			model, previousModel;
  1290 
  1326 
  1291 		// Get the current theme
  1327 		// Get the current theme.
  1292 		model = self.collection.get( args[0] );
  1328 		model = self.collection.get( args[0] );
  1293 		// Find the previous model within the collection
  1329 		// Find the previous model within the collection.
  1294 		previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
  1330 		previousModel = self.collection.at( self.collection.indexOf( model ) - 1 );
  1295 
  1331 
  1296 		if ( previousModel !== undefined ) {
  1332 		if ( previousModel !== undefined ) {
  1297 
  1333 
  1298 			// We have a new theme...
  1334 			// We have a new theme...
  1299 			// Close the overlay
  1335 			// Close the overlay.
  1300 			this.overlay.closeOverlay();
  1336 			this.overlay.closeOverlay();
  1301 
  1337 
  1302 			// Trigger a route update for the current model
  1338 			// Trigger a route update for the current model.
  1303 			self.theme.trigger( 'theme:expand', previousModel.cid );
  1339 			self.theme.trigger( 'theme:expand', previousModel.cid );
  1304 
  1340 
  1305 		}
  1341 		}
  1306 	},
  1342 	},
  1307 
  1343 
  1308 	// Dispatch audible search results feedback message
  1344 	// Dispatch audible search results feedback message.
  1309 	announceSearchResults: function( count ) {
  1345 	announceSearchResults: function( count ) {
  1310 		if ( 0 === count ) {
  1346 		if ( 0 === count ) {
  1311 			wp.a11y.speak( l10n.noThemesFound );
  1347 			wp.a11y.speak( l10n.noThemesFound );
  1312 		} else {
  1348 		} else {
  1313 			wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
  1349 			wp.a11y.speak( l10n.themesFound.replace( '%d', count ) );
  1359 	doSearch: function( event ) {
  1395 	doSearch: function( event ) {
  1360 		var options = {};
  1396 		var options = {};
  1361 
  1397 
  1362 		this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );
  1398 		this.collection.doSearch( event.target.value.replace( /\+/g, ' ' ) );
  1363 
  1399 
  1364 		// if search is initiated and key is not return
  1400 		// if search is initiated and key is not return.
  1365 		if ( this.searching && event.which !== 13 ) {
  1401 		if ( this.searching && event.which !== 13 ) {
  1366 			options.replace = true;
  1402 			options.replace = true;
  1367 		} else {
  1403 		} else {
  1368 			this.searching = true;
  1404 			this.searching = true;
  1369 		}
  1405 		}
  1370 
  1406 
  1371 		// Update the URL hash
  1407 		// Update the URL hash.
  1372 		if ( event.target.value ) {
  1408 		if ( event.target.value ) {
  1373 			themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
  1409 			themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + event.target.value ), options );
  1374 		} else {
  1410 		} else {
  1375 			themes.router.navigate( themes.router.baseUrl( '' ) );
  1411 			themes.router.navigate( themes.router.baseUrl( '' ) );
  1376 		}
  1412 		}
  1393  * Navigate router.
  1429  * Navigate router.
  1394  *
  1430  *
  1395  * @since 4.9.0
  1431  * @since 4.9.0
  1396  *
  1432  *
  1397  * @param {string} url - URL to navigate to.
  1433  * @param {string} url - URL to navigate to.
  1398  * @param {object} state - State.
  1434  * @param {Object} state - State.
  1399  * @returns {void}
  1435  * @return {void}
  1400  */
  1436  */
  1401 function navigateRouter( url, state ) {
  1437 function navigateRouter( url, state ) {
  1402 	var router = this;
  1438 	var router = this;
  1403 	if ( Backbone.history._hasPushState ) {
  1439 	if ( Backbone.history._hasPushState ) {
  1404 		Backbone.Router.prototype.navigate.call( router, url, state );
  1440 		Backbone.Router.prototype.navigate.call( router, url, state );
  1405 	}
  1441 	}
  1406 }
  1442 }
  1407 
  1443 
  1408 // Sets up the routes events for relevant url queries
  1444 // Sets up the routes events for relevant url queries.
  1409 // Listens to [theme] and [search] params
  1445 // Listens to [theme] and [search] params.
  1410 themes.Router = Backbone.Router.extend({
  1446 themes.Router = Backbone.Router.extend({
  1411 
  1447 
  1412 	routes: {
  1448 	routes: {
  1413 		'themes.php?theme=:slug': 'theme',
  1449 		'themes.php?theme=:slug': 'theme',
  1414 		'themes.php?search=:query': 'search',
  1450 		'themes.php?search=:query': 'search',
  1434 
  1470 
  1435 	navigate: navigateRouter
  1471 	navigate: navigateRouter
  1436 
  1472 
  1437 });
  1473 });
  1438 
  1474 
  1439 // Execute and setup the application
  1475 // Execute and setup the application.
  1440 themes.Run = {
  1476 themes.Run = {
  1441 	init: function() {
  1477 	init: function() {
  1442 		// Initializes the blog's theme library view
  1478 		// Initializes the blog's theme library view.
  1443 		// Create a new collection with data
  1479 		// Create a new collection with data.
  1444 		this.themes = new themes.Collection( themes.data.themes );
  1480 		this.themes = new themes.Collection( themes.data.themes );
  1445 
  1481 
  1446 		// Set up the view
  1482 		// Set up the view.
  1447 		this.view = new themes.view.Appearance({
  1483 		this.view = new themes.view.Appearance({
  1448 			collection: this.themes
  1484 			collection: this.themes
  1449 		});
  1485 		});
  1450 
  1486 
  1451 		this.render();
  1487 		this.render();
  1454 		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
  1490 		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
  1455 	},
  1491 	},
  1456 
  1492 
  1457 	render: function() {
  1493 	render: function() {
  1458 
  1494 
  1459 		// Render results
  1495 		// Render results.
  1460 		this.view.render();
  1496 		this.view.render();
  1461 		this.routes();
  1497 		this.routes();
  1462 
  1498 
  1463 		if ( Backbone.History.started ) {
  1499 		if ( Backbone.History.started ) {
  1464 			Backbone.history.stop();
  1500 			Backbone.history.stop();
  1471 	},
  1507 	},
  1472 
  1508 
  1473 	routes: function() {
  1509 	routes: function() {
  1474 		var self = this;
  1510 		var self = this;
  1475 		// Bind to our global thx object
  1511 		// Bind to our global thx object
  1476 		// so that the object is available to sub-views
  1512 		// so that the object is available to sub-views.
  1477 		themes.router = new themes.Router();
  1513 		themes.router = new themes.Router();
  1478 
  1514 
  1479 		// Handles theme details route event
  1515 		// Handles theme details route event.
  1480 		themes.router.on( 'route:theme', function( slug ) {
  1516 		themes.router.on( 'route:theme', function( slug ) {
  1481 			self.view.view.expand( slug );
  1517 			self.view.view.expand( slug );
  1482 		});
  1518 		});
  1483 
  1519 
  1484 		themes.router.on( 'route:themes', function() {
  1520 		themes.router.on( 'route:themes', function() {
  1485 			self.themes.doSearch( '' );
  1521 			self.themes.doSearch( '' );
  1486 			self.view.trigger( 'theme:close' );
  1522 			self.view.trigger( 'theme:close' );
  1487 		});
  1523 		});
  1488 
  1524 
  1489 		// Handles search route event
  1525 		// Handles search route event.
  1490 		themes.router.on( 'route:search', function() {
  1526 		themes.router.on( 'route:search', function() {
  1491 			$( '.wp-filter-search' ).trigger( 'keyup' );
  1527 			$( '.wp-filter-search' ).trigger( 'keyup' );
  1492 		});
  1528 		});
  1493 
  1529 
  1494 		this.extraRoutes();
  1530 		this.extraRoutes();
  1497 	extraRoutes: function() {
  1533 	extraRoutes: function() {
  1498 		return false;
  1534 		return false;
  1499 	}
  1535 	}
  1500 };
  1536 };
  1501 
  1537 
  1502 // Extend the main Search view
  1538 // Extend the main Search view.
  1503 themes.view.InstallerSearch =  themes.view.Search.extend({
  1539 themes.view.InstallerSearch =  themes.view.Search.extend({
  1504 
  1540 
  1505 	events: {
  1541 	events: {
  1506 		'input': 'search',
  1542 		'input': 'search',
  1507 		'keyup': 'search'
  1543 		'keyup': 'search'
  1508 	},
  1544 	},
  1509 
  1545 
  1510 	terms: '',
  1546 	terms: '',
  1511 
  1547 
  1512 	// Handles Ajax request for searching through themes in public repo
  1548 	// Handles Ajax request for searching through themes in public repo.
  1513 	search: function( event ) {
  1549 	search: function( event ) {
  1514 
  1550 
  1515 		// Tabbing or reverse tabbing into the search input shouldn't trigger a search
  1551 		// Tabbing or reverse tabbing into the search input shouldn't trigger a search.
  1516 		if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
  1552 		if ( event.type === 'keyup' && ( event.which === 9 || event.which === 16 ) ) {
  1517 			return;
  1553 			return;
  1518 		}
  1554 		}
  1519 
  1555 
  1520 		this.collection = this.options.parent.view.collection;
  1556 		this.collection = this.options.parent.view.collection;
  1538 		// Updates terms with the value passed.
  1574 		// Updates terms with the value passed.
  1539 		this.terms = value;
  1575 		this.terms = value;
  1540 
  1576 
  1541 		request.search = value;
  1577 		request.search = value;
  1542 
  1578 
  1543 		// Intercept an [author] search.
  1579 		/*
  1544 		//
  1580 		 * Intercept an [author] search.
  1545 		// If input value starts with `author:` send a request
  1581 		 *
  1546 		// for `author` instead of a regular `search`
  1582 		 * If input value starts with `author:` send a request
       
  1583 		 * for `author` instead of a regular `search`.
       
  1584 		 */
  1547 		if ( value.substring( 0, 7 ) === 'author:' ) {
  1585 		if ( value.substring( 0, 7 ) === 'author:' ) {
  1548 			request.search = '';
  1586 			request.search = '';
  1549 			request.author = value.slice( 7 );
  1587 			request.author = value.slice( 7 );
  1550 		}
  1588 		}
  1551 
  1589 
  1552 		// Intercept a [tag] search.
  1590 		/*
  1553 		//
  1591 		 * Intercept a [tag] search.
  1554 		// If input value starts with `tag:` send a request
  1592 		 *
  1555 		// for `tag` instead of a regular `search`
  1593 		 * If input value starts with `tag:` send a request
       
  1594 		 * for `tag` instead of a regular `search`.
       
  1595 		 */
  1556 		if ( value.substring( 0, 4 ) === 'tag:' ) {
  1596 		if ( value.substring( 0, 4 ) === 'tag:' ) {
  1557 			request.search = '';
  1597 			request.search = '';
  1558 			request.tag = [ value.slice( 4 ) ];
  1598 			request.tag = [ value.slice( 4 ) ];
  1559 		}
  1599 		}
  1560 
  1600 
  1564 
  1604 
  1565 		$( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
  1605 		$( 'body' ).removeClass( 'show-filters filters-applied show-favorites-form' );
  1566 		$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
  1606 		$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
  1567 
  1607 
  1568 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1608 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1569 		// or searching the local cache
  1609 		// or searching the local cache.
  1570 		this.collection.query( request );
  1610 		this.collection.query( request );
  1571 
  1611 
  1572 		// Set route
  1612 		// Set route.
  1573 		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
  1613 		themes.router.navigate( themes.router.baseUrl( themes.router.searchPath + encodeURIComponent( value ) ), { replace: true } );
  1574 	}
  1614 	}
  1575 });
  1615 });
  1576 
  1616 
  1577 themes.view.Installer = themes.view.Appearance.extend({
  1617 themes.view.Installer = themes.view.Appearance.extend({
  1578 
  1618 
  1579 	el: '#wpbody-content .wrap',
  1619 	el: '#wpbody-content .wrap',
  1580 
  1620 
  1581 	// Register events for sorting and filters in theme-navigation
  1621 	// Register events for sorting and filters in theme-navigation.
  1582 	events: {
  1622 	events: {
  1583 		'click .filter-links li > a': 'onSort',
  1623 		'click .filter-links li > a': 'onSort',
  1584 		'click .theme-filter': 'onFilter',
  1624 		'click .theme-filter': 'onFilter',
  1585 		'click .drawer-toggle': 'moreFilters',
  1625 		'click .drawer-toggle': 'moreFilters',
  1586 		'click .filter-drawer .apply-filters': 'applyFilters',
  1626 		'click .filter-drawer .apply-filters': 'applyFilters',
  1589 		'click .edit-filters': 'backToFilters',
  1629 		'click .edit-filters': 'backToFilters',
  1590 		'click .favorites-form-submit' : 'saveUsername',
  1630 		'click .favorites-form-submit' : 'saveUsername',
  1591 		'keyup #wporg-username-input': 'saveUsername'
  1631 		'keyup #wporg-username-input': 'saveUsername'
  1592 	},
  1632 	},
  1593 
  1633 
  1594 	// Initial render method
  1634 	// Initial render method.
  1595 	render: function() {
  1635 	render: function() {
  1596 		var self = this;
  1636 		var self = this;
  1597 
  1637 
  1598 		this.search();
  1638 		this.search();
  1599 		this.uploader();
  1639 		this.uploader();
  1601 		this.collection = new themes.Collection();
  1641 		this.collection = new themes.Collection();
  1602 
  1642 
  1603 		// Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
  1643 		// Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
  1604 		this.listenTo( this, 'theme:end', function() {
  1644 		this.listenTo( this, 'theme:end', function() {
  1605 
  1645 
  1606 			// Make sure we are not already loading
  1646 			// Make sure we are not already loading.
  1607 			if ( self.collection.loadingThemes ) {
  1647 			if ( self.collection.loadingThemes ) {
  1608 				return;
  1648 				return;
  1609 			}
  1649 			}
  1610 
  1650 
  1611 			// Set loadingThemes to true and bump page instance of currentQuery.
  1651 			// Set loadingThemes to true and bump page instance of currentQuery.
  1634 
  1674 
  1635 		if ( this.view ) {
  1675 		if ( this.view ) {
  1636 			this.view.remove();
  1676 			this.view.remove();
  1637 		}
  1677 		}
  1638 
  1678 
  1639 		// Set ups the view and passes the section argument
  1679 		// Sets up the view and passes the section argument.
  1640 		this.view = new themes.view.Themes({
  1680 		this.view = new themes.view.Themes({
  1641 			collection: this.collection,
  1681 			collection: this.collection,
  1642 			parent: this
  1682 			parent: this
  1643 		});
  1683 		});
  1644 
  1684 
  1645 		// Reset pagination every time the install view handler is run
  1685 		// Reset pagination every time the install view handler is run.
  1646 		this.page = 0;
  1686 		this.page = 0;
  1647 
  1687 
  1648 		// Render and append
  1688 		// Render and append.
  1649 		this.$el.find( '.themes' ).remove();
  1689 		this.$el.find( '.themes' ).remove();
  1650 		this.view.render();
  1690 		this.view.render();
  1651 		this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
  1691 		this.$el.find( '.theme-browser' ).append( this.view.el ).addClass( 'rendered' );
  1652 	},
  1692 	},
  1653 
  1693 
  1654 	// Handles all the rendering of the public theme directory
  1694 	// Handles all the rendering of the public theme directory.
  1655 	browse: function( section ) {
  1695 	browse: function( section ) {
  1656 		// Create a new collection with the proper theme data
  1696 		// Create a new collection with the proper theme data
  1657 		// for each section
  1697 		// for each section.
  1658 		this.collection.query( { browse: section } );
  1698 		this.collection.query( { browse: section } );
  1659 	},
  1699 	},
  1660 
  1700 
  1661 	// Sorting navigation
  1701 	// Sorting navigation.
  1662 	onSort: function( event ) {
  1702 	onSort: function( event ) {
  1663 		var $el = $( event.target ),
  1703 		var $el = $( event.target ),
  1664 			sort = $el.data( 'sort' );
  1704 			sort = $el.data( 'sort' );
  1665 
  1705 
  1666 		event.preventDefault();
  1706 		event.preventDefault();
  1667 
  1707 
  1668 		$( 'body' ).removeClass( 'filters-applied show-filters' );
  1708 		$( 'body' ).removeClass( 'filters-applied show-filters' );
  1669 		$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
  1709 		$( '.drawer-toggle' ).attr( 'aria-expanded', 'false' );
  1670 
  1710 
  1671 		// Bail if this is already active
  1711 		// Bail if this is already active.
  1672 		if ( $el.hasClass( this.activeClass ) ) {
  1712 		if ( $el.hasClass( this.activeClass ) ) {
  1673 			return;
  1713 			return;
  1674 		}
  1714 		}
  1675 
  1715 
  1676 		this.sort( sort );
  1716 		this.sort( sort );
  1677 
  1717 
  1678 		// Trigger a router.naviagte update
  1718 		// Trigger a router.navigate update.
  1679 		themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
  1719 		themes.router.navigate( themes.router.baseUrl( themes.router.browsePath + sort ) );
  1680 	},
  1720 	},
  1681 
  1721 
  1682 	sort: function( sort ) {
  1722 	sort: function( sort ) {
  1683 		this.clearSearch();
  1723 		this.clearSearch();
  1700 		}
  1740 		}
  1701 
  1741 
  1702 		this.browse( sort );
  1742 		this.browse( sort );
  1703 	},
  1743 	},
  1704 
  1744 
  1705 	// Filters and Tags
  1745 	// Filters and Tags.
  1706 	onFilter: function( event ) {
  1746 	onFilter: function( event ) {
  1707 		var request,
  1747 		var request,
  1708 			$el = $( event.target ),
  1748 			$el = $( event.target ),
  1709 			filter = $el.data( 'filter' );
  1749 			filter = $el.data( 'filter' );
  1710 
  1750 
  1711 		// Bail if this is already active
  1751 		// Bail if this is already active.
  1712 		if ( $el.hasClass( this.activeClass ) ) {
  1752 		if ( $el.hasClass( this.activeClass ) ) {
  1713 			return;
  1753 			return;
  1714 		}
  1754 		}
  1715 
  1755 
  1716 		$( '.filter-links li > a, .theme-section' )
  1756 		$( '.filter-links li > a, .theme-section' )
  1723 		if ( ! filter ) {
  1763 		if ( ! filter ) {
  1724 			return;
  1764 			return;
  1725 		}
  1765 		}
  1726 
  1766 
  1727 		// Construct the filter request
  1767 		// Construct the filter request
  1728 		// using the default values
  1768 		// using the default values.
  1729 		filter = _.union( [ filter, this.filtersChecked() ] );
  1769 		filter = _.union( [ filter, this.filtersChecked() ] );
  1730 		request = { tag: [ filter ] };
  1770 		request = { tag: [ filter ] };
  1731 
  1771 
  1732 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1772 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1733 		// or searching the local cache
  1773 		// or searching the local cache.
  1734 		this.collection.query( request );
  1774 		this.collection.query( request );
  1735 	},
  1775 	},
  1736 
  1776 
  1737 	// Clicking on a checkbox to add another filter to the request
  1777 	// Clicking on a checkbox to add another filter to the request.
  1738 	addFilter: function() {
  1778 	addFilter: function() {
  1739 		this.filtersChecked();
  1779 		this.filtersChecked();
  1740 	},
  1780 	},
  1741 
  1781 
  1742 	// Applying filters triggers a tag request
  1782 	// Applying filters triggers a tag request.
  1743 	applyFilters: function( event ) {
  1783 	applyFilters: function( event ) {
  1744 		var name,
  1784 		var name,
  1745 			tags = this.filtersChecked(),
  1785 			tags = this.filtersChecked(),
  1746 			request = { tag: tags },
  1786 			request = { tag: tags },
  1747 			filteringBy = $( '.filtered-by .tags' );
  1787 			filteringBy = $( '.filtered-by .tags' );
  1766 			name = $( 'label[for="filter-id-' + tag + '"]' ).text();
  1806 			name = $( 'label[for="filter-id-' + tag + '"]' ).text();
  1767 			filteringBy.append( '<span class="tag">' + name + '</span>' );
  1807 			filteringBy.append( '<span class="tag">' + name + '</span>' );
  1768 		});
  1808 		});
  1769 
  1809 
  1770 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1810 		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1771 		// or searching the local cache
  1811 		// or searching the local cache.
  1772 		this.collection.query( request );
  1812 		this.collection.query( request );
  1773 	},
  1813 	},
  1774 
  1814 
  1775 	// Save the user's WordPress.org username and get his favorite themes.
  1815 	// Save the user's WordPress.org username and get his favorite themes.
  1776 	saveUsername: function ( event ) {
  1816 	saveUsername: function ( event ) {
  1781 
  1821 
  1782 		if ( event ) {
  1822 		if ( event ) {
  1783 			event.preventDefault();
  1823 			event.preventDefault();
  1784 		}
  1824 		}
  1785 
  1825 
  1786 		// save username on enter
  1826 		// Save username on enter.
  1787 		if ( event.type === 'keyup' && event.which !== 13 ) {
  1827 		if ( event.type === 'keyup' && event.which !== 13 ) {
  1788 			return;
  1828 			return;
  1789 		}
  1829 		}
  1790 
  1830 
  1791 		return wp.ajax.send( 'save-wporg-username', {
  1831 		return wp.ajax.send( 'save-wporg-username', {
  1793 				_wpnonce: nonce,
  1833 				_wpnonce: nonce,
  1794 				username: username
  1834 				username: username
  1795 			},
  1835 			},
  1796 			success: function () {
  1836 			success: function () {
  1797 				// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1837 				// Get the themes by sending Ajax POST request to api.wordpress.org/themes
  1798 				// or searching the local cache
  1838 				// or searching the local cache.
  1799 				that.collection.query( request );
  1839 				that.collection.query( request );
  1800 			}
  1840 			}
  1801 		} );
  1841 		} );
  1802 	},
  1842 	},
  1803 
  1843 
  1804 	// Get the checked filters
  1844 	/**
  1805 	// @return {array} of tags or false
  1845 	 * Get the checked filters.
       
  1846 	 *
       
  1847 	 * @return {Array} of tags or false
       
  1848 	 */
  1806 	filtersChecked: function() {
  1849 	filtersChecked: function() {
  1807 		var items = $( '.filter-group' ).find( ':checkbox' ),
  1850 		var items = $( '.filter-group' ).find( ':checkbox' ),
  1808 			tags = [];
  1851 			tags = [];
  1809 
  1852 
  1810 		_.each( items.filter( ':checked' ), function( item ) {
  1853 		_.each( items.filter( ':checked' ), function( item ) {
  1811 			tags.push( $( item ).prop( 'value' ) );
  1854 			tags.push( $( item ).prop( 'value' ) );
  1812 		});
  1855 		});
  1813 
  1856 
  1814 		// When no filters are checked, restore initial state and return
  1857 		// When no filters are checked, restore initial state and return.
  1815 		if ( tags.length === 0 ) {
  1858 		if ( tags.length === 0 ) {
  1816 			$( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
  1859 			$( '.filter-drawer .apply-filters' ).find( 'span' ).text( '' );
  1817 			$( '.filter-drawer .clear-filters' ).hide();
  1860 			$( '.filter-drawer .clear-filters' ).hide();
  1818 			$( 'body' ).removeClass( 'filters-applied' );
  1861 			$( 'body' ).removeClass( 'filters-applied' );
  1819 			return false;
  1862 			return false;
  1825 		return tags;
  1868 		return tags;
  1826 	},
  1869 	},
  1827 
  1870 
  1828 	activeClass: 'current',
  1871 	activeClass: 'current',
  1829 
  1872 
  1830 	/*
  1873 	/**
  1831 	 * When users press the "Upload Theme" button, show the upload form in place.
  1874 	 * When users press the "Upload Theme" button, show the upload form in place.
  1832 	 */
  1875 	 */
  1833 	uploader: function() {
  1876 	uploader: function() {
  1834 		var uploadViewToggle = $( '.upload-view-toggle' ),
  1877 		var uploadViewToggle = $( '.upload-view-toggle' ),
  1835 			$body = $( document.body );
  1878 			$body = $( document.body );
  1840 			// Toggle the `aria-expanded` button attribute.
  1883 			// Toggle the `aria-expanded` button attribute.
  1841 			uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
  1884 			uploadViewToggle.attr( 'aria-expanded', $body.hasClass( 'show-upload-view' ) );
  1842 		});
  1885 		});
  1843 	},
  1886 	},
  1844 
  1887 
  1845 	// Toggle the full filters navigation
  1888 	// Toggle the full filters navigation.
  1846 	moreFilters: function( event ) {
  1889 	moreFilters: function( event ) {
  1847 		var $body = $( 'body' ),
  1890 		var $body = $( 'body' ),
  1848 			$toggleButton = $( '.drawer-toggle' );
  1891 			$toggleButton = $( '.drawer-toggle' );
  1849 
  1892 
  1850 		event.preventDefault();
  1893 		event.preventDefault();
  1860 		$body.toggleClass( 'show-filters' );
  1903 		$body.toggleClass( 'show-filters' );
  1861 		// Toggle the `aria-expanded` button attribute.
  1904 		// Toggle the `aria-expanded` button attribute.
  1862 		$toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
  1905 		$toggleButton.attr( 'aria-expanded', $body.hasClass( 'show-filters' ) );
  1863 	},
  1906 	},
  1864 
  1907 
  1865 	// Clears all the checked filters
  1908 	/**
  1866 	// @uses filtersChecked()
  1909 	 * Clears all the checked filters.
       
  1910 	 *
       
  1911 	 * @uses filtersChecked()
       
  1912 	 */
  1867 	clearFilters: function( event ) {
  1913 	clearFilters: function( event ) {
  1868 		var items = $( '.filter-group' ).find( ':checkbox' ),
  1914 		var items = $( '.filter-group' ).find( ':checkbox' ),
  1869 			self = this;
  1915 			self = this;
  1870 
  1916 
  1871 		event.preventDefault();
  1917 		event.preventDefault();
  1914 
  1960 
  1915 
  1961 
  1916 themes.RunInstaller = {
  1962 themes.RunInstaller = {
  1917 
  1963 
  1918 	init: function() {
  1964 	init: function() {
  1919 		// Set up the view
  1965 		// Set up the view.
  1920 		// Passes the default 'section' as an option
  1966 		// Passes the default 'section' as an option.
  1921 		this.view = new themes.view.Installer({
  1967 		this.view = new themes.view.Installer({
  1922 			section: 'featured',
  1968 			section: 'featured',
  1923 			SearchView: themes.view.InstallerSearch
  1969 			SearchView: themes.view.InstallerSearch
  1924 		});
  1970 		});
  1925 
  1971 
  1926 		// Render results
  1972 		// Render results.
  1927 		this.render();
  1973 		this.render();
  1928 
  1974 
  1929 		// Start debouncing user searches after Backbone.history.start().
  1975 		// Start debouncing user searches after Backbone.history.start().
  1930 		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
  1976 		this.view.SearchView.doSearch = _.debounce( this.view.SearchView.doSearch, 500 );
  1931 	},
  1977 	},
  1932 
  1978 
  1933 	render: function() {
  1979 	render: function() {
  1934 
  1980 
  1935 		// Render results
  1981 		// Render results.
  1936 		this.view.render();
  1982 		this.view.render();
  1937 		this.routes();
  1983 		this.routes();
  1938 
  1984 
  1939 		if ( Backbone.History.started ) {
  1985 		if ( Backbone.History.started ) {
  1940 			Backbone.history.stop();
  1986 			Backbone.history.stop();
  1949 	routes: function() {
  1995 	routes: function() {
  1950 		var self = this,
  1996 		var self = this,
  1951 			request = {};
  1997 			request = {};
  1952 
  1998 
  1953 		// Bind to our global `wp.themes` object
  1999 		// Bind to our global `wp.themes` object
  1954 		// so that the router is available to sub-views
  2000 		// so that the router is available to sub-views.
  1955 		themes.router = new themes.InstallerRouter();
  2001 		themes.router = new themes.InstallerRouter();
  1956 
  2002 
  1957 		// Handles `theme` route event
  2003 		// Handles `theme` route event.
  1958 		// Queries the API for the passed theme slug
  2004 		// Queries the API for the passed theme slug.
  1959 		themes.router.on( 'route:preview', function( slug ) {
  2005 		themes.router.on( 'route:preview', function( slug ) {
  1960 
  2006 
  1961 			// Remove existing handlers.
  2007 			// Remove existing handlers.
  1962 			if ( themes.preview ) {
  2008 			if ( themes.preview ) {
  1963 				themes.preview.undelegateEvents();
  2009 				themes.preview.undelegateEvents();
  1981 				});
  2027 				});
  1982 
  2028 
  1983 			}
  2029 			}
  1984 		});
  2030 		});
  1985 
  2031 
  1986 		// Handles sorting / browsing routes
  2032 		/*
  1987 		// Also handles the root URL triggering a sort request
  2033 		 * Handles sorting / browsing routes.
  1988 		// for `featured`, the default view
  2034 		 * Also handles the root URL triggering a sort request
       
  2035 		 * for `featured`, the default view.
       
  2036 		 */
  1989 		themes.router.on( 'route:sort', function( sort ) {
  2037 		themes.router.on( 'route:sort', function( sort ) {
  1990 			if ( ! sort ) {
  2038 			if ( ! sort ) {
  1991 				sort = 'featured';
  2039 				sort = 'featured';
  1992 				themes.router.navigate( themes.router.baseUrl( '?browse=featured' ), { replace: true } );
  2040 				themes.router.navigate( themes.router.baseUrl( '?browse=featured' ), { replace: true } );
  1993 			}
  2041 			}
  2038 	});
  2086 	});
  2039 });
  2087 });
  2040 
  2088 
  2041 })( jQuery );
  2089 })( jQuery );
  2042 
  2090 
  2043 // Align theme browser thickbox
  2091 // Align theme browser thickbox.
  2044 jQuery(document).ready( function($) {
  2092 jQuery(document).ready( function($) {
  2045 	window.tb_position = function() {
  2093 	window.tb_position = function() {
  2046 		var tbWindow = $('#TB_window'),
  2094 		var tbWindow = $('#TB_window'),
  2047 			width = $(window).width(),
  2095 			width = $(window).width(),
  2048 			H = $(window).height(),
  2096 			H = $(window).height(),